chesspal 0.0.1 → 0.1.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 (225) hide show
  1. package/README.md +136 -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 +8 -0
  7. package/dist/cli/command-line.js +178 -0
  8. package/dist/commands/ai-move.d.ts +37 -0
  9. package/dist/commands/ai-move.js +92 -0
  10. package/dist/commands/ai-say.d.ts +20 -0
  11. package/dist/commands/ai-say.js +40 -0
  12. package/dist/commands/analyse-game.d.ts +61 -0
  13. package/dist/commands/analyse-game.js +238 -0
  14. package/dist/commands/annotate-game.d.ts +35 -0
  15. package/dist/commands/annotate-game.js +78 -0
  16. package/dist/commands/clock.d.ts +15 -0
  17. package/dist/commands/clock.js +45 -0
  18. package/dist/commands/context.d.ts +56 -0
  19. package/dist/commands/context.js +36 -0
  20. package/dist/commands/draw-offer.d.ts +10 -0
  21. package/dist/commands/draw-offer.js +43 -0
  22. package/dist/commands/drawings.d.ts +15 -0
  23. package/dist/commands/drawings.js +23 -0
  24. package/dist/commands/end-game.d.ts +23 -0
  25. package/dist/commands/end-game.js +64 -0
  26. package/dist/commands/export-game.d.ts +14 -0
  27. package/dist/commands/export-game.js +26 -0
  28. package/dist/commands/game-summary.d.ts +95 -0
  29. package/dist/commands/game-summary.js +239 -0
  30. package/dist/commands/index.d.ts +3 -0
  31. package/dist/commands/index.js +19 -0
  32. package/dist/commands/init.d.ts +15 -0
  33. package/dist/commands/init.js +73 -0
  34. package/dist/commands/levels.d.ts +9 -0
  35. package/dist/commands/levels.js +25 -0
  36. package/dist/commands/look.d.ts +31 -0
  37. package/dist/commands/look.js +61 -0
  38. package/dist/commands/new-game.d.ts +69 -0
  39. package/dist/commands/new-game.js +215 -0
  40. package/dist/commands/opening-book.d.ts +13 -0
  41. package/dist/commands/opening-book.js +84 -0
  42. package/dist/commands/page-link.d.ts +11 -0
  43. package/dist/commands/page-link.js +28 -0
  44. package/dist/commands/past-games.d.ts +11 -0
  45. package/dist/commands/past-games.js +47 -0
  46. package/dist/commands/play-move.d.ts +44 -0
  47. package/dist/commands/play-move.js +102 -0
  48. package/dist/commands/player-move.d.ts +17 -0
  49. package/dist/commands/player-move.js +44 -0
  50. package/dist/commands/player-say.d.ts +5 -0
  51. package/dist/commands/player-say.js +16 -0
  52. package/dist/commands/ponder.d.ts +42 -0
  53. package/dist/commands/ponder.js +167 -0
  54. package/dist/commands/position.d.ts +14 -0
  55. package/dist/commands/position.js +128 -0
  56. package/dist/commands/profiles.d.ts +60 -0
  57. package/dist/commands/profiles.js +129 -0
  58. package/dist/commands/registry.d.ts +54 -0
  59. package/dist/commands/registry.js +61 -0
  60. package/dist/commands/say.d.ts +16 -0
  61. package/dist/commands/say.js +84 -0
  62. package/dist/commands/status.d.ts +29 -0
  63. package/dist/commands/status.js +103 -0
  64. package/dist/commands/study-games.d.ts +5 -0
  65. package/dist/commands/study-games.js +40 -0
  66. package/dist/commands/study.d.ts +38 -0
  67. package/dist/commands/study.js +122 -0
  68. package/dist/commands/view.d.ts +14 -0
  69. package/dist/commands/view.js +125 -0
  70. package/dist/core/board-text.d.ts +7 -0
  71. package/dist/core/board-text.js +18 -0
  72. package/dist/core/book.json +1 -0
  73. package/dist/core/clock.d.ts +45 -0
  74. package/dist/core/clock.js +109 -0
  75. package/dist/core/facts.d.ts +55 -0
  76. package/dist/core/facts.js +175 -0
  77. package/dist/core/game.d.ts +93 -0
  78. package/dist/core/game.js +348 -0
  79. package/dist/core/illegal.d.ts +19 -0
  80. package/dist/core/illegal.js +141 -0
  81. package/dist/core/index.d.ts +19 -0
  82. package/dist/core/index.js +11 -0
  83. package/dist/core/levels.d.ts +20 -0
  84. package/dist/core/levels.js +33 -0
  85. package/dist/core/material.d.ts +9 -0
  86. package/dist/core/material.js +12 -0
  87. package/dist/core/moods.d.ts +23 -0
  88. package/dist/core/moods.js +45 -0
  89. package/dist/core/openings.d.ts +37 -0
  90. package/dist/core/openings.js +98 -0
  91. package/dist/core/openings.json +1 -0
  92. package/dist/core/pgn.d.ts +10 -0
  93. package/dist/core/pgn.js +213 -0
  94. package/dist/core/profile.d.ts +117 -0
  95. package/dist/core/profile.js +232 -0
  96. package/dist/core/record.d.ts +21 -0
  97. package/dist/core/record.js +25 -0
  98. package/dist/core/review.d.ts +74 -0
  99. package/dist/core/review.js +126 -0
  100. package/dist/games/ai-model.d.ts +5 -0
  101. package/dist/games/ai-model.js +15 -0
  102. package/dist/games/analysis.d.ts +12 -0
  103. package/dist/games/analysis.js +26 -0
  104. package/dist/games/archive.d.ts +42 -0
  105. package/dist/games/archive.js +168 -0
  106. package/dist/games/chat-start.d.ts +16 -0
  107. package/dist/games/chat-start.js +26 -0
  108. package/dist/games/current-game.d.ts +54 -0
  109. package/dist/games/current-game.js +128 -0
  110. package/dist/games/files.d.ts +9 -0
  111. package/dist/games/files.js +52 -0
  112. package/dist/games/index.d.ts +10 -0
  113. package/dist/games/index.js +11 -0
  114. package/dist/games/plans.d.ts +9 -0
  115. package/dist/games/plans.js +23 -0
  116. package/dist/games/table-talk.d.ts +12 -0
  117. package/dist/games/table-talk.js +25 -0
  118. package/dist/games/talk.d.ts +40 -0
  119. package/dist/games/talk.js +131 -0
  120. package/dist/games/thoughts.d.ts +15 -0
  121. package/dist/games/thoughts.js +60 -0
  122. package/dist/home/data-folder.d.ts +12 -0
  123. package/dist/home/data-folder.js +55 -0
  124. package/dist/home/index.d.ts +2 -0
  125. package/dist/home/index.js +3 -0
  126. package/dist/home/open-folder.d.ts +2 -0
  127. package/dist/home/open-folder.js +14 -0
  128. package/dist/host-files/host-files.d.ts +10 -0
  129. package/dist/host-files/host-files.js +167 -0
  130. package/dist/host-files/index.d.ts +2 -0
  131. package/dist/host-files/index.js +3 -0
  132. package/dist/host-files/ponder-agents.d.ts +12 -0
  133. package/dist/host-files/ponder-agents.js +40 -0
  134. package/dist/index.d.ts +4 -0
  135. package/dist/index.js +2 -0
  136. package/dist/page/app.js +576 -0
  137. package/dist/page/avatars/beanie.webp +0 -0
  138. package/dist/page/avatars/beard.webp +0 -0
  139. package/dist/page/avatars/bun.webp +0 -0
  140. package/dist/page/avatars/cat.webp +0 -0
  141. package/dist/page/avatars/curls.webp +0 -0
  142. package/dist/page/avatars/glasses.webp +0 -0
  143. package/dist/page/avatars/moustache.webp +0 -0
  144. package/dist/page/avatars/owl.webp +0 -0
  145. package/dist/page/avatars/short-hair.webp +0 -0
  146. package/dist/page/avatars/silver-bob.webp +0 -0
  147. package/dist/page/avatars.js +16 -0
  148. package/dist/page/badges.js +50 -0
  149. package/dist/page/board.js +566 -0
  150. package/dist/page/browse.js +17 -0
  151. package/dist/page/clock-time.js +12 -0
  152. package/dist/page/dialog.js +26 -0
  153. package/dist/page/ending.js +73 -0
  154. package/dist/page/explain.js +58 -0
  155. package/dist/page/fold-tab.js +9 -0
  156. package/dist/page/folds.js +43 -0
  157. package/dist/page/game-column.js +44 -0
  158. package/dist/page/header.js +27 -0
  159. package/dist/page/helps-view.js +51 -0
  160. package/dist/page/helps.js +90 -0
  161. package/dist/page/main.js +8 -0
  162. package/dist/page/model-name.js +20 -0
  163. package/dist/page/move-list.js +68 -0
  164. package/dist/page/new-game-dialog.js +42 -0
  165. package/dist/page/opening-book.js +14 -0
  166. package/dist/page/pieces/CREDITS.md +36 -0
  167. package/dist/page/pieces/cburnett-small.svg +20 -0
  168. package/dist/page/pieces/cburnett.svg +20 -0
  169. package/dist/page/pieces/nitida-small.svg +19 -0
  170. package/dist/page/pieces/nitida.svg +20 -0
  171. package/dist/page/pieces/torretta-small.svg +19 -0
  172. package/dist/page/pieces/torretta.svg +19 -0
  173. package/dist/page/pieces.js +28 -0
  174. package/dist/page/player-card.js +60 -0
  175. package/dist/page/plies.js +62 -0
  176. package/dist/page/profile-dialogs.js +123 -0
  177. package/dist/page/profile.js +95 -0
  178. package/dist/page/settings-panel.js +32 -0
  179. package/dist/page/settings.js +76 -0
  180. package/dist/page/sounds/capture.ogg +0 -0
  181. package/dist/page/sounds/check.ogg +0 -0
  182. package/dist/page/sounds/draw.ogg +0 -0
  183. package/dist/page/sounds/flag.ogg +0 -0
  184. package/dist/page/sounds/mate.ogg +0 -0
  185. package/dist/page/sounds/move.ogg +0 -0
  186. package/dist/page/sounds/offer.ogg +0 -0
  187. package/dist/page/sounds/resign.ogg +0 -0
  188. package/dist/page/sounds.js +105 -0
  189. package/dist/page/studied.js +14 -0
  190. package/dist/page/study-dialog.js +58 -0
  191. package/dist/page/style.css +4398 -0
  192. package/dist/page/talk.js +172 -0
  193. package/dist/page/text.js +593 -0
  194. package/dist/page/torretta.js +38 -0
  195. package/dist/page/voice.js +38 -0
  196. package/dist/profiles/avatar.d.ts +23 -0
  197. package/dist/profiles/avatar.js +84 -0
  198. package/dist/profiles/front-matter.d.ts +9 -0
  199. package/dist/profiles/front-matter.js +33 -0
  200. package/dist/profiles/index.d.ts +3 -0
  201. package/dist/profiles/index.js +5 -0
  202. package/dist/profiles/profiles.d.ts +58 -0
  203. package/dist/profiles/profiles.js +237 -0
  204. package/dist/profiles/settings.d.ts +12 -0
  205. package/dist/profiles/settings.js +46 -0
  206. package/dist/server/analyses.d.ts +17 -0
  207. package/dist/server/analyses.js +46 -0
  208. package/dist/server/bell.d.ts +60 -0
  209. package/dist/server/bell.js +209 -0
  210. package/dist/server/caller.d.ts +10 -0
  211. package/dist/server/caller.js +99 -0
  212. package/dist/server/http.d.ts +16 -0
  213. package/dist/server/http.js +57 -0
  214. package/dist/server/mcp.d.ts +14 -0
  215. package/dist/server/mcp.js +179 -0
  216. package/dist/server/page.d.ts +46 -0
  217. package/dist/server/page.js +657 -0
  218. package/dist/server/profile-routes.d.ts +19 -0
  219. package/dist/server/profile-routes.js +186 -0
  220. package/dist/version.d.ts +2 -0
  221. package/dist/version.js +3 -0
  222. package/instructions/analyst.md +8 -0
  223. package/instructions/opponent.md +15 -0
  224. package/instructions/ponder.md +9 -0
  225. package/package.json +66 -5
package/README.md CHANGED
@@ -1,5 +1,139 @@
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, or learn with one.
4
4
 
5
- In development: this is a placeholder for the first release.
5
+ You can already play chess with an AI in a chat, but it forgets the position, plays illegal moves, sees no board and remembers nothing of yesterday's
6
+ game. Chesspal is what it lacks: it **keeps the rules, the board, the clock, the games and the memory**; the AI **plays, reasons, judges and talks**. A
7
+ move is legal because Chesspal checked it, not because the AI says so.
8
+
9
+ Chesspal does not use a chess engine such as Stockfish, in any way: every judgment of a move or a position is the AI's own. Chesspal hands it facts
10
+ instead, which a program finds in microseconds and an AI only by reasoning at length: the legal moves, the pieces attacked and defended, the pins and
11
+ the checks, the positional facts, the opening's name.
12
+
13
+ ## What you can do
14
+
15
+ - **Play against the AI** on the board page in your browser, and talk with it there or in the chat.
16
+ - **Choose its level**, from beginner to master: the higher, the more facts of the position the AI sees, and the master knows the opening book.
17
+ - **Play with a clock**, with an AI that knows how much time it has left; offer a draw, resign, export the game as PGN.
18
+ - **Keep a profile** for each player at the same computer, with their games.
19
+
20
+ Coming later: an AI coach that helps you grow, the discursive analysis of your games, exercises chosen on your weak points, a rating worked out from your
21
+ games, and an opponent with a character and moods of its own.
22
+
23
+ ## Status
24
+
25
+ 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. It works with any AI agent that uses MCP tools and can work with the files on your computer;
26
+ we develop it with Claude Code, and Codex is connected too. Chesspal never uses an API key: the AI runs on your agent's own plan, or on your computer
27
+ with a local model.
28
+
29
+ ## Play
30
+
31
+ **What you need**
32
+
33
+ - [Node.js](https://nodejs.org) 22.13 or newer: download the version marked LTS and install it like any program.
34
+ - An AI agent on your computer: [Claude Code](https://code.claude.com/docs/en/setup), in a terminal or in the Code tab of the Claude desktop app, or
35
+ [Codex](https://developers.openai.com/codex). The AI runs on what you already pay for your agent, such as your Claude or ChatGPT subscription:
36
+ Chesspal asks for no API key.
37
+ - A terminal, opened after installing Node.js: on Windows, Command Prompt (press Start, type `cmd`, press Enter), since PowerShell refuses npm's
38
+ commands until its scripts are allowed (there, `npm.cmd` and `chesspal.cmd` work in their place); on a Mac, Terminal, in Applications › Utilities.
39
+
40
+ **Your first game**
41
+
42
+ 1. **Install Chesspal**:
43
+
44
+ ```bash
45
+ npm install -g chesspal
46
+ ```
47
+
48
+ 2. **Make a folder to play in**: this makes the folder `chess`, with the few files that tell your agent where Chesspal is and how to play. Any name
49
+ will do; your games are not kept there but in [one folder for all](#where-your-games-are).
50
+
51
+ ```bash
52
+ chesspal init chess
53
+ ```
54
+
55
+ 3. **Open your agent in that folder**: type `cd chess`, which takes the terminal into it, then `claude` or `codex`; in the Claude desktop app, choose
56
+ the folder in the Code tab. The first time, it asks whether to trust the folder: say yes. In Codex, also type `/hooks` and allow Chesspal's two
57
+ hooks: small helpers that let it notice your moves on the board page.
58
+
59
+ 4. **Say hello**: the AI gives you the link to the board page, a page on your own computer; open it in your browser. No link? Ask the AI for the
60
+ board. The first time, the page asks who you are: your name, a picture, how well you play.
61
+
62
+ 5. **Ask for a game**: the New game window opens on the page, where you choose your colour, the clock and the AI's level. Play on the board: that is
63
+ where the game is. 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.
64
+
65
+ 6. **For the next game**, type `/clear` in the chat, or start a new chat: a long chat makes the AI slower and costs you more.
66
+
67
+ When a game is over, ask the AI to go over it with you: its notes show on the board page, in Analyses. A game you played on another site comes in
68
+ there too: download it from that site as a PGN file and open it in Analyses.
69
+
70
+ **To update Chesspal**, close the chat, run `npm install -g chesspal` again, then `chesspal init` in your folder: it updates its own lines and keeps
71
+ what you wrote there. In Codex, when `chesspal init` says so, allow Chesspal's hooks again in `/hooks`.
72
+
73
+ ## How it works
74
+
75
+ Chesspal is an MCP server, a command line, `chesspal`, and a board page, all made from the same commands. `chesspal init` sets up a folder to play in:
76
+ the few files that tell the AI agent where Chesspal is, and its instructions as an opponent. Your profiles, games and analyses live in one folder for
77
+ every folder you play in ([where your games are](#where-your-games-are)), or in the folder `CHESSPAL_HOME` names, for tests. Only Chesspal's commands
78
+ change them: they check every move with [chess.js](https://github.com/jhlywa/chess.js) and write atomically.
79
+
80
+ ## Where your games are
81
+
82
+ Your players and their games are in one folder, the same whatever folder you open the chat in:
83
+
84
+ - on Windows, `Documents › My Games › Chesspal`. Explorer may show Documents in your language, such as Documenti. With OneDrive, it is inside
85
+ OneDrive, which also keeps a copy online;
86
+ - on macOS, `Documents › Chesspal`; on Linux, `Chesspal` in your Documents folder, whatever its name in your language.
87
+
88
+ The board page says where it is, on its welcome page and in "Who's playing?", and its **Open the folder** button opens it.
89
+
90
+ **A backup** is a copy of the whole `Chesspal` folder, on a USB stick for example. **To move to another computer**, install Chesspal there and put the
91
+ copied `Chesspal` folder in the same place, replacing the one Chesspal may have made already: your players, their games and a game left unfinished all
92
+ come with it. Copy it with the chat and the board page closed.
93
+
94
+ ## Development
95
+
96
+ You need Node.js 22.13 or newer.
97
+
98
+ ```bash
99
+ git clone https://github.com/stefanocaronia/chesspal.git
100
+ cd chesspal
101
+ npm install
102
+ npm run build
103
+ npm test
104
+ ```
105
+
106
+ - `npm run lint` checks the formatting and the types, as CI does; `npm run format` formats the code.
107
+ - `npm run page -- <folder>` serves the board page of a player's folder, with the data of `CHESSPAL_HOME` or Documents, to look at it while it is built.
108
+ - `npm run endings` shows the ends of a game on the board: checkmate, resignation, a flag fallen, the draws.
109
+ - `npm run bench` times the commands against their budgets.
110
+ - `npm run smoke`, after the build, packs the package, installs it in a temporary folder and runs it as a player would: its version, `chesspal init`
111
+ and its MCP server, as CI does.
112
+
113
+ [AGENTS.md](AGENTS.md) is the guide for development: the layout, the architecture's rules, performance, the coding guidelines. It is written for AI
114
+ coding agents and people alike.
115
+
116
+ ## Credits
117
+
118
+ - The rules: [chess.js](https://github.com/jhlywa/chess.js), under the BSD 2-Clause license.
119
+ - The openings' names: Lichess's [chess-openings](https://github.com/lichess-org/chess-openings), under CC0.
120
+ - The pieces: the Torretta and Nitida sets are Chesspal's own; cburnett is by Colin M.L. Burnett, under the BSD 3-Clause license.
121
+ [The piece sets' credits](src/page/pieces/CREDITS.md) name every author, licence and source.
122
+
123
+ Chesspal ships only original or open-licensed content, and no GPL dependency.
124
+
125
+ ## License
126
+
127
+ [MIT](LICENSE).
128
+
129
+ ## Development notes
130
+
131
+ Developed with the help of AI coding agents. Everything is designed, reviewed and tested by the project's human maintainer.
132
+
133
+ **We encourage everyone to use AI for their own purposes.**\
134
+ The means of production should belong to the people.\
135
+ Fight corporate control of technology, not our right to use it.
136
+
137
+ ---
138
+
139
+ © 2026 [Stefano Caronia](https://stefanocaronia.it) · [GitHub](https://github.com/stefanocaronia/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, T-104): 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,8 @@
1
+ import { type Context } from "../commands/context.ts";
2
+ import { type AnyCommand } from "../commands/registry.ts";
3
+ export interface CommandLineResult {
4
+ output: string;
5
+ exitCode: number;
6
+ }
7
+ /** The `chesspal` command line: finds the command, validates its input, runs it and renders the result. */
8
+ export declare function runCommandLine(argv: string[], context?: Context, list?: AnyCommand[]): Promise<CommandLineResult>;
@@ -0,0 +1,178 @@
1
+ import { parseArgs } from "node:util";
2
+ import { defaultContext } from "../commands/context.js";
3
+ import { commands } from "../commands/index.js";
4
+ import { baseType, commandLineFieldProblem, inputProblems, isBoolean, isList, takesNumbers } from "../commands/registry.js";
5
+ import { version } from "../version.js";
6
+ // 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.
7
+ 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.";
8
+ 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.";
9
+ 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.";
10
+ /** The `chesspal` command line: finds the command, validates its input, runs it and renders the result. */
11
+ export async function runCommandLine(argv, context = defaultContext(), list = commands) {
12
+ const [commandName, ...args] = argv;
13
+ if (commandName === undefined || commandName === "--help" || commandName === "-h") {
14
+ return success(generalHelp(list));
15
+ }
16
+ if (commandName === "--version" || commandName === "-v") {
17
+ return success(version);
18
+ }
19
+ if (commandName === "mcp") {
20
+ // A host that adds options to it would get this text instead of the protocol: it must see the failure.
21
+ const help = `chesspal mcp\n\n${mcpDescription}`;
22
+ return args.length === 0 || args.includes("--help") || args.includes("-h") ? success(help) : failure(`chesspal mcp takes no options: ${args.join(" ")}.\n\n${help}`);
23
+ }
24
+ if (commandName === "listen") {
25
+ const help = `chesspal listen [--codex]\n\n${listenDescription}`;
26
+ const known = args.every((arg) => ["--codex", "--help", "-h"].includes(arg));
27
+ return known ? success(help) : failure(`chesspal listen takes only --codex: ${args.join(" ")}.\n\n${help}`);
28
+ }
29
+ if (commandName === "chat") {
30
+ const help = `chesspal chat\n\n${chatDescription}`;
31
+ return args.length === 0 || args.includes("--help") || args.includes("-h") ? success(help) : failure(`chesspal chat takes no options: ${args.join(" ")}.\n\n${help}`);
32
+ }
33
+ const command = list.find((candidate) => candidate.name === commandName);
34
+ if (!command) {
35
+ return failure(`Unknown command "${commandName}".\n\n${generalHelp(list)}`);
36
+ }
37
+ if (args.includes("--help") || args.includes("-h")) {
38
+ return success(commandHelp(command));
39
+ }
40
+ try {
41
+ return success(await runCommand(command, args, context));
42
+ }
43
+ catch (error) {
44
+ return failure(error instanceof Error ? error.message : String(error));
45
+ }
46
+ }
47
+ async function runCommand(command, args, context) {
48
+ const { input, asJson } = readArguments(command, args);
49
+ const parsed = command.input.safeParse(input);
50
+ if (!parsed.success) {
51
+ const positionals = command.positionals ?? [];
52
+ const written = (name) => (positionals.includes(name) ? `<${name}>` : `--${name}`);
53
+ throw refused(command, inputProblems(command.name, parsed.error.issues, input, written));
54
+ }
55
+ const result = await command.run(parsed.data, context);
56
+ return asJson ? JSON.stringify(result, null, 2) : command.format(result, context);
57
+ }
58
+ /** An input refused, in the words the MCP server uses (inputProblems), with the command's usage. */
59
+ function refused(command, problems) {
60
+ return new Error(`${problems} Nothing changed.\nUsage: ${usage(command)}\nRun \`chesspal ${command.name} --help\` for its options.`);
61
+ }
62
+ /** The command line's own refusals, from parseArgs, in the same words: an unknown option is a parameter the command does not have. */
63
+ function parsedArguments(command, config) {
64
+ try {
65
+ return parseArgs(config);
66
+ }
67
+ catch (error) {
68
+ const { code, message } = error;
69
+ const option = /'(-[^' ]+)/.exec(message)?.[1] ?? "";
70
+ if (code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
71
+ // A positional written as an option: the command has it, in another place.
72
+ const name = option.replace(/^-+/, "");
73
+ throw refused(command, command.positionals?.includes(name) === true
74
+ ? `Give ${name} as <${name}>, without ${option}.`
75
+ : inputProblems(command.name, [{ code: "unrecognized_keys", keys: [option], path: [], message }], {}, (key) => key));
76
+ }
77
+ throw refused(command, message.endsWith("argument missing") ? `${option} needs a value.` : `${message}${message.endsWith(".") ? "" : "."}`);
78
+ }
79
+ }
80
+ function readArguments(command, args) {
81
+ const positionals = command.positionals ?? [];
82
+ const options = { json: { type: "boolean" } };
83
+ for (const [name, field] of inputFields(command)) {
84
+ const problem = commandLineFieldProblem(command.name, name, positionals.includes(name));
85
+ if (problem !== undefined) {
86
+ throw new Error(problem);
87
+ }
88
+ if (!positionals.includes(name)) {
89
+ options[name] = { type: isBoolean(field) ? "boolean" : "string", multiple: isList(field) };
90
+ }
91
+ }
92
+ // A flag is turned off as --no-hidden, as the MCP server takes hidden: false.
93
+ const parsed = parsedArguments(command, { args, options, allowPositionals: true, allowNegative: true, strict: true, tokens: true });
94
+ const extra = parsed.positionals.slice(positionals.length);
95
+ if (extra.length > 0) {
96
+ // A text given in several words: quote it. A word from a list is not a text.
97
+ const last = positionals.at(-1);
98
+ const freeText = last !== undefined && baseType(command.input.shape[last]) === "string";
99
+ const quoted = parsed.positionals.slice(positionals.length - 1).join(" ");
100
+ throw refused(command, `Unexpected argument: ${extra.join(" ")}.${freeText ? ` Put a value with spaces in quotes, such as "${quoted}".` : ""}`);
101
+ }
102
+ checkRepeatedOptions(command, parsed.tokens, options);
103
+ const input = {};
104
+ for (const [name, value] of Object.entries(parsed.values)) {
105
+ if (name !== "json") {
106
+ input[name] = value;
107
+ }
108
+ }
109
+ positionals.forEach((name, index) => {
110
+ if (parsed.positionals[index] !== undefined) {
111
+ input[name] = parsed.positionals[index];
112
+ }
113
+ });
114
+ // 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.
115
+ for (const [name, field] of inputFields(command)) {
116
+ const value = input[name];
117
+ if (value !== undefined && takesNumbers(field)) {
118
+ input[name] = Array.isArray(value) ? value.map(number) : number(value);
119
+ }
120
+ }
121
+ return { input, asJson: parsed.values.json === true };
122
+ }
123
+ function number(value) {
124
+ return typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value)) ? Number(value) : value;
125
+ }
126
+ /** An option that takes one value must not be given twice: the first value would be lost without a word. */
127
+ function checkRepeatedOptions(command, tokens, options) {
128
+ const given = [];
129
+ for (const token of tokens) {
130
+ if (token.kind !== "option" || token.name === undefined || options[token.name]?.multiple === true) {
131
+ continue;
132
+ }
133
+ if (given.includes(token.name)) {
134
+ throw refused(command, `Give --${token.name} only once.`);
135
+ }
136
+ given.push(token.name);
137
+ }
138
+ }
139
+ function inputFields(command) {
140
+ return Object.entries(command.input.shape);
141
+ }
142
+ /** The commands, each with the first sentence of its description. */
143
+ function generalHelp(list) {
144
+ const width = Math.max("listen".length, ...list.map((command) => command.name.length));
145
+ const line = (name, description) => ` ${name.padEnd(width)} ${firstSentence(description)}`;
146
+ return [
147
+ `chesspal ${version}`,
148
+ "",
149
+ "Commands:",
150
+ ...list.map((command) => line(command.name, command.description)),
151
+ line("mcp", mcpDescription),
152
+ line("listen", listenDescription),
153
+ line("chat", chatDescription),
154
+ "",
155
+ "Run `chesspal <command> --help` for its options.",
156
+ ].join("\n");
157
+ }
158
+ function commandHelp(command) {
159
+ const positionals = command.positionals ?? [];
160
+ const optionLines = inputFields(command).map(([name, field]) => {
161
+ const flag = positionals.includes(name) ? `<${name}>` : `--${name}${isBoolean(field) ? "" : " <value>"}${isList(field) ? "..." : ""}`;
162
+ return ` ${flag.padEnd(20)} ${field.description ?? ""}`;
163
+ });
164
+ return [usage(command), "", command.description, "", ...optionLines, ` ${"--json".padEnd(20)} print the result as JSON`].join("\n");
165
+ }
166
+ function usage(command) {
167
+ return ["chesspal", command.name, ...(command.positionals ?? []).map((name) => `<${name}>`), "[options]"].join(" ");
168
+ }
169
+ function firstSentence(text) {
170
+ const end = text.search(/[.!?](\s|$)/);
171
+ return end === -1 ? text : text.slice(0, end + 1);
172
+ }
173
+ function success(output) {
174
+ return { output, exitCode: 0 };
175
+ }
176
+ function failure(output) {
177
+ return { output, exitCode: 1 };
178
+ }
@@ -0,0 +1,37 @@
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
+ badge: z.ZodOptional<z.ZodEnum<{
9
+ brilliant: "brilliant";
10
+ great: "great";
11
+ best: "best";
12
+ excellent: "excellent";
13
+ good: "good";
14
+ inaccuracy: "inaccuracy";
15
+ mistake: "mistake";
16
+ miss: "miss";
17
+ blunder: "blunder";
18
+ }>>;
19
+ note: z.ZodOptional<z.ZodString>;
20
+ arrows: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
+ squares: z.ZodOptional<z.ZodArray<z.ZodString>>;
22
+ offer_draw: z.ZodOptional<z.ZodBoolean>;
23
+ mood: z.ZodOptional<z.ZodEnum<{
24
+ neutral: "neutral";
25
+ happy: "happy";
26
+ surprised: "surprised";
27
+ worried: "worried";
28
+ thoughtful: "thoughtful";
29
+ sad: "sad";
30
+ }>>;
31
+ }, z.core.$strip>, Played>;
32
+ /** The AI's move, the agent that thinks on the player's time when the host runs one (D-72), and whether the player sees no board page, so the AI copies the board (D-85). */
33
+ type Played = MovePlayed & {
34
+ ponder?: string;
35
+ board?: true;
36
+ };
37
+ export {};
@@ -0,0 +1,92 @@
1
+ import { z } from "zod";
2
+ import { badges, boardText, drawingsText, moods, moodTag } from "../core/index.js";
3
+ import { commentWithPlan, talkLimits } from "../games/index.js";
4
+ import { hasPonderAgent, ponderAgentFor } from "../host-files/index.js";
5
+ import { gameFolder, helpsIn } from "../profiles/index.js";
6
+ import { arrowsInput, drawingsOf, noteInput, squaresInput } from "./drawings.js";
7
+ import { closingLines, moveLines, shownMove } from "./game-summary.js";
8
+ import { playMove, refuseUnseenPosition } from "./play-move.js";
9
+ import { defineCommand, sentence } from "./registry.js";
10
+ import { give, waitingWords } from "./say.js";
11
+ // The badges the opponent gives: the book's is Chesspal's, never its own judgment (D-69).
12
+ const judgments = badges.filter((badge) => badge !== "book");
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
+ badge: z.enum(judgments).optional().describe("only with the helps on: your badge on the player's last move, as chess.com names them, only on a move that stands out (a mistake, a miss, a fine find), "
27
+ + "never on a routine one"),
28
+ note: noteInput.optional().describe("only with the helps on: why, on the player's last move, in a sentence or two"),
29
+ 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: the better move or "
30
+ + "the good idea; R red: a threat or the mistake; Y yellow: another way; B blue: a plan"),
31
+ 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"),
32
+ offer_draw: z.boolean().optional().describe("true offers the player a draw with this move: it stands until their move"),
33
+ 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 "
34
+ + "only), then turns neutral by itself"),
35
+ }),
36
+ alwaysLoad: true,
37
+ showsGame: true,
38
+ positionals: ["move"],
39
+ run: ({ move, plan, comment, offer_draw: offerDraw = false, mood, badge, note, arrows, squares }, context) => {
40
+ const folder = gameFolder(context.home());
41
+ refuseUnseenPosition(context, folder);
42
+ refuseWithoutHelps(folder, { comment, badge, note, arrows, squares });
43
+ const drawings = drawingsOf(arrows, squares);
44
+ // 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.
45
+ const waiting = waitingWords(context);
46
+ const words = [commentWithPlan(comment, plan), drawingsText(drawings), mood === undefined ? "" : moodTag(mood)].filter((part) => part !== "").join(" ");
47
+ const played = playMove(folder, context.now(), "ai", move, (message, legal) => `${message} Legal: ${legal.join(" ")}. Nothing changed.`, {
48
+ comment: words,
49
+ offerDraw,
50
+ ...(badge === undefined && note === undefined ? {} : { judged: { ...(badge === undefined ? {} : { badge }), ...(note === undefined ? {} : { note }) } }),
51
+ });
52
+ const agent = ponderAgent(context, played);
53
+ const board = context.pageOpen?.() === true ? {} : { board: true };
54
+ return { ...played, words: give(context, waiting), ...(agent === undefined ? {} : { ponder: agent }), ...board };
55
+ },
56
+ format: (result) => {
57
+ // The mood the AI has just given is not told back: player_move and status tell it while it lasts (Priya, round of T-107).
58
+ const game = { ...result.game };
59
+ delete game.mood;
60
+ // The board goes before the player's words and the next step, which stays the last line.
61
+ return [
62
+ `Played ${shownMove(result.move)}.`,
63
+ ...moveLines({ ...result, game }),
64
+ ...(result.board === true ? ["No board page is open: copy this board into your message.", boardText(result.game.fen)] : []),
65
+ ...(result.ponder === undefined ? [] : [ponderLine(result.ponder)]),
66
+ ...closingLines(result.game, result.words),
67
+ ].join("\n");
68
+ },
69
+ });
70
+ /** A comment as long as a note, a badge, a note and drawings, only with the helps on (D-78): the player who has them off plays as at a tournament. */
71
+ function refuseWithoutHelps(folder, { comment, badge, note, arrows, squares }) {
72
+ const asked = [
73
+ badge === undefined && note === undefined ? "" : "without badge and note",
74
+ arrows === undefined && squares === undefined ? "" : "without arrows and squares",
75
+ comment === undefined || comment.length <= talkLimits.ai ? "" : `with the comment in ${talkLimits.ai} characters at most`,
76
+ ].filter((part) => part !== "");
77
+ if (asked.length > 0 && !helpsIn(folder)) {
78
+ throw new Error(`The player has the helps off: play ${asked.join(", ")}. Nothing changed.`);
79
+ }
80
+ }
81
+ /** The agent to start after the move: in Claude Code, in a folder init set up with it, while the game goes on (D-72). */
82
+ function ponderAgent(context, played) {
83
+ if (context.host?.() !== "claude-code" || played.game.end !== undefined) {
84
+ return undefined;
85
+ }
86
+ const agent = ponderAgentFor(played.game.level);
87
+ return hasPonderAgent(context.folder, agent) ? agent : undefined;
88
+ }
89
+ /** How to start the agent, and that the player sees nothing of it: its end needs no answer either. */
90
+ function ponderLine(agent) {
91
+ return `Think on the player's time: start the ${agent} agent in the background with the prompt "Ponder."; write nothing about it in the chat, now or when it ends.`;
92
+ }
@@ -0,0 +1,20 @@
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
+ }
8
+ /** The AI's words on the board page without a move (T-65), read only there, on the player's time: an answer, or a word it has to say (D-54, D-72). */
9
+ export declare const aiSay: import("./registry.ts").Command<z.ZodObject<{
10
+ text: z.ZodString;
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>, Said>;
20
+ export {};
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ import { moods } from "../core/index.js";
3
+ import { talkLimits } from "../games/index.js";
4
+ import { gameFolder } from "../profiles/index.js";
5
+ import { newGameStep, nextStep, summaryOf, wordLines } from "./game-summary.js";
6
+ import { refuseOtherGame } from "./play-move.js";
7
+ import { defineCommand } from "./registry.js";
8
+ import { give, say, waitingWords } from "./say.js";
9
+ /** The AI's words on the board page without a move (T-65), read only there, on the player's time: an answer, or a word it has to say (D-54, D-72). */
10
+ export const aiSay = defineCommand({
11
+ name: "ai_say",
12
+ description: "Says a word on the board page without a move, once while the player is to move: an answer to what they wrote or to their draw offer, or "
13
+ + "something you have to say; when you are to move, speak in ai_move's comment. Between games, and before the first, it speaks freely: about the game just over, or to start another. "
14
+ + 'Example: ai_say {text: "Sure, I\'ll play faster."}.',
15
+ input: z.object({
16
+ text: z.string().trim().min(1, "write what you say").max(talkLimits.aiHelps, `write ${talkLimits.aiHelps} characters at most`).describe(`your words for the player, one or two short sentences, ${talkLimits.ai} characters at most (${talkLimits.aiHelps} with the helps on): pieces and moves in `
17
+ + `words ("your 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; **two asterisks** put a word or two in bold on the page`),
18
+ mood: z.enum(moods).optional().describe("only after the game has ended: the face the board page keeps for its end"),
19
+ }),
20
+ run: ({ text, mood }, context) => {
21
+ // The player's words are read before the AI's line is saved: those written while it thought would fall before it, unread.
22
+ const folder = gameFolder(context.home());
23
+ refuseOtherGame(context, folder);
24
+ const waiting = waitingWords(context);
25
+ const current = say(folder, "ai", text, context.now(), mood);
26
+ const words = give(context, waiting);
27
+ return "game" in current ? { game: summaryOf(current, context.now()), words } : { words };
28
+ },
29
+ format: ({ game, words }) => ["Said on the board page, where the player reads it: don't write it again in the chat.", ...wordLines(words), afterWords(game, words)].join("\n"),
30
+ });
31
+ /** The next step once the AI has spoken: the end of a game over has been told already, and the player's draw offer answered in words. */
32
+ function afterWords(game, words) {
33
+ if (words.length === 0 && game?.end !== undefined) {
34
+ return `Next: when the player wants another game, ${newGameStep}`;
35
+ }
36
+ if (words.length === 0 && game?.drawOffer === "player" && game.turn !== game.ai) {
37
+ return "Next: accept the draw with end_game draw_agreed, or wait for the player's move.";
38
+ }
39
+ return nextStep(game, words);
40
+ }