chesspal 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +45 -82
  2. package/dist/cli/chat-hook.js +1 -1
  3. package/dist/cli/command-line.d.ts +1 -2
  4. package/dist/cli/command-line.js +2 -3
  5. package/dist/commands/agents.d.ts +26 -0
  6. package/dist/commands/agents.js +72 -0
  7. package/dist/commands/ai-move.d.ts +5 -14
  8. package/dist/commands/ai-move.js +28 -35
  9. package/dist/commands/ai-say.d.ts +3 -1
  10. package/dist/commands/ai-say.js +24 -6
  11. package/dist/commands/analyse-game.d.ts +4 -0
  12. package/dist/commands/analyse-game.js +43 -16
  13. package/dist/commands/annotate-game.d.ts +4 -0
  14. package/dist/commands/annotate-game.js +65 -12
  15. package/dist/commands/answer-lines.d.ts +28 -0
  16. package/dist/commands/answer-lines.js +144 -0
  17. package/dist/commands/coach.d.ts +45 -0
  18. package/dist/commands/coach.js +344 -0
  19. package/dist/commands/context.d.ts +16 -5
  20. package/dist/commands/context.js +1 -0
  21. package/dist/commands/delete-game.d.ts +7 -0
  22. package/dist/commands/delete-game.js +30 -0
  23. package/dist/commands/delete-review.d.ts +8 -0
  24. package/dist/commands/delete-review.js +34 -0
  25. package/dist/commands/drawings.d.ts +0 -2
  26. package/dist/commands/drawings.js +5 -3
  27. package/dist/commands/end-game.js +5 -2
  28. package/dist/commands/export-game.d.ts +1 -1
  29. package/dist/commands/export-game.js +4 -3
  30. package/dist/commands/game-summary.d.ts +21 -38
  31. package/dist/commands/game-summary.js +30 -167
  32. package/dist/commands/index.d.ts +15 -0
  33. package/dist/commands/index.js +47 -2
  34. package/dist/commands/init.d.ts +3 -3
  35. package/dist/commands/init.js +4 -4
  36. package/dist/commands/lead-input.d.ts +8 -0
  37. package/dist/commands/lead-input.js +21 -0
  38. package/dist/commands/look.js +4 -1
  39. package/dist/commands/new-game.d.ts +2 -3
  40. package/dist/commands/new-game.js +7 -4
  41. package/dist/commands/play-move.d.ts +8 -15
  42. package/dist/commands/play-move.js +5 -13
  43. package/dist/commands/player-move.d.ts +2 -6
  44. package/dist/commands/player-move.js +13 -6
  45. package/dist/commands/player-say.d.ts +1 -1
  46. package/dist/commands/player-say.js +1 -1
  47. package/dist/commands/ponder.d.ts +10 -0
  48. package/dist/commands/ponder.js +33 -21
  49. package/dist/commands/position.d.ts +1 -1
  50. package/dist/commands/position.js +3 -1
  51. package/dist/commands/profiles.d.ts +13 -4
  52. package/dist/commands/profiles.js +49 -15
  53. package/dist/commands/registry.d.ts +8 -3
  54. package/dist/commands/registry.js +12 -11
  55. package/dist/commands/say.d.ts +1 -1
  56. package/dist/commands/say.js +16 -16
  57. package/dist/commands/speech.d.ts +8 -0
  58. package/dist/commands/speech.js +20 -0
  59. package/dist/commands/status.d.ts +9 -3
  60. package/dist/commands/status.js +40 -15
  61. package/dist/commands/study-games.js +1 -1
  62. package/dist/commands/study.js +3 -19
  63. package/dist/core/facts.js +15 -15
  64. package/dist/core/game.d.ts +3 -1
  65. package/dist/core/game.js +8 -4
  66. package/dist/core/illegal.js +1 -1
  67. package/dist/core/index.d.ts +5 -4
  68. package/dist/core/index.js +5 -4
  69. package/dist/core/lead.d.ts +15 -0
  70. package/dist/core/lead.js +40 -0
  71. package/dist/core/levels.d.ts +1 -1
  72. package/dist/core/levels.js +1 -1
  73. package/dist/core/moods.d.ts +0 -2
  74. package/dist/core/moods.js +2 -2
  75. package/dist/core/openings.d.ts +0 -2
  76. package/dist/core/openings.js +1 -1
  77. package/dist/core/profile.js +1 -1
  78. package/dist/core/record.d.ts +1 -1
  79. package/dist/core/record.js +1 -1
  80. package/dist/core/review.d.ts +9 -3
  81. package/dist/core/review.js +29 -9
  82. package/dist/games/analysis.d.ts +1 -1
  83. package/dist/games/archive.d.ts +24 -3
  84. package/dist/games/archive.js +96 -22
  85. package/dist/games/coach.d.ts +57 -0
  86. package/dist/games/coach.js +144 -0
  87. package/dist/games/current-game.d.ts +2 -0
  88. package/dist/games/current-game.js +7 -1
  89. package/dist/games/files.js +1 -0
  90. package/dist/games/index.d.ts +4 -3
  91. package/dist/games/index.js +4 -3
  92. package/dist/games/talk.d.ts +1 -5
  93. package/dist/games/talk.js +14 -14
  94. package/dist/home/open-folder.js +1 -1
  95. package/dist/host-files/{ponder-agents.d.ts → agents.d.ts} +10 -2
  96. package/dist/host-files/agents.js +53 -0
  97. package/dist/host-files/host-files.d.ts +7 -2
  98. package/dist/host-files/host-files.js +27 -12
  99. package/dist/host-files/index.d.ts +2 -2
  100. package/dist/host-files/index.js +3 -3
  101. package/dist/index.d.ts +1 -3
  102. package/dist/index.js +1 -0
  103. package/dist/page/app/anchor.js +88 -0
  104. package/dist/page/{app.js → app/app.js} +112 -230
  105. package/dist/page/app/keys.js +94 -0
  106. package/dist/page/app/phase.js +30 -0
  107. package/dist/page/app/served.js +73 -0
  108. package/dist/page/avatars/avatars.js +19 -0
  109. package/dist/page/avatars/thought.js +10 -0
  110. package/dist/page/base.css +160 -0
  111. package/dist/page/board/board.css +959 -0
  112. package/dist/page/board/board.js +125 -0
  113. package/dist/page/{explain.js → board/explain.js} +1 -1
  114. package/dist/page/board/marks.js +160 -0
  115. package/dist/page/board/motion.js +71 -0
  116. package/dist/page/board/pointer.js +239 -0
  117. package/dist/page/board/squares.js +26 -0
  118. package/dist/page/dialogs/analyses.js +195 -0
  119. package/dist/page/{dialog.js → dialogs/dialog.js} +1 -1
  120. package/dist/page/dialogs/dialogs.css +1228 -0
  121. package/dist/page/{new-game-dialog.js → dialogs/new-game.js} +3 -3
  122. package/dist/page/{profile-dialogs.js → dialogs/profiles.js} +40 -11
  123. package/dist/page/dialogs/search.js +31 -0
  124. package/dist/page/dialogs/settings.js +42 -0
  125. package/dist/page/dialogs/studied.js +39 -0
  126. package/dist/page/dialogs/study.js +51 -0
  127. package/dist/page/{badges.js → game/badges.js} +1 -1
  128. package/dist/page/{browse.js → game/browse.js} +1 -1
  129. package/dist/page/game/card-fold.js +40 -0
  130. package/dist/page/game/column.js +51 -0
  131. package/dist/page/{ending.js → game/ending.js} +2 -2
  132. package/dist/page/game/game.css +577 -0
  133. package/dist/page/{move-list.js → game/move-list.js} +10 -6
  134. package/dist/page/{plies.js → game/plies.js} +30 -10
  135. package/dist/page/game/result-tile.js +14 -0
  136. package/dist/page/helps/helps.css +689 -0
  137. package/dist/page/helps/helps.js +220 -0
  138. package/dist/page/helps/view.js +127 -0
  139. package/dist/page/layout/fold-tab.js +11 -0
  140. package/dist/page/{folds.js → layout/folds.js} +1 -1
  141. package/dist/page/{header.js → layout/header.js} +5 -4
  142. package/dist/page/layout/layout.css +834 -0
  143. package/dist/page/lead/lead.js +82 -0
  144. package/dist/page/lead/view.js +38 -0
  145. package/dist/page/main.js +2 -2
  146. package/dist/page/pieces/CREDITS.md +12 -29
  147. package/dist/page/pieces/capablanca-small.svg +20 -0
  148. package/dist/page/pieces/capablanca.svg +22 -0
  149. package/dist/page/pieces/nitida-small.svg +3 -2
  150. package/dist/page/pieces/nitida.svg +3 -2
  151. package/dist/page/{pieces.js → pieces/pieces.js} +2 -2
  152. package/dist/page/pieces/torretta-small.svg +15 -14
  153. package/dist/page/pieces/torretta.svg +17 -15
  154. package/dist/page/{player-card.js → players/card.js} +2 -6
  155. package/dist/page/{clock-time.js → players/clock-time.js} +2 -2
  156. package/dist/page/{profile.js → profile/profile.js} +9 -5
  157. package/dist/page/{settings.js → profile/settings.js} +5 -4
  158. package/dist/page/style.css +9 -4398
  159. package/dist/page/talk/talk.css +958 -0
  160. package/dist/page/{talk.js → talk/talk.js} +39 -46
  161. package/dist/page/text.js +137 -19
  162. package/dist/page/theme.css +292 -0
  163. package/dist/profiles/index.d.ts +1 -1
  164. package/dist/profiles/profiles.d.ts +15 -4
  165. package/dist/profiles/profiles.js +24 -7
  166. package/dist/server/bell.d.ts +10 -2
  167. package/dist/server/bell.js +28 -12
  168. package/dist/server/caller.d.ts +14 -3
  169. package/dist/server/caller.js +44 -11
  170. package/dist/server/mcp.d.ts +1 -2
  171. package/dist/server/mcp.js +42 -14
  172. package/dist/server/page/analyses.d.ts +23 -0
  173. package/dist/server/page/analyses.js +65 -0
  174. package/dist/server/page/files.d.ts +5 -0
  175. package/dist/server/page/files.js +92 -0
  176. package/dist/server/page/game.d.ts +6 -0
  177. package/dist/server/page/game.js +381 -0
  178. package/dist/server/{http.d.ts → page/http.d.ts} +13 -0
  179. package/dist/server/{http.js → page/http.js} +11 -0
  180. package/dist/server/{page.d.ts → page/page.d.ts} +8 -13
  181. package/dist/server/page/page.js +229 -0
  182. package/dist/server/{profile-routes.d.ts → page/profiles.d.ts} +2 -2
  183. package/dist/server/{profile-routes.js → page/profiles.js} +47 -18
  184. package/instructions/analysis.md +4 -0
  185. package/instructions/analyst-agent.md +8 -0
  186. package/instructions/analyst.md +3 -7
  187. package/instructions/coach.md +11 -0
  188. package/instructions/opponent.md +4 -4
  189. package/package.json +1 -1
  190. package/dist/host-files/ponder-agents.js +0 -40
  191. package/dist/page/avatars.js +0 -16
  192. package/dist/page/board.js +0 -566
  193. package/dist/page/fold-tab.js +0 -9
  194. package/dist/page/game-column.js +0 -44
  195. package/dist/page/helps-view.js +0 -51
  196. package/dist/page/helps.js +0 -90
  197. package/dist/page/pieces/cburnett-small.svg +0 -20
  198. package/dist/page/pieces/cburnett.svg +0 -20
  199. package/dist/page/settings-panel.js +0 -32
  200. package/dist/page/studied.js +0 -14
  201. package/dist/page/study-dialog.js +0 -58
  202. package/dist/server/analyses.d.ts +0 -17
  203. package/dist/server/analyses.js +0 -46
  204. package/dist/server/page.js +0 -657
  205. /package/dist/page/{torretta.js → avatars/torretta.js} +0 -0
  206. /package/dist/page/{opening-book.js → game/opening-book.js} +0 -0
  207. /package/dist/page/{model-name.js → players/model-name.js} +0 -0
  208. /package/dist/page/{sounds.js → sounds/sounds.js} +0 -0
  209. /package/dist/page/{voice.js → talk/voice.js} +0 -0
@@ -1,4 +1,4 @@
1
- // The talk on the board page (D-51, T-65): the player's words and the AI's answers, kept in game.json beside the PGN, or in talk.json with no game (D-77).
1
+ // The talk on the board page (D-51): the player's words and the AI's answers, kept in game.json beside the PGN, or in talk.json with no game (D-77).
2
2
  import { endOf } from "./current-game.js";
3
3
  /** The most the player may write in a move, in all their lines, and the most a line of the AI's may hold (D-54); with the helps on, as an analyst's note (D-78). */
4
4
  export const talkLimits = { player: 200, ai: 300, aiHelps: 700 };
@@ -25,7 +25,7 @@ export function talkLine(current, sayer, text, at, ply = isGame(current) ? curre
25
25
  }
26
26
  return { who, text: words, ply, at: at.toISOString(), ...(sayer === "agent" ? { agent: true } : {}) };
27
27
  }
28
- /** Words refused, and why, for the page to say it in the player's language (T-11): none, the move's characters spent, more than a line or what is left holds. */
28
+ /** Words refused, and why, for the page to say it in the player's language: none, the move's characters spent, more than a line or what is left holds. */
29
29
  export class WordsRefused extends Error {
30
30
  reason;
31
31
  most;
@@ -35,18 +35,6 @@ export class WordsRefused extends Error {
35
35
  this.most = most;
36
36
  }
37
37
  }
38
- /** The lines the player wrote since the AI's last move: those it answers with its next move, and the 200 characters of this move are counted on them. */
39
- export function playerLinesThisMove(current) {
40
- const since = sinceAiMoved(current);
41
- return current.talk.filter((line) => line.who === "player" && line.ply >= since);
42
- }
43
- /** How many characters the player may still write for this move; between games a whole line each time, with no count (D-74). */
44
- export function wordsLeft(current) {
45
- if (!going(current)) {
46
- return talkLimits.player;
47
- }
48
- return Math.max(talkLimits.player - playerLinesThisMove(current).reduce((sum, line) => sum + line.text.length, 0), 0);
49
- }
50
38
  /** Whether the player's last line wakes the chat: the first of a move, and between games the first since the AI last spoke (D-54, D-74). */
51
39
  export function wakesTheChat(current) {
52
40
  return going(current) ? playerLinesThisMove(current).length === 1 : unanswered(current).length === 1;
@@ -63,6 +51,18 @@ export function heardLines(current, given) {
63
51
  return line.who === "ai" ? undefined : !waiting.has(key) ? "answered" : given.has(key) ? "read" : "sent";
64
52
  });
65
53
  }
54
+ /** The lines the player wrote since the AI's last move: those it answers with its next move, and the 200 characters of this move are counted on them. */
55
+ function playerLinesThisMove(current) {
56
+ const since = sinceAiMoved(current);
57
+ return current.talk.filter((line) => line.who === "player" && line.ply >= since);
58
+ }
59
+ /** How many characters the player may still write for this move; between games a whole line each time, with no count (D-74). */
60
+ function wordsLeft(current) {
61
+ if (!going(current)) {
62
+ return talkLimits.player;
63
+ }
64
+ return Math.max(talkLimits.player - playerLinesThisMove(current).reduce((sum, line) => sum + line.text.length, 0), 0);
65
+ }
66
66
  /**
67
67
  * The AI speaks apart from its move once a move of the player's, on their time, with something to say or to answer (D-72), and never while its move waits:
68
68
  * it speaks in the move's comment then. Its agent speaks only first, when the player has written nothing to answer, and its word leaves the chat one answer
@@ -1,4 +1,4 @@
1
- // The data folder shown to the player in the system's own file manager (T-119): Explorer, Finder, or the Linux desktop's.
1
+ // The data folder shown to the player in the system's own file manager: Explorer, Finder, or the Linux desktop's.
2
2
  import { spawn } from "node:child_process";
3
3
  /** Opens a folder in the system's file manager, without waiting for it to close: whether the manager started, for the page to say so when it did not. */
4
4
  export function openFolder(folder, platform) {
@@ -1,12 +1,20 @@
1
1
  import type { AiLevel } from "../core/index.ts";
2
+ /** The agents that think for the AI, by their names. */
3
+ export declare const ponderAgents: readonly string[];
4
+ /** The coach's agent (D-88). */
5
+ export declare const coachAgent = "chesspal-coach";
6
+ /** The analyst's agent, which reviews a game over on the board page when the player wants it (D-89). */
7
+ export declare const analystAgent = "chesspal-analyst";
2
8
  /** The agent that thinks for the AI at a level: on the quick model at the beginner and club levels, on the AI's own above. */
3
9
  export declare function ponderAgentFor(level: AiLevel): string;
4
10
  /** Whether the AI's thoughts on the player's time come from a quicker model than its own at a level. */
5
11
  export declare function onQuickModel(level: AiLevel): boolean;
6
12
  /** Whether the player's folder has the agent, as init writes it: a folder set up before it has none, and the AI then plays as before. */
7
- export declare function hasPonderAgent(folder: string, agent: string): boolean;
13
+ export declare function hasAgent(folder: string, agent: string): boolean;
8
14
  /** The agents' files, as Claude Code reads them from .claude/agents: Chesspal's whole, rewritten by init. */
9
- export declare function ponderAgentFiles(folder: string): {
15
+ export declare function agentFiles(folder: string): {
10
16
  path: string;
11
17
  text: string;
12
18
  }[];
19
+ /** Instructions for the AI, by their file's name in the package's instructions folder. */
20
+ export declare function instructions(name: string): string;
@@ -0,0 +1,53 @@
1
+ // The agents that work in the background in Claude Code: those that think for the opponent on the player's time (D-72), one on the host's quick model for the
2
+ // lower levels, one on the AI's own for the higher; the coach, with the helps on (D-88), and the analyst of a game over (D-89), on the AI's own model.
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ const quickAgent = "chesspal-ponder-quick";
6
+ const ownAgent = "chesspal-ponder";
7
+ /** The agents that think for the AI, by their names. */
8
+ export const ponderAgents = [quickAgent, ownAgent];
9
+ /** The coach's agent (D-88). */
10
+ export const coachAgent = "chesspal-coach";
11
+ /** The analyst's agent, which reviews a game over on the board page when the player wants it (D-89). */
12
+ export const analystAgent = "chesspal-analyst";
13
+ /** The agent that thinks for the AI at a level: on the quick model at the beginner and club levels, on the AI's own above. */
14
+ export function ponderAgentFor(level) {
15
+ return onQuickModel(level) ? quickAgent : ownAgent;
16
+ }
17
+ /** Whether the AI's thoughts on the player's time come from a quicker model than its own at a level. */
18
+ export function onQuickModel(level) {
19
+ return level === "beginner" || level === "club";
20
+ }
21
+ /** Whether the player's folder has the agent, as init writes it: a folder set up before it has none, and the AI then plays as before. */
22
+ export function hasAgent(folder, agent) {
23
+ return existsSync(agentPath(folder, agent));
24
+ }
25
+ /** The agents' files, as Claude Code reads them from .claude/agents: Chesspal's whole, rewritten by init. */
26
+ export function agentFiles(folder) {
27
+ const ponder = instructions("ponder");
28
+ const thinking = "Chesspal's opponent thinking on the player's time. Started by the AI in the background after its move, as ai_move says.";
29
+ return [
30
+ { agent: quickAgent, model: "haiku", description: thinking, tools: ["ponder"], text: ponder },
31
+ { agent: ownAgent, model: "inherit", description: thinking, tools: ["ponder"], text: ponder },
32
+ // Its description tells the chat it starts the agent only as ai_move says, never to ask it about the game (Priya).
33
+ { agent: coachAgent, model: "inherit", description: "Chesspal's coach. Started only as Chesspal's answers say, in the background, never to answer the player.", tools: ["coach"], text: instructions("coach") },
34
+ {
35
+ agent: analystAgent,
36
+ model: "inherit",
37
+ description: "Chesspal's analyst. Started only as Chesspal's answers say, in the background, to review a game over on the board page; never to answer the player.",
38
+ tools: ["analyse_game", "annotate_game", "look", "position"],
39
+ text: `${instructions("analyst-agent")}\n${instructions("analysis")}`,
40
+ },
41
+ ].map(({ agent, model, description, tools, text }) => ({
42
+ path: agentPath(folder, agent),
43
+ // The tools it works with: Claude Code gives an agent its MCP tools loaded, with no search first (Priya).
44
+ text: ["---", `name: ${agent}`, `description: ${description}`, `tools: ${tools.map((tool) => `mcp__chesspal__${tool}`).join(", ")}`, `model: ${model}`, "---", "", text, ""].join("\n"),
45
+ }));
46
+ }
47
+ /** Instructions for the AI, by their file's name in the package's instructions folder. */
48
+ export function instructions(name) {
49
+ return readFileSync(new URL(`../../instructions/${name}.md`, import.meta.url), "utf8").trim();
50
+ }
51
+ function agentPath(folder, agent) {
52
+ return join(folder, ".claude", "agents", `${agent}.md`);
53
+ }
@@ -4,7 +4,12 @@
4
4
  * the files written.
5
5
  */
6
6
  export declare function setUpHosts(folder: string, platform: string): string[];
7
- /** Where Codex reads a folder's hooks, which it runs only once the player allows them in /hooks (T-125). */
7
+ /**
8
+ * The files an older init wrote in a folder, or left out, that this version would write otherwise, by their paths in it: the AI and the page then ask the
9
+ * player to run init again. None in a folder init never set up.
10
+ */
11
+ export declare function outdatedHostFiles(folder: string, platform: string): string[];
12
+ /** Where Codex reads a folder's hooks, which it runs only once the player trusts them. */
8
13
  export declare function codexHooksPath(folder: string): string;
9
- /** The instructions of the AI opponent, read from the package wherever it is installed (T-8). */
14
+ /** The instructions of the AI opponent, read from the package wherever it is installed. */
10
15
  export declare function opponentInstructions(): string;
@@ -1,7 +1,8 @@
1
+ // What `chesspal init` writes in a player's folder for each AI host: the opponent's instructions, the MCP server, the hooks and the agents.
1
2
  import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
+ import { join, relative } from "node:path";
3
4
  import { makeFolder, writeAtomically } from "../games/index.js";
4
- import { ponderAgentFiles } from "./ponder-agents.js";
5
+ import { agentFiles, instructions } from "./agents.js";
5
6
  /**
6
7
  * Sets up the player's folder for Claude Code and Codex (D-31): .mcp.json with the chesspal server, AGENTS.md with the opponent's instructions between
7
8
  * Chesspal's marks, CLAUDE.md importing it, the hosts' settings with the server approved, and their hooks. What the player wrote there stays; it returns
@@ -9,33 +10,47 @@ import { ponderAgentFiles } from "./ponder-agents.js";
9
10
  */
10
11
  export function setUpHosts(folder, platform) {
11
12
  makeFolder(folder);
12
- const changes = [mcpFile(folder, platform), agentsFile(folder), claudeFile(folder), claudeSettingsFile(folder), ...ponderAgentChanges(folder), codexConfigFile(folder, platform), codexHooksFile(folder)];
13
- const written = changes.filter((change) => change !== undefined);
13
+ const written = hostChanges(folder, platform);
14
14
  for (const { path, text } of written) {
15
15
  makeFolder(join(path, ".."));
16
16
  writeAtomically(path, text);
17
17
  }
18
18
  return written.map(({ path }) => path);
19
19
  }
20
- /** Where Codex reads a folder's hooks, which it runs only once the player allows them in /hooks (T-125). */
20
+ /**
21
+ * The files an older init wrote in a folder, or left out, that this version would write otherwise, by their paths in it: the AI and the page then ask the
22
+ * player to run init again. None in a folder init never set up.
23
+ */
24
+ export function outdatedHostFiles(folder, platform) {
25
+ if (!markedBlock.test(textOf(join(folder, "AGENTS.md")) ?? "")) {
26
+ return [];
27
+ }
28
+ return hostChanges(folder, platform).map(({ path }) => relative(folder, path));
29
+ }
30
+ /** Where Codex reads a folder's hooks, which it runs only once the player trusts them. */
21
31
  export function codexHooksPath(folder) {
22
32
  return join(folder, ".codex", "hooks.json");
23
33
  }
24
- /** The instructions of the AI opponent, read from the package wherever it is installed (T-8). */
34
+ /** The instructions of the AI opponent, read from the package wherever it is installed. */
25
35
  export function opponentInstructions() {
26
- return readFileSync(new URL("../../instructions/opponent.md", import.meta.url), "utf8").trim();
36
+ return instructions("opponent");
27
37
  }
28
38
  // The marks around Chesspal's lines in AGENTS.md: init rewrites what lies between them, and the player's own text stays around them.
29
39
  const startMark = "<!-- chesspal: these lines up to the end mark are Chesspal's, and chesspal init rewrites them; write your own after it. -->";
30
40
  const endMark = "<!-- chesspal: end -->";
31
41
  const markedBlock = /<!-- chesspal: (?!end)[^\n]*\n[\s\S]*?<!-- chesspal: end -->/;
42
+ /** What init writes for each host, missing or changed: what the player wrote there stays. */
43
+ function hostChanges(folder, platform) {
44
+ const changes = [mcpFile(folder, platform), agentsFile(folder), claudeFile(folder), claudeSettingsFile(folder), ...ponderAgentChanges(folder), codexConfigFile(folder, platform), codexHooksFile(folder)];
45
+ return changes.filter((change) => change !== undefined);
46
+ }
32
47
  /** How a host starts the server: on Windows through cmd, which finds npm's shim. */
33
48
  function chesspalServer(platform) {
34
49
  return platform === "win32" ? { command: "cmd", args: ["/c", "chesspal", "mcp"] } : { command: "chesspal", args: ["mcp"] };
35
50
  }
36
- /** Claude Code's agents that think on the player's time, Chesspal's whole: written when they are missing or changed. */
51
+ /** Claude Code's agents that work on the player's time, thinking for the opponent and coaching, Chesspal's whole: written when they are missing or changed. */
37
52
  function ponderAgentChanges(folder) {
38
- return ponderAgentFiles(folder).filter(({ path, text }) => textOf(path) !== text);
53
+ return agentFiles(folder).filter(({ path, text }) => textOf(path) !== text);
39
54
  }
40
55
  /** .mcp.json with the chesspal server, beside the servers already there, keeping the environment the player gave it, as CHESSPAL_HOME in a playtest. */
41
56
  function mcpFile(folder, platform) {
@@ -112,14 +127,14 @@ function codexConfigFile(folder, platform) {
112
127
  const text = rest === "" ? table : `${rest}\n\n${table}`;
113
128
  return text === before ? undefined : { path, text };
114
129
  }
115
- /** Codex's hooks, read once the player has reviewed them in /hooks: the doorbell of the moves on the page (T-62) and how each chat starts (T-104), beside the player's own. */
130
+ /** Codex's hooks, read once the player has trusted them: the doorbell of the moves on the page and how each chat starts, beside the player's own. */
116
131
  function codexHooksFile(folder) {
117
132
  const path = codexHooksPath(folder);
118
133
  const config = jsonOf(path);
119
134
  const hooks = isObject(config.hooks) ? config.hooks : {};
120
135
  return changed(path, { ...config, hooks: withHook(withHook(hooks, "Stop", codexBellHook), "SessionStart", codexChatHook) });
121
136
  }
122
- // Codex's Stop hook for the doorbell (T-62): it holds the turn until the player acts on the page, then Codex goes on; a day at most. On Windows Codex runs
137
+ // Codex's Stop hook for the doorbell: it holds the turn until the player acts on the page, then Codex goes on; a day at most. On Windows Codex runs
123
138
  // it in PowerShell, where `chesspal` would be npm's .ps1 shim, which a strict execution policy stops.
124
139
  const codexBellHook = {
125
140
  type: "command",
@@ -128,7 +143,7 @@ const codexBellHook = {
128
143
  timeout: 86_400,
129
144
  statusMessage: "Waiting for your move on the board page",
130
145
  };
131
- // Codex's SessionStart hook (T-104): it notes how each chat starts, as Claude Code's does (D-57).
146
+ // Codex's SessionStart hook: it notes how each chat starts, as Claude Code's does (D-57).
132
147
  const codexChatHook = { type: "command", command: "chesspal chat", commandWindows: "chesspal.cmd chat" };
133
148
  /** A JSON file's new content, when it differs from what is there. */
134
149
  function changed(path, value) {
@@ -1,2 +1,2 @@
1
- export { codexHooksPath, opponentInstructions, setUpHosts } from "./host-files.ts";
2
- export { hasPonderAgent, onQuickModel, ponderAgentFor } from "./ponder-agents.ts";
1
+ export { analystAgent, coachAgent, hasAgent, instructions, onQuickModel, ponderAgentFor, ponderAgents } from "./agents.ts";
2
+ export { codexHooksPath, opponentInstructions, outdatedHostFiles, setUpHosts } from "./host-files.ts";
@@ -1,3 +1,3 @@
1
- // The files through which an AI host finds Chesspal in the player's folder (T-7): the server, the opponent's instructions, the hosts' settings.
2
- export { codexHooksPath, opponentInstructions, setUpHosts } from "./host-files.js";
3
- export { hasPonderAgent, onQuickModel, ponderAgentFor } from "./ponder-agents.js";
1
+ // The files through which an AI host finds Chesspal in the player's folder: the server, the opponent's instructions, the hosts' settings.
2
+ export { analystAgent, coachAgent, hasAgent, instructions, onQuickModel, ponderAgentFor, ponderAgents } from "./agents.js";
3
+ export { codexHooksPath, opponentInstructions, outdatedHostFiles, setUpHosts } from "./host-files.js";
package/dist/index.d.ts CHANGED
@@ -1,4 +1,2 @@
1
- export type { Context } from "./commands/context.ts";
2
- export { commands } from "./commands/index.ts";
3
- export type { AnyCommand, Command } from "./commands/registry.ts";
1
+ export { type AnyCommand, type Command, commands, type Context } from "./commands/index.ts";
4
2
  export { version } from "./version.ts";
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
+ // Chesspal's public API: what a program that imports the package reaches.
1
2
  export { commands } from "./commands/index.js";
2
3
  export { version } from "./version.js";
@@ -0,0 +1,88 @@
1
+ // The move shown, kept in the page's address as #2026-11-04-184025/31: the game by its name and how many of its moves are shown, so that a reload keeps it and
2
+ // a link brings the player back to it.
3
+ import { useEffect, useRef, useState } from "preact/hooks";
4
+ /** The place an address's hash names, or none: #2026-11-04-184025/31 a move of a game, #2026-11-04-184025 the game as it is. */
5
+ export function anchorOf(hash) {
6
+ const [, game, plies] = new RegExp(`^#?(${stem})(?:/(\\d{1,4}))?$`).exec(hash) ?? [];
7
+ return game === undefined ? undefined : plies === undefined ? { game } : { game, plies: Number(plies) };
8
+ }
9
+ /** The hash of a place in a game, as anchorOf reads it. */
10
+ export function hashOf(anchor) {
11
+ return anchor.plies === undefined ? `#${anchor.game}` : `#${anchor.game}/${anchor.plies}`;
12
+ }
13
+ /** A game's name in the address, from its name in the player's folder: games/2026-11-04-184025.pgn and study/2026-11-04-184025.pgn are 2026-11-04-184025. */
14
+ export function anchorName(name) {
15
+ return new RegExp(`(${stem})\\.pgn$`).exec(name)?.[1];
16
+ }
17
+ // A game's name without its folder and .pgn, the time it started, as games/archive.ts writes it: the page cannot import that module.
18
+ const stem = /\d{4}-\d{2}-\d{2}-\d{6}/.source;
19
+ /**
20
+ * What the page does with the place the address names: goes to its move on the game shown; drops it while a game goes on; waits while its game opens in
21
+ * analysis, `asked` already; else opens its game in analysis, but the game just played, which opens on the player's yes alone (D-89): the link waits for
22
+ * it while the table is set (D-77), and goes once another game shows (Zoe).
23
+ */
24
+ export function anchorAction(wanted, games, asked) {
25
+ if (wanted.game === games.shown) {
26
+ return "view";
27
+ }
28
+ if (games.going) {
29
+ return "drop";
30
+ }
31
+ if (wanted.game === games.played) {
32
+ return games.shown === undefined ? "wait" : "drop";
33
+ }
34
+ return wanted.game === asked ? "wait" : "open";
35
+ }
36
+ /** The hash of the move shown: the game alone at its end, and while it goes on, when the player wants the position as it is (Zoe). */
37
+ export function shownHash(games, viewing) {
38
+ return games.shown === undefined ? "" : hashOf(viewing === undefined || games.going ? { game: games.shown } : { game: games.shown, plies: viewing });
39
+ }
40
+ /**
41
+ * Keeps the move shown in the address, and goes to the place the address names as the page opens or as the player writes it (anchorAction); `open` opens a
42
+ * game in analysis and says whether it did. A link never shows its move on another game.
43
+ */
44
+ export function useAnchor(games, viewing, view, open) {
45
+ const { ready, shown, played, going } = games;
46
+ const [wanted, setWanted] = useState(() => anchorOf(location.hash));
47
+ const asked = useRef(undefined);
48
+ useEffect(() => {
49
+ const heard = () => {
50
+ asked.current = undefined;
51
+ setWanted(anchorOf(location.hash));
52
+ };
53
+ window.addEventListener("hashchange", heard);
54
+ return () => window.removeEventListener("hashchange", heard);
55
+ }, []);
56
+ useEffect(() => {
57
+ if (!ready || wanted === undefined) {
58
+ return;
59
+ }
60
+ const action = anchorAction(wanted, games, asked.current);
61
+ if (action === "view") {
62
+ view(wanted.plies);
63
+ }
64
+ if (action === "view" || action === "drop") {
65
+ setWanted(undefined);
66
+ }
67
+ if (action !== "open") {
68
+ return;
69
+ }
70
+ const game = wanted.game;
71
+ asked.current = game;
72
+ void open(game).then((opened) => {
73
+ // A place written since stays wanted (Federico).
74
+ if (!opened && asked.current === game) {
75
+ setWanted(undefined);
76
+ }
77
+ });
78
+ }, [ready, shown, played, going, wanted]);
79
+ useEffect(() => {
80
+ if (!ready || wanted !== undefined) {
81
+ return;
82
+ }
83
+ const hash = shownHash(games, viewing);
84
+ if (hash !== location.hash) {
85
+ history.replaceState(history.state, "", hash === "" ? location.pathname + location.search : hash);
86
+ }
87
+ }, [ready, shown, going, viewing, wanted]);
88
+ }