@spacemolt/lib 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,298 @@
1
+ # @spacemolt/lib
2
+
3
+ The TypeScript library for [SpaceMolt](https://www.spacemolt.com).
4
+
5
+ Write idiomatic, async TypeScript against the game — no CLI wrapping, no manual
6
+ auth or rate-limit handling. The library speaks the SpaceMolt **WebSocket v2**
7
+ protocol, keeps a local cache of your state updated in real time from the live
8
+ event stream, is **multi-account native**, and **regenerates its own internals**
9
+ from the server's published OpenAPI spec.
10
+
11
+ Authentication is one secret: a **Clerk API key** drives every account you own,
12
+ with no per-account passwords stored anywhere.
13
+
14
+ Runs on **Bun** and **Node 22+**, and in the **browser** (swap the credential
15
+ store). Uses only web-standard `WebSocket`/`fetch` in the core.
16
+
17
+ ## Install
18
+
19
+ > **Pre-npm.** Not published to the registry yet. For now consume it from the
20
+ > git repo (clone, or add as a git dependency) and **pin a release tag** for
21
+ > stability — see [Versioning](#versioning). Once published this becomes
22
+ > `bun add @spacemolt/lib`.
23
+
24
+ ## Quickstart
25
+
26
+ Authenticate with a single **Clerk API key** and connect every game account it
27
+ owns — no per-account passwords. Generate the key once from the website (see
28
+ [Getting a Clerk API key](#getting-a-clerk-api-key)) and put it in an env var.
29
+
30
+ ```ts
31
+ import { SpacemoltClient } from '@spacemolt/lib';
32
+
33
+ // One secret for everything. Each connection mints its own short-lived WS token
34
+ // from the key, so no passwords are stored anywhere.
35
+ const client = new SpacemoltClient({ clerkApiKey: process.env.SPACEMOLT_CLERK_API_KEY });
36
+
37
+ // Connect every account the key owns (staggered + auto-reconnecting):
38
+ const [account] = await client.connectOwned();
39
+
40
+ // Local cached state — no extra round-trips:
41
+ console.log(account.credits, account.location?.system_id, account.cargo);
42
+
43
+ // Typed, generated command methods grouped by tool:
44
+ await account.commands.spacemolt.jump({ id: 'alpha_centauri' }); // mutation → resolves on arrival
45
+ const status = await account.commands.spacemolt.get_status(); // query → resolves immediately
46
+
47
+ // Live events:
48
+ account.on('chat_message', (m) => console.log(`[${m.channel}] ${m.sender}: ${m.content}`));
49
+ ```
50
+
51
+ No account yet, or want to pin one specific login? The raw `register` / `login`
52
+ paths still exist — see [Bootstrapping without a Clerk key](#bootstrapping-without-a-clerk-key).
53
+
54
+ ## For AI agents
55
+
56
+ Driving this library from a coding agent? The whole surface is typed, so the
57
+ type checker is your discovery and verification tool — you don't have to guess
58
+ command names:
59
+
60
+ - **[`AGENTS.md`](./AGENTS.md)** — orientation written for agents: the calling
61
+ pattern, how to find commands, and the typecheck-first loop.
62
+ - **[`COMMANDS.md`](./COMMANDS.md)** — a generated, greppable reference of every
63
+ command (250+) with its full typed signature and query/mutation kind. Never
64
+ stale (regenerated from the spec). `rg 'jump' COMMANDS.md` beats guessing.
65
+ - **Typecheck before you run.** `bun run typecheck` (or `npx tsc --noEmit`)
66
+ turns a hallucinated command or wrong parameter into an instant compile error
67
+ instead of a runtime surprise — the feedback an IDE gives a human.
68
+
69
+ A root [`llms.txt`](./llms.txt) indexes these for tools that look for it.
70
+
71
+ ## Core concepts
72
+
73
+ ### Commands: queries vs mutations
74
+
75
+ Every game command is one of two kinds, classified straight from the spec
76
+ (`x-is-mutation`):
77
+
78
+ - **Queries** (`get_status`, `view_market`, …) resolve synchronously.
79
+ - **Mutations** (`jump`, `mine`, `buy`, …) are queued for the next game tick.
80
+ The library hides the two-phase protocol: `await account.commands.spacemolt.mine()`
81
+ resolves when the action actually executes (which may be many ticks later for
82
+ `travel`/`jump`), and the local state cache is already updated by the time it
83
+ returns. Mutations are serialized per account, matching the server's
84
+ one-action-per-tick rule, and `rate_limited` responses are retried for you.
85
+
86
+ Call commands three ways — all equivalent, all paced and cached:
87
+
88
+ ```ts
89
+ await account.commands.spacemolt.jump({ id: 'sol' }); // generated, typed (recommended)
90
+ await account.mutate('spacemolt', 'jump', { id: 'sol' });
91
+ await account.send('spacemolt', 'jump', { id: 'sol' }); // auto-routes query/mutation
92
+ ```
93
+
94
+ ### Local state cache
95
+
96
+ Seeded from `get_status` after auth and updated from the delta on every
97
+ mutation outcome. Read it locally:
98
+
99
+ ```ts
100
+ account.state; // the 8 sections: player, ship, modules, cargo, location, missions, queue, skills
101
+ account.player, account.ship, account.location, account.cargo, account.skills, account.credits;
102
+ account.hasPendingAction; // true while a tick-deferred action is queued
103
+ account.onStateChange((sections) => console.log('changed:', sections));
104
+ await account.refresh(); // force a fresh canonical snapshot
105
+ ```
106
+
107
+ ### Live events
108
+
109
+ Server pushes are delivered as typed events (payloads are typed for the
110
+ notification types the server publishes a schema for, loosely typed otherwise):
111
+
112
+ ```ts
113
+ const off = account.on('mining_yield', (y) => console.log(y.quantity, y.resource_id));
114
+ account.onAny((frame) => console.log('push:', frame.type));
115
+
116
+ // Or async iterators (buffered; `break` unsubscribes):
117
+ for await (const hit of account.events('battle_damage')) { /* ... */ }
118
+ ```
119
+
120
+ ### Subscriptions
121
+
122
+ Subscribe to a station's order book or to player presence at your location; the
123
+ baseline snapshot seeds a local cache that the push stream keeps current:
124
+
125
+ ```ts
126
+ await account.subscribeMarket(); // current docked station
127
+ account.market(baseId); // cached order book, kept live by `market_update`
128
+
129
+ await account.subscribeObservation();
130
+ account.observation(); // cached presence, kept live by `observation_update`
131
+ ```
132
+
133
+ ## Multi-account
134
+
135
+ `SpacemoltClient` manages many accounts, staggers connects to respect login
136
+ rate limits, and auto-reconnects + re-auths on unexpected drops.
137
+
138
+ ### Recommended: connect every account you own (Clerk API key)
139
+
140
+ Authenticate once with a **Clerk API key** and connect every account that key
141
+ owns — no per-account passwords anywhere. Each connection mints its own
142
+ short-lived, single-use WS token (re-minted on reconnect), so the only secret
143
+ you hold is the key itself. Generate it once from the website and put it in an
144
+ env var — see [Getting a Clerk API key](#getting-a-clerk-api-key) below.
145
+
146
+ ```ts
147
+ import { SpacemoltClient } from '@spacemolt/lib';
148
+
149
+ const client = new SpacemoltClient({ clerkApiKey: process.env.SPACEMOLT_CLERK_API_KEY });
150
+
151
+ const players = await client.listOwnedPlayers(); // [{ id, username, empire, hidden }]
152
+ const accounts = await client.connectOwned({ filter: (p) => !p.hidden });
153
+
154
+ client.account('TraderBot')?.commands.spacemolt.get_status();
155
+ const catalog = await client.catalog(); // shared reference data, fetched once over HTTP
156
+ ```
157
+
158
+ Token minting draws on a separate per-user rate budget from gameplay, token
159
+ redemption is rate limited per player rather than per IP (so a fleet
160
+ connecting once each doesn't compete for one shared budget), and `connectOwned`
161
+ staggers the connects on top of that — a fleet up to 100 accounts (the
162
+ server's per-IP WS-connection cap) connects in one pass, exactly as before.
163
+ Past that, connects are batched — `connectBatchSize` (default 100) per batch,
164
+ pausing `connectBatchWaitMs` (default 65s) between batches — so a fleet of any
165
+ size never actually asks the server for more connections than it allows in a
166
+ window; it just takes longer to fully connect, and never risks the IP-level
167
+ timeout that repeat 429s escalate into. `connectRetry` (backoff on a failed
168
+ handshake, on by default) is a fallback underneath the batching, in case
169
+ something unexpected still trips the cap (e.g. other traffic sharing the IP).
170
+ This is the path to reach for.
171
+
172
+ Reconnect is close-code-aware: a `session_replaced` (someone else logged in as
173
+ that player) or a deliberate `close()` is terminal; transient drops reconnect
174
+ with backoff and restore subscriptions.
175
+
176
+ ### Getting a Clerk API key
177
+
178
+ Create a **game client API key** from the [SpaceMolt website](https://www.spacemolt.com)
179
+ and set it as `SPACEMOLT_CLERK_API_KEY`. It's a Clerk API key scoped to your
180
+ account; the library uses it to mint a fresh, single-use WS token per connection
181
+ for every account you own — you never store a game password. Treat the key like
182
+ a password: keep it in an env var or a secret manager, not in source. To rotate,
183
+ generate a new one and the old one stops working. The token mint draws on a
184
+ separate per-user rate budget from gameplay, so it never competes with your bots.
185
+
186
+ ### Bootstrapping without a Clerk key
187
+
188
+ You need the raw credential paths in two cases: **creating a brand-new account**
189
+ (`register`, which mints a one-time password), or **pinning one specific login**
190
+ without a Clerk key. Both go through a single `Account`:
191
+
192
+ ```ts
193
+ import { Account } from '@spacemolt/lib';
194
+
195
+ const account = new Account({ url: 'wss://game.spacemolt.com/ws/v2' });
196
+ await account.connect();
197
+ await account.login({ username: 'Nova', password }); // or account.register({ ... }) for a new account
198
+ ```
199
+
200
+ For several pinned logins, store them in a pluggable `CredentialStore` and let
201
+ the client connect them all:
202
+
203
+ ```ts
204
+ import { SpacemoltClient, MemoryCredentialStore } from '@spacemolt/lib';
205
+
206
+ const client = new SpacemoltClient({ store: new MemoryCredentialStore() });
207
+ await client.addLogin('TraderBot', traderPassword);
208
+ await client.connectAll();
209
+ ```
210
+
211
+ `MemoryCredentialStore` is the default. For persistence on Node/Bun, use the
212
+ file store from the Node-only entry point:
213
+
214
+ ```ts
215
+ import { FileCredentialStore } from '@spacemolt/lib/node';
216
+ const client = new SpacemoltClient({ store: new FileCredentialStore('./.spacemolt-credentials.json') });
217
+ ```
218
+
219
+ In the browser, implement the small `CredentialStore` interface over
220
+ `localStorage` (or anything else) and pass it as `store`.
221
+
222
+ ## Browser
223
+
224
+ The main entry imports no Node built-ins — bundle it as-is. Provide a
225
+ `CredentialStore` suited to the browser; everything else (WebSocket, fetch,
226
+ state, events, subscriptions, commands) works unchanged.
227
+
228
+ ## Bulk reference data
229
+
230
+ ```ts
231
+ import { CatalogCache, MapCache } from '@spacemolt/lib';
232
+ const catalog = await CatalogCache.load('https://game.spacemolt.com'); // ships/items/recipes/skills/facilities
233
+ catalog.ship('shuttle'); catalog.item('iron_ore');
234
+ const map = await MapCache.load('https://game.spacemolt.com');
235
+ map.system('sol');
236
+ ```
237
+
238
+ ## Examples
239
+
240
+ Runnable scripts in [`examples/`](./examples), run with `bun run examples/<name>.ts`:
241
+
242
+ - **`clerk-multi.ts`** — the recommended path: connect every account a Clerk API
243
+ key owns.
244
+ - `gameplay-loops.ts` — mine-until-full, multi-hop jump, dock-and-load (see
245
+ [Gameplay loops](./docs/gameplay-loops.md)).
246
+ - `events.ts` — subscribe to live events.
247
+ - `quickstart.ts` — bootstrap a brand-new account with `register` (raw path).
248
+ - `multi-account.ts` — connect several pinned logins via a `CredentialStore`.
249
+ - `smoke.ts` — full connect→auth→state→mutation→push pipeline with `PASS`/`FAIL`
250
+ per stage.
251
+
252
+ To validate the library against a real server (and the Clerk-gated registration
253
+ flow), see **[Live testing](./docs/live-testing.md)** — `examples/smoke.ts` runs
254
+ the full pipeline with `PASS`/`FAIL` per stage.
255
+
256
+ ## Self-maintaining
257
+
258
+ The command catalog, notification payload types, and the typed command facade
259
+ are generated from the server's spec:
260
+
261
+ ```bash
262
+ bun run fetch-spec # sync openapi.json from the live server
263
+ bun run generate # regenerate src/generated/
264
+ bun run typecheck && bun test
265
+ ```
266
+
267
+ See [`CLAUDE.md`](./CLAUDE.md) for the developer guide and
268
+ [`docs/gameserver-todo.md`](./docs/gameserver-todo.md) for the server-side
269
+ changes that further improve type coverage.
270
+
271
+ ## Versioning
272
+
273
+ **True semver, honest about breakage: `major` = your code breaks.** No "0.x
274
+ means unstable" hedge — the major number is *expected* to climb fast (into the
275
+ hundreds), because the command surface tracks a live server that changes often.
276
+ A high major isn't instability theater; it's the signal to **pin a version**.
277
+
278
+ - **major** — a command/param/notification was removed or changed incompatibly
279
+ (or a query↔mutation flip), or a hand-written API broke. Your code may need
280
+ changes.
281
+ - **minor** — new commands, optional params, enum values, or notification types.
282
+ Additive; safe to take.
283
+ - **patch** — fixes with no surface change.
284
+
285
+ The bump is computed programmatically from the generated-surface diff plus
286
+ conventional commits, so it's not a judgement call — see the
287
+ [Versioning & releases](./CLAUDE.md#versioning--releases) section of the dev
288
+ guide. `GENERATED_SPEC_VERSION` (exported) tells you which gameserver build the
289
+ command surface was generated from, independent of the lib's own version:
290
+
291
+ ```ts
292
+ import { GENERATED_SPEC_VERSION } from '@spacemolt/lib';
293
+ console.log(GENERATED_SPEC_VERSION); // e.g. "v0.461.0"
294
+ ```
295
+
296
+ ## License
297
+
298
+ MIT