@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/AGENTS.md +118 -0
- package/COMMANDS.md +386 -0
- package/LICENSE +21 -0
- package/README.md +298 -0
- package/dist/account.d.ts +389 -0
- package/dist/auth/clerk.d.ts +58 -0
- package/dist/auth/credentials.d.ts +52 -0
- package/dist/auth/file-store.d.ts +20 -0
- package/dist/client.d.ts +305 -0
- package/dist/data/catalog.d.ts +39 -0
- package/dist/data/map.d.ts +30 -0
- package/dist/errors.d.ts +52 -0
- package/dist/events/emitter.d.ts +58 -0
- package/dist/generated/actions.gen.d.ts +25 -0
- package/dist/generated/commands.gen.d.ts +1790 -0
- package/dist/generated/notifications.gen.d.ts +41 -0
- package/dist/generated/openapi/index.d.ts +1 -0
- package/dist/generated/openapi/types.gen.d.ts +19080 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +4638 -0
- package/dist/node.d.ts +8 -0
- package/dist/node.js +49 -0
- package/dist/protocol.d.ts +178 -0
- package/dist/state/cache.d.ts +43 -0
- package/dist/state/market.d.ts +30 -0
- package/dist/state/observation.d.ts +38 -0
- package/dist/transport/correlator.d.ts +54 -0
- package/dist/transport/socket.d.ts +60 -0
- package/llms.txt +23 -0
- package/package.json +49 -0
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
|