llm-chess-mcp 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -4
- package/dist/chess.d.ts +15 -0
- package/dist/chess.js +145 -4
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +17 -3
- package/dist/engines/stockfish.d.ts +50 -0
- package/dist/engines/stockfish.js +49 -5
- package/dist/env.d.ts +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/eval.d.ts +24 -0
- package/dist/explorer.d.ts +64 -0
- package/dist/explorer.js +272 -98
- package/dist/games.d.ts +33 -0
- package/dist/games.js +26 -15
- package/dist/http-work.d.ts +8 -0
- package/dist/http.d.ts +32 -0
- package/dist/http.js +206 -127
- package/dist/index.d.ts +6 -0
- package/dist/index.js +18 -16
- package/dist/intents.d.ts +39 -0
- package/dist/intents.js +47 -32
- package/dist/maia3/inference.d.ts +7 -0
- package/dist/maia3/inference.js +2 -0
- package/dist/maia3/mirror.d.ts +3 -0
- package/dist/maia3/tokenize.d.ts +5 -0
- package/dist/maia3/vocab.d.ts +3 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +18 -2
- package/dist/services.d.ts +21 -0
- package/dist/services.js +30 -5
- package/dist/tool-inputs.d.ts +235 -0
- package/dist/tool-meta.d.ts +141 -0
- package/dist/tool-names.d.ts +2 -0
- package/dist/tool-names.js +15 -0
- package/dist/tool-result.d.ts +13 -0
- package/dist/tool-result.js +7 -2
- package/dist/tool-schemas.d.ts +870 -0
- package/dist/tool-schemas.js +0 -7
- package/dist/tools/analysis.d.ts +5 -0
- package/dist/tools/analysis.js +3 -6
- package/dist/tools/candidates.d.ts +5 -0
- package/dist/tools/candidates.js +45 -45
- package/dist/tools/explorer.d.ts +5 -0
- package/dist/tools/explorer.js +1 -3
- package/dist/tools/game.d.ts +5 -0
- package/dist/tools/game.js +12 -10
- package/dist/types.d.ts +44 -0
- package/docs/architecture.md +22 -13
- package/package.json +13 -2
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ provided separately.
|
|
|
23
23
|
|
|
24
24
|
## Install
|
|
25
25
|
|
|
26
|
-
Requires Node.js 20 or newer.
|
|
26
|
+
Requires Node.js 20.3 or newer.
|
|
27
27
|
|
|
28
28
|
No install needed — run it directly with `npx`:
|
|
29
29
|
|
|
@@ -93,6 +93,19 @@ HTTP options:
|
|
|
93
93
|
--allowed-host <host> Allowed Host/Origin hostname; repeat as needed
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
The package also exposes a typed ESM API:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
import { serveHttp } from "llm-chess-mcp";
|
|
100
|
+
|
|
101
|
+
const server = await serveHttp({ port: 3000, bodyTimeoutMs: 15_000 });
|
|
102
|
+
await server.close();
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`bodyTimeoutMs` limits HTTP body upload time; it is not a whole-tool deadline.
|
|
106
|
+
The deprecated `requestTimeoutMs` alias remains supported when `bodyTimeoutMs`
|
|
107
|
+
is omitted.
|
|
108
|
+
|
|
96
109
|
Binding to `0.0.0.0` or `::` requires at least one `--allowed-host`. HTTP mode
|
|
97
110
|
does not provide authentication or TLS; use a trusted network or an
|
|
98
111
|
authenticated reverse proxy when exposing it beyond localhost. Origin values
|
|
@@ -373,12 +386,14 @@ rejected:
|
|
|
373
386
|
- `move_evaluate` accepts at most 10 moves per call.
|
|
374
387
|
- Imported PGNs are limited to 1 MiB and 4,096 plies.
|
|
375
388
|
- Stockfish accepts up to 32 active or queued analyses.
|
|
376
|
-
-
|
|
377
|
-
|
|
389
|
+
- Lichess Explorer requests run one at a time and share 429 cooldowns.
|
|
390
|
+
- HTTP retains at most 64 MCP sessions; sessions with no active request expire
|
|
391
|
+
after 30 minutes. An open GET/SSE stream keeps its session active.
|
|
378
392
|
- HTTP accepts bodies up to 2 MiB. It permits 16 concurrent POSTs and downstream
|
|
379
393
|
compute/network jobs process-wide, with two of each per session. Work keeps
|
|
380
394
|
its slot after a raw disconnect until it settles. HTTP also caps connections
|
|
381
|
-
at 128 and applies
|
|
395
|
+
at 128 and applies a 15-second body upload deadline plus bounded header,
|
|
396
|
+
socket, and keep-alive timeouts.
|
|
382
397
|
|
|
383
398
|
Programmatic users can override the HTTP limits through `HttpServerOptions`.
|
|
384
399
|
These safeguards do not replace public-edge quotas: a public deployment must
|
package/dist/chess.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Chess } from "chess.js";
|
|
2
|
+
import type { Move } from "chess.js";
|
|
3
|
+
import type { ChessState, DrawResult } from "./types.js";
|
|
4
|
+
export declare const MAX_EVALUATED_MOVES = 10;
|
|
5
|
+
export declare const MAX_PGN_BYTES: number;
|
|
6
|
+
export declare const MAX_PGN_PLIES = 4096;
|
|
7
|
+
export declare function assertSafeFenCounters(fen: string): void;
|
|
8
|
+
export declare function snapshotChess(chess: Chess): Chess;
|
|
9
|
+
export declare function drawResult(chess: Chess): DrawResult | null;
|
|
10
|
+
export declare function pgnOf(chess: Chess): string;
|
|
11
|
+
export declare function parseImportedPgn(pgn: string): Chess;
|
|
12
|
+
export declare function stateOf(chess: Chess, revision: number): ChessState;
|
|
13
|
+
export declare function parseMove(chess: Chess, move: string): Move;
|
|
14
|
+
export declare function playParsedMove(chess: Chess, move: Move): Move;
|
|
15
|
+
export declare function pvToSan(chess: Chess, pv: readonly string[]): string[];
|
package/dist/chess.js
CHANGED
|
@@ -3,15 +3,45 @@ import { ChessError } from "./errors.js";
|
|
|
3
3
|
export const MAX_EVALUATED_MOVES = 10;
|
|
4
4
|
export const MAX_PGN_BYTES = 1024 * 1024;
|
|
5
5
|
export const MAX_PGN_PLIES = 4096;
|
|
6
|
+
const PGN_RESULTS = ["1-0", "0-1", "1/2-1/2", "*"];
|
|
6
7
|
function moveDescriptor(move) {
|
|
7
8
|
const base = { from: move.from, to: move.to };
|
|
8
9
|
return move.promotion ? { ...base, promotion: move.promotion } : base;
|
|
9
10
|
}
|
|
11
|
+
function isSafeDecimal(value, minimum) {
|
|
12
|
+
return (/^(?:0|[1-9]\d*)$/.test(value) &&
|
|
13
|
+
Number.isSafeInteger(Number(value)) &&
|
|
14
|
+
Number(value) >= minimum);
|
|
15
|
+
}
|
|
16
|
+
export function assertSafeFenCounters(fen) {
|
|
17
|
+
const fields = fen.split(/\s+/);
|
|
18
|
+
if (fields.length >= 5 && !isSafeDecimal(fields[4] ?? "", 0)) {
|
|
19
|
+
throw new ChessError("INVALID_FEN", "FEN halfmove clock must be a non-negative safe decimal integer");
|
|
20
|
+
}
|
|
21
|
+
if (fields.length >= 6 && !isSafeDecimal(fields[5] ?? "", 1)) {
|
|
22
|
+
throw new ChessError("INVALID_FEN", "FEN fullmove number must be a positive safe decimal integer");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
10
25
|
export function snapshotChess(chess) {
|
|
11
26
|
const history = chess.history({ verbose: true });
|
|
12
|
-
const
|
|
13
|
-
|
|
27
|
+
const initialFen = history[0]?.before ?? chess.fen();
|
|
28
|
+
assertSafeFenCounters(initialFen);
|
|
29
|
+
const snapshot = new Chess(initialFen);
|
|
30
|
+
const comments = new Map(chess.getComments().map(({ fen, comment }) => [fen, comment]));
|
|
31
|
+
for (const [key, value] of Object.entries(chess.getHeaders())) {
|
|
32
|
+
snapshot.setHeader(key, value);
|
|
33
|
+
}
|
|
34
|
+
const restoreComment = () => {
|
|
35
|
+
const comment = comments.get(snapshot.fen());
|
|
36
|
+
if (comment !== undefined)
|
|
37
|
+
snapshot.setComment(comment);
|
|
38
|
+
};
|
|
39
|
+
restoreComment();
|
|
40
|
+
for (const move of history) {
|
|
14
41
|
snapshot.move(moveDescriptor(move));
|
|
42
|
+
restoreComment();
|
|
43
|
+
}
|
|
44
|
+
assertSafeFenCounters(snapshot.fen());
|
|
15
45
|
return snapshot;
|
|
16
46
|
}
|
|
17
47
|
export function drawResult(chess) {
|
|
@@ -27,10 +57,117 @@ export function drawResult(chess) {
|
|
|
27
57
|
return "fifty_move_rule";
|
|
28
58
|
return "draw";
|
|
29
59
|
}
|
|
60
|
+
function withoutPgnComments(pgn) {
|
|
61
|
+
let result = "";
|
|
62
|
+
let braceComment = false;
|
|
63
|
+
let lineComment = false;
|
|
64
|
+
let quoted = false;
|
|
65
|
+
let escaped = false;
|
|
66
|
+
for (const char of pgn) {
|
|
67
|
+
if (braceComment) {
|
|
68
|
+
if (char === "}")
|
|
69
|
+
braceComment = false;
|
|
70
|
+
result += char === "\n" || char === "\r" ? char : " ";
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (lineComment) {
|
|
74
|
+
if (char === "\n" || char === "\r") {
|
|
75
|
+
lineComment = false;
|
|
76
|
+
result += char;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
result += " ";
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (quoted) {
|
|
84
|
+
result += char;
|
|
85
|
+
if (escaped)
|
|
86
|
+
escaped = false;
|
|
87
|
+
else if (char === "\\")
|
|
88
|
+
escaped = true;
|
|
89
|
+
else if (char === '"')
|
|
90
|
+
quoted = false;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (char === '"') {
|
|
94
|
+
quoted = true;
|
|
95
|
+
result += char;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (char === "{") {
|
|
99
|
+
braceComment = true;
|
|
100
|
+
result += " ";
|
|
101
|
+
}
|
|
102
|
+
else if (char === ";") {
|
|
103
|
+
lineComment = true;
|
|
104
|
+
result += " ";
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
result += char;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
function isPgnResult(value) {
|
|
113
|
+
return PGN_RESULTS.includes(value);
|
|
114
|
+
}
|
|
115
|
+
function declaredPgnResult(pgn) {
|
|
116
|
+
const visiblePgn = withoutPgnComments(pgn);
|
|
117
|
+
const headerResults = [
|
|
118
|
+
...visiblePgn.matchAll(/^\s*\[\s*Result\s+"((?:\\.|[^"\\])*)"\s*\]\s*$/gm),
|
|
119
|
+
].map((match) => match[1] ?? "");
|
|
120
|
+
const movetext = visiblePgn.replace(/^\s*\[[^\r\n]*\]\s*$/gm, "");
|
|
121
|
+
const markers = [
|
|
122
|
+
...movetext.matchAll(/(?:^|\s)(1-0|0-1|1\/2-1\/2|\*)(?=\s|$)/g),
|
|
123
|
+
].map((match) => match[1] ?? "");
|
|
124
|
+
const results = [...headerResults, ...markers];
|
|
125
|
+
if (!results.every(isPgnResult)) {
|
|
126
|
+
throw new ChessError("INVALID_PGN", "invalid PGN result");
|
|
127
|
+
}
|
|
128
|
+
const result = results[0];
|
|
129
|
+
if (results.some((value) => value !== result)) {
|
|
130
|
+
throw new ChessError("INVALID_PGN", "PGN result header and marker disagree");
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
function validatePgnFenCounters(pgn) {
|
|
135
|
+
for (const match of withoutPgnComments(pgn).matchAll(/^\s*\[\s*FEN\s+"((?:\\.|[^"\\])*)"\s*\]\s*$/gim)) {
|
|
136
|
+
assertSafeFenCounters(match[1] ?? "");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function validateResultForPosition(chess, result) {
|
|
140
|
+
if (result === undefined)
|
|
141
|
+
return;
|
|
142
|
+
if (chess.isCheckmate()) {
|
|
143
|
+
const expected = chess.turn() === "w" ? "0-1" : "1-0";
|
|
144
|
+
if (result === expected)
|
|
145
|
+
return;
|
|
146
|
+
throw new ChessError("INVALID_PGN", `checkmate result must be ${expected}`);
|
|
147
|
+
}
|
|
148
|
+
if ((chess.isStalemate() || chess.isInsufficientMaterial()) &&
|
|
149
|
+
(result === "1-0" || result === "0-1")) {
|
|
150
|
+
throw new ChessError("INVALID_PGN", "a drawn position cannot have a decisive result");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function pgnOf(chess) {
|
|
154
|
+
const result = chess.isCheckmate()
|
|
155
|
+
? (chess.turn() === "w" ? "0-1" : "1-0")
|
|
156
|
+
: chess.isDraw() && (!chess.getHeaders().Result || chess.getHeaders().Result === "*")
|
|
157
|
+
? "1/2-1/2"
|
|
158
|
+
: undefined;
|
|
159
|
+
if (result === undefined)
|
|
160
|
+
return chess.pgn();
|
|
161
|
+
const snapshot = snapshotChess(chess);
|
|
162
|
+
snapshot.setHeader("Result", result);
|
|
163
|
+
return snapshot.pgn();
|
|
164
|
+
}
|
|
30
165
|
export function parseImportedPgn(pgn) {
|
|
31
166
|
if (Buffer.byteLength(pgn, "utf8") > MAX_PGN_BYTES) {
|
|
32
167
|
throw new ChessError("PGN_TOO_LARGE", `PGN exceeds the ${MAX_PGN_BYTES}-byte limit`);
|
|
33
168
|
}
|
|
169
|
+
validatePgnFenCounters(pgn);
|
|
170
|
+
const result = declaredPgnResult(pgn);
|
|
34
171
|
let chess;
|
|
35
172
|
try {
|
|
36
173
|
chess = new Chess();
|
|
@@ -39,9 +176,11 @@ export function parseImportedPgn(pgn) {
|
|
|
39
176
|
catch {
|
|
40
177
|
throw new ChessError("INVALID_PGN", "invalid or illegal PGN");
|
|
41
178
|
}
|
|
179
|
+
assertSafeFenCounters(chess.fen());
|
|
42
180
|
if (chess.history().length > MAX_PGN_PLIES) {
|
|
43
181
|
throw new ChessError("PGN_TOO_MANY_MOVES", `PGN exceeds the ${MAX_PGN_PLIES}-ply limit`);
|
|
44
182
|
}
|
|
183
|
+
validateResultForPosition(chess, result);
|
|
45
184
|
return chess;
|
|
46
185
|
}
|
|
47
186
|
export function stateOf(chess, revision) {
|
|
@@ -71,8 +210,10 @@ export function stateOf(chess, revision) {
|
|
|
71
210
|
}
|
|
72
211
|
export function parseMove(chess, move) {
|
|
73
212
|
const legal = chess.moves({ verbose: true });
|
|
74
|
-
const
|
|
75
|
-
|
|
213
|
+
const found = legal.find((candidate) => candidate.san === move) ??
|
|
214
|
+
(!/[+#]$/.test(move)
|
|
215
|
+
? legal.find((candidate) => candidate.san.replace(/[+#]$/, "") === move)
|
|
216
|
+
: undefined) ??
|
|
76
217
|
legal.find((candidate) => candidate.lan === move);
|
|
77
218
|
if (!found)
|
|
78
219
|
throw new ChessError("ILLEGAL_MOVE", `illegal move: ${move}`);
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type TransportKind = "stdio" | "http";
|
|
2
|
+
export type CliOptions = {
|
|
3
|
+
transport: TransportKind;
|
|
4
|
+
host: string;
|
|
5
|
+
port: number;
|
|
6
|
+
path: string;
|
|
7
|
+
allowedHosts: string[];
|
|
8
|
+
help: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare const HELP = "Usage: llm-chess-mcp [options]\n\nOptions:\n --transport <stdio|http> Transport to use (default: stdio)\n --http Shortcut for --transport http\n --host <host> HTTP bind host (default: 127.0.0.1)\n --port <port> HTTP listen port (default: 3000)\n --path <path> HTTP endpoint path (default: /mcp)\n --allowed-host <host> Allowed HTTP Host/Origin hostname (repeatable)\n -h, --help Show this help\n";
|
|
11
|
+
export declare function parseCli(args: string[]): CliOptions;
|
package/dist/cli.js
CHANGED
|
@@ -20,6 +20,20 @@ function splitOption(arg) {
|
|
|
20
20
|
const index = arg.indexOf("=");
|
|
21
21
|
return index === -1 ? null : [arg.slice(0, index), arg.slice(index + 1)];
|
|
22
22
|
}
|
|
23
|
+
function isCanonicalHttpPath(path) {
|
|
24
|
+
if (!path.startsWith("/") ||
|
|
25
|
+
path.startsWith("//") ||
|
|
26
|
+
path.includes("?") ||
|
|
27
|
+
path.includes("#")) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return new URL(path, "http://localhost").pathname === path;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
23
37
|
export function parseCli(args) {
|
|
24
38
|
let transport = "stdio";
|
|
25
39
|
let host = "127.0.0.1";
|
|
@@ -89,11 +103,11 @@ export function parseCli(args) {
|
|
|
89
103
|
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
90
104
|
throw new Error("--port must be between 1 and 65535");
|
|
91
105
|
}
|
|
92
|
-
if (!
|
|
106
|
+
if (!isCanonicalHttpPath(path)) {
|
|
93
107
|
throw new Error("--path must be an absolute URL path without query or fragment");
|
|
94
108
|
}
|
|
95
|
-
if (!host || allowedHosts.some((value) => !value)) {
|
|
96
|
-
throw new Error("HTTP hostnames must
|
|
109
|
+
if (!host || allowedHosts.some((value) => !value || /[/?#]/.test(value))) {
|
|
110
|
+
throw new Error("HTTP hostnames must be non-empty hostnames");
|
|
97
111
|
}
|
|
98
112
|
if (transport === "stdio" && hasHttpOption) {
|
|
99
113
|
throw new Error("HTTP options require --transport http");
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { SfLine } from "../types.js";
|
|
2
|
+
export declare const STOCKFISH_FLAVORS: readonly ["full", "lite", "single", "lite-single", "single-lite", "asm"];
|
|
3
|
+
export type StockfishFlavor = (typeof STOCKFISH_FLAVORS)[number];
|
|
4
|
+
export type StockfishEngine = {
|
|
5
|
+
listener: ((line: string) => void) | null;
|
|
6
|
+
sendCommand: (cmd: string) => void;
|
|
7
|
+
terminate: () => void;
|
|
8
|
+
};
|
|
9
|
+
export type StockfishInit = (enginePath: string, cb: (err: Error | null, engine: StockfishEngine) => void) => StockfishEngine;
|
|
10
|
+
type Timeouts = {
|
|
11
|
+
init: number;
|
|
12
|
+
handshake: number;
|
|
13
|
+
analyze: number;
|
|
14
|
+
stopGrace: number;
|
|
15
|
+
};
|
|
16
|
+
export type StockfishOptions = {
|
|
17
|
+
init?: StockfishInit;
|
|
18
|
+
flavor?: string;
|
|
19
|
+
maxQueue?: number;
|
|
20
|
+
timeouts?: Partial<Timeouts>;
|
|
21
|
+
};
|
|
22
|
+
export declare function resolveStockfishFlavor(value?: string): StockfishFlavor;
|
|
23
|
+
export declare class Stockfish {
|
|
24
|
+
private session;
|
|
25
|
+
private queue;
|
|
26
|
+
private queueRunning;
|
|
27
|
+
private queueScheduled;
|
|
28
|
+
private queued;
|
|
29
|
+
private quitGeneration;
|
|
30
|
+
private readonly terminated;
|
|
31
|
+
private readonly initEngine;
|
|
32
|
+
private readonly configuredFlavor;
|
|
33
|
+
private readonly maxQueue;
|
|
34
|
+
private readonly timeouts;
|
|
35
|
+
constructor(options?: StockfishOptions);
|
|
36
|
+
private init;
|
|
37
|
+
private handshake;
|
|
38
|
+
private release;
|
|
39
|
+
private scheduleQueue;
|
|
40
|
+
private removeQueued;
|
|
41
|
+
private enqueue;
|
|
42
|
+
analyze(fen: string, depth: number, multipv: number, signal?: AbortSignal): Promise<SfLine[]>;
|
|
43
|
+
private doAnalyze;
|
|
44
|
+
private failSession;
|
|
45
|
+
private invalidateSession;
|
|
46
|
+
private terminate;
|
|
47
|
+
quit(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
export declare const stockfish: Stockfish;
|
|
50
|
+
export {};
|
|
@@ -48,7 +48,9 @@ function parseWdl(line) {
|
|
|
48
48
|
}
|
|
49
49
|
export class Stockfish {
|
|
50
50
|
session = null;
|
|
51
|
-
queue =
|
|
51
|
+
queue = [];
|
|
52
|
+
queueRunning = false;
|
|
53
|
+
queueScheduled = false;
|
|
52
54
|
queued = 0;
|
|
53
55
|
quitGeneration = 0;
|
|
54
56
|
terminated = new WeakSet();
|
|
@@ -191,14 +193,44 @@ export class Stockfish {
|
|
|
191
193
|
}
|
|
192
194
|
request.abortListener = null;
|
|
193
195
|
}
|
|
196
|
+
scheduleQueue() {
|
|
197
|
+
if (this.queueRunning || this.queueScheduled)
|
|
198
|
+
return;
|
|
199
|
+
this.queueScheduled = true;
|
|
200
|
+
queueMicrotask(() => {
|
|
201
|
+
this.queueScheduled = false;
|
|
202
|
+
if (this.queueRunning)
|
|
203
|
+
return;
|
|
204
|
+
const next = this.queue.shift();
|
|
205
|
+
if (!next)
|
|
206
|
+
return;
|
|
207
|
+
if (next.request.cancelled) {
|
|
208
|
+
this.release(next.request);
|
|
209
|
+
this.scheduleQueue();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
this.queueRunning = true;
|
|
213
|
+
void next.run().finally(() => {
|
|
214
|
+
this.release(next.request);
|
|
215
|
+
this.queueRunning = false;
|
|
216
|
+
this.scheduleQueue();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
removeQueued(request) {
|
|
221
|
+
const index = this.queue.findIndex((item) => item.request === request);
|
|
222
|
+
if (index < 0)
|
|
223
|
+
return false;
|
|
224
|
+
this.queue.splice(index, 1);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
194
227
|
enqueue(request, fn) {
|
|
195
228
|
if (this.queued >= this.maxQueue) {
|
|
196
229
|
return false;
|
|
197
230
|
}
|
|
198
231
|
this.queued++;
|
|
199
|
-
|
|
200
|
-
this.
|
|
201
|
-
void run.finally(() => this.release(request));
|
|
232
|
+
this.queue.push({ request, run: fn });
|
|
233
|
+
this.scheduleQueue();
|
|
202
234
|
return true;
|
|
203
235
|
}
|
|
204
236
|
analyze(fen, depth, multipv, signal) {
|
|
@@ -229,11 +261,18 @@ export class Stockfish {
|
|
|
229
261
|
request.cancelled = true;
|
|
230
262
|
request.cancellation = error;
|
|
231
263
|
if (request.started) {
|
|
232
|
-
request.stop
|
|
264
|
+
if (request.stop)
|
|
265
|
+
request.stop(error);
|
|
266
|
+
else {
|
|
267
|
+
request.reject(error);
|
|
268
|
+
this.release(request);
|
|
269
|
+
}
|
|
233
270
|
}
|
|
234
271
|
else {
|
|
272
|
+
this.removeQueued(request);
|
|
235
273
|
request.reject(error);
|
|
236
274
|
this.release(request);
|
|
275
|
+
this.scheduleQueue();
|
|
237
276
|
}
|
|
238
277
|
};
|
|
239
278
|
request.abortListener = cancel;
|
|
@@ -288,6 +327,7 @@ export class Stockfish {
|
|
|
288
327
|
const byPv = new Map();
|
|
289
328
|
let settled = false;
|
|
290
329
|
let cancellation = null;
|
|
330
|
+
let timeout = null;
|
|
291
331
|
let stopSent = false;
|
|
292
332
|
let stopTimer = null;
|
|
293
333
|
let failTimer = null;
|
|
@@ -323,6 +363,8 @@ export class Stockfish {
|
|
|
323
363
|
const stop = (error, cancelled) => {
|
|
324
364
|
if (cancelled)
|
|
325
365
|
cancellation ??= error;
|
|
366
|
+
else
|
|
367
|
+
timeout ??= error;
|
|
326
368
|
if (stopSent)
|
|
327
369
|
return;
|
|
328
370
|
stopSent = true;
|
|
@@ -363,6 +405,8 @@ export class Stockfish {
|
|
|
363
405
|
else if (line.startsWith("bestmove")) {
|
|
364
406
|
if (cancellation)
|
|
365
407
|
fail(cancellation, false);
|
|
408
|
+
else if (timeout)
|
|
409
|
+
fail(timeout, false);
|
|
366
410
|
else
|
|
367
411
|
succeed();
|
|
368
412
|
}
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function loadEnv(path?: string): void;
|
package/dist/errors.d.ts
ADDED
package/dist/eval.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SfLine } from "./types.js";
|
|
2
|
+
export type Eval = {
|
|
3
|
+
type: "cp";
|
|
4
|
+
value: number;
|
|
5
|
+
} | {
|
|
6
|
+
type: "mate";
|
|
7
|
+
plies: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function toEval(line: SfLine): Eval | null;
|
|
10
|
+
export declare function evalToCp(e: Eval): number;
|
|
11
|
+
export declare function negateEval(e: Eval): Eval;
|
|
12
|
+
export declare const CLASSIFICATION: {
|
|
13
|
+
readonly best: 0;
|
|
14
|
+
readonly excellent: 30;
|
|
15
|
+
readonly good: 80;
|
|
16
|
+
readonly inaccuracy: 150;
|
|
17
|
+
readonly mistake: 300;
|
|
18
|
+
};
|
|
19
|
+
export declare function classifyCpLoss(cpLoss: number): string;
|
|
20
|
+
export type AnalysisLevel = "fast" | "normal" | "deep";
|
|
21
|
+
export declare const ANALYSIS_PRESETS: Record<AnalysisLevel, {
|
|
22
|
+
depth: number;
|
|
23
|
+
multipv: number;
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Chess } from "chess.js";
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
import type { LichessMove } from "./types.js";
|
|
4
|
+
export declare const LICHESS_SPEEDS: readonly ["ultraBullet", "bullet", "blitz", "rapid", "classical", "correspondence"];
|
|
5
|
+
export declare const LICHESS_RATINGS: readonly [0, 1000, 1200, 1400, 1600, 1800, 2000, 2200, 2500];
|
|
6
|
+
export type LichessSpeed = (typeof LICHESS_SPEEDS)[number];
|
|
7
|
+
export type LichessRating = (typeof LICHESS_RATINGS)[number];
|
|
8
|
+
export declare const lichessSpeedSchema: z.ZodEnum<{
|
|
9
|
+
blitz: "blitz";
|
|
10
|
+
bullet: "bullet";
|
|
11
|
+
classical: "classical";
|
|
12
|
+
correspondence: "correspondence";
|
|
13
|
+
rapid: "rapid";
|
|
14
|
+
ultraBullet: "ultraBullet";
|
|
15
|
+
}>;
|
|
16
|
+
export declare const lichessRatingSchema: z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<1000>, z.ZodLiteral<1200>, z.ZodLiteral<1400>, z.ZodLiteral<1600>, z.ZodLiteral<1800>, z.ZodLiteral<2000>, z.ZodLiteral<2200>, z.ZodLiteral<2500>]>;
|
|
17
|
+
export declare const EXPLORER_ATTEMPT_TIMEOUT_MS = 5000;
|
|
18
|
+
export declare const EXPLORER_MAX_ATTEMPTS = 2;
|
|
19
|
+
export declare const EXPLORER_DEFAULT_RETRY_DELAY_MS = 250;
|
|
20
|
+
export declare const EXPLORER_TOTAL_TIMEOUT_MS = 12000;
|
|
21
|
+
export declare const EXPLORER_RATE_LIMIT_COOLDOWN_MS = 60000;
|
|
22
|
+
export declare const EXPLORER_ERROR_KINDS: readonly ["disabled", "invalid_input", "timeout", "network", "auth", "rate_limited", "upstream", "http", "invalid_response"];
|
|
23
|
+
export type ExplorerErrorKind = (typeof EXPLORER_ERROR_KINDS)[number];
|
|
24
|
+
export declare class ExplorerError extends Error {
|
|
25
|
+
readonly kind: ExplorerErrorKind;
|
|
26
|
+
readonly status?: number | undefined;
|
|
27
|
+
readonly reason: ExplorerErrorKind;
|
|
28
|
+
constructor(kind: ExplorerErrorKind, message: string, status?: number | undefined);
|
|
29
|
+
}
|
|
30
|
+
export interface ExplorerResult {
|
|
31
|
+
db: string;
|
|
32
|
+
white: number;
|
|
33
|
+
draws: number;
|
|
34
|
+
black: number;
|
|
35
|
+
moves: LichessMove[];
|
|
36
|
+
opening: {
|
|
37
|
+
eco: string;
|
|
38
|
+
name: string;
|
|
39
|
+
} | null;
|
|
40
|
+
}
|
|
41
|
+
export type ExplorerFetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
42
|
+
export interface ExplorerRequestOptions {
|
|
43
|
+
fetch?: ExplorerFetch;
|
|
44
|
+
limiter?: ExplorerLimiter;
|
|
45
|
+
sleep?: (ms: number) => Promise<void>;
|
|
46
|
+
timeout?: (ms: number) => AbortSignal;
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
now?: () => number;
|
|
49
|
+
token?: string;
|
|
50
|
+
}
|
|
51
|
+
export declare function explorerEnabled(): boolean;
|
|
52
|
+
export interface ExplorerLimiterOptions {
|
|
53
|
+
callerSignal: AbortSignal | undefined;
|
|
54
|
+
deadline: number;
|
|
55
|
+
now: () => number;
|
|
56
|
+
sleep: (ms: number) => Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
export interface ExplorerLimiter {
|
|
59
|
+
readonly pending: number;
|
|
60
|
+
run<T>(options: ExplorerLimiterOptions, request: () => Promise<T>): Promise<T>;
|
|
61
|
+
cooldown(ms: number, now: number): void;
|
|
62
|
+
}
|
|
63
|
+
export declare function createExplorerLimiter(): ExplorerLimiter;
|
|
64
|
+
export declare function openingExplorer(chess: Chess, db: "lichess" | "masters", speeds: readonly string[], ratings: readonly number[], options?: ExplorerRequestOptions): Promise<ExplorerResult>;
|