chesspal 0.2.4 → 0.3.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 +11 -12
- package/dist/cli/cli.js +83 -3
- package/dist/cli/command-line.js +26 -9
- package/dist/cli/icon.d.ts +25 -0
- package/dist/cli/icon.js +80 -0
- package/dist/commands/agents.d.ts +19 -2
- package/dist/commands/agents.js +39 -13
- package/dist/commands/ai-say.js +3 -4
- package/dist/commands/analyse-game.d.ts +4 -0
- package/dist/commands/analyse-game.js +46 -13
- package/dist/commands/annotate-game.d.ts +2 -0
- package/dist/commands/annotate-game.js +40 -18
- package/dist/commands/answer-lines.d.ts +2 -0
- package/dist/commands/answer-lines.js +13 -3
- package/dist/commands/coach.js +4 -2
- package/dist/commands/context.d.ts +11 -1
- package/dist/commands/context.js +5 -0
- package/dist/commands/delete-review.js +9 -5
- package/dist/commands/game-summary.d.ts +2 -0
- package/dist/commands/game-summary.js +4 -0
- package/dist/commands/index.d.ts +3 -2
- package/dist/commands/index.js +7 -4
- package/dist/commands/init.js +2 -4
- package/dist/commands/instructions.d.ts +9 -0
- package/dist/commands/instructions.js +30 -0
- package/dist/commands/lead-input.d.ts +1 -1
- package/dist/commands/lead-input.js +7 -4
- package/dist/commands/page-link.js +6 -5
- package/dist/commands/past-games.d.ts +1 -1
- package/dist/commands/past-games.js +12 -5
- package/dist/commands/player-move.js +2 -3
- package/dist/commands/profiles.d.ts +23 -9
- package/dist/commands/profiles.js +76 -35
- package/dist/commands/ratings.d.ts +8 -0
- package/dist/commands/ratings.js +45 -0
- package/dist/commands/registry.d.ts +1 -1
- package/dist/commands/registry.js +3 -2
- package/dist/commands/say.d.ts +1 -1
- package/dist/commands/say.js +1 -1
- package/dist/commands/status.d.ts +3 -1
- package/dist/commands/status.js +46 -27
- package/dist/commands/study.js +8 -5
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/review.d.ts +2 -0
- package/dist/core/review.js +5 -1
- package/dist/games/ai-model.d.ts +4 -2
- package/dist/games/ai-model.js +10 -1
- package/dist/games/analysis.d.ts +26 -3
- package/dist/games/analysis.js +41 -6
- package/dist/games/archive.d.ts +6 -3
- package/dist/games/archive.js +13 -5
- package/dist/games/current-game.d.ts +10 -1
- package/dist/games/current-game.js +49 -15
- package/dist/games/files.d.ts +5 -0
- package/dist/games/files.js +42 -1
- package/dist/games/index.d.ts +3 -3
- package/dist/games/index.js +3 -3
- package/dist/home/data-folder.d.ts +2 -0
- package/dist/home/data-folder.js +5 -1
- package/dist/home/index.d.ts +1 -1
- package/dist/home/index.js +1 -1
- package/dist/host-files/agents.d.ts +2 -2
- package/dist/host-files/agents.js +12 -6
- package/dist/host-files/host-files.d.ts +5 -3
- package/dist/host-files/host-files.js +15 -8
- package/dist/host-files/index.d.ts +2 -2
- package/dist/host-files/index.js +2 -2
- package/dist/page/app/app.js +49 -20
- package/dist/page/app/title.js +17 -0
- package/dist/page/avatars/torretta.js +4 -4
- package/dist/page/dialogs/analyses.js +4 -4
- package/dist/page/dialogs/dialog.js +4 -1
- package/dist/page/dialogs/dialogs.css +84 -10
- package/dist/page/dialogs/profiles.js +96 -43
- package/dist/page/game/column.js +5 -1
- package/dist/page/game/plies.js +7 -4
- package/dist/page/guide/blocks.js +67 -0
- package/dist/page/guide/en.md +66 -0
- package/dist/page/guide/foot.js +18 -0
- package/dist/page/guide/guide.css +200 -0
- package/dist/page/guide/guide.js +89 -0
- package/dist/page/guide/it.md +66 -0
- package/dist/page/guide/markdown.js +67 -0
- package/dist/page/helps/helps.css +7 -10
- package/dist/page/layout/favicon.svg +13 -0
- package/dist/page/layout/header.js +3 -3
- package/dist/page/layout/icon.icns +0 -0
- package/dist/page/layout/icon.ico +0 -0
- package/dist/page/layout/icon.png +0 -0
- package/dist/page/layout/layout.css +85 -4
- package/dist/page/layout/window.js +45 -0
- package/dist/page/main.js +4 -1
- package/dist/page/players/card.js +1 -1
- package/dist/page/profile/profile.js +14 -9
- package/dist/page/profile/settings.js +9 -0
- package/dist/page/style.css +2 -0
- package/dist/page/table/table.css +123 -0
- package/dist/page/table/window.js +130 -0
- package/dist/page/talk/chat.css +23 -5
- package/dist/page/talk/talk.css +38 -26
- package/dist/page/talk/talk.js +6 -4
- package/dist/page/talk/voice.js +35 -9
- package/dist/page/text.js +131 -17
- package/dist/page/theme.css +7 -0
- package/dist/profiles/index.d.ts +2 -1
- package/dist/profiles/index.js +2 -1
- package/dist/profiles/profiles.d.ts +10 -15
- package/dist/profiles/profiles.js +30 -15
- package/dist/profiles/ratings.d.ts +35 -0
- package/dist/profiles/ratings.js +61 -0
- package/dist/server/bell.d.ts +24 -3
- package/dist/server/bell.js +84 -10
- package/dist/server/caller.d.ts +2 -1
- package/dist/server/caller.js +3 -2
- package/dist/server/mcp.d.ts +7 -3
- package/dist/server/mcp.js +100 -52
- package/dist/server/page/analyses.d.ts +2 -0
- package/dist/server/page/analyses.js +20 -5
- package/dist/server/page/attach.d.ts +4 -0
- package/dist/server/page/attach.js +112 -0
- package/dist/server/page/chat.d.ts +65 -0
- package/dist/server/page/chat.js +87 -0
- package/dist/server/page/events.d.ts +7 -0
- package/dist/server/page/events.js +20 -0
- package/dist/server/page/files.js +10 -1
- package/dist/server/page/game.js +43 -23
- package/dist/server/page/page.d.ts +51 -7
- package/dist/server/page/page.js +74 -48
- package/dist/server/page/profiles.d.ts +5 -0
- package/dist/server/page/profiles.js +26 -10
- package/dist/server/page/table.d.ts +58 -0
- package/dist/server/page/table.js +210 -0
- package/dist/server/table/browser.d.ts +6 -0
- package/dist/server/table/browser.js +48 -0
- package/dist/server/table/hosts/chats.d.ts +7 -0
- package/dist/server/table/hosts/chats.js +34 -0
- package/dist/server/table/hosts/find.d.ts +2 -0
- package/dist/server/table/hosts/find.js +20 -0
- package/dist/server/table/hosts/hosts.d.ts +34 -0
- package/dist/server/table/hosts/hosts.js +49 -0
- package/dist/server/table/hosts/index.d.ts +3 -0
- package/dist/server/table/hosts/index.js +4 -0
- package/dist/server/table/hosts/play.d.ts +29 -0
- package/dist/server/table/hosts/play.js +180 -0
- package/dist/server/table/hosts/seat.d.ts +33 -0
- package/dist/server/table/hosts/seat.js +80 -0
- package/dist/server/table/hosts/terminal.d.ts +6 -0
- package/dist/server/table/hosts/terminal.js +63 -0
- package/dist/server/table/icon/index.d.ts +32 -0
- package/dist/server/table/icon/index.js +83 -0
- package/dist/server/table/icon/linux.d.ts +5 -0
- package/dist/server/table/icon/linux.js +47 -0
- package/dist/server/table/icon/macos.d.ts +3 -0
- package/dist/server/table/icon/macos.js +38 -0
- package/dist/server/table/icon/windows.d.ts +3 -0
- package/dist/server/table/icon/windows.js +48 -0
- package/dist/server/table/index.d.ts +6 -0
- package/dist/server/table/index.js +7 -0
- package/dist/server/table/launch.d.ts +19 -0
- package/dist/server/table/launch.js +78 -0
- package/dist/server/table/link.d.ts +10 -0
- package/dist/server/table/link.js +231 -0
- package/dist/server/table/table.d.ts +35 -0
- package/dist/server/table/table.js +94 -0
- package/dist/sites/chesscom.d.ts +4 -0
- package/dist/sites/chesscom.js +25 -0
- package/dist/sites/choice.d.ts +17 -0
- package/dist/sites/choice.js +13 -0
- package/dist/sites/index.d.ts +2 -0
- package/dist/sites/index.js +4 -0
- package/dist/sites/lichess.d.ts +4 -0
- package/dist/sites/lichess.js +32 -0
- package/dist/sites/request.d.ts +11 -0
- package/dist/sites/request.js +15 -0
- package/dist/sites/sites.d.ts +6 -0
- package/dist/sites/sites.js +33 -0
- package/instructions/analysis.md +1 -1
- package/instructions/analyst-agent.md +1 -1
- package/instructions/coach.md +2 -1
- package/instructions/opponent.md +6 -6
- package/instructions/ponder.md +1 -0
- package/instructions/start.md +1 -0
- package/package.json +1 -1
package/dist/server/bell.d.ts
CHANGED
|
@@ -29,16 +29,32 @@ export declare class Bell {
|
|
|
29
29
|
ring(): void;
|
|
30
30
|
/** 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. */
|
|
31
31
|
listen(request: IncomingMessage, response: ServerResponse): void;
|
|
32
|
+
/** 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. */
|
|
33
|
+
chatLeft(): void;
|
|
32
34
|
private tellIf;
|
|
33
35
|
}
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Whose page answers at a port, for the hook to find its own: the chat's session, the player's folder, whether a browser has the page open, Chesspal's
|
|
38
|
+
* version, and whether the table serves it (D-104), with the session and the folder of the chat attached to it, never its own process's.
|
|
39
|
+
*/
|
|
40
|
+
export declare function bellProbe(folder: string, open?: boolean, table?: TableChat): BellProbe;
|
|
41
|
+
/** The chat attached to the table, as its probe says it: its session, null for none or for Codex, and its folder; neither while no chat is attached. */
|
|
42
|
+
export interface TableChat {
|
|
43
|
+
session: string | null;
|
|
44
|
+
chat?: string;
|
|
45
|
+
}
|
|
36
46
|
/** What a page says of itself at `/api/bell?probe`. */
|
|
37
47
|
export interface BellProbe {
|
|
38
48
|
session: string | null;
|
|
39
49
|
folder: string;
|
|
40
50
|
open: boolean;
|
|
51
|
+
version?: string;
|
|
52
|
+
table?: true;
|
|
53
|
+
/** The folder of the chat attached to the table. */
|
|
54
|
+
chat?: string;
|
|
41
55
|
}
|
|
56
|
+
/** What the page at a port of this computer says of itself; undefined when none answers there. */
|
|
57
|
+
export declare function pageAt(port: number): Promise<BellProbe | undefined>;
|
|
42
58
|
/** The ports a folder's page tries first: one in each of a few ranges, by its path, since Windows may reserve a range. The hook looks for it there. */
|
|
43
59
|
export declare function pagePorts(folder: string): number[];
|
|
44
60
|
/** What Codex's Stop hook answers to wake the chat: its turn goes on with the doorbell's line. */
|
|
@@ -62,7 +78,12 @@ export type BellHeard = {
|
|
|
62
78
|
rang: false;
|
|
63
79
|
why: string;
|
|
64
80
|
};
|
|
65
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Waits at the page of the hook's chat, as waitForBell does, and says what it heard. The chat may be attached to the table (D-104), beside another chat
|
|
83
|
+
* of its folder that serves its own page: the table is one more page to choose from, unless the ports to try are given (Priya).
|
|
84
|
+
*/
|
|
66
85
|
export declare function bellHeard(input: string, ports?: number[]): Promise<BellHeard>;
|
|
67
86
|
/** Adds a line to the log of the hooks that heard no bell, keeping its last lines; a log that cannot be written never stops the hook. */
|
|
68
87
|
export declare function noteSilence(folder: string, why: string, now?: Date): void;
|
|
88
|
+
/** Whether two paths name the same folder: without regard to case on Windows. */
|
|
89
|
+
export declare function samePath(one: string, other: string): boolean;
|
package/dist/server/bell.js
CHANGED
|
@@ -5,6 +5,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
5
5
|
import { get } from "node:http";
|
|
6
6
|
import { join, resolve } from "node:path";
|
|
7
7
|
import { writeAtomically } from "../games/index.js";
|
|
8
|
+
import { dataFolder, tableFolder } from "../home/index.js";
|
|
9
|
+
import { version } from "../version.js";
|
|
8
10
|
/** What the chat hears from the doorbell: one line for whatever the player did, never what, only where to read it. */
|
|
9
11
|
export const bellLine = "The player acted on the board page: call status to see it.";
|
|
10
12
|
/**
|
|
@@ -84,15 +86,32 @@ export class Bell {
|
|
|
84
86
|
}
|
|
85
87
|
});
|
|
86
88
|
}
|
|
89
|
+
/** 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. */
|
|
90
|
+
chatLeft() {
|
|
91
|
+
this.busy = false;
|
|
92
|
+
this.woken = false;
|
|
93
|
+
this.pending = false;
|
|
94
|
+
this.listener?.end("dismissed");
|
|
95
|
+
this.listener = undefined;
|
|
96
|
+
this.changed();
|
|
97
|
+
}
|
|
87
98
|
tellIf(changed) {
|
|
88
99
|
if (changed) {
|
|
89
100
|
this.changed();
|
|
90
101
|
}
|
|
91
102
|
}
|
|
92
103
|
}
|
|
93
|
-
/**
|
|
94
|
-
|
|
95
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Whose page answers at a port, for the hook to find its own: the chat's session, the player's folder, whether a browser has the page open, Chesspal's
|
|
106
|
+
* version, and whether the table serves it (D-104), with the session and the folder of the chat attached to it, never its own process's.
|
|
107
|
+
*/
|
|
108
|
+
export function bellProbe(folder, open = false, table) {
|
|
109
|
+
const session = table === undefined ? process.env.CLAUDE_CODE_SESSION_ID ?? null : table.session;
|
|
110
|
+
return { session, folder: resolve(folder), open, version, ...(table === undefined ? {} : { table: true, ...(table.chat === undefined ? {} : { chat: table.chat }) }) };
|
|
111
|
+
}
|
|
112
|
+
/** What the page at a port of this computer says of itself; undefined when none answers there. */
|
|
113
|
+
export async function pageAt(port) {
|
|
114
|
+
return probeOf(await body(port, "/api/bell?probe", probeWait));
|
|
96
115
|
}
|
|
97
116
|
/** The ports a folder's page tries first: one in each of a few ranges, by its path, since Windows may reserve a range. The hook looks for it there. */
|
|
98
117
|
export function pagePorts(folder) {
|
|
@@ -133,13 +152,18 @@ export async function listen(codex = false) {
|
|
|
133
152
|
export async function waitForBell(input, ports) {
|
|
134
153
|
return (await bellHeard(input, ports)).rang;
|
|
135
154
|
}
|
|
136
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* Waits at the page of the hook's chat, as waitForBell does, and says what it heard. The chat may be attached to the table (D-104), beside another chat
|
|
157
|
+
* of its folder that serves its own page: the table is one more page to choose from, unless the ports to try are given (Priya).
|
|
158
|
+
*/
|
|
137
159
|
export async function bellHeard(input, ports) {
|
|
138
160
|
const hook = hookInput(input);
|
|
139
161
|
const folder = chatFolder(hook);
|
|
140
162
|
const tried = ports ?? pagePorts(folder);
|
|
141
|
-
const probes = await Promise.all(tried.map(async (port) => ({ port, said:
|
|
142
|
-
const
|
|
163
|
+
const [probes, table] = await Promise.all([Promise.all(tried.map(async (port) => ({ port, said: await pageAt(port) }))), ports === undefined ? tableOf(folder, hook.session) : undefined]);
|
|
164
|
+
const pages = probes.flatMap(({ port, said }) => (said !== undefined && samePath(said.folder, folder) ? [{ port, said }] : []));
|
|
165
|
+
// A chat opened in the table's own folder finds the table among the pages of its folder already.
|
|
166
|
+
const ours = table === undefined || pages.some(({ port }) => port === table.port) ? pages : [...pages, table];
|
|
143
167
|
const port = chatPage(ours, hook.session)?.port;
|
|
144
168
|
if (port === undefined) {
|
|
145
169
|
const others = probes.flatMap((one) => (one.said === undefined ? [] : [`${one.port} is ${one.said.folder}'s`]));
|
|
@@ -152,6 +176,50 @@ export async function bellHeard(input, ports) {
|
|
|
152
176
|
}
|
|
153
177
|
// How long a port may take to say whose page it is: one of this computer answers at once.
|
|
154
178
|
const probeWait = 1000;
|
|
179
|
+
/**
|
|
180
|
+
* The table, when the chat attached to it is the hook's: of its folder, or of its session; none when the data folder cannot be found, or no table has
|
|
181
|
+
* this chat. Finding the data folder costs some tens of milliseconds on Windows, off the AI's clock: the hook waits after the turn.
|
|
182
|
+
*/
|
|
183
|
+
async function tableOf(folder, session) {
|
|
184
|
+
let ports;
|
|
185
|
+
try {
|
|
186
|
+
ports = pagePorts(tableFolder(serverHome(folder) ?? dataFolder()));
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
const probes = await Promise.all(ports.map(async (port) => ({ port, said: await pageAt(port) })));
|
|
192
|
+
return probes.find((one) => one.said?.table === true && ((one.said.chat !== undefined && samePath(one.said.chat, folder)) || (session !== undefined && one.said.session === session)));
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* 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
|
|
196
|
+
* server's (Federico). Claude Code's .mcp.json first, then Codex's config.
|
|
197
|
+
*/
|
|
198
|
+
function serverHome(folder) {
|
|
199
|
+
const chosen = [claudeHome(folder), codexHome(folder)].find((one) => one !== undefined && one.trim() !== "");
|
|
200
|
+
return chosen === undefined ? undefined : resolve(chosen.trim());
|
|
201
|
+
}
|
|
202
|
+
function claudeHome(folder) {
|
|
203
|
+
try {
|
|
204
|
+
const config = JSON.parse(readFileSync(join(folder, ".mcp.json"), "utf8"));
|
|
205
|
+
const home = config.mcpServers?.chesspal?.env?.CHESSPAL_HOME;
|
|
206
|
+
return typeof home === "string" ? home : undefined;
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// 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.
|
|
213
|
+
function codexHome(folder) {
|
|
214
|
+
try {
|
|
215
|
+
const config = readFileSync(join(folder, ".codex", "config.toml"), "utf8");
|
|
216
|
+
const [, basic, literal] = /\bCHESSPAL_HOME\s*=\s*(?:("(?:[^"\\]|\\.)*")|'([^']*)')/.exec(config) ?? [];
|
|
217
|
+
return basic === undefined ? literal : JSON.parse(basic);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
155
223
|
// The log of Codex's hooks that heard no bell, in the chat's folder: its last lines only.
|
|
156
224
|
const silenceLog = "listen.log";
|
|
157
225
|
const silenceLines = 100;
|
|
@@ -193,15 +261,21 @@ function chatPage(pages, session) {
|
|
|
193
261
|
function probeOf(said) {
|
|
194
262
|
try {
|
|
195
263
|
const parsed = said === undefined ? undefined : JSON.parse(said);
|
|
196
|
-
return parsed === undefined || typeof parsed.folder !== "string"
|
|
197
|
-
?
|
|
198
|
-
|
|
264
|
+
return parsed === undefined || typeof parsed.folder !== "string" ? undefined : {
|
|
265
|
+
session: typeof parsed.session === "string" ? parsed.session : null,
|
|
266
|
+
folder: parsed.folder,
|
|
267
|
+
open: parsed.open === true,
|
|
268
|
+
...(typeof parsed.version === "string" ? { version: parsed.version } : {}),
|
|
269
|
+
...(parsed.table === true ? { table: true } : {}),
|
|
270
|
+
...(typeof parsed.chat === "string" ? { chat: parsed.chat } : {}),
|
|
271
|
+
};
|
|
199
272
|
}
|
|
200
273
|
catch {
|
|
201
274
|
return undefined;
|
|
202
275
|
}
|
|
203
276
|
}
|
|
204
|
-
|
|
277
|
+
/** Whether two paths name the same folder: without regard to case on Windows. */
|
|
278
|
+
export function samePath(one, other) {
|
|
205
279
|
return process.platform === "win32" ? resolve(one).toLowerCase() === resolve(other).toLowerCase() : resolve(one) === resolve(other);
|
|
206
280
|
}
|
|
207
281
|
/** The body a page of this computer answers with, once it ends; undefined when it does not answer, or not with 200. */
|
package/dist/server/caller.d.ts
CHANGED
|
@@ -6,11 +6,12 @@ export interface Client {
|
|
|
6
6
|
}
|
|
7
7
|
/** Who made the call with these metadata: the model and effort the host ties to it, else the host's name; undefined without a host. */
|
|
8
8
|
export declare function callerOf(meta: Record<string, unknown> | undefined, client: Client | undefined, folder: string, wait?: number): Promise<Caller | undefined>;
|
|
9
|
-
/** Who in Claude Code or Codex made a tool call: the chat itself, or one of its agents by its type. */
|
|
9
|
+
/** Who in Claude Code or Codex made a tool call: the chat itself, or one of its agents by its type, with the run it is, when the host tells it. */
|
|
10
10
|
export type CallingAgent = {
|
|
11
11
|
chat: true;
|
|
12
12
|
} | {
|
|
13
13
|
agent: string;
|
|
14
|
+
run?: string;
|
|
14
15
|
};
|
|
15
16
|
/**
|
|
16
17
|
* Who in Claude Code or Codex made a tool call: the chat, when its line is in the chat's transcript, or the agent whose file has it, by the type Claude Code
|
package/dist/server/caller.js
CHANGED
|
@@ -39,8 +39,9 @@ export async function callingAgent(meta, folder, wait = 50) {
|
|
|
39
39
|
if (found.file === transcript) {
|
|
40
40
|
return { chat: true };
|
|
41
41
|
}
|
|
42
|
+
// Each agent Claude Code starts writes a file of its own: its name tells two runs of the same agent apart.
|
|
42
43
|
const type = objectOf(textOf(found.file.replace(/\.jsonl$/, ".meta.json")))?.agentType;
|
|
43
|
-
return typeof type === "string" ? { agent: type } : undefined;
|
|
44
|
+
return typeof type === "string" ? { agent: type, run: basename(found.file, ".jsonl") } : undefined;
|
|
44
45
|
}
|
|
45
46
|
/** Whether a call comes from one of Codex's agents, as its metadata say: its own process starts no board page (D-98). */
|
|
46
47
|
export function fromCodexAgent(meta) {
|
|
@@ -110,7 +111,7 @@ async function codexCaller(turn, wait) {
|
|
|
110
111
|
const start = headOf(codexSession(thread));
|
|
111
112
|
const role = /"agent_role":"([^"]+)"/.exec(start)?.[1];
|
|
112
113
|
if (role !== undefined) {
|
|
113
|
-
const caller = start.includes('"forked_from_id":"') ? { chat: true } : { agent: role };
|
|
114
|
+
const caller = start.includes('"forked_from_id":"') ? { chat: true } : { agent: role, run: thread };
|
|
114
115
|
codexThreads.set(thread, caller);
|
|
115
116
|
return caller;
|
|
116
117
|
}
|
package/dist/server/mcp.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { type AnyCommand, type Context } from "../commands/index.ts";
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* What the host reads as the server starts, before any tool: the lines that start a chat, `instructions` then `status`, and the rule of every move (D-27,
|
|
5
|
+
* D-102). Short, since hosts cut them or copy them into every tool.
|
|
6
|
+
*/
|
|
4
7
|
export declare const serverInstructions: string;
|
|
5
8
|
/** The MCP server for an AI host: every command of the registry as a tool, with its description and parameters (D-5). */
|
|
6
9
|
export declare function createMcpServer(context?: Context, list?: AnyCommand[], beforeCall?: (meta: Record<string, unknown> | undefined) => Promise<void>): McpServer;
|
|
@@ -10,9 +13,10 @@ export declare function createMcpServer(context?: Context, list?: AnyCommand[],
|
|
|
10
13
|
*/
|
|
11
14
|
export declare function startMcpServer(context?: Context, list?: AnyCommand[]): Promise<void>;
|
|
12
15
|
/**
|
|
13
|
-
* The board page started once, before the chat's first call, the status every chat opens with, which then gives its address
|
|
16
|
+
* The board page started once, before the chat's first call, the status every chat opens with, which then gives its address; started `again` when the
|
|
17
|
+
* table the chat was attached to is lost, at once, off the AI's clock, or at the next call when a table to go to has come (D-104). The process of a Codex
|
|
14
18
|
* agent, which has one of its own, starts none, which would stand beside the chat's and could keep the doorbell from it (D-98, Priya).
|
|
15
19
|
*/
|
|
16
|
-
export declare function lazyPage(start: () => Promise<void>): (meta: Record<string, unknown> | undefined) => Promise<void>;
|
|
20
|
+
export declare function lazyPage(start: (again: (now?: boolean) => void) => Promise<void>): (meta: Record<string, unknown> | undefined) => Promise<void>;
|
|
17
21
|
/** The opponent's name for the page, from the host's (D-97): another host than Claude's and Codex is unnamed. */
|
|
18
22
|
export declare function opponentOf(host: string | undefined): string | undefined;
|
package/dist/server/mcp.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
// The MCP server an AI host starts: every command as a tool (D-5), and the board page
|
|
1
|
+
// The MCP server an AI host starts: every command as a tool (D-5), and the board page: the table's, which the chat attaches to, or one it serves itself
|
|
2
|
+
// for as long as it lasts (D-47, D-104).
|
|
2
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
import { aiMove, commands, defaultContext, inputProblems, isBoolean, PositionChanged } from "../commands/index.js";
|
|
6
7
|
import { aiNameOf } from "../core/index.js";
|
|
7
|
-
import { endOf, loadGame, noteAiHost, noteAiModel } from "../games/index.js";
|
|
8
|
-
import { agentTools, hasAgent } from "../host-files/index.js";
|
|
8
|
+
import { endOf, loadGame, noteAiHost, noteAiModel, rerunIfChanged } from "../games/index.js";
|
|
9
|
+
import { agentTools, hasAgent, startInstructions } from "../host-files/index.js";
|
|
9
10
|
import { gameFolder } from "../profiles/index.js";
|
|
10
11
|
import { version } from "../version.js";
|
|
11
12
|
import { pagePorts } from "./bell.js";
|
|
12
13
|
import { allowedCaller, callerOf, callingAgent, fromCodexAgent } from "./caller.js";
|
|
13
14
|
import { chatHasRead, startPage } from "./page/page.js";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
import { attachTable, tableToJoin } from "./table/index.js";
|
|
16
|
+
/**
|
|
17
|
+
* What the host reads as the server starts, before any tool: the lines that start a chat, `instructions` then `status`, and the rule of every move (D-27,
|
|
18
|
+
* D-102). Short, since hosts cut them or copy them into every tool.
|
|
19
|
+
*/
|
|
20
|
+
export const serverInstructions = startInstructions();
|
|
17
21
|
/** The MCP server for an AI host: every command of the registry as a tool, with its description and parameters (D-5). */
|
|
18
22
|
export function createMcpServer(context = defaultContext(), list = commands, beforeCall) {
|
|
19
23
|
const server = new McpServer({ name: "chesspal", version }, { instructions: serverInstructions });
|
|
@@ -24,28 +28,8 @@ export function createMcpServer(context = defaultContext(), list = commands, bef
|
|
|
24
28
|
context.host ??= () => server.server.getClientVersion()?.name;
|
|
25
29
|
const forSomeHosts = [];
|
|
26
30
|
for (const command of list.filter((each) => each.cliOnly !== true)) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const codexAgent = fromCodexAgent(extra._meta);
|
|
30
|
-
const calling = command.byAgents !== undefined || codexAgent ? await callingAgent(extra._meta, context.folder) : undefined;
|
|
31
|
-
// An agent's tool called by the chat, or by another agent, would give what only that agent may see (Priya).
|
|
32
|
-
if (command.byAgents !== undefined && !allowedCaller(calling, command.byAgents)) {
|
|
33
|
-
// It names no agent to start: the chat that asked would start one itself (Priya).
|
|
34
|
-
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 };
|
|
35
|
-
}
|
|
36
|
-
// Codex gives its agents every tool: one of Chesspal's keeps to its own, and never plays or talks for the opponent (D-98, Federico).
|
|
37
|
-
const own = calling !== undefined && "agent" in calling ? agentTools(calling.agent) : undefined;
|
|
38
|
-
if (codexAgent && own !== undefined && !own.includes(command.name)) {
|
|
39
|
-
return { content: [{ type: "text", text: `${command.name} is not yours to call: you work only with ${own.join(", ")}. Nothing changed.` }], isError: true };
|
|
40
|
-
}
|
|
41
|
-
// Each call knows who makes it, for the commands that write it down (D-81).
|
|
42
|
-
const caller = () => callerOf(extra._meta, server.server.getClientVersion(), context.folder);
|
|
43
|
-
const result = await track(runTool(command, input, context, caller, extra._meta));
|
|
44
|
-
if (command === aiMove && result.isError !== true) {
|
|
45
|
-
noteTheAi(context, caller);
|
|
46
|
-
}
|
|
47
|
-
return result;
|
|
48
|
-
});
|
|
31
|
+
// A call runs from its start, the page started before it too: the server that leaves waits for it.
|
|
32
|
+
const tool = server.registerTool(command.name, toolConfig(command), (input, extra) => track(callTool(command, input, context, server, extra._meta, beforeCall)));
|
|
49
33
|
if (command.hosts !== undefined) {
|
|
50
34
|
tool.disable();
|
|
51
35
|
forSomeHosts.push({ hosts: command.hosts, tool });
|
|
@@ -73,21 +57,35 @@ export async function startMcpServer(context = defaultContext(), list = commands
|
|
|
73
57
|
};
|
|
74
58
|
process.stdin.on("end", leave);
|
|
75
59
|
process.stdin.on("close", leave);
|
|
76
|
-
|
|
77
|
-
|
|
60
|
+
let page;
|
|
61
|
+
// The page served before, of its own, closes when the chat goes to a table.
|
|
62
|
+
const pageFirst = lazyPage(async (again) => {
|
|
63
|
+
await page?.close();
|
|
64
|
+
page = await pageOf(context, () => opponentOf(context.host?.()), again);
|
|
65
|
+
context.page = page?.url;
|
|
78
66
|
});
|
|
79
67
|
const server = createMcpServer(context, list, pageFirst);
|
|
80
68
|
await server.connect(new StdioServerTransport());
|
|
81
69
|
}
|
|
82
70
|
/**
|
|
83
|
-
* The board page started once, before the chat's first call, the status every chat opens with, which then gives its address
|
|
71
|
+
* The board page started once, before the chat's first call, the status every chat opens with, which then gives its address; started `again` when the
|
|
72
|
+
* table the chat was attached to is lost, at once, off the AI's clock, or at the next call when a table to go to has come (D-104). The process of a Codex
|
|
84
73
|
* agent, which has one of its own, starts none, which would stand beside the chat's and could keep the doorbell from it (D-98, Priya).
|
|
85
74
|
*/
|
|
86
75
|
export function lazyPage(start) {
|
|
87
76
|
let page;
|
|
77
|
+
const begin = () => {
|
|
78
|
+
// What an earlier start asks is no reason to drop the page this one serves.
|
|
79
|
+
const started = start((now = false) => {
|
|
80
|
+
if (page === started) {
|
|
81
|
+
page = now ? begin() : undefined;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return started;
|
|
85
|
+
};
|
|
88
86
|
return async (meta) => {
|
|
89
87
|
if (!fromCodexAgent(meta)) {
|
|
90
|
-
page ??=
|
|
88
|
+
page ??= begin();
|
|
91
89
|
await page;
|
|
92
90
|
}
|
|
93
91
|
};
|
|
@@ -96,19 +94,24 @@ export function lazyPage(start) {
|
|
|
96
94
|
export function opponentOf(host) {
|
|
97
95
|
return aiNameOf(host);
|
|
98
96
|
}
|
|
99
|
-
/**
|
|
100
|
-
|
|
97
|
+
/**
|
|
98
|
+
* The board page of the player's folder: the table's, when one of this version takes the chat (D-104), else served by this process, which looks for a
|
|
99
|
+
* table after each tool; without it the game goes on in the chat. It starts `again`: at once when the table is lost, at the next call when one is found.
|
|
100
|
+
*/
|
|
101
|
+
async function pageOf(context, opponent, again) {
|
|
101
102
|
try {
|
|
102
|
-
const
|
|
103
|
+
const table = await attachTable(context, () => again(true));
|
|
104
|
+
const page = table ?? await startPage(context, { opponent });
|
|
103
105
|
// Words the AI is given change no file: the pages hear of them from the commands.
|
|
104
|
-
context.heard = heard;
|
|
105
|
-
context.calls = chatCalls;
|
|
106
|
-
context.
|
|
107
|
-
context.
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
context.heard = page.heard;
|
|
107
|
+
context.calls = page.chatCalls;
|
|
108
|
+
context.toolEnded = table === undefined ? lookForTable(context, again) : page.toolEnded;
|
|
109
|
+
context.pageOpen = page.open;
|
|
110
|
+
context.askNewGame = page.askNewGame;
|
|
111
|
+
if (table === undefined && !pagePorts(context.folder).includes(Number(new URL(page.url).port))) {
|
|
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
113
|
}
|
|
111
|
-
return
|
|
114
|
+
return page;
|
|
112
115
|
}
|
|
113
116
|
catch (error) {
|
|
114
117
|
// The host keeps what the server writes on stderr in its logs.
|
|
@@ -116,6 +119,27 @@ async function pageOf(context, opponent) {
|
|
|
116
119
|
return undefined;
|
|
117
120
|
}
|
|
118
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* After each tool of a chat that serves its own page, a look for a table of its version with no chat at it, off the AI's clock: one started since, by
|
|
124
|
+
* `chesspal` or the icon, takes the chat at its next tool, and its page then closes (Priya).
|
|
125
|
+
*/
|
|
126
|
+
function lookForTable(context, again) {
|
|
127
|
+
let looking = false;
|
|
128
|
+
return () => {
|
|
129
|
+
if (looking) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
looking = true;
|
|
133
|
+
setImmediate(() => {
|
|
134
|
+
void tableToJoin(context).then((found) => {
|
|
135
|
+
looking = false;
|
|
136
|
+
if (found) {
|
|
137
|
+
again();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
}
|
|
119
143
|
/**
|
|
120
144
|
* 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
|
|
121
145
|
* 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.
|
|
@@ -125,19 +149,41 @@ function noteTheAi(context, caller) {
|
|
|
125
149
|
setImmediate(() => {
|
|
126
150
|
// Nothing that fails here may stop the server: the move is played and answered.
|
|
127
151
|
caller().then((found) => {
|
|
152
|
+
const folder = gameFolder(context.home());
|
|
128
153
|
const host = context.host?.();
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
if (noteAiModel(gameFolder(context.home()), { model: found.annotator, ...(found.effort === undefined ? {} : { effort: found.effort }) })) {
|
|
154
|
+
const hostChanged = host !== undefined && host.trim() !== "" && noteAiHost(folder, host);
|
|
155
|
+
const modelChanged = found !== undefined && found.hostOnly !== true
|
|
156
|
+
&& noteAiModel(folder, { model: found.annotator, ...(found.effort === undefined ? {} : { effort: found.effort }) });
|
|
157
|
+
// The pages, and the table the chat is attached to, hear of it now: the move's copy went before (Federico).
|
|
158
|
+
if (hostChanged || modelChanged) {
|
|
136
159
|
context.heard?.();
|
|
137
160
|
}
|
|
138
161
|
}).catch(() => undefined);
|
|
139
162
|
});
|
|
140
163
|
}
|
|
164
|
+
/** A tool called: the page started first, at the chat's first call; refused to an agent it is not for (D-89, D-98); and who played as the AI noted after a move. */
|
|
165
|
+
async function callTool(command, input, context, server, meta, beforeCall) {
|
|
166
|
+
await beforeCall?.(meta);
|
|
167
|
+
const codexAgent = fromCodexAgent(meta);
|
|
168
|
+
const calling = command.byAgents !== undefined || codexAgent ? await callingAgent(meta, context.folder) : undefined;
|
|
169
|
+
// An agent's tool called by the chat, or by another agent, would give what only that agent may see (Priya).
|
|
170
|
+
if (command.byAgents !== undefined && !allowedCaller(calling, command.byAgents)) {
|
|
171
|
+
// It names no agent to start: the chat that asked would start one itself (Priya).
|
|
172
|
+
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 };
|
|
173
|
+
}
|
|
174
|
+
// Codex gives its agents every tool: one of Chesspal's keeps to its own, and never plays or talks for the opponent (D-98, Federico).
|
|
175
|
+
const own = calling !== undefined && "agent" in calling ? agentTools(calling.agent) : undefined;
|
|
176
|
+
if (codexAgent && own !== undefined && !own.includes(command.name)) {
|
|
177
|
+
return { content: [{ type: "text", text: `${command.name} is not yours to call: you work only with ${own.join(", ")}. Nothing changed.` }], isError: true };
|
|
178
|
+
}
|
|
179
|
+
// Each call knows who makes it, for the commands that write it down (D-81).
|
|
180
|
+
const caller = () => callerOf(meta, server.server.getClientVersion(), context.folder);
|
|
181
|
+
const result = await runTool(command, input, context, caller, meta);
|
|
182
|
+
if (command === aiMove && result.isError !== true) {
|
|
183
|
+
noteTheAi(context, caller);
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
141
187
|
// The tool calls still running, which the server waits for before it leaves.
|
|
142
188
|
const running = new Set();
|
|
143
189
|
function track(call) {
|
|
@@ -160,10 +206,12 @@ async function runTool(command, input, context, caller, meta) {
|
|
|
160
206
|
let refused = false;
|
|
161
207
|
try {
|
|
162
208
|
if (chat) {
|
|
163
|
-
context.calls?.();
|
|
209
|
+
await context.calls?.();
|
|
164
210
|
}
|
|
165
|
-
const
|
|
166
|
-
const
|
|
211
|
+
const run = calling !== undefined && "run" in calling ? calling.run : undefined;
|
|
212
|
+
const who = agent !== undefined ? { agent, ...(run === undefined ? {} : { agentRun: run }) } : calling !== undefined ? { chat: true } : {};
|
|
213
|
+
// 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 }));
|
|
167
215
|
return { content: [{ type: "text", text: command.format(result, context) }] };
|
|
168
216
|
}
|
|
169
217
|
catch (error) {
|
|
@@ -12,6 +12,8 @@ export interface AnalysisItem {
|
|
|
12
12
|
readonly opening?: string;
|
|
13
13
|
readonly moves: number;
|
|
14
14
|
readonly reviewed: boolean;
|
|
15
|
+
/** A review begun and not finished, its notes or the analyst that took it, which the page lets the player write again or delete (Stefano). */
|
|
16
|
+
readonly begun?: true;
|
|
15
17
|
/** Who reviewed it (D-81). */
|
|
16
18
|
readonly annotator?: AiModel;
|
|
17
19
|
/** Where a game loaded from elsewhere was played, as its Site tag says: the player's name there makes it theirs (D-93). */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
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";
|
|
2
|
+
import { aiModelIn, annotatorOf, isReviewed, leadOf, reviewOf } from "../../core/index.js";
|
|
3
|
+
import { archivedGame, archivedGames, gameInAnalysis, gameStemOf, 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. */
|
|
@@ -9,7 +9,8 @@ export function analysesOf(home) {
|
|
|
9
9
|
// By the time in their names, which orders kept and played games together.
|
|
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) : archivedGame(folder, name), taken === name));
|
|
13
14
|
}
|
|
14
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. */
|
|
15
16
|
export function answerAnalyses(request, response, url, served) {
|
|
@@ -33,7 +34,7 @@ export function answerAnalyses(request, response, url, served) {
|
|
|
33
34
|
return true;
|
|
34
35
|
}
|
|
35
36
|
const deletions = { "/api/delete-game": deleteGame, "/api/delete-review": deleteReview };
|
|
36
|
-
function itemOf(name, read) {
|
|
37
|
+
function itemOf(name, read, taken) {
|
|
37
38
|
let record;
|
|
38
39
|
try {
|
|
39
40
|
record = read();
|
|
@@ -42,6 +43,10 @@ function itemOf(name, read) {
|
|
|
42
43
|
// A game that cannot be read stays out of the list.
|
|
43
44
|
return [];
|
|
44
45
|
}
|
|
46
|
+
// A game closed before any move has nothing to analyse; a position brought to study without moves stays, to open again or delete.
|
|
47
|
+
if (record.moves.length === 0 && name.startsWith("games/")) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
45
50
|
const { tags } = record;
|
|
46
51
|
const date = tags.Date === undefined || tags.Date.includes("?") ? undefined : tags.Date;
|
|
47
52
|
const opening = bookOpening(record)?.name;
|
|
@@ -49,6 +54,8 @@ function itemOf(name, read) {
|
|
|
49
54
|
const site = tags.Site === undefined || /^[?\-\s]*$/.test(tags.Site) ? undefined : tags.Site;
|
|
50
55
|
const signed = annotatorOf(record);
|
|
51
56
|
const annotator = signed === undefined ? undefined : { model: signed.annotator, ...(signed.effort === undefined ? {} : { effort: signed.effort }) };
|
|
57
|
+
const reviewed = isReviewed(record);
|
|
58
|
+
const begun = !reviewed && (taken || reviewBegun(record));
|
|
52
59
|
return [{
|
|
53
60
|
name,
|
|
54
61
|
white: tags.White ?? "?",
|
|
@@ -58,8 +65,16 @@ function itemOf(name, read) {
|
|
|
58
65
|
result: tags.Result ?? "*",
|
|
59
66
|
...(opening === undefined ? {} : { opening }),
|
|
60
67
|
moves: Math.ceil(record.moves.length / 2),
|
|
61
|
-
reviewed
|
|
68
|
+
reviewed,
|
|
69
|
+
...(begun ? { begun: true } : {}),
|
|
62
70
|
...(annotator === undefined ? {} : { annotator }),
|
|
63
71
|
...(site === undefined ? {} : { site }),
|
|
64
72
|
}];
|
|
65
73
|
}
|
|
74
|
+
/** Whether a game holds part of a review on its moves: a note, a badge but the book's, which Chesspal puts there, or who is ahead. */
|
|
75
|
+
function reviewBegun(record) {
|
|
76
|
+
return record.moves.some(({ comment }) => {
|
|
77
|
+
const review = reviewOf(comment);
|
|
78
|
+
return review?.note !== undefined || (review?.badge !== undefined && review.badge !== "book") || leadOf(comment) !== undefined;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { Served } from "./page.ts";
|
|
3
|
+
/** Answers the chat's requests to the table, under /api/chat; false for any other, and for every request to a page a chat serves itself. */
|
|
4
|
+
export declare function answerChat(request: IncomingMessage, response: ServerResponse, url: URL, served: Served): boolean;
|