chesspal 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +45 -82
  2. package/dist/cli/chat-hook.js +1 -1
  3. package/dist/cli/command-line.d.ts +1 -2
  4. package/dist/cli/command-line.js +2 -3
  5. package/dist/commands/agents.d.ts +26 -0
  6. package/dist/commands/agents.js +72 -0
  7. package/dist/commands/ai-move.d.ts +5 -14
  8. package/dist/commands/ai-move.js +28 -35
  9. package/dist/commands/ai-say.d.ts +3 -1
  10. package/dist/commands/ai-say.js +24 -6
  11. package/dist/commands/analyse-game.d.ts +4 -0
  12. package/dist/commands/analyse-game.js +43 -16
  13. package/dist/commands/annotate-game.d.ts +4 -0
  14. package/dist/commands/annotate-game.js +65 -12
  15. package/dist/commands/answer-lines.d.ts +28 -0
  16. package/dist/commands/answer-lines.js +144 -0
  17. package/dist/commands/coach.d.ts +45 -0
  18. package/dist/commands/coach.js +344 -0
  19. package/dist/commands/context.d.ts +16 -5
  20. package/dist/commands/context.js +1 -0
  21. package/dist/commands/delete-game.d.ts +7 -0
  22. package/dist/commands/delete-game.js +30 -0
  23. package/dist/commands/delete-review.d.ts +8 -0
  24. package/dist/commands/delete-review.js +34 -0
  25. package/dist/commands/drawings.d.ts +0 -2
  26. package/dist/commands/drawings.js +5 -3
  27. package/dist/commands/end-game.js +5 -2
  28. package/dist/commands/export-game.d.ts +1 -1
  29. package/dist/commands/export-game.js +4 -3
  30. package/dist/commands/game-summary.d.ts +21 -38
  31. package/dist/commands/game-summary.js +30 -167
  32. package/dist/commands/index.d.ts +15 -0
  33. package/dist/commands/index.js +47 -2
  34. package/dist/commands/init.d.ts +3 -3
  35. package/dist/commands/init.js +4 -4
  36. package/dist/commands/lead-input.d.ts +8 -0
  37. package/dist/commands/lead-input.js +21 -0
  38. package/dist/commands/look.js +4 -1
  39. package/dist/commands/new-game.d.ts +2 -3
  40. package/dist/commands/new-game.js +7 -4
  41. package/dist/commands/play-move.d.ts +8 -15
  42. package/dist/commands/play-move.js +5 -13
  43. package/dist/commands/player-move.d.ts +2 -6
  44. package/dist/commands/player-move.js +13 -6
  45. package/dist/commands/player-say.d.ts +1 -1
  46. package/dist/commands/player-say.js +1 -1
  47. package/dist/commands/ponder.d.ts +10 -0
  48. package/dist/commands/ponder.js +33 -21
  49. package/dist/commands/position.d.ts +1 -1
  50. package/dist/commands/position.js +3 -1
  51. package/dist/commands/profiles.d.ts +13 -4
  52. package/dist/commands/profiles.js +49 -15
  53. package/dist/commands/registry.d.ts +8 -3
  54. package/dist/commands/registry.js +12 -11
  55. package/dist/commands/say.d.ts +1 -1
  56. package/dist/commands/say.js +16 -16
  57. package/dist/commands/speech.d.ts +8 -0
  58. package/dist/commands/speech.js +20 -0
  59. package/dist/commands/status.d.ts +9 -3
  60. package/dist/commands/status.js +40 -15
  61. package/dist/commands/study-games.js +1 -1
  62. package/dist/commands/study.js +3 -19
  63. package/dist/core/facts.js +15 -15
  64. package/dist/core/game.d.ts +3 -1
  65. package/dist/core/game.js +8 -4
  66. package/dist/core/illegal.js +1 -1
  67. package/dist/core/index.d.ts +5 -4
  68. package/dist/core/index.js +5 -4
  69. package/dist/core/lead.d.ts +15 -0
  70. package/dist/core/lead.js +40 -0
  71. package/dist/core/levels.d.ts +1 -1
  72. package/dist/core/levels.js +1 -1
  73. package/dist/core/moods.d.ts +0 -2
  74. package/dist/core/moods.js +2 -2
  75. package/dist/core/openings.d.ts +0 -2
  76. package/dist/core/openings.js +1 -1
  77. package/dist/core/profile.js +1 -1
  78. package/dist/core/record.d.ts +1 -1
  79. package/dist/core/record.js +1 -1
  80. package/dist/core/review.d.ts +9 -3
  81. package/dist/core/review.js +29 -9
  82. package/dist/games/analysis.d.ts +1 -1
  83. package/dist/games/archive.d.ts +24 -3
  84. package/dist/games/archive.js +96 -22
  85. package/dist/games/coach.d.ts +57 -0
  86. package/dist/games/coach.js +144 -0
  87. package/dist/games/current-game.d.ts +2 -0
  88. package/dist/games/current-game.js +7 -1
  89. package/dist/games/files.js +1 -0
  90. package/dist/games/index.d.ts +4 -3
  91. package/dist/games/index.js +4 -3
  92. package/dist/games/talk.d.ts +1 -5
  93. package/dist/games/talk.js +14 -14
  94. package/dist/home/open-folder.js +1 -1
  95. package/dist/host-files/{ponder-agents.d.ts → agents.d.ts} +10 -2
  96. package/dist/host-files/agents.js +53 -0
  97. package/dist/host-files/host-files.d.ts +7 -2
  98. package/dist/host-files/host-files.js +27 -12
  99. package/dist/host-files/index.d.ts +2 -2
  100. package/dist/host-files/index.js +3 -3
  101. package/dist/index.d.ts +1 -3
  102. package/dist/index.js +1 -0
  103. package/dist/page/app/anchor.js +88 -0
  104. package/dist/page/{app.js → app/app.js} +112 -230
  105. package/dist/page/app/keys.js +94 -0
  106. package/dist/page/app/phase.js +30 -0
  107. package/dist/page/app/served.js +73 -0
  108. package/dist/page/avatars/avatars.js +19 -0
  109. package/dist/page/avatars/thought.js +10 -0
  110. package/dist/page/base.css +160 -0
  111. package/dist/page/board/board.css +959 -0
  112. package/dist/page/board/board.js +125 -0
  113. package/dist/page/{explain.js → board/explain.js} +1 -1
  114. package/dist/page/board/marks.js +160 -0
  115. package/dist/page/board/motion.js +71 -0
  116. package/dist/page/board/pointer.js +239 -0
  117. package/dist/page/board/squares.js +26 -0
  118. package/dist/page/dialogs/analyses.js +195 -0
  119. package/dist/page/{dialog.js → dialogs/dialog.js} +1 -1
  120. package/dist/page/dialogs/dialogs.css +1228 -0
  121. package/dist/page/{new-game-dialog.js → dialogs/new-game.js} +3 -3
  122. package/dist/page/{profile-dialogs.js → dialogs/profiles.js} +40 -11
  123. package/dist/page/dialogs/search.js +31 -0
  124. package/dist/page/dialogs/settings.js +42 -0
  125. package/dist/page/dialogs/studied.js +39 -0
  126. package/dist/page/dialogs/study.js +51 -0
  127. package/dist/page/{badges.js → game/badges.js} +1 -1
  128. package/dist/page/{browse.js → game/browse.js} +1 -1
  129. package/dist/page/game/card-fold.js +40 -0
  130. package/dist/page/game/column.js +51 -0
  131. package/dist/page/{ending.js → game/ending.js} +2 -2
  132. package/dist/page/game/game.css +577 -0
  133. package/dist/page/{move-list.js → game/move-list.js} +10 -6
  134. package/dist/page/{plies.js → game/plies.js} +30 -10
  135. package/dist/page/game/result-tile.js +14 -0
  136. package/dist/page/helps/helps.css +689 -0
  137. package/dist/page/helps/helps.js +220 -0
  138. package/dist/page/helps/view.js +127 -0
  139. package/dist/page/layout/fold-tab.js +11 -0
  140. package/dist/page/{folds.js → layout/folds.js} +1 -1
  141. package/dist/page/{header.js → layout/header.js} +5 -4
  142. package/dist/page/layout/layout.css +834 -0
  143. package/dist/page/lead/lead.js +82 -0
  144. package/dist/page/lead/view.js +38 -0
  145. package/dist/page/main.js +2 -2
  146. package/dist/page/pieces/CREDITS.md +12 -29
  147. package/dist/page/pieces/capablanca-small.svg +20 -0
  148. package/dist/page/pieces/capablanca.svg +22 -0
  149. package/dist/page/pieces/nitida-small.svg +3 -2
  150. package/dist/page/pieces/nitida.svg +3 -2
  151. package/dist/page/{pieces.js → pieces/pieces.js} +2 -2
  152. package/dist/page/pieces/torretta-small.svg +15 -14
  153. package/dist/page/pieces/torretta.svg +17 -15
  154. package/dist/page/{player-card.js → players/card.js} +2 -6
  155. package/dist/page/{clock-time.js → players/clock-time.js} +2 -2
  156. package/dist/page/{profile.js → profile/profile.js} +9 -5
  157. package/dist/page/{settings.js → profile/settings.js} +5 -4
  158. package/dist/page/style.css +9 -4398
  159. package/dist/page/talk/talk.css +958 -0
  160. package/dist/page/{talk.js → talk/talk.js} +39 -46
  161. package/dist/page/text.js +137 -19
  162. package/dist/page/theme.css +292 -0
  163. package/dist/profiles/index.d.ts +1 -1
  164. package/dist/profiles/profiles.d.ts +15 -4
  165. package/dist/profiles/profiles.js +24 -7
  166. package/dist/server/bell.d.ts +10 -2
  167. package/dist/server/bell.js +28 -12
  168. package/dist/server/caller.d.ts +14 -3
  169. package/dist/server/caller.js +44 -11
  170. package/dist/server/mcp.d.ts +1 -2
  171. package/dist/server/mcp.js +42 -14
  172. package/dist/server/page/analyses.d.ts +23 -0
  173. package/dist/server/page/analyses.js +65 -0
  174. package/dist/server/page/files.d.ts +5 -0
  175. package/dist/server/page/files.js +92 -0
  176. package/dist/server/page/game.d.ts +6 -0
  177. package/dist/server/page/game.js +381 -0
  178. package/dist/server/{http.d.ts → page/http.d.ts} +13 -0
  179. package/dist/server/{http.js → page/http.js} +11 -0
  180. package/dist/server/{page.d.ts → page/page.d.ts} +8 -13
  181. package/dist/server/page/page.js +229 -0
  182. package/dist/server/{profile-routes.d.ts → page/profiles.d.ts} +2 -2
  183. package/dist/server/{profile-routes.js → page/profiles.js} +47 -18
  184. package/instructions/analysis.md +4 -0
  185. package/instructions/analyst-agent.md +8 -0
  186. package/instructions/analyst.md +3 -7
  187. package/instructions/coach.md +11 -0
  188. package/instructions/opponent.md +4 -4
  189. package/package.json +1 -1
  190. package/dist/host-files/ponder-agents.js +0 -40
  191. package/dist/page/avatars.js +0 -16
  192. package/dist/page/board.js +0 -566
  193. package/dist/page/fold-tab.js +0 -9
  194. package/dist/page/game-column.js +0 -44
  195. package/dist/page/helps-view.js +0 -51
  196. package/dist/page/helps.js +0 -90
  197. package/dist/page/pieces/cburnett-small.svg +0 -20
  198. package/dist/page/pieces/cburnett.svg +0 -20
  199. package/dist/page/settings-panel.js +0 -32
  200. package/dist/page/studied.js +0 -14
  201. package/dist/page/study-dialog.js +0 -58
  202. package/dist/server/analyses.d.ts +0 -17
  203. package/dist/server/analyses.js +0 -46
  204. package/dist/server/page.js +0 -657
  205. /package/dist/page/{torretta.js → avatars/torretta.js} +0 -0
  206. /package/dist/page/{opening-book.js → game/opening-book.js} +0 -0
  207. /package/dist/page/{model-name.js → players/model-name.js} +0 -0
  208. /package/dist/page/{sounds.js → sounds/sounds.js} +0 -0
  209. /package/dist/page/{voice.js → talk/voice.js} +0 -0
@@ -1,17 +1,15 @@
1
+ // The MCP server an AI host starts: every command as a tool (D-5), and the board page served for as long as the chat lasts (D-47).
1
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
4
  import { z } from "zod";
4
- import { aiMove } from "../commands/ai-move.js";
5
- import { defaultContext } from "../commands/context.js";
6
- import { commands } from "../commands/index.js";
7
- import { PositionChanged } from "../commands/play-move.js";
8
- import { inputProblems, isBoolean } from "../commands/registry.js";
9
- import { noteAiModel } from "../games/index.js";
5
+ import { aiMove, commands, defaultContext, inputProblems, isBoolean, PositionChanged } from "../commands/index.js";
6
+ import { endOf, loadGame, noteAiModel } from "../games/index.js";
7
+ import { hasAgent } from "../host-files/index.js";
10
8
  import { gameFolder } from "../profiles/index.js";
11
9
  import { version } from "../version.js";
12
10
  import { pagePorts } from "./bell.js";
13
- import { callerOf } from "./caller.js";
14
- import { chatHasRead, startPage } from "./page.js";
11
+ import { allowedCaller, callerOf, callingAgent } from "./caller.js";
12
+ import { chatHasRead, startPage } from "./page/page.js";
15
13
  /** What the host reads as the server starts, before any tool: where to begin, and the rule of every move (D-27). Short, since hosts may cut it. */
16
14
  export const serverInstructions = "Chesspal keeps the chess for you: the rules, the board, the clock, the games and the player's memory. Call `status` at the start of every "
17
15
  + "chat. Every move goes through Chesspal's tools: never keep the board in your head or invent a position.";
@@ -24,9 +22,14 @@ export function createMcpServer(context = defaultContext(), list = commands) {
24
22
  const forOneHost = [];
25
23
  for (const command of list.filter((each) => each.cliOnly !== true)) {
26
24
  const tool = server.registerTool(command.name, toolConfig(command), async (input, extra) => {
25
+ // An agent's tool called by the chat, or by another agent, would give what only that agent may see (Priya).
26
+ if (command.byAgents !== undefined && !allowedCaller(await callingAgent(extra._meta, context.folder), command.byAgents)) {
27
+ // It names no agent to start: the chat that asked would start one itself (Priya).
28
+ return { content: [{ type: "text", text: `${command.name} is not yours to call: the player reads what it gives on the board page. Nothing changed.` }], isError: true };
29
+ }
27
30
  // Each call knows who makes it, for the commands that write it down (D-81).
28
31
  const caller = () => callerOf(extra._meta, server.server.getClientVersion(), context.folder);
29
- const result = await track(runTool(command, input, context, caller));
32
+ const result = await track(runTool(command, input, context, caller, extra._meta));
30
33
  if (command === aiMove && result.isError !== true) {
31
34
  noteTheAi(context, caller);
32
35
  }
@@ -71,10 +74,11 @@ export function opponentOf(host) {
71
74
  /** The board page of the player's folder, served by this process for as long as the chat lasts (D-47); without it the game goes on in the chat. */
72
75
  async function pageOf(context, opponent) {
73
76
  try {
74
- const { url, heard, chatCalls, open, askNewGame } = await startPage(context, opponent);
77
+ const { url, heard, chatCalls, ponders, open, askNewGame } = await startPage(context, opponent);
75
78
  // Words the AI is given change no file: the pages hear of them from the commands.
76
79
  context.heard = heard;
77
80
  context.calls = chatCalls;
81
+ context.ponders = ponders;
78
82
  context.pageOpen = open;
79
83
  context.askNewGame = askNewGame;
80
84
  if (!pagePorts(context.folder).includes(Number(new URL(url).port))) {
@@ -121,13 +125,15 @@ function toolConfig(command) {
121
125
  // The SDK writes the draft's address in every schema: it says nothing to an AI, and costs tokens in every tool.
122
126
  return { description: command.description, inputSchema: z.looseObject({}).meta({ ...shownInput(command), $schema: undefined }), annotations, _meta: meta };
123
127
  }
124
- async function runTool(command, input, context, caller) {
128
+ async function runTool(command, input, context, caller, meta) {
129
+ const agent = await sharingAgent(command, meta, context);
130
+ const chat = command.byAgents === undefined && agent === undefined;
125
131
  let refused = false;
126
132
  try {
127
- if (command.byAgent !== true) {
133
+ if (chat) {
128
134
  context.calls?.();
129
135
  }
130
- const result = await command.run(checkedInput(command, input), { ...context, caller });
136
+ const result = await command.run(checkedInput(command, input), { ...context, caller, ...(agent === undefined ? {} : { agent }) });
131
137
  return { content: [{ type: "text", text: command.format(result, context) }] };
132
138
  }
133
139
  catch (error) {
@@ -137,11 +143,33 @@ async function runTool(command, input, context, caller) {
137
143
  finally {
138
144
  // What the tool read or wrote, the chat has seen: the doorbell rings it only for what changes after, and it may move on the game a tool showed it,
139
145
  // unless that tool was refused for a position it had not seen. An agent's call is not the chat's.
140
- if (command.byAgent !== true) {
146
+ if (chat) {
141
147
  chatHasRead(context, command.showsGame === true && !refused);
142
148
  }
143
149
  }
144
150
  }
151
+ /**
152
+ * The agent that calls a tool it shares with the chat, as the analyst (D-89), by its type: looked up only in a folder where one may run and while no game
153
+ * goes on, so that no call of the chat's waits for it while it plays; none for the chat, or when it cannot be told.
154
+ */
155
+ async function sharingAgent(command, meta, context) {
156
+ const agents = command.sharedWith?.filter((agent) => hasAgent(context.folder, agent)) ?? [];
157
+ if (agents.length === 0 || gameGoingOn(context)) {
158
+ return undefined;
159
+ }
160
+ const caller = await callingAgent(meta, context.folder);
161
+ return caller !== undefined && "agent" in caller && agents.includes(caller.agent) ? caller.agent : undefined;
162
+ }
163
+ /** Whether a game goes on, or may: a game that cannot be read is the command's to report, as the chat's call (Federico). */
164
+ function gameGoingOn(context) {
165
+ try {
166
+ const current = loadGame(gameFolder(context.home()));
167
+ return current !== undefined && endOf(current.game) === undefined;
168
+ }
169
+ catch {
170
+ return true;
171
+ }
172
+ }
145
173
  // The JSON schema of each command's input, as the SDK would write it from the strict schema: the tool takes any object, and the command checks it, so that
146
174
  // what is wrong comes back in words an AI can act on.
147
175
  const shownInputs = new WeakMap();
@@ -0,0 +1,23 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { type AiModel } from "../../core/index.ts";
3
+ import type { Served } from "./page.ts";
4
+ /** An analysis as the page lists it: who played, with the AI's model (D-82), when, how it ended, its opening, how long, and whether the AI has reviewed it. */
5
+ export interface AnalysisItem {
6
+ readonly name: string;
7
+ readonly white: string;
8
+ readonly black: string;
9
+ readonly aiModel?: AiModel;
10
+ readonly date?: string;
11
+ readonly result: string;
12
+ readonly opening?: string;
13
+ readonly moves: number;
14
+ readonly reviewed: boolean;
15
+ /** Who reviewed it (D-81). */
16
+ readonly annotator?: AiModel;
17
+ /** Where a game loaded from elsewhere was played, as its Site tag says: the player's name there makes it theirs (D-93). */
18
+ readonly site?: string;
19
+ }
20
+ /** The games to analyse in the profile in use, the latest first, all of them for the page's search: those kept to study and those played, reviewed or not. */
21
+ export declare function analysesOf(home: string): AnalysisItem[];
22
+ /** Answers the page's deletions, of a game or of its review alone, done as the command line does them: true when the request was one. */
23
+ export declare function answerAnalyses(request: IncomingMessage, response: ServerResponse, url: URL, served: Served): boolean;
@@ -0,0 +1,65 @@
1
+ import { bookOpening, deleteGame, deleteReview } from "../../commands/index.js";
2
+ import { aiModelIn, annotatorOf, isReviewed } from "../../core/index.js";
3
+ import { archivedGame, archivedGames, gameStemOf, keptGame, keptGames } from "../../games/index.js";
4
+ import { gameFolder } from "../../profiles/index.js";
5
+ import { bodyOf, isJson, jsonOf, runRoute, sendJson, textOf } from "./http.js";
6
+ /** The games to analyse in the profile in use, the latest first, all of them for the page's search: those kept to study and those played, reviewed or not. */
7
+ export function analysesOf(home) {
8
+ const folder = gameFolder(home);
9
+ // By the time in their names, which orders kept and played games together.
10
+ const stamp = (name) => gameStemOf(name) ?? "";
11
+ const names = [...keptGames(folder), ...archivedGames(folder)].sort((one, other) => stamp(other).localeCompare(stamp(one)));
12
+ return names.flatMap((name) => itemOf(name, () => name.startsWith("study/") ? keptGame(folder, name) : archivedGame(folder, name)));
13
+ }
14
+ /** Answers the page's deletions, of a game or of its review alone, done as the command line does them: true when the request was one. */
15
+ export function answerAnalyses(request, response, url, served) {
16
+ const command = request.method === "POST" ? deletions[url.pathname] : undefined;
17
+ if (command === undefined) {
18
+ return false;
19
+ }
20
+ runRoute(response, async () => {
21
+ const game = textOf(jsonOf(isJson(request) ? await bodyOf(request) : undefined)?.game);
22
+ if (game === undefined) {
23
+ sendJson(response, 400, { error: 'Send the game as JSON: {"game": "games/2026-11-02-153012.pgn"}.' });
24
+ return;
25
+ }
26
+ try {
27
+ sendJson(response, 200, await command.run({ game }, served.context));
28
+ }
29
+ catch (error) {
30
+ sendJson(response, 409, { error: error.message });
31
+ }
32
+ });
33
+ return true;
34
+ }
35
+ const deletions = { "/api/delete-game": deleteGame, "/api/delete-review": deleteReview };
36
+ function itemOf(name, read) {
37
+ let record;
38
+ try {
39
+ record = read();
40
+ }
41
+ catch {
42
+ // A game that cannot be read stays out of the list.
43
+ return [];
44
+ }
45
+ const { tags } = record;
46
+ const date = tags.Date === undefined || tags.Date.includes("?") ? undefined : tags.Date;
47
+ const opening = bookOpening(record)?.name;
48
+ const aiModel = aiModelIn(record);
49
+ const site = tags.Site === undefined || /^[?\-\s]*$/.test(tags.Site) ? undefined : tags.Site;
50
+ const signed = annotatorOf(record);
51
+ const annotator = signed === undefined ? undefined : { model: signed.annotator, ...(signed.effort === undefined ? {} : { effort: signed.effort }) };
52
+ return [{
53
+ name,
54
+ white: tags.White ?? "?",
55
+ black: tags.Black ?? "?",
56
+ ...(aiModel === undefined ? {} : { aiModel }),
57
+ ...(date === undefined ? {} : { date }),
58
+ result: tags.Result ?? "*",
59
+ ...(opening === undefined ? {} : { opening }),
60
+ moves: Math.ceil(record.moves.length / 2),
61
+ reviewed: isReviewed(record),
62
+ ...(annotator === undefined ? {} : { annotator }),
63
+ ...(site === undefined ? {} : { site }),
64
+ }];
65
+ }
@@ -0,0 +1,5 @@
1
+ import type { ServerResponse } from "node:http";
2
+ /** The page's HTML, which runs only its own files and its import map. */
3
+ export declare function sendHtml(response: ServerResponse): void;
4
+ /** A script, style, picture or sound of the page, a module of the core, or a package's module: only from their folders, and only of the types the page uses. */
5
+ export declare function sendFile(response: ServerResponse, path: string): void;
@@ -0,0 +1,92 @@
1
+ // The page's files: its HTML with the import map, its scripts, styles, pictures and sounds, the chess core and the packages they import, each only from its
2
+ // folder and of the types the page uses.
3
+ import { createHash } from "node:crypto";
4
+ import { readFileSync } from "node:fs";
5
+ import { createRequire } from "node:module";
6
+ import { extname, isAbsolute, join, relative, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { send } from "./http.js";
9
+ /** The page's HTML, which runs only its own files and its import map. */
10
+ export function sendHtml(response) {
11
+ send(response, 200, "text/html; charset=utf-8", html, { "Content-Security-Policy": policy });
12
+ }
13
+ /** A script, style, picture or sound of the page, a module of the core, or a package's module: only from their folders, and only of the types the page uses. */
14
+ export function sendFile(response, path) {
15
+ const file = path.startsWith("/vendor/") ? vendorModule(path.slice("/vendor/".length)) : builtFile(path);
16
+ const type = file === undefined ? undefined : types[extname(file)];
17
+ let bytes;
18
+ try {
19
+ bytes = file === undefined || type === undefined ? undefined : readFileSync(file);
20
+ }
21
+ catch {
22
+ bytes = undefined;
23
+ }
24
+ if (bytes === undefined || type === undefined) {
25
+ send(response, 404, "text/plain; charset=utf-8", `Nothing at ${path}.`);
26
+ return;
27
+ }
28
+ // A pieces' set loaded again for each piece that moves blanks the pieces drawn from it, at times until the next move: the browser keeps it an hour,
29
+ // and the drawn avatars too, all at once in the profile's dialog.
30
+ const kept = type === types[".svg"] || type === types[".webp"];
31
+ send(response, 200, type, bytes, kept ? { "Cache-Control": "max-age=3600" } : {});
32
+ }
33
+ // The built page and core: dist/, from src/server/page and from dist/server/page alike.
34
+ const built = fileURLToPath(new URL("../../../dist/", import.meta.url));
35
+ // The modules the page imports from packages, by the names the import map gives them: a package, and its file.
36
+ const vendorModules = {
37
+ "preact.js": ["preact", "dist/preact.module.js"],
38
+ "hooks.js": ["preact", "hooks/dist/hooks.module.js"],
39
+ "jsx-runtime.js": ["preact", "jsx-runtime/dist/jsxRuntime.module.js"],
40
+ "chess.js": ["chess.js", "dist/esm/chess.js"],
41
+ };
42
+ const importMap = JSON.stringify({
43
+ imports: { "preact": "/vendor/preact.js", "preact/hooks": "/vendor/hooks.js", "preact/jsx-runtime": "/vendor/jsx-runtime.js", "chess.js": "/vendor/chess.js" },
44
+ });
45
+ // The page runs only its own files and the import map written in it, known by its hash.
46
+ const policy = `default-src 'self'; script-src 'self' 'sha256-${createHash("sha256").update(importMap).digest("base64")}'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; `
47
+ + "base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
48
+ const html = [
49
+ "<!doctype html>",
50
+ '<html lang="en">',
51
+ '<meta charset="utf-8">',
52
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
53
+ '<meta name="color-scheme" content="light dark">',
54
+ "<title>Chesspal</title>",
55
+ '<link rel="stylesheet" href="/page/style.css">',
56
+ `<script type="importmap">${importMap}</script>`,
57
+ '<script type="module" src="/page/main.js"></script>',
58
+ '<div id="app"></div>',
59
+ "",
60
+ ].join("\n");
61
+ const types = { ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".svg": "image/svg+xml", ".webp": "image/webp", ".json": "application/json; charset=utf-8", ".ogg": "audio/ogg" };
62
+ function builtFile(path) {
63
+ const area = /^\/(page|core)\//.exec(path)?.[1];
64
+ if (area === undefined) {
65
+ return undefined;
66
+ }
67
+ try {
68
+ const root = join(built, area);
69
+ const file = resolve(built, `.${decodeURIComponent(path)}`);
70
+ const inside = relative(root, file);
71
+ return inside === "" || inside.startsWith("..") || isAbsolute(inside) ? undefined : file;
72
+ }
73
+ catch {
74
+ // An address that does not decode.
75
+ return undefined;
76
+ }
77
+ }
78
+ function vendorModule(name) {
79
+ const module = vendorModules[name];
80
+ if (module === undefined) {
81
+ return undefined;
82
+ }
83
+ try {
84
+ // The package's folder, from where its main file is: node_modules/<package>.
85
+ const main = createRequire(import.meta.url).resolve(module[0]);
86
+ const marker = join("node_modules", module[0]);
87
+ return join(main.slice(0, main.lastIndexOf(marker) + marker.length), module[1]);
88
+ }
89
+ catch {
90
+ return undefined;
91
+ }
92
+ }
@@ -0,0 +1,6 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { Served } from "./page.ts";
3
+ /** Answers the page's requests about the game: true when the request was one. */
4
+ export declare function answerGame(request: IncomingMessage, response: ServerResponse, url: URL, served: Served): boolean;
5
+ /** The chat calls a tool: when nobody heard the player before, the AI's clock, held since, starts now, with the time it was held given back. */
6
+ export declare function chatCalls(served: Served): void;