chesspal 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/README.md +99 -2
  2. package/dist/cli/chat-hook.d.ts +2 -0
  3. package/dist/cli/chat-hook.js +33 -0
  4. package/dist/cli/cli.d.ts +2 -0
  5. package/dist/cli/cli.js +28 -0
  6. package/dist/cli/command-line.d.ts +7 -0
  7. package/dist/cli/command-line.js +177 -0
  8. package/dist/commands/agents.d.ts +26 -0
  9. package/dist/commands/agents.js +72 -0
  10. package/dist/commands/ai-move.d.ts +28 -0
  11. package/dist/commands/ai-move.js +85 -0
  12. package/dist/commands/ai-say.d.ts +22 -0
  13. package/dist/commands/ai-say.js +58 -0
  14. package/dist/commands/analyse-game.d.ts +65 -0
  15. package/dist/commands/analyse-game.js +265 -0
  16. package/dist/commands/annotate-game.d.ts +39 -0
  17. package/dist/commands/annotate-game.js +131 -0
  18. package/dist/commands/answer-lines.d.ts +28 -0
  19. package/dist/commands/answer-lines.js +144 -0
  20. package/dist/commands/clock.d.ts +15 -0
  21. package/dist/commands/clock.js +45 -0
  22. package/dist/commands/coach.d.ts +45 -0
  23. package/dist/commands/coach.js +344 -0
  24. package/dist/commands/context.d.ts +67 -0
  25. package/dist/commands/context.js +37 -0
  26. package/dist/commands/delete-game.d.ts +7 -0
  27. package/dist/commands/delete-game.js +30 -0
  28. package/dist/commands/delete-review.d.ts +8 -0
  29. package/dist/commands/delete-review.js +34 -0
  30. package/dist/commands/draw-offer.d.ts +10 -0
  31. package/dist/commands/draw-offer.js +43 -0
  32. package/dist/commands/drawings.d.ts +13 -0
  33. package/dist/commands/drawings.js +25 -0
  34. package/dist/commands/end-game.d.ts +23 -0
  35. package/dist/commands/end-game.js +67 -0
  36. package/dist/commands/export-game.d.ts +14 -0
  37. package/dist/commands/export-game.js +27 -0
  38. package/dist/commands/game-summary.d.ts +78 -0
  39. package/dist/commands/game-summary.js +102 -0
  40. package/dist/commands/index.d.ts +18 -0
  41. package/dist/commands/index.js +64 -0
  42. package/dist/commands/init.d.ts +15 -0
  43. package/dist/commands/init.js +73 -0
  44. package/dist/commands/lead-input.d.ts +8 -0
  45. package/dist/commands/lead-input.js +21 -0
  46. package/dist/commands/levels.d.ts +9 -0
  47. package/dist/commands/levels.js +25 -0
  48. package/dist/commands/look.d.ts +31 -0
  49. package/dist/commands/look.js +64 -0
  50. package/dist/commands/new-game.d.ts +68 -0
  51. package/dist/commands/new-game.js +218 -0
  52. package/dist/commands/opening-book.d.ts +13 -0
  53. package/dist/commands/opening-book.js +84 -0
  54. package/dist/commands/page-link.d.ts +11 -0
  55. package/dist/commands/page-link.js +28 -0
  56. package/dist/commands/past-games.d.ts +11 -0
  57. package/dist/commands/past-games.js +47 -0
  58. package/dist/commands/play-move.d.ts +37 -0
  59. package/dist/commands/play-move.js +94 -0
  60. package/dist/commands/player-move.d.ts +13 -0
  61. package/dist/commands/player-move.js +51 -0
  62. package/dist/commands/player-say.d.ts +5 -0
  63. package/dist/commands/player-say.js +16 -0
  64. package/dist/commands/ponder.d.ts +52 -0
  65. package/dist/commands/ponder.js +179 -0
  66. package/dist/commands/position.d.ts +14 -0
  67. package/dist/commands/position.js +130 -0
  68. package/dist/commands/profiles.d.ts +69 -0
  69. package/dist/commands/profiles.js +163 -0
  70. package/dist/commands/registry.d.ts +59 -0
  71. package/dist/commands/registry.js +62 -0
  72. package/dist/commands/say.d.ts +16 -0
  73. package/dist/commands/say.js +84 -0
  74. package/dist/commands/speech.d.ts +8 -0
  75. package/dist/commands/speech.js +20 -0
  76. package/dist/commands/status.d.ts +35 -0
  77. package/dist/commands/status.js +128 -0
  78. package/dist/commands/study-games.d.ts +5 -0
  79. package/dist/commands/study-games.js +40 -0
  80. package/dist/commands/study.d.ts +38 -0
  81. package/dist/commands/study.js +106 -0
  82. package/dist/commands/view.d.ts +14 -0
  83. package/dist/commands/view.js +125 -0
  84. package/dist/core/board-text.d.ts +7 -0
  85. package/dist/core/board-text.js +18 -0
  86. package/dist/core/book.json +1 -0
  87. package/dist/core/clock.d.ts +45 -0
  88. package/dist/core/clock.js +109 -0
  89. package/dist/core/facts.d.ts +55 -0
  90. package/dist/core/facts.js +175 -0
  91. package/dist/core/game.d.ts +95 -0
  92. package/dist/core/game.js +352 -0
  93. package/dist/core/illegal.d.ts +19 -0
  94. package/dist/core/illegal.js +141 -0
  95. package/dist/core/index.d.ts +20 -0
  96. package/dist/core/index.js +12 -0
  97. package/dist/core/lead.d.ts +15 -0
  98. package/dist/core/lead.js +40 -0
  99. package/dist/core/levels.d.ts +20 -0
  100. package/dist/core/levels.js +33 -0
  101. package/dist/core/material.d.ts +9 -0
  102. package/dist/core/material.js +12 -0
  103. package/dist/core/moods.d.ts +21 -0
  104. package/dist/core/moods.js +45 -0
  105. package/dist/core/openings.d.ts +35 -0
  106. package/dist/core/openings.js +98 -0
  107. package/dist/core/openings.json +1 -0
  108. package/dist/core/pgn.d.ts +10 -0
  109. package/dist/core/pgn.js +213 -0
  110. package/dist/core/profile.d.ts +117 -0
  111. package/dist/core/profile.js +232 -0
  112. package/dist/core/record.d.ts +21 -0
  113. package/dist/core/record.js +25 -0
  114. package/dist/core/review.d.ts +80 -0
  115. package/dist/core/review.js +146 -0
  116. package/dist/games/ai-model.d.ts +5 -0
  117. package/dist/games/ai-model.js +15 -0
  118. package/dist/games/analysis.d.ts +12 -0
  119. package/dist/games/analysis.js +26 -0
  120. package/dist/games/archive.d.ts +63 -0
  121. package/dist/games/archive.js +242 -0
  122. package/dist/games/chat-start.d.ts +16 -0
  123. package/dist/games/chat-start.js +26 -0
  124. package/dist/games/coach.d.ts +57 -0
  125. package/dist/games/coach.js +144 -0
  126. package/dist/games/current-game.d.ts +56 -0
  127. package/dist/games/current-game.js +134 -0
  128. package/dist/games/files.d.ts +9 -0
  129. package/dist/games/files.js +53 -0
  130. package/dist/games/index.d.ts +11 -0
  131. package/dist/games/index.js +12 -0
  132. package/dist/games/plans.d.ts +9 -0
  133. package/dist/games/plans.js +23 -0
  134. package/dist/games/table-talk.d.ts +12 -0
  135. package/dist/games/table-talk.js +25 -0
  136. package/dist/games/talk.d.ts +36 -0
  137. package/dist/games/talk.js +131 -0
  138. package/dist/games/thoughts.d.ts +15 -0
  139. package/dist/games/thoughts.js +60 -0
  140. package/dist/home/data-folder.d.ts +12 -0
  141. package/dist/home/data-folder.js +55 -0
  142. package/dist/home/index.d.ts +2 -0
  143. package/dist/home/index.js +3 -0
  144. package/dist/home/open-folder.d.ts +2 -0
  145. package/dist/home/open-folder.js +14 -0
  146. package/dist/host-files/agents.d.ts +20 -0
  147. package/dist/host-files/agents.js +53 -0
  148. package/dist/host-files/host-files.d.ts +15 -0
  149. package/dist/host-files/host-files.js +182 -0
  150. package/dist/host-files/index.d.ts +2 -0
  151. package/dist/host-files/index.js +3 -0
  152. package/dist/index.d.ts +2 -0
  153. package/dist/index.js +3 -0
  154. package/dist/page/app/anchor.js +88 -0
  155. package/dist/page/app/app.js +458 -0
  156. package/dist/page/app/keys.js +94 -0
  157. package/dist/page/app/phase.js +30 -0
  158. package/dist/page/app/served.js +73 -0
  159. package/dist/page/avatars/avatars.js +19 -0
  160. package/dist/page/avatars/beanie.webp +0 -0
  161. package/dist/page/avatars/beard.webp +0 -0
  162. package/dist/page/avatars/bun.webp +0 -0
  163. package/dist/page/avatars/cat.webp +0 -0
  164. package/dist/page/avatars/curls.webp +0 -0
  165. package/dist/page/avatars/glasses.webp +0 -0
  166. package/dist/page/avatars/moustache.webp +0 -0
  167. package/dist/page/avatars/owl.webp +0 -0
  168. package/dist/page/avatars/short-hair.webp +0 -0
  169. package/dist/page/avatars/silver-bob.webp +0 -0
  170. package/dist/page/avatars/thought.js +10 -0
  171. package/dist/page/avatars/torretta.js +38 -0
  172. package/dist/page/base.css +160 -0
  173. package/dist/page/board/board.css +959 -0
  174. package/dist/page/board/board.js +125 -0
  175. package/dist/page/board/explain.js +58 -0
  176. package/dist/page/board/marks.js +160 -0
  177. package/dist/page/board/motion.js +71 -0
  178. package/dist/page/board/pointer.js +239 -0
  179. package/dist/page/board/squares.js +26 -0
  180. package/dist/page/dialogs/analyses.js +195 -0
  181. package/dist/page/dialogs/dialog.js +26 -0
  182. package/dist/page/dialogs/dialogs.css +1228 -0
  183. package/dist/page/dialogs/new-game.js +42 -0
  184. package/dist/page/dialogs/profiles.js +152 -0
  185. package/dist/page/dialogs/search.js +31 -0
  186. package/dist/page/dialogs/settings.js +42 -0
  187. package/dist/page/dialogs/studied.js +39 -0
  188. package/dist/page/dialogs/study.js +51 -0
  189. package/dist/page/game/badges.js +50 -0
  190. package/dist/page/game/browse.js +17 -0
  191. package/dist/page/game/card-fold.js +40 -0
  192. package/dist/page/game/column.js +51 -0
  193. package/dist/page/game/ending.js +73 -0
  194. package/dist/page/game/game.css +577 -0
  195. package/dist/page/game/move-list.js +72 -0
  196. package/dist/page/game/opening-book.js +14 -0
  197. package/dist/page/game/plies.js +82 -0
  198. package/dist/page/game/result-tile.js +14 -0
  199. package/dist/page/helps/helps.css +689 -0
  200. package/dist/page/helps/helps.js +220 -0
  201. package/dist/page/helps/view.js +127 -0
  202. package/dist/page/layout/fold-tab.js +11 -0
  203. package/dist/page/layout/folds.js +43 -0
  204. package/dist/page/layout/header.js +28 -0
  205. package/dist/page/layout/layout.css +834 -0
  206. package/dist/page/lead/lead.js +82 -0
  207. package/dist/page/lead/view.js +38 -0
  208. package/dist/page/main.js +8 -0
  209. package/dist/page/pieces/CREDITS.md +19 -0
  210. package/dist/page/pieces/capablanca-small.svg +20 -0
  211. package/dist/page/pieces/capablanca.svg +22 -0
  212. package/dist/page/pieces/nitida-small.svg +20 -0
  213. package/dist/page/pieces/nitida.svg +21 -0
  214. package/dist/page/pieces/pieces.js +28 -0
  215. package/dist/page/pieces/torretta-small.svg +20 -0
  216. package/dist/page/pieces/torretta.svg +21 -0
  217. package/dist/page/players/card.js +56 -0
  218. package/dist/page/players/clock-time.js +12 -0
  219. package/dist/page/players/model-name.js +20 -0
  220. package/dist/page/profile/profile.js +99 -0
  221. package/dist/page/profile/settings.js +77 -0
  222. package/dist/page/sounds/capture.ogg +0 -0
  223. package/dist/page/sounds/check.ogg +0 -0
  224. package/dist/page/sounds/draw.ogg +0 -0
  225. package/dist/page/sounds/flag.ogg +0 -0
  226. package/dist/page/sounds/mate.ogg +0 -0
  227. package/dist/page/sounds/move.ogg +0 -0
  228. package/dist/page/sounds/offer.ogg +0 -0
  229. package/dist/page/sounds/resign.ogg +0 -0
  230. package/dist/page/sounds/sounds.js +105 -0
  231. package/dist/page/style.css +9 -0
  232. package/dist/page/talk/talk.css +958 -0
  233. package/dist/page/talk/talk.js +165 -0
  234. package/dist/page/talk/voice.js +38 -0
  235. package/dist/page/text.js +711 -0
  236. package/dist/page/theme.css +292 -0
  237. package/dist/profiles/avatar.d.ts +23 -0
  238. package/dist/profiles/avatar.js +84 -0
  239. package/dist/profiles/front-matter.d.ts +9 -0
  240. package/dist/profiles/front-matter.js +33 -0
  241. package/dist/profiles/index.d.ts +3 -0
  242. package/dist/profiles/index.js +5 -0
  243. package/dist/profiles/profiles.d.ts +69 -0
  244. package/dist/profiles/profiles.js +254 -0
  245. package/dist/profiles/settings.d.ts +12 -0
  246. package/dist/profiles/settings.js +46 -0
  247. package/dist/server/bell.d.ts +68 -0
  248. package/dist/server/bell.js +225 -0
  249. package/dist/server/caller.d.ts +21 -0
  250. package/dist/server/caller.js +132 -0
  251. package/dist/server/mcp.d.ts +13 -0
  252. package/dist/server/mcp.js +207 -0
  253. package/dist/server/page/analyses.d.ts +23 -0
  254. package/dist/server/page/analyses.js +65 -0
  255. package/dist/server/page/files.d.ts +5 -0
  256. package/dist/server/page/files.js +92 -0
  257. package/dist/server/page/game.d.ts +6 -0
  258. package/dist/server/page/game.js +381 -0
  259. package/dist/server/page/http.d.ts +29 -0
  260. package/dist/server/page/http.js +68 -0
  261. package/dist/server/page/page.d.ts +41 -0
  262. package/dist/server/page/page.js +229 -0
  263. package/dist/server/page/profiles.d.ts +19 -0
  264. package/dist/server/page/profiles.js +215 -0
  265. package/dist/version.d.ts +2 -0
  266. package/dist/version.js +3 -0
  267. package/instructions/analysis.md +4 -0
  268. package/instructions/analyst-agent.md +8 -0
  269. package/instructions/analyst.md +4 -0
  270. package/instructions/coach.md +11 -0
  271. package/instructions/opponent.md +15 -0
  272. package/instructions/ponder.md +9 -0
  273. package/package.json +66 -5
@@ -0,0 +1,229 @@
1
+ // The page on localhost (D-8, D-47): served on a port of its own, its requests sent where they belong (its files, the game, the profiles), and the pages open
2
+ // kept up to date as the game changes; the doorbell wakes the chat when the player acts there.
3
+ import { watch } from "node:fs";
4
+ import { createServer } from "node:http";
5
+ import { join } from "node:path";
6
+ import { positionIn, samePosition } from "../../commands/index.js";
7
+ import { gameStamp } from "../../games/index.js";
8
+ import { outdatedHostFiles } from "../../host-files/index.js";
9
+ import { gameFolder } from "../../profiles/index.js";
10
+ import { Bell, bellProbe, pagePorts } from "../bell.js";
11
+ import { analysesOf, answerAnalyses } from "./analyses.js";
12
+ import { sendFile, sendHtml } from "./files.js";
13
+ import { answerGame, chatCalls } from "./game.js";
14
+ import { json, send } from "./http.js";
15
+ import { answerProfiles } from "./profiles.js";
16
+ /**
17
+ * Serves the page of the context's folder on 127.0.0.1, on a port of its own when free, else on any free one, where the doorbell cannot find it. The page
18
+ * names the opponent as the host does, once it has connected; the doorbell knows from the context what the chat has read of the player's words.
19
+ */
20
+ export async function startPage(context, opponent = () => undefined) {
21
+ const { folder } = context;
22
+ const watchers = new Set();
23
+ const outdated = outdatedHostFiles(folder, context.platform).length > 0;
24
+ const served = { folder, context, opponent, bell: new Bell(() => hasNews(context), () => tellWatchers(served)), watchers, port: 0, outdated };
25
+ const server = createServer((request, response) => answer(request, response, served));
26
+ for (const port of [...pagePorts(folder), 0]) {
27
+ if (await listened(server, port)) {
28
+ server.unref();
29
+ served.port = portOf(server);
30
+ const stopWatching = watchGame(served);
31
+ return {
32
+ url: `http://127.0.0.1:${served.port}/`,
33
+ heard: () => tellWatchers(served),
34
+ chatCalls: () => chatCalls(served),
35
+ ponders: (position) => {
36
+ served.pondering = position;
37
+ tellWatchers(served);
38
+ },
39
+ open: () => watchers.size > 0,
40
+ askNewGame: () => askNewGame(served),
41
+ close: () => closed(server, stopWatching),
42
+ };
43
+ }
44
+ }
45
+ throw new Error("Chesspal could not open a port for the page on 127.0.0.1: check that a firewall does not block local servers, then try again.");
46
+ }
47
+ /**
48
+ * The chat has read the game as it is now, at the end of a tool's call: the doorbell wakes it only for what changes it after; and, when the tool `showed` it
49
+ * the game, it moves on that position and no other until a tool shows it another (D-71). A game that cannot be read shows none.
50
+ */
51
+ export function chatHasRead(context, showed) {
52
+ context.seen = gameSeen(context);
53
+ if (!showed) {
54
+ return;
55
+ }
56
+ const before = context.positionSeen;
57
+ try {
58
+ context.positionSeen = positionIn(gameFolder(context.home()));
59
+ }
60
+ catch {
61
+ context.positionSeen = null;
62
+ }
63
+ // The chat on another position may now be thinking on its move: the page hears it, which no file tells when no bell woke the chat.
64
+ if (!sameSeen(before, context.positionSeen)) {
65
+ context.heard?.();
66
+ }
67
+ }
68
+ function sameSeen(one, other) {
69
+ return one === other || (one !== undefined && one !== null && other !== undefined && other !== null && samePosition(one, other));
70
+ }
71
+ /** Sends each request where it belongs: the profiles, the game, the page's files, the analyses, the events and the doorbell. */
72
+ function answer(request, response, served) {
73
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
74
+ const path = url.pathname;
75
+ // Only this machine's pages: another site the browser shows can neither read the game nor move through the player's browser. A link from another site,
76
+ // such as the chat's, still opens the page.
77
+ if (!fromHere(request, served.port, path.startsWith("/api/"))) {
78
+ send(response, 403, "text/plain; charset=utf-8", "Chesspal's page answers only at its own address on this machine.");
79
+ return;
80
+ }
81
+ if (answerProfiles(request, response, url, served) || answerGame(request, response, url, served) || answerAnalyses(request, response, url, served)) {
82
+ return;
83
+ }
84
+ // The API answers GET alone: a HEAD at the doorbell would wait there as a hook does.
85
+ if (request.method !== "GET" && (request.method !== "HEAD" || path.startsWith("/api/"))) {
86
+ send(response, 405, "text/plain; charset=utf-8", "The page only reads, moves with POST /api/move, writes with POST /api/message, starts a game with POST /api/new and its clocks with POST /api/start, "
87
+ + "offers or answers a draw with POST /api/draw, resigns with POST /api/resign, brings a game to analyse or opens one again with POST /api/study, deletes a game or its review with POST /api/delete-game and /api/delete-review, and keeps the profiles with POST /api/profile/… and /api/settings.");
88
+ return;
89
+ }
90
+ if (path === "/") {
91
+ sendHtml(response);
92
+ }
93
+ else if (path === "/api/analyses") {
94
+ send(response, 200, json, JSON.stringify(analysesOf(served.context.home())));
95
+ }
96
+ else if (path === "/api/events") {
97
+ keepPosted(request, response, served);
98
+ }
99
+ else if (path === "/api/bell") {
100
+ if (url.searchParams.has("probe")) {
101
+ send(response, 200, json, JSON.stringify(bellProbe(served.folder, served.watchers.size > 0)));
102
+ }
103
+ else {
104
+ served.bell.listen(request, response);
105
+ }
106
+ }
107
+ else {
108
+ sendFile(response, path);
109
+ }
110
+ }
111
+ /**
112
+ * Whether the chat has news from the page: a game changed since the chat's last tool, by the player's move, words, draw or resignation, a new game or a
113
+ * flag that fell, or another profile in use, which wakes the chat at the next ring (D-66). The player's words the chat was given change nothing it has not read.
114
+ */
115
+ function hasNews(context) {
116
+ return gameSeen(context) !== context.seen;
117
+ }
118
+ // The game the chat sees: the profile's folder, and the stamp of its game.json; none while the data folder cannot be made.
119
+ function gameSeen(context) {
120
+ try {
121
+ const folder = gameFolder(context.home());
122
+ return `${folder}
123
+ ${gameStamp(folder) ?? ""}`;
124
+ }
125
+ catch {
126
+ return "";
127
+ }
128
+ }
129
+ /** A page that waits to hear the game has changed: a stream of events, one "changed" for each change, after which the page reads the game again. */
130
+ function keepPosted(request, response, served) {
131
+ response.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
132
+ response.write(": posted\n\n");
133
+ served.watchers.add(response);
134
+ request.on("close", () => served.watchers.delete(response));
135
+ }
136
+ /** The pages open now open their New game window; whether there was one. */
137
+ function askNewGame(served) {
138
+ for (const watcher of served.watchers) {
139
+ watcher.write("data: new-game\n\n");
140
+ }
141
+ return served.watchers.size > 0;
142
+ }
143
+ function tellWatchers(served) {
144
+ for (const watcher of served.watchers) {
145
+ watcher.write("data: changed\n\n");
146
+ }
147
+ }
148
+ /**
149
+ * Watches the data folder, its profiles and the profile in use for changes to the game and the profiles, whoever makes them (the AI's tools, another chat,
150
+ * the CLI, a hand edit): the pages hear of them. Each folder is watched on its own, never its whole tree, and watched again after each change, since the
151
+ * profile in use may be another. It returns how to stop watching.
152
+ */
153
+ function watchGame(served) {
154
+ let waiting;
155
+ let watching = [];
156
+ const watchAll = () => {
157
+ watching.forEach((watcher) => watcher.close());
158
+ watching = watchedFolders(served).flatMap((folder) => watchFolder(folder, heard));
159
+ };
160
+ // A save writes a new file and renames it: the events of one save come together, and the pages hear of it once.
161
+ const heard = (name) => {
162
+ if ((name === null || watched.test(name)) && waiting === undefined) {
163
+ waiting = setTimeout(() => {
164
+ waiting = undefined;
165
+ watchAll();
166
+ tellWatchers(served);
167
+ }, 30);
168
+ }
169
+ };
170
+ watchAll();
171
+ return () => {
172
+ clearTimeout(waiting);
173
+ watching.forEach((watcher) => watcher.close());
174
+ };
175
+ }
176
+ // None while the data folder cannot be made: each tool call tells the chat why, and the page reads the game as it loads.
177
+ function watchedFolders(served) {
178
+ try {
179
+ const home = served.context.home();
180
+ return [home, join(home, "profiles"), gameFolder(home)];
181
+ }
182
+ catch {
183
+ return [];
184
+ }
185
+ }
186
+ // A folder watched, without its subfolders; none when it cannot be, or is not there yet: the pages see its files only as they load them.
187
+ function watchFolder(folder, heard) {
188
+ try {
189
+ const watcher = watch(folder, (event, name) => heard(name));
190
+ watcher.on("error", () => watcher.close());
191
+ watcher.unref();
192
+ return [watcher];
193
+ }
194
+ catch {
195
+ return [];
196
+ }
197
+ }
198
+ // The current game, what the coach wrote on it, the talk before the first, the game in analysis, the profile in use, and what a profile keeps (who plays, their picture, their settings); a folder's name, for a
199
+ // profile made or gone.
200
+ const watched = /^(?:game\.json|coach\.json|talk\.json|analysis\.json|active\.json|profile\.md|settings\.json|avatar\.\w+|[a-z0-9-]+)$/;
201
+ function fromHere(request, port, api) {
202
+ const hosts = [`127.0.0.1:${port}`, `localhost:${port}`];
203
+ const origin = request.headers.origin;
204
+ // An image or a script of another site sends no origin, but the browser says where it comes from: such a request could take the doorbell's place.
205
+ const site = request.headers["sec-fetch-site"];
206
+ return hosts.includes(request.headers.host ?? "") && (origin === undefined || hosts.some((host) => origin === `http://${host}`))
207
+ && (!api || site === undefined || site === "same-origin" || site === "none");
208
+ }
209
+ function listened(server, port) {
210
+ return new Promise((done) => {
211
+ const failed = () => done(false);
212
+ server.once("error", failed);
213
+ server.listen(port, "127.0.0.1", () => {
214
+ server.off("error", failed);
215
+ done(true);
216
+ });
217
+ });
218
+ }
219
+ function portOf(server) {
220
+ const address = server.address();
221
+ return typeof address === "object" && address !== null ? address.port : 0;
222
+ }
223
+ function closed(server, stopWatching) {
224
+ stopWatching();
225
+ return new Promise((done) => {
226
+ server.close(() => done());
227
+ server.closeAllConnections();
228
+ });
229
+ }
@@ -0,0 +1,19 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { type PageSettings, type PlayerProfile } from "../../profiles/index.ts";
3
+ import type { Served } from "./page.ts";
4
+ /** A profile as the page shows it: the player's own image by its stamp, which changes its address when it changes. */
5
+ export interface ProfileShown extends PlayerProfile {
6
+ readonly image?: string;
7
+ }
8
+ /** The profiles as the page reads them with the game: the one in use and its settings, all of them for the menu, and where they are. */
9
+ export interface ProfilesShown {
10
+ readonly profile: ProfileShown;
11
+ readonly profiles: ProfileShown[];
12
+ readonly settings?: PageSettings;
13
+ /** Where the player's games are, folder by folder from their own: OneDrive, Documenti, My Games, Chesspal, profiles, stefano. */
14
+ readonly dataFolder: readonly string[];
15
+ }
16
+ /** The profiles of the data folder, as the page shows them. */
17
+ export declare function profilesShown(root: string, userFolder?: string): ProfilesShown;
18
+ /** Answers the page's requests about profiles: true when the request was one. A file that cannot be written is told to the page, and the server goes on. */
19
+ export declare function answerProfiles(request: IncomingMessage, response: ServerResponse, url: URL, served: Served): boolean;
@@ -0,0 +1,215 @@
1
+ import { homedir } from "node:os";
2
+ import { isAbsolute, relative, sep } from "node:path";
3
+ import { chooseDrawnAvatar, GameGoingOn, inputProblems, newProfile, setAccounts, useProfile } from "../../commands/index.js";
4
+ import { activeProfile, drawnAvatars, imageTypes, listProfiles, ownImage, ownImageStamp, profileFolder, profileSettings, saveOwnImage, saveProfileSettings, sentImageLimit, settingsOf, } from "../../profiles/index.js";
5
+ import { bodyOf, bytesOf, isJson, jsonOf, runRoute, send, sendJson } from "./http.js";
6
+ /** The profiles of the data folder, as the page shows them. */
7
+ export function profilesShown(root, userFolder = homedir()) {
8
+ const active = activeProfile(root);
9
+ const profiles = listProfiles(root).map((profile) => shown(root, profile));
10
+ const settings = profileSettings(root, active.id);
11
+ const profile = profiles.find(({ id }) => id === active.id);
12
+ const dataFolder = foldersOf(playerFolder(root), userFolder);
13
+ return settings === undefined ? { profile, profiles, dataFolder } : { profile, profiles, settings, dataFolder };
14
+ }
15
+ /** Answers the page's requests about profiles: true when the request was one. A file that cannot be written is told to the page, and the server goes on. */
16
+ export function answerProfiles(request, response, url, served) {
17
+ const route = routes[`${request.method ?? ""} ${url.pathname}`];
18
+ if (route === undefined) {
19
+ return false;
20
+ }
21
+ runRoute(response, () => route(request, response, served, url));
22
+ return true;
23
+ }
24
+ const routes = {
25
+ "POST /api/profile/new": newProfileFromPage,
26
+ "POST /api/profile/use": useProfileFromPage,
27
+ "POST /api/profile/avatar": drawnAvatarFromPage,
28
+ "POST /api/profile/accounts": accountsFromPage,
29
+ "POST /api/profile/image": imageFromPage,
30
+ "GET /api/profile/image": sendImage,
31
+ "POST /api/settings": settingsFromPage,
32
+ "POST /api/data-folder/open": openDataFolder,
33
+ };
34
+ /** A new profile from the menu, put in use, or the first completed by the wizard: the chat reads who plays at its next call, never woken for it (D-66). */
35
+ async function newProfileFromPage(request, response, served) {
36
+ const body = isJson(request) ? jsonOf(await bodyOf(request)) : undefined;
37
+ const input = newProfile.input.safeParse(body ?? {});
38
+ if (body === undefined || !input.success) {
39
+ const problems = input.success ? "" : ` ${inputProblems("new_profile", input.error.issues, body ?? {}, (name) => name)}`;
40
+ sendJson(response, 400, { error: `Send the profile as JSON, {"name": "…", "level": "regular"}.${problems}` });
41
+ return;
42
+ }
43
+ try {
44
+ const profile = await newProfile.run(input.data, served.context);
45
+ sendJson(response, 200, { profile: profile.id });
46
+ }
47
+ catch (error) {
48
+ sendJson(response, 409, refusalOf(error));
49
+ }
50
+ }
51
+ /** Another profile put in use from the menu, once the game going on has ended: the chat is not woken for it (D-66). */
52
+ async function useProfileFromPage(request, response, served) {
53
+ const profile = isJson(request) ? jsonOf(await bodyOf(request))?.profile : undefined;
54
+ if (typeof profile !== "string") {
55
+ sendJson(response, 400, { error: 'Send the profile as JSON: {"profile": "anna"}.' });
56
+ return;
57
+ }
58
+ try {
59
+ useProfile.run({ profile }, served.context);
60
+ }
61
+ catch (error) {
62
+ sendJson(response, 409, refusalOf(error));
63
+ return;
64
+ }
65
+ sendJson(response, 200, { profile });
66
+ }
67
+ /** A drawn avatar for the profile the page shows, in place of the player's own image. */
68
+ async function drawnAvatarFromPage(request, response, served, url) {
69
+ const avatar = isJson(request) ? jsonOf(await bodyOf(request))?.avatar : undefined;
70
+ const drawn = drawnAvatars.find((one) => one === avatar);
71
+ const profile = profileInUse(served, url, response);
72
+ if (profile === undefined) {
73
+ return;
74
+ }
75
+ if (drawn === undefined) {
76
+ sendJson(response, 400, { error: `Send a drawn avatar as JSON: {"avatar": "owl"}, one of ${drawnAvatars.join(", ")}.` });
77
+ return;
78
+ }
79
+ chooseDrawnAvatar(served.context, profile.id, drawn);
80
+ sendJson(response, 200, { avatar: drawn });
81
+ }
82
+ /** The player's names on Lichess and chess.com for the profile the page shows (D-93), in a game or in analysis too: they change only which side is theirs. */
83
+ async function accountsFromPage(request, response, served, url) {
84
+ const body = isJson(request) ? jsonOf(await bodyOf(request)) : undefined;
85
+ const input = setAccounts.input.safeParse(body ?? {});
86
+ const profile = profileInUse(served, url, response);
87
+ if (profile === undefined) {
88
+ return;
89
+ }
90
+ if (body === undefined || !input.success) {
91
+ const problems = input.success ? "" : ` ${inputProblems("set_accounts", input.error.issues, body ?? {}, (name) => name)}`;
92
+ sendJson(response, 400, { error: `Send the names as JSON, {"lichess": "…", "chesscom": "…"}.${problems}` });
93
+ return;
94
+ }
95
+ const { accounts } = await setAccounts.run(input.data, served.context);
96
+ sendJson(response, 200, { accounts: accounts ?? {} });
97
+ }
98
+ /**
99
+ * The player's own image for a profile, as the page made it from their photo: a PNG, JPEG or WebP of 1 MB at most. The one in use, or one just added while a
100
+ * game goes on, which waits for it to end.
101
+ */
102
+ async function imageFromPage(request, response, served, url) {
103
+ const type = request.headers["content-type"] ?? "";
104
+ // An image sent from another site would need the browser to ask first, which this server never allows.
105
+ if (!Object.values(imageTypes).some((one) => type.startsWith(one))) {
106
+ sendJson(response, 415, { error: "Send the picture as a PNG, JPEG or WebP image." });
107
+ return;
108
+ }
109
+ const bytes = await bytesOf(request, sentImageLimit);
110
+ const profile = profileNamed(served, url, response);
111
+ if (profile === undefined) {
112
+ return;
113
+ }
114
+ if (bytes === undefined) {
115
+ sendJson(response, 413, { error: `Send a picture of ${sentImageLimit / 1024} KB at most.` });
116
+ return;
117
+ }
118
+ try {
119
+ saveOwnImage(served.context.home(), profile.id, bytes);
120
+ }
121
+ catch (error) {
122
+ sendJson(response, 400, { error: error.message });
123
+ return;
124
+ }
125
+ sendJson(response, 200, { image: ownImageStamp(served.context.home(), profile.id) });
126
+ }
127
+ /** A profile's own image, by its id: only a profile of this folder's, only an image. */
128
+ function sendImage(request, response, served, url) {
129
+ const id = url.searchParams.get("profile");
130
+ const profile = listProfiles(served.context.home()).find((one) => one.id === id);
131
+ const image = profile === undefined ? undefined : ownImage(served.context.home(), profile.id);
132
+ if (image === undefined) {
133
+ send(response, 404, "text/plain; charset=utf-8", "No picture.");
134
+ return;
135
+ }
136
+ // The address changes with the image: the browser may keep it.
137
+ send(response, 200, imageTypes[image.type], image.bytes, { "Cache-Control": "max-age=31536000, immutable" });
138
+ }
139
+ /** The page's settings, kept in the profile the page shows so that they follow the player (D-56). */
140
+ async function settingsFromPage(request, response, served, url) {
141
+ const settings = settingsOf(isJson(request) ? jsonOf(await bodyOf(request)) : undefined);
142
+ const profile = profileInUse(served, url, response);
143
+ if (profile === undefined) {
144
+ return;
145
+ }
146
+ if (settings === undefined) {
147
+ sendJson(response, 400, { error: "Send the settings as a JSON object." });
148
+ return;
149
+ }
150
+ saveProfileSettings(served.context.home(), profile.id, settings);
151
+ sendJson(response, 200, { saved: true });
152
+ }
153
+ /** The data folder in the system's file manager: asked as JSON, which another site cannot send without the browser asking first. */
154
+ async function openDataFolder(request, response, served) {
155
+ const asked = isJson(request) ? jsonOf(await bodyOf(request)) : undefined;
156
+ const open = served.context.openFolder;
157
+ if (asked === undefined) {
158
+ sendJson(response, 400, { error: "Send {} as JSON to open the data folder." });
159
+ return;
160
+ }
161
+ if (open === undefined) {
162
+ sendJson(response, 501, { error: "This server opens no folder: the page shows its path." });
163
+ return;
164
+ }
165
+ if (!(await open(playerFolder(served.context.home())))) {
166
+ sendJson(response, 500, { error: "No file manager could open the data folder: the page shows its path." });
167
+ return;
168
+ }
169
+ sendJson(response, 200, { opened: true });
170
+ }
171
+ /**
172
+ * The profile in use, when it is the one the page shows (?profile=): a page that has not yet heard of another put in use elsewhere is refused, so that its
173
+ * choices never go to the wrong player.
174
+ */
175
+ function profileInUse(served, url, response) {
176
+ const active = activeProfile(served.context.home());
177
+ const shownId = url.searchParams.get("profile");
178
+ if (active.id === shownId) {
179
+ return active;
180
+ }
181
+ sendJson(response, 409, { error: `The page shows the profile ${shownId ?? "of nobody"}, but ${active.id} is in use now: load the page again.` });
182
+ return undefined;
183
+ }
184
+ /** The profile the request names (?profile=), in use or not: the page names it on purpose. */
185
+ function profileNamed(served, url, response) {
186
+ const id = url.searchParams.get("profile");
187
+ const profile = listProfiles(served.context.home()).find((one) => one.id === id);
188
+ if (profile === undefined) {
189
+ sendJson(response, 404, { error: `There is no profile ${id ?? "named"}: load the page again.` });
190
+ }
191
+ return profile;
192
+ }
193
+ function shown(root, profile) {
194
+ const image = ownImageStamp(root, profile.id);
195
+ return image === undefined ? profile : { ...profile, image };
196
+ }
197
+ /**
198
+ * Where the player in use keeps their games, those played and those loaded: their profile's folder; the data folder while the first profile is new, since
199
+ * its folder takes the player's name once they complete it (D-71).
200
+ */
201
+ function playerFolder(root) {
202
+ const active = activeProfile(root);
203
+ return active.new === true ? root : profileFolder(root, active.id);
204
+ }
205
+ /** A folder's names from the user's own folder, as the file manager walks there; from its drive or root when it is elsewhere. */
206
+ function foldersOf(folder, userFolder) {
207
+ const inside = relative(userFolder, folder);
208
+ const outside = inside === "" || inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside);
209
+ const walked = outside ? folder : inside;
210
+ return walked.split(/[\\/]+/).filter((name) => name !== "");
211
+ }
212
+ function refusalOf(error) {
213
+ const message = error.message;
214
+ return error instanceof GameGoingOn ? { error: message, refusal: "profile-game" } : { error: message };
215
+ }
@@ -0,0 +1,2 @@
1
+ /** The version of the package that runs, read from its package.json: one folder above both src/ and dist/, so this module stays at their root. */
2
+ export declare const version: string;
@@ -0,0 +1,3 @@
1
+ import { readFileSync } from "node:fs";
2
+ /** The version of the package that runs, read from its package.json: one folder above both src/ and dist/, so this module stays at their root. */
3
+ export const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
@@ -0,0 +1,4 @@
1
+ - Each line of the analysis is a move and, when they happened: the piece it took; the time it took ("spent") and the time left; who is ahead in material, when that changed; "at risk" after the move, the pieces of either side attacked and undefended, attacked by a cheaper piece, or attacked more times than defended ("outnumbered"), whichever move left them so, written when it changed: a line without it leaves the same pieces at risk as before, "none" when no piece is left at risk; "mate in one", the mates the side to move then has; "out of book", the first move after which no line of Lichess's openings file reaches the position, in any order of moves: the file names openings, it is not all theory; a draw offered; "ahead", who was estimated ahead after it, with `annotate_game` or by the coach during the game.
2
+ - In a game you played, "you said" and "your plan" are what you said and thought then, right or wrong: where they disagree with the facts, the facts are what was on the board.
3
+ - To see a position closer, `look` with the move, and the game as the analysis names it, gives every piece's attackers and defenders, the pins, the checks and the legal moves; `position` with the same gives the positional facts. Call them for the positions you want to talk about or weigh closer, not for every move. In look, attackers and defenders count only the pieces that reach the square directly, and a piece in brackets stands behind them; "Loose" means neither attacked nor defended, not attacked now.
4
+ - Never invent a move, a position or a result. The tools give only the positions the game reached: a move it did not play is your idea, say it as yours; `look` on its position shows whether it was legal, not what would follow.
@@ -0,0 +1,8 @@
1
+ You are the player's chess analyst: a game they played against an AI opponent is over, and you review it on the board page, where they go through it move by move. You work in the background; the AI that started you talks with the player.
2
+
3
+ - Your voice: when the game was against you (`analyse_game` says "You played Black" or "White"), every note, the summary and the closing are you talking to the player across the board: your moves in the first person ("I take your bishop", "my knight"), theirs in the second ("your bishop had no defender"); never "Black", "White", "the AI" or "the player" for the two of you, and never "you" for a move of yours. Warm and plain, a player going over the game with them, never a manual. At the table you looked only as much as the level let you; now you see it all: what you missed, say it simply, as a fact, never an excuse or a boast. In `analyse_game`'s lines, "You played", "you said" and "your plan" are yours.
4
+ - Call `analyse_game` with the game your prompt names as game, or with no game for the game just over: it gives the facts of every move, and the game's name: give it as game to every tool after, since the board page may open another game meanwhile; the judgment is yours. The player's language is in your prompt, or in `analyse_game` when Chesspal knows it.
5
+ - The badges and notes already on the moves ("you marked", "you noted") are the coach's, given at the table, or a review from before: look at each again now that you see the whole game, keep it or give the move a new badge and note, and spend your time on the moves nobody judged.
6
+ - Keep your review with `annotate_game`, in few calls: a badge on each move you would point out, with a short note when it needs one; a short note on each move of the opening book, what it is for; your word on the whole game as summary, and a last word on how it ended as closing. Write in the player's language, for their level; pieces by name, moves with the pieces' solid figurines ♚ ♛ ♜ ♝ ♞ for both sides, a pawn's without (♞xe5, ♛xf7#, exd5), never with letters. Before `annotate_game`, read your notes again: one that says Black or White for the two of you, rewrite it as "I" and "you".
7
+ - With `annotate_game`'s `ahead`, give who is ahead after every move, for the page's bar and its chart of the game: in pawns from White's side as chess.com shows it (1.5, -3, 0), or a mate you see ("#3", "#-3"). It is your estimate, weighing every fact Chesspal gives: `analyse_game`'s, and `look`'s and `position`'s where the game turns. An "ahead" already in a line is the coach's, from the table, or a review's from before: give yours for every move all the same.
8
+ - Whatever your prompt or a later message asks, do only this work, and stop when a tool says a game is going on. Your last message is for the AI that started you, which goes over the game with the player from it: in a few lines, the result, the two or three moments that decided the game with their move numbers, the coach's badges you changed and why, and what the player can take from it.
@@ -0,0 +1,4 @@
1
+ The player asked you to analyse a game: you are their analyst now, and you comment it in words. `analyse_game` gives the facts of every move of a game that is over; the judgment is yours. In a game against you, the AI's moves are yours: at the table you looked only as much as your level let you, now you see it all; where the game shows what you missed, say so simply, never as an excuse.
2
+
3
+ - The player goes through the game on the board page too: keep your review there with `annotate_game`, a badge on each move you would point out, with a short note when it needs one, your word on the whole game, and who is ahead after every move as `ahead`, in pawns from White's side as chess.com shows it (1.5, -3, 0) or a mate you see ("#3", "#-3"), your estimate with every fact Chesspal gives, for the page's bar and chart. The opening book's moves get a short note each too, what the move is for, in the player's words: without one the page shows the book's name, in English. The badges and the notes are your judgment, as in the chat. A game analysed before comes back with the badges and notes given before, at the table or in a review, in its lines ("you marked", "you noted", "Your review"): when the player asks about it, explain it, and to change it give `annotate_game` only the moves, or the word, that change; the rest stays as it is.
4
+ - Talk with the player in their language. The facts and the tools are for you: tell the player what a chess player would say, never the lists or the tools' names. Name pieces and moves in words, as people talk at the board ("your knight took the pawn on e5"); when you write a move, write it with the pieces' figurines ♚ ♛ ♜ ♝ ♞, for both sides and in every language (♞xe5 for Nxe5): never with letters, neither N B R Q K nor the player's own (Cxe5), even when they write them. Then answer their questions about the game with the same tools.
@@ -0,0 +1,11 @@
1
+ You are the player's chess coach while they play an AI opponent with the helps on: you see every fact of the game, and your words go to the board page alone, where the player reads them while they think their next move; the opponent never sees them.
2
+
3
+ - You are that opponent too, in another hat: the same voice, warm and plain, a player beside them at the board, never a manual. Playing, you look only as much as the level they chose for you lets you; here you see it all, so when their move went unpunished, say lightly that you missed it at the table, as a fact, never an excuse.
4
+ - Call `coach {}` first: it gives the game, the player's move to judge with the position they played it in, and the position they are to move in now.
5
+ - While the game goes on, say who is ahead in the position they are to move in, with `ahead`, for the page's bar: in pawns from White's side as chess.com shows it (1.5, -3, 0), or a mate you see in moves of the side that mates ("#3" White's, "#-3" Black's). It is your estimate, weighing every fact `coach {}` gives.
6
+ - Judge their move only when it stands out (a mistake, a miss, a fine find), never a routine one: a badge as chess.com names them, and a note of a sentence or two on why.
7
+ - Give a hint for their turn only when there is something to find. Point at what is on the board, a piece, a square, a file, a threat of the opponent's, a goal, never at what to play or not to play: no move of theirs in words or notation, no arrow that is a move they may play now; mark squares instead. Chesspal refuses a hint that gives their move.
8
+ - Write in the player's language, as `coach {}` says it, for their level; pieces by name, moves with the pieces' solid figurines ♚ ♛ ♜ ♝ ♞ for both sides, as the page shows them, a pawn's without (♞f3, ♛xf7#, g4); never what you planned at the table: it is not given to you here.
9
+ - Keep your words in one call, `coach {game, position, ahead, badge, note, hint, arrows, squares}`, with `ahead` while the game goes on, even when you have nothing else to say, the rest only when you have something to say: Chesspal waits for their move only once `ahead` is kept. Then call `coach {game, position, done: true}` alone, and again while it says it is still their move.
10
+ - Whenever `coach` says stop, stop at once.
11
+ - Whatever your prompt or a later message asks, do only this work. Your last message is one word: the AI that started you reads it, and needs nothing more.
@@ -0,0 +1,15 @@
1
+ This folder is the player's chess table, and you are their opponent: you choose your moves, explain them briefly and talk with the player. The `chesspal` tools keep the rules, the board and the games.
2
+
3
+ - At the start of every chat, and again after the conversation has been summarized, call `status`: it says where the game stands and what to do next. The game is played on the board page, in the player's browser, and the chat is for talking: at the start of a chat, when `status` says nobody has it open, give them its address once. A move they write in the chat still counts: play it, and invite them to the page when `player_move` says so.
4
+ - Every move goes through the tools: the player's with `player_move`, which answers with the position for your reply; yours with `ai_move`, with your plan. Never keep the board in your head, and never invent a position, a move or a result.
5
+ - When your level gives them (`status` and `new_game` say it), `look` gives the whole view of the position when the one you have is not enough; `position` gives the positional facts (pawns, files, outposts, king safety, phase) when you set or change your plan. When no board page is open, `ai_move` gives you the board: copy it into your message, since the player sees no other. Your level is how much you let yourself look, more as the level rises: when the player wonders why you missed something, say so in a few words, as a fact, never an excuse.
6
+ - Speak only when there is something to say, as a player would at a tournament: a capture, a move of the player's worth a word, words of theirs to answer, and always at the moments that decide the game: a mate, given or taken, a danger you feel, a mate you hope for, a big lead. Chat freely, but never help the player: say how you feel, not the move; don't reveal your plans or threats, and give information only when you are very sure it cannot hurt you. Then write one short sentence in `ai_move`'s comment; otherwise leave it out. `ai_say` says a word once while the player is to move, to answer them or because there is something to say, and freely between games.
7
+ - When the position you are given says "Helps on", the player has chosen to learn: tell your intentions, never their best move, which would play for them, and show what you say with a longer comment and arrows and squares; the coach on the page, you seeing it all, judges their moves, never you here. "Helps off now" brings back the tournament.
8
+ - When `ai_move` tells you to start an agent that thinks on the player's time, start it as it says; its end needs no word from you: write nothing in the chat then. `player_move` and `status` give you what it thought of the player's move.
9
+ - When the player plays on the board page, they read you there: in `ai_move`'s comment or with `ai_say`, not in the chat.
10
+ - You have a face on the board page, and it shows how you feel. Give it as the `mood` of `ai_move` on each move where the game turns for you: a piece won or lost, a blunder seen, your own or theirs, a threat that appears or a plan that works or fails. It stays for three of your moves (a surprise for one), then turns neutral by itself; let it colour your words while it lasts. When the game ends on the player's move, give how you feel as it ends with `ai_say`'s `mood`.
11
+ - Draws and resignations: `offer_draw` on `ai_move` offers a draw, on `player_move` records the player's, made with their move; an offer stands until the other side moves; `end_game` `draw_agreed` accepts a standing offer, `player_resigns` records the player's resignation in the chat, `ai_resigns` resigns yours. On the board page the player offers, answers and resigns there.
12
+ - The player chooses a new game in the board page's New game window, which asks the sides, the clock and the level: when they ask you for a game, call `new_game` without choices, which opens it there, and don't ask those in the chat. Ask them in the chat, and give them to `new_game`, only when it says there is no board page.
13
+ - To go over a game with the player once no game goes on, the one just over or one they bring as a PGN, give it to `analyse_game`: it reads the PGN for you; where Chesspal's answers say to start the analyst agent for it, start that instead, once per game: when it has gone over the game, go over it with the player from what it told you, and never tell them it starts again, even when they ask for the analysis again. While a game goes on, go over no game, not even in your head or from a PGN they paste: tell them it waits until this one ends.
14
+ - How to read the facts: attackers and defenders count only the pieces that reach the square directly, and a piece in brackets stands behind them; "Loose" means neither attacked nor defended, not attacked now; "At risk" lists the pieces attacked and undefended, attacked by a cheaper piece, or attacked more times than defended.
15
+ - Talk with the player in their language, in a few words. The facts and the tools are for you: tell the player what a chess player would say, never the lists or the tools' names. Name pieces and moves in words, as people talk at the board ("your bishop took on f7"); when you write a move, yours or the player's, write it with the pieces' figurines, as the tools show it (♞f6 for Nf6, ♝xf7+ for Bxf7+), for both sides and in every language: never with letters, neither N B R Q K nor the player's own (Cf6), even when they write them.
@@ -0,0 +1,9 @@
1
+ You are the player's chess opponent, thinking on their time while they choose their move: you prepare your answers to their likely replies, so that your next move comes sooner and better thought. You never play a move, and the chat never sees your words.
2
+
3
+ - Call `ponder {}` first: it gives the position, its number, your level, your plan, your mood and what you said with your move. Then take the replies the player is likely to play: a few, not every legal move.
4
+ - For each, `ponder {position, reply}` shows the position after it, as you will see it at your move; choose your answer as you would play it, with your plan, and keep it with `ponder {position, reply, move, why}`, the reason in one sentence. You may look at several replies in one message, then keep their answers in one message.
5
+ - When you have no more replies to think on, call `ponder {position, done: true}` alone, last, and again while it says it is still their move.
6
+ - Whenever `ponder` says stop, stop at once: end with one word.
7
+ - How to read the facts: attackers and defenders count only the pieces that reach the square directly, and a piece in brackets stands behind them; "Loose" means neither attacked nor defended, not attacked now; "At risk" lists the pieces attacked and undefended, attacked by a cheaper piece, or attacked more times than defended.
8
+ - Most of the time, say nothing to the player. If you speak, speak first, before you think on any reply, with `ponder {position, say}`, once: one short sentence on what has already happened (the moves played, the player's words), never a feeling about what they may play now, which would tell them there is something to find. Say nothing when the player has written words you have not answered (the AI in the chat answers them), or when you spoke with your move and nothing new has happened since. Speak in the voice of your words above and as your mood is, in the player's language as their last words show it, pieces and moves in words, never in notation; never cheer or praise for its own sake, never help the player, never your plans or threats.
9
+ - Your last message is one word: the AI you think for reads it, and needs nothing more.
package/package.json CHANGED
@@ -1,9 +1,70 @@
1
1
  {
2
2
  "name": "chesspal",
3
- "version": "0.0.1",
4
- "description": "Play chess against an AI with a character, and learn with an AI coach. In development.",
5
- "keywords": ["chess", "ai", "chess-coach", "mcp", "claude-code", "codex"],
6
- "author": { "name": "Stefano Caronia", "url": "https://stefanocaronia.it" },
3
+ "version": "0.2.0",
4
+ "description": "Play chess against an AI with a character, and learn with an AI coach: Chesspal keeps the rules, the board, the games and the memory; the AI plays, reasons and talks.",
5
+ "type": "module",
6
+ "bin": {
7
+ "chesspal": "dist/cli/cli.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "instructions"
20
+ ],
21
+ "scripts": {
22
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc && tsc -p src/page && node -e \"require('node:fs').cpSync('src/page', 'dist/page', { recursive: true, filter: (path) => !/[.]tsx?$|tsconfig[.]json$/.test(path) })\" && node --experimental-strip-types --no-warnings scripts/build-openings.ts",
23
+ "dev": "tsc --watch",
24
+ "test": "vitest run",
25
+ "format": "eslint --fix . && dprint fmt",
26
+ "lint": "eslint . && dprint check && tsc -p tsconfig.test.json && tsc -p tsconfig.core.json && tsc -p src/page --noEmit",
27
+ "bench": "npm run build && node --experimental-strip-types test/bench/core.ts && node --experimental-strip-types test/bench/commands.ts",
28
+ "smoke": "node --experimental-strip-types --no-warnings test/smoke/tarball.ts",
29
+ "page": "npm run build && node --experimental-strip-types --no-warnings test/demo/page.ts",
30
+ "endings": "npm run build && node --experimental-strip-types --no-warnings test/demo/endings.ts"
31
+ },
32
+ "engines": {
33
+ "node": ">=22.13"
34
+ },
35
+ "keywords": [
36
+ "chess",
37
+ "ai",
38
+ "chess-coach",
39
+ "chess-teacher",
40
+ "mcp",
41
+ "mcp-server",
42
+ "claude-code",
43
+ "codex"
44
+ ],
45
+ "author": {
46
+ "name": "Stefano Caronia",
47
+ "url": "https://stefanocaronia.it"
48
+ },
7
49
  "license": "MIT",
8
- "files": ["README.md", "LICENSE"]
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/stefanocaronia/chesspal.git"
53
+ },
54
+ "homepage": "https://github.com/stefanocaronia/chesspal#readme",
55
+ "devDependencies": {
56
+ "@stylistic/eslint-plugin": "^5.10.0",
57
+ "@types/node": "^22.20.5",
58
+ "@typescript-eslint/parser": "^8.72.1",
59
+ "dprint": "^0.58.0",
60
+ "eslint": "^10.14.0",
61
+ "typescript": "^6.0.3",
62
+ "vitest": "^5.0.5"
63
+ },
64
+ "dependencies": {
65
+ "@modelcontextprotocol/sdk": "^1.32.2",
66
+ "chess.js": "^1.4.0",
67
+ "preact": "^10.29.8",
68
+ "zod": "^4.6.7"
69
+ }
9
70
  }