llm-chess-mcp 0.4.9 → 0.4.10

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.
@@ -5,6 +5,42 @@ const ORIGINAL_PIECES = {
5
5
  r: 2,
6
6
  n: 2,
7
7
  };
8
+ const CANONICAL_PGN_HEADERS = new Map([
9
+ "Event",
10
+ "Site",
11
+ "Date",
12
+ "Round",
13
+ "White",
14
+ "Black",
15
+ "Result",
16
+ "SetUp",
17
+ "FEN",
18
+ ].map((name) => [name.toLowerCase(), name]));
19
+ function restoreHeaders(chess, sourceHeaders) {
20
+ const sourceNames = new Set(sourceHeaders.map(([key]) => key.toLowerCase()));
21
+ for (const key of Object.keys(chess.getHeaders())) {
22
+ if (!sourceNames.has(key.toLowerCase()))
23
+ chess.removeHeader(key);
24
+ }
25
+ const names = new Map();
26
+ for (const key of Object.keys(chess.getHeaders())) {
27
+ const existing = names.get(key.toLowerCase());
28
+ if (existing)
29
+ existing.push(key);
30
+ else
31
+ names.set(key.toLowerCase(), [key]);
32
+ }
33
+ for (const [key, value] of sourceHeaders) {
34
+ const lower = key.toLowerCase();
35
+ const canonical = CANONICAL_PGN_HEADERS.get(lower) ?? key;
36
+ for (const existing of names.get(lower) ?? []) {
37
+ if (existing !== canonical)
38
+ chess.removeHeader(existing);
39
+ }
40
+ chess.setHeader(canonical, value);
41
+ names.set(lower, [canonical]);
42
+ }
43
+ }
8
44
  function squareColor(square) {
9
45
  return ((square.charCodeAt(0) - 97 + Number(square[1])) % 2);
10
46
  }
@@ -146,14 +182,30 @@ export function snapshotChess(chess) {
146
182
  assertSafeFenCounters(initialFen);
147
183
  const snapshot = new Chess(initialFen);
148
184
  assertLegalPosition(snapshot);
149
- const comments = new Map(chess.getComments().map(({ fen, comment }) => [fen, comment]));
150
- for (const [key, value] of Object.entries(chess.getHeaders())) {
151
- snapshot.setHeader(key, value);
185
+ const comments = new Map(chess.getComments().map(({ fen, comment }) => [
186
+ fen,
187
+ /[{}]/.test(comment) ? comment.replace(/[\r\n]+/g, " ") : comment,
188
+ ]));
189
+ const sourceHeaders = Object.entries(chess.getHeaders());
190
+ const unsafeComments = [...comments.values()].some((comment) => /[{}]/.test(comment));
191
+ let markerPrefix = "\uE000";
192
+ if (unsafeComments) {
193
+ const occupied = [...sourceHeaders.flat(), ...comments.values()].join("\u0000");
194
+ while (occupied.includes(markerPrefix))
195
+ markerPrefix += "\uE001";
152
196
  }
197
+ const markerComments = [];
153
198
  const restoreComment = () => {
154
199
  const comment = comments.get(snapshot.fen());
155
- if (comment !== undefined)
200
+ if (comment === undefined)
201
+ return;
202
+ if (!unsafeComments || !/[{}]/.test(comment)) {
156
203
  snapshot.setComment(comment);
204
+ return;
205
+ }
206
+ const marker = `${markerPrefix}${markerComments.length}${markerPrefix}`;
207
+ markerComments.push(comment);
208
+ snapshot.setComment(marker);
157
209
  };
158
210
  restoreComment();
159
211
  for (const move of history) {
@@ -161,5 +213,31 @@ export function snapshotChess(chess) {
161
213
  restoreComment();
162
214
  }
163
215
  assertSafeFenCounters(snapshot.fen());
164
- return snapshot;
216
+ if (!unsafeComments) {
217
+ restoreHeaders(snapshot, sourceHeaders);
218
+ return snapshot;
219
+ }
220
+ const escapedPrefix = markerPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
221
+ const marker = new RegExp(`\\{${escapedPrefix}(\\d+)${escapedPrefix}\\}`, "g");
222
+ const pgn = snapshot.pgn().replace(marker, (_match, index) => {
223
+ return `;${markerComments[Number(index)]}\n`;
224
+ });
225
+ const restored = new Chess();
226
+ restored.loadPgn(pgn);
227
+ const restoredHistory = restored.history({ verbose: true });
228
+ while (restored.undo()) { }
229
+ const restoreSafeComment = () => {
230
+ const comment = comments.get(restored.fen());
231
+ if (comment !== undefined && !/[{}]/.test(comment)) {
232
+ restored.setComment(comment);
233
+ }
234
+ };
235
+ restoreSafeComment();
236
+ for (const move of restoredHistory) {
237
+ restored.move(moveDescriptor(move));
238
+ restoreSafeComment();
239
+ }
240
+ restoreHeaders(restored, sourceHeaders);
241
+ assertSafeFenCounters(restored.fen());
242
+ return restored;
165
243
  }
package/dist/pgn.js CHANGED
@@ -278,12 +278,14 @@ function pgnText(chess) {
278
278
  for (const { comment } of comments)
279
279
  assertPgnTokenSize(comment);
280
280
  const raw = chess.pgn();
281
- const separator = raw.indexOf("\n\n");
282
- let movetext = headers.length === 0
283
- ? raw
284
- : separator < 0
285
- ? ""
286
- : raw.slice(separator + 2);
281
+ let movetextStart = 0;
282
+ for (let index = 0; index < headers.length; index += 1) {
283
+ const lineEnd = raw.indexOf("\n", movetextStart);
284
+ movetextStart = lineEnd < 0 ? raw.length : lineEnd + 1;
285
+ }
286
+ if (headers.length > 0 && raw[movetextStart] === "\n")
287
+ movetextStart += 1;
288
+ let movetext = raw.slice(movetextStart);
287
289
  const unsafeComments = [
288
290
  ...new Set(comments
289
291
  .map(({ comment }) => comment)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-chess-mcp",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "description": "MCP server that lets an LLM analyze, judge, and choose chess moves (Stockfish + Maia3 + Lichess)",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",