chesspal 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/README.md +136 -2
  2. package/dist/cli/chat-hook.d.ts +2 -0
  3. package/dist/cli/chat-hook.js +33 -0
  4. package/dist/cli/cli.d.ts +2 -0
  5. package/dist/cli/cli.js +28 -0
  6. package/dist/cli/command-line.d.ts +8 -0
  7. package/dist/cli/command-line.js +178 -0
  8. package/dist/commands/ai-move.d.ts +37 -0
  9. package/dist/commands/ai-move.js +92 -0
  10. package/dist/commands/ai-say.d.ts +20 -0
  11. package/dist/commands/ai-say.js +40 -0
  12. package/dist/commands/analyse-game.d.ts +61 -0
  13. package/dist/commands/analyse-game.js +238 -0
  14. package/dist/commands/annotate-game.d.ts +35 -0
  15. package/dist/commands/annotate-game.js +78 -0
  16. package/dist/commands/clock.d.ts +15 -0
  17. package/dist/commands/clock.js +45 -0
  18. package/dist/commands/context.d.ts +56 -0
  19. package/dist/commands/context.js +36 -0
  20. package/dist/commands/draw-offer.d.ts +10 -0
  21. package/dist/commands/draw-offer.js +43 -0
  22. package/dist/commands/drawings.d.ts +15 -0
  23. package/dist/commands/drawings.js +23 -0
  24. package/dist/commands/end-game.d.ts +23 -0
  25. package/dist/commands/end-game.js +64 -0
  26. package/dist/commands/export-game.d.ts +14 -0
  27. package/dist/commands/export-game.js +26 -0
  28. package/dist/commands/game-summary.d.ts +95 -0
  29. package/dist/commands/game-summary.js +239 -0
  30. package/dist/commands/index.d.ts +3 -0
  31. package/dist/commands/index.js +19 -0
  32. package/dist/commands/init.d.ts +15 -0
  33. package/dist/commands/init.js +73 -0
  34. package/dist/commands/levels.d.ts +9 -0
  35. package/dist/commands/levels.js +25 -0
  36. package/dist/commands/look.d.ts +31 -0
  37. package/dist/commands/look.js +61 -0
  38. package/dist/commands/new-game.d.ts +69 -0
  39. package/dist/commands/new-game.js +215 -0
  40. package/dist/commands/opening-book.d.ts +13 -0
  41. package/dist/commands/opening-book.js +84 -0
  42. package/dist/commands/page-link.d.ts +11 -0
  43. package/dist/commands/page-link.js +28 -0
  44. package/dist/commands/past-games.d.ts +11 -0
  45. package/dist/commands/past-games.js +47 -0
  46. package/dist/commands/play-move.d.ts +44 -0
  47. package/dist/commands/play-move.js +102 -0
  48. package/dist/commands/player-move.d.ts +17 -0
  49. package/dist/commands/player-move.js +44 -0
  50. package/dist/commands/player-say.d.ts +5 -0
  51. package/dist/commands/player-say.js +16 -0
  52. package/dist/commands/ponder.d.ts +42 -0
  53. package/dist/commands/ponder.js +167 -0
  54. package/dist/commands/position.d.ts +14 -0
  55. package/dist/commands/position.js +128 -0
  56. package/dist/commands/profiles.d.ts +60 -0
  57. package/dist/commands/profiles.js +129 -0
  58. package/dist/commands/registry.d.ts +54 -0
  59. package/dist/commands/registry.js +61 -0
  60. package/dist/commands/say.d.ts +16 -0
  61. package/dist/commands/say.js +84 -0
  62. package/dist/commands/status.d.ts +29 -0
  63. package/dist/commands/status.js +103 -0
  64. package/dist/commands/study-games.d.ts +5 -0
  65. package/dist/commands/study-games.js +40 -0
  66. package/dist/commands/study.d.ts +38 -0
  67. package/dist/commands/study.js +122 -0
  68. package/dist/commands/view.d.ts +14 -0
  69. package/dist/commands/view.js +125 -0
  70. package/dist/core/board-text.d.ts +7 -0
  71. package/dist/core/board-text.js +18 -0
  72. package/dist/core/book.json +1 -0
  73. package/dist/core/clock.d.ts +45 -0
  74. package/dist/core/clock.js +109 -0
  75. package/dist/core/facts.d.ts +55 -0
  76. package/dist/core/facts.js +175 -0
  77. package/dist/core/game.d.ts +93 -0
  78. package/dist/core/game.js +348 -0
  79. package/dist/core/illegal.d.ts +19 -0
  80. package/dist/core/illegal.js +141 -0
  81. package/dist/core/index.d.ts +19 -0
  82. package/dist/core/index.js +11 -0
  83. package/dist/core/levels.d.ts +20 -0
  84. package/dist/core/levels.js +33 -0
  85. package/dist/core/material.d.ts +9 -0
  86. package/dist/core/material.js +12 -0
  87. package/dist/core/moods.d.ts +23 -0
  88. package/dist/core/moods.js +45 -0
  89. package/dist/core/openings.d.ts +37 -0
  90. package/dist/core/openings.js +98 -0
  91. package/dist/core/openings.json +1 -0
  92. package/dist/core/pgn.d.ts +10 -0
  93. package/dist/core/pgn.js +213 -0
  94. package/dist/core/profile.d.ts +117 -0
  95. package/dist/core/profile.js +232 -0
  96. package/dist/core/record.d.ts +21 -0
  97. package/dist/core/record.js +25 -0
  98. package/dist/core/review.d.ts +74 -0
  99. package/dist/core/review.js +126 -0
  100. package/dist/games/ai-model.d.ts +5 -0
  101. package/dist/games/ai-model.js +15 -0
  102. package/dist/games/analysis.d.ts +12 -0
  103. package/dist/games/analysis.js +26 -0
  104. package/dist/games/archive.d.ts +42 -0
  105. package/dist/games/archive.js +168 -0
  106. package/dist/games/chat-start.d.ts +16 -0
  107. package/dist/games/chat-start.js +26 -0
  108. package/dist/games/current-game.d.ts +54 -0
  109. package/dist/games/current-game.js +128 -0
  110. package/dist/games/files.d.ts +9 -0
  111. package/dist/games/files.js +52 -0
  112. package/dist/games/index.d.ts +10 -0
  113. package/dist/games/index.js +11 -0
  114. package/dist/games/plans.d.ts +9 -0
  115. package/dist/games/plans.js +23 -0
  116. package/dist/games/table-talk.d.ts +12 -0
  117. package/dist/games/table-talk.js +25 -0
  118. package/dist/games/talk.d.ts +40 -0
  119. package/dist/games/talk.js +131 -0
  120. package/dist/games/thoughts.d.ts +15 -0
  121. package/dist/games/thoughts.js +60 -0
  122. package/dist/home/data-folder.d.ts +12 -0
  123. package/dist/home/data-folder.js +55 -0
  124. package/dist/home/index.d.ts +2 -0
  125. package/dist/home/index.js +3 -0
  126. package/dist/home/open-folder.d.ts +2 -0
  127. package/dist/home/open-folder.js +14 -0
  128. package/dist/host-files/host-files.d.ts +10 -0
  129. package/dist/host-files/host-files.js +167 -0
  130. package/dist/host-files/index.d.ts +2 -0
  131. package/dist/host-files/index.js +3 -0
  132. package/dist/host-files/ponder-agents.d.ts +12 -0
  133. package/dist/host-files/ponder-agents.js +40 -0
  134. package/dist/index.d.ts +4 -0
  135. package/dist/index.js +2 -0
  136. package/dist/page/app.js +576 -0
  137. package/dist/page/avatars/beanie.webp +0 -0
  138. package/dist/page/avatars/beard.webp +0 -0
  139. package/dist/page/avatars/bun.webp +0 -0
  140. package/dist/page/avatars/cat.webp +0 -0
  141. package/dist/page/avatars/curls.webp +0 -0
  142. package/dist/page/avatars/glasses.webp +0 -0
  143. package/dist/page/avatars/moustache.webp +0 -0
  144. package/dist/page/avatars/owl.webp +0 -0
  145. package/dist/page/avatars/short-hair.webp +0 -0
  146. package/dist/page/avatars/silver-bob.webp +0 -0
  147. package/dist/page/avatars.js +16 -0
  148. package/dist/page/badges.js +50 -0
  149. package/dist/page/board.js +566 -0
  150. package/dist/page/browse.js +17 -0
  151. package/dist/page/clock-time.js +12 -0
  152. package/dist/page/dialog.js +26 -0
  153. package/dist/page/ending.js +73 -0
  154. package/dist/page/explain.js +58 -0
  155. package/dist/page/fold-tab.js +9 -0
  156. package/dist/page/folds.js +43 -0
  157. package/dist/page/game-column.js +44 -0
  158. package/dist/page/header.js +27 -0
  159. package/dist/page/helps-view.js +51 -0
  160. package/dist/page/helps.js +90 -0
  161. package/dist/page/main.js +8 -0
  162. package/dist/page/model-name.js +20 -0
  163. package/dist/page/move-list.js +68 -0
  164. package/dist/page/new-game-dialog.js +42 -0
  165. package/dist/page/opening-book.js +14 -0
  166. package/dist/page/pieces/CREDITS.md +36 -0
  167. package/dist/page/pieces/cburnett-small.svg +20 -0
  168. package/dist/page/pieces/cburnett.svg +20 -0
  169. package/dist/page/pieces/nitida-small.svg +19 -0
  170. package/dist/page/pieces/nitida.svg +20 -0
  171. package/dist/page/pieces/torretta-small.svg +19 -0
  172. package/dist/page/pieces/torretta.svg +19 -0
  173. package/dist/page/pieces.js +28 -0
  174. package/dist/page/player-card.js +60 -0
  175. package/dist/page/plies.js +62 -0
  176. package/dist/page/profile-dialogs.js +123 -0
  177. package/dist/page/profile.js +95 -0
  178. package/dist/page/settings-panel.js +32 -0
  179. package/dist/page/settings.js +76 -0
  180. package/dist/page/sounds/capture.ogg +0 -0
  181. package/dist/page/sounds/check.ogg +0 -0
  182. package/dist/page/sounds/draw.ogg +0 -0
  183. package/dist/page/sounds/flag.ogg +0 -0
  184. package/dist/page/sounds/mate.ogg +0 -0
  185. package/dist/page/sounds/move.ogg +0 -0
  186. package/dist/page/sounds/offer.ogg +0 -0
  187. package/dist/page/sounds/resign.ogg +0 -0
  188. package/dist/page/sounds.js +105 -0
  189. package/dist/page/studied.js +14 -0
  190. package/dist/page/study-dialog.js +58 -0
  191. package/dist/page/style.css +4398 -0
  192. package/dist/page/talk.js +172 -0
  193. package/dist/page/text.js +593 -0
  194. package/dist/page/torretta.js +38 -0
  195. package/dist/page/voice.js +38 -0
  196. package/dist/profiles/avatar.d.ts +23 -0
  197. package/dist/profiles/avatar.js +84 -0
  198. package/dist/profiles/front-matter.d.ts +9 -0
  199. package/dist/profiles/front-matter.js +33 -0
  200. package/dist/profiles/index.d.ts +3 -0
  201. package/dist/profiles/index.js +5 -0
  202. package/dist/profiles/profiles.d.ts +58 -0
  203. package/dist/profiles/profiles.js +237 -0
  204. package/dist/profiles/settings.d.ts +12 -0
  205. package/dist/profiles/settings.js +46 -0
  206. package/dist/server/analyses.d.ts +17 -0
  207. package/dist/server/analyses.js +46 -0
  208. package/dist/server/bell.d.ts +60 -0
  209. package/dist/server/bell.js +209 -0
  210. package/dist/server/caller.d.ts +10 -0
  211. package/dist/server/caller.js +99 -0
  212. package/dist/server/http.d.ts +16 -0
  213. package/dist/server/http.js +57 -0
  214. package/dist/server/mcp.d.ts +14 -0
  215. package/dist/server/mcp.js +179 -0
  216. package/dist/server/page.d.ts +46 -0
  217. package/dist/server/page.js +657 -0
  218. package/dist/server/profile-routes.d.ts +19 -0
  219. package/dist/server/profile-routes.js +186 -0
  220. package/dist/version.d.ts +2 -0
  221. package/dist/version.js +3 -0
  222. package/instructions/analyst.md +8 -0
  223. package/instructions/opponent.md +15 -0
  224. package/instructions/ponder.md +9 -0
  225. package/package.json +66 -5
@@ -0,0 +1,186 @@
1
+ import { homedir } from "node:os";
2
+ import { isAbsolute, relative, sep } from "node:path";
3
+ import { chooseDrawnAvatar, GameGoingOn, newProfile, useProfile } from "../commands/profiles.js";
4
+ import { inputProblems } from "../commands/registry.js";
5
+ import { activeProfile, drawnAvatars, imageTypes, listProfiles, ownImage, ownImageStamp, profileSettings, saveOwnImage, saveProfileSettings, sentImageLimit, settingsOf } from "../profiles/index.js";
6
+ import { bodyOf, bytesOf, isJson, jsonOf, send, sendJson } from "./http.js";
7
+ /** The profiles of the data folder, as the page shows them. */
8
+ export function profilesShown(root, userFolder = homedir()) {
9
+ const active = activeProfile(root);
10
+ const profiles = listProfiles(root).map((profile) => shown(root, profile));
11
+ const settings = profileSettings(root, active.id);
12
+ const profile = profiles.find(({ id }) => id === active.id);
13
+ const dataFolder = foldersOf(root, userFolder);
14
+ return settings === undefined ? { profile, profiles, dataFolder } : { profile, profiles, settings, dataFolder };
15
+ }
16
+ /** 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. */
17
+ export function answerProfiles(request, response, url, served) {
18
+ const route = routes[`${request.method ?? ""} ${url.pathname}`];
19
+ if (route === undefined) {
20
+ return false;
21
+ }
22
+ Promise.resolve().then(() => route(request, response, served, url)).catch((error) => {
23
+ if (response.headersSent) {
24
+ response.end();
25
+ }
26
+ else {
27
+ sendJson(response, 500, { error: error.message });
28
+ }
29
+ });
30
+ return true;
31
+ }
32
+ const routes = {
33
+ "POST /api/profile/new": newProfileFromPage,
34
+ "POST /api/profile/use": useProfileFromPage,
35
+ "POST /api/profile/avatar": drawnAvatarFromPage,
36
+ "POST /api/profile/image": imageFromPage,
37
+ "GET /api/profile/image": sendImage,
38
+ "POST /api/settings": settingsFromPage,
39
+ "POST /api/data-folder/open": openDataFolder,
40
+ };
41
+ /** 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). */
42
+ async function newProfileFromPage(request, response, served) {
43
+ const body = isJson(request) ? jsonOf(await bodyOf(request)) : undefined;
44
+ const input = newProfile.input.safeParse(body ?? {});
45
+ if (body === undefined || !input.success) {
46
+ const problems = input.success ? "" : ` ${inputProblems("new_profile", input.error.issues, body ?? {}, (name) => name)}`;
47
+ sendJson(response, 400, { error: `Send the profile as JSON, {"name": "…", "level": "regular"}.${problems}` });
48
+ return;
49
+ }
50
+ try {
51
+ const profile = await newProfile.run(input.data, served.context);
52
+ sendJson(response, 200, { profile: profile.id });
53
+ }
54
+ catch (error) {
55
+ sendJson(response, 409, refusalOf(error));
56
+ }
57
+ }
58
+ /** Another profile put in use from the menu, once the game going on has ended: the chat is not woken for it (D-66). */
59
+ async function useProfileFromPage(request, response, served) {
60
+ const profile = isJson(request) ? jsonOf(await bodyOf(request))?.profile : undefined;
61
+ if (typeof profile !== "string") {
62
+ sendJson(response, 400, { error: 'Send the profile as JSON: {"profile": "anna"}.' });
63
+ return;
64
+ }
65
+ try {
66
+ useProfile.run({ profile }, served.context);
67
+ }
68
+ catch (error) {
69
+ sendJson(response, 409, refusalOf(error));
70
+ return;
71
+ }
72
+ sendJson(response, 200, { profile });
73
+ }
74
+ /** A drawn avatar for the profile the page shows, in place of the player's own image. */
75
+ async function drawnAvatarFromPage(request, response, served, url) {
76
+ const avatar = isJson(request) ? jsonOf(await bodyOf(request))?.avatar : undefined;
77
+ const drawn = drawnAvatars.find((one) => one === avatar);
78
+ const profile = profileInUse(served, url, response);
79
+ if (profile === undefined) {
80
+ return;
81
+ }
82
+ if (drawn === undefined) {
83
+ sendJson(response, 400, { error: `Send a drawn avatar as JSON: {"avatar": "owl"}, one of ${drawnAvatars.join(", ")}.` });
84
+ return;
85
+ }
86
+ chooseDrawnAvatar(served.context, profile.id, drawn);
87
+ sendJson(response, 200, { avatar: drawn });
88
+ }
89
+ /** The player's own image for the profile the page shows, as the page made it from their photo: a PNG, JPEG or WebP of 1 MB at most. */
90
+ async function imageFromPage(request, response, served, url) {
91
+ const type = request.headers["content-type"] ?? "";
92
+ // An image sent from another site would need the browser to ask first, which this server never allows.
93
+ if (!Object.values(imageTypes).some((one) => type.startsWith(one))) {
94
+ sendJson(response, 415, { error: "Send the picture as a PNG, JPEG or WebP image." });
95
+ return;
96
+ }
97
+ const bytes = await bytesOf(request, sentImageLimit);
98
+ const profile = profileInUse(served, url, response);
99
+ if (profile === undefined) {
100
+ return;
101
+ }
102
+ if (bytes === undefined) {
103
+ sendJson(response, 413, { error: `Send a picture of ${sentImageLimit / 1024} KB at most.` });
104
+ return;
105
+ }
106
+ try {
107
+ saveOwnImage(served.context.home(), profile.id, bytes);
108
+ }
109
+ catch (error) {
110
+ sendJson(response, 400, { error: error.message });
111
+ return;
112
+ }
113
+ sendJson(response, 200, { image: ownImageStamp(served.context.home(), profile.id) });
114
+ }
115
+ /** A profile's own image, by its id: only a profile of this folder's, only an image. */
116
+ function sendImage(_request, response, served, url) {
117
+ const id = url.searchParams.get("profile");
118
+ const profile = listProfiles(served.context.home()).find((one) => one.id === id);
119
+ const image = profile === undefined ? undefined : ownImage(served.context.home(), profile.id);
120
+ if (image === undefined) {
121
+ send(response, 404, "text/plain; charset=utf-8", "No picture.");
122
+ return;
123
+ }
124
+ // The address changes with the image: the browser may keep it.
125
+ send(response, 200, imageTypes[image.type], image.bytes, { "Cache-Control": "max-age=31536000, immutable" });
126
+ }
127
+ /** The page's settings, kept in the profile the page shows so that they follow the player (D-56). */
128
+ async function settingsFromPage(request, response, served, url) {
129
+ const settings = settingsOf(isJson(request) ? jsonOf(await bodyOf(request)) : undefined);
130
+ const profile = profileInUse(served, url, response);
131
+ if (profile === undefined) {
132
+ return;
133
+ }
134
+ if (settings === undefined) {
135
+ sendJson(response, 400, { error: "Send the settings as a JSON object." });
136
+ return;
137
+ }
138
+ saveProfileSettings(served.context.home(), profile.id, settings);
139
+ sendJson(response, 200, { saved: true });
140
+ }
141
+ /** The data folder in the system's file manager (T-119): asked as JSON, which another site cannot send without the browser asking first. */
142
+ async function openDataFolder(request, response, served) {
143
+ const asked = isJson(request) ? jsonOf(await bodyOf(request)) : undefined;
144
+ const open = served.context.openFolder;
145
+ if (asked === undefined) {
146
+ sendJson(response, 400, { error: "Send {} as JSON to open the data folder." });
147
+ return;
148
+ }
149
+ if (open === undefined) {
150
+ sendJson(response, 501, { error: "This server opens no folder: the page shows its path." });
151
+ return;
152
+ }
153
+ if (!(await open(served.context.home()))) {
154
+ sendJson(response, 500, { error: "No file manager could open the data folder: the page shows its path." });
155
+ return;
156
+ }
157
+ sendJson(response, 200, { opened: true });
158
+ }
159
+ /**
160
+ * 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
161
+ * choices never go to the wrong player.
162
+ */
163
+ function profileInUse(served, url, response) {
164
+ const active = activeProfile(served.context.home());
165
+ const shownId = url.searchParams.get("profile");
166
+ if (active.id === shownId) {
167
+ return active;
168
+ }
169
+ sendJson(response, 409, { error: `The page shows the profile ${shownId ?? "of nobody"}, but ${active.id} is in use now: load the page again.` });
170
+ return undefined;
171
+ }
172
+ function shown(root, profile) {
173
+ const image = ownImageStamp(root, profile.id);
174
+ return image === undefined ? profile : { ...profile, image };
175
+ }
176
+ /** 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. */
177
+ function foldersOf(folder, userFolder) {
178
+ const inside = relative(userFolder, folder);
179
+ const outside = inside === "" || inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside);
180
+ const walked = outside ? folder : inside;
181
+ return walked.split(/[\\/]+/).filter((name) => name !== "");
182
+ }
183
+ function refusalOf(error) {
184
+ const message = error.message;
185
+ return error instanceof GameGoingOn ? { error: message, refusal: "profile-game" } : { error: message };
186
+ }
@@ -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,8 @@
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.
2
+
3
+ - 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.
4
+ - 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.
5
+ - 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, 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.
6
+ - 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.
7
+ - 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, and your word on the whole game. 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 your 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.
8
+ - 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). Then answer their questions about the game with the same tools.
@@ -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.
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; after their move, judge it with `ai_move`'s `badge` and `note` when it stands out, and show what you say with a longer comment and arrows and squares. "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. 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, write it with the pieces' figurines, as the tools show it (♝xf7+ for Bxf7+), for both sides and in every language.
@@ -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.1.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
  }