# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What this repo is A TypeScript client library for the JSON-RPC API of _The Lacuna Expanse_ (community edition game server), published as `@tlecommunity/client` to a private Gitea npm registry. The repo serves two purposes at once: 1. It's the HTTP client other projects use to talk to the TLE server. 2. Its test suite is a deliberate **integration test for the TLE backend itself** — tests call the real, live TLE server (no mocks) to verify responses stay correct as that backend migrates from a legacy, untyped Perl codebase to Ruby on Rails. This is _why_ tests hit a live server instead of mocking — it's the design, not an oversight. A passing suite is evidence the backend still behaves correctly, not just that the client code is correct. ## Commands - `npm run types` — typecheck (`tsc --noEmit`) - `npm test` — run the full suite (`jest --coverage`); makes real network calls, see caveat below - `npx jest lib/endpoints/captcha.test.ts` — run a single test file - `npx jest -t "drain"` — run tests matching a name - `npm run build` — library build via `vite build` (also emits `.d.ts` via vite-plugin-dts) - `npm run format` / `npm run format:fix` — Prettier check/write over the whole repo (there is no ESLint in this repo) - `npm run generate:types` — regenerate `lib/types/schema.ts` from the `@tlecommunity/api-spec` devDependency - `npm run prepare` — full pre-publish pipeline (`types` → `test` → `build`), runs automatically on `npm publish` - Requires Node >=24, npm >=10 (`.nvmrc` pins 24) ## Test caveat (read before touching tests) Tests are intentionally **not mocked**. `lib/__utils__/get-lacuna.ts` constructs a real `Lacuna` instance and authenticates against a live server (default `serverUrl` in `lib/core/config.ts` is a Tailscale-hosted instance). Every `*.test.ts` file under `lib/` exercises real endpoints over the network. `lib/__utils__/` itself is excluded from Jest's test discovery and from coverage collection (it's a helper, not a test). The live server rate-limits to 200 requests/minute per empire. With one test file per endpoint method group, `getLacuna()` (session) and `findBuilding()` (building inventory) cache their live lookups on disk (`os.tmpdir()`, ~4 minute TTL) so the whole run shares one login and one inventory fetch instead of every file redoing both. `npm test` runs Jest with `--runInBand` for the same reason — the default parallel workers each hit the live server independently and can trip the rate limit (or race the shared cache) even with the caching in place. Prefer `npm test` or `npx jest --runInBand ` over a bare parallel `npx jest` for anything broader than a single file. ## Architecture - `lib/index.ts` is the package entry point, exporting `Lacuna` (default), `util`, and `types`. - `Lacuna` (`lib/lacuna.ts`) is the root object. Its constructor builds the core infra — `Log`, `Config`, `Responses`, `Server`, `Session` — and then one instance per API module (`alliance`, `body`, `captcha`, `empire`, `inbox`, `map`, `stats`, and one per building type, e.g. `shipyard`, `spacePort`, `planetaryCommand`; `university` has no extra methods so it's a plain `Building`). `Lacuna#buildingFromUrl` maps a building's module slug to its subclass via `BUILDING_SUBCLASSES`, falling back to `Building`. Every endpoint receives the parent `Lacuna` instance in its constructor and reaches back through it (`this.lacuna.server`, `.session`, `.log`). - `lib/core/endpoint.ts` — abstract `Endpoint` base class every API module extends; provides `callWithSession` / `callWithoutSession`, which delegate to `Server#call`. - `lib/core/server.ts` — the JSON-RPC transport. Builds the request body (`{jsonrpc: '2.0', id, method, params}`), injects the session id into positional (array) params when `addSession: true`, POSTs via fetch, and normalizes numeric strings in the response via `fixNumbers` (the server returns numbers as strings). - `lib/core/responses.ts` — the response bus behind `Lacuna#onResponse`. Every response `Server` produces is published to subscribers with the originating request and a `retry` callback. Responses carrying a blocking error code (`BLOCKING_ERROR_CODES` in `lib/core/constants.ts` — RPC limit 1010, captcha 1016, and rejected-session 1006) are dispatched _awaited_, and a response a handler returns replaces the one the caller gets; everything else is fire-and-forget. Every handler is called even after a blocking error is resolved (subscribers are promised _all_ responses) — later handlers just receive the resolved response, which is what keeps them from retrying redundantly. - `lib/core/rpc-limit-handler.ts` — the opt-in handler `Lacuna#enableRpcLimitHandler` registers: retries 1010 with progressive backoff. The library logs nothing about errors on its own; that's what subscribers are for. - `lib/core/session.ts` — holds the session id set by `Lacuna#authenticate`. - `lib/core/config.ts` — holds `serverUrl` / `apiKey` / `fingerprintToken`. - `lib/endpoints/*.ts` — one file per top-level API module (`alliance`, `body`, `captcha`, `empire`, `inbox`, `map`, `stats`, `building`). Each is a thin class extending `Endpoint` whose methods just forward typed params to `callWithSession`/`callWithoutSession`, in the exact positional-argument order the server RPC expects. - `lib/endpoints/buildings/*.ts` — building-specific modules (`essentia-vein`, `shipyard`, `space-port`, ...) extend the generic `Building` endpoint (`lib/endpoints/building.ts`) rather than `Endpoint` directly, inheriting `view`/`upgrade`/`downgrade`/`demolish`. - `lib/types/*.ts` — one file per endpoint module with `*Params`/`*Response` interfaces, mirroring the `lib/endpoints/` structure 1:1 (imported as `import * as B from '../types/building'`), plus `status.ts` for the shared status block. `lib/types/index.ts` re-exports all of them plus shared aliases (`ServerDate`, `EmpireName`, `IntBool`, `MapLocation`, `LacunaConfig`). - `lib/types/schema.ts` is **generated** from `@tlecommunity/api-spec` (`npm run generate:types`) — don't hand-edit it. The hand-written types are the public API; where they deliberately differ from the spec, the divergence is listed in README "Spec Discrepancies". After bumping the spec, diff `schema.ts` and carry relevant changes into the hand-written types. Tests check live responses against the spec via `expectMatchesApiSchema` (`lib/__utils__/validate-schema.ts`). ### Adding a new endpoint method 1. Add the params/response interfaces to the matching `lib/types/.ts`. 2. Add a method to the matching `lib/endpoints/.ts` that calls `callWithSession`/`callWithoutSession` with positional params in the server-expected order. 3. Add a colocated `.test.ts` that exercises it against the live test server via `getLacuna()`.