Agent guidance for the @echecs/uci repository — a TypeScript wrapper around
UCI chess engine processes, providing a typed event-emitter API.
@echecs/uci wraps a UCI chess engine subprocess and exposes it as a typed
Emittery-based event emitter. The public API is a single default export: the
UCI class. Runtime dependencies: emittery (event emitter) and zod (option
validation).
Use these to cross-check output when testing:
node-uci— UCI protocol implementation for Node.js; promise-based API.uci— thin wrapper on a UCI chess engine.uci.js— TypeScript UCI library with async engine interaction.
Key source files:
| File | Role |
|---|---|
src/index.ts |
UCI class — public API and engine lifecycle |
src/types.ts |
Exported types: ID, InfoCommand, Option, Score |
src/options.ts |
Options class — engine option store with Zod validation |
src/process.ts |
Process class — child process wrapper with line emitter |
src/parser/index.ts |
Parser dispatcher — routes UCI output tokens to handlers |
src/parser/bestmove.ts |
Parses bestmove responses |
src/parser/info.ts |
Parses info lines into typed InfoCommand objects |
src/parser/identity.ts |
Parses id responses |
src/parser/extract.ts |
Utility: extracts named fields from UCI token strings |
src/parser/noop.ts |
Pass-through for commands with no payload |
src/__tests__/index.spec.ts |
Unit tests for the UCI class |
src/__tests__/parser.spec.ts |
Unit tests for the parser module |
src/__tests__/integration.spec.ts |
Integration tests (skipped without a real engine binary) |
Use pnpm exclusively (no npm/yarn).
pnpm build # bundle TypeScript → dist/ via tsdownpnpm test # run all tests once (vitest run)
pnpm test:watch # watch mode
pnpm test:coverage # with v8 coverage report
# Run a single test file
pnpm test src/__tests__/parser.spec.ts
# Run tests matching a name substring
pnpm test -- --reporter=verbose -t "bestmove"Note:
integration.spec.tstests are skipped whenUCI_ENGINE_PATHis not set. Set it to a UCI engine path to enable them. 6 skipped tests is expected and normal in CI.
pnpm lint # ESLint + tsc type-check (auto-fixes style issues)
pnpm lint:ci # strict — zero warnings allowed, no auto-fix
pnpm lint:style # ESLint only (auto-fixes)
pnpm lint:types # tsc --noEmit type-check only
pnpm format # Prettier (writes changes)
pnpm format:ci # Prettier check only (no writes)pnpm lint && pnpm test && pnpm build- Strict mode fully enabled:
strict,noUncheckedIndexedAccess,noImplicitOverride. - Target:
ESNext; module system:NodeNextwith NodeNext resolution. - All type-only imports must use
import type { ... }(enforced by@typescript-eslint/consistent-type-imports). - All exported functions and methods must have explicit return types
(
@typescript-eslint/explicit-module-boundary-types). - Avoid non-null assertions (
!); use explicit narrowing instead (@typescript-eslint/no-non-null-assertionis a warning). - Use
interfacefor object shapes andtypefor unions/aliases (@typescript-eslint/consistent-type-definitions: ['error', 'interface']). - Always include
.jsextension on relative imports — NodeNext resolution requires it even for.tssource files. - Class members follow natural ordering (fields → constructor → getters/setters
→ methods, each group by visibility) — enforced by
@typescript-eslint/member-ordering.
- Single quotes for strings.
- Trailing commas everywhere (
all). quoteProps: 'consistent'— quote all object keys or none within an object.proseWrap: 'always'— wrap markdown prose at print width.- Prettier runs automatically via lint-staged on every commit.
eqeqeq— always use===/!==.curly: 'all'— always use braces for control flow bodies, even single lines.sort-keys— object literal keys and interface fields must be sorted alphabetically in source files. Disabled in test files.sort-imports— named import specifiers must be sorted within each import statement. Declaration-level ordering is handled byimport-x/order.no-console— disallowed in source (warning); permitted in tests.eslint-plugin-unicorn(recommended) is enabled — modern JS/TS idioms enforced throughout.@vitest/eslint-plugin(recommended) is enabled in test files.
Groups, separated by a blank line, in this order:
- Built-in + external packages
- Internal (
@/…path aliases) - Parent and sibling relative imports
- Type-only imports
| Construct | Convention | Examples |
|---|---|---|
| Classes | PascalCase |
UCI, Process, Options |
| Functions / methods | camelCase |
execute, ingest, ready |
| Types / Interfaces | PascalCase |
ID, InfoCommand, Score |
| Module-level constants | SCREAMING_SNAKE_CASE |
TIMEOUT |
| Variables / Parameters | camelCase |
command, payload, path |
| Source files | camelCase.ts |
index.ts, process.ts |
- Framework: Vitest (
vitest run). - Test files live in
src/__tests__/with the.spec.tssuffix. - Use
describeto group cases; useit(nottest) inside them. - Prefer
expect(x).toBe(y)for exact equality. sort-keysandno-consoleare relaxed inside__tests__/.- Integration tests (requiring a real engine binary) are skipped automatically
when
UCI_ENGINE_PATHis not set. To run them locally, export the path to a UCI-compatible engine (e.g. Stockfish):Never remove theUCI_ENGINE_PATH=/usr/local/bin/stockfish pnpm testskipIfguard — integration tests must never fail CI unconditionally.
Input validation is mostly provided by TypeScript's strict type system at
compile time. The exception in this package is engine option validation, which
uses zod for runtime schema validation (since engine options arrive as untyped
strings from the UCI protocol). Do not add additional runtime type-checking
guards elsewhere unless there is an explicit trust boundary.
UCIdoes not extendEmittery— it holds a private#emitterfield and exposes onlyon(),off(), andonce(). All engine output is surfaced as typed events (bestmove,info,id,option,error, etc.).- Engine communication flows:
Process(child process + line reader) →UCI#ingest(parser dispatch) → typedemitcalls. Optionsvalidates and stores engine options using Zod schemas.src/types.tsholds all public types as named exports — do not use global ambient namespaces.- Propagate errors via
this.#emitter.emit('error', ...)— do not swallow them silently. - Runtime dependencies (
emittery,zod) are intentional; do not remove them.
ready() and execute() are error-absorbing: they catch all rejections
internally and route them to this.#emitter.emit('error', ...), then return a
resolved promise. They never reject.
This means adding a .catch() to a chain like
this.ready().then(() => this.execute(...)) is dead code — neither call
will reject, so the .catch() can never fire. Do not propose "add a .catch()
to propagate errors" as a fix anywhere in src/index.ts; it will not work
without first refactoring ready() or execute() to throw instead of absorb.
Note: ready() does not permanently cache errors. A failed isready
handshake does not prevent subsequent ready() calls from retrying — each call
performs a fresh handshake.
Step-by-step process for releasing a new version. CI auto-publishes to npm when
version in package.json changes on main.
-
Verify the package is clean:
pnpm lint && pnpm test && pnpm build
Do not proceed if any step fails.
-
Decide the semver level:
patch— bug fixes, internal refactors with no API changeminor— new features, new exports, non-breaking additionsmajor— breaking changes to the public API
-
Update
CHANGELOG.mdfollowing Keep a Changelog format:## [x.y.z] - YYYY-MM-DD ### Added - … ### Changed - … ### Fixed - … ### Removed - …
Include only sections that apply. Use past tense.
-
Update
README.mdif the release introduces new public API, changes usage examples, or deprecates/removes existing features. -
Bump the version:
npm version <major|minor|patch> --no-git-tag-version
-
Commit and push:
git add package.json CHANGELOG.md README.md git commit -m "release: @echecs/uci@x.y.z" git push -
CI takes over: GitHub Actions detects the version bump, runs format → lint → test, and publishes to npm.
Do not manually publish with npm publish.