npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@30k/starcomm-client

v0.6.0

Published

TypeScript API client for Star Comms shard Owner API

Readme

@30k/starcomm-client

TypeScript client for the Star Comms shard Owner API. Zero dependencies, uses native fetch (Node 18+, Bun, Deno, browsers).

Install

pnpm add @30k/starcomm-client

Usage

import { StarCommsClient } from "@30k/starcomm-client";

const client = new StarCommsClient({
  baseUrl: "http://your-shard:25588",
  ownerApiKey: "scok_your_key_here",
});

const status = await client.status.get();
await client.assignments.assign("discord_user_id", 1);
await client.comms.sendAcars("Fleet departing!");

Docs

Full API documentation: 30k-inc.github.io/starcomm-client

Config

| Option | Type | Default | Description | |--------|------|---------|-------------| | baseUrl | string | — | Shard URL (no trailing slash) | | ownerApiKey | string | — | Owner key (scok_...) | | serviceKey | string? | — | Service key (scsk_...) for audio listening | | shardToken | string? | — | Shard token for debug endpoint | | timeoutMs | number? | 10000 | Request timeout | | connectTimeoutMs | number? | 30000 | Connection timeout for streaming endpoints | | fetch | typeof fetch? | globalThis.fetch | Custom fetch impl |

Audio Listener

Listen-only voice connection that receives Opus audio frames and PTT events from shard nets. Requires a service key (scsk_...) obtained from the shard API key management.

const client = new StarCommsClient({
  baseUrl: "http://your-shard:25588",
  ownerApiKey: "scok_...",
  serviceKey: "scsk_...",
});

// Event handlers
client.audio.on("ptt.start", (evt) => {
  console.log(`${evt.displayName} keyed up on net ${evt.netId}`);
});

client.audio.on("ptt.stop", (evt) => {
  console.log(`${evt.displayName} stopped on net ${evt.netId}`);
});

client.audio.on("audio", (frame) => {
  // frame.netId — which net
  // frame.userId — who's transmitting
  // frame.opus  — raw Opus data (Uint8Array)
});

// Lifecycle events
client.audio.on("connected", ({ guildId }) => console.log("Connected to", guildId));
client.audio.on("disconnected", ({ reason }) => console.warn("Lost:", reason));
client.audio.on("reconnected", ({ attempt }) => console.log("Back after", attempt, "tries"));
client.audio.on("error", ({ reason }) => console.error(reason));

// Connect and listen on specific nets
await client.audio.listen({
  guildId: "your_guild_id",
  nets: [1, 2, 3],
});

// Dynamic net management (no reconnect needed)
await client.audio.addNet(4);
await client.audio.removeNet(1);
client.audio.nets; // [2, 3, 4]

// Stop and cleanup
await client.audio.stop();

Audio Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | guildId | string | — | Discord guild ID | | nets | number[]? | [] | Initial nets to listen on | | displayName | string? | "" | Name shown in shard roster | | autoReconnect | boolean? | true | Reconnect on connection loss | | initialDelayMs | number? | 2000 | First reconnect delay | | maxDelayMs | number? | 30000 | Max reconnect delay | | maxAttempts | number? | Infinity | Max reconnect attempts |

Event Stream & Reconnection

The client includes a built-in SSE event stream with automatic reconnection:

// Listen for events
client.on("user.joined", (event) => {
  console.log(event.data.displayName, "connected");
});

client.on("ready-check.started", (event) => {
  console.log(`Ready check initiated by ${event.data.initiatorName}, ${event.data.participantCount} participants`);
});

client.on("ready-check.completed", (event) => {
  const { summary } = event.data;
  console.log(`Ready check done: ${summary.ready}/${summary.total} ready`);
});

// Lifecycle hooks
client.onLifecycle("stream.disconnected", ({ reason }) => {
  console.warn("Stream lost:", reason);
});
client.onLifecycle("stream.reconnected", ({ attempt }) => {
  console.log("Back online after", attempt, "attempts");
});

// Connect with reconnection options (all optional)
await client.connect({
  autoReconnect: true,   // default: true
  initialDelayMs: 1000,  // default: 1000
  maxDelayMs: 30000,     // default: 30000
  maxAttempts: Infinity,  // default: Infinity
});

// Intentional disconnect (won't trigger reconnection)
client.disconnect();

Ready Checks

20-second attendance polls targeting connected operators. Templates define reusable configs; sessions are live instances.

// List templates
const { readyChecks } = await client.readyChecks.list();

// Create a template
const { readyCheck } = await client.readyChecks.upsert({
  message: "Fleet departing in 2 minutes — confirm ready",
  color: "#34CD84",
  target: { everyone: true },
});

// Start a session
const { session, summary } = await client.readyChecks.start(readyCheck.id, "Fleet Commander");
console.log(`${summary.total} participants, expires at ${session.expiresAt}`);

// Poll session status
const status = await client.readyChecks.getSession(session.id);
console.log(`${status.summary.ready}/${status.summary.total} ready`);

// Remove a template
await client.readyChecks.remove(readyCheck.id);

ACARS Alerts

Broadcast short text alerts to all connected operators:

await client.comms.sendAcars("Fleet departing in 60 seconds", {
  senderName: "Fleet Commander",
  durationMs: 8500,
  alertType: "emergency", // "critical" | "emergency" | "non-emergency"
});

Error Handling

All methods throw StarCommsError on failure:

import { StarCommsError } from "@30k/starcomm-client";

try {
  await client.assignments.assign("bad_id", 99);
} catch (err) {
  if (err instanceof StarCommsError) {
    console.error(err.statusCode, err.message);
  }
}

SSE Event Types

| Event | Data Fields | |-------|-------------| | user.joined | userId, displayName, transport, nets | | user.left | userId, displayName, transport | | ptt.start | userId, displayName, netId | | ptt.stop | userId, displayName, netId, reason? | | operation.opened | open | | operation.closed | open | | assignments.changed | source?, action?, userId?, netId?, keyId? | | config.changed | keyId?, action?, uid?, preset? | | client.disconnected | userId, keyId, disconnected | | acars.sent | id, text, durationMs, alertType, senderId, senderName, source, routed | | ready-check.configured | templateId, keyId? | | ready-check.removed | templateId, keyId? | | ready-check.started | sessionId, templateId, initiatorId, initiatorName, participantCount, expiresAt | | ready-check.response | sessionId, userId, name, status, respondedAt | | ready-check.completed | sessionId, templateId, summary |

Local Client (Desktop Companion)

A standalone WebSocket client for communicating with the StarComm desktop application's Mobile Remote interface. Completely independent from the shard Owner API client — no API keys required, just the pairing token from StarComm's settings.

Setup

In StarComm desktop: Settings → Mobile Remote → Enable APK companion link. Note the LAN control port and pairing token.

Usage

import { StarCommsLocalClient } from "@30k/starcomm-client";

const local = new StarCommsLocalClient({
  token: "your_pairing_token",  // from StarComm settings
  // host: "127.0.0.1",         // default; use LAN IP for remote access
  // port: 8798,                // default; matches StarComm's "LAN control port"
  debug: true,                  // enable console logging (optional)
});

// Listen for state updates
local.on("snapshot", (snapshot) => {
  console.log(`Connected as ${snapshot.displayName}`);
  console.log(`Guild: ${snapshot.guildName}`);
  console.log(`Channels: ${snapshot.channels.length}`);
});

local.on("error", (err) => {
  console.error("StarComm error:", err.message);
});

// Lifecycle events
local.onLifecycle("connected", () => console.log("Connected"));
local.onLifecycle("disconnected", ({ reason }) => console.warn("Disconnected:", reason));
local.onLifecycle("reconnecting", ({ attempt }) => console.log("Reconnecting...", attempt));

// Connect (returns a Promise — resolves when socket is open)
await local.connect();

// Access the latest snapshot at any time
console.log(local.snapshot?.channels);

PTT (Push-to-Talk)

Three modes available:

// ptt.start — momentary (like holding a physical PTT button)
// Releases automatically if no keepalive is sent.
local.pttStart(netId);

// ptt.hold — toggle/latch mode (tap to lock on, tap again or stop to release)
// Use this for programmatic PTT that should stay active.
local.pttHold(netId);

// ptt.stop — release PTT on a net
local.pttStop(netId);

For sustained transmission, use pttHold() + pttStop():

local.pttHold(255);      // latch transmit on net 255
// ... do things ...
local.pttStop(255);      // release

Admin Commands

Only work when the connected user has admin access (snapshot.access.isAdmin === true):

// Assign/unassign a user to a net
local.adminAssign("userId", netId);
local.adminUnassign("userId", netId);

// Disconnect a user
local.adminDisconnect("userId");

// Broadcast an ACARS alert
local.adminAcars("Fleet departing in 60 seconds");

Config

| Option | Type | Default | Description | |--------|------|---------|-------------| | token | string | — | Pairing token (required) | | host | string? | "127.0.0.1" | StarComm host IP | | port | number? | 8798 | LAN control port | | autoReconnect | boolean? | true | Auto-reconnect on disconnect | | initialDelayMs | number? | 1000 | First reconnect delay | | maxDelayMs | number? | 15000 | Max reconnect delay | | maxAttempts | number? | Infinity | Max reconnect attempts | | debug | boolean? | false | Log all messages to console |

Snapshot Structure

The snapshot is sent immediately on connection and again whenever state changes:

interface LocalSnapshot {
  type: "snapshot";
  protocol: string;           // "starcomms-client-companion"
  protocolVersion: number;    // 1
  app: string;                // "star-comms-client"
  version: string;            // e.g. "1.50.3"
  connected: boolean;
  pairedDevices: number;
  userId: string;             // Discord user ID
  displayName: string;
  guildId: string;
  guildName: string;
  operationOpen: boolean;
  socketStatus: string;       // "connected" | "disconnected" | ...
  activeNetId: number;        // -1 = none
  activeGuildId: string;
  relay: { guildId, guildName, voiceEndpoint, udpVoiceEndpoint, shardName, shardActive, operationOpen };
  access: { isAdmin, isFullAdmin, canCustomizeTheme };
  ptt: { active, netId, channel, color };
  overlay: { transmitting, transmitChannel, transmitColor, tag, language, labels, receiving[] };
  receiving: [];              // users currently transmitting
  channels: Channel[];        // all visible nets with members, volume, mute state
  admin?: { canAssign, canManageNets, canSendAcars, acarsEnabled, acarsDurationMs, streamerMode, users[], channels[] };
}

Health Check

Before connecting via WebSocket, you can verify reachability with a plain HTTP call:

GET http://<host>:<port>/health
→ {"ok":true,"app":"star-comms-local-control","port":8798,"clients":1}

License

MIT