chesspal 0.3.5 → 0.4.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 (191) hide show
  1. package/README.md +1 -0
  2. package/dist/cli/cli.js +17 -4
  3. package/dist/cli/command-line.js +22 -12
  4. package/dist/cli/icon.d.ts +2 -0
  5. package/dist/cli/icon.js +11 -1
  6. package/dist/cli/prompt-hook.d.ts +17 -0
  7. package/dist/cli/prompt-hook.js +100 -0
  8. package/dist/commands/analysis/analyse.d.ts +2 -2
  9. package/dist/commands/analysis/analyse.js +10 -16
  10. package/dist/commands/analysis/annotate.d.ts +8 -0
  11. package/dist/commands/analysis/annotate.js +19 -3
  12. package/dist/commands/answers/agents.d.ts +17 -7
  13. package/dist/commands/answers/agents.js +46 -16
  14. package/dist/commands/answers/clock.js +2 -1
  15. package/dist/commands/answers/drawings.js +6 -2
  16. package/dist/commands/answers/index.d.ts +4 -3
  17. package/dist/commands/answers/index.js +4 -3
  18. package/dist/commands/answers/lines.d.ts +5 -3
  19. package/dist/commands/answers/lines.js +17 -9
  20. package/dist/commands/answers/opening-book.d.ts +13 -7
  21. package/dist/commands/answers/opening-book.js +34 -38
  22. package/dist/commands/answers/opening-tags.d.ts +4 -0
  23. package/dist/commands/answers/opening-tags.js +11 -0
  24. package/dist/commands/answers/play-move.d.ts +3 -1
  25. package/dist/commands/answers/play-move.js +9 -6
  26. package/dist/commands/answers/say.js +2 -1
  27. package/dist/commands/answers/summary.d.ts +7 -3
  28. package/dist/commands/answers/summary.js +16 -5
  29. package/dist/commands/answers/view.d.ts +1 -1
  30. package/dist/commands/answers/view.js +2 -2
  31. package/dist/commands/game/ai-move.js +4 -4
  32. package/dist/commands/game/coach.js +7 -6
  33. package/dist/commands/game/end.js +2 -2
  34. package/dist/commands/game/export.d.ts +1 -1
  35. package/dist/commands/game/index.d.ts +1 -1
  36. package/dist/commands/game/index.js +1 -1
  37. package/dist/commands/game/look.d.ts +7 -3
  38. package/dist/commands/game/look.js +9 -4
  39. package/dist/commands/game/new.js +42 -12
  40. package/dist/commands/game/player-move.d.ts +16 -0
  41. package/dist/commands/game/player-move.js +69 -19
  42. package/dist/commands/game/ponder.js +7 -4
  43. package/dist/commands/game/position.d.ts +2 -1
  44. package/dist/commands/game/position.js +9 -4
  45. package/dist/commands/game/status.d.ts +4 -2
  46. package/dist/commands/game/status.js +24 -14
  47. package/dist/commands/index.d.ts +3 -3
  48. package/dist/commands/index.js +3 -3
  49. package/dist/commands/profiles/past-games.d.ts +7 -2
  50. package/dist/commands/profiles/past-games.js +45 -18
  51. package/dist/commands/registry/context.d.ts +2 -0
  52. package/dist/commands/registry/context.js +5 -0
  53. package/dist/commands/registry/index.d.ts +1 -1
  54. package/dist/commands/registry/index.js +1 -1
  55. package/dist/commands/talk/ai-say.d.ts +1 -1
  56. package/dist/commands/talk/ai-say.js +7 -6
  57. package/dist/core/game.d.ts +13 -0
  58. package/dist/core/game.js +29 -3
  59. package/dist/core/index.d.ts +4 -4
  60. package/dist/core/index.js +3 -3
  61. package/dist/core/openings.d.ts +20 -6
  62. package/dist/core/openings.js +45 -22
  63. package/dist/core/pgn.d.ts +4 -0
  64. package/dist/core/pgn.js +35 -6
  65. package/dist/games/analysis.d.ts +7 -4
  66. package/dist/games/analysis.js +5 -3
  67. package/dist/games/archive.d.ts +25 -5
  68. package/dist/games/archive.js +52 -19
  69. package/dist/games/chat-start.d.ts +2 -2
  70. package/dist/games/chat-start.js +6 -1
  71. package/dist/games/current-game.d.ts +2 -2
  72. package/dist/games/current-game.js +3 -2
  73. package/dist/games/index.d.ts +3 -2
  74. package/dist/games/index.js +3 -2
  75. package/dist/games/position-shown.d.ts +24 -0
  76. package/dist/games/position-shown.js +51 -0
  77. package/dist/games/thoughts.d.ts +12 -4
  78. package/dist/games/thoughts.js +27 -10
  79. package/dist/home/index.d.ts +1 -0
  80. package/dist/home/index.js +1 -0
  81. package/dist/home/same-path.d.ts +7 -0
  82. package/dist/home/same-path.js +24 -0
  83. package/dist/host-files/host-files.d.ts +5 -0
  84. package/dist/host-files/host-files.js +21 -7
  85. package/dist/host-files/index.d.ts +1 -1
  86. package/dist/host-files/index.js +1 -1
  87. package/dist/page/app/anchor.js +15 -11
  88. package/dist/page/app/app.js +49 -14
  89. package/dist/page/app/served.js +30 -38
  90. package/dist/page/app/watch.js +97 -0
  91. package/dist/page/avatars/torretta.css +108 -0
  92. package/dist/page/base.css +1 -1
  93. package/dist/page/board/board.css +11 -8
  94. package/dist/page/board/marks.js +2 -2
  95. package/dist/page/board/pointer.js +2 -2
  96. package/dist/page/dialogs/analyses.js +5 -3
  97. package/dist/page/dialogs/dialogs.css +3 -1
  98. package/dist/page/dialogs/rows.js +72 -0
  99. package/dist/page/dialogs/site-games.js +3 -1
  100. package/dist/page/game/column.js +10 -8
  101. package/dist/page/game/game.css +137 -16
  102. package/dist/page/game/move-list.js +7 -5
  103. package/dist/page/game/plies.js +4 -0
  104. package/dist/page/game/review-progress.js +11 -0
  105. package/dist/page/guide/drawings/drawings.css +0 -1
  106. package/dist/page/guide/en.md +2 -2
  107. package/dist/page/guide/it.md +2 -2
  108. package/dist/page/helps/helps.css +5 -5
  109. package/dist/page/helps/helps.js +3 -181
  110. package/dist/page/helps/rows.js +16 -0
  111. package/dist/page/helps/strategy.js +105 -0
  112. package/dist/page/helps/tactics.js +69 -0
  113. package/dist/page/layout/header.js +49 -6
  114. package/dist/page/layout/icon.ico +0 -0
  115. package/dist/page/layout/layout.css +49 -17
  116. package/dist/page/layout/window.js +45 -6
  117. package/dist/page/lead/lead.js +41 -4
  118. package/dist/page/legend/legend.css +4 -16
  119. package/dist/page/legend/legend.js +28 -15
  120. package/dist/page/profile/settings.js +1 -0
  121. package/dist/page/sounds/sounds.js +1 -1
  122. package/dist/page/style.css +1 -0
  123. package/dist/page/table/action.js +42 -0
  124. package/dist/page/table/table.css +57 -39
  125. package/dist/page/table/window.js +67 -67
  126. package/dist/page/talk/talk.css +52 -118
  127. package/dist/page/talk/talk.js +65 -20
  128. package/dist/page/talk/voice.js +92 -21
  129. package/dist/page/text.js +34 -8
  130. package/dist/page/theme.css +13 -0
  131. package/dist/profiles/profiles.js +5 -1
  132. package/dist/server/bell/bell.d.ts +24 -29
  133. package/dist/server/bell/bell.js +79 -133
  134. package/dist/server/bell/index.d.ts +1 -1
  135. package/dist/server/bell/index.js +2 -2
  136. package/dist/server/mcp/mcp.js +49 -7
  137. package/dist/server/page/analyses.js +15 -4
  138. package/dist/server/page/chat.d.ts +5 -1
  139. package/dist/server/page/chat.js +5 -1
  140. package/dist/server/page/files.js +3 -1
  141. package/dist/server/page/game.d.ts +5 -0
  142. package/dist/server/page/game.js +95 -15
  143. package/dist/server/page/http.d.ts +1 -1
  144. package/dist/server/page/page.d.ts +14 -9
  145. package/dist/server/page/page.js +111 -26
  146. package/dist/server/page/table.d.ts +40 -5
  147. package/dist/server/page/table.js +65 -25
  148. package/dist/server/page/update.d.ts +1 -1
  149. package/dist/server/page/update.js +4 -4
  150. package/dist/server/page/windows.d.ts +22 -0
  151. package/dist/server/page/windows.js +45 -0
  152. package/dist/server/ports/index.d.ts +1 -0
  153. package/dist/server/ports/index.js +2 -0
  154. package/dist/server/ports/ports.d.ts +78 -0
  155. package/dist/server/ports/ports.js +222 -0
  156. package/dist/server/table/browser.d.ts +22 -6
  157. package/dist/server/table/browser.js +98 -20
  158. package/dist/server/table/hosts/chats.d.ts +5 -0
  159. package/dist/server/table/hosts/chats.js +11 -4
  160. package/dist/server/table/hosts/hooks.d.ts +6 -0
  161. package/dist/server/table/hosts/hooks.js +38 -0
  162. package/dist/server/table/hosts/hosts.d.ts +13 -5
  163. package/dist/server/table/hosts/hosts.js +16 -10
  164. package/dist/server/table/hosts/index.d.ts +1 -0
  165. package/dist/server/table/hosts/index.js +1 -0
  166. package/dist/server/table/hosts/play.d.ts +7 -3
  167. package/dist/server/table/hosts/play.js +63 -9
  168. package/dist/server/table/icon/index.d.ts +2 -0
  169. package/dist/server/table/icon/index.js +16 -2
  170. package/dist/server/table/index.d.ts +2 -2
  171. package/dist/server/table/index.js +1 -1
  172. package/dist/server/table/launch.d.ts +5 -4
  173. package/dist/server/table/launch.js +18 -5
  174. package/dist/server/table/link.js +4 -1
  175. package/dist/server/table/table.d.ts +7 -0
  176. package/dist/server/table/table.js +40 -12
  177. package/dist/sites/index.d.ts +1 -1
  178. package/dist/sites/index.js +1 -1
  179. package/dist/sites/{chesscom.d.ts → ratings/chesscom.d.ts} +2 -2
  180. package/dist/sites/{chesscom.js → ratings/chesscom.js} +1 -1
  181. package/dist/sites/{choice.d.ts → ratings/choice.d.ts} +1 -1
  182. package/dist/sites/{choice.js → ratings/choice.js} +1 -1
  183. package/dist/sites/ratings/index.d.ts +1 -0
  184. package/dist/sites/ratings/index.js +2 -0
  185. package/dist/sites/{lichess.d.ts → ratings/lichess.d.ts} +2 -2
  186. package/dist/sites/{lichess.js → ratings/lichess.js} +1 -1
  187. package/dist/sites/{sites.d.ts → ratings/ratings.d.ts} +2 -2
  188. package/dist/sites/{sites.js → ratings/ratings.js} +1 -1
  189. package/instructions/opponent.md +1 -1
  190. package/instructions/start.md +1 -1
  191. package/package.json +1 -1
package/README.md CHANGED
@@ -87,6 +87,7 @@ Chesspal works with any agent that can use Chesspal's tools (MCP) and the files
87
87
  - **To play in a folder of your own**, as with the Claude desktop app, type `chesspal init chess`, then open your agent in the folder `chess` and say hello: the AI answers with the link to the board page. Any name will do; your games are kept [elsewhere](#where-your-games-are).
88
88
  - **A move written in the chat** counts too: `e4`, `Nf3`, in the international notation (K Q R B N in every language), or in words.
89
89
  - **To update**, when a new version is out the table's page shows an arrow at the top: press it between games, and the table installs it and starts again, with its chat. By hand: close the AI's terminal, run `npm install -g chesspal` again, then `chesspal`. In a folder of your own, run `chesspal init` there too: it updates its own lines and keeps what you wrote there. In Codex, allow the hooks again when it says so.
90
+ - **To uninstall**, run `chesspal icon --remove` first, which takes the icon off your desktop and menu, then `npm uninstall -g chesspal`. Your games stay [where they are](#where-your-games-are).
90
91
 
91
92
  ## Where your games are
92
93
 
package/dist/cli/cli.js CHANGED
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { runCommandLine } from "./command-line.js";
3
2
  const args = process.argv.slice(2);
4
- // The table's own words (D-104): alone or open, its page; table, icon, play <host>. The host's: mcp, its server; listen, its doorbell; chat, how the chat
5
- // started. Everything else is a command.
3
+ // The table's own words (D-104): alone or open, its page; table, icon (--remove), play <host>. The host's: mcp, its server; listen, its doorbell; chat, how the chat
4
+ // started; prompt, the move the player wrote alone. Everything else is a command. Each loads only what it needs: prompt runs on every message of the chat.
6
5
  if (args.length === 0) {
7
6
  await openTableHere(true);
8
7
  }
@@ -16,7 +15,15 @@ else if (args.length === 1 && args[0] === "table") {
16
15
  else if (args.length === 1 && args[0] === "icon") {
17
16
  await putIconHere();
18
17
  }
19
- else if (args.length === 2 && args[0] === "play" && !args[1].startsWith("-")) {
18
+ else if (args.length === 2 && args[0] === "icon" && args[1] === "--remove") {
19
+ const [{ defaultContext }, { iconsRemoved }] = await Promise.all([import("../commands/index.js"), import("./icon.js")]);
20
+ console.log(iconsRemoved(defaultContext()));
21
+ }
22
+ else if (args[0] === "play" && (args.length === 2 || (args.length === 4 && args[2] === "--home")) && !args[1].startsWith("-")) {
23
+ // The data folder of a table started with CHESSPAL_HOME, which a terminal the table opens may not pass on (Terminal.app): the chat and its server read it.
24
+ if (args[3] !== undefined) {
25
+ process.env.CHESSPAL_HOME = args[3];
26
+ }
20
27
  await playHere(args[1]);
21
28
  }
22
29
  else if (args.length === 1 && args[0] === "mcp") {
@@ -32,7 +39,12 @@ else if (args.length === 1 && args[0] === "chat") {
32
39
  const { recordChatStart } = await import("./chat-hook.js");
33
40
  await recordChatStart();
34
41
  }
42
+ else if (args.length === 1 && args[0] === "prompt") {
43
+ const { answerPrompt } = await import("./prompt-hook.js");
44
+ await answerPrompt();
45
+ }
35
46
  else {
47
+ const { runCommandLine } = await import("./command-line.js");
36
48
  const { output, exitCode } = await runCommandLine(args);
37
49
  if (exitCode === 0) {
38
50
  console.log(output);
@@ -106,3 +118,4 @@ async function serveTable() {
106
118
  process.exitCode = 1;
107
119
  }
108
120
  }
121
+ export {};
@@ -3,17 +3,21 @@ import { parseArgs } from "node:util";
3
3
  import { baseType, commandLineFieldProblem, commands, defaultContext, inputProblems, isBoolean, isList, takesNumbers } from "../commands/index.js";
4
4
  import { rerunIfChanged } from "../games/index.js";
5
5
  import { version } from "../version.js";
6
- // The commands that exist only on the command line: cli.ts starts the table, opens its page, puts its icon, starts the server, the doorbell or the chat's
7
- // note, when one comes alone.
6
+ // The commands that exist only on the command line: cli.ts starts the table, opens its page, puts its icon, starts the server, the doorbell, the chat's
7
+ // note or the move written alone, when one comes alone.
8
8
  const mcpDescription = "Start the MCP server for an AI host. Through it the host uses these commands as tools; the host starts it, with no options.";
9
9
  const listenDescription = "Wait for the player's move on the board page and wake the chat. Claude Code starts it at the end of each turn, and Codex with --codex, as init sets them up.";
10
10
  const chatDescription = "Note how the chat started: new, cleared, resumed or compacted. Claude Code and Codex run it when a chat starts, as init sets them up.";
11
+ const promptDescription = "Play the move the player wrote alone in the chat, before the AI reads it, and tell the AI the position. Claude Code runs it on each message, as init sets it up.";
11
12
  const tableDescription = "Serve the board page with no chat, until no browser has had it open for five minutes. `chesspal` alone starts it apart and opens the page.";
12
13
  const openDescription = "Open the board page as `chesspal` alone does, without asking about the icon: what the icon runs.";
13
- const iconDescription = "Put Chesspal's icon on the desktop and in the menu: it opens the board page as `chesspal` alone does, with no terminal.";
14
- const playDescription = "Play at the table in Claude Code or Codex, in this terminal: what the page's buttons open, with the model and effort chosen there; New game on the page starts the chat again, clean.";
14
+ const iconDescription = "Put Chesspal's icon on the desktop and in the menu: it opens the board page as `chesspal` alone does, with no terminal. `chesspal icon --remove` takes it away.";
15
+ const playDescription = "Start the AI in this terminal, in Claude Code or Codex, as the page's buttons do. It plays with the model and effort chosen on the page; New game there starts the chat again, clean.";
16
+ // What init and the icon's removal are to a player, in the general help: the command's own help says the files.
17
+ const initForPlayer = "Set up a folder of your own to play in, from Claude Code, Codex or the Claude desktop app.";
18
+ const iconRemoveForPlayer = "Take Chesspal's icon away, before you uninstall it.";
15
19
  // Those of them that take no options, by name.
16
- const plainCommands = new Map([["mcp", mcpDescription], ["chat", chatDescription], ["table", tableDescription], ["open", openDescription], ["icon", iconDescription]]);
20
+ const plainCommands = new Map([["mcp", mcpDescription], ["chat", chatDescription], ["prompt", promptDescription], ["table", tableDescription], ["open", openDescription], ["icon", iconDescription]]);
17
21
  /** The `chesspal` command line: finds the command, validates its input, runs it and renders the result. */
18
22
  export async function runCommandLine(argv, context = defaultContext(), list = commands) {
19
23
  const [commandName, ...args] = argv;
@@ -149,24 +153,30 @@ function checkRepeatedOptions(command, tokens, options) {
149
153
  function inputFields(command) {
150
154
  return Object.entries(command.input.shape);
151
155
  }
152
- /** The commands, each with the first sentence of its description. */
156
+ /** The commands, each with the first sentence of its description: the player's first, then those of the AI and of whoever develops Chesspal. */
153
157
  function generalHelp(list) {
154
- const width = Math.max("listen".length, ...list.map((command) => command.name.length));
158
+ const width = Math.max("icon --remove".length, ...list.map((command) => command.name.length));
155
159
  const line = (name, description) => ` ${name.padEnd(width)} ${firstSentence(description)}`;
160
+ const init = list.find((command) => command.name === "init");
156
161
  return [
157
162
  `chesspal ${version}`,
158
163
  "",
159
- "Run `chesspal` alone to open the board page in your browser.",
164
+ "To play, type `chesspal` alone: the board opens in your browser, and from there you start the AI.",
160
165
  "",
161
- "Commands:",
162
- ...list.map((command) => line(command.name, command.description)),
166
+ "For the player:",
167
+ line("play", playDescription),
168
+ ...(init === undefined ? [] : [line(init.name, initForPlayer)]),
169
+ line("icon", iconDescription),
170
+ line("icon --remove", iconRemoveForPlayer),
171
+ "",
172
+ "For the AI, and for whoever develops Chesspal:",
173
+ ...list.filter((command) => command !== init).map((command) => line(command.name, command.description)),
163
174
  line("table", tableDescription),
164
175
  line("open", openDescription),
165
- line("icon", iconDescription),
166
- line("play", playDescription),
167
176
  line("mcp", mcpDescription),
168
177
  line("listen", listenDescription),
169
178
  line("chat", chatDescription),
179
+ line("prompt", promptDescription),
170
180
  "",
171
181
  "Run `chesspal <command> --help` for its options.",
172
182
  ].join("\n");
@@ -23,3 +23,5 @@ export declare function iconsMade(context: Context, places?: IconPlaces): Promis
23
23
  said: string;
24
24
  failed: boolean;
25
25
  }>;
26
+ /** Takes the icon off the desktop and the menu, before Chesspal is uninstalled: what to tell the player. */
27
+ export declare function iconsRemoved(context: Context): string;
package/dist/cli/icon.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import { createInterface } from "node:readline/promises";
4
4
  import { styleText } from "node:util";
5
5
  import { speaksItalian } from "../commands/index.js";
6
- import { iconAsked, iconCommand, iconToRemake, makeIcons, noteIconAsked, noteIconMade } from "../server/table/index.js";
6
+ import { iconAsked, iconCommand, iconToRemake, makeIcons, noteIconAsked, noteIconMade, removeIcons } from "../server/table/index.js";
7
7
  /**
8
8
  * Asks once a machine, in a terminal on a screen, whether to put the icon on the desktop and in the menu, and puts it there on a yes: what to tell the
9
9
  * player, if anything. Once asked, an icon made by another Node or script is made again, without a word; a playtest's data folder is never asked about.
@@ -46,6 +46,12 @@ export async function iconsMade(context, places) {
46
46
  return { said: words.failed(error.message), failed: true };
47
47
  }
48
48
  }
49
+ /** Takes the icon off the desktop and the menu, before Chesspal is uninstalled: what to tell the player. */
50
+ export function iconsRemoved(context) {
51
+ const words = wordsFor(context);
52
+ const removed = removeIcons(context.home());
53
+ return removed.length === 0 ? words.noneRemoved : `${words.removed}\n${styleText("dim", removed.map((file) => ` ${file}`).join("\n"))}`;
54
+ }
49
55
  function processTerminal() {
50
56
  return { input: process.stdin, output: process.stdout, env: process.env };
51
57
  }
@@ -68,6 +74,8 @@ function wordsFor(context) {
68
74
  made: `Fatto: Chesspal è sul desktop e ${where}.`,
69
75
  later: "Va bene, niente icona. Se la vuoi più avanti, scrivi chesspal icon.",
70
76
  failed: (reason) => `Chesspal non è riuscito a mettere l'icona (${reason}). Riprova con chesspal icon.`,
77
+ removed: `Fatto: l'icona di Chesspal non è più sul desktop né ${where}. Per togliere Chesspal: npm uninstall -g chesspal. Le tue partite restano dove sono.`,
78
+ noneRemoved: "Chesspal non ha trovato sue icone su questo computer.",
71
79
  };
72
80
  }
73
81
  const where = { windows: "your Start menu", mac: "Applications", linux: "your applications menu" }[system];
@@ -76,5 +84,7 @@ function wordsFor(context) {
76
84
  made: `Done: Chesspal is on your desktop and in ${where}.`,
77
85
  later: "All right, no icon. To add it later, type chesspal icon.",
78
86
  failed: (reason) => `Chesspal could not add its icon (${reason}). Try again with chesspal icon.`,
87
+ removed: `Done: Chesspal's icon is gone from your desktop and ${where}. To remove Chesspal: npm uninstall -g chesspal. Your games stay where they are.`,
88
+ noneRemoved: "Chesspal found no icon of its own on this computer.",
79
89
  };
80
90
  }
@@ -0,0 +1,17 @@
1
+ import type { Context } from "../commands/index.ts";
2
+ /**
3
+ * Reads the hook's input, tells the chat's page the chat is at work and, when the prompt is a move Chesspal plays, writes what the AI is told; it never
4
+ * blocks the prompt.
5
+ */
6
+ export declare function answerPrompt(input?: NodeJS.ReadableStream, sent?: Date): Promise<void>;
7
+ /**
8
+ * What the hook writes, the JSON Claude Code reads, when the prompt is one move and Chesspal played it; undefined, with nothing changed, for any other
9
+ * prompt or a move refused. `contextOf` gives what the move is played with, in the chat's folder; it then notes there the position shown to the chat.
10
+ */
11
+ export declare function promptAnswer(input: string, contextOf: (hook: HookInput) => Promise<Context>): Promise<string | undefined>;
12
+ /** What the hook reads of Claude Code's input: the prompt trimmed, the chat's session and the folder it runs in. */
13
+ export interface HookInput {
14
+ prompt?: string;
15
+ session?: string;
16
+ cwd?: string;
17
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Reads the hook's input, tells the chat's page the chat is at work and, when the prompt is a move Chesspal plays, writes what the AI is told; it never
3
+ * blocks the prompt.
4
+ */
5
+ export async function answerPrompt(input = process.stdin, sent = new Date()) {
6
+ try {
7
+ const text = await textOf(input);
8
+ const [answer] = await Promise.all([promptAnswer(text, (hook) => hookContext(hook, sent)), tellPage(hookInput(text))]);
9
+ if (answer !== undefined) {
10
+ process.stdout.write(`${answer}\n`);
11
+ }
12
+ }
13
+ catch (error) {
14
+ process.stderr.write(`Chesspal could not play the move written in the chat: ${String(error.message ?? error).replace(/\s+/g, " ")}\n`);
15
+ }
16
+ }
17
+ /**
18
+ * What the hook writes, the JSON Claude Code reads, when the prompt is one move and Chesspal played it; undefined, with nothing changed, for any other
19
+ * prompt or a move refused. `contextOf` gives what the move is played with, in the chat's folder; it then notes there the position shown to the chat.
20
+ */
21
+ export async function promptAnswer(input, contextOf) {
22
+ const hook = hookInput(input);
23
+ if (hook.prompt === undefined || !moveShape.test(hook.prompt) || hook.session === undefined) {
24
+ return undefined;
25
+ }
26
+ const [context, { playMoveSentAlone }, { hostProcess, notePositionShown }] = await Promise.all([contextOf(hook), import("../commands/index.js"), import("../games/index.js")]);
27
+ let played;
28
+ try {
29
+ played = playMoveSentAlone(hook.prompt, context);
30
+ }
31
+ catch (error) {
32
+ // Chesspal's refusals end so: the game as it was, the AI reads the prompt and answers it.
33
+ if (error instanceof Error && error.message.endsWith("Nothing changed.")) {
34
+ return undefined;
35
+ }
36
+ throw error;
37
+ }
38
+ try {
39
+ const { position, stamp, given } = played;
40
+ const host = hostProcess();
41
+ notePositionShown(context.folder, {
42
+ session: hook.session,
43
+ ...(host === undefined ? {} : { process: host }),
44
+ ...(stamp === undefined ? {} : { stamp }),
45
+ ...position,
46
+ at: context.now().toISOString(),
47
+ given,
48
+ });
49
+ }
50
+ catch (error) {
51
+ // The move is played: the AI hears it all the same, and its server asks it for status before its own, which may give the words again.
52
+ process.stderr.write(`Chesspal could not note the position it showed the chat: ${error.message}\n`);
53
+ }
54
+ return JSON.stringify({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: played.answer } });
55
+ }
56
+ // One move and nothing else, as player_move takes it: SAN with K, Q, R, B and N, small or as figurines, or with the letters of other languages that name
57
+ // one piece (Cf3, Tf1, Dd1, e8=D), or as squares, or castling; a check or mate sign.
58
+ const moveShape = /^(?:[KQRBNkqrbnCSAFLTDt♔-♟]?[a-h]?[1-8]?[-x:×–—−]?[a-h][1-8](?:=?[QRBNqrbnCSAFLTD])?|[0Oo]-[0Oo](?:-[0Oo])?)[+#]?$/u;
59
+ /**
60
+ * Tells the chat's page that the prompt starts its turn: the doorbell lets the hook of the turn before go, and a new game asked on the page waits for
61
+ * the turn's end. A slash command or a shell line may start no turn, and so no Stop hook to end it: the page is not told.
62
+ */
63
+ async function tellPage(hook) {
64
+ if (hook.prompt === undefined || /^[/!]/.test(hook.prompt)) {
65
+ return;
66
+ }
67
+ // A page not told leaves the doorbell as it was: the move, when the prompt is one, is played all the same.
68
+ await import("../server/ports/index.js").then(({ tellChatAtWork }) => tellChatAtWork(hook), () => false);
69
+ }
70
+ /** The context of the chat's folder: the data folder its server is given, the moment the player sent the move, and Claude Code, which runs this hook. */
71
+ async function hookContext(hook, sent) {
72
+ const [{ defaultContext }, { chatFolder, serverHome }] = await Promise.all([import("../commands/index.js"), import("../server/ports/index.js")]);
73
+ const folder = chatFolder(hook);
74
+ const home = serverHome(folder);
75
+ // A playtest's data folder, which .mcp.json gives the server and not the hook.
76
+ if (home !== undefined) {
77
+ process.env.CHESSPAL_HOME = home;
78
+ }
79
+ return { ...defaultContext(), folder, now: () => sent, host: () => "claude-code" };
80
+ }
81
+ async function textOf(input) {
82
+ let text = "";
83
+ for await (const chunk of input) {
84
+ text += String(chunk);
85
+ }
86
+ return text;
87
+ }
88
+ function hookInput(text) {
89
+ try {
90
+ const { prompt, session_id: session, cwd } = JSON.parse(text);
91
+ return {
92
+ ...(typeof prompt === "string" ? { prompt: prompt.trim() } : {}),
93
+ ...(typeof session === "string" ? { session } : {}),
94
+ ...(typeof cwd === "string" ? { cwd } : {}),
95
+ };
96
+ }
97
+ catch {
98
+ return {};
99
+ }
100
+ }
@@ -2,14 +2,14 @@ import { z } from "zod";
2
2
  import { type AiLevel, type MoveReview, type PieceLetter, type Side } from "../../core/index.ts";
3
3
  /** A move as the analysis gives it: what happened, never how good it was (D-6). */
4
4
  interface MoveFacts {
5
- /** The move, numbered: 12...Nxe5. */
5
+ /** The move, numbered, with the pieces' figurines as the other tools write moves (D-86), since the analyst copies them into its notes: 12...♞xe5. */
6
6
  move: string;
7
7
  /** The piece it took. */
8
8
  took?: PieceLetter;
9
9
  /** The time it took and the time left after it, in milliseconds, where the record has the clock. */
10
10
  spent?: number;
11
11
  left?: number;
12
- /** Who is ahead in material after it, when it changed: White +1. */
12
+ /** Who is ahead in material after it, when it changed: White +1 in pawns. */
13
13
  material?: string;
14
14
  /** After it, the pieces at risk of either side when they changed from the line before ("none" once no piece is), and the mates the side to move has; none once the board ends the game. */
15
15
  atRisk?: string;
@@ -5,7 +5,7 @@ import { gameInAnalysis, isGameName, keepToStudy, lastChatStart, planOf, rewrite
5
5
  import { analystAgent, instructionsFile } from "../../host-files/index.js";
6
6
  import { activeProfile, gameFolder } from "../../profiles/index.js";
7
7
  import { pointsAtSite, siteGameName, siteOfGameName } from "../../sites/index.js";
8
- import { analystOf, anotherAnalyst, balanceText, bookOpening, bookReaches, colourName, gameToStudy, knownTag, moveNumbers, namedGame, noGameGoingOn, riskLine, speechLine, speechOf, tacticsText } from "../answers/index.js";
8
+ import { analystOf, anotherAnalyst, balanceText, bookOpening, bookPlace, bookPlies, colourName, gameToStudy, knownTag, moveNumbers, namedGame, noGameGoingOn, riskLine, shownMove, speechLine, speechOf, tacticsText, } from "../answers/index.js";
9
9
  import { playerLine, siteText } from "../profiles/index.js";
10
10
  import { defineCommand } from "../registry/index.js";
11
11
  import { profileSiteGames } from "./site-games.js";
@@ -49,7 +49,7 @@ export const analyseGame = defineCommand({
49
49
  }
50
50
  const analysis = analysisOf(studied, activeProfile(context.home()));
51
51
  if (studied.name !== undefined) {
52
- markBook(context, { record: studied.record, name: studied.name }, analysis.moves);
52
+ markBook(context, { record: studied.record, name: studied.name });
53
53
  startAnalysis(gameFolder(context.home()), studied.name, context.now());
54
54
  }
55
55
  // The analyst's agent has its instructions in its own file, and the player's language in its prompt or, when Chesspal knows it, here (D-89);
@@ -148,8 +148,8 @@ async function siteGamePgn(context, name) {
148
148
  * The opening book's moves marked as such in the game kept (D-69): a fact, not a judgment, so Chesspal puts the badge itself on each move from the start to the
149
149
  * last one the book knows, where the AI gave none; a game from another position has none.
150
150
  */
151
- function markBook(context, { record, name }, moves) {
152
- const inBook = bookPliesOfFacts(record, moves);
151
+ function markBook(context, { record, name }) {
152
+ const inBook = bookPlies(record);
153
153
  const unmarked = record.moves.slice(0, inBook).some((move) => reviewOf(move.comment)?.badge === undefined);
154
154
  if (!unmarked) {
155
155
  return;
@@ -160,11 +160,6 @@ function markBook(context, { record, name }, moves) {
160
160
  });
161
161
  rewriteGame(gameFolder(context.home()), name, { ...record, moves: marked });
162
162
  }
163
- /** How many plies from the start the opening book knows, as opening-book's bookPlies counts them, read from the facts already found: none from another position. */
164
- function bookPliesOfFacts(record, moves) {
165
- const firstOut = moves.findIndex((move) => move.outOfBook);
166
- return record.tags.FEN !== undefined ? 0 : firstOut >= 0 ? firstOut : moves.length;
167
- }
168
163
  function analysisOf({ record, name }, profile) {
169
164
  const { tags } = record;
170
165
  const ai = sideNamed(record, "AI");
@@ -186,7 +181,7 @@ function analysisOf({ record, name }, profile) {
186
181
  termination: ended?.termination,
187
182
  opening: opening === undefined ? undefined : `${opening.eco} ${opening.name}`,
188
183
  inBook: opening !== undefined && !moves.some((move) => move.outOfBook) ? true : undefined,
189
- bookTo: opening === undefined ? undefined : moves[bookPliesOfFacts(record, moves) - 1]?.move,
184
+ bookTo: opening === undefined ? undefined : moves[bookPlies(record) - 1]?.move,
190
185
  review: [gameReviewOf(record), closingOf(record)].filter((word) => word !== undefined).join(" ") || undefined,
191
186
  name,
192
187
  start: tags.FEN === undefined ? undefined : { fen: tags.FEN, ...start },
@@ -200,7 +195,8 @@ function movesOf(record, ai) {
200
195
  const increment = timeControlOfTag(record.tags.TimeControl ?? "")?.increment;
201
196
  // The time each side had left after its last move: the first move of each side spends no time it can tell, as the sites start their clocks apart.
202
197
  const left = {};
203
- let inBook = record.tags.FEN === undefined && bookReaches(game.position.fen);
198
+ // The first move no line of the book reaches is out of book (D-68); a game from another position has no book.
199
+ const leftBook = bookPlace(record)?.plies;
204
200
  let before = game.facts;
205
201
  let balance = materialOf(game.position.fen).balance;
206
202
  const start = { atRisk: riskLine(before), oneLegal: oneLegalMove(before), mates: matesOf(before), tactics: tacticsText(before, game.possibilities, true) };
@@ -212,8 +208,6 @@ function movesOf(record, ai) {
212
208
  if (!played.played) {
213
209
  throw new Error(`The game's move ${numbers[ply]}${san} cannot be played: ${played.message} Fix the game's PGN.`);
214
210
  }
215
- const outOfBook = inBook && !bookReaches(game.position.fen);
216
- inBook &&= !outOfBook;
217
211
  const facts = game.facts;
218
212
  const over = game.outcome.over;
219
213
  const now = materialOf(game.position.fen).balance;
@@ -221,7 +215,7 @@ function movesOf(record, ai) {
221
215
  const previous = left[side];
222
216
  const risks = riskLine(facts);
223
217
  const move = {
224
- move: `${numbers[ply]}${san}`,
218
+ move: shownMove(`${numbers[ply]}${san}`),
225
219
  took: before.pieces.find((piece) => piece.square === played.move.to)?.piece ?? (san.includes("x") ? "P" : undefined),
226
220
  spent: clock === undefined || previous === undefined || increment === undefined ? undefined : Math.max(0, previous - clock + increment),
227
221
  left: clock,
@@ -230,7 +224,7 @@ function movesOf(record, ai) {
230
224
  oneLegal: over ? undefined : oneLegalMove(facts),
231
225
  mates: over ? undefined : matesOf(facts),
232
226
  tactics: over ? undefined : tacticsText(facts, game.possibilities, true),
233
- outOfBook: outOfBook ? true : undefined,
227
+ outOfBook: ply === leftBook ? true : undefined,
234
228
  drawOffer: comment?.includes("(=)") === true ? true : undefined,
235
229
  said: side === ai ? saidIn(comment) : undefined,
236
230
  review: reviewOf(comment),
@@ -266,7 +260,7 @@ function analysisHead(analysis) {
266
260
  const review = analysis.review === undefined ? [] : [`Your review: ${analysis.review}`];
267
261
  return [head, ...opening, ...review, `${name}.`, ...start];
268
262
  }
269
- /** 12...Nxe5: took P; spent 0:12, 4:32 left; material White +1; at risk: undefended Black Nf6; mate in one: Qh7#; tactics: fork Nc7+ (Ke8 Ra8); out of book; you said "…"; your plan: …; ahead -1.5 */
263
+ /** 12...♞xe5: took P; spent 0:12, 4:32 left; material White +1 in pawns; at risk: undefended Black Nf6; mate in one: Qh7#; tactics: fork Nc7+ (Ke8 Ra8); out of book; you said "…"; your plan: …; ahead -1.5 */
270
264
  function moveLine(move) {
271
265
  const left = move.left === undefined ? undefined : `${clockText(move.left)} left`;
272
266
  const parts = [
@@ -14,6 +14,14 @@ interface Annotated {
14
14
  left: string[];
15
15
  /** The chat corrected a note of a review that already has its word on the game. */
16
16
  summaryStands: boolean;
17
+ /** Where this call wrote a move with another language's letters (5., your summary), and the first such move: the page shows them as plain text (D-87). */
18
+ lettered: {
19
+ where: string[];
20
+ first?: {
21
+ written: string;
22
+ san: string;
23
+ };
24
+ };
17
25
  }
18
26
  /** The AI's badges and notes on a game over, and its word on the whole game, written into the game's PGN (D-69). */
19
27
  export declare const annotateGame: import("../registry/registry.ts").Command<z.ZodObject<{
@@ -1,7 +1,7 @@
1
1
  // The AI's review of a game over (D-69): a badge on each move that matters, as chess.com marks them, with a note when it needs one, and its word on the
2
2
  // whole game, kept in the game's PGN for the board page. The judgment is the AI's alone: Chesspal gives no criteria (D-6, D-43).
3
3
  import { z } from "zod";
4
- import { annotatorOf, badges, Game, gameReviewOf, leadOf, reviewed, reviewOf, withAnnotator, withGameReview, withLead } from "../../core/index.js";
4
+ import { annotatorOf, badges, figurinesOf, foreignMovesIn, Game, gameReviewOf, leadOf, reviewed, reviewOf, withAnnotator, withGameReview, withLead } from "../../core/index.js";
5
5
  import { gameInAnalysis, rewriteGame, startAnalysis } from "../../games/index.js";
6
6
  import { analystAgent } from "../../host-files/index.js";
7
7
  import { gameFolder } from "../../profiles/index.js";
@@ -105,9 +105,10 @@ export const annotateGame = defineCommand({
105
105
  left: [...corrected],
106
106
  // A correction after the review's word on the game: that word may now say otherwise (Priya).
107
107
  summaryStands: correction && gameReviewOf(studied.record) !== undefined,
108
+ lettered: letteredIn([...notes.map(({ move, note }) => [move, note]), ["your summary", summary], ["your last word", closing]]),
108
109
  };
109
110
  },
110
- format: ({ game, marked, summary, closing, bookUnnoted, estimated, unestimated, left, summaryStands }) => {
111
+ format: ({ game, marked, summary, closing, bookUnnoted, estimated, unestimated, left, summaryStands, lettered }) => {
111
112
  const kept = [
112
113
  marked === 0 ? undefined : `${marked} ${marked === 1 ? "move" : "moves"} marked`,
113
114
  estimated === 0 ? undefined : `who is ahead after ${estimated} ${estimated === 1 ? "move" : "moves"}`,
@@ -129,9 +130,24 @@ export const annotateGame = defineCommand({
129
130
  : [`${left.join(" ")} ${one ? "keeps the note" : "keep the notes"} the AI wrote with the player: leave ${one ? "it" : "them"}, and don't contradict ${one ? "it" : "them"} in your summary${summary ? "; if this one does, give it again" : ""}.`];
130
131
  const head = kept.length === 0 ? `Nothing new kept in ${game}.` : `Kept in ${game}: ${[kept.slice(0, -1).join(", "), kept.at(-1)].filter((part) => part !== "").join(" and ")}. The board page shows them.`;
131
132
  const stands = summaryStands ? ["The review's word on the game stays as written: if it says otherwise, give summary again."] : [];
132
- return [head, ...stands, ...corrected, ...book, ...ahead].join("\n");
133
+ const single = lettered.where.length === 1;
134
+ const letters = lettered.first === undefined
135
+ ? []
136
+ : [
137
+ `${lettered.where.join(", ")} ${single ? "writes" : "write"} moves with another language's letters, which the page shows as plain text: give ${single ? "it" : "them"} `
138
+ + `again with the figurines (${figurinesOf(lettered.first.san)} for ${lettered.first.written}).`,
139
+ ];
140
+ return [head, ...stands, ...corrected, ...letters, ...book, ...ahead].join("\n");
133
141
  },
134
142
  });
143
+ /** Where the texts of a call write a move with another language's letters, by the move or the part they were given for, and the first such move. */
144
+ function letteredIn(texts) {
145
+ const found = texts.flatMap(([where, text]) => {
146
+ const [first] = foreignMovesIn(text ?? "");
147
+ return first === undefined ? [] : [{ where, first }];
148
+ });
149
+ return { where: [...new Set(found.map(({ where }) => where))], ...(found[0] === undefined ? {} : { first: found[0].first }) };
150
+ }
135
151
  /** The moves without who is ahead after them, as analyse_game numbers them: all but the last when the board ended the game there, which says it. */
136
152
  function unestimatedMoves(record, numbers, over) {
137
153
  return record.moves.flatMap((move, index) => leadOf(move.comment) !== undefined || (over && index === record.moves.length - 1) ? [] : [numbers[index]]);
@@ -8,8 +8,16 @@ export declare function analystRuns(context: Context): boolean;
8
8
  * its end: once reviewed, or taken by the agent, the chat goes over that review and doesn't start the agent for it (Stefano).
9
9
  */
10
10
  export declare function withAnalyst(summary: GameSummary, context: Context, over?: CurrentGame): GameSummary;
11
- /** How the chat has a game over gone over by the analyst agent, on the player's yes or for the game they brought (D-89). */
12
- export declare function analystLine(game?: string): string;
11
+ /**
12
+ * How the chat has a game over gone over by the analyst agent, on the player's yes or for the game they brought (D-89); in Codex with spawn_agent by name,
13
+ * since a chat there called analyse_game itself instead (Priya).
14
+ */
15
+ export declare function analystLine(game?: string, codex?: boolean): string;
16
+ /**
17
+ * The analyst's line for the chat that looks at the game in analysis with no review and no analyst at it, as after «Delete the analysis», which rings no
18
+ * doorbell (Priya): once a change of the analysis, where the server keeps what it told.
19
+ */
20
+ export declare function analystOnView(context: Context, game: string | undefined): string | undefined;
13
21
  /** How the chat goes over a game the analyst agent has reviewed, or is reviewing: from that review, never with another agent (Stefano). */
14
22
  export declare function reviewedLine(game?: string): string;
15
23
  /** Whether a coach judges the player's moves when the helps are on: in Claude Code or Codex, with its agent in the player's folder (D-80, D-88, D-98). */
@@ -17,15 +25,15 @@ export declare function coachRuns(context: Context): boolean;
17
25
  /** Whether one of Chesspal's agents runs beside this chat: in a host that starts agents, with its file in the player's folder. */
18
26
  export declare function agentRuns(context: Context, agent: string): boolean;
19
27
  /**
20
- * Whether to start the coach after the player's own move has ended the game on the board, with the helps on: it judges that move too (D-88). Told again
21
- * until it starts, since a chat woken by the page may lose the line on its way to its words (Priya).
28
+ * Whether to start the coach once a move has ended the game on the board, with the helps on: it judges the player's last move, their own that ended it too
29
+ * (D-88). Told again until it starts, since a chat busy telling the end may lose the line on its way to its words (Priya).
22
30
  */
23
31
  export declare function coachOnEnd(context: Context, folder: string, current: CurrentGame): boolean;
24
32
  /**
25
- * How to start the coach on the player's move that ended the game: whatever the player asks, beside the analyst when the chat starts it too, since a chat on
26
- * a quick model started only the agent that answered the player (Priya).
33
+ * How to start the coach once a move ended the game, on the player's last move, theirs that ended it or the one before the AI's: whatever the player asks,
34
+ * beside the analyst when the chat starts it too, since a chat on a quick model started only the agent that answered the player (Priya).
27
35
  */
28
- export declare const coachOnEndLine: string;
36
+ export declare function coachOnEndLine({ turn, ai }: GameSummary): string;
29
37
  /**
30
38
  * How to start the agents, in one message, the one that thinks first, since it needs the time (Priya), and that the player sees nothing of
31
39
  * them: their end needs no answer either. The end of the last move's agents, told just after, must not take their place (Priya).
@@ -41,5 +49,7 @@ export declare function goneOver(context: Context, game: string): boolean;
41
49
  * agent with it, or it has written nothing for half an hour, as one long dead (D-109): a quiet minute proves nothing, since an analyst may think long.
42
50
  */
43
51
  export declare function analystOf(context: Context, game: string): string | undefined;
52
+ /** When the analyst that holds a game counts as gone if it writes nothing before: none while no analyst holds one. */
53
+ export declare function analystQuietAt(context: Context): Date | undefined;
44
54
  /** What stops an analyst agent on a game another analyst has taken: the chat hears of the review from that one (Priya). */
45
55
  export declare const anotherAnalyst: string;
@@ -1,7 +1,7 @@
1
1
  // The agents that work beside the chat in Claude Code and Codex (D-98): the analyst that goes over a game over (D-89) and the coach that judges the player's moves (D-88).
2
2
  import { isReviewed } from "../../core/index.js";
3
3
  import { archivedName, coachStarted, gameInAnalysis, lastChatStart } from "../../games/index.js";
4
- import { agentHosts, analystAgent, coachAgent, hasAgent } from "../../host-files/index.js";
4
+ import { agentHosts, analystAgent, coachAgent, hasAgent, isCodex } from "../../host-files/index.js";
5
5
  import { gameFolder, helpsIn } from "../../profiles/index.js";
6
6
  import { namedGame } from "./study.js";
7
7
  import { unplayed } from "./summary.js";
@@ -20,13 +20,36 @@ export function withAnalyst(summary, context, over) {
20
20
  return summary;
21
21
  }
22
22
  const archived = over === undefined ? undefined : archivedName(gameFolder(context.home()), over);
23
- return { ...summary, end: { ...end, analyst: archived !== undefined && goneOver(context, archived) ? "done" : "offer" } };
23
+ const analyst = archived !== undefined && goneOver(context, archived) ? "done" : "offer";
24
+ return { ...summary, end: { ...end, analyst, ...(isCodex(context.host?.()) ? { codex: true } : {}) } };
24
25
  }
25
- /** How the chat has a game over gone over by the analyst agent, on the player's yes or for the game they brought (D-89). */
26
- export function analystLine(game) {
27
- return `start the chesspal-analyst agent in the background with the prompt "Analyse${game === undefined ? "" : ` ${game}`}. The player speaks <their language>.": it writes its review on the board `
28
- + "page, which shows it at work; tell the player in a word, talk the game over with them meanwhile whenever they ask, and when it ends go over it with them "
29
- + "from what it tells you.";
26
+ /**
27
+ * How the chat has a game over gone over by the analyst agent, on the player's yes or for the game they brought (D-89); in Codex with spawn_agent by name,
28
+ * since a chat there called analyse_game itself instead (Priya).
29
+ */
30
+ export function analystLine(game, codex = false) {
31
+ const prompt = `Analyse${game === undefined ? "" : ` ${game}`}. The player speaks <their language>.`;
32
+ const start = codex
33
+ ? `start the ${analystAgent} agent with spawn_agent {"agent_type": "${analystAgent}", "message": "${prompt}", "fork_turns": "none"}`
34
+ : `start the ${analystAgent} agent in the background with the prompt "${prompt}"`;
35
+ return `${start}: it writes its review on the board page, which shows it at work; tell the player in a word, talk the game over with them meanwhile whenever `
36
+ + "they ask, and when it ends go over it with them from what it tells you.";
37
+ }
38
+ /**
39
+ * The analyst's line for the chat that looks at the game in analysis with no review and no analyst at it, as after «Delete the analysis», which rings no
40
+ * doorbell (Priya): once a change of the analysis, where the server keeps what it told.
41
+ */
42
+ export function analystOnView(context, game) {
43
+ if (game === undefined || context.agent !== undefined || !analystRuns(context)) {
44
+ return undefined;
45
+ }
46
+ const analysis = gameInAnalysis(gameFolder(context.home()));
47
+ const key = `analyst on view ${game} ${analysis?.at ?? ""}`;
48
+ if (analysis?.game !== game || context.told?.has(key) === true || goneOver(context, game)) {
49
+ return undefined;
50
+ }
51
+ context.told?.add(key);
52
+ return `${game} has no review. When the player wants it gone over, ${analystLine(game, isCodex(context.host?.()))}`;
30
53
  }
31
54
  /** How the chat goes over a game the analyst agent has reviewed, or is reviewing: from that review, never with another agent (Stefano). */
32
55
  export function reviewedLine(game) {
@@ -43,20 +66,22 @@ export function agentRuns(context, agent) {
43
66
  return host !== undefined && agentHosts.includes(host) && hasAgent(context.folder, agent, host);
44
67
  }
45
68
  /**
46
- * Whether to start the coach after the player's own move has ended the game on the board, with the helps on: it judges that move too (D-88). Told again
47
- * until it starts, since a chat woken by the page may lose the line on its way to its words (Priya).
69
+ * Whether to start the coach once a move has ended the game on the board, with the helps on: it judges the player's last move, their own that ended it too
70
+ * (D-88). Told again until it starts, since a chat busy telling the end may lose the line on its way to its words (Priya).
48
71
  */
49
72
  export function coachOnEnd(context, folder, current) {
50
- const { game, ai } = current;
51
- return game.outcome.over && game.position.turn === ai && helpsIn(folder) && coachRuns(context) && !coachStarted(folder, current);
73
+ return current.game.outcome.over && helpsIn(folder) && coachRuns(context) && !coachStarted(folder, current);
52
74
  }
53
75
  /**
54
- * How to start the coach on the player's move that ended the game: whatever the player asks, beside the analyst when the chat starts it too, since a chat on
55
- * a quick model started only the agent that answered the player (Priya).
76
+ * How to start the coach once a move ended the game, on the player's last move, theirs that ended it or the one before the AI's: whatever the player asks,
77
+ * beside the analyst when the chat starts it too, since a chat on a quick model started only the agent that answered the player (Priya).
56
78
  */
57
- export const coachOnEndLine = `Coach the player: start the ${coachAgent} agent in the background with the prompt "Coach.", whatever the player asks: it judges `
58
- + "the move that ended the game, apart from the analyst; when you start the analyst too, start both in one message. Start it even when an agent of the move "
59
- + "before has just ended, and write nothing about it in the chat, now or when it ends.";
79
+ export function coachOnEndLine({ turn, ai }) {
80
+ const judged = turn === ai ? "the move that ended the game" : "the player's last move, the one before the end";
81
+ return `Coach the player: start the ${coachAgent} agent in the background with the prompt "Coach.", whatever the player asks: it judges ${judged}, apart `
82
+ + "from the analyst; when you start the analyst too, start both in one message. Start it even when an agent of the move before has just ended, and write "
83
+ + "nothing about it in the chat, now or when it ends.";
84
+ }
60
85
  /**
61
86
  * How to start the agents, in one message, the one that thinks first, since it needs the time (Priya), and that the player sees nothing of
62
87
  * them: their end needs no answer either. The end of the last move's agents, told just after, must not take their place (Priya).
@@ -100,6 +125,11 @@ export function analystOf(context, game) {
100
125
  const quiet = analysis !== undefined && context.now().getTime() - Date.parse(analysis.at) > analystQuiet;
101
126
  return taken?.game === game && !restarted && !quiet ? taken.run : undefined;
102
127
  }
128
+ /** When the analyst that holds a game counts as gone if it writes nothing before: none while no analyst holds one. */
129
+ export function analystQuietAt(context) {
130
+ const analysis = gameInAnalysis(gameFolder(context.home()));
131
+ return analysis?.analyst === undefined ? undefined : new Date(Date.parse(analysis.at) + analystQuiet);
132
+ }
103
133
  /** How long an analyst may write nothing before it counts as gone. */
104
134
  const analystQuiet = 30 * 60 * 1000;
105
135
  /** What stops an analyst agent on a game another analyst has taken: the chat hears of the review from that one (Priya). */