chesspal 0.2.3 → 0.3.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 (214) hide show
  1. package/README.md +11 -12
  2. package/dist/cli/cli.js +83 -3
  3. package/dist/cli/command-line.js +26 -9
  4. package/dist/cli/icon.d.ts +25 -0
  5. package/dist/cli/icon.js +80 -0
  6. package/dist/commands/agents.d.ts +19 -2
  7. package/dist/commands/agents.js +41 -14
  8. package/dist/commands/ai-say.d.ts +2 -1
  9. package/dist/commands/ai-say.js +3 -4
  10. package/dist/commands/analyse-game.d.ts +6 -0
  11. package/dist/commands/analyse-game.js +51 -15
  12. package/dist/commands/annotate-game.d.ts +4 -0
  13. package/dist/commands/annotate-game.js +60 -20
  14. package/dist/commands/answer-lines.d.ts +10 -4
  15. package/dist/commands/answer-lines.js +30 -4
  16. package/dist/commands/coach.js +6 -4
  17. package/dist/commands/context.d.ts +11 -1
  18. package/dist/commands/context.js +5 -0
  19. package/dist/commands/delete-review.js +9 -4
  20. package/dist/commands/game-summary.d.ts +9 -2
  21. package/dist/commands/game-summary.js +11 -0
  22. package/dist/commands/index.d.ts +4 -3
  23. package/dist/commands/index.js +8 -5
  24. package/dist/commands/init.js +2 -4
  25. package/dist/commands/instructions.d.ts +9 -0
  26. package/dist/commands/instructions.js +30 -0
  27. package/dist/commands/lead-input.d.ts +1 -1
  28. package/dist/commands/lead-input.js +7 -4
  29. package/dist/commands/look.d.ts +9 -5
  30. package/dist/commands/look.js +19 -9
  31. package/dist/commands/page-link.js +6 -5
  32. package/dist/commands/past-games.d.ts +1 -1
  33. package/dist/commands/past-games.js +12 -5
  34. package/dist/commands/player-move.js +2 -3
  35. package/dist/commands/ponder.js +1 -1
  36. package/dist/commands/position.js +2 -2
  37. package/dist/commands/profiles.d.ts +23 -9
  38. package/dist/commands/profiles.js +76 -35
  39. package/dist/commands/ratings.d.ts +8 -0
  40. package/dist/commands/ratings.js +45 -0
  41. package/dist/commands/registry.d.ts +1 -1
  42. package/dist/commands/registry.js +3 -2
  43. package/dist/commands/say.d.ts +11 -6
  44. package/dist/commands/say.js +21 -13
  45. package/dist/commands/status.d.ts +6 -2
  46. package/dist/commands/status.js +70 -26
  47. package/dist/commands/study.d.ts +11 -1
  48. package/dist/commands/study.js +39 -7
  49. package/dist/commands/view.d.ts +17 -5
  50. package/dist/commands/view.js +67 -10
  51. package/dist/core/facts.d.ts +14 -2
  52. package/dist/core/facts.js +32 -7
  53. package/dist/core/game.d.ts +5 -0
  54. package/dist/core/game.js +9 -0
  55. package/dist/core/index.d.ts +2 -1
  56. package/dist/core/index.js +1 -1
  57. package/dist/core/placement.d.ts +63 -0
  58. package/dist/core/placement.js +259 -0
  59. package/dist/core/possibilities.d.ts +38 -0
  60. package/dist/core/possibilities.js +254 -0
  61. package/dist/core/review.d.ts +6 -1
  62. package/dist/core/review.js +21 -6
  63. package/dist/games/ai-model.d.ts +4 -2
  64. package/dist/games/ai-model.js +10 -1
  65. package/dist/games/analysis.d.ts +31 -4
  66. package/dist/games/analysis.js +44 -5
  67. package/dist/games/archive.d.ts +6 -3
  68. package/dist/games/archive.js +13 -5
  69. package/dist/games/current-game.d.ts +26 -2
  70. package/dist/games/current-game.js +57 -21
  71. package/dist/games/files.d.ts +7 -0
  72. package/dist/games/files.js +48 -1
  73. package/dist/games/index.d.ts +4 -4
  74. package/dist/games/index.js +4 -4
  75. package/dist/games/table-talk.d.ts +2 -0
  76. package/dist/games/table-talk.js +5 -1
  77. package/dist/games/talk.d.ts +4 -3
  78. package/dist/games/talk.js +3 -3
  79. package/dist/home/data-folder.d.ts +2 -0
  80. package/dist/home/data-folder.js +5 -1
  81. package/dist/home/index.d.ts +1 -1
  82. package/dist/home/index.js +1 -1
  83. package/dist/host-files/agents.d.ts +2 -2
  84. package/dist/host-files/agents.js +12 -6
  85. package/dist/host-files/host-files.d.ts +5 -3
  86. package/dist/host-files/host-files.js +15 -8
  87. package/dist/host-files/index.d.ts +2 -2
  88. package/dist/host-files/index.js +2 -2
  89. package/dist/page/app/app.js +59 -22
  90. package/dist/page/app/keys.js +16 -2
  91. package/dist/page/app/served.js +2 -2
  92. package/dist/page/app/title.js +17 -0
  93. package/dist/page/avatars/torretta.js +4 -4
  94. package/dist/page/board/board.js +2 -1
  95. package/dist/page/board/marks.js +4 -24
  96. package/dist/page/board/signs.js +23 -0
  97. package/dist/page/dialogs/analyses.js +4 -4
  98. package/dist/page/dialogs/dialog.js +4 -1
  99. package/dist/page/dialogs/dialogs.css +84 -10
  100. package/dist/page/dialogs/profiles.js +95 -42
  101. package/dist/page/game/column.js +6 -2
  102. package/dist/page/game/game.css +12 -0
  103. package/dist/page/game/move-list.js +2 -2
  104. package/dist/page/game/plies.js +11 -0
  105. package/dist/page/guide/blocks.js +67 -0
  106. package/dist/page/guide/en.md +66 -0
  107. package/dist/page/guide/foot.js +18 -0
  108. package/dist/page/guide/guide.css +200 -0
  109. package/dist/page/guide/guide.js +89 -0
  110. package/dist/page/guide/it.md +66 -0
  111. package/dist/page/guide/markdown.js +67 -0
  112. package/dist/page/helps/helps.css +10 -11
  113. package/dist/page/helps/helps.js +15 -15
  114. package/dist/page/layout/favicon.svg +13 -0
  115. package/dist/page/layout/header.js +3 -3
  116. package/dist/page/layout/icon.icns +0 -0
  117. package/dist/page/layout/icon.ico +0 -0
  118. package/dist/page/layout/icon.png +0 -0
  119. package/dist/page/layout/layout.css +140 -9
  120. package/dist/page/layout/window.js +45 -0
  121. package/dist/page/legend/legend.css +203 -0
  122. package/dist/page/legend/legend.js +169 -0
  123. package/dist/page/legend/view.js +54 -0
  124. package/dist/page/main.js +4 -1
  125. package/dist/page/players/card.js +1 -1
  126. package/dist/page/profile/profile.js +14 -9
  127. package/dist/page/profile/settings.js +9 -0
  128. package/dist/page/style.css +4 -0
  129. package/dist/page/table/opener.css +65 -0
  130. package/dist/page/table/opener.js +59 -0
  131. package/dist/page/talk/chat.css +97 -0
  132. package/dist/page/talk/talk.css +38 -26
  133. package/dist/page/talk/talk.js +31 -7
  134. package/dist/page/text.js +120 -18
  135. package/dist/page/theme.css +7 -0
  136. package/dist/profiles/index.d.ts +2 -1
  137. package/dist/profiles/index.js +2 -1
  138. package/dist/profiles/profiles.d.ts +10 -15
  139. package/dist/profiles/profiles.js +30 -15
  140. package/dist/profiles/ratings.d.ts +35 -0
  141. package/dist/profiles/ratings.js +61 -0
  142. package/dist/server/bell.d.ts +24 -3
  143. package/dist/server/bell.js +84 -10
  144. package/dist/server/caller.d.ts +2 -1
  145. package/dist/server/caller.js +3 -2
  146. package/dist/server/mcp.d.ts +7 -3
  147. package/dist/server/mcp.js +106 -56
  148. package/dist/server/page/analyses.d.ts +2 -0
  149. package/dist/server/page/analyses.js +20 -5
  150. package/dist/server/page/attach.d.ts +4 -0
  151. package/dist/server/page/attach.js +112 -0
  152. package/dist/server/page/chat.d.ts +60 -0
  153. package/dist/server/page/chat.js +79 -0
  154. package/dist/server/page/events.d.ts +7 -0
  155. package/dist/server/page/events.js +20 -0
  156. package/dist/server/page/files.js +10 -1
  157. package/dist/server/page/game.js +62 -29
  158. package/dist/server/page/page.d.ts +47 -7
  159. package/dist/server/page/page.js +74 -51
  160. package/dist/server/page/profiles.d.ts +5 -0
  161. package/dist/server/page/profiles.js +26 -10
  162. package/dist/server/page/table.d.ts +39 -0
  163. package/dist/server/page/table.js +102 -0
  164. package/dist/server/table/browser.d.ts +6 -0
  165. package/dist/server/table/browser.js +48 -0
  166. package/dist/server/table/hosts/chats.d.ts +7 -0
  167. package/dist/server/table/hosts/chats.js +31 -0
  168. package/dist/server/table/hosts/find.d.ts +2 -0
  169. package/dist/server/table/hosts/find.js +20 -0
  170. package/dist/server/table/hosts/hosts.d.ts +34 -0
  171. package/dist/server/table/hosts/hosts.js +49 -0
  172. package/dist/server/table/hosts/index.d.ts +3 -0
  173. package/dist/server/table/hosts/index.js +4 -0
  174. package/dist/server/table/hosts/play.d.ts +25 -0
  175. package/dist/server/table/hosts/play.js +152 -0
  176. package/dist/server/table/hosts/seat.d.ts +25 -0
  177. package/dist/server/table/hosts/seat.js +74 -0
  178. package/dist/server/table/hosts/terminal.d.ts +6 -0
  179. package/dist/server/table/hosts/terminal.js +63 -0
  180. package/dist/server/table/icon/index.d.ts +32 -0
  181. package/dist/server/table/icon/index.js +83 -0
  182. package/dist/server/table/icon/linux.d.ts +5 -0
  183. package/dist/server/table/icon/linux.js +47 -0
  184. package/dist/server/table/icon/macos.d.ts +3 -0
  185. package/dist/server/table/icon/macos.js +38 -0
  186. package/dist/server/table/icon/windows.d.ts +3 -0
  187. package/dist/server/table/icon/windows.js +48 -0
  188. package/dist/server/table/index.d.ts +6 -0
  189. package/dist/server/table/index.js +7 -0
  190. package/dist/server/table/launch.d.ts +19 -0
  191. package/dist/server/table/launch.js +78 -0
  192. package/dist/server/table/link.d.ts +10 -0
  193. package/dist/server/table/link.js +230 -0
  194. package/dist/server/table/table.d.ts +35 -0
  195. package/dist/server/table/table.js +94 -0
  196. package/dist/sites/chesscom.d.ts +4 -0
  197. package/dist/sites/chesscom.js +25 -0
  198. package/dist/sites/choice.d.ts +17 -0
  199. package/dist/sites/choice.js +13 -0
  200. package/dist/sites/index.d.ts +2 -0
  201. package/dist/sites/index.js +4 -0
  202. package/dist/sites/lichess.d.ts +4 -0
  203. package/dist/sites/lichess.js +32 -0
  204. package/dist/sites/request.d.ts +11 -0
  205. package/dist/sites/request.js +15 -0
  206. package/dist/sites/sites.d.ts +6 -0
  207. package/dist/sites/sites.js +33 -0
  208. package/instructions/analysis.md +1 -1
  209. package/instructions/analyst-agent.md +1 -1
  210. package/instructions/coach.md +2 -1
  211. package/instructions/opponent.md +6 -6
  212. package/instructions/ponder.md +1 -0
  213. package/instructions/start.md +1 -0
  214. package/package.json +1 -1
@@ -1,10 +1,10 @@
1
1
  // The player's current game on disk (D-27, D-41): game.json, its PGN and the talk on the board page inside, written whole at every change, and kept in
2
2
  // memory while nobody else changes it.
3
- import { readFileSync, rmSync, statSync } from "node:fs";
3
+ import { readFileSync, rmSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { z } from "zod";
6
6
  import { Game, readPgn, writePgn } from "../core/index.js";
7
- import { writeAtomically } from "./files.js";
7
+ import { stampOf, whileLocked, writeAtomically } from "./files.js";
8
8
  /** The current game of a folder, as it is on disk, or undefined when there is none. A game.json Chesspal cannot read throws: nothing overwrites it. */
9
9
  export function loadGame(folder) {
10
10
  const path = join(folder, fileName);
@@ -21,27 +21,41 @@ export function loadGame(folder) {
21
21
  kept.set(path, { stamp, current });
22
22
  return current;
23
23
  }
24
- /** Writes the current game to game.json, whole or not at all; from then on it is the one kept in memory. */
24
+ /**
25
+ * Writes the current game to game.json, whole or not at all; from then on it is the one kept in memory. Refused with GameChanged when another process wrote
26
+ * the file since this one read it: the change made on the game read before would lose theirs.
27
+ */
25
28
  export function saveGame(folder, current) {
26
29
  const path = join(folder, fileName);
27
- const file = {
28
- format: 1,
29
- ai: current.ai,
30
- started: current.started,
31
- pgn: writePgn(current.game.record),
32
- ...(current.talk.length === 0 ? {} : { talk: current.talk }),
33
- ...(current.clock === undefined ? {} : { clock: savedClockOf(current.clock) }),
34
- ...(drawOfferOf(current) === undefined ? {} : { drawOffer: current.drawOffer }),
30
+ whileLocked(path, () => {
31
+ if (stampOf(path) !== kept.get(path)?.stamp) {
32
+ kept.delete(path);
33
+ throw new GameChanged();
34
+ }
35
+ write(path, current);
36
+ });
37
+ }
38
+ /** The refusal of a save on a game another process changed since this one read it: what wrote it reads it again and starts over, once (rerunIfChanged). */
39
+ export class GameChanged extends Error {
40
+ constructor() {
41
+ super("The game changed on disk while this call was changing it, in another chat or on the board page: call it again. Nothing changed.");
42
+ }
43
+ }
44
+ /** Runs a change of the game, and once more from the start when another process changed the game under it: a second refusal stands. */
45
+ export function rerunIfChanged(run) {
46
+ const again = (error) => {
47
+ if (!(error instanceof GameChanged)) {
48
+ throw error;
49
+ }
50
+ return run();
35
51
  };
36
52
  try {
37
- writeAtomically(path, `${JSON.stringify(file, null, 2)}\n`);
53
+ const result = run();
54
+ return result instanceof Promise ? result.catch(again) : result;
38
55
  }
39
56
  catch (error) {
40
- // The live game may hold a move the file does not: the next call reads the file again.
41
- kept.delete(path);
42
- throw new Error(`Chesspal could not save the game: ${error.message} Nothing changed.`, { cause: error });
57
+ return again(error);
43
58
  }
44
- kept.set(path, { stamp: stampOf(path) ?? "", current });
45
59
  }
46
60
  /** How the game ended, or undefined while it goes on: by the board, or by what the record says, such as a resignation. */
47
61
  export function endOf(game) {
@@ -73,7 +87,14 @@ export function removeGame(folder) {
73
87
  }
74
88
  const fileName = "game.json";
75
89
  /** A line of the talk as game.json and talk.json keep it. */
76
- export const savedLine = z.object({ who: z.enum(["player", "ai"]), text: z.string(), ply: z.int().min(0), at: z.iso.datetime(), agent: z.literal(true).optional() });
90
+ export const savedLine = z.object({
91
+ who: z.enum(["player", "ai"]),
92
+ text: z.string(),
93
+ ply: z.int().min(0),
94
+ at: z.iso.datetime(),
95
+ agent: z.literal(true).optional(),
96
+ shown: z.object({ game: z.string(), at: z.int().min(0), move: z.string().optional() }).optional(),
97
+ });
77
98
  const savedClock = z.object({
78
99
  base: z.int().positive(),
79
100
  increment: z.int().min(0),
@@ -93,10 +114,25 @@ const gameFile = z.object({
93
114
  });
94
115
  // The games read or written in this process, by their file, with the file's stamp then: a call reads a file again only when another process changed it.
95
116
  const kept = new Map();
96
- function stampOf(path) {
97
- // The file's id changes at every rename: a write within the same tick of the clock, and of the same size, is seen too.
98
- const stats = statSync(path, { bigint: true, throwIfNoEntry: false });
99
- return stats === undefined ? undefined : `${stats.ino}:${stats.mtimeNs}:${stats.size}`;
117
+ function write(path, current) {
118
+ const file = {
119
+ format: 1,
120
+ ai: current.ai,
121
+ started: current.started,
122
+ pgn: writePgn(current.game.record),
123
+ ...(current.talk.length === 0 ? {} : { talk: current.talk }),
124
+ ...(current.clock === undefined ? {} : { clock: savedClockOf(current.clock) }),
125
+ ...(drawOfferOf(current) === undefined ? {} : { drawOffer: current.drawOffer }),
126
+ };
127
+ try {
128
+ writeAtomically(path, `${JSON.stringify(file, null, 2)}\n`);
129
+ }
130
+ catch (error) {
131
+ // The live game may hold a move the file does not: the next call reads the file again.
132
+ kept.delete(path);
133
+ throw new Error(`Chesspal could not save the game: ${error.message} Nothing changed.`, { cause: error });
134
+ }
135
+ kept.set(path, { stamp: stampOf(path) ?? "", current });
100
136
  }
101
137
  function parsed(text, path) {
102
138
  try {
@@ -1,7 +1,14 @@
1
1
  /** Writes a file whole or not at all: a temporary file beside it, then renamed over it (D-27). A failure says what to do about it. */
2
2
  export declare function writeAtomically(path: string, text: string | Uint8Array): void;
3
+ /**
4
+ * Runs `write` while this process alone holds the file's lock, a folder beside it made only if missing, cheaper than a file on Windows: another process
5
+ * waits, a second at most. A lock older than two seconds is of a process that stopped while it held it, and goes.
6
+ */
7
+ export declare function whileLocked<T>(path: string, write: () => T): T;
3
8
  /** Makes a folder, and those above it, when missing. A failure says what to do about it. */
4
9
  export declare function makeFolder(path: string): void;
10
+ /** A stamp of a file that changes whenever it is written, whoever writes it; undefined when there is none. */
11
+ export declare function stampOf(path: string): string | undefined;
5
12
  /**
6
13
  * Renames a file or a folder. Windows refuses while another program holds a file in it open (an editor, an antivirus, a sync client): Chesspal waits a
7
14
  * little and tries again, half a second at most.
@@ -1,5 +1,5 @@
1
1
  // Writing the player's files safely: whole or not at all, with failures that say what to do (D-27).
2
- import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { mkdirSync, renameSync, rmdirSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  /** Writes a file whole or not at all: a temporary file beside it, then renamed over it (D-27). A failure says what to do about it. */
4
4
  export function writeAtomically(path, text) {
5
5
  const temporary = `${path}.${process.pid}.tmp`;
@@ -14,6 +14,40 @@ export function writeAtomically(path, text) {
14
14
  rmSync(temporary, { force: true });
15
15
  }
16
16
  }
17
+ /**
18
+ * Runs `write` while this process alone holds the file's lock, a folder beside it made only if missing, cheaper than a file on Windows: another process
19
+ * waits, a second at most. A lock older than two seconds is of a process that stopped while it held it, and goes.
20
+ */
21
+ export function whileLocked(path, write) {
22
+ const lock = `${path}.lock`;
23
+ for (let attempt = 1;; attempt++) {
24
+ try {
25
+ mkdirSync(lock);
26
+ break;
27
+ }
28
+ catch (error) {
29
+ // Windows refuses to make a folder another process is deleting: the lock is still held.
30
+ if (!["EEXIST", "EPERM", "EACCES"].includes(error.code ?? "")) {
31
+ throw new Error(`${lock} could not be made (${error.message}): check that its folder can be written.`, { cause: error });
32
+ }
33
+ if (isStale(lock)) {
34
+ rmSync(lock, { recursive: true, force: true });
35
+ }
36
+ else if (attempt === lockAttempts) {
37
+ throw new Error(`${lock} is held by another Chesspal writing the game: try again in a moment, or delete that folder when no Chesspal runs.`);
38
+ }
39
+ else {
40
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
41
+ }
42
+ }
43
+ }
44
+ try {
45
+ return write();
46
+ }
47
+ finally {
48
+ rmdirSync(lock);
49
+ }
50
+ }
17
51
  /** Makes a folder, and those above it, when missing. A failure says what to do about it. */
18
52
  export function makeFolder(path) {
19
53
  try {
@@ -23,7 +57,20 @@ export function makeFolder(path) {
23
57
  throw new Error(`${path} could not be made (${error.message}): check that it is a folder, and can be written.`, { cause: error });
24
58
  }
25
59
  }
60
+ /** A stamp of a file that changes whenever it is written, whoever writes it; undefined when there is none. */
61
+ export function stampOf(path) {
62
+ // The file's id changes at every rename: a write within the same tick of the clock, and of the same size, is seen too.
63
+ const stats = statSync(path, { bigint: true, throwIfNoEntry: false });
64
+ return stats === undefined ? undefined : `${stats.ino}:${stats.mtimeNs}:${stats.size}`;
65
+ }
26
66
  const attempts = 10;
67
+ // A lock is waited for 5 ms at a time, a second at most; one older than two seconds is of a process that stopped while it held it.
68
+ const lockAttempts = 200;
69
+ const staleLock = 2_000;
70
+ function isStale(lock) {
71
+ const stats = statSync(lock, { throwIfNoEntry: false });
72
+ return stats !== undefined && Date.now() - stats.mtimeMs > staleLock;
73
+ }
27
74
  /**
28
75
  * Renames a file or a folder. Windows refuses while another program holds a file in it open (an editor, an antivirus, a sync client): Chesspal waits a
29
76
  * little and tries again, half a second at most.
@@ -1,11 +1,11 @@
1
- export { aiHostOf, aiModelOf, noteAiHost, noteAiModel } from "./ai-model.ts";
2
- export { type Analysis, endAnalysis, gameInAnalysis, startAnalysis } from "./analysis.ts";
1
+ export { aiHostOf, aiModelOf, forgetTheAi, noteAiHost, noteAiModel } from "./ai-model.ts";
2
+ export { type Analysis, type AnalysisStart, endAnalysis, forgetAnalyst, gameInAnalysis, startAnalysis } from "./analysis.ts";
3
3
  export { archiveCoached, archivedGame, archivedGames, archivedName, archivedWhen, archiveEnded, archiveGame, archiveName, deleteGame, gameStemOf, isGameName, isOverOnTable, keepToStudy, keptGame, keptGames, listedName, movesPgn, playerPgn, rewriteGame, startOfNewGame, } from "./archive.ts";
4
4
  export { type ChatStart, cleanSince, lastChatStart, saveChatStart } from "./chat-start.ts";
5
5
  export { type CoachHint, type CoachJudgment, type CoachLead, coachShown, coachStarted, coachWaitingSince, type CoachWords, coachWords, forgetCoach, keepHint, keepJudgment, keepLead, startCoach, withCoach } from "./coach.ts";
6
- export { bothSidesMoved, type CurrentGame, drawOfferOf, endOf, type GameEnd, gameStamp, loadGame, removeGame, saveGame, type Speaker, type TalkLine } from "./current-game.ts";
6
+ export { bothSidesMoved, type CurrentGame, drawOfferOf, endOf, GameChanged, type GameEnd, gameStamp, loadGame, removeGame, rerunIfChanged, saveGame, type Shown, type Speaker, type TalkLine } from "./current-game.ts";
7
7
  export { makeFolder, renameOver, writeAtomically } from "./files.ts";
8
8
  export { commentWithPlan, currentPlan, planOf, withoutPlans } from "./plans.ts";
9
- export { endTableTalk, loadTableTalk, saveTableTalk, type TableTalk } from "./table-talk.ts";
9
+ export { endTableTalk, loadTableTalk, saveTableTalk, type TableTalk, tableTalkStamp } from "./table-talk.ts";
10
10
  export { type Heard, heardLines, type Sayer, type Talk, talkLimits, talkLine, unheardWords, wakesTheChat, type Words, WordsRefused } from "./talk.ts";
11
11
  export { keepThought, notePondering, ponderingOn, type Thought, thoughtForLastMove, thoughtKey, waitingSince } from "./thoughts.ts";
@@ -1,12 +1,12 @@
1
1
  // The player's games on disk: the current one in game.json, the finished ones in games/ (D-27, D-41), the games kept to study in study/ (D-68). Below the commands, above the core.
2
- export { aiHostOf, aiModelOf, noteAiHost, noteAiModel } from "./ai-model.js";
3
- export { endAnalysis, gameInAnalysis, startAnalysis } from "./analysis.js";
2
+ export { aiHostOf, aiModelOf, forgetTheAi, noteAiHost, noteAiModel } from "./ai-model.js";
3
+ export { endAnalysis, forgetAnalyst, gameInAnalysis, startAnalysis } from "./analysis.js";
4
4
  export { archiveCoached, archivedGame, archivedGames, archivedName, archivedWhen, archiveEnded, archiveGame, archiveName, deleteGame, gameStemOf, isGameName, isOverOnTable, keepToStudy, keptGame, keptGames, listedName, movesPgn, playerPgn, rewriteGame, startOfNewGame, } from "./archive.js";
5
5
  export { cleanSince, lastChatStart, saveChatStart } from "./chat-start.js";
6
6
  export { coachShown, coachStarted, coachWaitingSince, coachWords, forgetCoach, keepHint, keepJudgment, keepLead, startCoach, withCoach } from "./coach.js";
7
- export { bothSidesMoved, drawOfferOf, endOf, gameStamp, loadGame, removeGame, saveGame } from "./current-game.js";
7
+ export { bothSidesMoved, drawOfferOf, endOf, GameChanged, gameStamp, loadGame, removeGame, rerunIfChanged, saveGame } from "./current-game.js";
8
8
  export { makeFolder, renameOver, writeAtomically } from "./files.js";
9
9
  export { commentWithPlan, currentPlan, planOf, withoutPlans } from "./plans.js";
10
- export { endTableTalk, loadTableTalk, saveTableTalk } from "./table-talk.js";
10
+ export { endTableTalk, loadTableTalk, saveTableTalk, tableTalkStamp } from "./table-talk.js";
11
11
  export { heardLines, talkLimits, talkLine, unheardWords, wakesTheChat, WordsRefused } from "./talk.js";
12
12
  export { keepThought, notePondering, ponderingOn, thoughtForLastMove, thoughtKey, waitingSince } from "./thoughts.js";
@@ -8,5 +8,7 @@ export interface TableTalk {
8
8
  export declare function loadTableTalk(folder: string): TableTalk;
9
9
  /** Writes the talk with no game, whole or not at all. */
10
10
  export declare function saveTableTalk(folder: string, table: TableTalk): void;
11
+ /** A stamp of talk.json that changes whenever it is written; undefined when there is none. */
12
+ export declare function tableTalkStamp(folder: string): string | undefined;
11
13
  /** Forgets the talk with no game, when a game starts. */
12
14
  export declare function endTableTalk(folder: string): void;
@@ -3,7 +3,7 @@ import { readFileSync, rmSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { z } from "zod";
5
5
  import { savedLine } from "./current-game.js";
6
- import { writeAtomically } from "./files.js";
6
+ import { stampOf, writeAtomically } from "./files.js";
7
7
  /** The talk with no game: empty when there is none, or talk.json cannot be read, since it is only words. */
8
8
  export function loadTableTalk(folder) {
9
9
  try {
@@ -17,6 +17,10 @@ export function loadTableTalk(folder) {
17
17
  export function saveTableTalk(folder, table) {
18
18
  writeAtomically(join(folder, fileName), `${JSON.stringify(table, null, 2)}\n`);
19
19
  }
20
+ /** A stamp of talk.json that changes whenever it is written; undefined when there is none. */
21
+ export function tableTalkStamp(folder) {
22
+ return stampOf(join(folder, fileName));
23
+ }
20
24
  /** Forgets the talk with no game, when a game starts. */
21
25
  export function endTableTalk(folder) {
22
26
  rmSync(join(folder, fileName), { force: true });
@@ -1,4 +1,4 @@
1
- import { type CurrentGame, type Speaker, type TalkLine } from "./current-game.ts";
1
+ import { type CurrentGame, type Shown, type Speaker, type TalkLine } from "./current-game.ts";
2
2
  import type { TableTalk } from "./table-talk.ts";
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 declare const talkLimits: {
@@ -14,7 +14,7 @@ export type Talk = CurrentGame | TableTalk;
14
14
  * A new line of the talk, said when the game has `ply` moves: refused when it is empty or too long. The player writes 200 characters a move in all, the
15
15
  * words sent with the move included; the AI speaks once a move of theirs, on their time, and its agent's word leaves it an answer to what they write after (D-54, D-72).
16
16
  */
17
- export declare function talkLine(current: Talk, sayer: Sayer, text: string, at: Date, ply?: number, aiMost?: number): TalkLine;
17
+ export declare function talkLine(current: Talk, sayer: Sayer, text: string, at: Date, ply?: number, aiMost?: number, shown?: Shown): TalkLine;
18
18
  /** 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. */
19
19
  export declare class WordsRefused extends Error {
20
20
  readonly reason: "empty" | "spent" | "long" | "longer-than-left";
@@ -23,10 +23,11 @@ export declare class WordsRefused extends Error {
23
23
  }
24
24
  /** 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). */
25
25
  export declare function wakesTheChat(current: Talk): boolean;
26
- /** Words of the player's, with the key that tells them apart across games. */
26
+ /** Words of the player's, with the key that tells them apart across games, and in analysis the position the page showed (D-101). */
27
27
  export interface Words {
28
28
  readonly key: string;
29
29
  readonly text: string;
30
+ readonly shown?: Shown;
30
31
  }
31
32
  /** What the player said that the AI has not answered, but for the words whose keys are in `given`. */
32
33
  export declare function unheardWords(current: Talk, given: ReadonlySet<string>): Words[];
@@ -6,7 +6,7 @@ export const talkLimits = { player: 200, ai: 300, aiHelps: 700 };
6
6
  * A new line of the talk, said when the game has `ply` moves: refused when it is empty or too long. The player writes 200 characters a move in all, the
7
7
  * words sent with the move included; the AI speaks once a move of theirs, on their time, and its agent's word leaves it an answer to what they write after (D-54, D-72).
8
8
  */
9
- export function talkLine(current, sayer, text, at, ply = isGame(current) ? current.game.record.moves.length : 0, aiMost = talkLimits.ai) {
9
+ export function talkLine(current, sayer, text, at, ply = isGame(current) ? current.game.record.moves.length : 0, aiMost = talkLimits.ai, shown) {
10
10
  const who = sayer === "agent" ? "ai" : sayer;
11
11
  const words = text.trim();
12
12
  const most = who === "player" ? wordsLeft(current) : aiMost;
@@ -23,7 +23,7 @@ export function talkLine(current, sayer, text, at, ply = isGame(current) ? curre
23
23
  if (who === "ai" && isGame(current)) {
24
24
  refuseAiWords(current, sayer === "agent");
25
25
  }
26
- return { who, text: words, ply, at: at.toISOString(), ...(sayer === "agent" ? { agent: true } : {}) };
26
+ return { who, text: words, ply, at: at.toISOString(), ...(sayer === "agent" ? { agent: true } : {}), ...(shown === undefined ? {} : { shown }) };
27
27
  }
28
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 {
@@ -41,7 +41,7 @@ export function wakesTheChat(current) {
41
41
  }
42
42
  /** What the player said that the AI has not answered, but for the words whose keys are in `given`. */
43
43
  export function unheardWords(current, given) {
44
- return unanswered(current).filter(({ key }) => !given.has(key)).map(({ key, line }) => ({ key, text: line.text }));
44
+ return unanswered(current).filter(({ key }) => !given.has(key)).map(({ key, line }) => ({ key, text: line.text, ...(line.shown === undefined ? {} : { shown: line.shown }) }));
45
45
  }
46
46
  /** How far each line of the talk got: the player's by `Heard`, the AI's undefined (D-53). */
47
47
  export function heardLines(current, given) {
@@ -10,3 +10,5 @@ export interface System {
10
10
  }
11
11
  /** The player's data folder: CHESSPAL_HOME when set, else the system's Documents with Chesspal's folder in it. */
12
12
  export declare function dataFolder(system?: System): string;
13
+ /** The table's folder inside the data folder: one for every profile, where the chats the table opens start (D-104). */
14
+ export declare function tableFolder(home: string): string;
@@ -3,7 +3,7 @@
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { readFileSync } from "node:fs";
5
5
  import { homedir } from "node:os";
6
- import { posix, win32 } from "node:path";
6
+ import { join, posix, win32 } from "node:path";
7
7
  /** The player's data folder: CHESSPAL_HOME when set, else the system's Documents with Chesspal's folder in it. */
8
8
  export function dataFolder(system = thisSystem()) {
9
9
  const chosen = system.env.CHESSPAL_HOME?.trim();
@@ -16,6 +16,10 @@ export function dataFolder(system = thisSystem()) {
16
16
  }
17
17
  return posix.join(system.platform === "darwin" ? posix.join(system.home, "Documents") : xdgDocuments(system), "Chesspal");
18
18
  }
19
+ /** The table's folder inside the data folder: one for every profile, where the chats the table opens start (D-104). */
20
+ export function tableFolder(home) {
21
+ return join(home, "table");
22
+ }
19
23
  // Where Windows keeps Documents for this user, as Explorer's known folder does: the registry names it, with variables such as %USERPROFILE%.
20
24
  const shellFolders = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders";
21
25
  function thisSystem() {
@@ -1,2 +1,2 @@
1
- export { dataFolder, type System } from "./data-folder.ts";
1
+ export { dataFolder, type System, tableFolder } from "./data-folder.ts";
2
2
  export { openFolder } from "./open-folder.ts";
@@ -1,3 +1,3 @@
1
1
  // Where the player's data is (D-71): one folder for every chat, apart from the host's folder. Below the commands, beside the games and the profiles.
2
- export { dataFolder } from "./data-folder.js";
2
+ export { dataFolder, tableFolder } from "./data-folder.js";
3
3
  export { openFolder } from "./open-folder.js";
@@ -22,5 +22,5 @@ export declare function agentFiles(folder: string): {
22
22
  path: string;
23
23
  text: string;
24
24
  }[];
25
- /** Instructions for the AI, by their file's name in the package's instructions folder. */
26
- export declare function instructions(name: string): string;
25
+ /** Instructions for the AI, by their file's name in the package's instructions folder: read once, as the version installed has them. */
26
+ export declare function instructionsFile(name: string): string;
@@ -38,10 +38,16 @@ export function agentFiles(folder) {
38
38
  const agents = agentSpecs();
39
39
  return [...agents.map((agent) => claudeAgentFile(folder, agent)), ...agents.map((agent) => codexAgentFile(folder, agent))];
40
40
  }
41
- /** Instructions for the AI, by their file's name in the package's instructions folder. */
42
- export function instructions(name) {
43
- return readFileSync(new URL(`../../instructions/${name}.md`, import.meta.url), "utf8").trim();
41
+ /** Instructions for the AI, by their file's name in the package's instructions folder: read once, as the version installed has them. */
42
+ export function instructionsFile(name) {
43
+ let text = instructionTexts.get(name);
44
+ if (text === undefined) {
45
+ text = readFileSync(new URL(`../../instructions/${name}.md`, import.meta.url), "utf8").trim();
46
+ instructionTexts.set(name, text);
47
+ }
48
+ return text;
44
49
  }
50
+ const instructionTexts = new Map();
45
51
  const toolsOf = {
46
52
  [quickAgent]: ["ponder"],
47
53
  [ownAgent]: ["ponder"],
@@ -51,7 +57,7 @@ const toolsOf = {
51
57
  // Codex's quick model for the lower levels: written in its agent, since Codex ignores a model the chat names when it starts one that has it (Priya).
52
58
  const codexQuickModel = "gpt-6-luna";
53
59
  function agentSpecs() {
54
- const ponder = instructions("ponder");
60
+ const ponder = instructionsFile("ponder");
55
61
  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.";
56
62
  return [
57
63
  { agent: quickAgent, claudeModel: "haiku", codexModel: codexQuickModel, description: thinking, text: ponder },
@@ -61,13 +67,13 @@ function agentSpecs() {
61
67
  agent: coachAgent,
62
68
  claudeModel: "inherit",
63
69
  description: "Chesspal's coach. Started only as Chesspal's answers say, in the background, never to answer the player.",
64
- text: instructions("coach"),
70
+ text: instructionsFile("coach"),
65
71
  },
66
72
  {
67
73
  agent: analystAgent,
68
74
  claudeModel: "inherit",
69
75
  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.",
70
- text: `${instructions("analyst-agent")}\n${instructions("analysis")}`,
76
+ text: `${instructionsFile("analyst-agent")}\n${instructionsFile("analysis")}`,
71
77
  },
72
78
  ];
73
79
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * 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
3
- * 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
2
+ * Sets up the player's folder for Claude Code and Codex (D-31): .mcp.json with the chesspal server, AGENTS.md with the lines that start a chat between
3
+ * Chesspal's marks (D-102), CLAUDE.md importing it, the hosts' settings with the server approved, and their hooks. What the player wrote there stays; it returns
4
4
  * the files written.
5
5
  */
6
6
  export declare function setUpHosts(folder: string, platform: string): string[];
@@ -11,5 +11,7 @@ export declare function setUpHosts(folder: string, platform: string): string[];
11
11
  export declare function outdatedHostFiles(folder: string, platform: string): string[];
12
12
  /** Where Codex reads a folder's hooks, which it runs only once the player trusts them. */
13
13
  export declare function codexHooksPath(folder: string): string;
14
- /** The instructions of the AI opponent, read from the package wherever it is installed. */
14
+ /** The instructions of the AI opponent, read from the package wherever it is installed: the MCP server's `instructions` tool gives them (D-102). */
15
15
  export declare function opponentInstructions(): string;
16
+ /** The lines that start a chat, in the MCP server's instructions and in AGENTS.md: call `instructions`, then `status` (D-102). */
17
+ export declare function startInstructions(): string;
@@ -1,11 +1,11 @@
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
+ // What `chesspal init` writes in a player's folder for each AI host: the lines that start a chat, the MCP server, the hooks and the agents.
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { join, relative } from "node:path";
4
4
  import { makeFolder, writeAtomically } from "../games/index.js";
5
- import { agentFiles, instructions } from "./agents.js";
5
+ import { agentFiles, instructionsFile } from "./agents.js";
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
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
7
+ * Sets up the player's folder for Claude Code and Codex (D-31): .mcp.json with the chesspal server, AGENTS.md with the lines that start a chat between
8
+ * Chesspal's marks (D-102), CLAUDE.md importing it, the hosts' settings with the server approved, and their hooks. What the player wrote there stays; it returns
9
9
  * the files written.
10
10
  */
11
11
  export function setUpHosts(folder, platform) {
@@ -31,9 +31,13 @@ export function outdatedHostFiles(folder, platform) {
31
31
  export function codexHooksPath(folder) {
32
32
  return join(folder, ".codex", "hooks.json");
33
33
  }
34
- /** The instructions of the AI opponent, read from the package wherever it is installed. */
34
+ /** The instructions of the AI opponent, read from the package wherever it is installed: the MCP server's `instructions` tool gives them (D-102). */
35
35
  export function opponentInstructions() {
36
- return instructions("opponent");
36
+ return instructionsFile("opponent");
37
+ }
38
+ /** The lines that start a chat, in the MCP server's instructions and in AGENTS.md: call `instructions`, then `status` (D-102). */
39
+ export function startInstructions() {
40
+ return instructionsFile("start");
37
41
  }
38
42
  // The marks around Chesspal's lines in AGENTS.md: init rewrites what lies between them, and the player's own text stays around them.
39
43
  const startMark = "<!-- chesspal: these lines up to the end mark are Chesspal's, and chesspal init rewrites them; write your own after it. -->";
@@ -60,11 +64,14 @@ function mcpFile(folder, platform) {
60
64
  const before = isObject(servers.chesspal) ? servers.chesspal : {};
61
65
  return changed(path, { ...config, mcpServers: { ...servers, chesspal: { ...chesspalServer(platform), ...(isObject(before.env) ? { env: before.env } : {}) } } });
62
66
  }
63
- /** AGENTS.md with the opponent's instructions between Chesspal's marks: rewritten when they changed, put first in a file that has none. */
67
+ /**
68
+ * AGENTS.md with the lines that start a chat between Chesspal's marks, which Codex needs beside the server's instructions (D-31, D-102): rewritten when
69
+ * they changed, put first in a file that has none.
70
+ */
64
71
  function agentsFile(folder) {
65
72
  const path = join(folder, "AGENTS.md");
66
73
  const before = textOf(path);
67
- const block = [startMark, opponentInstructions(), endMark].join("\n");
74
+ const block = [startMark, startInstructions(), endMark].join("\n");
68
75
  if (before === undefined) {
69
76
  return { path, text: `${block}\n` };
70
77
  }
@@ -1,2 +1,2 @@
1
- export { agentHosts, agentTools, analystAgent, coachAgent, hasAgent, instructions, isCodex, onQuickModel, ponderAgentFor, ponderAgents } from "./agents.ts";
2
- export { codexHooksPath, opponentInstructions, outdatedHostFiles, setUpHosts } from "./host-files.ts";
1
+ export { agentHosts, agentTools, analystAgent, coachAgent, hasAgent, instructionsFile, isCodex, onQuickModel, ponderAgentFor, ponderAgents } from "./agents.ts";
2
+ export { codexHooksPath, opponentInstructions, outdatedHostFiles, setUpHosts, startInstructions } from "./host-files.ts";
@@ -1,3 +1,3 @@
1
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 { agentHosts, agentTools, analystAgent, coachAgent, hasAgent, instructions, isCodex, onQuickModel, ponderAgentFor, ponderAgents } from "./agents.js";
3
- export { codexHooksPath, opponentInstructions, outdatedHostFiles, setUpHosts } from "./host-files.js";
2
+ export { agentHosts, agentTools, analystAgent, coachAgent, hasAgent, instructionsFile, isCodex, onQuickModel, ponderAgentFor, ponderAgents } from "./agents.js";
3
+ export { codexHooksPath, opponentInstructions, outdatedHostFiles, setUpHosts, startInstructions } from "./host-files.js";