@chessceo/mcp 0.50.0 → 0.50.1
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/dist/analysis/eval_cache.js +138 -0
- package/dist/analysis/response.js +13 -3
- package/dist/index.js +68 -55
- package/dist/prep/mutations.js +15 -3
- package/dist/tools.js +2 -2
- package/docs/engine-usage.md +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Per-user memory of cloud-engine results, so analysis is never lost.
|
|
2
|
+
//
|
|
3
|
+
// Claude often analyses loose FENs or move lines first and adds the moves
|
|
4
|
+
// to a file later. Without this, those nodes land with no [%ceo-eval] and
|
|
5
|
+
// the final pass has to analyse them again. Every result that comes back
|
|
6
|
+
// from the analyse endpoint is remembered here, keyed by user id and
|
|
7
|
+
// position (positionKey, same as transpositions), and every file write
|
|
8
|
+
// (add_move, add_line, apply_mutations, ...) fills nodes whose position is
|
|
9
|
+
// remembered and whose stored eval for that engine is missing or shallower.
|
|
10
|
+
//
|
|
11
|
+
// Keyed by the user id from GET /api/agent/me, not by the token: a hosted
|
|
12
|
+
// OAuth access token rotates hourly, the user id doesn't, and one user's
|
|
13
|
+
// analysed positions (their prep) must never land in another user's file.
|
|
14
|
+
// In memory only: a process restart forgets, which costs a re-analysis,
|
|
15
|
+
// never a wrong eval. Every function here is best-effort and never throws
|
|
16
|
+
// into the write path.
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { authedRequest, resolveAuthHeader } from "../http.js";
|
|
19
|
+
import { buildIdIndex, positionKey, resolveNodeId } from "../pgn/paths.js";
|
|
20
|
+
import { setCeoEval } from "../pgn/mutations.js";
|
|
21
|
+
import { STORED_EVAL_KEYS } from "../pgn/types.js";
|
|
22
|
+
const TTL_MS = 24 * 60 * 60 * 1000;
|
|
23
|
+
const MAX_POSITIONS_PER_USER = 20_000;
|
|
24
|
+
const MAX_KNOWN_TOKENS = 1_000;
|
|
25
|
+
const cacheByUser = new Map();
|
|
26
|
+
// sha256(Authorization header) → user id. Never the raw token.
|
|
27
|
+
const userByToken = new Map();
|
|
28
|
+
async function currentUserId() {
|
|
29
|
+
const auth = resolveAuthHeader();
|
|
30
|
+
if (!auth)
|
|
31
|
+
return null;
|
|
32
|
+
const key = createHash("sha256").update(auth).digest("hex");
|
|
33
|
+
let p = userByToken.get(key);
|
|
34
|
+
if (!p) {
|
|
35
|
+
if (userByToken.size >= MAX_KNOWN_TOKENS)
|
|
36
|
+
userByToken.clear();
|
|
37
|
+
p = authedRequest("GET", "/api/agent/me")
|
|
38
|
+
.then(r => {
|
|
39
|
+
const id = r?.userId;
|
|
40
|
+
return typeof id === "string" && id ? id : null;
|
|
41
|
+
})
|
|
42
|
+
.catch(() => null);
|
|
43
|
+
userByToken.set(key, p);
|
|
44
|
+
}
|
|
45
|
+
const id = await p;
|
|
46
|
+
// Don't pin a failure (backend blip, old backend): ask again next time.
|
|
47
|
+
if (!id)
|
|
48
|
+
userByToken.delete(key);
|
|
49
|
+
return id;
|
|
50
|
+
}
|
|
51
|
+
const deeper = (a, b) => (a ?? 0) > (b ?? 0);
|
|
52
|
+
// Remember fresh results. Per engine, a deeper result replaces a shallower
|
|
53
|
+
// one; equal depth takes the newer.
|
|
54
|
+
export async function rememberEvals(entries) {
|
|
55
|
+
try {
|
|
56
|
+
if (entries.length === 0)
|
|
57
|
+
return;
|
|
58
|
+
const user = await currentUserId();
|
|
59
|
+
if (!user)
|
|
60
|
+
return;
|
|
61
|
+
let cache = cacheByUser.get(user);
|
|
62
|
+
if (!cache) {
|
|
63
|
+
cache = new Map();
|
|
64
|
+
cacheByUser.set(user, cache);
|
|
65
|
+
}
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
for (const { fen, ev } of entries) {
|
|
68
|
+
const key = positionKey(fen);
|
|
69
|
+
const merged = { ...(cache.get(key) ?? {}) };
|
|
70
|
+
for (const k of STORED_EVAL_KEYS) {
|
|
71
|
+
const fresh = ev[k];
|
|
72
|
+
if (!fresh)
|
|
73
|
+
continue;
|
|
74
|
+
const old = merged[k];
|
|
75
|
+
const oldLive = old && now - old.at < TTL_MS;
|
|
76
|
+
if (!oldLive || !deeper(old.ev.depth, fresh.depth))
|
|
77
|
+
merged[k] = { ev: fresh, at: now };
|
|
78
|
+
}
|
|
79
|
+
// Re-insert so Map order is least-recently-written first.
|
|
80
|
+
cache.delete(key);
|
|
81
|
+
cache.set(key, merged);
|
|
82
|
+
}
|
|
83
|
+
while (cache.size > MAX_POSITIONS_PER_USER) {
|
|
84
|
+
const oldest = cache.keys().next().value;
|
|
85
|
+
if (oldest === undefined)
|
|
86
|
+
break;
|
|
87
|
+
cache.delete(oldest);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// best-effort
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Fill remembered evals into the file: every node whose position was
|
|
95
|
+
// analysed gets the engines it lacks, or a deeper read than it holds.
|
|
96
|
+
// Returns the file unchanged (and filled = 0) on any problem. `skipIds`
|
|
97
|
+
// are nodes whose eval the caller set or cleared on purpose in this write.
|
|
98
|
+
export async function fillFromCache(file, skipIds = new Set()) {
|
|
99
|
+
try {
|
|
100
|
+
const user = await currentUserId();
|
|
101
|
+
const cache = user ? cacheByUser.get(user) : undefined;
|
|
102
|
+
if (!cache || cache.size === 0)
|
|
103
|
+
return { file, filled: 0 };
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
const patches = [];
|
|
106
|
+
const walk = (node) => {
|
|
107
|
+
if (!skipIds.has(node.id)) {
|
|
108
|
+
const hit = cache.get(positionKey(node.fen));
|
|
109
|
+
if (hit) {
|
|
110
|
+
const patch = {};
|
|
111
|
+
for (const k of STORED_EVAL_KEYS) {
|
|
112
|
+
const c = hit[k];
|
|
113
|
+
if (!c || now - c.at >= TTL_MS)
|
|
114
|
+
continue;
|
|
115
|
+
const stored = node.ceoEval?.[k];
|
|
116
|
+
if (!stored || deeper(c.ev.depth, stored.depth))
|
|
117
|
+
patch[k] = c.ev;
|
|
118
|
+
}
|
|
119
|
+
if (patch.sf || patch.lc0 || patch.human)
|
|
120
|
+
patches.push({ id: node.id, ev: patch });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const c of node.children)
|
|
124
|
+
walk(c);
|
|
125
|
+
};
|
|
126
|
+
walk(file.root);
|
|
127
|
+
if (patches.length === 0)
|
|
128
|
+
return { file, filled: 0 };
|
|
129
|
+
const idIndex = buildIdIndex(file.root);
|
|
130
|
+
let out = file;
|
|
131
|
+
for (const p of patches)
|
|
132
|
+
out = setCeoEval(out, resolveNodeId(idIndex, p.id), p.ev).file;
|
|
133
|
+
return { file: out, filled: patches.length };
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return { file, filled: 0 };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// to SAN and WDL to percent, and turns a result into the stored ceoEval.
|
|
6
6
|
import { Chess } from "chess.js";
|
|
7
7
|
import { authedRequest } from "../http.js";
|
|
8
|
+
import { rememberEvals } from "./eval_cache.js";
|
|
8
9
|
export const ENGINES = ["stockfish", "lc0", "human"];
|
|
9
10
|
// Engine name → key in the stored ceoEval / [%ceo-eval] escape.
|
|
10
11
|
export const STORED_KEY = { stockfish: "sf", lc0: "lc0", human: "human" };
|
|
@@ -80,9 +81,18 @@ export async function analysePositions(positions, opts) {
|
|
|
80
81
|
const raw = (await authedRequest("POST", "/api/agent/cloud-engines/analyse", body));
|
|
81
82
|
if (!raw || !Array.isArray(raw.positions))
|
|
82
83
|
throw new Error("unexpected response from the analyse endpoint");
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
const remembered = [];
|
|
85
|
+
for (const r of raw.positions) {
|
|
86
|
+
if (!r || typeof r.id !== "string")
|
|
87
|
+
continue;
|
|
88
|
+
byId.set(r.id, r);
|
|
89
|
+
const ev = resultToStoredEval(convertPositionResult(structuredClone(r)));
|
|
90
|
+
if (ev)
|
|
91
|
+
remembered.push({ fen: r.fen, ev });
|
|
92
|
+
}
|
|
93
|
+
// Remembered per chunk, so a later chunk failing loses nothing already
|
|
94
|
+
// paid for; a later write fills these into the file (eval_cache.ts).
|
|
95
|
+
await rememberEvals(remembered);
|
|
86
96
|
}
|
|
87
97
|
// A position the backend didn't answer comes back with no engines, so
|
|
88
98
|
// callers report it as failed rather than shifting other results.
|
package/dist/index.js
CHANGED
|
@@ -138,6 +138,7 @@ const ENGINE_WORKFLOW = [
|
|
|
138
138
|
"2. Ask: show the user the price of anything you would start and get a yes. Rentals bill per minute until stopped.",
|
|
139
139
|
"3. Start: start_cloud_engine({machine_type}) per missing engine. Each SKU runs one engine; the engine comes from the SKU. Wait until list_cloud_engines shows it running (static engines are instant, others ~1-5 min).",
|
|
140
140
|
"4. Analyse: cloud_analyse({fens | lines | file_id+node_ids, engines}) for up to 10 positions, each engine on its own rental, in parallel. auto_evaluate({id, engines}) for a whole file or subtree: fills only the engines each node is missing and saves as it goes. deep_analyse({engine, ...}) for one long think.",
|
|
141
|
+
" Nothing analysed is lost: every result is remembered for 24 h, and any later file write (add_move, add_line, apply_mutations, ...) fills nodes reaching an analysed position with that eval (`evals_filled` in the response). So analyse loose lines freely, then add the keepers.",
|
|
141
142
|
"5. Read back: quote_engine_eval gives the stored sf (cp/mate), lc0 and human (win/draw/loss %) per node.",
|
|
142
143
|
"6. Stop: stop_cloud_engine when the work is done, unless the user wants it kept running.",
|
|
143
144
|
].join("\n");
|
|
@@ -631,72 +632,80 @@ Preparation workflow — follow the steps in order and be explicit about which t
|
|
|
631
632
|
Don't just dump data. Reason about it. Cite specific numbers (game counts, win rates, dates) so the user can trust your conclusions.`;
|
|
632
633
|
};
|
|
633
634
|
// ── Server wiring ──────────────────────────────────────────────────
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
635
|
+
// One Server per connection. HTTP mode is stateless (a transport per
|
|
636
|
+
// request), and a Server holds one transport at a time, so sharing a
|
|
637
|
+
// single Server made overlapping requests fail with "Already connected
|
|
638
|
+
// to a transport" (two users calling at once, or a client's notification
|
|
639
|
+
// racing its previous request).
|
|
640
|
+
function createServer() {
|
|
641
|
+
const server = new Server({ name: "chessceo-mcp", version: process.env.npm_package_version ?? "0.1.0" }, { capabilities: { tools: {}, prompts: {} } });
|
|
642
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
643
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: PROMPTS }));
|
|
644
|
+
server.setRequestHandler(GetPromptRequestSchema, async (req) => {
|
|
645
|
+
const { name, arguments: args } = req.params;
|
|
646
|
+
const promptArgs = {};
|
|
647
|
+
if (args)
|
|
648
|
+
for (const [k, v] of Object.entries(args))
|
|
649
|
+
promptArgs[k] = String(v);
|
|
650
|
+
let text;
|
|
651
|
+
switch (name) {
|
|
652
|
+
case "prepare_for_game":
|
|
653
|
+
text = PREP_WORKFLOW(promptArgs);
|
|
654
|
+
break;
|
|
655
|
+
case "scout_player": {
|
|
656
|
+
const p = promptArgs.player ?? "the player";
|
|
657
|
+
text = `Produce a scouting report on ${p}. Steps:
|
|
651
658
|
1. \`search_player\` to get their FIDE ID.
|
|
652
659
|
2. \`get_player_profile\` — pull rating history, career splits by color and time control, opening repertoire, opponent analysis, top events, notable wins and losses.
|
|
653
660
|
3. Weight the data: recent (last 12-24 months) > older, classical OTB > rapid/blitz > online.
|
|
654
661
|
4. \`prepare_opponent\` twice (once per colour, or once with two sources), then \`get_prep_position(session_token, node_id="r")\` to summarise their opening choices with actual frequencies and win rates. Filter with \`start_month\` if you only care about their current repertoire.
|
|
655
662
|
5. Deliver: current strength, characteristic openings, one-sentence style read, biggest wins, biggest losses / recurring weakness. Cite the numbers.`;
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
case "engine_usage_primer":
|
|
666
|
+
text = ENGINE_USAGE_DOC;
|
|
667
|
+
break;
|
|
668
|
+
case "prep_strategy_primer":
|
|
669
|
+
text = PREP_STRATEGY_DOC;
|
|
670
|
+
break;
|
|
671
|
+
case "head_to_head_briefing": {
|
|
672
|
+
const a = promptArgs.player_a ?? "player A";
|
|
673
|
+
const b = promptArgs.player_b ?? "player B";
|
|
674
|
+
text = `Briefing on the ${a} vs ${b} history. Steps:
|
|
668
675
|
1. Resolve both FIDE IDs with \`search_player\`.
|
|
669
676
|
2. \`get_head_to_head\` for the pair — pull overall + per-color W/D/L (from ${a}'s perspective), splits by time format, first / last meeting, most-played openings between them, average game length.
|
|
670
677
|
3. Read the pattern: who has the edge, in which colour, in which time format. Which openings decide the meetings? Anything unusual — very drawish, very sharp, big rating gap?
|
|
671
678
|
4. If either player is currently live in a tournament, note it with \`list_player_live_tournaments\`.
|
|
672
679
|
5. Deliver a one-paragraph read: score, dominant openings, one-line style clash, current form.`;
|
|
673
|
-
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
default:
|
|
683
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
674
684
|
}
|
|
675
|
-
default:
|
|
676
|
-
throw new Error(`Unknown prompt: ${name}`);
|
|
677
|
-
}
|
|
678
|
-
return {
|
|
679
|
-
description: `chessceo prompt: ${name}`,
|
|
680
|
-
messages: [
|
|
681
|
-
{ role: "user", content: { type: "text", text } },
|
|
682
|
-
],
|
|
683
|
-
};
|
|
684
|
-
});
|
|
685
|
-
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
686
|
-
const { name, arguments: args } = req.params;
|
|
687
|
-
try {
|
|
688
|
-
const result = await callTool(name, (args ?? {}));
|
|
689
|
-
return {
|
|
690
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
catch (err) {
|
|
694
685
|
return {
|
|
695
|
-
|
|
696
|
-
|
|
686
|
+
description: `chessceo prompt: ${name}`,
|
|
687
|
+
messages: [
|
|
688
|
+
{ role: "user", content: { type: "text", text } },
|
|
689
|
+
],
|
|
697
690
|
};
|
|
698
|
-
}
|
|
699
|
-
|
|
691
|
+
});
|
|
692
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
693
|
+
const { name, arguments: args } = req.params;
|
|
694
|
+
try {
|
|
695
|
+
const result = await callTool(name, (args ?? {}));
|
|
696
|
+
return {
|
|
697
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
catch (err) {
|
|
701
|
+
return {
|
|
702
|
+
isError: true,
|
|
703
|
+
content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
return server;
|
|
708
|
+
}
|
|
700
709
|
// ── Transport selection ────────────────────────────────────────────
|
|
701
710
|
//
|
|
702
711
|
// Two modes:
|
|
@@ -721,7 +730,7 @@ const arg = (name, def) => {
|
|
|
721
730
|
};
|
|
722
731
|
const transportKind = (arg("transport", process.env.MCP_TRANSPORT ?? "stdio") ?? "stdio").toLowerCase();
|
|
723
732
|
if (transportKind === "stdio") {
|
|
724
|
-
await
|
|
733
|
+
await createServer().connect(new StdioServerTransport());
|
|
725
734
|
}
|
|
726
735
|
else if (transportKind === "http" || transportKind === "streamable-http") {
|
|
727
736
|
const port = Number(arg("http-port", process.env.MCP_HTTP_PORT ?? "8080"));
|
|
@@ -819,7 +828,11 @@ else if (transportKind === "http" || transportKind === "streamable-http") {
|
|
|
819
828
|
sessionIdGenerator: undefined,
|
|
820
829
|
enableJsonResponse: true,
|
|
821
830
|
});
|
|
822
|
-
|
|
831
|
+
const server = createServer();
|
|
832
|
+
res.on("close", () => {
|
|
833
|
+
void transport.close();
|
|
834
|
+
void server.close();
|
|
835
|
+
});
|
|
823
836
|
await server.connect(transport);
|
|
824
837
|
// Forward the caller's Authorization header down to tool handlers so
|
|
825
838
|
// they can attach it when calling authenticated backend endpoints.
|
package/dist/prep/mutations.js
CHANGED
|
@@ -19,6 +19,7 @@ import { exportPGN } from "../pgn/exporter.js";
|
|
|
19
19
|
import { buildIdIndex, NodeIdError, PathError, resolveNodeId, ROOT_ID, } from "../pgn/paths.js";
|
|
20
20
|
import { addLine, addMove, deleteSubtree, MutationError, promoteVariation, setAnnotations, setCeoEval, setComment, setNags, setTag, } from "../pgn/mutations.js";
|
|
21
21
|
import { getNodeByPath } from "../analysis/file_handle.js";
|
|
22
|
+
import { fillFromCache } from "../analysis/eval_cache.js";
|
|
22
23
|
import { commentAntiPatterns, longLineWarning, noDescribeWarning, noStatsCheckWarning, positionalNagOnIntermediateWarning, } from "../warnings.js";
|
|
23
24
|
// Extract a node id from the args. Accepts either `node_id` or a
|
|
24
25
|
// `parent_id` alias for the add-style tools. Throws with a helpful
|
|
@@ -137,10 +138,17 @@ export async function applyBatchMutations(args) {
|
|
|
137
138
|
throw new Error(`mutation #${i} (${String(op.op)}) failed: ${msg}`);
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
|
-
|
|
141
|
+
// An explicit set_ceo_eval (including a clear) wins over remembered evals.
|
|
142
|
+
const explicit = new Set();
|
|
143
|
+
mutations.forEach((op, i) => {
|
|
144
|
+
if (op.op === "set_ceo_eval")
|
|
145
|
+
explicit.add(results[i].node_id);
|
|
146
|
+
});
|
|
147
|
+
const filled = await fillFromCache(file, explicit);
|
|
148
|
+
const newPgn = exportPGN(filled.file);
|
|
141
149
|
const expected = typeof args.expected_version === "number" ? args.expected_version : g.version;
|
|
142
150
|
const saved = await saveGame(id, newPgn, expected);
|
|
143
|
-
return { ok: true, results, version: saved.version };
|
|
151
|
+
return { ok: true, results, ...(filled.filled > 0 ? { evals_filled: filled.filled } : {}), version: saved.version };
|
|
144
152
|
}
|
|
145
153
|
// Load-mutate-save: fetch current PGN, parse, apply mutation, re-export,
|
|
146
154
|
// save with optimistic lock. Auto-saves so every tool call is atomic;
|
|
@@ -162,7 +170,10 @@ export async function applyMutation(args, mutator) {
|
|
|
162
170
|
}
|
|
163
171
|
throw err;
|
|
164
172
|
}
|
|
165
|
-
|
|
173
|
+
// Nodes whose position the cloud engines already analysed this session
|
|
174
|
+
// get that eval now (eval_cache.ts), so a final pass has less to do.
|
|
175
|
+
const filled = await fillFromCache(result.file);
|
|
176
|
+
const newPgn = exportPGN(filled.file);
|
|
166
177
|
const expected = typeof args.expected_version === "number" ? args.expected_version : g.version;
|
|
167
178
|
const saved = await saveGame(id, newPgn, expected);
|
|
168
179
|
return {
|
|
@@ -171,6 +182,7 @@ export async function applyMutation(args, mutator) {
|
|
|
171
182
|
...(result.results !== undefined ? { line: result.results } : {}),
|
|
172
183
|
...(result.warning ? { warning: result.warning } : {}),
|
|
173
184
|
...(result.warnings && result.warnings.length > 0 ? { warnings: result.warnings } : {}),
|
|
185
|
+
...(filled.filled > 0 ? { evals_filled: filled.filled } : {}),
|
|
174
186
|
version: saved.version,
|
|
175
187
|
};
|
|
176
188
|
}
|
package/dist/tools.js
CHANGED
|
@@ -314,7 +314,7 @@ export const TOOLS = [
|
|
|
314
314
|
"Positions: `fens` (list), `lines` (list of SAN move sequences from `fen` or the start), `file_id` + `node_ids` (list), or a single `fen` / `moves` / `file_id`+`node_id`. Examples: analyse three FENs with Stockfish → `{fens: [a, b, c], engines: [\"stockfish\"]}`; with the human engine and Stockfish → `engines: [\"stockfish\", \"human\"]`. For a whole file or subtree use auto_evaluate instead.\n\n" +
|
|
315
315
|
"Scores: `scoreCp` and `mate` are White's point of view (+20 = White +0.20; mate +5 = White mates in 5). lc0 and human lines carry `wdl`: win/draw/loss percent, White's point of view. Read the response's `eval_scale` before calling a position equal.\n\n" +
|
|
316
316
|
"GROUNDING: every claim about a position must trace back to engine output from this session. Don't invent evaluations, best moves or variations. When you have no data for a position, run it or say so.\n\n" +
|
|
317
|
-
"Storing: with `file_id`, each result is merged into the `ceoEval` of every node that reaches that position (`stored_on` in the response), keeping other engines' stored reads. quote_engine_eval cites them later.\n\n" +
|
|
317
|
+
"Storing: with `file_id`, each result is merged into the `ceoEval` of every node that reaches that position (`stored_on` in the response), keeping other engines' stored reads. quote_engine_eval cites them later. Without `file_id` nothing is lost either: results are remembered for 24 h and filled into any node reaching that position the next time you write to a file (`evals_filled` in add_move / add_line / apply_mutations responses).\n\n" +
|
|
318
318
|
"Contempt (`contempt`, lc0 only): signed -100..100, positive favours White. Use it to find ideas (e.g. -20 makes Black play for a win). Never quote a contempt eval as objective.\n\n" +
|
|
319
319
|
"PVs are capped at 6 plies (`pv_truncated: true` when cut). To see further, analyse the position at the end of the line; raise `pv_max_plies` only to verify a forcing line. Don't paste PVs into add_line as prep.\n\n" +
|
|
320
320
|
"Needs running rentals for the engines you ask for (see ensure_engines). Costs money while rentals run; use get_position_stats for casual questions.",
|
|
@@ -442,7 +442,7 @@ export const TOOLS = [
|
|
|
442
442
|
name: "list_transpositions",
|
|
443
443
|
description: "Group every position in a prep file that appears more than once — the same piece placement + side-to-move + castling rights reached by different move orders. Chess move orders diverge and re-converge constantly (1.d4 Nf6 2.c4 e6 3.Nc3 vs 1.c4 e6 2.Nc3 Nf6 3.d4 land on the same position); if you analyse both branches independently or write the same commentary twice, you're wasting engine time and inviting inconsistency.\n\n" +
|
|
444
444
|
"Call this BEFORE `auto_evaluate` on a big subtree to see how much work will actually be new, and BEFORE writing prose to know which nodes can share a comment or should point at each other with 'transposes to line X'.\n\n" +
|
|
445
|
-
"Note: engine evals auto-propagate — when `cloud_analyse({file_id, node_id})` stores `ceoEval` on a node, it also stamps every transposition of that position in the same file (see the response's `
|
|
445
|
+
"Note: engine evals auto-propagate — when `cloud_analyse({file_id, node_id})` stores `ceoEval` on a node, it also stamps every transposition of that position in the same file (see the response's `stored_on`). And `auto_evaluate` analyses each position once and skips nodes that already hold every requested engine. So detection is cheap AND propagation is automatic; this tool is for prose planning and one-shot audits, not for gating engine work.\n\n" +
|
|
446
446
|
"Response: `{ file_id, group_count, node_count, groups: [{ position_key, size, node_ids, sans }] }`. `position_key` is the 3-field FEN prefix used as the match key; `size` is how many nodes share it; `sans` are the moves that led to each occurrence (parallel with `node_ids`, DFS order — first entry is the earliest/mainline-preferred occurrence). Only groups with size ≥ 2 are returned; sorted by size descending.",
|
|
447
447
|
inputSchema: {
|
|
448
448
|
type: "object",
|
package/docs/engine-usage.md
CHANGED
|
@@ -205,6 +205,7 @@ Every analysis needs a running rental for each engine you ask for. Each rental r
|
|
|
205
205
|
3. **Start:** `start_cloud_engine({machine_type})` for each missing engine. The engine comes from the SKU. Static engines are ready at once; others take ~1-5 min (`list_cloud_engines` shows when).
|
|
206
206
|
4. **Analyse:**
|
|
207
207
|
- A few positions: `cloud_analyse` with `fens`, `lines`, or `file_id`+`node_ids` (up to 10 per call), e.g. `{fens: [a, b, c], engines: ["stockfish", "human"]}`.
|
|
208
|
+
- Loose FENs or lines first, file later: fine. Every result is remembered for 24 h (per user), and the next write to a file fills each node that reaches an analysed position (`evals_filled` in the write's response). A deeper stored eval is never replaced by a shallower one.
|
|
208
209
|
- A whole file or subtree: `auto_evaluate({id, engines})`. It runs only the engines each node is missing, analyses transpositions once, and saves as it goes. Adding an engine later runs just that engine.
|
|
209
210
|
- One critical position deep: `deep_analyse`.
|
|
210
211
|
5. **Read back:** `quote_engine_eval` returns the stored evals per node.
|
package/package.json
CHANGED