chesspal 0.1.0 → 0.2.1
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.
- package/README.md +45 -82
- package/dist/cli/chat-hook.js +1 -1
- package/dist/cli/command-line.d.ts +1 -2
- package/dist/cli/command-line.js +2 -3
- package/dist/commands/agents.d.ts +26 -0
- package/dist/commands/agents.js +72 -0
- package/dist/commands/ai-move.d.ts +5 -14
- package/dist/commands/ai-move.js +28 -35
- package/dist/commands/ai-say.d.ts +3 -1
- package/dist/commands/ai-say.js +24 -6
- package/dist/commands/analyse-game.d.ts +4 -0
- package/dist/commands/analyse-game.js +43 -16
- package/dist/commands/annotate-game.d.ts +4 -0
- package/dist/commands/annotate-game.js +65 -12
- package/dist/commands/answer-lines.d.ts +28 -0
- package/dist/commands/answer-lines.js +144 -0
- package/dist/commands/coach.d.ts +45 -0
- package/dist/commands/coach.js +344 -0
- package/dist/commands/context.d.ts +16 -5
- package/dist/commands/context.js +1 -0
- package/dist/commands/delete-game.d.ts +7 -0
- package/dist/commands/delete-game.js +30 -0
- package/dist/commands/delete-review.d.ts +8 -0
- package/dist/commands/delete-review.js +34 -0
- package/dist/commands/drawings.d.ts +0 -2
- package/dist/commands/drawings.js +5 -3
- package/dist/commands/end-game.js +5 -2
- package/dist/commands/export-game.d.ts +1 -1
- package/dist/commands/export-game.js +4 -3
- package/dist/commands/game-summary.d.ts +21 -38
- package/dist/commands/game-summary.js +30 -167
- package/dist/commands/index.d.ts +15 -0
- package/dist/commands/index.js +47 -2
- package/dist/commands/init.d.ts +3 -3
- package/dist/commands/init.js +4 -4
- package/dist/commands/lead-input.d.ts +8 -0
- package/dist/commands/lead-input.js +21 -0
- package/dist/commands/look.js +4 -1
- package/dist/commands/new-game.d.ts +2 -3
- package/dist/commands/new-game.js +7 -4
- package/dist/commands/play-move.d.ts +8 -15
- package/dist/commands/play-move.js +5 -13
- package/dist/commands/player-move.d.ts +2 -6
- package/dist/commands/player-move.js +13 -6
- package/dist/commands/player-say.d.ts +1 -1
- package/dist/commands/player-say.js +1 -1
- package/dist/commands/ponder.d.ts +10 -0
- package/dist/commands/ponder.js +33 -21
- package/dist/commands/position.d.ts +1 -1
- package/dist/commands/position.js +3 -1
- package/dist/commands/profiles.d.ts +13 -4
- package/dist/commands/profiles.js +49 -15
- package/dist/commands/registry.d.ts +8 -3
- package/dist/commands/registry.js +12 -11
- package/dist/commands/say.d.ts +1 -1
- package/dist/commands/say.js +16 -16
- package/dist/commands/speech.d.ts +8 -0
- package/dist/commands/speech.js +20 -0
- package/dist/commands/status.d.ts +9 -3
- package/dist/commands/status.js +40 -15
- package/dist/commands/study-games.js +1 -1
- package/dist/commands/study.js +3 -19
- package/dist/core/facts.js +15 -15
- package/dist/core/game.d.ts +3 -1
- package/dist/core/game.js +8 -4
- package/dist/core/illegal.js +1 -1
- package/dist/core/index.d.ts +5 -4
- package/dist/core/index.js +5 -4
- package/dist/core/lead.d.ts +15 -0
- package/dist/core/lead.js +40 -0
- package/dist/core/levels.d.ts +1 -1
- package/dist/core/levels.js +1 -1
- package/dist/core/moods.d.ts +0 -2
- package/dist/core/moods.js +2 -2
- package/dist/core/openings.d.ts +0 -2
- package/dist/core/openings.js +1 -1
- package/dist/core/profile.js +1 -1
- package/dist/core/record.d.ts +1 -1
- package/dist/core/record.js +1 -1
- package/dist/core/review.d.ts +18 -3
- package/dist/core/review.js +52 -9
- package/dist/games/ai-model.d.ts +4 -0
- package/dist/games/ai-model.js +9 -0
- package/dist/games/analysis.d.ts +1 -1
- package/dist/games/archive.d.ts +25 -4
- package/dist/games/archive.js +99 -25
- package/dist/games/coach.d.ts +57 -0
- package/dist/games/coach.js +144 -0
- package/dist/games/current-game.d.ts +2 -0
- package/dist/games/current-game.js +7 -1
- package/dist/games/files.js +1 -0
- package/dist/games/index.d.ts +5 -4
- package/dist/games/index.js +5 -4
- package/dist/games/talk.d.ts +1 -5
- package/dist/games/talk.js +14 -14
- package/dist/home/open-folder.js +1 -1
- package/dist/host-files/{ponder-agents.d.ts → agents.d.ts} +10 -2
- package/dist/host-files/agents.js +53 -0
- package/dist/host-files/host-files.d.ts +7 -2
- package/dist/host-files/host-files.js +27 -12
- package/dist/host-files/index.d.ts +2 -2
- package/dist/host-files/index.js +3 -3
- package/dist/index.d.ts +1 -3
- package/dist/index.js +1 -0
- package/dist/page/app/anchor.js +88 -0
- package/dist/page/{app.js → app/app.js} +116 -231
- package/dist/page/app/keys.js +94 -0
- package/dist/page/app/phase.js +30 -0
- package/dist/page/app/served.js +73 -0
- package/dist/page/avatars/avatars.js +19 -0
- package/dist/page/avatars/thought.js +10 -0
- package/dist/page/base.css +160 -0
- package/dist/page/board/board.css +959 -0
- package/dist/page/board/board.js +125 -0
- package/dist/page/{explain.js → board/explain.js} +1 -1
- package/dist/page/board/marks.js +160 -0
- package/dist/page/board/motion.js +71 -0
- package/dist/page/board/pointer.js +239 -0
- package/dist/page/board/squares.js +26 -0
- package/dist/page/dialogs/analyses.js +195 -0
- package/dist/page/{dialog.js → dialogs/dialog.js} +1 -1
- package/dist/page/dialogs/dialogs.css +1228 -0
- package/dist/page/{new-game-dialog.js → dialogs/new-game.js} +3 -3
- package/dist/page/{profile-dialogs.js → dialogs/profiles.js} +40 -11
- package/dist/page/dialogs/search.js +31 -0
- package/dist/page/dialogs/settings.js +42 -0
- package/dist/page/dialogs/studied.js +39 -0
- package/dist/page/dialogs/study.js +51 -0
- package/dist/page/{badges.js → game/badges.js} +1 -1
- package/dist/page/{browse.js → game/browse.js} +1 -1
- package/dist/page/game/card-fold.js +40 -0
- package/dist/page/game/column.js +55 -0
- package/dist/page/{ending.js → game/ending.js} +2 -2
- package/dist/page/game/game.css +577 -0
- package/dist/page/{move-list.js → game/move-list.js} +10 -6
- package/dist/page/{plies.js → game/plies.js} +30 -10
- package/dist/page/game/result-tile.js +14 -0
- package/dist/page/helps/helps.css +689 -0
- package/dist/page/helps/helps.js +220 -0
- package/dist/page/helps/view.js +127 -0
- package/dist/page/layout/fold-tab.js +11 -0
- package/dist/page/{folds.js → layout/folds.js} +1 -1
- package/dist/page/{header.js → layout/header.js} +5 -4
- package/dist/page/layout/layout.css +834 -0
- package/dist/page/lead/lead.js +82 -0
- package/dist/page/lead/view.js +38 -0
- package/dist/page/main.js +2 -2
- package/dist/page/pieces/CREDITS.md +12 -29
- package/dist/page/pieces/capablanca-small.svg +20 -0
- package/dist/page/pieces/capablanca.svg +22 -0
- package/dist/page/pieces/nitida-small.svg +3 -2
- package/dist/page/pieces/nitida.svg +3 -2
- package/dist/page/{pieces.js → pieces/pieces.js} +2 -2
- package/dist/page/pieces/torretta-small.svg +15 -14
- package/dist/page/pieces/torretta.svg +17 -15
- package/dist/page/{player-card.js → players/card.js} +2 -6
- package/dist/page/{clock-time.js → players/clock-time.js} +2 -2
- package/dist/page/{profile.js → profile/profile.js} +9 -5
- package/dist/page/{settings.js → profile/settings.js} +5 -4
- package/dist/page/style.css +9 -4398
- package/dist/page/talk/talk.css +958 -0
- package/dist/page/{talk.js → talk/talk.js} +39 -46
- package/dist/page/text.js +137 -19
- package/dist/page/theme.css +292 -0
- package/dist/profiles/index.d.ts +1 -1
- package/dist/profiles/profiles.d.ts +15 -4
- package/dist/profiles/profiles.js +24 -7
- package/dist/server/bell.d.ts +10 -2
- package/dist/server/bell.js +28 -12
- package/dist/server/caller.d.ts +14 -3
- package/dist/server/caller.js +44 -11
- package/dist/server/mcp.d.ts +2 -3
- package/dist/server/mcp.js +53 -19
- package/dist/server/page/analyses.d.ts +23 -0
- package/dist/server/page/analyses.js +65 -0
- package/dist/server/page/files.d.ts +5 -0
- package/dist/server/page/files.js +92 -0
- package/dist/server/page/game.d.ts +6 -0
- package/dist/server/page/game.js +381 -0
- package/dist/server/{http.d.ts → page/http.d.ts} +13 -0
- package/dist/server/{http.js → page/http.js} +11 -0
- package/dist/server/{page.d.ts → page/page.d.ts} +8 -13
- package/dist/server/page/page.js +229 -0
- package/dist/server/{profile-routes.d.ts → page/profiles.d.ts} +2 -2
- package/dist/server/{profile-routes.js → page/profiles.js} +47 -18
- package/instructions/analysis.md +4 -0
- package/instructions/analyst-agent.md +8 -0
- package/instructions/analyst.md +3 -7
- package/instructions/coach.md +11 -0
- package/instructions/opponent.md +4 -4
- package/package.json +1 -1
- package/dist/host-files/ponder-agents.js +0 -40
- package/dist/page/avatars.js +0 -16
- package/dist/page/board.js +0 -566
- package/dist/page/fold-tab.js +0 -9
- package/dist/page/game-column.js +0 -44
- package/dist/page/helps-view.js +0 -51
- package/dist/page/helps.js +0 -90
- package/dist/page/pieces/cburnett-small.svg +0 -20
- package/dist/page/pieces/cburnett.svg +0 -20
- package/dist/page/settings-panel.js +0 -32
- package/dist/page/studied.js +0 -14
- package/dist/page/study-dialog.js +0 -58
- package/dist/server/analyses.d.ts +0 -17
- package/dist/server/analyses.js +0 -46
- package/dist/server/page.js +0 -657
- /package/dist/page/{torretta.js → avatars/torretta.js} +0 -0
- /package/dist/page/{opening-book.js → game/opening-book.js} +0 -0
- /package/dist/page/{model-name.js → players/model-name.js} +0 -0
- /package/dist/page/{sounds.js → sounds/sounds.js} +0 -0
- /package/dist/page/{voice.js → talk/voice.js} +0 -0
package/dist/server/mcp.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
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/
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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 { aiNameOf } from "../core/index.js";
|
|
7
|
+
import { endOf, loadGame, noteAiHost, noteAiModel } from "../games/index.js";
|
|
8
|
+
import { hasAgent } from "../host-files/index.js";
|
|
10
9
|
import { gameFolder } from "../profiles/index.js";
|
|
11
10
|
import { version } from "../version.js";
|
|
12
11
|
import { pagePorts } from "./bell.js";
|
|
13
|
-
import { callerOf } from "./caller.js";
|
|
14
|
-
import { chatHasRead, startPage } from "./page.js";
|
|
12
|
+
import { allowedCaller, callerOf, callingAgent } from "./caller.js";
|
|
13
|
+
import { chatHasRead, startPage } from "./page/page.js";
|
|
15
14
|
/** 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
15
|
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
16
|
+ "chat. Every move goes through Chesspal's tools: never keep the board in your head or invent a position.";
|
|
@@ -21,12 +20,19 @@ export function createMcpServer(context = defaultContext(), list = commands) {
|
|
|
21
20
|
// The server keeps what it told the chat, the instructions a tool gives once, and the position it showed: none yet (D-71).
|
|
22
21
|
context.told ??= new Set();
|
|
23
22
|
context.positionSeen ??= null;
|
|
23
|
+
// The host the chat runs in, as it introduced itself: it names the AI on the page and in the game's PGN (D-97).
|
|
24
|
+
context.host ??= () => server.server.getClientVersion()?.name;
|
|
24
25
|
const forOneHost = [];
|
|
25
26
|
for (const command of list.filter((each) => each.cliOnly !== true)) {
|
|
26
27
|
const tool = server.registerTool(command.name, toolConfig(command), async (input, extra) => {
|
|
28
|
+
// An agent's tool called by the chat, or by another agent, would give what only that agent may see (Priya).
|
|
29
|
+
if (command.byAgents !== undefined && !allowedCaller(await callingAgent(extra._meta, context.folder), command.byAgents)) {
|
|
30
|
+
// It names no agent to start: the chat that asked would start one itself (Priya).
|
|
31
|
+
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 };
|
|
32
|
+
}
|
|
27
33
|
// Each call knows who makes it, for the commands that write it down (D-81).
|
|
28
34
|
const caller = () => callerOf(extra._meta, server.server.getClientVersion(), context.folder);
|
|
29
|
-
const result = await track(runTool(command, input, context, caller));
|
|
35
|
+
const result = await track(runTool(command, input, context, caller, extra._meta));
|
|
30
36
|
if (command === aiMove && result.isError !== true) {
|
|
31
37
|
noteTheAi(context, caller);
|
|
32
38
|
}
|
|
@@ -60,21 +66,21 @@ export async function startMcpServer(context = defaultContext(), list = commands
|
|
|
60
66
|
process.stdin.on("end", leave);
|
|
61
67
|
process.stdin.on("close", leave);
|
|
62
68
|
const server = createMcpServer(context, list);
|
|
63
|
-
context.host = () => server.server.getClientVersion()?.name;
|
|
64
69
|
context.page = await pageOf(context, () => opponentOf(context.host?.()));
|
|
65
70
|
await server.connect(new StdioServerTransport());
|
|
66
71
|
}
|
|
67
|
-
/** The opponent's name for the page, from the host's:
|
|
72
|
+
/** The opponent's name for the page, from the host's (D-97): another host than Claude's and Codex is unnamed. */
|
|
68
73
|
export function opponentOf(host) {
|
|
69
|
-
return
|
|
74
|
+
return aiNameOf(host);
|
|
70
75
|
}
|
|
71
76
|
/** 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
77
|
async function pageOf(context, opponent) {
|
|
73
78
|
try {
|
|
74
|
-
const { url, heard, chatCalls, open, askNewGame } = await startPage(context, opponent);
|
|
79
|
+
const { url, heard, chatCalls, ponders, open, askNewGame } = await startPage(context, opponent);
|
|
75
80
|
// Words the AI is given change no file: the pages hear of them from the commands.
|
|
76
81
|
context.heard = heard;
|
|
77
82
|
context.calls = chatCalls;
|
|
83
|
+
context.ponders = ponders;
|
|
78
84
|
context.pageOpen = open;
|
|
79
85
|
context.askNewGame = askNewGame;
|
|
80
86
|
if (!pagePorts(context.folder).includes(Number(new URL(url).port))) {
|
|
@@ -89,14 +95,18 @@ async function pageOf(context, opponent) {
|
|
|
89
95
|
}
|
|
90
96
|
}
|
|
91
97
|
/**
|
|
92
|
-
* Who played the AI's move, read once the move has gone back to the chat, and kept in memory (D-82): the AI's card on the
|
|
93
|
-
* PGN once it ends; a change shows from the next move. A host that ties no model to the call leaves the last one known.
|
|
98
|
+
* Who played the AI's move, its host and model, read once the move has gone back to the chat, and kept in memory (D-82, D-97): the AI's card on the
|
|
99
|
+
* page shows it, and the game's PGN once it ends; a change shows from the next move. A host that ties no model to the call leaves the last one known.
|
|
94
100
|
*/
|
|
95
101
|
function noteTheAi(context, caller) {
|
|
96
102
|
// Claude Code writes the call's line a few milliseconds late: read after the answer, it costs the AI's clock nothing (D-13).
|
|
97
103
|
setImmediate(() => {
|
|
98
104
|
// Nothing that fails here may stop the server: the move is played and answered.
|
|
99
105
|
caller().then((found) => {
|
|
106
|
+
const host = context.host?.();
|
|
107
|
+
if (host !== undefined && host.trim() !== "") {
|
|
108
|
+
noteAiHost(gameFolder(context.home()), host);
|
|
109
|
+
}
|
|
100
110
|
if (found === undefined || found.hostOnly === true) {
|
|
101
111
|
return;
|
|
102
112
|
}
|
|
@@ -121,13 +131,15 @@ function toolConfig(command) {
|
|
|
121
131
|
// The SDK writes the draft's address in every schema: it says nothing to an AI, and costs tokens in every tool.
|
|
122
132
|
return { description: command.description, inputSchema: z.looseObject({}).meta({ ...shownInput(command), $schema: undefined }), annotations, _meta: meta };
|
|
123
133
|
}
|
|
124
|
-
async function runTool(command, input, context, caller) {
|
|
134
|
+
async function runTool(command, input, context, caller, meta) {
|
|
135
|
+
const agent = await sharingAgent(command, meta, context);
|
|
136
|
+
const chat = command.byAgents === undefined && agent === undefined;
|
|
125
137
|
let refused = false;
|
|
126
138
|
try {
|
|
127
|
-
if (
|
|
139
|
+
if (chat) {
|
|
128
140
|
context.calls?.();
|
|
129
141
|
}
|
|
130
|
-
const result = await command.run(checkedInput(command, input), { ...context, caller });
|
|
142
|
+
const result = await command.run(checkedInput(command, input), { ...context, caller, ...(agent === undefined ? {} : { agent }) });
|
|
131
143
|
return { content: [{ type: "text", text: command.format(result, context) }] };
|
|
132
144
|
}
|
|
133
145
|
catch (error) {
|
|
@@ -137,11 +149,33 @@ async function runTool(command, input, context, caller) {
|
|
|
137
149
|
finally {
|
|
138
150
|
// 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
151
|
// unless that tool was refused for a position it had not seen. An agent's call is not the chat's.
|
|
140
|
-
if (
|
|
152
|
+
if (chat) {
|
|
141
153
|
chatHasRead(context, command.showsGame === true && !refused);
|
|
142
154
|
}
|
|
143
155
|
}
|
|
144
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* 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
|
|
159
|
+
* 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.
|
|
160
|
+
*/
|
|
161
|
+
async function sharingAgent(command, meta, context) {
|
|
162
|
+
const agents = command.sharedWith?.filter((agent) => hasAgent(context.folder, agent)) ?? [];
|
|
163
|
+
if (agents.length === 0 || gameGoingOn(context)) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
const caller = await callingAgent(meta, context.folder);
|
|
167
|
+
return caller !== undefined && "agent" in caller && agents.includes(caller.agent) ? caller.agent : undefined;
|
|
168
|
+
}
|
|
169
|
+
/** 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). */
|
|
170
|
+
function gameGoingOn(context) {
|
|
171
|
+
try {
|
|
172
|
+
const current = loadGame(gameFolder(context.home()));
|
|
173
|
+
return current !== undefined && endOf(current.game) === undefined;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
145
179
|
// 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
180
|
// what is wrong comes back in words an AI can act on.
|
|
147
181
|
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;
|