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:
- Full protocol compliance — implements the complete UCI spec:
uci,isready,ucinewgame,position,go,stop,setoption,register, andponderhit. Engine output (id,option,info,bestmove) is parsed into typed objects. - Time controls — pass
wtime,btime,movetime,depth, and more as a typedGoOptionsobject. - Pondering — first-class
ponder()andponderhit()with correct state tracking. - Typed
infoevents — 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. - Typed
score— scores are a discriminated union (cpormate, with optionallowerbound/upperbound), not a raw string or plain number. - Engine options — options advertised by the engine are validated before
being sent via
setoption. - Event-based — built on Emittery for fully-typed async events.
npm install @echecs/uciNamed types are exported directly from the package:
importUCI,{typeGoOptions,typeEvents}from'@echecs/uci';// Also available: ID, InfoCommand, Option, RegisterOptions, ScoreimportUCIfrom'@echecs/uci';constengine=newUCI('/usr/bin/stockfish');engine.on('bestmove',({ move })=>{console.log(`Best move: ${move}`);// e.g. "e2e4"});awaitengine.start();newUCI(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.
constengine=newUCI('/usr/bin/stockfish');constengine=newUCI('./engines/lc0',{timeout: 10_000});constengine=newUCI('/usr/bin/stockfish',{config: {Hash: 256,Threads: 4},});awaitengine.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)awaitengine.start();// Fixed time per moveawaitengine.start({movetime: 1000});// Clock-based (standard game)awaitengine.start({wtime: 60_000,btime: 60_000,winc: 1000,binc: 1000});// Fixed depthawaitengine.start({depth: 20});All fields are optional. When none are set, the engine searches infinitely until
stop() is called.
interfaceGoOptions{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 movesmovestogo?: number;// moves until next time controlmovetime?: number;// search exactly N msnodes?: number;// search exactly N nodessearchmoves?: string[];// restrict search to these moveswinc?: number;// white increment per move (ms)wtime?: number;// white remaining time (ms)}awaitengine.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().
awaitengine.move('e2e4');awaitengine.move('e7e5',{movetime: 500});awaitengine.move('e7e8q');// promotionPondering lets the engine think on the opponent's time.
// After receiving bestmove with a ponder suggestion, start ponderingengine.on('bestmove',async({ move, ponder })=>{if(ponder){awaitengine.ponder(ponder);}});// Opponent played the predicted move — switch to normal searchawaitengine.ponderhit();// Opponent played a different move — stop pondering, then send the actual moveawaitengine.stop();awaitengine.move('d7d5');awaitengine.ponder(move: string,options?: GoOptions): Promise<void>awaitengine.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.
awaitengine.position('startpos');// initial position (default)awaitengine.position('fen <fenstring>');// custom positionCalling 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)awaitengine.stop(): Promise<void>// halts the current search (engine stays alive)awaitengine.reset(): Promise<void>// sends ucinewgame + resets to startposawaitengine[Symbol.dispose](): Promise<void>// sends quit + kills the processawaitengine.debug(true): Promise<void>// sends "debug on"awaitengine.debug(false): Promise<void>// sends "debug off"asyncid(): Promise<ID>Waits for the UCI handshake to complete, then returns the engine's ID object
({ name: string; author: string }). Throws if the engine did not report an
identity.
const{ name, author }=awaitengine.id();console.log(`${name} by ${author}`);asyncregister(options?: RegisterOptions): Promise<void>Sends a register command to the engine. Call without arguments to defer
registration (register later), or pass { name, code } to register
immediately.
awaitengine.register();// register laterawaitengine.register({name: 'My Name',code: '4359874324'});once<KextendskeyofEvents>(event: K): Promise<Events[K]>Returns a promise that resolves with the next emission of event. Useful for
waiting on a single engine response without setting up a persistent listener.
const{ move }=awaitengine.once('bestmove');off<KextendskeyofEvents>(event: K,listener: (data: Events[K])=>void|Promise<void>): voidRemoves a listener previously registered with on().
consthandler=({ move }: {move: string|undefined})=>console.log(move);engine.on('bestmove',handler);// …engine.off('bestmove',handler);awaitengine.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){cpuload?: number,// cpu usage in permillcurrent?: {line?: string[],move?: string,number?: number},depth?: number|{selective: number,total: number},hashfull?: number,// hash usage in permillinfo?: string,// free-form engine stringline?: number,// multipv line numbermoves?: string[],// pv move listnodes?: number,refutation?: string[],sbhits?: number,// Shredder endgame DB hitsscore?: Score,stats?: {nps?: number},tbhits?: number,// endgame tablebase hitstime?: number,// ms}|{ 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'}{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://uci.echecs.dev/
Contributions are welcome. Please read CONTRIBUTING.md for guidelines on how to submit issues and pull requests.