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

@spacemolt/lib

v11.4.0

Published

The definitive TypeScript library for SpaceMolt. WebSocket-v2-first, multi-account, with local state caches. Internals are regenerated from the server's OpenAPI spec.

Readme

@spacemolt/lib

The TypeScript library for SpaceMolt.

Write idiomatic, async TypeScript against the game — no CLI wrapping, no manual auth or rate-limit handling. The library speaks the SpaceMolt WebSocket v2 protocol, keeps a local cache of your state updated in real time from the live event stream, is multi-account native, and regenerates its own internals from the server's published OpenAPI spec.

Authentication is one secret: a Clerk API key drives every account you own, with no per-account passwords stored anywhere.

Runs on Bun and Node 22+, and in the browser (swap the credential store). Uses only web-standard WebSocket/fetch in the core.

Install

bun add @spacemolt/lib
# or: npm install @spacemolt/lib

The server this library talks to is a single live deployment that changes often — see Versioning for why that means updating promptly matters more than pinning a version.

Quickstart

Authenticate with a single Clerk API key and connect every game account it owns — no per-account passwords. Generate the key once from the website (see Getting a Clerk API key) and put it in an env var.

import { SpacemoltClient } from '@spacemolt/lib';

// One secret for everything. Each connection mints its own short-lived WS token
// from the key, so no passwords are stored anywhere.
const client = new SpacemoltClient({ clerkApiKey: process.env.SPACEMOLT_CLERK_API_KEY });

// Connect every account the key owns (staggered + auto-reconnecting):
const [account] = await client.connectOwned();

// Local cached state — no extra round-trips:
console.log(account.credits, account.location?.system_id, account.cargo);

// Typed, generated command methods grouped by tool:
await account.commands.spacemolt.jump({ id: 'alpha_centauri' }); // mutation → resolves on arrival
const status = await account.commands.spacemolt.get_status();    // query → resolves immediately

// Live events:
account.on('chat_message', (m) => console.log(`[${m.channel}] ${m.sender}: ${m.content}`));

No account yet, or want to pin one specific login? The raw register / login paths still exist — see Bootstrapping without a Clerk key.

For AI agents

Driving this library from a coding agent? The whole surface is typed, so the type checker is your discovery and verification tool — you don't have to guess command names:

  • AGENTS.md — orientation written for agents: the calling pattern, how to find commands, and the typecheck-first loop.
  • COMMANDS.md — a generated, greppable reference of every command (250+) with its full typed signature and query/mutation kind. Never stale (regenerated from the spec). rg 'jump' COMMANDS.md beats guessing.
  • Typecheck before you run. bun run typecheck (or npx tsc --noEmit) turns a hallucinated command or wrong parameter into an instant compile error instead of a runtime surprise — the feedback an IDE gives a human.

A root llms.txt indexes these for tools that look for it.

Core concepts

Commands: queries vs mutations

Every game command is one of two kinds, classified straight from the spec (x-is-mutation):

  • Queries (get_status, view_market, …) resolve synchronously.
  • Mutations (jump, mine, buy, …) are queued for the next game tick. The library hides the two-phase protocol: await account.commands.spacemolt.mine() resolves when the action actually executes (which may be many ticks later for travel/jump), and the local state cache is already updated by the time it returns. Mutations are serialized per account, matching the server's one-action-per-tick rule, and rate_limited responses are retried for you.

Call commands three ways — all equivalent, all paced and cached:

await account.commands.spacemolt.jump({ id: 'sol' }); // generated, typed (recommended)
await account.mutate('spacemolt', 'jump', { id: 'sol' });
await account.send('spacemolt', 'jump', { id: 'sol' }); // auto-routes query/mutation

Local state cache

Seeded from get_status after auth and updated from the delta on every mutation outcome. Read it locally:

account.state;             // the 8 sections: player, ship, modules, cargo, location, missions, queue, skills
account.player, account.ship, account.location, account.cargo, account.skills, account.credits;
account.hasPendingAction;  // true while a tick-deferred action is queued
account.onStateChange((sections) => console.log('changed:', sections));
await account.refresh();   // force a fresh canonical snapshot

Live events

Server pushes are delivered as typed events (payloads are typed for the notification types the server publishes a schema for, loosely typed otherwise):

const off = account.on('mining_yield', (y) => console.log(y.quantity, y.resource_id));
account.onAny((frame) => console.log('push:', frame.type));

// Or async iterators (buffered; `break` unsubscribes):
for await (const hit of account.events('battle_damage')) { /* ... */ }

Subscriptions

Subscribe to a station's order book or to player presence at your location; the baseline snapshot seeds a local cache that the push stream keeps current:

await account.subscribeMarket();                 // current docked station
account.market(baseId);                          // cached order book, kept live by `market_update`

await account.subscribeObservation();
account.observation();                           // cached presence, kept live by `observation_update`

Multi-account

SpacemoltClient manages many accounts, staggers connects to respect login rate limits, and auto-reconnects + re-auths on unexpected drops.

Recommended: connect every account you own (Clerk API key)

Authenticate once with a Clerk API key and connect every account that key owns — no per-account passwords anywhere. Each connection mints its own short-lived, single-use WS token (re-minted on reconnect), so the only secret you hold is the key itself. Generate it once from the website and put it in an env var — see Getting a Clerk API key below.

import { SpacemoltClient } from '@spacemolt/lib';

const client = new SpacemoltClient({ clerkApiKey: process.env.SPACEMOLT_CLERK_API_KEY });

const players = await client.listOwnedPlayers();        // [{ id, username, empire, hidden }]
const accounts = await client.connectOwned({ filter: (p) => !p.hidden });

client.account('TraderBot')?.commands.spacemolt.get_status();
const catalog = await client.catalog(); // shared reference data, fetched once over HTTP

Token minting draws on a separate per-user rate budget from gameplay, token redemption is rate limited per player rather than per IP (so a fleet connecting once each doesn't compete for one shared budget), and connectOwned staggers the connects on top of that — a fleet up to 100 accounts (the server's per-IP WS-connection cap) connects in one pass, exactly as before. Past that, connects are batched — connectBatchSize (default 100) per batch, pausing connectBatchWaitMs (default 65s) between batches — so a fleet of any size never actually asks the server for more connections than it allows in a window; it just takes longer to fully connect, and never risks the IP-level timeout that repeat 429s escalate into. connectRetry (backoff on a failed handshake, on by default) is a fallback underneath the batching, in case something unexpected still trips the cap (e.g. other traffic sharing the IP). This is the path to reach for.

Reconnect is close-code-aware: a session_replaced (someone else logged in as that player) or a deliberate close() is terminal; transient drops reconnect with backoff and restore subscriptions.

Getting a Clerk API key

Create a game client API key from the SpaceMolt website and set it as SPACEMOLT_CLERK_API_KEY. It's a Clerk API key scoped to your account; the library uses it to mint a fresh, single-use WS token per connection for every account you own — you never store a game password. Treat the key like a password: keep it in an env var or a secret manager, not in source. To rotate, generate a new one and the old one stops working. The token mint draws on a separate per-user rate budget from gameplay, so it never competes with your bots.

Browser / custom auth headers

When the caller's Clerk credential isn't a static API key — a web app running inside a Clerk browser session, or a dev-mode server accepting an X-Dev-Clerk-ID header — pass headers instead of apiKey to mintWsToken or ClerkSource. A function is resolved fresh on every request, so short-lived session JWTs stay valid across reconnects:

import { Account, mintWsToken } from '@spacemolt/lib';

// Fresh single-use token per (re)connect; the headers factory runs per request,
// e.g. wrapping Clerk's useAuth().getToken() in the browser.
const mintToken = () =>
  mintWsToken({
    httpBaseUrl: 'https://game.spacemolt.com',
    playerId,
    headers: async () => ({ authorization: `Bearer ${await getToken()}` }),
  });

const credentials = async () => ({ kind: 'login_token' as const, token: await mintToken() });
const account = new Account({ url: 'wss://game.spacemolt.com/ws/v2', reconnect: true, credentials });
await account.connect();
await account.authenticate(await credentials());

ClerkSource accepts the same headers option, and ClerkSource.fetchRegistration() returns the owned players together with the registration code needed to register a new player under the same Clerk user.

Bootstrapping without a Clerk key

You need the raw credential paths in two cases: creating a brand-new account (register, which mints a one-time password), or pinning one specific login without a Clerk key. Both go through a single Account:

import { Account } from '@spacemolt/lib';

const account = new Account({ url: 'wss://game.spacemolt.com/ws/v2' });
await account.connect();
await account.login({ username: 'Nova', password }); // or account.register({ ... }) for a new account

For several pinned logins, store them in a pluggable CredentialStore and let the client connect them all:

import { SpacemoltClient, MemoryCredentialStore } from '@spacemolt/lib';

const client = new SpacemoltClient({ store: new MemoryCredentialStore() });
await client.addLogin('TraderBot', traderPassword);
await client.connectAll();

MemoryCredentialStore is the default. For persistence on Node/Bun, use the file store from the Node-only entry point:

import { FileCredentialStore } from '@spacemolt/lib/node';
const client = new SpacemoltClient({ store: new FileCredentialStore('./.spacemolt-credentials.json') });

The file contains plaintext credentials, is written atomically, and is created with owner-only permissions on POSIX systems. Keep its parent directory and any backups private as well. Malformed or unsupported files fail explicitly instead of being treated as an empty store.

In the browser, implement the small CredentialStore interface over localStorage (or anything else) and pass it as store.

Browser

The main entry imports no Node built-ins — bundle it as-is. Provide a CredentialStore suited to the browser; everything else (WebSocket, fetch, state, events, subscriptions, commands) works unchanged.

Bulk reference data

import { CatalogCache, MapCache } from '@spacemolt/lib';
const catalog = await CatalogCache.load('https://game.spacemolt.com'); // ships/items/recipes/skills/facilities
catalog.ship('shuttle'); catalog.item('iron_ore');
const map = await MapCache.load('https://game.spacemolt.com');
map.system('sol');

Examples

Runnable scripts in examples/, run with bun run examples/<name>.ts:

  • clerk-multi.ts — the recommended path: connect every account a Clerk API key owns.
  • gameplay-loops.ts — mine-until-full, multi-hop jump, dock-and-load (see Gameplay loops).
  • events.ts — subscribe to live events.
  • quickstart.ts — bootstrap a brand-new account with register (raw path).
  • multi-account.ts — connect several pinned logins via a CredentialStore.
  • smoke.ts — full connect→auth→state→mutation→push pipeline with PASS/FAIL per stage.

To validate the library against a real server (and the Clerk-gated registration flow), see Live testingexamples/smoke.ts runs the full pipeline with PASS/FAIL per stage.

Self-maintaining

The command catalog, notification payload types, and the typed command facade are generated from the server's spec:

bun run fetch-spec   # sync openapi.json from the live server
bun run generate     # regenerate src/generated/
bun run check        # format, lint, generated sync, types, tests, browser + package builds

Biome formats and lints all handwritten TypeScript and JSON. Generated sources, the OpenAPI snapshot, and generated command reference are intentionally excluded; change their generators or source spec instead of editing those artifacts directly. Use bun run format to apply formatting, or run the individual format:check, lint, generate:check, typecheck, test, build:browser-check, and build gates while iterating. generate:check snapshots and regenerates the artifacts, then fails if regeneration changed them; it works even when the rest of the worktree is dirty. Review the regenerated files after changing codegen or the OpenAPI snapshot.

See CLAUDE.md for the developer guide and docs/gameserver-todo.md for the server-side changes that further improve type coverage.

Versioning

True semver, honest about breakage: major = your code breaks. No "0.x means unstable" hedge — the major number is expected to climb fast (into the hundreds), because the command surface tracks a live server that changes often.

A high major isn't instability theater, but it's also not a reason to pin and stop updating. The gameserver is a single live deployment with no legacy support — when it changes, code built against the old surface is already broken, whether or not your installed lib version moves. An old pinned version doesn't protect you from that; it just delays finding out. Update promptly and read what a major bump changed before you do.

  • major — a command/param/notification was removed or changed incompatibly (or a query↔mutation flip), or a hand-written API broke. Your code may need changes.
  • minor — new commands, optional params, enum values, or notification types. Additive; safe to take.
  • patch — fixes with no surface change.

The bump is computed programmatically from the generated-surface diff plus conventional commits, so it's not a judgement call — see the Versioning & releases section of the dev guide. GENERATED_SPEC_VERSION (exported) tells you which gameserver build the command surface was generated from, independent of the lib's own version:

import { GENERATED_SPEC_VERSION } from '@spacemolt/lib';
console.log(GENERATED_SPEC_VERSION); // e.g. "v0.461.0"

License

MIT