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
|
@@ -1,40 +1,118 @@
|
|
|
1
|
-
// The table's page opened in the player's browser (D-104): in a window of its own, as an application,
|
|
2
|
-
//
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
// The table's page opened in the player's browser (D-104): in a window of its own, as an application, in the default browser when it opens one with --app,
|
|
2
|
+
// else in Chrome or Edge when either is installed; else in a tab of the system's default browser.
|
|
3
|
+
import { execFile, spawn } from "node:child_process";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { posix, win32 } from "node:path";
|
|
6
6
|
import { openFolder } from "../../home/index.js";
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Opens an address in a window of its own, marked with the table's opening, else in a tab of the default browser, without waiting for it: how it opened.
|
|
9
|
+
* A tab is not marked: a page cannot close a tab it did not open.
|
|
10
|
+
*/
|
|
11
|
+
export async function openInBrowser(url, platform, opening) {
|
|
12
|
+
const browser = await appBrowser(platform, process.env);
|
|
13
|
+
if (browser !== undefined && await started(browser, [`--app=${openedAddress(url, opening)}`])) {
|
|
14
|
+
return "window";
|
|
15
|
+
}
|
|
10
16
|
// Explorer, open and xdg-open take an address as they take a folder, and hand it to the default browser.
|
|
11
|
-
return
|
|
17
|
+
return await openFolder(url, platform) ? "tab" : "none";
|
|
12
18
|
}
|
|
13
|
-
/**
|
|
14
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The address of the window the table opens, marked so that the page sizes it as it opens, with its opening, so that it closes once a newer window has
|
|
21
|
+
* opened (page/layout/window.ts).
|
|
22
|
+
*/
|
|
23
|
+
export function openedAddress(url, opening) {
|
|
15
24
|
const address = new URL(url);
|
|
16
|
-
address.searchParams.set("opened", "");
|
|
25
|
+
address.searchParams.set("opened", opening === undefined ? "" : String(opening));
|
|
17
26
|
return address.href;
|
|
18
27
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
/**
|
|
29
|
+
* The browser that opens the table's window: the default one when it is a Chromium that takes --app and is where it installs itself, so that the window
|
|
30
|
+
* opens in the browser the player uses; else Chrome or Edge, the first there is; none when neither is.
|
|
31
|
+
*/
|
|
32
|
+
export async function appBrowser(platform, env, system = realSystem) {
|
|
33
|
+
const chosen = await defaultBrowser(platform, system);
|
|
34
|
+
const tried = platform === "linux" ? [chrome, chromium, edge] : [chrome, edge];
|
|
35
|
+
const browsers = chosen === undefined ? tried : [chosen, ...tried];
|
|
36
|
+
return browsers.flatMap((browser) => browserPaths(browser, platform, env)).find(system.exists);
|
|
37
|
+
}
|
|
38
|
+
const chrome = {
|
|
39
|
+
windows: ["Google", "Chrome", "Application", "chrome.exe"],
|
|
40
|
+
progId: "ChromeHTML",
|
|
41
|
+
mac: "Google Chrome",
|
|
42
|
+
linux: ["google-chrome", "google-chrome-stable"],
|
|
43
|
+
desktop: ["google-chrome"],
|
|
44
|
+
};
|
|
45
|
+
const edge = {
|
|
46
|
+
windows: ["Microsoft", "Edge", "Application", "msedge.exe"],
|
|
47
|
+
progId: "MSEdgeHTM",
|
|
48
|
+
mac: "Microsoft Edge",
|
|
49
|
+
linux: ["microsoft-edge", "microsoft-edge-stable"],
|
|
50
|
+
desktop: ["microsoft-edge"],
|
|
51
|
+
};
|
|
52
|
+
const brave = {
|
|
53
|
+
windows: ["BraveSoftware", "Brave-Browser", "Application", "brave.exe"],
|
|
54
|
+
progId: "BraveHTML",
|
|
55
|
+
mac: "Brave Browser",
|
|
56
|
+
linux: ["brave-browser", "brave-browser-stable", "brave"],
|
|
57
|
+
desktop: ["brave-browser", "brave_brave"],
|
|
58
|
+
};
|
|
59
|
+
const chromium = {
|
|
60
|
+
windows: ["Chromium", "Application", "chrome.exe"],
|
|
61
|
+
progId: "ChromiumHTM",
|
|
62
|
+
mac: "Chromium",
|
|
63
|
+
linux: ["chromium", "chromium-browser"],
|
|
64
|
+
desktop: ["chromium", "chromium-browser", "chromium_chromium"],
|
|
65
|
+
};
|
|
66
|
+
const chromiums = [chrome, edge, brave, chromium];
|
|
67
|
+
// Windows keeps the default browser for http in UserChoiceLatest when it has one, which it then reads in place of UserChoice; macOS is not asked.
|
|
68
|
+
async function defaultBrowser(platform, system) {
|
|
69
|
+
if (platform === "win32") {
|
|
70
|
+
const choices = await system.output("reg", ["query", String.raw `HKCU\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http`, "/s"]);
|
|
71
|
+
const progId = progIdIn(choices, String.raw `\UserChoiceLatest\ProgId`) ?? progIdIn(choices, String.raw `\UserChoice`);
|
|
72
|
+
return chromiums.find((browser) => progId === browser.progId || progId?.startsWith(`${browser.progId}.`) === true);
|
|
73
|
+
}
|
|
74
|
+
if (platform === "linux") {
|
|
75
|
+
const desktop = (await system.output("xdg-settings", ["get", "default-web-browser"])).trim().replace(/\.desktop$/, "");
|
|
76
|
+
return chromiums.find((browser) => browser.desktop.includes(desktop));
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
// The ProgId under the key of reg's answer that ends so.
|
|
81
|
+
function progIdIn(choices, key) {
|
|
82
|
+
let inKey = false;
|
|
83
|
+
for (const line of choices.split(/\r?\n/)) {
|
|
84
|
+
if (line.startsWith("HKEY_")) {
|
|
85
|
+
inKey = line.trimEnd().endsWith(key);
|
|
86
|
+
}
|
|
87
|
+
else if (inKey) {
|
|
88
|
+
const value = /^\s+ProgId\s+REG_SZ\s+(\S+)/.exec(line);
|
|
89
|
+
if (value !== null) {
|
|
90
|
+
return value[1];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
22
95
|
}
|
|
23
|
-
//
|
|
24
|
-
function browserPaths(platform, env) {
|
|
96
|
+
// Where each system installs a browser: Program Files and the user's AppData, the Applications of macOS, the PATH of Linux.
|
|
97
|
+
function browserPaths(browser, platform, env) {
|
|
25
98
|
if (platform === "win32") {
|
|
26
99
|
const roots = [env.ProgramFiles, env["ProgramFiles(x86)"], env.LOCALAPPDATA].filter((root) => root !== undefined && root !== "");
|
|
27
|
-
|
|
28
|
-
return programs.flatMap((program) => roots.map((root) => win32.join(root, ...program)));
|
|
100
|
+
return roots.map((root) => win32.join(root, ...browser.windows));
|
|
29
101
|
}
|
|
30
102
|
if (platform === "darwin") {
|
|
31
103
|
const roots = ["/Applications", ...(env.HOME === undefined ? [] : [posix.join(env.HOME, "Applications")])];
|
|
32
|
-
return
|
|
104
|
+
return roots.map((root) => posix.join(root, `${browser.mac}.app`, "Contents", "MacOS", browser.mac));
|
|
33
105
|
}
|
|
34
106
|
const folders = (env.PATH ?? "").split(":").filter((folder) => folder !== "");
|
|
35
|
-
|
|
36
|
-
return names.flatMap((name) => folders.map((folder) => posix.join(folder, name)));
|
|
107
|
+
return browser.linux.flatMap((name) => folders.map((folder) => posix.join(folder, name)));
|
|
37
108
|
}
|
|
109
|
+
const realSystem = {
|
|
110
|
+
exists: existsSync,
|
|
111
|
+
output: (command, args) => new Promise((done) => {
|
|
112
|
+
// A second at most: the window would rather open in Chrome or Edge than wait on a registry or a desktop that does not answer.
|
|
113
|
+
execFile(command, args, { windowsHide: true, timeout: 1000 }, (error, output) => done(error === null ? output : ""));
|
|
114
|
+
}),
|
|
115
|
+
};
|
|
38
116
|
// A browser started apart from this process, which it outlives: whether it started.
|
|
39
117
|
function started(browser, args) {
|
|
40
118
|
return new Promise((done) => {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Codex's hooks as Chesspal last wrote them, which Codex asks the player to trust: the first time, or changed over ones trusted before. */
|
|
2
|
+
export type HooksNews = "new" | "changed";
|
|
3
|
+
/** Sets the table's folder up as init does, noting when Chesspal wrote Codex's hooks: Codex then asks the player to trust them. */
|
|
4
|
+
export declare function setUpTable(folder: string, platform: string): void;
|
|
5
|
+
/** What Chesspal wrote of Codex's hooks since the player last heard it; the note is taken. One that cannot be read is no note. */
|
|
6
|
+
export declare function takeHooksNews(folder: string): HooksNews | undefined;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// The table's folder set up as init does, for the table and for `chesspal play` alike: when Chesspal writes Codex's hooks, the first time or again over
|
|
2
|
+
// older ones, a note says so, which the next `chesspal play codex` takes, for the player to hear it whichever of the two wrote them.
|
|
3
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { writeAtomically } from "../../../games/index.js";
|
|
6
|
+
import { codexHooksPath, setUpHosts } from "../../../host-files/index.js";
|
|
7
|
+
/** Sets the table's folder up as init does, noting when Chesspal wrote Codex's hooks: Codex then asks the player to trust them. */
|
|
8
|
+
export function setUpTable(folder, platform) {
|
|
9
|
+
const hooks = codexHooksPath(folder);
|
|
10
|
+
const hadHooks = existsSync(hooks);
|
|
11
|
+
if (setUpHosts(folder, platform).includes(hooks)) {
|
|
12
|
+
// Hooks never trusted stay new, whatever Chesspal wrote over them before Codex asked.
|
|
13
|
+
const news = hadHooks && readNote(folder) !== "new" ? "changed" : "new";
|
|
14
|
+
writeAtomically(join(folder, hooksNoteFile), `${JSON.stringify({ hooks: news })}\n`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** What Chesspal wrote of Codex's hooks since the player last heard it; the note is taken. One that cannot be read is no note. */
|
|
18
|
+
export function takeHooksNews(folder) {
|
|
19
|
+
const news = readNote(folder);
|
|
20
|
+
try {
|
|
21
|
+
rmSync(join(folder, hooksNoteFile), { force: true });
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// A note that cannot go now (the table rewrites it) is said again at the next play, which is harmless.
|
|
25
|
+
}
|
|
26
|
+
return news;
|
|
27
|
+
}
|
|
28
|
+
const hooksNoteFile = "codex-hooks-note.json";
|
|
29
|
+
// The note's news, none when there is no note or it cannot be read.
|
|
30
|
+
function readNote(folder) {
|
|
31
|
+
try {
|
|
32
|
+
const hooks = JSON.parse(readFileSync(join(folder, hooksNoteFile), "utf8"))?.hooks;
|
|
33
|
+
return hooks === "new" || hooks === "changed" ? hooks : undefined;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/** The hosts the page offers, by their command's name. */
|
|
2
2
|
export declare const hostNames: readonly ["claude", "codex"];
|
|
3
3
|
export type HostName = typeof hostNames[number];
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* The models and efforts the player may choose for a host, as its command line names them: aliases, which follow the host's newest models; and the
|
|
6
|
+
* `model` and `effort` a chat starts with until the player chooses, the middle ones.
|
|
7
|
+
*/
|
|
5
8
|
export interface HostChoices {
|
|
6
9
|
readonly models: readonly string[];
|
|
7
10
|
readonly efforts: readonly string[];
|
|
11
|
+
readonly model: string;
|
|
12
|
+
readonly effort: string;
|
|
8
13
|
}
|
|
9
|
-
/** What the player chose for a host on the page: none of either
|
|
14
|
+
/** What the player chose for a host on the page: none of either, or one the host does not offer, is the host's middle one. */
|
|
10
15
|
export interface HostChoice {
|
|
11
16
|
readonly model?: string | undefined;
|
|
12
17
|
readonly effort?: string | undefined;
|
|
@@ -18,8 +23,11 @@ export interface HostChoice {
|
|
|
18
23
|
export declare const hostChoices: Readonly<Record<HostName, HostChoices>>;
|
|
19
24
|
/** Whether a word names a host the page offers. */
|
|
20
25
|
export declare function isHostName(name: unknown): name is HostName;
|
|
21
|
-
/**
|
|
22
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The message a chat opened by the table starts with, in the player's language: the AI answers it by calling status, as every chat does. A chat opened
|
|
28
|
+
* `toAnalyse` the game the page shows is asked for that: told to play, it opened the New game window over the analysis it had just started.
|
|
29
|
+
*/
|
|
30
|
+
export declare function startMessage(language: string | undefined, toAnalyse?: boolean): string;
|
|
23
31
|
/**
|
|
24
32
|
* The start message and the choices the host knows: first for Claude Code, whose options may take the word after; last for Codex, where "resume" is no
|
|
25
33
|
* subcommand, kept from its update screen, which an Enter would accept.
|
|
@@ -30,5 +38,5 @@ export declare function choiceKeys(host: HostName): {
|
|
|
30
38
|
model: string;
|
|
31
39
|
effort: string;
|
|
32
40
|
};
|
|
33
|
-
/** A host's choice as the profile's settings keep it: the values the host offers alone, the rest is
|
|
41
|
+
/** A host's choice as the profile's settings keep it: the values the host offers alone, the rest is its middle one. */
|
|
34
42
|
export declare function choiceIn(settings: Readonly<Record<string, unknown>> | undefined, host: HostName): HostChoice;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The AI hosts the table opens a chat in (D-104): Claude Code and Codex, each with the start message, and the model and effort the player chose on the page
|
|
2
|
-
// for the profile in use
|
|
2
|
+
// for the profile in use, or each host's middle ones until they choose: the chat never starts with whatever the host was left at.
|
|
3
3
|
import { speaksItalian } from "../../../commands/index.js";
|
|
4
4
|
/** The hosts the page offers, by their command's name. */
|
|
5
5
|
export const hostNames = ["claude", "codex"];
|
|
@@ -8,15 +8,21 @@ export const hostNames = ["claude", "codex"];
|
|
|
8
8
|
* models a ChatGPT account lists, which change with its releases.
|
|
9
9
|
*/
|
|
10
10
|
export const hostChoices = {
|
|
11
|
-
claude: { models: ["fable", "opus", "sonnet", "haiku"], efforts: ["low", "medium", "high", "xhigh", "max"] },
|
|
12
|
-
codex: { models: ["gpt-6.1-sol", "gpt-6-astra", "gpt-6.1-terra", "gpt-6-luna"], efforts: ["low", "medium", "high", "xhigh", "max"] },
|
|
11
|
+
claude: { models: ["fable", "opus", "sonnet", "haiku"], efforts: ["low", "medium", "high", "xhigh", "max"], model: "sonnet", effort: "medium" },
|
|
12
|
+
codex: { models: ["gpt-6.1-sol", "gpt-6-astra", "gpt-6.1-terra", "gpt-6-luna"], efforts: ["low", "medium", "high", "xhigh", "max"], model: "gpt-6.1-terra", effort: "medium" },
|
|
13
13
|
};
|
|
14
14
|
/** Whether a word names a host the page offers. */
|
|
15
15
|
export function isHostName(name) {
|
|
16
16
|
return hostNames.includes(name);
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* The message a chat opened by the table starts with, in the player's language: the AI answers it by calling status, as every chat does. A chat opened
|
|
20
|
+
* `toAnalyse` the game the page shows is asked for that: told to play, it opened the New game window over the analysis it had just started.
|
|
21
|
+
*/
|
|
22
|
+
export function startMessage(language, toAnalyse = false) {
|
|
23
|
+
if (toAnalyse) {
|
|
24
|
+
return speaksItalian(language) ? "Analizziamo la partita!" : "Let's analyse the game!";
|
|
25
|
+
}
|
|
20
26
|
return speaksItalian(language) ? "Giochiamo!" : "Let's play!";
|
|
21
27
|
}
|
|
22
28
|
/**
|
|
@@ -34,16 +40,16 @@ export function hostArgs(host, message, choice = {}) {
|
|
|
34
40
|
export function choiceKeys(host) {
|
|
35
41
|
return { model: `${host}Model`, effort: `${host}Effort` };
|
|
36
42
|
}
|
|
37
|
-
/** A host's choice as the profile's settings keep it: the values the host offers alone, the rest is
|
|
43
|
+
/** A host's choice as the profile's settings keep it: the values the host offers alone, the rest is its middle one. */
|
|
38
44
|
export function choiceIn(settings, host) {
|
|
39
45
|
const keys = choiceKeys(host);
|
|
40
46
|
const text = (value) => (typeof value === "string" ? value : undefined);
|
|
41
47
|
return offered(host, { model: text(settings?.[keys.model]), effort: text(settings?.[keys.effort]) });
|
|
42
48
|
}
|
|
43
|
-
// The choice kept to the values the host offers.
|
|
49
|
+
// The choice kept to the values the host offers: its middle model and effort where none is chosen, or one it does not offer.
|
|
44
50
|
function offered(host, choice) {
|
|
45
|
-
const
|
|
46
|
-
const model = choice.model !== undefined && models.includes(choice.model) ? choice.model :
|
|
47
|
-
const effort = choice.effort !== undefined && efforts.includes(choice.effort) ? choice.effort :
|
|
51
|
+
const choices = hostChoices[host];
|
|
52
|
+
const model = choice.model !== undefined && choices.models.includes(choice.model) ? choice.model : choices.model;
|
|
53
|
+
const effort = choice.effort !== undefined && choices.efforts.includes(choice.effort) ? choice.effort : choices.effort;
|
|
48
54
|
return { model, effort };
|
|
49
55
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// The AI hosts a table opens a chat in (D-104): their choices, the terminal they open in, and `chesspal play`, which starts one there.
|
|
2
2
|
export { terminalChats } from "./chats.js";
|
|
3
|
+
export { setUpTable } from "./hooks.js";
|
|
3
4
|
export { isHostName } from "./hosts.js";
|
|
4
5
|
export { endTree, hostLaunch, playHost } from "./play.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ChildProcess } from "node:child_process";
|
|
2
2
|
import { type Context } from "../../../commands/index.ts";
|
|
3
|
+
import { type HooksNews } from "./hooks.ts";
|
|
3
4
|
import { type HostChoice, type HostName } from "./hosts.ts";
|
|
4
5
|
/**
|
|
5
6
|
* What `chesspal play` starts: the host's program, its arguments, folder and environment, whether it needs cmd, as a .cmd shim does on Windows, and the
|
|
@@ -12,13 +13,14 @@ export interface HostLaunch {
|
|
|
12
13
|
readonly env: NodeJS.ProcessEnv;
|
|
13
14
|
readonly shell: boolean;
|
|
14
15
|
readonly choice: HostChoice;
|
|
15
|
-
/** Codex's hooks
|
|
16
|
-
readonly
|
|
16
|
+
/** Chesspal wrote Codex's hooks, the first time or again: Codex runs them only once the player trusts them. */
|
|
17
|
+
readonly hooks?: HooksNews;
|
|
17
18
|
}
|
|
18
19
|
/**
|
|
19
20
|
* Runs the host in the table's folder, in this terminal, until it ends: its exit code; started again, clean, when the table asks for a new game (D-57),
|
|
20
|
-
* or for another model or host, which then takes its place (D-107); after an update, by the Chesspal installed (D-108).
|
|
21
|
-
*
|
|
21
|
+
* or for another model or host, which then takes its place (D-107); after an update, by the Chesspal installed (D-108). Claude Code closed at its
|
|
22
|
+
* start with an error, as when the player keeps No at its question on the folder, is told how to answer and started once more. Ctrl+C is the host's:
|
|
23
|
+
* this process ignores it, or the terminal would close with the chat.
|
|
22
24
|
*/
|
|
23
25
|
export declare function playHost(context: Context, host: HostName): Promise<number>;
|
|
24
26
|
/**
|
|
@@ -4,17 +4,19 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { win32 } from "node:path";
|
|
6
6
|
import { speaksItalian } from "../../../commands/index.js";
|
|
7
|
+
import { endOf, gameInAnalysis, loadGame } from "../../../games/index.js";
|
|
7
8
|
import { tableFolder } from "../../../home/index.js";
|
|
8
|
-
import { codexHooksPath, setUpHosts } from "../../../host-files/index.js";
|
|
9
9
|
import { activeProfile, gameFolder, languageIn, profileSettings } from "../../../profiles/index.js";
|
|
10
10
|
import { programPath } from "./find.js";
|
|
11
|
+
import { setUpTable, takeHooksNews } from "./hooks.js";
|
|
11
12
|
import { choiceIn, hostArgs, isHostName, startMessage } from "./hosts.js";
|
|
12
13
|
import { askedAgain, leaveSeat, refreshSeat, seatTimes, takeSeat } from "./seat.js";
|
|
13
14
|
import { cmdQuoted } from "./terminal.js";
|
|
14
15
|
/**
|
|
15
16
|
* Runs the host in the table's folder, in this terminal, until it ends: its exit code; started again, clean, when the table asks for a new game (D-57),
|
|
16
|
-
* or for another model or host, which then takes its place (D-107); after an update, by the Chesspal installed (D-108).
|
|
17
|
-
*
|
|
17
|
+
* or for another model or host, which then takes its place (D-107); after an update, by the Chesspal installed (D-108). Claude Code closed at its
|
|
18
|
+
* start with an error, as when the player keeps No at its question on the folder, is told how to answer and started once more. Ctrl+C is the host's:
|
|
19
|
+
* this process ignores it, or the terminal would close with the chat.
|
|
18
20
|
*/
|
|
19
21
|
export async function playHost(context, host) {
|
|
20
22
|
const folder = tableFolder(context.home());
|
|
@@ -33,6 +35,8 @@ export async function playHost(context, host) {
|
|
|
33
35
|
let refreshed = Date.now();
|
|
34
36
|
// Chesspal was updated under this process (D-108): the new one plays on.
|
|
35
37
|
let updated = false;
|
|
38
|
+
// Claude Code was started once more after it closed at its start.
|
|
39
|
+
let startedOnceMore = false;
|
|
36
40
|
const asked = () => {
|
|
37
41
|
if (Date.now() - refreshed >= seatTimes.refresh) {
|
|
38
42
|
refreshed = Date.now();
|
|
@@ -47,6 +51,7 @@ export async function playHost(context, host) {
|
|
|
47
51
|
for (;;) {
|
|
48
52
|
// A request left from a second click while the host before was ending is not for this one.
|
|
49
53
|
askedAgain(folder, process.pid);
|
|
54
|
+
const startedAt = context.now();
|
|
50
55
|
const ran = await started(context, playing, asked).catch((error) => {
|
|
51
56
|
if (before === undefined) {
|
|
52
57
|
throw error;
|
|
@@ -61,7 +66,16 @@ export async function playHost(context, host) {
|
|
|
61
66
|
}
|
|
62
67
|
before = undefined;
|
|
63
68
|
if (!ran.again) {
|
|
64
|
-
|
|
69
|
+
if (playing !== "claude" || ran.code === 0 || context.now().getTime() - startedAt.getTime() >= closedAtStart) {
|
|
70
|
+
return ran.code;
|
|
71
|
+
}
|
|
72
|
+
const words = speaksItalian(context.language) ? closedAtStartWords.italian : closedAtStartWords.english;
|
|
73
|
+
process.stdout.write(`\n${words.join("\n")}\n`);
|
|
74
|
+
if (startedOnceMore) {
|
|
75
|
+
return ran.code;
|
|
76
|
+
}
|
|
77
|
+
startedOnceMore = true;
|
|
78
|
+
continue;
|
|
65
79
|
}
|
|
66
80
|
if (updated) {
|
|
67
81
|
terminal.restore();
|
|
@@ -121,33 +135,59 @@ export function hostLaunch(context, host, env = process.env, exists) {
|
|
|
121
135
|
if (found === undefined) {
|
|
122
136
|
throw new Error(`${hostTitle[host]} is not installed on this computer, or not on its PATH: install it (${installHint[host]}), then try again.`);
|
|
123
137
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
138
|
+
setUpTable(folder, context.platform);
|
|
139
|
+
// Codex's hooks written, by this play or by the table before it: the note is for Codex's chat, and waits for it.
|
|
140
|
+
const news = host === "codex" ? takeHooksNews(folder) : undefined;
|
|
141
|
+
const hooks = news === undefined ? {} : { hooks: news };
|
|
127
142
|
const choice = choiceIn(profileSettings(home, activeProfile(home).id), host);
|
|
128
|
-
const
|
|
143
|
+
const games = gameFolder(home);
|
|
144
|
+
const args = hostArgs(host, startMessage(languageIn(games, context.language), analysisAsked(games)), choice);
|
|
129
145
|
// An effort the player set for every chat would win over the one chosen on the page: spawn leaves out a variable that is undefined.
|
|
130
146
|
const effort = host === "claude" && choice.effort !== undefined ? { CLAUDE_CODE_EFFORT_LEVEL: undefined } : {};
|
|
131
147
|
// The seat this play holds, by which its chat's server tells the table it is the chat held, and not another opened in the same folder.
|
|
132
148
|
const childEnv = { ...env, ...Object.fromEntries(sessionMarks.map((name) => [name, undefined])), ...effort, CHESSPAL_SEAT: String(process.pid) };
|
|
133
149
|
const script = npmScript(host, found, context.platform, exists ?? existsSync);
|
|
134
150
|
if (script !== undefined) {
|
|
135
|
-
return { program: process.execPath, args: [script, ...args], folder, env: childEnv, shell: false, choice, ...
|
|
151
|
+
return { program: process.execPath, args: [script, ...args], folder, env: childEnv, shell: false, choice, ...hooks };
|
|
152
|
+
}
|
|
153
|
+
return { program: found, args, folder, env: childEnv, shell: context.platform === "win32" && /\.(cmd|bat)$/i.test(found), choice, ...hooks };
|
|
154
|
+
}
|
|
155
|
+
/** Whether the board page shows a game in analysis, none going on: the chat opened there is for that game, brought to review or to talk over, not for a new one. */
|
|
156
|
+
function analysisAsked(folder) {
|
|
157
|
+
try {
|
|
158
|
+
const current = loadGame(folder);
|
|
159
|
+
return (current === undefined || endOf(current.game) !== undefined) && gameInAnalysis(folder) !== undefined;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// A game that cannot be read is status's to report.
|
|
163
|
+
return false;
|
|
136
164
|
}
|
|
137
|
-
return { program: found, args, folder, env: childEnv, shell: context.platform === "win32" && /\.(cmd|bat)$/i.test(found), choice, ...hooksChanged };
|
|
138
165
|
}
|
|
139
166
|
const hostTitle = { claude: "Claude Code", codex: "Codex" };
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
167
|
+
// What the player is told before Codex asks about the hooks Chesspal wrote: the first time, and when they changed.
|
|
168
|
+
const hooksWords = {
|
|
169
|
+
new: {
|
|
170
|
+
english: "Codex will ask about Chesspal's two hooks, small helpers that let it see your moves on the board: choose Trust all and continue.",
|
|
171
|
+
italian: "Codex ti chiederà dei due «hook» di Chesspal, piccoli aiuti che gli fanno vedere le tue mosse sulla scacchiera: scegli Trust all and continue.",
|
|
172
|
+
},
|
|
173
|
+
changed: {
|
|
174
|
+
english: "Chesspal's hooks for Codex have changed: when Codex asks about them, choose Trust all again.",
|
|
175
|
+
italian: "Gli «hook» di Chesspal per Codex sono cambiati: quando Codex te lo chiede, scegli di nuovo Trust all.",
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
// What the player is told when Claude Code closed at its start: its question on the folder starts on No.
|
|
179
|
+
const closedAtStartWords = {
|
|
180
|
+
english: ["Claude Code closed as soon as it opened.", 'If it asks whether you trust this folder, go down to "Yes, I trust this folder" with the arrow keys, then press Enter.'],
|
|
181
|
+
italian: ["Claude Code si è chiuso appena aperto.", "Se ti chiede se ti fidi della cartella, scendi con le frecce su «Yes, I trust this folder», poi premi Invio."],
|
|
143
182
|
};
|
|
144
183
|
// Runs the host as runHost does, the seat noting the model and effort it is given; one not installed, or that could not start, fails saying so.
|
|
145
184
|
async function started(context, host, again) {
|
|
146
185
|
const launch = hostLaunch(context, host);
|
|
147
186
|
takeSeat(launch.folder, { pid: process.pid, host, ...launch.choice });
|
|
148
|
-
//
|
|
149
|
-
if (launch.
|
|
150
|
-
|
|
187
|
+
// Codex asks the player to trust the hooks Chesspal wrote: said here, above its question, as init says it.
|
|
188
|
+
if (launch.hooks !== undefined) {
|
|
189
|
+
const words = hooksWords[launch.hooks];
|
|
190
|
+
process.stdout.write(`${speaksItalian(context.language) ? words.italian : words.english}\n`);
|
|
151
191
|
}
|
|
152
192
|
return runHost(launch, again, context.platform).catch((error) => {
|
|
153
193
|
throw new Error(`${hostTitle[host]} did not start: ${error.message}`);
|
|
@@ -179,6 +219,8 @@ function playUpdated(host) {
|
|
|
179
219
|
const killAfter = 3_000;
|
|
180
220
|
// A play process that ends sooner never got to its host.
|
|
181
221
|
const quickExit = 10_000;
|
|
222
|
+
// Claude Code ends 1.5 s after No at its question on the folder, a chat the player ends ends with no error (Priya): a minute is the time to read it.
|
|
223
|
+
const closedAtStart = 60_000;
|
|
182
224
|
// A host ended in the middle leaves the modes it set: colours, scroll region, synchronized output, bracketed paste, focus and mouse reports, kitty's
|
|
183
225
|
// keyboard flags, its own screen, a hidden cursor (Priya). All off, and the screen cleared, as a new chat.
|
|
184
226
|
const cleanScreen = "\x1b[0m\x1b[r\x1b[?2026l\x1b[?2004l\x1b[?1004l\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[<99u\x1b[?1049l\x1b[?25h\x1b[2J\x1b[3J\x1b[H";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { appBrowser, openedAddress } from "./browser.ts";
|
|
1
|
+
export { appBrowser, type BrowserSystem, type Opened, openedAddress } from "./browser.ts";
|
|
2
2
|
export { hostLaunch, isHostName, playHost, terminalChats } from "./hosts/index.ts";
|
|
3
3
|
export { iconAsked, type IconCommand, iconCommand, type IconPlaces, iconToRemake, makeIcons, noteIconAsked, noteIconMade, removeIcons } from "./icon/index.ts";
|
|
4
4
|
export { noteFailure, openTable, processLauncher, type TableLauncher } from "./launch.ts";
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { type Context } from "../../commands/index.ts";
|
|
2
|
+
import { type Opened } from "./browser.ts";
|
|
2
3
|
/** What opening the table does outside this process: start the table apart, open the browser, wait. The tests give their own. */
|
|
3
4
|
export interface TableLauncher {
|
|
4
5
|
/** Starts `chesspal table` apart from this process, which it outlives, in the data folder. */
|
|
5
6
|
start(home: string): void;
|
|
6
|
-
/** Opens the page in the browser:
|
|
7
|
-
open(url: string): Promise<
|
|
7
|
+
/** Opens the page in the browser, its window marked with its opening when the table gave one: how it opened. */
|
|
8
|
+
open(url: string, opening?: number): Promise<Opened>;
|
|
8
9
|
wait(milliseconds: number): Promise<void>;
|
|
9
10
|
}
|
|
10
11
|
/**
|
|
11
12
|
* Opens the table's page in the browser, the table started first when none answers, or when the one that does is of another version, which is asked to
|
|
12
|
-
* quit; a table of this version another `chesspal` started meanwhile is as good.
|
|
13
|
-
* within ten seconds throws.
|
|
13
|
+
* quit; a table of this version another `chesspal` started meanwhile is as good. The window opened last stays, the table's older ones close, and none opens
|
|
14
|
+
* while another is opening (D-104). What to tell the player, in their language; a table that does not start within ten seconds throws.
|
|
14
15
|
*/
|
|
15
16
|
export declare function openTable(context: Context, launcher?: TableLauncher): Promise<string>;
|
|
16
17
|
/**
|
|
@@ -7,11 +7,11 @@ import { speaksItalian } from "../../commands/index.js";
|
|
|
7
7
|
import { tableFolder } from "../../home/index.js";
|
|
8
8
|
import { version } from "../../version.js";
|
|
9
9
|
import { openInBrowser } from "./browser.js";
|
|
10
|
-
import { askToQuit, tableAt } from "./table.js";
|
|
10
|
+
import { askToOpen, askToQuit, askToRelease, tableAt } from "./table.js";
|
|
11
11
|
/**
|
|
12
12
|
* Opens the table's page in the browser, the table started first when none answers, or when the one that does is of another version, which is asked to
|
|
13
|
-
* quit; a table of this version another `chesspal` started meanwhile is as good.
|
|
14
|
-
* within ten seconds throws.
|
|
13
|
+
* quit; a table of this version another `chesspal` started meanwhile is as good. The window opened last stays, the table's older ones close, and none opens
|
|
14
|
+
* while another is opening (D-104). What to tell the player, in their language; a table that does not start within ten seconds throws.
|
|
15
15
|
*/
|
|
16
16
|
export async function openTable(context, launcher = processLauncher(context.platform)) {
|
|
17
17
|
const home = context.home();
|
|
@@ -23,7 +23,7 @@ export async function openTable(context, launcher = processLauncher(context.plat
|
|
|
23
23
|
for (;;) {
|
|
24
24
|
const table = await tableAt(folder);
|
|
25
25
|
if (table?.version === version) {
|
|
26
|
-
return styleText(["bold", "green"], `♜ ${(await
|
|
26
|
+
return styleText(["bold", "green"], `♜ ${(await openWindow(table.url, table.port, launcher)) ? words.opened(table.url) : words.notOpened(table.url)}`);
|
|
27
27
|
}
|
|
28
28
|
if (table !== undefined && !asked.has(table.port)) {
|
|
29
29
|
asked.add(table.port);
|
|
@@ -60,10 +60,23 @@ export function processLauncher(platform, command = [...process.execArgv, proces
|
|
|
60
60
|
// Its own process group, with no console and nothing of this one's: the terminal can close, the table stays.
|
|
61
61
|
spawn(process.execPath, [...command, "table"], { cwd: home, detached: true, stdio: "ignore", windowsHide: true }).unref();
|
|
62
62
|
},
|
|
63
|
-
open: (url) => openInBrowser(url, platform),
|
|
63
|
+
open: (url, opening) => openInBrowser(url, platform, opening),
|
|
64
64
|
wait: (milliseconds) => new Promise((done) => setTimeout(done, milliseconds)),
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
+
// The table's window, opened unless another opening has not reached the table yet, as two clicks on the icon ask: then the one opening is as good. One
|
|
68
|
+
// that will never reach it, as no browser started or only a tab with no opening did, is let go at once: the next chesspal opens its own.
|
|
69
|
+
async function openWindow(url, port, launcher) {
|
|
70
|
+
const opening = await askToOpen(port);
|
|
71
|
+
if (opening === null) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
const opened = await launcher.open(url, opening);
|
|
75
|
+
if (opening !== undefined && opened !== "window") {
|
|
76
|
+
await askToRelease(port, opening);
|
|
77
|
+
}
|
|
78
|
+
return opened !== "none";
|
|
79
|
+
}
|
|
67
80
|
// A table answers within ten seconds of its start, an older one quitting first, looked for every tenth of a second.
|
|
68
81
|
const startWait = 10_000;
|
|
69
82
|
const every = 100;
|
|
@@ -32,3 +32,10 @@ export declare function startTable(context: Context, times?: TableTimes, chats?:
|
|
|
32
32
|
export declare function tableAt(folder: string): Promise<TableFound | undefined>;
|
|
33
33
|
/** Asks the table at a port to quit; it answers before it goes, and a table that cannot be reached, or does not answer within a second or so, is let be. */
|
|
34
34
|
export declare function askToQuit(port: number): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Asks the table at a port for an opening of its window: its number, which the window carries, null while another opening has not reached the table
|
|
37
|
+
* yet, so that no second window opens; undefined when the table did not answer.
|
|
38
|
+
*/
|
|
39
|
+
export declare function askToOpen(port: number): Promise<number | null | undefined>;
|
|
40
|
+
/** Lets go of the opening the table at a port gave, whose window will never reach it: the next `chesspal` opens its own at once. */
|
|
41
|
+
export declare function askToRelease(port: number, opening: number): Promise<void>;
|