chesspal 0.3.5 → 0.4.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.
- package/README.md +1 -0
- package/dist/cli/cli.js +17 -4
- package/dist/cli/command-line.js +22 -12
- package/dist/cli/icon.d.ts +2 -0
- package/dist/cli/icon.js +11 -1
- package/dist/cli/prompt-hook.d.ts +17 -0
- package/dist/cli/prompt-hook.js +100 -0
- package/dist/commands/analysis/analyse.d.ts +2 -2
- package/dist/commands/analysis/analyse.js +10 -16
- package/dist/commands/analysis/annotate.d.ts +8 -0
- package/dist/commands/analysis/annotate.js +19 -3
- package/dist/commands/answers/agents.d.ts +17 -7
- package/dist/commands/answers/agents.js +46 -16
- package/dist/commands/answers/clock.js +2 -1
- package/dist/commands/answers/drawings.js +6 -2
- package/dist/commands/answers/index.d.ts +4 -3
- package/dist/commands/answers/index.js +4 -3
- package/dist/commands/answers/lines.d.ts +5 -3
- package/dist/commands/answers/lines.js +17 -9
- package/dist/commands/answers/opening-book.d.ts +13 -7
- package/dist/commands/answers/opening-book.js +34 -38
- package/dist/commands/answers/opening-tags.d.ts +4 -0
- package/dist/commands/answers/opening-tags.js +11 -0
- package/dist/commands/answers/play-move.d.ts +3 -1
- package/dist/commands/answers/play-move.js +9 -6
- package/dist/commands/answers/say.js +2 -1
- package/dist/commands/answers/summary.d.ts +7 -3
- package/dist/commands/answers/summary.js +16 -5
- package/dist/commands/answers/view.d.ts +1 -1
- package/dist/commands/answers/view.js +2 -2
- package/dist/commands/game/ai-move.js +4 -4
- package/dist/commands/game/coach.js +7 -6
- package/dist/commands/game/end.js +2 -2
- package/dist/commands/game/export.d.ts +1 -1
- package/dist/commands/game/index.d.ts +1 -1
- package/dist/commands/game/index.js +1 -1
- package/dist/commands/game/look.d.ts +7 -3
- package/dist/commands/game/look.js +9 -4
- package/dist/commands/game/new.js +42 -12
- package/dist/commands/game/player-move.d.ts +16 -0
- package/dist/commands/game/player-move.js +69 -19
- package/dist/commands/game/ponder.js +7 -4
- package/dist/commands/game/position.d.ts +2 -1
- package/dist/commands/game/position.js +9 -4
- package/dist/commands/game/status.d.ts +4 -2
- package/dist/commands/game/status.js +24 -14
- package/dist/commands/index.d.ts +3 -3
- package/dist/commands/index.js +3 -3
- package/dist/commands/profiles/past-games.d.ts +7 -2
- package/dist/commands/profiles/past-games.js +45 -18
- package/dist/commands/registry/context.d.ts +2 -0
- package/dist/commands/registry/context.js +5 -0
- package/dist/commands/registry/index.d.ts +1 -1
- package/dist/commands/registry/index.js +1 -1
- package/dist/commands/talk/ai-say.d.ts +1 -1
- package/dist/commands/talk/ai-say.js +7 -6
- package/dist/core/game.d.ts +13 -0
- package/dist/core/game.js +29 -3
- package/dist/core/index.d.ts +4 -4
- package/dist/core/index.js +3 -3
- package/dist/core/openings.d.ts +20 -6
- package/dist/core/openings.js +45 -22
- package/dist/core/pgn.d.ts +4 -0
- package/dist/core/pgn.js +35 -6
- package/dist/games/analysis.d.ts +7 -4
- package/dist/games/analysis.js +5 -3
- package/dist/games/archive.d.ts +25 -5
- package/dist/games/archive.js +52 -19
- package/dist/games/chat-start.d.ts +2 -2
- package/dist/games/chat-start.js +6 -1
- package/dist/games/current-game.d.ts +2 -2
- package/dist/games/current-game.js +3 -2
- package/dist/games/index.d.ts +3 -2
- package/dist/games/index.js +3 -2
- package/dist/games/position-shown.d.ts +24 -0
- package/dist/games/position-shown.js +51 -0
- package/dist/games/thoughts.d.ts +12 -4
- package/dist/games/thoughts.js +27 -10
- package/dist/home/index.d.ts +1 -0
- package/dist/home/index.js +1 -0
- package/dist/home/same-path.d.ts +7 -0
- package/dist/home/same-path.js +24 -0
- package/dist/host-files/host-files.d.ts +5 -0
- package/dist/host-files/host-files.js +21 -7
- package/dist/host-files/index.d.ts +1 -1
- package/dist/host-files/index.js +1 -1
- package/dist/page/app/anchor.js +15 -11
- package/dist/page/app/app.js +49 -14
- package/dist/page/app/served.js +30 -38
- package/dist/page/app/watch.js +97 -0
- package/dist/page/avatars/torretta.css +108 -0
- package/dist/page/base.css +1 -1
- package/dist/page/board/board.css +11 -8
- package/dist/page/board/marks.js +2 -2
- package/dist/page/board/pointer.js +2 -2
- package/dist/page/dialogs/analyses.js +5 -3
- package/dist/page/dialogs/dialogs.css +3 -1
- package/dist/page/dialogs/rows.js +72 -0
- package/dist/page/dialogs/site-games.js +3 -1
- package/dist/page/game/column.js +10 -8
- package/dist/page/game/game.css +137 -16
- package/dist/page/game/move-list.js +7 -5
- package/dist/page/game/plies.js +4 -0
- package/dist/page/game/review-progress.js +11 -0
- package/dist/page/guide/drawings/drawings.css +0 -1
- package/dist/page/guide/en.md +2 -2
- package/dist/page/guide/it.md +2 -2
- package/dist/page/helps/helps.css +5 -5
- package/dist/page/helps/helps.js +3 -181
- package/dist/page/helps/rows.js +16 -0
- package/dist/page/helps/strategy.js +105 -0
- package/dist/page/helps/tactics.js +69 -0
- package/dist/page/layout/header.js +49 -6
- package/dist/page/layout/icon.ico +0 -0
- package/dist/page/layout/layout.css +49 -17
- package/dist/page/layout/window.js +45 -6
- package/dist/page/lead/lead.js +41 -4
- package/dist/page/legend/legend.css +4 -16
- package/dist/page/legend/legend.js +28 -15
- package/dist/page/profile/settings.js +1 -0
- package/dist/page/sounds/sounds.js +1 -1
- package/dist/page/style.css +1 -0
- package/dist/page/table/action.js +42 -0
- package/dist/page/table/table.css +57 -39
- package/dist/page/table/window.js +67 -67
- package/dist/page/talk/talk.css +52 -118
- package/dist/page/talk/talk.js +65 -20
- package/dist/page/talk/voice.js +92 -21
- package/dist/page/text.js +34 -8
- package/dist/page/theme.css +13 -0
- package/dist/profiles/profiles.js +5 -1
- package/dist/server/bell/bell.d.ts +24 -29
- package/dist/server/bell/bell.js +79 -133
- package/dist/server/bell/index.d.ts +1 -1
- package/dist/server/bell/index.js +2 -2
- package/dist/server/mcp/mcp.js +49 -7
- package/dist/server/page/analyses.js +15 -4
- package/dist/server/page/chat.d.ts +5 -1
- package/dist/server/page/chat.js +5 -1
- package/dist/server/page/files.js +3 -1
- package/dist/server/page/game.d.ts +5 -0
- package/dist/server/page/game.js +95 -15
- package/dist/server/page/http.d.ts +1 -1
- package/dist/server/page/page.d.ts +14 -9
- package/dist/server/page/page.js +111 -26
- package/dist/server/page/table.d.ts +40 -5
- package/dist/server/page/table.js +65 -25
- package/dist/server/page/update.d.ts +1 -1
- package/dist/server/page/update.js +4 -4
- package/dist/server/page/windows.d.ts +22 -0
- package/dist/server/page/windows.js +45 -0
- package/dist/server/ports/index.d.ts +1 -0
- package/dist/server/ports/index.js +2 -0
- package/dist/server/ports/ports.d.ts +78 -0
- package/dist/server/ports/ports.js +222 -0
- package/dist/server/table/browser.d.ts +22 -6
- package/dist/server/table/browser.js +98 -20
- package/dist/server/table/hosts/chats.d.ts +5 -0
- package/dist/server/table/hosts/chats.js +11 -4
- package/dist/server/table/hosts/hooks.d.ts +6 -0
- package/dist/server/table/hosts/hooks.js +38 -0
- package/dist/server/table/hosts/hosts.d.ts +13 -5
- package/dist/server/table/hosts/hosts.js +16 -10
- package/dist/server/table/hosts/index.d.ts +1 -0
- package/dist/server/table/hosts/index.js +1 -0
- package/dist/server/table/hosts/play.d.ts +7 -3
- package/dist/server/table/hosts/play.js +63 -9
- package/dist/server/table/icon/index.d.ts +2 -0
- package/dist/server/table/icon/index.js +16 -2
- package/dist/server/table/index.d.ts +2 -2
- package/dist/server/table/index.js +1 -1
- package/dist/server/table/launch.d.ts +5 -4
- package/dist/server/table/launch.js +18 -5
- package/dist/server/table/link.js +4 -1
- package/dist/server/table/table.d.ts +7 -0
- package/dist/server/table/table.js +40 -12
- package/dist/sites/index.d.ts +1 -1
- package/dist/sites/index.js +1 -1
- package/dist/sites/{chesscom.d.ts → ratings/chesscom.d.ts} +2 -2
- package/dist/sites/{chesscom.js → ratings/chesscom.js} +1 -1
- package/dist/sites/{choice.d.ts → ratings/choice.d.ts} +1 -1
- package/dist/sites/{choice.js → ratings/choice.js} +1 -1
- package/dist/sites/ratings/index.d.ts +1 -0
- package/dist/sites/ratings/index.js +2 -0
- package/dist/sites/{lichess.d.ts → ratings/lichess.d.ts} +2 -2
- package/dist/sites/{lichess.js → ratings/lichess.js} +1 -1
- package/dist/sites/{sites.d.ts → ratings/ratings.d.ts} +2 -2
- package/dist/sites/{sites.js → ratings/ratings.js} +1 -1
- package/instructions/opponent.md +1 -1
- package/instructions/start.md +1 -1
- package/package.json +1 -1
package/dist/server/bell/bell.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
// The doorbell for the page (D-47): `chesspal listen`, the Stop hook init writes for Claude Code and Codex, waits at the page of its chat and, when
|
|
2
2
|
// the player moves or writes there, tells the chat to call status. It never carries the move, the words or the position: status reads them fresh.
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { writeAtomically } from "../../games/index.js";
|
|
8
|
-
import { dataFolder, tableFolder } from "../../home/index.js";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { positionShownNote, writeAtomically } from "../../games/index.js";
|
|
6
|
+
import { samePath } from "../../home/index.js";
|
|
9
7
|
import { analystAgent } from "../../host-files/index.js";
|
|
10
|
-
import {
|
|
8
|
+
import { askPage, chatFolder, findChatPage, notedFolder, notedPagePorts, pageNote, probeOf, probeWait } from "../ports/index.js";
|
|
11
9
|
/** What the chat hears from the doorbell: one line for whatever the player did, never what, only where to read it. */
|
|
12
10
|
export const bellLine = "The player acted on the board page: call status to see it.";
|
|
13
11
|
/**
|
|
@@ -17,10 +15,13 @@ export const bellLine = "The player acted on the board page: call status to see
|
|
|
17
15
|
export class Bell {
|
|
18
16
|
listener;
|
|
19
17
|
pending = false;
|
|
20
|
-
// The chat is in a turn: it called a tool, or a hook woke it, and its turn has not ended yet. A chat never started,
|
|
18
|
+
// The chat is in a turn: the player's message started it, it called a tool, or a hook woke it, and its turn has not ended yet. A chat never started,
|
|
19
|
+
// or just cleared, is not.
|
|
21
20
|
busy = false;
|
|
22
21
|
// The doorbell woke the chat in this turn: the player acted on the page, and the chat goes to see it.
|
|
23
22
|
woken = false;
|
|
23
|
+
// A hook has waited for the bell since the chat came: its host runs Chesspal's hooks, and only they tell where a turn ends.
|
|
24
|
+
hooked = false;
|
|
24
25
|
news;
|
|
25
26
|
changed;
|
|
26
27
|
/**
|
|
@@ -50,10 +51,31 @@ export class Bell {
|
|
|
50
51
|
this.tellIf(asleep);
|
|
51
52
|
return asleep;
|
|
52
53
|
}
|
|
53
|
-
/**
|
|
54
|
-
|
|
54
|
+
/**
|
|
55
|
+
* The player's message, or one the host sent for the doorbell or an agent, starts the chat's turn, as the prompt hook says: the hook of the turn
|
|
56
|
+
* before is let go without a ring, and what the player does on the page meanwhile waits for the turn's end, as during a tool. A chat nobody heard
|
|
57
|
+
* stays unheard until its first tool, which starts the AI's clock, held till then: the message moves no clock.
|
|
58
|
+
*/
|
|
59
|
+
chatPrompted() {
|
|
60
|
+
if (this.listener === undefined) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.listener.end("dismissed");
|
|
64
|
+
this.listener = undefined;
|
|
65
|
+
this.busy = true;
|
|
66
|
+
this.changed();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The player acted on the page: the hook waiting hears it now, or the next one as soon as it comes. In a host that runs no hook nothing tells the end of
|
|
70
|
+
* a turn: an act the chat waits for finds it out of its turn, and nobody hears the player, unless the chat `mayBeAtWork` on the AI's move meanwhile.
|
|
71
|
+
*/
|
|
72
|
+
ring(mayBeAtWork = false) {
|
|
55
73
|
if (this.listener === undefined) {
|
|
56
74
|
this.pending = true;
|
|
75
|
+
if (!this.hooked && !mayBeAtWork && this.busy) {
|
|
76
|
+
this.busy = false;
|
|
77
|
+
this.changed();
|
|
78
|
+
}
|
|
57
79
|
return;
|
|
58
80
|
}
|
|
59
81
|
this.listener.end("ring");
|
|
@@ -62,10 +84,14 @@ export class Bell {
|
|
|
62
84
|
this.woken = true;
|
|
63
85
|
this.changed();
|
|
64
86
|
}
|
|
65
|
-
/**
|
|
66
|
-
|
|
87
|
+
/**
|
|
88
|
+
* A hook waits for the bell, so the chat's turn has ended; the hook before it, of a turn already ended, is let go. A ring no hook heard is dropped when
|
|
89
|
+
* the chat is `told` all of it otherwise: by the player's message that came with the move the prompt hook played.
|
|
90
|
+
*/
|
|
91
|
+
listen(request, response, told = false) {
|
|
67
92
|
response.writeHead(200, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
|
|
68
|
-
|
|
93
|
+
this.hooked = true;
|
|
94
|
+
const missed = this.pending && !told;
|
|
69
95
|
this.pending = false;
|
|
70
96
|
if (missed && this.news()) {
|
|
71
97
|
response.end("ring");
|
|
@@ -89,6 +115,7 @@ export class Bell {
|
|
|
89
115
|
}
|
|
90
116
|
/** The chat left the table (D-104): its process ended, and nobody is in a turn or waits for the bell; a ring no hook heard is no longer news for anyone. */
|
|
91
117
|
chatLeft() {
|
|
118
|
+
this.hooked = false;
|
|
92
119
|
this.busy = false;
|
|
93
120
|
this.woken = false;
|
|
94
121
|
this.pending = false;
|
|
@@ -103,21 +130,27 @@ export class Bell {
|
|
|
103
130
|
}
|
|
104
131
|
}
|
|
105
132
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
133
|
+
* Notes in the data folder the port a chat's page took off its folder's usual ports, one file per page, for the hook to find it there; then forgets, off
|
|
134
|
+
* the caller's clock, the notes of its folder whose page is gone: nobody listens at the port, or another folder's page does. A page that answers late
|
|
135
|
+
* is busy, not gone, and keeps its note. It throws when the note cannot be written.
|
|
108
136
|
*/
|
|
109
|
-
export function
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export async function pageAt(port) {
|
|
115
|
-
return probeOf(await body(port, "/api/bell?probe", probeWait));
|
|
137
|
+
export function notePagePort(home, folder, port, wait = probeWait) {
|
|
138
|
+
const note = pageNote(home, port);
|
|
139
|
+
mkdirSync(dirname(note), { recursive: true });
|
|
140
|
+
writeAtomically(note, JSON.stringify({ folder: resolve(folder) }));
|
|
141
|
+
return forgetGonePages(home, folder, port, wait);
|
|
116
142
|
}
|
|
117
|
-
/**
|
|
118
|
-
export function
|
|
119
|
-
const
|
|
120
|
-
|
|
143
|
+
/** Forgets the note of a page that closes, unless another folder's page took its port since; a note that cannot go is left for the next page to clear. */
|
|
144
|
+
export function forgetPagePort(home, folder, port) {
|
|
145
|
+
const noted = notedFolder(home, port);
|
|
146
|
+
if (noted !== undefined && samePath(noted, folder)) {
|
|
147
|
+
try {
|
|
148
|
+
rmSync(pageNote(home, port), { force: true });
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// The next page of the folder clears it.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
121
154
|
}
|
|
122
155
|
/** What Codex's Stop hook answers to wake the chat: its turn goes on with the doorbell's line. */
|
|
123
156
|
export const codexBellAnswer = JSON.stringify({ decision: "block", reason: bellLine });
|
|
@@ -159,68 +192,32 @@ export async function waitForBell(input, ports) {
|
|
|
159
192
|
*/
|
|
160
193
|
export async function bellHeard(input, ports) {
|
|
161
194
|
const hook = hookInput(input);
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
const [probes, table] = await Promise.all([Promise.all(tried.map(async (port) => ({ port, said: await pageAt(port) }))), ports === undefined ? tableOf(folder, hook.session) : undefined]);
|
|
165
|
-
const pages = probes.flatMap(({ port, said }) => (said !== undefined && samePath(said.folder, folder) ? [{ port, said }] : []));
|
|
166
|
-
// A chat opened in the table's own folder finds the table among the pages of its folder already.
|
|
167
|
-
const ours = table === undefined || pages.some(({ port }) => port === table.port) ? pages : [...pages, table];
|
|
168
|
-
const port = chatPage(ours, hook.session)?.port;
|
|
195
|
+
const found = await findChatPage(hook, ports);
|
|
196
|
+
const { folder, port } = found;
|
|
169
197
|
if (port === undefined) {
|
|
170
|
-
|
|
171
|
-
const open = ours.filter(({ said }) => said.open).length;
|
|
172
|
-
const pages = ours.length > 1 ? `${ours.length} pages of it, none of this chat's session, ${open} open in a browser` : "no page of it";
|
|
173
|
-
return { folder, rang: false, why: `${pages} at ports ${tried.join(", ")}${others.length > 0 ? `; ${others.join(", ")}` : ""}` };
|
|
198
|
+
return { folder, rang: false, why: found.why };
|
|
174
199
|
}
|
|
175
|
-
// The page lets the analyst go as the hook starts waiting, when none runs any more
|
|
176
|
-
|
|
200
|
+
// The page lets the analyst go as the hook starts waiting, when none runs any more; and it rings for nothing older than the move the prompt hook played
|
|
201
|
+
// for this chat, which hears it from the player's message itself.
|
|
202
|
+
const shown = hook.session === undefined ? undefined : positionShownNote(folder);
|
|
203
|
+
const told = shown !== undefined && shown.session === hook.session ? shown.stamp : undefined;
|
|
204
|
+
const query = new URLSearchParams({ ...(hook.analystGone === true ? { analyst: "gone" } : {}), ...(told === undefined ? {} : { shown: told }) });
|
|
205
|
+
const said = (await askPage(port, `/api/bell${query.size === 0 ? "" : `?${query}`}`)).text;
|
|
177
206
|
return said === "ring" ? { folder, rang: true } : { folder, rang: false, why: said === undefined ? `the page at port ${port} did not answer` : `the page at port ${port} said "${said}"` };
|
|
178
207
|
}
|
|
179
|
-
// How long a port may take to say whose page it is: one of this computer answers at once.
|
|
180
|
-
const probeWait = 1000;
|
|
181
208
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
209
|
+
* Forgets the notes of a folder's pages that are gone, but the one at `kept`: a page closed without forgetting its own, when nobody listens at its port or
|
|
210
|
+
* another folder's page took it. A page that does not answer in time, or not as a page, may only be busy, and its note stays.
|
|
184
211
|
*/
|
|
185
|
-
async function
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return probes.find((one) => one.said?.table === true && ((one.said.chat !== undefined && samePath(one.said.chat, folder)) || (session !== undefined && one.said.session === session)));
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* The data folder the chat's server is given in its folder's settings, as CHESSPAL_HOME in a playtest: the hook runs in the host's environment, not the
|
|
198
|
-
* server's (Federico). Claude Code's .mcp.json first, then Codex's config.
|
|
199
|
-
*/
|
|
200
|
-
function serverHome(folder) {
|
|
201
|
-
const chosen = [claudeHome(folder), codexHome(folder)].find((one) => one !== undefined && one.trim() !== "");
|
|
202
|
-
return chosen === undefined ? undefined : resolve(chosen.trim());
|
|
203
|
-
}
|
|
204
|
-
function claudeHome(folder) {
|
|
205
|
-
try {
|
|
206
|
-
const config = JSON.parse(readFileSync(join(folder, ".mcp.json"), "utf8"));
|
|
207
|
-
const home = config.mcpServers?.chesspal?.env?.CHESSPAL_HOME;
|
|
208
|
-
return typeof home === "string" ? home : undefined;
|
|
209
|
-
}
|
|
210
|
-
catch {
|
|
211
|
-
return undefined;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
// The setting as init keeps it in Codex's table: a basic string, whose escapes are JSON's for a path, or a literal one.
|
|
215
|
-
function codexHome(folder) {
|
|
216
|
-
try {
|
|
217
|
-
const config = readFileSync(join(folder, ".codex", "config.toml"), "utf8");
|
|
218
|
-
const [, basic, literal] = /\bCHESSPAL_HOME\s*=\s*(?:("(?:[^"\\]|\\.)*")|'([^']*)')/.exec(config) ?? [];
|
|
219
|
-
return basic === undefined ? literal : JSON.parse(basic);
|
|
220
|
-
}
|
|
221
|
-
catch {
|
|
222
|
-
return undefined;
|
|
223
|
-
}
|
|
212
|
+
async function forgetGonePages(home, folder, kept, wait) {
|
|
213
|
+
const others = notedPagePorts(home, folder).filter((port) => port !== kept);
|
|
214
|
+
await Promise.all(others.map(async (port) => {
|
|
215
|
+
const { refused, text } = await askPage(port, "/api/bell?probe", wait);
|
|
216
|
+
const said = probeOf(text);
|
|
217
|
+
if (refused || (said !== undefined && !samePath(said.folder, folder))) {
|
|
218
|
+
forgetPagePort(home, folder, port);
|
|
219
|
+
}
|
|
220
|
+
}));
|
|
224
221
|
}
|
|
225
222
|
// The log of Codex's hooks that heard no bell, in the chat's folder: its last lines only.
|
|
226
223
|
const silenceLog = "listen.log";
|
|
@@ -236,10 +233,6 @@ export function noteSilence(folder, why, now = new Date()) {
|
|
|
236
233
|
// The log only explains; the hook ends the same.
|
|
237
234
|
}
|
|
238
235
|
}
|
|
239
|
-
/** The chat's folder, where its server started: Claude Code names it, and the hook's cwd follows the cd's of the chat. */
|
|
240
|
-
function chatFolder(hook) {
|
|
241
|
-
return resolve(process.env.CLAUDE_PROJECT_DIR ?? hook.cwd ?? process.cwd());
|
|
242
|
-
}
|
|
243
236
|
function hookInput(input) {
|
|
244
237
|
try {
|
|
245
238
|
const parsed = JSON.parse(input);
|
|
@@ -263,50 +256,3 @@ function analystGone(tasks) {
|
|
|
263
256
|
return agent === analystAgent && status === "running";
|
|
264
257
|
});
|
|
265
258
|
}
|
|
266
|
-
/**
|
|
267
|
-
* The chat's page among those of its folder: the one of its session; else the only one, or the only one a browser has open, where the player plays. Codex
|
|
268
|
-
* gives the page no session, and a chat closed may leave its page behind for a while.
|
|
269
|
-
*/
|
|
270
|
-
function chatPage(pages, session) {
|
|
271
|
-
const open = pages.filter(({ said }) => said.open);
|
|
272
|
-
return pages.find(({ said }) => session !== undefined && said.session === session) ?? (pages.length === 1 ? pages[0] : open.length === 1 ? open[0] : undefined);
|
|
273
|
-
}
|
|
274
|
-
function probeOf(said) {
|
|
275
|
-
try {
|
|
276
|
-
const parsed = said === undefined ? undefined : JSON.parse(said);
|
|
277
|
-
return parsed === undefined || typeof parsed.folder !== "string" ? undefined : {
|
|
278
|
-
session: typeof parsed.session === "string" ? parsed.session : null,
|
|
279
|
-
folder: parsed.folder,
|
|
280
|
-
open: parsed.open === true,
|
|
281
|
-
...(typeof parsed.version === "string" ? { version: parsed.version } : {}),
|
|
282
|
-
...(parsed.table === true ? { table: true } : {}),
|
|
283
|
-
...(typeof parsed.chat === "string" ? { chat: parsed.chat } : {}),
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
catch {
|
|
287
|
-
return undefined;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
/** Whether two paths name the same folder: without regard to case on Windows. */
|
|
291
|
-
export function samePath(one, other) {
|
|
292
|
-
return process.platform === "win32" ? resolve(one).toLowerCase() === resolve(other).toLowerCase() : resolve(one) === resolve(other);
|
|
293
|
-
}
|
|
294
|
-
/** The body a page of this computer answers with, once it ends; undefined when it does not answer, or not with 200. */
|
|
295
|
-
function body(port, path, wait) {
|
|
296
|
-
return new Promise((done) => {
|
|
297
|
-
// A connection of its own: Node's shared one ends a request idle for five seconds, and the doorbell waits for hours.
|
|
298
|
-
const request = get({ host: "127.0.0.1", port, path, agent: false, ...(wait === undefined ? {} : { timeout: wait }) }, (response) => {
|
|
299
|
-
let text = "";
|
|
300
|
-
response.setEncoding("utf8");
|
|
301
|
-
response.on("data", (chunk) => {
|
|
302
|
-
text += chunk;
|
|
303
|
-
});
|
|
304
|
-
response.on("end", () => done(response.statusCode === 200 ? text : undefined));
|
|
305
|
-
response.on("error", () => done(undefined));
|
|
306
|
-
});
|
|
307
|
-
if (wait !== undefined) {
|
|
308
|
-
request.on("timeout", () => request.destroy());
|
|
309
|
-
}
|
|
310
|
-
request.on("error", () => done(undefined));
|
|
311
|
-
});
|
|
312
|
-
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Bell,
|
|
1
|
+
export { Bell, forgetPagePort, listen, notePagePort } from "./bell.ts";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
// The doorbell that wakes a chat when the player acts on the page (D-47), and the ports
|
|
2
|
-
export { Bell,
|
|
1
|
+
// The doorbell that wakes a chat when the player acts on the page (D-47), and the notes of the ports a folder's page took off its usual ones.
|
|
2
|
+
export { Bell, forgetPagePort, listen, notePagePort } from "./bell.js";
|
package/dist/server/mcp/mcp.js
CHANGED
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { aiMove, commands, defaultContext, inputProblems, isBoolean, PositionChanged } from "../../commands/index.js";
|
|
6
|
+
import { aiMove, commands, defaultContext, homeForOneCall, inputProblems, isBoolean, PositionChanged, positionIn, samePosition } from "../../commands/index.js";
|
|
7
7
|
import { aiNameOf } from "../../core/index.js";
|
|
8
|
-
import { endOf, loadGame, noteAiHost, noteAiModel, rerunIfChanged } from "../../games/index.js";
|
|
8
|
+
import { endOf, forgetPositionShown, hostProcess, lastChatStart, loadGame, noteAiHost, noteAiModel, positionShownNote, rerunIfChanged } from "../../games/index.js";
|
|
9
9
|
import { agentTools, hasAgent, startInstructions } from "../../host-files/index.js";
|
|
10
10
|
import { gameFolder } from "../../profiles/index.js";
|
|
11
11
|
import { version } from "../../version.js";
|
|
12
|
-
import { pagePorts } from "../bell/index.js";
|
|
13
12
|
import { chatHasRead, startPage } from "../page/index.js";
|
|
14
13
|
import { attachTable, tableToJoin } from "../table/index.js";
|
|
15
14
|
import { allowedCaller, callerOf, callingAgent, fromCodexAgent } from "./caller.js";
|
|
@@ -108,8 +107,8 @@ async function pageOf(context, opponent, again) {
|
|
|
108
107
|
context.toolEnded = table === undefined ? lookForTable(context, again) : page.toolEnded;
|
|
109
108
|
context.pageOpen = page.open;
|
|
110
109
|
context.askNewGame = page.askNewGame;
|
|
111
|
-
if (
|
|
112
|
-
process.stderr.write(`The board page is at ${page.url}, off its usual ports: the doorbell cannot find it, so moves on the page do not wake the chat.\n`);
|
|
110
|
+
if (!page.findable) {
|
|
111
|
+
process.stderr.write(`The board page is at ${page.url}, off its usual ports, and its port could not be noted in the data folder: the doorbell cannot find it, so moves on the page do not wake the chat.\n`);
|
|
113
112
|
}
|
|
114
113
|
return page;
|
|
115
114
|
}
|
|
@@ -203,15 +202,17 @@ async function runTool(command, input, context, caller, meta) {
|
|
|
203
202
|
const calling = await sharingCaller(command, meta, context);
|
|
204
203
|
const agent = calling !== undefined && "agent" in calling ? calling.agent : undefined;
|
|
205
204
|
const chat = command.byAgents === undefined && agent === undefined;
|
|
205
|
+
const home = homeForOneCall(context);
|
|
206
206
|
let refused = false;
|
|
207
207
|
try {
|
|
208
208
|
if (chat) {
|
|
209
|
+
takePositionShown(context, home);
|
|
209
210
|
await context.calls?.();
|
|
210
211
|
}
|
|
211
212
|
const run = calling !== undefined && "run" in calling ? calling.run : undefined;
|
|
212
213
|
const who = agent !== undefined ? { agent, ...(run === undefined ? {} : { agentRun: run }) } : calling !== undefined ? { chat: true } : {};
|
|
213
214
|
// A game another process changed under the call is read again, and the call made once more.
|
|
214
|
-
const result = await rerunIfChanged(() => command.run(checkedInput(command, input), { ...context, caller, ...who }));
|
|
215
|
+
const result = await rerunIfChanged(() => command.run(checkedInput(command, input), { ...context, home, caller, ...who }));
|
|
215
216
|
return { content: [{ type: "text", text: command.format(result, context) }] };
|
|
216
217
|
}
|
|
217
218
|
catch (error) {
|
|
@@ -222,10 +223,51 @@ async function runTool(command, input, context, caller, meta) {
|
|
|
222
223
|
// 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,
|
|
223
224
|
// unless that tool was refused for a position it had not seen. An agent's call is not the chat's.
|
|
224
225
|
if (chat) {
|
|
225
|
-
chatHasRead(context, command.showsGame === true && !refused);
|
|
226
|
+
chatHasRead(context, command.showsGame === true && !refused, home);
|
|
226
227
|
}
|
|
227
228
|
}
|
|
228
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* The position the prompt hook showed the chat with the move the player wrote alone (D-71), taken before the chat's tool runs as if a tool had shown it,
|
|
232
|
+
* while the game is still there: the AI moves on it without a status, and the player's words the hook gave count as given, which the page hears. A note
|
|
233
|
+
* on another position goes; one of another chat's stays for it.
|
|
234
|
+
*/
|
|
235
|
+
function takePositionShown(context, home) {
|
|
236
|
+
const shown = positionShownNote(context.folder);
|
|
237
|
+
if (shown === undefined) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
let current;
|
|
241
|
+
try {
|
|
242
|
+
current = samePosition(shown, positionIn(gameFolder(home())));
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// A game that cannot be read is the tool's to report.
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (current && !chatsNote(shown, context.folder)) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
forgetPositionShown(context.folder);
|
|
252
|
+
if (!current) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
shown.given.forEach((key) => context.given.add(key));
|
|
256
|
+
// The position is new to the chat, a move later than any it saw: the page hears of it, and of the words read with it.
|
|
257
|
+
chatHasRead(context, true, home);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Whether a note of the hooks is this server's chat's: by the Claude Code process, which names itself alike to the server and its hooks, when both have its
|
|
261
|
+
* name. Else by the session: the server's is the one Claude Code started it with, and the hooks get a new one after /clear, which the chat's start notes;
|
|
262
|
+
* a chat started or resumed since in the folder is taken for another Claude Code.
|
|
263
|
+
*/
|
|
264
|
+
function chatsNote({ process: noted, session }, folder, own = hostProcess(), ownSession = process.env.CLAUDE_CODE_SESSION_ID) {
|
|
265
|
+
if (noted !== undefined && own !== undefined) {
|
|
266
|
+
return noted === own;
|
|
267
|
+
}
|
|
268
|
+
const start = lastChatStart(folder);
|
|
269
|
+
return session === ownSession || (start?.session === session && start.source !== "startup" && start.source !== "resume");
|
|
270
|
+
}
|
|
229
271
|
/**
|
|
230
272
|
* Who calls a tool the chat shares with an agent, as the analyst (D-89): the chat, or the agent by its type; looked up only in a folder where one may run
|
|
231
273
|
* and while no game goes on, so that no call of the chat's waits for it while it plays. None when it cannot be told, or for an agent the tool is not shared with.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { bookOpening, deleteGame, deleteReview, siteGames, SiteGamesRefused } from "../../commands/index.js";
|
|
2
2
|
import { aiModelIn, annotatorOf, isReviewed, leadOf, reviewOf } from "../../core/index.js";
|
|
3
|
-
import { archivedGame, archivedGames, gameInAnalysis, gameStemOf, keptGame, keptGames } from "../../games/index.js";
|
|
3
|
+
import { archivedGame, archivedGames, archivedGameUnplayed, gameInAnalysis, gameStemOf, hasListTags, keptGame, keptGames } from "../../games/index.js";
|
|
4
4
|
import { gameFolder } from "../../profiles/index.js";
|
|
5
5
|
import { bodyOf, isJson, jsonOf, runRoute, sendJson, textOf } from "./http.js";
|
|
6
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. */
|
|
@@ -10,7 +10,7 @@ export function analysesOf(home) {
|
|
|
10
10
|
const stamp = (name) => gameStemOf(name) ?? "";
|
|
11
11
|
const names = [...keptGames(folder), ...archivedGames(folder)].sort((one, other) => stamp(other).localeCompare(stamp(one)));
|
|
12
12
|
const taken = gameInAnalysis(folder)?.analyst?.game;
|
|
13
|
-
return names.flatMap((name) => itemOf(name, () => name.startsWith("study/") ? keptGame(folder, name) :
|
|
13
|
+
return names.flatMap((name) => itemOf(name, () => name.startsWith("study/") ? withBookOpening(keptGame(folder, name)) : archivedListed(folder, name), taken === name));
|
|
14
14
|
}
|
|
15
15
|
/** 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. */
|
|
16
16
|
export function answerAnalyses(request, response, url, served) {
|
|
@@ -51,10 +51,22 @@ export function sendSiteGames(response, served) {
|
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
53
|
const deletions = { "/api/delete-game": deleteGame, "/api/delete-review": deleteReview };
|
|
54
|
+
/**
|
|
55
|
+
* A game of the archive for the list: its moves taken as written, not played, when the archive wrote its PlyCount, with its opening's tags, none for a game
|
|
56
|
+
* that has none; a game an older Chesspal archived is read whole, its opening looked up.
|
|
57
|
+
*/
|
|
58
|
+
function archivedListed(folder, name) {
|
|
59
|
+
const unplayed = archivedGameUnplayed(folder, name);
|
|
60
|
+
return hasListTags(unplayed.tags) ? { record: unplayed, opening: unplayed.tags.Opening } : withBookOpening(archivedGame(folder, name));
|
|
61
|
+
}
|
|
62
|
+
function withBookOpening(record) {
|
|
63
|
+
return { record, opening: bookOpening(record)?.name };
|
|
64
|
+
}
|
|
54
65
|
function itemOf(name, read, taken) {
|
|
55
66
|
let record;
|
|
67
|
+
let opening;
|
|
56
68
|
try {
|
|
57
|
-
record = read();
|
|
69
|
+
({ record, opening } = read());
|
|
58
70
|
}
|
|
59
71
|
catch {
|
|
60
72
|
// A game that cannot be read stays out of the list.
|
|
@@ -66,7 +78,6 @@ function itemOf(name, read, taken) {
|
|
|
66
78
|
}
|
|
67
79
|
const { tags } = record;
|
|
68
80
|
const date = tags.Date === undefined || tags.Date.includes("?") ? undefined : tags.Date;
|
|
69
|
-
const opening = bookOpening(record)?.name;
|
|
70
81
|
const aiModel = aiModelIn(record);
|
|
71
82
|
const site = tags.Site === undefined || /^[?\-\s]*$/.test(tags.Site) ? undefined : tags.Site;
|
|
72
83
|
const signed = annotatorOf(record);
|
|
@@ -20,13 +20,17 @@ export declare function chatSeenOf(chat: Context): ChatSeen;
|
|
|
20
20
|
export declare function pageContext(chat: Context): Context;
|
|
21
21
|
/** The context of a new game started on the page: the page's, with what the chat saw and its host, by which the game before asks for a clean chat (D-57). */
|
|
22
22
|
export declare function asTheChatSees(context: Context, chat: ChatSeen): Context;
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* What a chat says of itself as it attaches to the table (D-104): its version, its session, its folder, its host, whether its folder needs init again,
|
|
25
|
+
* and the seat of the `chesspal play` that started it, if one did.
|
|
26
|
+
*/
|
|
24
27
|
export declare const chatAttach: z.ZodObject<{
|
|
25
28
|
version: z.ZodString;
|
|
26
29
|
session: z.ZodNullable<z.ZodString>;
|
|
27
30
|
folder: z.ZodString;
|
|
28
31
|
host: z.ZodOptional<z.ZodString>;
|
|
29
32
|
outdated: z.ZodBoolean;
|
|
33
|
+
seat: z.ZodOptional<z.ZodInt>;
|
|
30
34
|
}, z.core.$strip>;
|
|
31
35
|
export type ChatAttach = z.infer<typeof chatAttach>;
|
|
32
36
|
/**
|
package/dist/server/page/chat.js
CHANGED
|
@@ -42,13 +42,17 @@ export function pageContext(chat) {
|
|
|
42
42
|
export function asTheChatSees(context, chat) {
|
|
43
43
|
return { ...context, host: () => chat.host(), positionSeen: chat.positionSeen };
|
|
44
44
|
}
|
|
45
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* What a chat says of itself as it attaches to the table (D-104): its version, its session, its folder, its host, whether its folder needs init again,
|
|
47
|
+
* and the seat of the `chesspal play` that started it, if one did.
|
|
48
|
+
*/
|
|
46
49
|
export const chatAttach = z.object({
|
|
47
50
|
version: z.string(),
|
|
48
51
|
session: z.string().nullable(),
|
|
49
52
|
folder: z.string(),
|
|
50
53
|
host: z.string().optional(),
|
|
51
54
|
outdated: z.boolean(),
|
|
55
|
+
seat: z.int().optional(),
|
|
52
56
|
});
|
|
53
57
|
const seenPosition = z.object({ started: z.string().optional(), plies: z.int().min(0) });
|
|
54
58
|
/**
|
|
@@ -47,8 +47,10 @@ const policy = `default-src 'self'; script-src 'self' 'sha256-${createHash("sha2
|
|
|
47
47
|
+ "base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
|
|
48
48
|
const html = [
|
|
49
49
|
"<!doctype html>",
|
|
50
|
-
'<html lang="en">',
|
|
50
|
+
'<html lang="en" translate="no">',
|
|
51
51
|
'<meta charset="utf-8">',
|
|
52
|
+
// The page speaks the player's language, set once the app starts, and moves are never translated: no browser offers to.
|
|
53
|
+
'<meta name="google" content="notranslate">',
|
|
52
54
|
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
53
55
|
'<meta name="color-scheme" content="light dark">',
|
|
54
56
|
"<title>Chesspal</title>",
|
|
@@ -4,3 +4,8 @@ import type { Served } from "./page.ts";
|
|
|
4
4
|
export declare function answerGame(request: IncomingMessage, response: ServerResponse, url: URL, served: Served): boolean;
|
|
5
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
6
|
export declare function chatCalls(served: Served): void;
|
|
7
|
+
/**
|
|
8
|
+
* Whether the AI's sign on the page is not the one the pages were last given: the ponder agent's every thought rewrites thoughts.json, and only the sign
|
|
9
|
+
* that comes or goes is news for them. True when the game cannot be read: they read it again.
|
|
10
|
+
*/
|
|
11
|
+
export declare function thinkingChanged(served: Served): boolean;
|