chesspal 0.0.1 → 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 (273) hide show
  1. package/README.md +99 -2
  2. package/dist/cli/chat-hook.d.ts +2 -0
  3. package/dist/cli/chat-hook.js +33 -0
  4. package/dist/cli/cli.d.ts +2 -0
  5. package/dist/cli/cli.js +28 -0
  6. package/dist/cli/command-line.d.ts +7 -0
  7. package/dist/cli/command-line.js +177 -0
  8. package/dist/commands/agents.d.ts +26 -0
  9. package/dist/commands/agents.js +72 -0
  10. package/dist/commands/ai-move.d.ts +28 -0
  11. package/dist/commands/ai-move.js +85 -0
  12. package/dist/commands/ai-say.d.ts +22 -0
  13. package/dist/commands/ai-say.js +58 -0
  14. package/dist/commands/analyse-game.d.ts +65 -0
  15. package/dist/commands/analyse-game.js +265 -0
  16. package/dist/commands/annotate-game.d.ts +39 -0
  17. package/dist/commands/annotate-game.js +131 -0
  18. package/dist/commands/answer-lines.d.ts +28 -0
  19. package/dist/commands/answer-lines.js +144 -0
  20. package/dist/commands/clock.d.ts +15 -0
  21. package/dist/commands/clock.js +45 -0
  22. package/dist/commands/coach.d.ts +45 -0
  23. package/dist/commands/coach.js +344 -0
  24. package/dist/commands/context.d.ts +67 -0
  25. package/dist/commands/context.js +37 -0
  26. package/dist/commands/delete-game.d.ts +7 -0
  27. package/dist/commands/delete-game.js +30 -0
  28. package/dist/commands/delete-review.d.ts +8 -0
  29. package/dist/commands/delete-review.js +34 -0
  30. package/dist/commands/draw-offer.d.ts +10 -0
  31. package/dist/commands/draw-offer.js +43 -0
  32. package/dist/commands/drawings.d.ts +13 -0
  33. package/dist/commands/drawings.js +25 -0
  34. package/dist/commands/end-game.d.ts +23 -0
  35. package/dist/commands/end-game.js +67 -0
  36. package/dist/commands/export-game.d.ts +14 -0
  37. package/dist/commands/export-game.js +27 -0
  38. package/dist/commands/game-summary.d.ts +78 -0
  39. package/dist/commands/game-summary.js +102 -0
  40. package/dist/commands/index.d.ts +18 -0
  41. package/dist/commands/index.js +64 -0
  42. package/dist/commands/init.d.ts +15 -0
  43. package/dist/commands/init.js +73 -0
  44. package/dist/commands/lead-input.d.ts +8 -0
  45. package/dist/commands/lead-input.js +21 -0
  46. package/dist/commands/levels.d.ts +9 -0
  47. package/dist/commands/levels.js +25 -0
  48. package/dist/commands/look.d.ts +31 -0
  49. package/dist/commands/look.js +64 -0
  50. package/dist/commands/new-game.d.ts +68 -0
  51. package/dist/commands/new-game.js +218 -0
  52. package/dist/commands/opening-book.d.ts +13 -0
  53. package/dist/commands/opening-book.js +84 -0
  54. package/dist/commands/page-link.d.ts +11 -0
  55. package/dist/commands/page-link.js +28 -0
  56. package/dist/commands/past-games.d.ts +11 -0
  57. package/dist/commands/past-games.js +47 -0
  58. package/dist/commands/play-move.d.ts +37 -0
  59. package/dist/commands/play-move.js +94 -0
  60. package/dist/commands/player-move.d.ts +13 -0
  61. package/dist/commands/player-move.js +51 -0
  62. package/dist/commands/player-say.d.ts +5 -0
  63. package/dist/commands/player-say.js +16 -0
  64. package/dist/commands/ponder.d.ts +52 -0
  65. package/dist/commands/ponder.js +179 -0
  66. package/dist/commands/position.d.ts +14 -0
  67. package/dist/commands/position.js +130 -0
  68. package/dist/commands/profiles.d.ts +69 -0
  69. package/dist/commands/profiles.js +163 -0
  70. package/dist/commands/registry.d.ts +59 -0
  71. package/dist/commands/registry.js +62 -0
  72. package/dist/commands/say.d.ts +16 -0
  73. package/dist/commands/say.js +84 -0
  74. package/dist/commands/speech.d.ts +8 -0
  75. package/dist/commands/speech.js +20 -0
  76. package/dist/commands/status.d.ts +35 -0
  77. package/dist/commands/status.js +128 -0
  78. package/dist/commands/study-games.d.ts +5 -0
  79. package/dist/commands/study-games.js +40 -0
  80. package/dist/commands/study.d.ts +38 -0
  81. package/dist/commands/study.js +106 -0
  82. package/dist/commands/view.d.ts +14 -0
  83. package/dist/commands/view.js +125 -0
  84. package/dist/core/board-text.d.ts +7 -0
  85. package/dist/core/board-text.js +18 -0
  86. package/dist/core/book.json +1 -0
  87. package/dist/core/clock.d.ts +45 -0
  88. package/dist/core/clock.js +109 -0
  89. package/dist/core/facts.d.ts +55 -0
  90. package/dist/core/facts.js +175 -0
  91. package/dist/core/game.d.ts +95 -0
  92. package/dist/core/game.js +352 -0
  93. package/dist/core/illegal.d.ts +19 -0
  94. package/dist/core/illegal.js +141 -0
  95. package/dist/core/index.d.ts +20 -0
  96. package/dist/core/index.js +12 -0
  97. package/dist/core/lead.d.ts +15 -0
  98. package/dist/core/lead.js +40 -0
  99. package/dist/core/levels.d.ts +20 -0
  100. package/dist/core/levels.js +33 -0
  101. package/dist/core/material.d.ts +9 -0
  102. package/dist/core/material.js +12 -0
  103. package/dist/core/moods.d.ts +21 -0
  104. package/dist/core/moods.js +45 -0
  105. package/dist/core/openings.d.ts +35 -0
  106. package/dist/core/openings.js +98 -0
  107. package/dist/core/openings.json +1 -0
  108. package/dist/core/pgn.d.ts +10 -0
  109. package/dist/core/pgn.js +213 -0
  110. package/dist/core/profile.d.ts +117 -0
  111. package/dist/core/profile.js +232 -0
  112. package/dist/core/record.d.ts +21 -0
  113. package/dist/core/record.js +25 -0
  114. package/dist/core/review.d.ts +80 -0
  115. package/dist/core/review.js +146 -0
  116. package/dist/games/ai-model.d.ts +5 -0
  117. package/dist/games/ai-model.js +15 -0
  118. package/dist/games/analysis.d.ts +12 -0
  119. package/dist/games/analysis.js +26 -0
  120. package/dist/games/archive.d.ts +63 -0
  121. package/dist/games/archive.js +242 -0
  122. package/dist/games/chat-start.d.ts +16 -0
  123. package/dist/games/chat-start.js +26 -0
  124. package/dist/games/coach.d.ts +57 -0
  125. package/dist/games/coach.js +144 -0
  126. package/dist/games/current-game.d.ts +56 -0
  127. package/dist/games/current-game.js +134 -0
  128. package/dist/games/files.d.ts +9 -0
  129. package/dist/games/files.js +53 -0
  130. package/dist/games/index.d.ts +11 -0
  131. package/dist/games/index.js +12 -0
  132. package/dist/games/plans.d.ts +9 -0
  133. package/dist/games/plans.js +23 -0
  134. package/dist/games/table-talk.d.ts +12 -0
  135. package/dist/games/table-talk.js +25 -0
  136. package/dist/games/talk.d.ts +36 -0
  137. package/dist/games/talk.js +131 -0
  138. package/dist/games/thoughts.d.ts +15 -0
  139. package/dist/games/thoughts.js +60 -0
  140. package/dist/home/data-folder.d.ts +12 -0
  141. package/dist/home/data-folder.js +55 -0
  142. package/dist/home/index.d.ts +2 -0
  143. package/dist/home/index.js +3 -0
  144. package/dist/home/open-folder.d.ts +2 -0
  145. package/dist/home/open-folder.js +14 -0
  146. package/dist/host-files/agents.d.ts +20 -0
  147. package/dist/host-files/agents.js +53 -0
  148. package/dist/host-files/host-files.d.ts +15 -0
  149. package/dist/host-files/host-files.js +182 -0
  150. package/dist/host-files/index.d.ts +2 -0
  151. package/dist/host-files/index.js +3 -0
  152. package/dist/index.d.ts +2 -0
  153. package/dist/index.js +3 -0
  154. package/dist/page/app/anchor.js +88 -0
  155. package/dist/page/app/app.js +458 -0
  156. package/dist/page/app/keys.js +94 -0
  157. package/dist/page/app/phase.js +30 -0
  158. package/dist/page/app/served.js +73 -0
  159. package/dist/page/avatars/avatars.js +19 -0
  160. package/dist/page/avatars/beanie.webp +0 -0
  161. package/dist/page/avatars/beard.webp +0 -0
  162. package/dist/page/avatars/bun.webp +0 -0
  163. package/dist/page/avatars/cat.webp +0 -0
  164. package/dist/page/avatars/curls.webp +0 -0
  165. package/dist/page/avatars/glasses.webp +0 -0
  166. package/dist/page/avatars/moustache.webp +0 -0
  167. package/dist/page/avatars/owl.webp +0 -0
  168. package/dist/page/avatars/short-hair.webp +0 -0
  169. package/dist/page/avatars/silver-bob.webp +0 -0
  170. package/dist/page/avatars/thought.js +10 -0
  171. package/dist/page/avatars/torretta.js +38 -0
  172. package/dist/page/base.css +160 -0
  173. package/dist/page/board/board.css +959 -0
  174. package/dist/page/board/board.js +125 -0
  175. package/dist/page/board/explain.js +58 -0
  176. package/dist/page/board/marks.js +160 -0
  177. package/dist/page/board/motion.js +71 -0
  178. package/dist/page/board/pointer.js +239 -0
  179. package/dist/page/board/squares.js +26 -0
  180. package/dist/page/dialogs/analyses.js +195 -0
  181. package/dist/page/dialogs/dialog.js +26 -0
  182. package/dist/page/dialogs/dialogs.css +1228 -0
  183. package/dist/page/dialogs/new-game.js +42 -0
  184. package/dist/page/dialogs/profiles.js +152 -0
  185. package/dist/page/dialogs/search.js +31 -0
  186. package/dist/page/dialogs/settings.js +42 -0
  187. package/dist/page/dialogs/studied.js +39 -0
  188. package/dist/page/dialogs/study.js +51 -0
  189. package/dist/page/game/badges.js +50 -0
  190. package/dist/page/game/browse.js +17 -0
  191. package/dist/page/game/card-fold.js +40 -0
  192. package/dist/page/game/column.js +51 -0
  193. package/dist/page/game/ending.js +73 -0
  194. package/dist/page/game/game.css +577 -0
  195. package/dist/page/game/move-list.js +72 -0
  196. package/dist/page/game/opening-book.js +14 -0
  197. package/dist/page/game/plies.js +82 -0
  198. package/dist/page/game/result-tile.js +14 -0
  199. package/dist/page/helps/helps.css +689 -0
  200. package/dist/page/helps/helps.js +220 -0
  201. package/dist/page/helps/view.js +127 -0
  202. package/dist/page/layout/fold-tab.js +11 -0
  203. package/dist/page/layout/folds.js +43 -0
  204. package/dist/page/layout/header.js +28 -0
  205. package/dist/page/layout/layout.css +834 -0
  206. package/dist/page/lead/lead.js +82 -0
  207. package/dist/page/lead/view.js +38 -0
  208. package/dist/page/main.js +8 -0
  209. package/dist/page/pieces/CREDITS.md +19 -0
  210. package/dist/page/pieces/capablanca-small.svg +20 -0
  211. package/dist/page/pieces/capablanca.svg +22 -0
  212. package/dist/page/pieces/nitida-small.svg +20 -0
  213. package/dist/page/pieces/nitida.svg +21 -0
  214. package/dist/page/pieces/pieces.js +28 -0
  215. package/dist/page/pieces/torretta-small.svg +20 -0
  216. package/dist/page/pieces/torretta.svg +21 -0
  217. package/dist/page/players/card.js +56 -0
  218. package/dist/page/players/clock-time.js +12 -0
  219. package/dist/page/players/model-name.js +20 -0
  220. package/dist/page/profile/profile.js +99 -0
  221. package/dist/page/profile/settings.js +77 -0
  222. package/dist/page/sounds/capture.ogg +0 -0
  223. package/dist/page/sounds/check.ogg +0 -0
  224. package/dist/page/sounds/draw.ogg +0 -0
  225. package/dist/page/sounds/flag.ogg +0 -0
  226. package/dist/page/sounds/mate.ogg +0 -0
  227. package/dist/page/sounds/move.ogg +0 -0
  228. package/dist/page/sounds/offer.ogg +0 -0
  229. package/dist/page/sounds/resign.ogg +0 -0
  230. package/dist/page/sounds/sounds.js +105 -0
  231. package/dist/page/style.css +9 -0
  232. package/dist/page/talk/talk.css +958 -0
  233. package/dist/page/talk/talk.js +165 -0
  234. package/dist/page/talk/voice.js +38 -0
  235. package/dist/page/text.js +711 -0
  236. package/dist/page/theme.css +292 -0
  237. package/dist/profiles/avatar.d.ts +23 -0
  238. package/dist/profiles/avatar.js +84 -0
  239. package/dist/profiles/front-matter.d.ts +9 -0
  240. package/dist/profiles/front-matter.js +33 -0
  241. package/dist/profiles/index.d.ts +3 -0
  242. package/dist/profiles/index.js +5 -0
  243. package/dist/profiles/profiles.d.ts +69 -0
  244. package/dist/profiles/profiles.js +254 -0
  245. package/dist/profiles/settings.d.ts +12 -0
  246. package/dist/profiles/settings.js +46 -0
  247. package/dist/server/bell.d.ts +68 -0
  248. package/dist/server/bell.js +225 -0
  249. package/dist/server/caller.d.ts +21 -0
  250. package/dist/server/caller.js +132 -0
  251. package/dist/server/mcp.d.ts +13 -0
  252. package/dist/server/mcp.js +207 -0
  253. package/dist/server/page/analyses.d.ts +23 -0
  254. package/dist/server/page/analyses.js +65 -0
  255. package/dist/server/page/files.d.ts +5 -0
  256. package/dist/server/page/files.js +92 -0
  257. package/dist/server/page/game.d.ts +6 -0
  258. package/dist/server/page/game.js +381 -0
  259. package/dist/server/page/http.d.ts +29 -0
  260. package/dist/server/page/http.js +68 -0
  261. package/dist/server/page/page.d.ts +41 -0
  262. package/dist/server/page/page.js +229 -0
  263. package/dist/server/page/profiles.d.ts +19 -0
  264. package/dist/server/page/profiles.js +215 -0
  265. package/dist/version.d.ts +2 -0
  266. package/dist/version.js +3 -0
  267. package/instructions/analysis.md +4 -0
  268. package/instructions/analyst-agent.md +8 -0
  269. package/instructions/analyst.md +4 -0
  270. package/instructions/coach.md +11 -0
  271. package/instructions/opponent.md +15 -0
  272. package/instructions/ponder.md +9 -0
  273. package/package.json +66 -5
package/README.md CHANGED
@@ -1,5 +1,102 @@
1
1
  # Chesspal
2
2
 
3
- Play chess against an AI with a character, and learn with an AI coach that remembers your games and helps you improve.
3
+ Play chess against an AI on a real board, or learn with one.
4
4
 
5
- In development: this is a placeholder for the first release.
5
+ ![The board page during a game: the AI's words on the left, the board in the middle, the opening's name and the moves on the right](https://raw.githubusercontent.com/stefanocaronia/chesspal/main/docs/screenshot.png)
6
+
7
+ ## Get started
8
+
9
+ You need [Node.js](https://nodejs.org) 22.13 or newer and an AI agent on your computer, [Claude Code](https://code.claude.com/docs/en/setup) or [Codex](https://developers.openai.com/codex): see [what you need](#what-you-need). Open a new terminal, after installing Node.js (one opened before does not find `npm`), and type:
10
+
11
+ ```bash
12
+ npm install -g chesspal
13
+ chesspal init chess
14
+ cd chess
15
+ claude
16
+ ```
17
+
18
+ - `npm install -g` installs the `chesspal` command for the whole computer, where your agent finds it.
19
+ - `chesspal init chess` makes the folder `chess` to play in. Any name will do; your games are kept [elsewhere](#where-your-games-are).
20
+ - `claude` opens Claude Code in that folder (in the Claude desktop app, choose the folder in the Code tab). With Codex, type `codex`, and when it asks about Chesspal's two hooks, choose **Trust all and continue**.
21
+ - The first time, your agent asks whether you trust the folder: say yes.
22
+
23
+ Then **say hello**. The AI answers with the link to the board page: open it in your browser, tell it who you are, and ask for a game. You choose your colour, the clock and the AI's level, and play on the board.
24
+
25
+ On Windows, use Command Prompt (press Start, type `cmd`, press Enter). PowerShell, on a new computer, refuses npm's commands with "running scripts is disabled on this system"; there, type `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` once and answer yes, or use Command Prompt. Codex on Windows also offers to set up its sandbox the first time: choose the first option, and answer yes when Windows asks for permission.
26
+
27
+ ## What it does
28
+
29
+ An AI in a plain chat forgets the position, plays illegal moves and sees no board. Chesspal is what it lacks: it **keeps the rules, the board, the clock and your games**; the AI **plays, reasons and talks**. Every move is checked by Chesspal, and no chess engine plays for the AI: every judgment is its own, made from the facts Chesspal hands it, such as the legal moves, the pieces attacked and defended, the pins and the opening's name.
30
+
31
+ - **Play on a board** in your browser, and talk with the AI there or in the chat.
32
+ - **Choose its level**, from beginner to master: the higher, the more of the position the AI sees.
33
+ - **Play with a clock**, against an AI that knows how much time it has left; offer a draw, resign, save the game as PGN.
34
+ - **Go over a game** with the AI when it is over: its notes show on the board page, in Analyses. Games from other sites come in as PGN files.
35
+ - **Keep a profile** for each player at the same computer, with their games.
36
+
37
+ Coming later: an AI coach that helps you grow, exercises chosen on your weak points, a rating worked out from your games, an opponent with a character and moods of its own, and local models.
38
+
39
+ ## What you need
40
+
41
+ - **Node.js 22.13** or newer: download the version marked LTS and install it like any program.
42
+ - **An AI agent** with your own subscription: Claude Code, with a paid Claude plan, or Codex, with a ChatGPT account. Chesspal never uses an API key, which is paid by the call: sign your agent in with your account.
43
+ - **A web browser**, for the board page.
44
+
45
+ Chesspal works with any agent that supports MCP tools and can work with the files on your computer: we develop it with Claude Code, and Codex is connected too.
46
+
47
+ ## Good to know
48
+
49
+ - **For the next game**, type `/clear` in the chat or start a new one: a long chat makes the AI slower and costs you more.
50
+ - **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.
51
+ - **To update**, close the chat, run `npm install -g chesspal` again, then `chesspal init` in your folder: it updates its own lines and keeps what you wrote there. In Codex, allow the hooks again when it says so.
52
+
53
+ ## Where your games are
54
+
55
+ In one folder for every folder you play in:
56
+
57
+ - Windows: `Documents › My Games › Chesspal`, inside OneDrive if you use it;
58
+ - macOS: `Documents › Chesspal`; Linux: `Chesspal` in your Documents folder.
59
+
60
+ The board page shows where, and its **Open the folder** button opens it. To back up your games, copy that whole folder; to move them to another computer, put the copy in the same place there. Do it with the chat and the board page closed.
61
+
62
+ ## Try it, and tell us
63
+
64
+ Chesspal is in early development: the version on npm is a first one to try, and it may change from one release to the next. When something breaks, confuses you or could work better, open an [issue on GitHub](https://github.com/stefanocaronia/chesspal/issues): what you did, what you expected, what happened.
65
+
66
+ ## Development
67
+
68
+ Chesspal is an MCP server, a command line and a board page, all made from the same commands, which check every move with [chess.js](https://github.com/jhlywa/chess.js) and write the games atomically.
69
+
70
+ ```bash
71
+ git clone https://github.com/stefanocaronia/chesspal.git
72
+ cd chesspal
73
+ npm install
74
+ npm run build
75
+ npm test
76
+ ```
77
+
78
+ [The guide for development](https://github.com/stefanocaronia/chesspal/blob/main/AGENTS.md), for people and AI coding agents alike, holds the other scripts, the layout, the architecture's rules and the coding guidelines.
79
+
80
+ ## Credits
81
+
82
+ - The rules: [chess.js](https://github.com/jhlywa/chess.js), under the BSD 2-Clause license.
83
+ - The openings' names: Lichess's [chess-openings](https://github.com/lichess-org/chess-openings), under CC0.
84
+ - The pieces: Nitida, Torretta and Capablanca are Chesspal's own, the last after the diagrams of Capablanca's *Chess Fundamentals* (1921), in the public domain; they are free to use under CC BY 4.0, crediting "Stefano Caronia, Chesspal". [The piece sets' credits](https://github.com/stefanocaronia/chesspal/blob/main/src/page/pieces/CREDITS.md).
85
+
86
+ Chesspal ships only original or open-licensed content, and no GPL dependency.
87
+
88
+ ## License
89
+
90
+ [MIT](https://github.com/stefanocaronia/chesspal/blob/main/LICENSE).
91
+
92
+ ## Development notes
93
+
94
+ Developed with the help of AI coding agents. Everything is designed, reviewed and tested by the project's human maintainer.
95
+
96
+ **We encourage everyone to use AI for their own purposes.**\
97
+ The means of production should belong to the people.\
98
+ Fight corporate control of technology, not our right to use it.
99
+
100
+ ---
101
+
102
+ © 2026 [Stefano Caronia](https://stefanocaronia.it) · [GitHub](https://github.com/stefanocaronia/chesspal) · [npm](https://www.npmjs.com/package/chesspal)
@@ -0,0 +1,2 @@
1
+ /** Reads the hook's input and writes how the chat started; a failure is told on stderr and never stops the chat. */
2
+ export declare function recordChatStart(input?: NodeJS.ReadableStream, folder?: string, now?: Date): Promise<void>;
@@ -0,0 +1,33 @@
1
+ // `chesspal chat`, the SessionStart hook of Claude Code and Codex (D-57): it writes how the chat started, new, cleared, resumed or compacted, for new_game to read.
2
+ import { saveChatStart } from "../games/index.js";
3
+ /** Reads the hook's input and writes how the chat started; a failure is told on stderr and never stops the chat. */
4
+ export async function recordChatStart(input = process.stdin, folder = process.cwd(), now = new Date()) {
5
+ try {
6
+ const hook = parsed(await textOf(input));
7
+ saveChatStart(folder, {
8
+ source: typeof hook.source === "string" ? hook.source : "startup",
9
+ at: now.toISOString(),
10
+ ...(typeof hook.session_id === "string" ? { session: hook.session_id } : {}),
11
+ ...(typeof hook.transcript_path === "string" ? { transcript: hook.transcript_path } : {}),
12
+ });
13
+ }
14
+ catch (error) {
15
+ process.stderr.write(`Chesspal could not note how the chat started: ${error.message}\n`);
16
+ }
17
+ }
18
+ async function textOf(input) {
19
+ let text = "";
20
+ for await (const chunk of input) {
21
+ text += String(chunk);
22
+ }
23
+ return text;
24
+ }
25
+ function parsed(text) {
26
+ try {
27
+ const value = JSON.parse(text);
28
+ return typeof value === "object" && value !== null ? value : {};
29
+ }
30
+ catch {
31
+ return {};
32
+ }
33
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import { runCommandLine } from "./command-line.js";
3
+ const args = process.argv.slice(2);
4
+ // "chesspal mcp" turns the process into a server for an AI host, "chesspal listen" into the doorbell of Claude Code, or of Codex with --codex,
5
+ // "chesspal chat" notes how the chat started; everything else is a command.
6
+ if (args.length === 1 && args[0] === "mcp") {
7
+ // The server and its SDK load only here: every other command starts faster without them.
8
+ const { startMcpServer } = await import("../server/mcp.js");
9
+ await startMcpServer();
10
+ }
11
+ else if (args[0] === "listen" && (args.length === 1 || (args.length === 2 && args[1] === "--codex"))) {
12
+ const { listen } = await import("../server/bell.js");
13
+ await listen(args[1] === "--codex");
14
+ }
15
+ else if (args.length === 1 && args[0] === "chat") {
16
+ const { recordChatStart } = await import("./chat-hook.js");
17
+ await recordChatStart();
18
+ }
19
+ else {
20
+ const { output, exitCode } = await runCommandLine(args);
21
+ if (exitCode === 0) {
22
+ console.log(output);
23
+ }
24
+ else {
25
+ console.error(output);
26
+ }
27
+ process.exitCode = exitCode;
28
+ }
@@ -0,0 +1,7 @@
1
+ import { type AnyCommand, type Context } from "../commands/index.ts";
2
+ export interface CommandLineResult {
3
+ output: string;
4
+ exitCode: number;
5
+ }
6
+ /** The `chesspal` command line: finds the command, validates its input, runs it and renders the result. */
7
+ export declare function runCommandLine(argv: string[], context?: Context, list?: AnyCommand[]): Promise<CommandLineResult>;
@@ -0,0 +1,177 @@
1
+ // The `chesspal` command line: a command's input read from the arguments, its result written for a person.
2
+ import { parseArgs } from "node:util";
3
+ import { baseType, commandLineFieldProblem, commands, defaultContext, inputProblems, isBoolean, isList, takesNumbers } from "../commands/index.js";
4
+ import { version } from "../version.js";
5
+ // The commands that exist only on the command line: cli.ts starts the server, the doorbell or the chat's note, when one comes alone.
6
+ 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.";
7
+ 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.";
8
+ 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.";
9
+ /** The `chesspal` command line: finds the command, validates its input, runs it and renders the result. */
10
+ export async function runCommandLine(argv, context = defaultContext(), list = commands) {
11
+ const [commandName, ...args] = argv;
12
+ if (commandName === undefined || commandName === "--help" || commandName === "-h") {
13
+ return success(generalHelp(list));
14
+ }
15
+ if (commandName === "--version" || commandName === "-v") {
16
+ return success(version);
17
+ }
18
+ if (commandName === "mcp") {
19
+ // A host that adds options to it would get this text instead of the protocol: it must see the failure.
20
+ const help = `chesspal mcp\n\n${mcpDescription}`;
21
+ return args.length === 0 || args.includes("--help") || args.includes("-h") ? success(help) : failure(`chesspal mcp takes no options: ${args.join(" ")}.\n\n${help}`);
22
+ }
23
+ if (commandName === "listen") {
24
+ const help = `chesspal listen [--codex]\n\n${listenDescription}`;
25
+ const known = args.every((arg) => ["--codex", "--help", "-h"].includes(arg));
26
+ return known ? success(help) : failure(`chesspal listen takes only --codex: ${args.join(" ")}.\n\n${help}`);
27
+ }
28
+ if (commandName === "chat") {
29
+ const help = `chesspal chat\n\n${chatDescription}`;
30
+ return args.length === 0 || args.includes("--help") || args.includes("-h") ? success(help) : failure(`chesspal chat takes no options: ${args.join(" ")}.\n\n${help}`);
31
+ }
32
+ const command = list.find((candidate) => candidate.name === commandName);
33
+ if (!command) {
34
+ return failure(`Unknown command "${commandName}".\n\n${generalHelp(list)}`);
35
+ }
36
+ if (args.includes("--help") || args.includes("-h")) {
37
+ return success(commandHelp(command));
38
+ }
39
+ try {
40
+ return success(await runCommand(command, args, context));
41
+ }
42
+ catch (error) {
43
+ return failure(error instanceof Error ? error.message : String(error));
44
+ }
45
+ }
46
+ async function runCommand(command, args, context) {
47
+ const { input, asJson } = readArguments(command, args);
48
+ const parsed = command.input.safeParse(input);
49
+ if (!parsed.success) {
50
+ const positionals = command.positionals ?? [];
51
+ const written = (name) => (positionals.includes(name) ? `<${name}>` : `--${name}`);
52
+ throw refused(command, inputProblems(command.name, parsed.error.issues, input, written));
53
+ }
54
+ const result = await command.run(parsed.data, context);
55
+ return asJson ? JSON.stringify(result, null, 2) : command.format(result, context);
56
+ }
57
+ /** An input refused, in the words the MCP server uses (inputProblems), with the command's usage. */
58
+ function refused(command, problems) {
59
+ return new Error(`${problems} Nothing changed.\nUsage: ${usage(command)}\nRun \`chesspal ${command.name} --help\` for its options.`);
60
+ }
61
+ /** The command line's own refusals, from parseArgs, in the same words: an unknown option is a parameter the command does not have. */
62
+ function parsedArguments(command, config) {
63
+ try {
64
+ return parseArgs(config);
65
+ }
66
+ catch (error) {
67
+ const { code, message } = error;
68
+ const option = /'(-[^' ]+)/.exec(message)?.[1] ?? "";
69
+ if (code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
70
+ // A positional written as an option: the command has it, in another place.
71
+ const name = option.replace(/^-+/, "");
72
+ throw refused(command, command.positionals?.includes(name) === true
73
+ ? `Give ${name} as <${name}>, without ${option}.`
74
+ : inputProblems(command.name, [{ code: "unrecognized_keys", keys: [option], path: [], message }], {}, (key) => key));
75
+ }
76
+ throw refused(command, message.endsWith("argument missing") ? `${option} needs a value.` : `${message}${message.endsWith(".") ? "" : "."}`);
77
+ }
78
+ }
79
+ function readArguments(command, args) {
80
+ const positionals = command.positionals ?? [];
81
+ const options = { json: { type: "boolean" } };
82
+ for (const [name, field] of inputFields(command)) {
83
+ const problem = commandLineFieldProblem(command.name, name, positionals.includes(name));
84
+ if (problem !== undefined) {
85
+ throw new Error(problem);
86
+ }
87
+ if (!positionals.includes(name)) {
88
+ options[name] = { type: isBoolean(field) ? "boolean" : "string", multiple: isList(field) };
89
+ }
90
+ }
91
+ // A flag is turned off as --no-hidden, as the MCP server takes hidden: false.
92
+ const parsed = parsedArguments(command, { args, options, allowPositionals: true, allowNegative: true, strict: true, tokens: true });
93
+ const extra = parsed.positionals.slice(positionals.length);
94
+ if (extra.length > 0) {
95
+ // A text given in several words: quote it. A word from a list is not a text.
96
+ const last = positionals.at(-1);
97
+ const freeText = last !== undefined && baseType(command.input.shape[last]) === "string";
98
+ const quoted = parsed.positionals.slice(positionals.length - 1).join(" ");
99
+ throw refused(command, `Unexpected argument: ${extra.join(" ")}.${freeText ? ` Put a value with spaces in quotes, such as "${quoted}".` : ""}`);
100
+ }
101
+ checkRepeatedOptions(command, parsed.tokens, options);
102
+ const input = {};
103
+ for (const [name, value] of Object.entries(parsed.values)) {
104
+ if (name !== "json") {
105
+ input[name] = value;
106
+ }
107
+ }
108
+ positionals.forEach((name, index) => {
109
+ if (parsed.positionals[index] !== undefined) {
110
+ input[name] = parsed.positionals[index];
111
+ }
112
+ });
113
+ // The command line reads every value as text: a field that takes numbers gets them, and a text that is no number stays, for the schema to refuse.
114
+ for (const [name, field] of inputFields(command)) {
115
+ const value = input[name];
116
+ if (value !== undefined && takesNumbers(field)) {
117
+ input[name] = Array.isArray(value) ? value.map(number) : number(value);
118
+ }
119
+ }
120
+ return { input, asJson: parsed.values.json === true };
121
+ }
122
+ function number(value) {
123
+ return typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value)) ? Number(value) : value;
124
+ }
125
+ /** An option that takes one value must not be given twice: the first value would be lost without a word. */
126
+ function checkRepeatedOptions(command, tokens, options) {
127
+ const given = [];
128
+ for (const token of tokens) {
129
+ if (token.kind !== "option" || token.name === undefined || options[token.name]?.multiple === true) {
130
+ continue;
131
+ }
132
+ if (given.includes(token.name)) {
133
+ throw refused(command, `Give --${token.name} only once.`);
134
+ }
135
+ given.push(token.name);
136
+ }
137
+ }
138
+ function inputFields(command) {
139
+ return Object.entries(command.input.shape);
140
+ }
141
+ /** The commands, each with the first sentence of its description. */
142
+ function generalHelp(list) {
143
+ const width = Math.max("listen".length, ...list.map((command) => command.name.length));
144
+ const line = (name, description) => ` ${name.padEnd(width)} ${firstSentence(description)}`;
145
+ return [
146
+ `chesspal ${version}`,
147
+ "",
148
+ "Commands:",
149
+ ...list.map((command) => line(command.name, command.description)),
150
+ line("mcp", mcpDescription),
151
+ line("listen", listenDescription),
152
+ line("chat", chatDescription),
153
+ "",
154
+ "Run `chesspal <command> --help` for its options.",
155
+ ].join("\n");
156
+ }
157
+ function commandHelp(command) {
158
+ const positionals = command.positionals ?? [];
159
+ const optionLines = inputFields(command).map(([name, field]) => {
160
+ const flag = positionals.includes(name) ? `<${name}>` : `--${name}${isBoolean(field) ? "" : " <value>"}${isList(field) ? "..." : ""}`;
161
+ return ` ${flag.padEnd(20)} ${field.description ?? ""}`;
162
+ });
163
+ return [usage(command), "", command.description, "", ...optionLines, ` ${"--json".padEnd(20)} print the result as JSON`].join("\n");
164
+ }
165
+ function usage(command) {
166
+ return ["chesspal", command.name, ...(command.positionals ?? []).map((name) => `<${name}>`), "[options]"].join(" ");
167
+ }
168
+ function firstSentence(text) {
169
+ const end = text.search(/[.!?](\s|$)/);
170
+ return end === -1 ? text : text.slice(0, end + 1);
171
+ }
172
+ function success(output) {
173
+ return { output, exitCode: 0 };
174
+ }
175
+ function failure(output) {
176
+ return { output, exitCode: 1 };
177
+ }
@@ -0,0 +1,26 @@
1
+ import { type CurrentGame } from "../games/index.ts";
2
+ import type { Context } from "./context.ts";
3
+ import type { GameSummary } from "./game-summary.ts";
4
+ /** Whether an analyst agent goes over a game over when the player wants: in Claude Code, with its agent in the player's folder (D-89). */
5
+ export declare function analystRuns(context: Context): boolean;
6
+ /**
7
+ * The game over, with the analyst agent that goes over it when the player wants, where it runs (D-89); over, the game from status and ai_say, which come after
8
+ * its end: once it is in analysis, by the agent or the chat, or reviewed, the chat goes over that review and never starts the agent again (Stefano).
9
+ */
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;
13
+ /** How the chat goes over a game the analyst agent has reviewed, or is reviewing: from that review, never with another agent (Stefano). */
14
+ export declare function reviewedLine(game?: string): string;
15
+ /** Whether a coach judges the player's moves when the helps are on: in Claude Code, with its agent in the player's folder (D-80, D-88). */
16
+ export declare function coachRuns(context: Context): boolean;
17
+ /**
18
+ * 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
19
+ * until it starts, since a chat woken by the page may lose the line on its way to its words (Priya).
20
+ */
21
+ export declare function coachOnEnd(context: Context, folder: string, current: CurrentGame): boolean;
22
+ /**
23
+ * 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
24
+ * them: their end needs no answer either. The end of the last move's agents, told just after, must not take their place (Priya).
25
+ */
26
+ export declare function agentsLine(agents: readonly string[]): string;
@@ -0,0 +1,72 @@
1
+ // The agents that work beside the chat in Claude Code: the analyst that goes over a game over (D-89) and the coach that judges the player's moves (D-88).
2
+ import { isReviewed } from "../core/index.js";
3
+ import { archivedGame, archivedName, coachStarted, gameInAnalysis } from "../games/index.js";
4
+ import { analystAgent, coachAgent, hasAgent } from "../host-files/index.js";
5
+ import { gameFolder, helpsIn } from "../profiles/index.js";
6
+ /** Whether an analyst agent goes over a game over when the player wants: in Claude Code, with its agent in the player's folder (D-89). */
7
+ export function analystRuns(context) {
8
+ return context.host?.() === "claude-code" && hasAgent(context.folder, analystAgent);
9
+ }
10
+ /**
11
+ * The game over, with the analyst agent that goes over it when the player wants, where it runs (D-89); over, the game from status and ai_say, which come after
12
+ * its end: once it is in analysis, by the agent or the chat, or reviewed, the chat goes over that review and never starts the agent again (Stefano).
13
+ */
14
+ export function withAnalyst(summary, context, over) {
15
+ const { end } = summary;
16
+ if (end === undefined || !analystRuns(context)) {
17
+ return summary;
18
+ }
19
+ return { ...summary, end: { ...end, analyst: over !== undefined && goneOver(context, over) ? "done" : "offer" } };
20
+ }
21
+ /** 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). */
22
+ export function analystLine(game) {
23
+ 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 `
24
+ + "page, which shows it at work; tell the player in a word, and when it ends go over the game with them from what it tells you.";
25
+ }
26
+ /** How the chat goes over a game the analyst agent has reviewed, or is reviewing: from that review, never with another agent (Stefano). */
27
+ export function reviewedLine(game) {
28
+ return "The analyst agent has gone over this game, or is at it: never start it again. When the player wants it gone over, go over it with them from what "
29
+ + `it told you, or, in a chat without it, from its review on the board page, which analyse_game ${game === undefined ? "" : `{"game": "${game}"} `}gives.`;
30
+ }
31
+ /** Whether a coach judges the player's moves when the helps are on: in Claude Code, with its agent in the player's folder (D-80, D-88). */
32
+ export function coachRuns(context) {
33
+ return context.host?.() === "claude-code" && hasAgent(context.folder, coachAgent);
34
+ }
35
+ /**
36
+ * 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
37
+ * until it starts, since a chat woken by the page may lose the line on its way to its words (Priya).
38
+ */
39
+ export function coachOnEnd(context, folder, current) {
40
+ const { game, ai } = current;
41
+ return game.outcome.over && game.position.turn === ai && helpsIn(folder) && coachRuns(context) && !coachStarted(folder, current);
42
+ }
43
+ /**
44
+ * 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
45
+ * them: their end needs no answer either. The end of the last move's agents, told just after, must not take their place (Priya).
46
+ */
47
+ export function agentsLine(agents) {
48
+ const prompt = (agent) => `the prompt "${agent === coachAgent ? "Coach." : "Ponder."}"`;
49
+ const [first = "", second] = agents;
50
+ if (second === undefined) {
51
+ const why = first === coachAgent ? "Coach the player" : "Think on the player's time";
52
+ return `${why}: start the ${first} agent in the background with ${prompt(first)}; start it even when an agent of the move before has just ended, `
53
+ + "and write nothing about it in the chat, now or when it ends.";
54
+ }
55
+ return `Think on the player's time and coach them: in one message, start two agents in the background, ${first} with ${prompt(first)} and ${second} with `
56
+ + `${prompt(second)}; start them even when an agent of the move before has just ended, and write nothing about them in the chat, now or when they end.`;
57
+ }
58
+ /** Whether the game over is gone over already: in analysis now, by the analyst agent or the chat, or reviewed, whatever the page opened since (Federico). */
59
+ function goneOver(context, over) {
60
+ const folder = gameFolder(context.home());
61
+ const archived = archivedName(folder, over);
62
+ if (archived === undefined || gameInAnalysis(folder)?.game === archived) {
63
+ return archived !== undefined;
64
+ }
65
+ try {
66
+ return isReviewed(archivedGame(folder, archived));
67
+ }
68
+ catch {
69
+ // A game the archive cannot read is offered as it would be: the analyst says what is wrong with it.
70
+ return false;
71
+ }
72
+ }
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ import { type MovePlayed } from "./game-summary.ts";
3
+ /** The AI's move, with its plan, kept hidden (D-29), and its comment for the player when it has something to say (D-12, D-54). */
4
+ export declare const aiMove: import("./registry.ts").Command<z.ZodObject<{
5
+ move: z.ZodString;
6
+ plan: z.ZodString;
7
+ comment: z.ZodOptional<z.ZodString>;
8
+ arrows: z.ZodOptional<z.ZodArray<z.ZodString>>;
9
+ squares: z.ZodOptional<z.ZodArray<z.ZodString>>;
10
+ offer_draw: z.ZodOptional<z.ZodBoolean>;
11
+ mood: z.ZodOptional<z.ZodEnum<{
12
+ neutral: "neutral";
13
+ happy: "happy";
14
+ surprised: "surprised";
15
+ worried: "worried";
16
+ thoughtful: "thoughtful";
17
+ sad: "sad";
18
+ }>>;
19
+ }, z.core.$strip>, Played>;
20
+ /**
21
+ * The AI's move, the agents to start on the player's time when the host runs them (D-72, D-88), and whether the player sees no board page, so the AI
22
+ * copies the board (D-85).
23
+ */
24
+ type Played = MovePlayed & {
25
+ agents?: string[];
26
+ board?: true;
27
+ };
28
+ export {};
@@ -0,0 +1,85 @@
1
+ import { z } from "zod";
2
+ import { boardText, drawingsText, moods, moodTag } from "../core/index.js";
3
+ import { commentWithPlan, talkLimits } from "../games/index.js";
4
+ import { coachAgent, hasAgent, ponderAgentFor } from "../host-files/index.js";
5
+ import { gameFolder, helpsIn } from "../profiles/index.js";
6
+ import { agentsLine, coachRuns, withAnalyst } from "./agents.js";
7
+ import { closingLines, moveLines, shownMove } from "./answer-lines.js";
8
+ import { arrowsInput, drawingsOf, squaresInput } from "./drawings.js";
9
+ import {} from "./game-summary.js";
10
+ import { playMove, refuseUnseenPosition } from "./play-move.js";
11
+ import { defineCommand, sentence } from "./registry.js";
12
+ import { give, waitingWords } from "./say.js";
13
+ /** The AI's move, with its plan, kept hidden (D-29), and its comment for the player when it has something to say (D-12, D-54). */
14
+ export const aiMove = defineCommand({
15
+ name: "ai_move",
16
+ description: "Plays your move, with your plan, hidden from the player, and a comment for them only when there is something to say: a capture, a move of theirs "
17
+ + "worth a word, words of theirs to answer; as a player at a tournament, never revealing your plans or threats unless the helps are on, and never the "
18
+ + "player's best move. When no board page is open, it answers with the board for your message. "
19
+ + 'Example: ai_move {move: "d5", plan: "After exd5 Na5, chase the bishop on c4.", comment: "An old favourite of mine."}.',
20
+ input: z.object({
21
+ move: z.string().min(1).describe("your move, in SAN (Nf3, exd5, O-O, e8=Q) or as squares (g1f3, e7e8q)"),
22
+ plan: sentence(200).describe("your plan, in one sentence: kept for you, hidden from the player"),
23
+ comment: sentence(talkLimits.aiHelps).optional().describe(`your words for the player, in one sentence of ${talkLimits.ai} characters at most, only when there is something to say: pieces and moves in words ("your `
24
+ + `knight takes on f6"), a move you write with the pieces' figurines, as the tools show it (♞xf6+ for Nxf6+); your plans and threats only with the helps on, and then as long as a note of ${talkLimits.aiHelps} `
25
+ + "characters, to teach; **two asterisks** put a word or two in bold on the page"),
26
+ arrows: arrowsInput.describe("only with the helps on: arrows on the board after your move, to show what your comment says, never your move itself: Rf2e1; G green: a good idea; "
27
+ + "R red: a threat or the mistake; Y yellow: another way; B blue: a plan"),
28
+ squares: squaresInput.describe("only with the helps on: squares to mark after your move, as PGN's %csl writes them: Rf2; colours as for arrows"),
29
+ offer_draw: z.boolean().optional().describe("true offers the player a draw with this move: it stands until their move"),
30
+ mood: z.enum(moods).optional().describe("how you feel, on a move where the game turns for you: your face on the board page shows it for three of your moves, this one included (a surprise for this one "
31
+ + "only), then turns neutral by itself"),
32
+ }),
33
+ alwaysLoad: true,
34
+ showsGame: true,
35
+ positionals: ["move"],
36
+ run: ({ move, plan, comment, offer_draw: offerDraw = false, mood, arrows, squares }, context) => {
37
+ const folder = gameFolder(context.home());
38
+ refuseUnseenPosition(context, folder);
39
+ refuseWithoutHelps(folder, { comment, arrows, squares });
40
+ const drawings = drawingsOf(arrows, squares);
41
+ // The player's words are read before the move, which would count as the answer to them; a move refused leaves them for the next call.
42
+ const waiting = waitingWords(context);
43
+ const words = [commentWithPlan(comment, plan), drawingsText(drawings), mood === undefined ? "" : moodTag(mood)].filter((part) => part !== "").join(" ");
44
+ const played = playMove(folder, context.now(), "ai", move, (message, legal) => `${message} Legal: ${legal.join(" ")}. Nothing changed.`, { comment: words, offerDraw });
45
+ const agents = agentsAfter(context, folder, played);
46
+ const board = context.pageOpen?.() === true ? {} : { board: true };
47
+ return { ...played, game: withAnalyst(played.game, context), words: give(context, waiting), ...(agents.length === 0 ? {} : { agents }), ...board };
48
+ },
49
+ format: (result) => {
50
+ // The mood the AI has just given is not told back: player_move and status tell it while it lasts (Priya).
51
+ const game = { ...result.game };
52
+ delete game.mood;
53
+ // The board goes before the player's words and the next step, which stays the last line.
54
+ return [
55
+ `Played ${shownMove(result.move)}.`,
56
+ ...moveLines({ ...result, game }),
57
+ ...(result.board === true ? ["No board page is open: copy this board into your message.", boardText(result.game.fen)] : []),
58
+ ...(result.agents === undefined ? [] : [agentsLine(result.agents)]),
59
+ ...closingLines(result.game, result.words),
60
+ ].join("\n");
61
+ },
62
+ });
63
+ /** A comment as long as a note, and drawings, only with the helps on (D-78): the player who has them off plays as at a tournament. */
64
+ function refuseWithoutHelps(folder, { comment, arrows, squares }) {
65
+ const asked = [
66
+ arrows === undefined && squares === undefined ? "" : "without arrows and squares",
67
+ comment === undefined || comment.length <= talkLimits.ai ? "" : `with the comment in ${talkLimits.ai} characters at most`,
68
+ ].filter((part) => part !== "");
69
+ if (asked.length > 0 && !helpsIn(folder)) {
70
+ throw new Error(`The player has the helps off: play ${asked.join(", ")}. Nothing changed.`);
71
+ }
72
+ }
73
+ /**
74
+ * The agents to start after the move, in Claude Code, in a folder init set up with them: the one that thinks for the AI while the game goes on (D-72), then
75
+ * the coach with the helps on, which judges the player's move even when the AI's ends the game (D-88).
76
+ */
77
+ function agentsAfter(context, folder, played) {
78
+ if (context.host?.() !== "claude-code") {
79
+ return [];
80
+ }
81
+ const ponder = ponderAgentFor(played.game.level);
82
+ const thinks = played.game.end === undefined && hasAgent(context.folder, ponder);
83
+ const coaches = helpsIn(folder) && coachRuns(context);
84
+ return [...(thinks ? [ponder] : []), ...(coaches ? [coachAgent] : [])];
85
+ }
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ import { type GameSummary } from "./game-summary.ts";
3
+ interface Said {
4
+ /** The game, none when the AI spoke before the first (D-77). */
5
+ game?: GameSummary;
6
+ words: string[];
7
+ /** The coach yet to start on the player's move that ended the game (D-88). */
8
+ coach?: true;
9
+ }
10
+ /** The AI's words on the board page without a move, read only there, on the player's time: an answer, or a word it has to say (D-54, D-72). */
11
+ export declare const aiSay: import("./registry.ts").Command<z.ZodObject<{
12
+ text: z.ZodString;
13
+ mood: z.ZodOptional<z.ZodEnum<{
14
+ neutral: "neutral";
15
+ happy: "happy";
16
+ surprised: "surprised";
17
+ worried: "worried";
18
+ thoughtful: "thoughtful";
19
+ sad: "sad";
20
+ }>>;
21
+ }, z.core.$strip>, Said>;
22
+ export {};