UCI is a TypeScript wrapper for the Universal Chess Interface protocol — the standard way for chess GUIs to communicate with chess engines such as Stockfish, Leela Chess Zero, and Komodo.
It spawns and manages the engine process, handles the full UCI handshake, and surfaces engine output as typed events. Zero configuration required.
Working with UCI engines directly means parsing a line-oriented text protocol, managing process lifecycle, and coordinating asynchronous handshakes. This library handles all of that:
uci,
isready, ucinewgame, position, go, stop, setoption, register,
and ponderhit. Engine output (id, option, info, bestmove) is parsed
into typed objects.wtime, btime, movetime, depth, and more as a
typed GoOptions object.ponder() and ponderhit() with correct state
tracking.info events — search information is fully parsed: depth, selective
depth, score (centipawns or mate distance with bound flags), PV moves, nodes,
NPS, time, hashfull, CPU load, and endgame tablebase hits.score — scores are a discriminated union (cp or mate, with
optional lowerbound/upperbound), not a raw string or plain number.setoption.npm install @echecs/uci
Named types are exported directly from the package:
import UCI, { type GoOptions, type Events } from '@echecs/uci';
// Also available: ID, InfoCommand, Option, Score
import UCI from '@echecs/uci';
const engine = new UCI('/usr/bin/stockfish');
engine.on('bestmove', ({ move }) => {
console.log(`Best move: ${move}`); // e.g. "e2e4"
});
await engine.start();
new UCI(path: string, options?: { config?: Record<string, unknown>; timeout?: number })
path is the path to the UCI engine binary. timeout (default 5000 ms) is how
long to wait for the engine to respond to the initial uci command before
emitting an error. config is an optional map of setoption values applied
once after the UCI handshake.
const engine = new UCI('/usr/bin/stockfish');
const engine = new UCI('./engines/lc0', { timeout: 10_000 });
const engine = new UCI('/usr/bin/stockfish', {
config: { Hash: 256, Threads: 4 },
});
await engine.start(options?: GoOptions): Promise<void>
Waits for the engine to be ready, applies setoption values from the
constructor config, then sends go. Accepts an optional GoOptions object
for time controls and search limits. Listen for info events during the search
and bestmove when it finishes.
engine.on('info', (info) => {
if (info.score?.type === 'cp') {
console.log(`Score: ${info.score.value} pawns at depth ${info.depth}`);
}
if (info.score?.type === 'mate') {
console.log(`Mate in ${info.score.value}`);
}
});
engine.on('bestmove', ({ move, ponder }) => {
console.log(`Best: ${move}, ponder: ${ponder}`);
});
// Infinite search (default)
await engine.start();
// Fixed time per move
await engine.start({ movetime: 1000 });
// Clock-based (standard game)
await engine.start({ wtime: 60_000, btime: 60_000, winc: 1000, binc: 1000 });
// Fixed depth
await engine.start({ depth: 20 });
GoOptionsAll fields are optional. When none are set, the engine searches infinitely until
stop() is called.
interface GoOptions {
binc?: number; // black increment per move (ms)
btime?: number; // black remaining time (ms)
depth?: number; // search to this depth (overrides engine.depth)
mate?: number; // search for mate in N moves
movestogo?: number; // moves until next time control
movetime?: number; // search exactly N ms
nodes?: number; // search exactly N nodes
searchmoves?: string[]; // restrict search to these moves
winc?: number; // white increment per move (ms)
wtime?: number; // white remaining time (ms)
}
await engine.move(move: string, options?: GoOptions): Promise<void>
Sends a move in long algebraic notation, stops the current search, updates the
position, and restarts the search. Moves accumulate — call reset() to start a
new game. Accepts the same GoOptions as start().
await engine.move('e2e4');
await engine.move('e7e5', { movetime: 500 });
await engine.move('e7e8q'); // promotion
Pondering lets the engine think on the opponent's time.
// After receiving bestmove with a ponder suggestion, start pondering
engine.on('bestmove', async ({ move, ponder }) => {
if (ponder) {
await engine.ponder(ponder);
}
});
// Opponent played the predicted move — switch to normal search
await engine.ponderhit();
// Opponent played a different move — stop pondering, then send the actual move
await engine.stop();
await engine.move('d7d5');
await engine.ponder(move: string, options?: GoOptions): Promise<void>
await engine.ponderhit(): Promise<void>
ponder() sends go ponder with the speculative opponent move. Calling it
while already pondering emits an error. ponderhit() commits the ponder move
and switches the engine to normal search; calling it when not pondering emits an
error.
engine.position = 'startpos'; // initial position (default)
engine.position = 'fen <fenstring>'; // custom position
Assigning position resets the move list and sends position to the engine.
engine.depth = 10; // default depth for go (overridden by GoOptions.depth)
engine.lines = 3; // MultiPV — return top N lines (default: 1)
await engine.stop(): Promise<void> // halts the current search (engine stays alive)
await engine.reset(): Promise<void> // sends ucinewgame + resets to startpos
await engine[Symbol.dispose](): Promise<void> // sends quit + kills the process
await engine.execute(command: string): Promise<void>
Sends an arbitrary UCI command string to the engine. Useful for engine-specific
extensions (e.g. d for board display in Stockfish).
engine.on('bestmove', ({ move, ponder }) => void)
engine.on('copyprotection', (status: string) => void)
engine.on('error', (error: Error) => void)
engine.on('id', ({ name, author }) => void)
engine.on('info', (info: InfoCommand) => void)
engine.on('option', (option: Option) => void)
engine.on('output', (line: string) => void)
engine.on('readyok', () => void)
engine.on('registration', (status: string) => void)
engine.on('uciok', () => void)
InfoCommand{
cpuload?: number, // cpu usage in permill
current?: { line?: string[], move?: string, number?: number },
depth?: number | { selective: number, total: number },
hashfull?: number, // hash usage in permill
info?: string, // free-form engine string
line?: number, // multipv line number
moves?: string[], // pv move list
nodes?: number,
refutation?: string[],
sbhits?: number, // Shredder endgame DB hits
score?: Score,
stats?: { nps?: number },
tbhits?: number, // endgame tablebase hits
time?: number, // ms
}
Score| { type: 'cp'; value: number } // centipawns ÷ 100
| { type: 'mate'; value: number } // moves to mate (negative = being mated)
| { type: 'cp'; value: number; bound: 'lower' }
| { type: 'cp'; value: number; bound: 'upper' }
Option{ name: string } & (
| { type: 'button' }
| { type: 'check', default: boolean }
| { type: 'combo', default: string, var: string[] }
| { type: 'spin', default: number, min?: number, max?: number }
| { type: 'string', default: string }
)
Full API reference is available at https://mormubis.github.io/uci/
Contributions are welcome. Please read CONTRIBUTING.md for guidelines on how to submit issues and pull requests.