chesspal 0.1.0 → 0.2.0

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.
Files changed (209) hide show
  1. package/README.md +45 -82
  2. package/dist/cli/chat-hook.js +1 -1
  3. package/dist/cli/command-line.d.ts +1 -2
  4. package/dist/cli/command-line.js +2 -3
  5. package/dist/commands/agents.d.ts +26 -0
  6. package/dist/commands/agents.js +72 -0
  7. package/dist/commands/ai-move.d.ts +5 -14
  8. package/dist/commands/ai-move.js +28 -35
  9. package/dist/commands/ai-say.d.ts +3 -1
  10. package/dist/commands/ai-say.js +24 -6
  11. package/dist/commands/analyse-game.d.ts +4 -0
  12. package/dist/commands/analyse-game.js +43 -16
  13. package/dist/commands/annotate-game.d.ts +4 -0
  14. package/dist/commands/annotate-game.js +65 -12
  15. package/dist/commands/answer-lines.d.ts +28 -0
  16. package/dist/commands/answer-lines.js +144 -0
  17. package/dist/commands/coach.d.ts +45 -0
  18. package/dist/commands/coach.js +344 -0
  19. package/dist/commands/context.d.ts +16 -5
  20. package/dist/commands/context.js +1 -0
  21. package/dist/commands/delete-game.d.ts +7 -0
  22. package/dist/commands/delete-game.js +30 -0
  23. package/dist/commands/delete-review.d.ts +8 -0
  24. package/dist/commands/delete-review.js +34 -0
  25. package/dist/commands/drawings.d.ts +0 -2
  26. package/dist/commands/drawings.js +5 -3
  27. package/dist/commands/end-game.js +5 -2
  28. package/dist/commands/export-game.d.ts +1 -1
  29. package/dist/commands/export-game.js +4 -3
  30. package/dist/commands/game-summary.d.ts +21 -38
  31. package/dist/commands/game-summary.js +30 -167
  32. package/dist/commands/index.d.ts +15 -0
  33. package/dist/commands/index.js +47 -2
  34. package/dist/commands/init.d.ts +3 -3
  35. package/dist/commands/init.js +4 -4
  36. package/dist/commands/lead-input.d.ts +8 -0
  37. package/dist/commands/lead-input.js +21 -0
  38. package/dist/commands/look.js +4 -1
  39. package/dist/commands/new-game.d.ts +2 -3
  40. package/dist/commands/new-game.js +7 -4
  41. package/dist/commands/play-move.d.ts +8 -15
  42. package/dist/commands/play-move.js +5 -13
  43. package/dist/commands/player-move.d.ts +2 -6
  44. package/dist/commands/player-move.js +13 -6
  45. package/dist/commands/player-say.d.ts +1 -1
  46. package/dist/commands/player-say.js +1 -1
  47. package/dist/commands/ponder.d.ts +10 -0
  48. package/dist/commands/ponder.js +33 -21
  49. package/dist/commands/position.d.ts +1 -1
  50. package/dist/commands/position.js +3 -1
  51. package/dist/commands/profiles.d.ts +13 -4
  52. package/dist/commands/profiles.js +49 -15
  53. package/dist/commands/registry.d.ts +8 -3
  54. package/dist/commands/registry.js +12 -11
  55. package/dist/commands/say.d.ts +1 -1
  56. package/dist/commands/say.js +16 -16
  57. package/dist/commands/speech.d.ts +8 -0
  58. package/dist/commands/speech.js +20 -0
  59. package/dist/commands/status.d.ts +9 -3
  60. package/dist/commands/status.js +40 -15
  61. package/dist/commands/study-games.js +1 -1
  62. package/dist/commands/study.js +3 -19
  63. package/dist/core/facts.js +15 -15
  64. package/dist/core/game.d.ts +3 -1
  65. package/dist/core/game.js +8 -4
  66. package/dist/core/illegal.js +1 -1
  67. package/dist/core/index.d.ts +5 -4
  68. package/dist/core/index.js +5 -4
  69. package/dist/core/lead.d.ts +15 -0
  70. package/dist/core/lead.js +40 -0
  71. package/dist/core/levels.d.ts +1 -1
  72. package/dist/core/levels.js +1 -1
  73. package/dist/core/moods.d.ts +0 -2
  74. package/dist/core/moods.js +2 -2
  75. package/dist/core/openings.d.ts +0 -2
  76. package/dist/core/openings.js +1 -1
  77. package/dist/core/profile.js +1 -1
  78. package/dist/core/record.d.ts +1 -1
  79. package/dist/core/record.js +1 -1
  80. package/dist/core/review.d.ts +9 -3
  81. package/dist/core/review.js +29 -9
  82. package/dist/games/analysis.d.ts +1 -1
  83. package/dist/games/archive.d.ts +24 -3
  84. package/dist/games/archive.js +96 -22
  85. package/dist/games/coach.d.ts +57 -0
  86. package/dist/games/coach.js +144 -0
  87. package/dist/games/current-game.d.ts +2 -0
  88. package/dist/games/current-game.js +7 -1
  89. package/dist/games/files.js +1 -0
  90. package/dist/games/index.d.ts +4 -3
  91. package/dist/games/index.js +4 -3
  92. package/dist/games/talk.d.ts +1 -5
  93. package/dist/games/talk.js +14 -14
  94. package/dist/home/open-folder.js +1 -1
  95. package/dist/host-files/{ponder-agents.d.ts → agents.d.ts} +10 -2
  96. package/dist/host-files/agents.js +53 -0
  97. package/dist/host-files/host-files.d.ts +7 -2
  98. package/dist/host-files/host-files.js +27 -12
  99. package/dist/host-files/index.d.ts +2 -2
  100. package/dist/host-files/index.js +3 -3
  101. package/dist/index.d.ts +1 -3
  102. package/dist/index.js +1 -0
  103. package/dist/page/app/anchor.js +88 -0
  104. package/dist/page/{app.js → app/app.js} +112 -230
  105. package/dist/page/app/keys.js +94 -0
  106. package/dist/page/app/phase.js +30 -0
  107. package/dist/page/app/served.js +73 -0
  108. package/dist/page/avatars/avatars.js +19 -0
  109. package/dist/page/avatars/thought.js +10 -0
  110. package/dist/page/base.css +160 -0
  111. package/dist/page/board/board.css +959 -0
  112. package/dist/page/board/board.js +125 -0
  113. package/dist/page/{explain.js → board/explain.js} +1 -1
  114. package/dist/page/board/marks.js +160 -0
  115. package/dist/page/board/motion.js +71 -0
  116. package/dist/page/board/pointer.js +239 -0
  117. package/dist/page/board/squares.js +26 -0
  118. package/dist/page/dialogs/analyses.js +195 -0
  119. package/dist/page/{dialog.js → dialogs/dialog.js} +1 -1
  120. package/dist/page/dialogs/dialogs.css +1228 -0
  121. package/dist/page/{new-game-dialog.js → dialogs/new-game.js} +3 -3
  122. package/dist/page/{profile-dialogs.js → dialogs/profiles.js} +40 -11
  123. package/dist/page/dialogs/search.js +31 -0
  124. package/dist/page/dialogs/settings.js +42 -0
  125. package/dist/page/dialogs/studied.js +39 -0
  126. package/dist/page/dialogs/study.js +51 -0
  127. package/dist/page/{badges.js → game/badges.js} +1 -1
  128. package/dist/page/{browse.js → game/browse.js} +1 -1
  129. package/dist/page/game/card-fold.js +40 -0
  130. package/dist/page/game/column.js +51 -0
  131. package/dist/page/{ending.js → game/ending.js} +2 -2
  132. package/dist/page/game/game.css +577 -0
  133. package/dist/page/{move-list.js → game/move-list.js} +10 -6
  134. package/dist/page/{plies.js → game/plies.js} +30 -10
  135. package/dist/page/game/result-tile.js +14 -0
  136. package/dist/page/helps/helps.css +689 -0
  137. package/dist/page/helps/helps.js +220 -0
  138. package/dist/page/helps/view.js +127 -0
  139. package/dist/page/layout/fold-tab.js +11 -0
  140. package/dist/page/{folds.js → layout/folds.js} +1 -1
  141. package/dist/page/{header.js → layout/header.js} +5 -4
  142. package/dist/page/layout/layout.css +834 -0
  143. package/dist/page/lead/lead.js +82 -0
  144. package/dist/page/lead/view.js +38 -0
  145. package/dist/page/main.js +2 -2
  146. package/dist/page/pieces/CREDITS.md +12 -29
  147. package/dist/page/pieces/capablanca-small.svg +20 -0
  148. package/dist/page/pieces/capablanca.svg +22 -0
  149. package/dist/page/pieces/nitida-small.svg +3 -2
  150. package/dist/page/pieces/nitida.svg +3 -2
  151. package/dist/page/{pieces.js → pieces/pieces.js} +2 -2
  152. package/dist/page/pieces/torretta-small.svg +15 -14
  153. package/dist/page/pieces/torretta.svg +17 -15
  154. package/dist/page/{player-card.js → players/card.js} +2 -6
  155. package/dist/page/{clock-time.js → players/clock-time.js} +2 -2
  156. package/dist/page/{profile.js → profile/profile.js} +9 -5
  157. package/dist/page/{settings.js → profile/settings.js} +5 -4
  158. package/dist/page/style.css +9 -4398
  159. package/dist/page/talk/talk.css +958 -0
  160. package/dist/page/{talk.js → talk/talk.js} +39 -46
  161. package/dist/page/text.js +137 -19
  162. package/dist/page/theme.css +292 -0
  163. package/dist/profiles/index.d.ts +1 -1
  164. package/dist/profiles/profiles.d.ts +15 -4
  165. package/dist/profiles/profiles.js +24 -7
  166. package/dist/server/bell.d.ts +10 -2
  167. package/dist/server/bell.js +28 -12
  168. package/dist/server/caller.d.ts +14 -3
  169. package/dist/server/caller.js +44 -11
  170. package/dist/server/mcp.d.ts +1 -2
  171. package/dist/server/mcp.js +42 -14
  172. package/dist/server/page/analyses.d.ts +23 -0
  173. package/dist/server/page/analyses.js +65 -0
  174. package/dist/server/page/files.d.ts +5 -0
  175. package/dist/server/page/files.js +92 -0
  176. package/dist/server/page/game.d.ts +6 -0
  177. package/dist/server/page/game.js +381 -0
  178. package/dist/server/{http.d.ts → page/http.d.ts} +13 -0
  179. package/dist/server/{http.js → page/http.js} +11 -0
  180. package/dist/server/{page.d.ts → page/page.d.ts} +8 -13
  181. package/dist/server/page/page.js +229 -0
  182. package/dist/server/{profile-routes.d.ts → page/profiles.d.ts} +2 -2
  183. package/dist/server/{profile-routes.js → page/profiles.js} +47 -18
  184. package/instructions/analysis.md +4 -0
  185. package/instructions/analyst-agent.md +8 -0
  186. package/instructions/analyst.md +3 -7
  187. package/instructions/coach.md +11 -0
  188. package/instructions/opponent.md +4 -4
  189. package/package.json +1 -1
  190. package/dist/host-files/ponder-agents.js +0 -40
  191. package/dist/page/avatars.js +0 -16
  192. package/dist/page/board.js +0 -566
  193. package/dist/page/fold-tab.js +0 -9
  194. package/dist/page/game-column.js +0 -44
  195. package/dist/page/helps-view.js +0 -51
  196. package/dist/page/helps.js +0 -90
  197. package/dist/page/pieces/cburnett-small.svg +0 -20
  198. package/dist/page/pieces/cburnett.svg +0 -20
  199. package/dist/page/settings-panel.js +0 -32
  200. package/dist/page/studied.js +0 -14
  201. package/dist/page/study-dialog.js +0 -58
  202. package/dist/server/analyses.d.ts +0 -17
  203. package/dist/server/analyses.js +0 -46
  204. package/dist/server/page.js +0 -657
  205. /package/dist/page/{torretta.js → avatars/torretta.js} +0 -0
  206. /package/dist/page/{opening-book.js → game/opening-book.js} +0 -0
  207. /package/dist/page/{model-name.js → players/model-name.js} +0 -0
  208. /package/dist/page/{sounds.js → sounds/sounds.js} +0 -0
  209. /package/dist/page/{voice.js → talk/voice.js} +0 -0
@@ -0,0 +1,344 @@
1
+ import { z } from "zod";
2
+ import { badges, Game, leadText, levelOf, materialOf, withSanLetters } from "../core/index.js";
3
+ import { archiveCoached, coachWaitingSince, coachWords, endOf, keepHint, keepJudgment, keepLead, loadGame, startCoach } from "../games/index.js";
4
+ import { coachAgent } from "../host-files/index.js";
5
+ import { activeProfile, gameFolder, helpsIn } from "../profiles/index.js";
6
+ import { clockLine, clockSummaryOf } from "./clock.js";
7
+ import { arrowsInput, drawingsOf, noteInput, squaresInput } from "./drawings.js";
8
+ import { lastMoves } from "./game-summary.js";
9
+ import { aheadWords, leadFrom, leadInput } from "./lead-input.js";
10
+ import { knownMoves, openingLine } from "./opening-book.js";
11
+ import { waitForPlayer, wordsOf } from "./ponder.js";
12
+ import { profileLines } from "./position.js";
13
+ import { defineCommand } from "./registry.js";
14
+ import { speechLine, speechOf } from "./speech.js";
15
+ import { colourName, compactView, materialText, riskLine } from "./view.js";
16
+ // The badges the coach gives: the book's is Chesspal's, never a judgment (D-69).
17
+ const judgments = badges.filter((badge) => badge !== "book");
18
+ /**
19
+ * The coach (D-80, D-88), for the agent that judges the player's moves and gives them hints with the helps on: every fact, never the opponent's plan; its
20
+ * words go to the board page alone, never to the opponent. Its calls are not the chat's, and the server refuses them from the chat. Offered to Claude Code alone.
21
+ */
22
+ export const coach = defineCommand({
23
+ name: "coach",
24
+ description: "For the chesspal-coach agent only, never from the chat: Chesspal's answers say when to start it. coach {} gives the player's move to judge and the position "
25
+ + "they are to move in; coach {game, position, ahead, badge, note, hint, arrows, squares} keeps who is ahead there, while the game goes on, and your judgment and your hint, "
26
+ + "each only when there is something to say; coach {game, position, done: true} waits for their move, once who is ahead is kept. Whenever it says stop, stop. "
27
+ + 'Example: coach {game: "t3k9qa", position: 24, ahead: -0.8, badge: "mistake", note: "It leaves the d5 pawn without a defender.", hint: "Look at what defends their king now."}.',
28
+ input: z.object({
29
+ game: z.string().optional().describe("the game's key, as coach {} gives it"),
30
+ position: z.int().min(0).optional().describe("the position's number, as coach {} gives it"),
31
+ ahead: leadInput.optional().describe("who is ahead in the position the player is to move in, for the page's bar: pawns from White's side, 1.5 for White, -3 for Black, 0 for even, or a mate "
32
+ + 'you see in moves of the side that mates, "#3" for White\'s, "#-3" for Black\'s'),
33
+ badge: z.enum(judgments).optional().describe("your badge on the player's move, as chess.com names them, only on a move that stands out, never on a routine one"),
34
+ note: noteInput.optional().describe("why, on the player's move, in a sentence or two, in their language"),
35
+ hint: noteInput.optional().describe("a hint for their turn, in a sentence or two, in their language, only when there is something to find: what is on the board (a piece, a square, a file, a "
36
+ + "threat of the opponent's, a goal), never what to play"),
37
+ arrows: arrowsInput.describe("only with a hint: arrows on the board as it is now, never a move the player can play: Rf2e1; R red: a threat of the opponent's; B blue: a plan or a target; "
38
+ + "Y yellow: a line; G green: a good idea"),
39
+ squares: squaresInput.describe("only with a hint: squares to mark on the board as it is now: Rf2; colours as for arrows"),
40
+ done: z.boolean().optional().describe("true once you have kept your words, who is ahead always while the game goes on: it waits for the player's move"),
41
+ }),
42
+ byAgents: [coachAgent],
43
+ host: "claude-code",
44
+ run: async ({ game, position, ahead, badge, note, hint, arrows, squares, done = false }, context) => {
45
+ const folder = gameFolder(context.home());
46
+ const current = loadGame(folder);
47
+ const words = { ahead, badge, note, hint, arrows, squares };
48
+ const writes = Object.values(words).some((word) => word !== undefined);
49
+ if (!helpsIn(folder)) {
50
+ return { stop: "helps" };
51
+ }
52
+ if (game === undefined || position === undefined) {
53
+ if (writes || done || game !== undefined || position !== undefined) {
54
+ throw new Error("Give the game's key and the position's number with it: call coach {} first, which gives them. Nothing changed.");
55
+ }
56
+ const start = current === undefined ? undefined : startOf(current, folder, context);
57
+ if (start === undefined || current === undefined) {
58
+ return { stop: "moved" };
59
+ }
60
+ // The chat may start a second coach on the same position, told again before the first called: one judges it (Priya).
61
+ return startCoach(folder, current) ? { start } : { stop: "started" };
62
+ }
63
+ // A new game may have started while the coach thought on the last one: its words never go to another game (Federico).
64
+ if (current === undefined || gameKey(current) !== game || position > current.game.record.moves.length) {
65
+ return { stop: "moved" };
66
+ }
67
+ const place = { game, position };
68
+ if (!done) {
69
+ return keep(folder, current, place, words);
70
+ }
71
+ if (writes) {
72
+ throw new Error(`Keep your words first, then call ${call(place, "done: true")} alone. Nothing changed.`);
73
+ }
74
+ // The bar on the page needs who is ahead in every position the player is to move in while the game goes on (D-90, Stefano).
75
+ if (leadMissing(folder, current, position)) {
76
+ throw new Error(`Who is ahead is still missing here: first ${aheadCall(place)}, then ${call(place, "done: true")}. Nothing changed.`);
77
+ }
78
+ const since = coachWaitingSince(folder, current, position, context.now());
79
+ const waited = await waitForPlayer(context, since, () => !theirTurn(loadGame(folder), place));
80
+ return waited === "waiting" ? { waiting: place } : { stop: waited };
81
+ },
82
+ format: (coached) => {
83
+ if ("stop" in coached) {
84
+ return coached.stop === "helps"
85
+ ? "Stop now: the player has turned the helps off. End with one word."
86
+ : coached.stop === "started"
87
+ ? "Stop now: another coach works on this position. End with one word."
88
+ : coached.stop === "moved"
89
+ ? "Stop now: the player has moved, or the game is over. End with one word."
90
+ : "Stop now: you have waited long enough. End with one word.";
91
+ }
92
+ if ("start" in coached) {
93
+ return coached.start.join("\n");
94
+ }
95
+ if ("kept" in coached) {
96
+ return keptText(coached);
97
+ }
98
+ return `Still the player's move: call ${call(coached.waiting, "done: true")} again.`;
99
+ },
100
+ });
101
+ /**
102
+ * What a call kept, and what comes next: who is ahead when it is still missing, then wait for the player's move; or stop, as they have moved or the game is
103
+ * over, with the hint that came too late.
104
+ */
105
+ function keptText({ kept, then, saved, hintLeft }) {
106
+ const what = saved.length === 0 ? "nothing" : saved.join(", ");
107
+ if (then === "lead") {
108
+ return `Kept on the board page: ${what}. Who is ahead is still missing: now ${aheadCall(kept)}, then ${call(kept, "done: true")} alone.`;
109
+ }
110
+ if (then === "wait") {
111
+ return `Kept on the board page: ${what}. Now call ${call(kept, "done: true")} alone.`;
112
+ }
113
+ const late = hintLeft ? ", so your hint is not kept" : "";
114
+ return then === "moved"
115
+ ? `Kept: ${what}; the player has moved${late}. Stop now: end with one word.`
116
+ : `Kept for the game's record: ${what}; the game is over${late}. Stop now: end with one word.`;
117
+ }
118
+ /** A call of coach on a game's position, with what else it gives. */
119
+ function call({ game, position }, rest) {
120
+ return `coach {game: "${game}", position: ${position}, ${rest}}`;
121
+ }
122
+ /** The call that gives who is ahead, with how to write it. */
123
+ function aheadCall(place) {
124
+ return `call ${call(place, "ahead")}, in pawns from White's side (1.5, -3, 0) or a mate you see ("#3" White's, "#-3" Black's)`;
125
+ }
126
+ /** Whether who is ahead is still to give in a position the player is to move in, while the game goes on. */
127
+ function leadMissing(folder, current, position) {
128
+ const { leads = [] } = coachWords(folder, current);
129
+ return endOf(current.game) === undefined && sideToMoveAt(current, position) !== current.ai && !leads.some((lead) => lead.position === position);
130
+ }
131
+ /** The game's short key, from its start: the coach names the game it works on, so that its words never go to the next one. */
132
+ function gameKey(current) {
133
+ return Math.floor(Date.parse(current.started) / 1000).toString(36);
134
+ }
135
+ /** How many plies of the game the coach shows: enough to follow it, few enough for its budget. */
136
+ const pliesShown = 40;
137
+ /**
138
+ * What the coach works on, from the position the player is to move in, or the game over: the game, the player's move the opponent answered, or their own
139
+ * that ended the game, with the position they played it in and the book there, and the position now with the hint it gave last; none while the opponent is
140
+ * to move.
141
+ */
142
+ function startOf(current, folder, context) {
143
+ const { game, ai } = current;
144
+ const { record } = game;
145
+ const plies = record.moves.length;
146
+ const over = endOf(game);
147
+ const player = ai === "white" ? "black" : "white";
148
+ const judged = judgedPly(current, plies);
149
+ if ((game.position.turn === ai && over === undefined) || (over !== undefined && judged === undefined)) {
150
+ return undefined;
151
+ }
152
+ const place = { game: gameKey(current), position: plies };
153
+ const ender = game.position.turn === ai ? "the player's" : "the opponent's";
154
+ const head = over === undefined
155
+ ? `Game "${place.game}", position ${plies}: the player (${colourName(player)}) to move, move ${game.position.moveNumber}; the opponent plays ${colourName(ai)} at the `
156
+ + `${levelOf(record)} level.`
157
+ : `Game "${place.game}", position ${plies}: the game is over, ${over.result}${over.termination === undefined ? "" : `, ${over.termination}`}, with ${ender} move.`;
158
+ const speech = speechOf(folder, context.language);
159
+ const clock = clockSummaryOf(current, context.now());
160
+ const keepLine = over === undefined
161
+ ? `Keep your words: ${call(place, "ahead, badge, note, hint, arrows, squares")}, who is ahead always, the rest only when there is something to say; then `
162
+ + `${call(place, "done: true")}.`
163
+ : `No hint: the game is over. Keep your judgment, when their move stands out: ${call(place, "badge, note")}; then end with one word.`;
164
+ return [
165
+ head,
166
+ speech === undefined ? "The player speaks English." : speechLine(speech),
167
+ ...playerLines(context),
168
+ ...movesLines(record),
169
+ ...(clock === undefined ? [] : [clockLine(clock, ai)]),
170
+ ...(judged === undefined ? [] : judgedLines(current, judged, player)),
171
+ ...(over === undefined ? nowLines(current, folder, player) : []),
172
+ keepLine,
173
+ ];
174
+ }
175
+ /** The game's moves as the coach reads them, with the position a game set up began in, the last ones of a long game, and its opening by the book (Anatolij). */
176
+ function movesLines(record) {
177
+ const opening = openingLine(record);
178
+ return [
179
+ ...(record.tags.FEN === undefined ? [] : [`The game began from a set position: ${record.tags.FEN}`]),
180
+ `${record.moves.length > pliesShown ? `Moves (the last ${pliesShown} plies)` : "Moves"}: ${lastMoves(record, pliesShown)}`,
181
+ ...(opening === undefined ? [] : [`Opening: ${opening}`]),
182
+ ];
183
+ }
184
+ /**
185
+ * The plies up to the player's move a judgment at `position` is for: the one the opponent answered, or the player's own when the opponent is to move, as
186
+ * after their move that ended the game (D-88); none before their first move.
187
+ */
188
+ function judgedPly(current, position) {
189
+ const ply = sideToMoveAt(current, position) === current.ai ? position : position - 1;
190
+ return ply >= 1 ? ply : undefined;
191
+ }
192
+ /**
193
+ * The player's move to judge, the position they played it in with the book there, and, when the opponent answered it, what it left at risk and the checks
194
+ * it let the opponent give (Anatolij), and the answer with its words.
195
+ */
196
+ function judgedLines({ game }, ply, player) {
197
+ const { record } = game;
198
+ const before = new Game({ tags: record.tags, moves: record.moves.slice(0, ply - 1) });
199
+ const played = { tags: record.tags, moves: record.moves.slice(0, ply) };
200
+ const book = knownMoves(before.position.fen);
201
+ const lines = [
202
+ `Their move to judge: ${lastMoves(played, 1)}. The position they played it in:`,
203
+ `FEN: ${before.position.fen}`,
204
+ `Material: ${materialText(materialOf(before.position.fen))}`,
205
+ ...compactView(before.facts, player),
206
+ ...(book.length === 0 ? [] : [`Book moves there, most named lines first: ${book.join(", ")}`]),
207
+ ];
208
+ if (ply === record.moves.length) {
209
+ return lines;
210
+ }
211
+ const after = new Game(played).facts;
212
+ const answer = lastMoves({ tags: record.tags, moves: record.moves.slice(0, ply + 1) }, 1);
213
+ const said = wordsOf(record.moves[ply]?.comment);
214
+ return [
215
+ ...lines,
216
+ `After their move: at risk ${riskLine(after) ?? "none"}; the opponent's checks: ${after.checks.join(" ") || "none"}.`,
217
+ said === undefined ? `The opponent answered ${answer} and said nothing.` : `The opponent answered ${answer} and said: «${said}»`,
218
+ ];
219
+ }
220
+ /**
221
+ * The position the player is to move in, for who is ahead and the hint, with its positional facts too (D-90), and what the coach said on their turn
222
+ * before.
223
+ */
224
+ function nowLines(current, folder, player) {
225
+ const { game } = current;
226
+ const plies = game.record.moves.length;
227
+ const { hint, leads = [] } = coachWords(folder, current);
228
+ const lead = leads.find(({ position }) => position === plies - 2)?.lead;
229
+ return [
230
+ "Now, for who is ahead and your hint:",
231
+ `FEN: ${game.position.fen}`,
232
+ ...compactView(game.facts, player),
233
+ ...profileLines(game.profile),
234
+ ...(lead === undefined ? [] : [`Who you saw ahead on their turn before: ${leadText(lead)}, ${aheadWords(lead)}`]),
235
+ ...(hint === undefined || hint.position !== plies - 2 ? [] : [`Your hint on their turn before: «${hint.text}»`]),
236
+ ];
237
+ }
238
+ /** How the player says they play, and their rating, when their profile says so. */
239
+ function playerLines(context) {
240
+ const { level, rating } = activeProfile(context.home());
241
+ const parts = [level === undefined ? undefined : `plays at the ${level} level`, rating === undefined ? undefined : `is rated ${rating.value} on ${rating.site}`];
242
+ const said = parts.filter((part) => part !== undefined);
243
+ return said.length === 0 ? [] : [`The player ${said.join(" and ")}.`];
244
+ }
245
+ /** Keeps the judgment of the player's move the opponent answered at `position`, or that ended the game, and the hint for their turn while it is still theirs. */
246
+ function keep(folder, current, place, { ahead, badge, note, hint, arrows, squares }) {
247
+ const { position } = place;
248
+ const judging = badge !== undefined || note !== undefined;
249
+ const over = endOf(current.game) !== undefined;
250
+ const toMove = theirTurn(current, place);
251
+ if (hint === undefined && (arrows !== undefined || squares !== undefined)) {
252
+ throw new Error("Arrows and squares show your hint: give them with it. Nothing changed.");
253
+ }
254
+ if (!judging && hint === undefined && ahead === undefined) {
255
+ throw new Error(`Give who is ahead (ahead), your judgment (badge, note), your hint (hint, arrows, squares); or ${call(place, "done: true")}. Nothing changed.`);
256
+ }
257
+ // On the position the game ended in there is no turn to help and the result says who won; a position before it keeps who was ahead there, when the game
258
+ // ended while the coach thought (Federico).
259
+ const ended = over && position === current.game.record.moves.length;
260
+ if (ended && (hint !== undefined || ahead !== undefined)) {
261
+ throw new Error("The game is over: no hint and no ahead. Keep your judgment, when their move stands out, or end with one word. Nothing changed.");
262
+ }
263
+ if (ahead !== undefined && sideToMoveAt(current, position) === current.ai) {
264
+ throw new Error("Who is ahead is for a position the player is to move in, as coach {} gives it: here the opponent is to move. Nothing changed.");
265
+ }
266
+ const lead = ahead === undefined ? undefined : leadFrom(ahead);
267
+ // The player's move to judge: the one the opponent answered, or their own that ended the game (D-88).
268
+ const ply = judgedPly(current, position);
269
+ if (judging && (ply === undefined || (ply === position && !ended))) {
270
+ throw new Error("There is no move of the player's to judge here: give only your hint. Nothing changed.");
271
+ }
272
+ const legal = toMove ? current.game.legalMoves() : [];
273
+ if (hint !== undefined && toMove) {
274
+ refuseMoves(hint, arrows ?? [], legal);
275
+ }
276
+ if (judging && ply !== undefined) {
277
+ // A note that names a move the player could play in the position it was given for waits until none of them is legal on their turn: until then it
278
+ // would play for them (Anatolij).
279
+ const then = toMove ? legal : new Game({ tags: current.game.record.tags, moves: current.game.record.moves.slice(0, position) }).legalMoves();
280
+ const held = note === undefined ? [] : [...new Set(namedMoves(note, then))];
281
+ keepJudgment(folder, current, { ply, ...(badge === undefined ? {} : { badge }), ...(note === undefined ? {} : { note }), ...(held.length === 0 ? {} : { held }) });
282
+ }
283
+ if (hint !== undefined && toMove) {
284
+ keepHint(folder, current, { position, text: hint, ...drawingsOf(arrows, squares) });
285
+ }
286
+ // Who is ahead in the position the player was to move in, kept even once they have moved: the bar and the chart show it on that position.
287
+ if (lead !== undefined) {
288
+ keepLead(folder, current, { position, lead });
289
+ }
290
+ const saved = [
291
+ judging ? "your judgment" : undefined,
292
+ lead === undefined ? undefined : `who is ahead, ${aheadWords(lead)}`,
293
+ hint !== undefined && toMove ? "your hint" : undefined,
294
+ ].filter((part) => part !== undefined);
295
+ const hintLeft = hint !== undefined && !toMove;
296
+ if (over) {
297
+ // The game went to the archive with the move that ended it: the judgment goes there too.
298
+ archiveCoached(folder, current);
299
+ return { kept: place, then: "over", saved, hintLeft };
300
+ }
301
+ if (!toMove) {
302
+ return { kept: place, then: "moved", saved, hintLeft };
303
+ }
304
+ return { kept: place, then: leadMissing(folder, current, position) ? "lead" : "wait", saved, hintLeft };
305
+ }
306
+ /** Whether the player is still to move in the position the coach works on, in the same game going on. */
307
+ function theirTurn(current, { game, position }) {
308
+ return current !== undefined && gameKey(current) === game && endOf(current.game) === undefined && current.game.record.moves.length === position
309
+ && current.game.position.turn !== current.ai;
310
+ }
311
+ /** The side to move after `position` plies of the game. */
312
+ function sideToMoveAt({ game }, position) {
313
+ const { turn } = game.position;
314
+ return (game.record.moves.length - position) % 2 === 0 ? turn : turn === "white" ? "black" : "white";
315
+ }
316
+ /** Refuses a hint that plays for the player: a move they may play now written in it, or drawn as an arrow (Anatolij). */
317
+ function refuseMoves(hint, arrows, legal) {
318
+ const drawn = arrows.map((arrow) => legal.find((move) => move.from === arrow.slice(1, 3) && move.to === arrow.slice(3, 5))?.san).filter((san) => san !== undefined);
319
+ const named = [...namedMoves(hint, legal), ...drawn];
320
+ if (named.length > 0) {
321
+ throw new Error(`Your hint gives the player a move they may play now (${[...new Set(named)].join(", ")}): point at what is on the board, a piece, a square, a file, a threat, a goal, `
322
+ + "never what to play, and mark squares rather than draw their move. Nothing changed.");
323
+ }
324
+ }
325
+ /** The moves a text names that the player may play now: a piece's move, a capture, a promotion or a castling, in letters or figurines; a square alone is no move. */
326
+ function namedMoves(text, legal) {
327
+ const sanOf = new Map(legal.map((move) => [moveKey(move.san), move.san]));
328
+ const written = withSanLetters(text).match(moveWords) ?? [];
329
+ return written.map((word) => sanOf.get(moveKey(word))).filter((san) => san !== undefined);
330
+ }
331
+ // A move as SAN writes it, but a pawn's move to a square, which reads as the square: Nf3, Nbxd2, exd5, e8=Q, O-O; never the opponent's, after "...".
332
+ const moveWords = /(?<![\p{L}\d]|\.\.\.|…)(?:O-O(?:-O)?|0-0(?:-0)?|[KQRBN][a-h]?[1-8]?x?[a-h][1-8]|[a-h]x[a-h][1-8](?:=[QRBN])?|[a-h][18]=[QRBN])(?![\p{L}\d])/gu;
333
+ /**
334
+ * A move as the coach may write it and Chesspal lists it, alike: its piece and capture, the square it goes to and its promotion; a castling as it is. A
335
+ * piece on a square without the capture names that piece, never a move taking it (Priya and Anatolij).
336
+ */
337
+ function moveKey(san) {
338
+ const bare = san.replace(/[+#!?]+$/, "").replaceAll("0", "O");
339
+ if (bare.startsWith("O-O")) {
340
+ return bare;
341
+ }
342
+ const [, piece = "", capture = "", to = "", promotion = ""] = /^([KQRBN])?[a-h]?[1-8]?(x?)([a-h][1-8])(=[QRBN])?$/.exec(bare) ?? [];
343
+ return `${piece}${capture}${to}${promotion}`;
344
+ }
@@ -1,4 +1,3 @@
1
- import type { Position } from "./play-move.ts";
2
1
  /** What a command runs with, besides its input: given by the command line or the MCP server, and by the tests. */
3
2
  export interface Context {
4
3
  /** The folder Chesspal runs in: the player's, where the AI host started it, which keeps only the host's files (D-71). */
@@ -19,37 +18,49 @@ export interface Context {
19
18
  page?: string;
20
19
  /** Whether a browser has the board page open now, when the MCP server serves it. */
21
20
  pageOpen?: () => boolean;
22
- /** Opens a folder in the system's file manager: the data folder, from the board page (T-119); whether it could; none in the tests. */
21
+ /** Opens a folder in the system's file manager: the data folder, from the board page; whether it could; none in the tests. */
23
22
  openFolder?: (folder: string) => Promise<boolean>;
24
23
  /** Opens the New game window on the board pages open now, where the player chooses the game: whether one was open. */
25
24
  askNewGame?: () => boolean;
26
25
  /** The AI host, as it names itself to the MCP server: claude-code, which notes how each chat starts (D-57); none on the command line. */
27
26
  host?: () => string | undefined;
28
- /** The player's words on the board page given to the AI in this process, by line: each comes once (T-65). */
27
+ /** The player's words on the board page given to the AI in this process, by line: each comes once. */
29
28
  readonly given: Set<string>;
30
29
  /** Told when the board page has news that no file holds: words the AI was given, which it shows read (D-53), and the AI's model (D-82). */
31
30
  heard?: () => void;
32
31
  /** Told when the chat calls a tool, before it runs: the board page hears the chat listens, and the AI's clock, held while nobody did, starts. */
33
32
  calls?: () => void;
33
+ /** Told when the AI starts thinking on the player's time (D-72), with the position, and when it is done: the board page shows it. */
34
+ ponders?: (position: SeenPosition | undefined) => void;
34
35
  /** The game the chat saw when its last tool ended, its folder and its stamp: the doorbell wakes it only for a game changed since. */
35
36
  seen?: string | undefined;
36
37
  /**
37
38
  * The position the last tool that showed the game showed the chat: its moves are refused on a game changed since, by another chat or on the page
38
39
  * (D-71); null in a chat no tool has shown the game yet, none outside a chat.
39
40
  */
40
- positionSeen?: Position | null | undefined;
41
+ positionSeen?: SeenPosition | null | undefined;
41
42
  /** The instructions the MCP server gave in this chat, by their name and the chat's start: each comes once a chat (D-68); none on the command line. */
42
43
  told?: Set<string>;
43
44
  /** Who makes this tool call, as far as the host ties it to the call (D-81); none on the command line. */
44
45
  caller?: () => Promise<Caller | undefined>;
46
+ /** The agent that makes this call of a tool it shares with the chat, as the analyst (D-89); none for the chat, or when it cannot be told. */
47
+ agent?: string;
45
48
  }
46
- /** Who made a tool call: the model, else the host's name, never what the AI says of itself; the effort only when the host gives it (T-113). */
49
+ /** Who made a tool call: the model, else the host's name, never what the AI says of itself; the effort only when the host gives it. */
47
50
  export interface Caller {
48
51
  readonly annotator: string;
49
52
  readonly effort?: string;
50
53
  /** The annotator is the host's name: the model is not known. */
51
54
  readonly hostOnly?: boolean;
52
55
  }
56
+ /**
57
+ * Where the game a chat sees stands: the game by its start, none when there is none, and how many plies it has. Not by its folder, which a profile
58
+ * completed renames (D-71); a game in another profile started at another time.
59
+ */
60
+ export interface SeenPosition {
61
+ readonly started?: string;
62
+ readonly plies: number;
63
+ }
53
64
  /** The context of a process started in the player's folder: the data folder is found at the first command that needs it, once. */
54
65
  export declare function defaultContext(): Context;
55
66
  /** A data folder with a profile in it, the first made when there is none, named after the system's account (D-71). */
@@ -1,3 +1,4 @@
1
+ // What every command runs with besides its input, injected by the command line, the MCP server or the tests: the folders, the time, the randomness.
1
2
  import { userInfo } from "node:os";
2
3
  import { dataFolder, openFolder } from "../home/index.js";
3
4
  import { ensureProfile } from "../profiles/index.js";
@@ -0,0 +1,7 @@
1
+ import { z } from "zod";
2
+ /** Deletes a game of the profile in use, by its name as past_games or study_games writes it. */
3
+ export declare const deleteGame: import("./registry.ts").Command<z.ZodObject<{
4
+ game: z.ZodString;
5
+ }, z.core.$strip>, {
6
+ deleted: string;
7
+ }>;
@@ -0,0 +1,30 @@
1
+ // A game deleted from a profile (D-92): played here or kept to study, with its review. Only the command line and the page delete, never the AI.
2
+ import { z } from "zod";
3
+ import { deleteGame as deleteGameFile, endAnalysis, gameInAnalysis, isOverOnTable, listedName, removeGame } from "../games/index.js";
4
+ import { gameFolder } from "../profiles/index.js";
5
+ import { defineCommand } from "./registry.js";
6
+ /** Deletes a game of the profile in use, by its name as past_games or study_games writes it. */
7
+ export const deleteGame = defineCommand({
8
+ name: "delete_game",
9
+ description: "Deletes a game of the profile in use, played here or kept to study, with its review: it cannot be undone. "
10
+ + "Example: chesspal delete_game games/2026-11-02-153012.pgn.",
11
+ input: z.object({ game: z.string().min(1).describe("the game, as past_games or study_games writes it") }),
12
+ cliOnly: true,
13
+ destructive: true,
14
+ positionals: ["game"],
15
+ run: ({ game }, context) => {
16
+ const folder = gameFolder(context.home());
17
+ const name = listedName(folder, game);
18
+ // Asked before deleting, so that nothing fails halfway: the game over still on the table would go back to the archive with the next game.
19
+ const onTable = isOverOnTable(folder, name);
20
+ deleteGameFile(folder, name);
21
+ if (gameInAnalysis(folder)?.game === name) {
22
+ endAnalysis(folder);
23
+ }
24
+ if (onTable) {
25
+ removeGame(folder);
26
+ }
27
+ return { deleted: name };
28
+ },
29
+ format: ({ deleted }) => `${deleted} deleted, with its review.`,
30
+ });
@@ -0,0 +1,8 @@
1
+ import { z } from "zod";
2
+ /** Deletes the review of a game of the profile in use, by its name as past_games or study_games writes it. */
3
+ export declare const deleteReview: import("./registry.ts").Command<z.ZodObject<{
4
+ game: z.ZodString;
5
+ }, z.core.$strip>, {
6
+ game: string;
7
+ reviewed: boolean;
8
+ }>;
@@ -0,0 +1,34 @@
1
+ // A game's review deleted (D-92): the game stays, to analyse again, with another model or effort or after an update. Only the command line and the page
2
+ // delete, never the AI.
3
+ import { z } from "zod";
4
+ import { isReviewed, unreviewed } from "../core/index.js";
5
+ import { forgetCoach, gameInAnalysis, isOverOnTable, rewriteGame, startAnalysis } from "../games/index.js";
6
+ import { gameFolder } from "../profiles/index.js";
7
+ import { defineCommand } from "./registry.js";
8
+ import { namedGame } from "./study.js";
9
+ /** Deletes the review of a game of the profile in use, by its name as past_games or study_games writes it. */
10
+ export const deleteReview = defineCommand({
11
+ name: "delete_review",
12
+ description: "Deletes the review of a game of the profile in use: the badges, notes, drawings and estimates on its moves, the word on the game and who "
13
+ + "signed it, the coach's judgments too; the game stays, to analyse again. Example: chesspal delete_review games/2026-11-02-153012.pgn.",
14
+ input: z.object({ game: z.string().min(1).describe("the game, as past_games or study_games writes it") }),
15
+ cliOnly: true,
16
+ destructive: true,
17
+ idempotent: true,
18
+ positionals: ["game"],
19
+ run: ({ game }, context) => {
20
+ const folder = gameFolder(context.home());
21
+ const { record, name } = namedGame(context, game);
22
+ rewriteGame(folder, name, unreviewed(record));
23
+ // The coach's judgments on the game just over would go back into it.
24
+ if (isOverOnTable(folder, name)) {
25
+ forgetCoach(folder);
26
+ }
27
+ // The page showing the game hears of it, and shows it without its review.
28
+ if (gameInAnalysis(folder)?.game === name) {
29
+ startAnalysis(folder, name, context.now());
30
+ }
31
+ return { game: name, reviewed: isReviewed(record) };
32
+ },
33
+ format: ({ game, reviewed }) => `${game}: ${reviewed ? "review deleted" : "it had no review; the judgments on its moves are deleted"}. analyse_game goes over it again.`,
34
+ });
@@ -1,7 +1,5 @@
1
1
  import { z } from "zod";
2
2
  import { type DrawnArrow, type DrawnSquare } from "../core/index.ts";
3
- /** The most a note of the AI's on a move may hold: a few sentences. */
4
- export declare const noteLimit = 700;
5
3
  /** A note of the AI's on a move, as its input reads it: on one line of commands, so without brackets or braces. */
6
4
  export declare const noteInput: z.ZodString;
7
5
  /** The arrows the AI draws, as their input reads them. */
@@ -3,17 +3,19 @@
3
3
  import { z } from "zod";
4
4
  import { arrowOf, squareOf } from "../core/index.js";
5
5
  /** The most a note of the AI's on a move may hold: a few sentences. */
6
- export const noteLimit = 700;
6
+ const noteLimit = 700;
7
7
  // The most arrows, and the most squares, on one move.
8
8
  const drawLimit = 6;
9
9
  /** A note of the AI's on a move, as its input reads it: on one line of commands, so without brackets or braces. */
10
10
  export const noteInput = z.string().trim().min(1).max(noteLimit).regex(/^[^[\]{}]*$/, "write it without [ ] { }");
11
+ // A drawing as the AI may write it, spaced or with a dash, as PGN writes it: "R f2 e1" and "Rf2-e1" are Rf2e1 (Priya).
12
+ const drawing = z.string().overwrite((text) => text.replace(/[\s-]+/g, ""));
11
13
  /** The arrows the AI draws, as their input reads them. */
12
- export const arrowsInput = z.array(z.string().regex(/^[GRYB][a-h][1-8][a-h][1-8]$/, "an arrow is a colour's letter and two squares: Gf6e4; a single square goes in squares")
14
+ export const arrowsInput = z.array(drawing.regex(/^[GRYB][a-h][1-8][a-h][1-8]$/, "an arrow is a colour's letter and two squares: Gf6e4; a single square goes in squares")
13
15
  .refine((arrow) => arrow.slice(1, 3) !== arrow.slice(3, 5), "an arrow goes from a square to another: a single square goes in squares")).max(drawLimit).optional()
14
16
  .describe("arrows on the board, as PGN's %cal writes them: Rf2e1. G green: the better move or the good idea; R red: a threat or the mistake; Y yellow: another way; B blue: a plan.");
15
17
  /** The squares the AI marks, as their input reads them. */
16
- export const squaresInput = z.array(z.string().regex(/^[GRYB][a-h][1-8]$/, "a square is a colour's letter and the square: Rf2")).max(drawLimit).optional()
18
+ export const squaresInput = z.array(drawing.regex(/^[GRYB][a-h][1-8]$/, "a square is a colour's letter and the square: Rf2")).max(drawLimit).optional()
17
19
  .describe("squares to mark, as PGN's %csl writes them: Rf2; colours as for arrows");
18
20
  /** The arrows and squares drawn, those that read; none of either left out. */
19
21
  export function drawingsOf(arrows = [], squares = []) {
@@ -2,7 +2,9 @@ import { z } from "zod";
2
2
  import { canMate, endWithMood, Game, moods, timeLeft } from "../core/index.js";
3
3
  import { archiveEnded, bothSidesMoved, drawOfferOf, saveGame } from "../games/index.js";
4
4
  import { gameFolder } from "../profiles/index.js";
5
- import { gameGoingOn, nextStep, positionLines, summaryOf } from "./game-summary.js";
5
+ import { withAnalyst } from "./agents.js";
6
+ import { nextStep, positionLines } from "./answer-lines.js";
7
+ import { gameGoingOn, summaryOf } from "./game-summary.js";
6
8
  import { refuseUnseenPosition } from "./play-move.js";
7
9
  import { defineCommand } from "./registry.js";
8
10
  import { colourName } from "./view.js";
@@ -38,7 +40,8 @@ export const endGame = defineCommand({
38
40
  : { ...current, game, clock: { ...current.clock, left: timeLeft(current.clock, current.game.position.turn, now), since: now.toISOString() } };
39
41
  saveGame(folder, ended);
40
42
  const { archive, warning } = archiveEnded(folder, ended);
41
- return warning === undefined ? { game: summaryOf(ended, now, archive) } : { game: summaryOf(ended, now, archive), warning };
43
+ const summary = withAnalyst(summaryOf(ended, now, archive), context);
44
+ return warning === undefined ? { game: summary } : { game: summary, warning };
42
45
  },
43
46
  format: ({ game, warning }) => [...positionLines(game), ...(warning === undefined ? [] : [warning]), nextStep(game)].join("\n"),
44
47
  });
@@ -3,7 +3,7 @@ interface Exported {
3
3
  format: "pgn" | "moves" | "fen";
4
4
  text: string;
5
5
  }
6
- /** The game as PGN or its position as FEN, for the player to copy or keep: the plans stay hidden while it goes on (D-29); the moves alone for another site (T-59). */
6
+ /** The game as PGN or its position as FEN, for the player to copy or keep: the plans stay hidden while it goes on (D-29); the moves alone for another site. */
7
7
  export declare const exportGame: import("./registry.ts").Command<z.ZodObject<{
8
8
  format: z.ZodEnum<{
9
9
  fen: "fen";
@@ -3,7 +3,7 @@ import { movesPgn, playerPgn } from "../games/index.js";
3
3
  import { gameFolder } from "../profiles/index.js";
4
4
  import { gameAt } from "./clock.js";
5
5
  import { defineCommand } from "./registry.js";
6
- /** The game as PGN or its position as FEN, for the player to copy or keep: the plans stay hidden while it goes on (D-29); the moves alone for another site (T-59). */
6
+ /** The game as PGN or its position as FEN, for the player to copy or keep: the plans stay hidden while it goes on (D-29); the moves alone for another site. */
7
7
  export const exportGame = defineCommand({
8
8
  name: "export",
9
9
  description: "Gives the game as PGN, its moves alone as PGN for Lichess or chess.com, or its position as FEN, for the player to copy or keep. While the game goes on, "
@@ -15,11 +15,12 @@ export const exportGame = defineCommand({
15
15
  readOnly: true,
16
16
  positionals: ["format"],
17
17
  run: ({ format }, context) => {
18
- const current = gameAt(gameFolder(context.home()), context.now());
18
+ const folder = gameFolder(context.home());
19
+ const current = gameAt(folder, context.now());
19
20
  if (current === undefined) {
20
21
  throw new Error("There is no game to export: start one with new_game.");
21
22
  }
22
- const text = format === "fen" ? current.game.position.fen : format === "moves" ? movesPgn(current) : playerPgn(current);
23
+ const text = format === "fen" ? current.game.position.fen : format === "moves" ? movesPgn(current) : playerPgn(folder, current);
23
24
  return { format, text };
24
25
  },
25
26
  format: (result) => result.text,