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:
- It's the HTTP client other projects use to talk to the TLE server.
- 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 belownpx jest lib/endpoints/captcha.test.ts— run a single test filenpx jest -t "drain"— run tests matching a namenpm run build— library build viavite build(also emits.d.tsvia 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— regeneratelib/types/schema.tsfrom the@tlecommunity/api-specdevDependencynpm run prepare— full pre-publish pipeline (types→test→build), runs automatically onnpm publish- Requires Node >=24, npm >=10 (
.nvmrcpins 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 <path> over a bare parallel npx jest for anything
broader than a single file.
Architecture #
lib/index.tsis the package entry point, exportingLacuna(default),util, andtypes.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;universityhas no extra methods so it's a plainBuilding).Lacuna#buildingFromUrlmaps a building's module slug to its subclass viaBUILDING_SUBCLASSES, falling back toBuilding. Every endpoint receives the parentLacunainstance in its constructor and reaches back through it (this.lacuna.server,.session,.log).lib/core/endpoint.ts— abstractEndpointbase class every API module extends; providescallWithSession/callWithoutSession, which delegate toServer#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 whenaddSession: true, POSTs via fetch, and normalizes numeric strings in the response viafixNumbers(the server returns numbers as strings).lib/core/responses.ts— the response bus behindLacuna#onResponse. Every responseServerproduces is published to subscribers with the originating request and aretrycallback. Responses carrying a blocking error code (BLOCKING_ERROR_CODESinlib/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 handlerLacuna#enableRpcLimitHandlerregisters: 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 byLacuna#authenticate.lib/core/config.ts— holdsserverUrl/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 extendingEndpointwhose methods just forward typed params tocallWithSession/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 genericBuildingendpoint (lib/endpoints/building.ts) rather thanEndpointdirectly, inheritingview/upgrade/downgrade/demolish.lib/types/*.ts— one file per endpoint module with*Params/*Responseinterfaces, mirroring thelib/endpoints/structure 1:1 (imported asimport * as B from '../types/building'), plusstatus.tsfor the shared status block.lib/types/index.tsre-exports all of them plus shared aliases (ServerDate,EmpireName,IntBool,MapLocation,LacunaConfig).lib/types/schema.tsis 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, diffschema.tsand carry relevant changes into the hand-written types. Tests check live responses against the spec viaexpectMatchesApiSchema(lib/__utils__/validate-schema.ts).
Adding a new endpoint method #
- Add the params/response interfaces to the matching
lib/types/<module>.ts. - Add a method to the matching
lib/endpoints/<module>.tsthat callscallWithSession/callWithoutSessionwith positional params in the server-expected order. - Add a colocated
<module>.test.tsthat exercises it against the live test server viagetLacuna().