chesspal 0.3.6 → 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/dist/cli/cli.js +7 -2
- package/dist/cli/command-line.js +5 -3
- package/dist/cli/prompt-hook.d.ts +17 -0
- package/dist/cli/prompt-hook.js +100 -0
- package/dist/commands/analysis/analyse.js +8 -14
- package/dist/commands/analysis/annotate.d.ts +8 -0
- package/dist/commands/analysis/annotate.js +19 -3
- package/dist/commands/answers/agents.d.ts +7 -5
- package/dist/commands/answers/agents.js +16 -9
- 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 +14 -7
- 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 +5 -2
- package/dist/commands/answers/summary.js +16 -5
- 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/new.js +41 -11
- 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 +4 -1
- package/dist/commands/game/status.d.ts +1 -1
- package/dist/commands/game/status.js +6 -6
- package/dist/commands/index.d.ts +3 -3
- package/dist/commands/index.js +3 -3
- package/dist/commands/profiles/past-games.d.ts +1 -1
- package/dist/commands/profiles/past-games.js +21 -10
- 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 +6 -5
- 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/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 +2 -1
- package/dist/games/index.js +2 -1
- package/dist/games/position-shown.d.ts +24 -0
- package/dist/games/position-shown.js +51 -0
- package/dist/home/index.d.ts +1 -1
- package/dist/home/index.js +1 -1
- package/dist/home/same-path.d.ts +6 -1
- package/dist/home/same-path.js +22 -3
- package/dist/host-files/host-files.d.ts +5 -0
- package/dist/host-files/host-files.js +13 -4
- package/dist/host-files/index.d.ts +1 -1
- package/dist/host-files/index.js +1 -1
- package/dist/page/app/app.js +30 -7
- package/dist/page/app/served.js +9 -4
- package/dist/page/avatars/torretta.css +4 -4
- package/dist/page/base.css +1 -1
- package/dist/page/board/board.css +4 -2
- package/dist/page/dialogs/dialogs.css +3 -1
- package/dist/page/game/column.js +10 -8
- package/dist/page/game/game.css +86 -6
- 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/layout/icon.ico +0 -0
- package/dist/page/layout/layout.css +32 -15
- package/dist/page/layout/window.js +41 -2
- 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/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 -9
- package/dist/page/talk/talk.js +65 -20
- package/dist/page/talk/voice.js +92 -21
- package/dist/page/text.js +24 -8
- package/dist/server/bell/bell.d.ts +24 -27
- package/dist/server/bell/bell.js +79 -143
- 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/files.js +3 -1
- package/dist/server/page/game.d.ts +5 -0
- package/dist/server/page/game.js +78 -13
- 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 +38 -4
- package/dist/server/page/table.js +61 -23
- 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/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 +6 -4
- package/dist/server/table/hosts/play.js +58 -16
- package/dist/server/table/index.d.ts +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 +1 -0
- package/dist/server/table/table.d.ts +7 -0
- package/dist/server/table/table.js +39 -11
- package/instructions/opponent.md +1 -1
- package/package.json +1 -1
package/dist/page/text.js
CHANGED
|
@@ -30,6 +30,7 @@ const english = {
|
|
|
30
30
|
sounds: "Sounds",
|
|
31
31
|
voice: "Read the AI's words aloud",
|
|
32
32
|
noVoice: "This browser has no voice for this language: add one in your system's settings",
|
|
33
|
+
skipReading: "Stop reading this aloud",
|
|
33
34
|
saidMove: (move) => moveInWords(englishMoveWords, move),
|
|
34
35
|
newGame: "New game",
|
|
35
36
|
offerDraw: "Offer a draw",
|
|
@@ -176,21 +177,25 @@ const english = {
|
|
|
176
177
|
choose: "Choose the chat to play with: it opens in a window of its own, to keep open.",
|
|
177
178
|
reviewTitle: "Who reviews the game?",
|
|
178
179
|
reviewChoose: "Choose the chat that reviews the game: it opens in a window of its own, to keep open.",
|
|
179
|
-
restartHint: "
|
|
180
|
+
restartHint: "The chat starts again, clean, in the same window, once it has ended its answer, with the chat, model and effort chosen here. The game stays as it is.",
|
|
180
181
|
byHand: (name) => `You opened this chat yourself: change ${name}'s model with /model in its window, or close it and open one from here, with the choices below.`,
|
|
181
182
|
noChat: "No chat at the table: choose one",
|
|
182
183
|
change: "Change the chat or its model",
|
|
183
184
|
install: (name) => `To play with ${name}, install Claude Code or Codex, then open this page again.`,
|
|
185
|
+
chat: "Chat",
|
|
184
186
|
model: "Model",
|
|
185
187
|
effort: "Effort",
|
|
186
|
-
|
|
187
|
-
|
|
188
|
+
atTable: "At the table",
|
|
189
|
+
act: { open: (host) => `Start ${host}`, review: (host) => `Review with ${host}`, restart: (host) => `Restart ${host}`, switch: (host) => `Switch to ${host}` },
|
|
190
|
+
opening: "Opening the chat… In the terminal, if it asks whether you trust the folder, say yes (in Claude Code, go down to Yes with the arrow keys) and press Enter; with Codex, on the hooks, choose Trust all and continue.",
|
|
188
191
|
again: (name) => `${name} starts again with a clean chat for the new game…`,
|
|
189
192
|
restart: "The chat starts again, clean…",
|
|
190
193
|
waiting: (name) => `${name} starts again once it has ended its answer…`,
|
|
191
|
-
|
|
194
|
+
newGameWaiting: (name) => `The new game starts once ${name} has ended its answer…`,
|
|
192
195
|
restartNow: "Start again now: it cuts the answer it is giving",
|
|
193
196
|
cancel: "Do not start again",
|
|
197
|
+
newGameNow: "Start the new game now: it cuts the answer it is giving",
|
|
198
|
+
newGameCancel: "Do not start the new game",
|
|
194
199
|
failed: "The terminal did not open: type the command below in a terminal.",
|
|
195
200
|
notRestarted: "The chat did not start again: close its window, then open another from here.",
|
|
196
201
|
typeIt: "Or type in a terminal:",
|
|
@@ -205,6 +210,7 @@ const english = {
|
|
|
205
210
|
"ai-to-move": () => "Wait for the AI's move.",
|
|
206
211
|
illegal: () => "That move is not legal here.",
|
|
207
212
|
"clear-chat": (most, name) => english.clearChat(name),
|
|
213
|
+
"new-game-profile": () => "The new game did not start: you changed profile while it waited. Start it again from this one.",
|
|
208
214
|
"words-empty": () => "Write something first.",
|
|
209
215
|
"words-spent": () => "You have used up your words for this move: write again after the AI's move.",
|
|
210
216
|
"words-long": (most) => `Write at most ${most} characters.`,
|
|
@@ -215,6 +221,8 @@ const english = {
|
|
|
215
221
|
"profile-game": () => english.profile.whilePlaying,
|
|
216
222
|
"game-going": () => "A game is going on: finish it before you bring another to analyse.",
|
|
217
223
|
"pgn-unreadable": () => "Chesspal cannot read this PGN: check you copied the whole game, its tags and its moves.",
|
|
224
|
+
"game-missing": () => "This game is no longer there: pick another.",
|
|
225
|
+
"game-not-archived": () => "The game just over is not saved yet: try again in a moment.",
|
|
218
226
|
"update-game": () => english.update.whilePlaying,
|
|
219
227
|
"update-chat": (most, name) => `${name} is answering: update once it has done.`,
|
|
220
228
|
},
|
|
@@ -460,6 +468,7 @@ const italian = {
|
|
|
460
468
|
sounds: "Suoni",
|
|
461
469
|
voice: "Leggi ad alta voce le parole dell'AI",
|
|
462
470
|
noVoice: "Questo browser non ha una voce per questa lingua: aggiungila nelle impostazioni del sistema",
|
|
471
|
+
skipReading: "Smetti di leggere questo a voce",
|
|
463
472
|
saidMove: (move) => moveInWords(italianMoveWords, move),
|
|
464
473
|
newGame: "Nuova partita",
|
|
465
474
|
offerDraw: "Proponi patta",
|
|
@@ -606,21 +615,25 @@ const italian = {
|
|
|
606
615
|
choose: "Scegli la chat con cui giocare: parte in una finestra a parte, da tenere aperta.",
|
|
607
616
|
reviewTitle: "Chi analizza la partita?",
|
|
608
617
|
reviewChoose: "Scegli la chat che analizza la partita: parte in una finestra a parte, da tenere aperta.",
|
|
609
|
-
restartHint: "
|
|
618
|
+
restartHint: "La chat riparte pulita nella stessa finestra, appena ha finito di rispondere, con la chat, il modello e il ragionamento scelti qui. La partita resta com'è.",
|
|
610
619
|
byHand: (name) => `Questa chat l'hai aperta tu: cambia il modello di ${name} con /model nella sua finestra, oppure chiudila e aprine una da qui, con le scelte qui sotto.`,
|
|
611
620
|
noChat: "Nessuna chat al tavolo: scegline una",
|
|
612
621
|
change: "Cambia la chat o il modello",
|
|
613
622
|
install: (name) => `Per giocare con ${name}, installa Claude Code o Codex, poi apri di nuovo questa pagina.`,
|
|
623
|
+
chat: "Chat",
|
|
614
624
|
model: "Modello",
|
|
615
625
|
effort: "Ragionamento",
|
|
616
|
-
|
|
617
|
-
|
|
626
|
+
atTable: "Al tavolo",
|
|
627
|
+
act: { open: (host) => `Avvia ${host}`, review: (host) => `Analizza con ${host}`, restart: (host) => `Riavvia ${host}`, switch: (host) => `Passa a ${host}` },
|
|
628
|
+
opening: "Apro la chat… Nel terminale, se ti chiede se ti fidi della cartella, rispondi sì (in Claude Code scendi su Yes con le frecce) e premi Invio; con Codex, sugli «hook», scegli Trust all and continue.",
|
|
618
629
|
again: (name) => `${name} riparte con una chat pulita per la nuova partita…`,
|
|
619
630
|
restart: "La chat riparte pulita…",
|
|
620
631
|
waiting: (name) => `${name} riparte appena ha finito di rispondere…`,
|
|
621
|
-
|
|
632
|
+
newGameWaiting: (name) => `La nuova partita comincia appena ${name} ha finito di rispondere…`,
|
|
622
633
|
restartNow: "Riparti adesso: interrompe la risposta in corso",
|
|
623
634
|
cancel: "Non ripartire",
|
|
635
|
+
newGameNow: "Comincia adesso la nuova partita: interrompe la risposta in corso",
|
|
636
|
+
newGameCancel: "Non cominciare la nuova partita",
|
|
624
637
|
failed: "Il terminale non si è aperto: scrivi il comando qui sotto in un terminale.",
|
|
625
638
|
notRestarted: "La chat non è ripartita: chiudi la sua finestra, poi aprine un'altra da qui.",
|
|
626
639
|
typeIt: "Oppure scrivi in un terminale:",
|
|
@@ -635,6 +648,7 @@ const italian = {
|
|
|
635
648
|
"ai-to-move": () => "Aspetta la mossa dell'AI.",
|
|
636
649
|
illegal: () => "Questa mossa qui non è legale.",
|
|
637
650
|
"clear-chat": (most, name) => italian.clearChat(name),
|
|
651
|
+
"new-game-profile": () => "La nuova partita non è cominciata: hai cambiato profilo mentre aspettava. Ricominciala da questo.",
|
|
638
652
|
"words-empty": () => "Scrivi prima qualcosa.",
|
|
639
653
|
"words-spent": () => "Hai finito le parole per questa mossa: scrivi di nuovo dopo quella dell'AI.",
|
|
640
654
|
"words-long": (most) => `Scrivi al massimo ${most} caratteri.`,
|
|
@@ -645,6 +659,8 @@ const italian = {
|
|
|
645
659
|
"profile-game": () => italian.profile.whilePlaying,
|
|
646
660
|
"game-going": () => "C'è una partita in corso: finiscila prima di portarne un'altra da analizzare.",
|
|
647
661
|
"pgn-unreadable": () => "Chesspal non riesce a leggere questo PGN: controlla di aver copiato la partita intera, intestazioni e mosse.",
|
|
662
|
+
"game-missing": () => "Questa partita non c'è più: scegline un'altra.",
|
|
663
|
+
"game-not-archived": () => "La partita appena finita non è ancora salvata: riprova fra un momento.",
|
|
648
664
|
"update-game": () => italian.update.whilePlaying,
|
|
649
665
|
"update-chat": (most, name) => `${name} sta rispondendo: aggiorna quando ha finito.`,
|
|
650
666
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
/** What the chat hears from the doorbell: one line for whatever the player did, never what, only where to read it. */
|
|
3
3
|
export declare const bellLine = "The player acted on the board page: call status to see it.";
|
|
4
4
|
/**
|
|
@@ -10,6 +10,7 @@ export declare class Bell {
|
|
|
10
10
|
private pending;
|
|
11
11
|
private busy;
|
|
12
12
|
private woken;
|
|
13
|
+
private hooked;
|
|
13
14
|
private readonly news;
|
|
14
15
|
private readonly changed;
|
|
15
16
|
/**
|
|
@@ -25,38 +26,34 @@ export declare class Bell {
|
|
|
25
26
|
get listening(): boolean;
|
|
26
27
|
/** The chat calls a tool, so it is in a turn: true when nobody was listening before. */
|
|
27
28
|
chatCalls(): boolean;
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
/**
|
|
30
|
+
* 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
|
|
31
|
+
* 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
|
|
32
|
+
* stays unheard until its first tool, which starts the AI's clock, held till then: the message moves no clock.
|
|
33
|
+
*/
|
|
34
|
+
chatPrompted(): void;
|
|
35
|
+
/**
|
|
36
|
+
* 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
|
|
37
|
+
* 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.
|
|
38
|
+
*/
|
|
39
|
+
ring(mayBeAtWork?: boolean): void;
|
|
40
|
+
/**
|
|
41
|
+
* 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
|
|
42
|
+
* the chat is `told` all of it otherwise: by the player's message that came with the move the prompt hook played.
|
|
43
|
+
*/
|
|
44
|
+
listen(request: IncomingMessage, response: ServerResponse, told?: boolean): void;
|
|
32
45
|
/** 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
46
|
chatLeft(): void;
|
|
34
47
|
private tellIf;
|
|
35
48
|
}
|
|
36
49
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
50
|
+
* 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
|
|
51
|
+
* 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
|
|
52
|
+
* is busy, not gone, and keeps its note. It throws when the note cannot be written.
|
|
39
53
|
*/
|
|
40
|
-
export declare function
|
|
41
|
-
/**
|
|
42
|
-
export
|
|
43
|
-
session: string | null;
|
|
44
|
-
chat?: string;
|
|
45
|
-
}
|
|
46
|
-
/** What a page says of itself at `/api/bell?probe`. */
|
|
47
|
-
export interface BellProbe {
|
|
48
|
-
session: string | null;
|
|
49
|
-
folder: string;
|
|
50
|
-
open: boolean;
|
|
51
|
-
version?: string;
|
|
52
|
-
table?: true;
|
|
53
|
-
/** The folder of the chat attached to the table. */
|
|
54
|
-
chat?: string;
|
|
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>;
|
|
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. */
|
|
59
|
-
export declare function pagePorts(folder: string): number[];
|
|
54
|
+
export declare function notePagePort(home: string, folder: string, port: number, wait?: number): Promise<void>;
|
|
55
|
+
/** 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. */
|
|
56
|
+
export declare function forgetPagePort(home: string, folder: string, port: number): void;
|
|
60
57
|
/** What Codex's Stop hook answers to wake the chat: its turn goes on with the doorbell's line. */
|
|
61
58
|
export declare const codexBellAnswer: string;
|
|
62
59
|
/**
|
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, samePath, 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
|
-
|
|
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);
|
|
112
142
|
}
|
|
113
|
-
/**
|
|
114
|
-
export
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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,60 +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
|
-
/** The body a page of this computer answers with, once it ends; undefined when it does not answer, or not with 200. */
|
|
291
|
-
function body(port, path, wait) {
|
|
292
|
-
return new Promise((done) => {
|
|
293
|
-
// A connection of its own: Node's shared one ends a request idle for five seconds, and the doorbell waits for hours.
|
|
294
|
-
const request = get({ host: "127.0.0.1", port, path, agent: false }, (response) => {
|
|
295
|
-
let text = "";
|
|
296
|
-
response.setEncoding("utf8");
|
|
297
|
-
response.on("data", (chunk) => {
|
|
298
|
-
text += chunk;
|
|
299
|
-
});
|
|
300
|
-
response.on("end", () => done(response.statusCode === 200 ? text : undefined));
|
|
301
|
-
response.on("error", () => done(undefined));
|
|
302
|
-
});
|
|
303
|
-
if (wait !== undefined) {
|
|
304
|
-
giveUpAfterTurns(request, wait);
|
|
305
|
-
}
|
|
306
|
-
request.on("error", () => done(undefined));
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Ends a request still unanswered after `wait`, counted in ten turns of this process's clock: after a stall (a busy computer, one waking up), a timer
|
|
311
|
-
* fires before the answer waiting is read, and a page of this process, or one that answered meanwhile, would be taken for one that does not answer.
|
|
312
|
-
*/
|
|
313
|
-
function giveUpAfterTurns(request, wait) {
|
|
314
|
-
let turns = 0;
|
|
315
|
-
const timer = setInterval(() => {
|
|
316
|
-
turns++;
|
|
317
|
-
if (turns === 10) {
|
|
318
|
-
request.destroy();
|
|
319
|
-
}
|
|
320
|
-
}, wait / 10);
|
|
321
|
-
request.on("close", () => clearInterval(timer));
|
|
322
|
-
}
|
|
@@ -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.
|