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.
Files changed (184) hide show
  1. package/README.md +11 -12
  2. package/dist/cli/cli.js +83 -3
  3. package/dist/cli/command-line.js +26 -9
  4. package/dist/cli/icon.d.ts +25 -0
  5. package/dist/cli/icon.js +80 -0
  6. package/dist/commands/agents.d.ts +19 -2
  7. package/dist/commands/agents.js +39 -13
  8. package/dist/commands/ai-say.js +3 -4
  9. package/dist/commands/analyse-game.d.ts +4 -0
  10. package/dist/commands/analyse-game.js +46 -13
  11. package/dist/commands/annotate-game.d.ts +2 -0
  12. package/dist/commands/annotate-game.js +40 -18
  13. package/dist/commands/answer-lines.d.ts +2 -0
  14. package/dist/commands/answer-lines.js +13 -3
  15. package/dist/commands/coach.js +4 -2
  16. package/dist/commands/context.d.ts +11 -1
  17. package/dist/commands/context.js +5 -0
  18. package/dist/commands/delete-review.js +9 -5
  19. package/dist/commands/game-summary.d.ts +2 -0
  20. package/dist/commands/game-summary.js +4 -0
  21. package/dist/commands/index.d.ts +3 -2
  22. package/dist/commands/index.js +7 -4
  23. package/dist/commands/init.js +2 -4
  24. package/dist/commands/instructions.d.ts +9 -0
  25. package/dist/commands/instructions.js +30 -0
  26. package/dist/commands/lead-input.d.ts +1 -1
  27. package/dist/commands/lead-input.js +7 -4
  28. package/dist/commands/page-link.js +6 -5
  29. package/dist/commands/past-games.d.ts +1 -1
  30. package/dist/commands/past-games.js +12 -5
  31. package/dist/commands/player-move.js +2 -3
  32. package/dist/commands/profiles.d.ts +23 -9
  33. package/dist/commands/profiles.js +76 -35
  34. package/dist/commands/ratings.d.ts +8 -0
  35. package/dist/commands/ratings.js +45 -0
  36. package/dist/commands/registry.d.ts +1 -1
  37. package/dist/commands/registry.js +3 -2
  38. package/dist/commands/say.d.ts +1 -1
  39. package/dist/commands/say.js +1 -1
  40. package/dist/commands/status.d.ts +3 -1
  41. package/dist/commands/status.js +46 -27
  42. package/dist/commands/study.js +8 -5
  43. package/dist/core/index.d.ts +1 -1
  44. package/dist/core/index.js +1 -1
  45. package/dist/core/review.d.ts +2 -0
  46. package/dist/core/review.js +5 -1
  47. package/dist/games/ai-model.d.ts +4 -2
  48. package/dist/games/ai-model.js +10 -1
  49. package/dist/games/analysis.d.ts +26 -3
  50. package/dist/games/analysis.js +41 -6
  51. package/dist/games/archive.d.ts +6 -3
  52. package/dist/games/archive.js +13 -5
  53. package/dist/games/current-game.d.ts +10 -1
  54. package/dist/games/current-game.js +49 -15
  55. package/dist/games/files.d.ts +5 -0
  56. package/dist/games/files.js +42 -1
  57. package/dist/games/index.d.ts +3 -3
  58. package/dist/games/index.js +3 -3
  59. package/dist/home/data-folder.d.ts +2 -0
  60. package/dist/home/data-folder.js +5 -1
  61. package/dist/home/index.d.ts +1 -1
  62. package/dist/home/index.js +1 -1
  63. package/dist/host-files/agents.d.ts +2 -2
  64. package/dist/host-files/agents.js +12 -6
  65. package/dist/host-files/host-files.d.ts +5 -3
  66. package/dist/host-files/host-files.js +15 -8
  67. package/dist/host-files/index.d.ts +2 -2
  68. package/dist/host-files/index.js +2 -2
  69. package/dist/page/app/app.js +49 -20
  70. package/dist/page/app/title.js +17 -0
  71. package/dist/page/avatars/torretta.js +4 -4
  72. package/dist/page/dialogs/analyses.js +4 -4
  73. package/dist/page/dialogs/dialog.js +4 -1
  74. package/dist/page/dialogs/dialogs.css +84 -10
  75. package/dist/page/dialogs/profiles.js +96 -43
  76. package/dist/page/game/column.js +5 -1
  77. package/dist/page/game/plies.js +7 -4
  78. package/dist/page/guide/blocks.js +67 -0
  79. package/dist/page/guide/en.md +66 -0
  80. package/dist/page/guide/foot.js +18 -0
  81. package/dist/page/guide/guide.css +200 -0
  82. package/dist/page/guide/guide.js +89 -0
  83. package/dist/page/guide/it.md +66 -0
  84. package/dist/page/guide/markdown.js +67 -0
  85. package/dist/page/helps/helps.css +7 -10
  86. package/dist/page/layout/favicon.svg +13 -0
  87. package/dist/page/layout/header.js +3 -3
  88. package/dist/page/layout/icon.icns +0 -0
  89. package/dist/page/layout/icon.ico +0 -0
  90. package/dist/page/layout/icon.png +0 -0
  91. package/dist/page/layout/layout.css +85 -4
  92. package/dist/page/layout/window.js +45 -0
  93. package/dist/page/main.js +4 -1
  94. package/dist/page/players/card.js +1 -1
  95. package/dist/page/profile/profile.js +14 -9
  96. package/dist/page/profile/settings.js +9 -0
  97. package/dist/page/style.css +2 -0
  98. package/dist/page/table/table.css +123 -0
  99. package/dist/page/table/window.js +130 -0
  100. package/dist/page/talk/chat.css +23 -5
  101. package/dist/page/talk/talk.css +38 -26
  102. package/dist/page/talk/talk.js +6 -4
  103. package/dist/page/talk/voice.js +35 -9
  104. package/dist/page/text.js +131 -17
  105. package/dist/page/theme.css +7 -0
  106. package/dist/profiles/index.d.ts +2 -1
  107. package/dist/profiles/index.js +2 -1
  108. package/dist/profiles/profiles.d.ts +10 -15
  109. package/dist/profiles/profiles.js +30 -15
  110. package/dist/profiles/ratings.d.ts +35 -0
  111. package/dist/profiles/ratings.js +61 -0
  112. package/dist/server/bell.d.ts +24 -3
  113. package/dist/server/bell.js +84 -10
  114. package/dist/server/caller.d.ts +2 -1
  115. package/dist/server/caller.js +3 -2
  116. package/dist/server/mcp.d.ts +7 -3
  117. package/dist/server/mcp.js +100 -52
  118. package/dist/server/page/analyses.d.ts +2 -0
  119. package/dist/server/page/analyses.js +20 -5
  120. package/dist/server/page/attach.d.ts +4 -0
  121. package/dist/server/page/attach.js +112 -0
  122. package/dist/server/page/chat.d.ts +65 -0
  123. package/dist/server/page/chat.js +87 -0
  124. package/dist/server/page/events.d.ts +7 -0
  125. package/dist/server/page/events.js +20 -0
  126. package/dist/server/page/files.js +10 -1
  127. package/dist/server/page/game.js +43 -23
  128. package/dist/server/page/page.d.ts +51 -7
  129. package/dist/server/page/page.js +74 -48
  130. package/dist/server/page/profiles.d.ts +5 -0
  131. package/dist/server/page/profiles.js +26 -10
  132. package/dist/server/page/table.d.ts +58 -0
  133. package/dist/server/page/table.js +210 -0
  134. package/dist/server/table/browser.d.ts +6 -0
  135. package/dist/server/table/browser.js +48 -0
  136. package/dist/server/table/hosts/chats.d.ts +7 -0
  137. package/dist/server/table/hosts/chats.js +34 -0
  138. package/dist/server/table/hosts/find.d.ts +2 -0
  139. package/dist/server/table/hosts/find.js +20 -0
  140. package/dist/server/table/hosts/hosts.d.ts +34 -0
  141. package/dist/server/table/hosts/hosts.js +49 -0
  142. package/dist/server/table/hosts/index.d.ts +3 -0
  143. package/dist/server/table/hosts/index.js +4 -0
  144. package/dist/server/table/hosts/play.d.ts +29 -0
  145. package/dist/server/table/hosts/play.js +180 -0
  146. package/dist/server/table/hosts/seat.d.ts +33 -0
  147. package/dist/server/table/hosts/seat.js +80 -0
  148. package/dist/server/table/hosts/terminal.d.ts +6 -0
  149. package/dist/server/table/hosts/terminal.js +63 -0
  150. package/dist/server/table/icon/index.d.ts +32 -0
  151. package/dist/server/table/icon/index.js +83 -0
  152. package/dist/server/table/icon/linux.d.ts +5 -0
  153. package/dist/server/table/icon/linux.js +47 -0
  154. package/dist/server/table/icon/macos.d.ts +3 -0
  155. package/dist/server/table/icon/macos.js +38 -0
  156. package/dist/server/table/icon/windows.d.ts +3 -0
  157. package/dist/server/table/icon/windows.js +48 -0
  158. package/dist/server/table/index.d.ts +6 -0
  159. package/dist/server/table/index.js +7 -0
  160. package/dist/server/table/launch.d.ts +19 -0
  161. package/dist/server/table/launch.js +78 -0
  162. package/dist/server/table/link.d.ts +10 -0
  163. package/dist/server/table/link.js +231 -0
  164. package/dist/server/table/table.d.ts +35 -0
  165. package/dist/server/table/table.js +94 -0
  166. package/dist/sites/chesscom.d.ts +4 -0
  167. package/dist/sites/chesscom.js +25 -0
  168. package/dist/sites/choice.d.ts +17 -0
  169. package/dist/sites/choice.js +13 -0
  170. package/dist/sites/index.d.ts +2 -0
  171. package/dist/sites/index.js +4 -0
  172. package/dist/sites/lichess.d.ts +4 -0
  173. package/dist/sites/lichess.js +32 -0
  174. package/dist/sites/request.d.ts +11 -0
  175. package/dist/sites/request.js +15 -0
  176. package/dist/sites/sites.d.ts +6 -0
  177. package/dist/sites/sites.js +33 -0
  178. package/instructions/analysis.md +1 -1
  179. package/instructions/analyst-agent.md +1 -1
  180. package/instructions/coach.md +2 -1
  181. package/instructions/opponent.md +6 -6
  182. package/instructions/ponder.md +1 -0
  183. package/instructions/start.md +1 -0
  184. package/package.json +1 -1
@@ -0,0 +1,34 @@
1
+ import { tableFolder } from "../../../home/index.js";
2
+ import { samePath } from "../../bell.js";
3
+ import { programPath } from "./find.js";
4
+ import { hostChoices, hostNames, isHostName } from "./hosts.js";
5
+ import { askAgain, seatHeld } from "./seat.js";
6
+ import { openTerminal } from "./terminal.js";
7
+ /**
8
+ * The chats of a real table: the hosts installed, looked for again once a while has passed, since the player may install one with the page open; a
9
+ * terminal opened with `chesspal play`, run by `chesspal`, the same Node and script as the table's.
10
+ */
11
+ export function terminalChats(context, chesspal = [process.execPath, ...process.execArgv, process.argv[1] ?? ""], env = process.env) {
12
+ let found;
13
+ return {
14
+ hosts: () => {
15
+ const now = context.now().getTime();
16
+ if (found === undefined || now - found.at >= lookAgain) {
17
+ found = { hosts: hostNames.map((name) => chatHost(name, context.platform, env)), at: now };
18
+ }
19
+ return found.hosts;
20
+ },
21
+ open: async (host) => isHostName(host) && await openTerminal(tableFolder(context.home()), [...chesspal, "play", host], context.platform, env),
22
+ // Only a chat in the table's folder can be the one chesspal play holds.
23
+ held: (folder) => {
24
+ const seat = samePath(folder, tableFolder(context.home())) ? seatHeld(folder, context.now()) : undefined;
25
+ return seat === undefined ? undefined : { model: seat.model, effort: seat.effort };
26
+ },
27
+ relaunch: (folder, host) => samePath(folder, tableFolder(context.home())) && askAgain(folder, context.now(), host === undefined ? {} : { host }),
28
+ };
29
+ }
30
+ // Looking on the PATH takes a few milliseconds, and the page asks at every change of the game.
31
+ const lookAgain = 10_000;
32
+ function chatHost(name, platform, env) {
33
+ return { name, installed: programPath(name, platform, env) !== undefined, ...hostChoices[name], command: `chesspal play ${name}` };
34
+ }
@@ -0,0 +1,2 @@
1
+ /** The program's path on the PATH of `env`, the first found; none when it is not installed there. */
2
+ export declare function programPath(name: string, platform: string, env: NodeJS.ProcessEnv, exists?: (path: string) => boolean): string | undefined;
@@ -0,0 +1,20 @@
1
+ // A program found on the PATH, as a shell would find it: on Windows with the extensions of PATHEXT, which npm's shims (.cmd) and installers (.exe) use.
2
+ import { existsSync, lstatSync } from "node:fs";
3
+ import { posix, win32 } from "node:path";
4
+ /** The program's path on the PATH of `env`, the first found; none when it is not installed there. */
5
+ export function programPath(name, platform, env, exists = present) {
6
+ const windows = platform === "win32";
7
+ const path = windows ? win32 : posix;
8
+ const folders = (env.PATH ?? env.Path ?? "").split(windows ? ";" : ":").filter((folder) => folder.trim() !== "");
9
+ const extensions = windows ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((extension) => extension !== "").map((extension) => extension.toLowerCase()) : [""];
10
+ return folders.flatMap((folder) => extensions.map((extension) => path.join(folder.trim(), `${name}${extension}`))).find(exists);
11
+ }
12
+ // Windows' app execution aliases, such as wt.exe in WindowsApps, are links that stat cannot follow: existsSync calls them missing, lstat finds them.
13
+ function present(path) {
14
+ try {
15
+ return existsSync(path) || lstatSync(path, { throwIfNoEntry: false }) !== undefined;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
@@ -0,0 +1,34 @@
1
+ /** The hosts the page offers, by their command's name. */
2
+ export declare const hostNames: readonly ["claude", "codex"];
3
+ export type HostName = typeof hostNames[number];
4
+ /** 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. */
5
+ export interface HostChoices {
6
+ readonly models: readonly string[];
7
+ readonly efforts: readonly string[];
8
+ }
9
+ /** What the player chose for a host on the page: none of either keeps the host's own. */
10
+ export interface HostChoice {
11
+ readonly model?: string | undefined;
12
+ readonly effort?: string | undefined;
13
+ }
14
+ /**
15
+ * Each host's choices, as tried in each (Priya): a value outside them would fail only at the chat's first turn. Claude Code's are its aliases; Codex's, the
16
+ * models a ChatGPT account lists, which change with its releases.
17
+ */
18
+ export declare const hostChoices: Readonly<Record<HostName, HostChoices>>;
19
+ /** Whether a word names a host the page offers. */
20
+ export declare function isHostName(name: unknown): name is HostName;
21
+ /** 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. */
22
+ export declare function startMessage(language: string | undefined): string;
23
+ /**
24
+ * 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
+ * subcommand, kept from its update screen, which an Enter would accept.
26
+ */
27
+ export declare function hostArgs(host: HostName, message: string, choice?: HostChoice): string[];
28
+ /** The settings' keys that keep a host's choice in the profile: claudeModel, claudeEffort, codexModel, codexEffort. */
29
+ export declare function choiceKeys(host: HostName): {
30
+ model: string;
31
+ effort: string;
32
+ };
33
+ /** A host's choice as the profile's settings keep it: the values the host offers alone, the rest is the host's own. */
34
+ export declare function choiceIn(settings: Readonly<Record<string, unknown>> | undefined, host: HostName): HostChoice;
@@ -0,0 +1,49 @@
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; "as the host" passes nothing, and the host's own settings stand.
3
+ import { speaksItalian } from "../../../commands/index.js";
4
+ /** The hosts the page offers, by their command's name. */
5
+ export const hostNames = ["claude", "codex"];
6
+ /**
7
+ * Each host's choices, as tried in each (Priya): a value outside them would fail only at the chat's first turn. Claude Code's are its aliases; Codex's, the
8
+ * models a ChatGPT account lists, which change with its releases.
9
+ */
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"] },
13
+ };
14
+ /** Whether a word names a host the page offers. */
15
+ export function isHostName(name) {
16
+ return hostNames.includes(name);
17
+ }
18
+ /** 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. */
19
+ export function startMessage(language) {
20
+ return speaksItalian(language) ? "Giochiamo!" : "Let's play!";
21
+ }
22
+ /**
23
+ * 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
24
+ * subcommand, kept from its update screen, which an Enter would accept.
25
+ */
26
+ export function hostArgs(host, message, choice = {}) {
27
+ const { model, effort } = offered(host, choice);
28
+ if (host === "claude") {
29
+ return [message, ...(model === undefined ? [] : ["--model", model]), ...(effort === undefined ? [] : ["--effort", effort])];
30
+ }
31
+ return ["-c", "check_for_update_on_startup=false", ...(model === undefined ? [] : ["-m", model]), ...(effort === undefined ? [] : ["-c", `model_reasoning_effort=${effort}`]), message];
32
+ }
33
+ /** The settings' keys that keep a host's choice in the profile: claudeModel, claudeEffort, codexModel, codexEffort. */
34
+ export function choiceKeys(host) {
35
+ return { model: `${host}Model`, effort: `${host}Effort` };
36
+ }
37
+ /** A host's choice as the profile's settings keep it: the values the host offers alone, the rest is the host's own. */
38
+ export function choiceIn(settings, host) {
39
+ const keys = choiceKeys(host);
40
+ const text = (value) => (typeof value === "string" ? value : undefined);
41
+ return offered(host, { model: text(settings?.[keys.model]), effort: text(settings?.[keys.effort]) });
42
+ }
43
+ // The choice kept to the values the host offers.
44
+ function offered(host, choice) {
45
+ const { models, efforts } = hostChoices[host];
46
+ const model = choice.model !== undefined && models.includes(choice.model) ? choice.model : undefined;
47
+ const effort = choice.effort !== undefined && efforts.includes(choice.effort) ? choice.effort : undefined;
48
+ return { model, effort };
49
+ }
@@ -0,0 +1,3 @@
1
+ export { terminalChats } from "./chats.ts";
2
+ export { isHostName } from "./hosts.ts";
3
+ export { hostLaunch, playHost } from "./play.ts";
@@ -0,0 +1,4 @@
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
+ export { terminalChats } from "./chats.js";
3
+ export { isHostName } from "./hosts.js";
4
+ export { hostLaunch, playHost } from "./play.js";
@@ -0,0 +1,29 @@
1
+ import type { Context } from "../../../commands/index.ts";
2
+ import { type HostChoice, type HostName } from "./hosts.ts";
3
+ /**
4
+ * 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
5
+ * model and effort it is given, which the seat notes for the page (D-107).
6
+ */
7
+ export interface HostLaunch {
8
+ readonly program: string;
9
+ readonly args: readonly string[];
10
+ readonly folder: string;
11
+ readonly env: NodeJS.ProcessEnv;
12
+ readonly shell: boolean;
13
+ readonly choice: HostChoice;
14
+ }
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),
17
+ * or for another model or host, which then takes its place (D-107). Ctrl+C is the host's: this process ignores it, or the terminal would close with the chat.
18
+ */
19
+ export declare function playHost(context: Context, host: HostName): Promise<number>;
20
+ /**
21
+ * Runs the host until it ends, or until `again`, asked every half second, says to start it again: then it is ended with what it started, killed when it
22
+ * does not end. Its exit code, and whether to start it again.
23
+ */
24
+ export declare function runHost(launch: HostLaunch, again: () => boolean, platform?: string, every?: number): Promise<{
25
+ code: number;
26
+ again: boolean;
27
+ }>;
28
+ /** How the host starts for the profile in use: found on the PATH, with its choice; it throws, saying how to install it, when it is not there. */
29
+ export declare function hostLaunch(context: Context, host: HostName, env?: NodeJS.ProcessEnv, exists?: (path: string) => boolean): HostLaunch;
@@ -0,0 +1,180 @@
1
+ // `chesspal play claude` (D-104): the chat the table's button opens, in the terminal the table opened for it, or one the player typed it in: the host started
2
+ // in the table's folder with the start message, and the model and effort chosen on the page for the profile in use; started again for a new game.
3
+ import { spawn, spawnSync } from "node:child_process";
4
+ import { existsSync } from "node:fs";
5
+ import { win32 } from "node:path";
6
+ import { tableFolder } from "../../../home/index.js";
7
+ import { setUpHosts } from "../../../host-files/index.js";
8
+ import { activeProfile, gameFolder, languageIn, profileSettings } from "../../../profiles/index.js";
9
+ import { programPath } from "./find.js";
10
+ import { choiceIn, hostArgs, isHostName, startMessage } from "./hosts.js";
11
+ import { askedAgain, leaveSeat, refreshSeat, seatTimes, takeSeat } from "./seat.js";
12
+ import { cmdQuoted } from "./terminal.js";
13
+ /**
14
+ * 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),
15
+ * or for another model or host, which then takes its place (D-107). Ctrl+C is the host's: this process ignores it, or the terminal would close with the chat.
16
+ */
17
+ export async function playHost(context, host) {
18
+ const folder = tableFolder(context.home());
19
+ takeSeat(folder, { pid: process.pid, host });
20
+ process.on("SIGINT", () => undefined);
21
+ // A window closed with its X ends this process without its finally: the seat goes first.
22
+ process.on("SIGHUP", () => {
23
+ leaveSeat(folder, process.pid);
24
+ process.exit(129);
25
+ });
26
+ const terminal = savedTerminal(context.platform);
27
+ let playing = host;
28
+ let next = host;
29
+ // The host before one asked in its place, until that one has started: it goes on when the other does not start.
30
+ let before;
31
+ let refreshed = Date.now();
32
+ const asked = () => {
33
+ if (Date.now() - refreshed >= seatTimes.refresh) {
34
+ refreshed = Date.now();
35
+ refreshSeat(folder, process.pid);
36
+ }
37
+ const again = askedAgain(folder, process.pid);
38
+ next = again?.host !== undefined && isHostName(again.host) ? again.host : playing;
39
+ return again !== undefined;
40
+ };
41
+ try {
42
+ for (;;) {
43
+ // A request left from a second click while the host before was ending is not for this one.
44
+ askedAgain(folder, process.pid);
45
+ const ran = await started(context, playing, asked).catch((error) => {
46
+ if (before === undefined) {
47
+ throw error;
48
+ }
49
+ process.stderr.write(`${error.message} ${hostTitle[before]} goes on.\n`);
50
+ return undefined;
51
+ });
52
+ if (ran === undefined) {
53
+ playing = before ?? playing;
54
+ before = undefined;
55
+ continue;
56
+ }
57
+ before = undefined;
58
+ if (!ran.again) {
59
+ return ran.code;
60
+ }
61
+ if (next !== playing) {
62
+ before = playing;
63
+ playing = next;
64
+ }
65
+ terminal.restore();
66
+ process.stdout.write(cleanScreen);
67
+ }
68
+ }
69
+ finally {
70
+ leaveSeat(folder, process.pid);
71
+ }
72
+ }
73
+ /**
74
+ * Runs the host until it ends, or until `again`, asked every half second, says to start it again: then it is ended with what it started, killed when it
75
+ * does not end. Its exit code, and whether to start it again.
76
+ */
77
+ export function runHost(launch, again, platform = process.platform, every = 500) {
78
+ return new Promise((done, failed) => {
79
+ const words = [launch.program, ...launch.args];
80
+ const command = launch.shell ? [words.map(cmdQuoted).join(" ")] : words;
81
+ const chat = spawn(command[0], command.slice(1), { cwd: launch.folder, env: launch.env, stdio: "inherit", shell: launch.shell });
82
+ let ended = false;
83
+ let killing;
84
+ const watch = setInterval(() => {
85
+ if (!ended && again()) {
86
+ ended = true;
87
+ endTree(chat, platform);
88
+ killing = setTimeout(() => chat.kill("SIGKILL"), killAfter);
89
+ }
90
+ }, every);
91
+ const stop = () => {
92
+ clearInterval(watch);
93
+ clearTimeout(killing);
94
+ };
95
+ chat.once("error", (error) => {
96
+ stop();
97
+ failed(error);
98
+ });
99
+ chat.once("exit", (code) => {
100
+ stop();
101
+ done({ code: code ?? 1, again: ended || again() });
102
+ });
103
+ });
104
+ }
105
+ /** How the host starts for the profile in use: found on the PATH, with its choice; it throws, saying how to install it, when it is not there. */
106
+ export function hostLaunch(context, host, env = process.env, exists) {
107
+ const home = context.home();
108
+ const folder = tableFolder(home);
109
+ const found = programPath(host, context.platform, env, exists);
110
+ if (found === undefined) {
111
+ throw new Error(`${hostTitle[host]} is not installed on this computer, or not on its PATH: install it (${installHint[host]}), then try again.`);
112
+ }
113
+ setUpHosts(folder, context.platform);
114
+ const choice = choiceIn(profileSettings(home, activeProfile(home).id), host);
115
+ const args = hostArgs(host, startMessage(languageIn(gameFolder(home), context.language)), choice);
116
+ // 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.
117
+ const effort = host === "claude" && choice.effort !== undefined ? { CLAUDE_CODE_EFFORT_LEVEL: undefined } : {};
118
+ const childEnv = { ...env, ...Object.fromEntries(sessionMarks.map((name) => [name, undefined])), ...effort };
119
+ const script = npmScript(host, found, context.platform, exists ?? existsSync);
120
+ if (script !== undefined) {
121
+ return { program: process.execPath, args: [script, ...args], folder, env: childEnv, shell: false, choice };
122
+ }
123
+ return { program: found, args, folder, env: childEnv, shell: context.platform === "win32" && /\.(cmd|bat)$/i.test(found), choice };
124
+ }
125
+ const hostTitle = { claude: "Claude Code", codex: "Codex" };
126
+ // 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.
127
+ async function started(context, host, again) {
128
+ const launch = hostLaunch(context, host);
129
+ takeSeat(launch.folder, { pid: process.pid, host, ...launch.choice });
130
+ return runHost(launch, again, context.platform).catch((error) => {
131
+ throw new Error(`${hostTitle[host]} did not start: ${error.message}`);
132
+ });
133
+ }
134
+ // A host that has not ended this long after it was asked is killed.
135
+ const killAfter = 3_000;
136
+ // A host ended in the middle leaves the modes it set: colours, scroll region, synchronized output, bracketed paste, focus and mouse reports, kitty's
137
+ // keyboard flags, its own screen, a hidden cursor (Priya). All off, and the screen cleared, as a new chat.
138
+ 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";
139
+ // The host and what it started, its MCP server among them: on Windows its whole tree, which taskkill ends, never the console (Priya); else the host,
140
+ // which ends its own. A taskkill that failed leaves it to the kill that follows.
141
+ function endTree(chat, platform) {
142
+ if (platform !== "win32" || chat.pid === undefined) {
143
+ chat.kill("SIGTERM");
144
+ return;
145
+ }
146
+ const taskkill = win32.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "taskkill.exe");
147
+ const ended = spawnSync(taskkill, ["/T", "/F", "/PID", String(chat.pid)], { stdio: "ignore", windowsHide: true });
148
+ if (ended.status !== 0) {
149
+ chat.kill();
150
+ }
151
+ }
152
+ /**
153
+ * The terminal's settings before the first host, which one ended in the middle leaves raw, for the next to take as its own; stty keeps them on macOS and
154
+ * Linux, while on Windows each host sets the console's itself.
155
+ */
156
+ function savedTerminal(platform) {
157
+ const saved = platform === "win32" || !process.stdin.isTTY ? undefined : spawnSync("stty", ["-g"], { stdio: ["inherit", "pipe", "ignore"], encoding: "utf8" });
158
+ const settings = saved?.status === 0 ? saved.stdout.trim() : undefined;
159
+ return {
160
+ restore: () => {
161
+ if (settings !== undefined) {
162
+ spawnSync("stty", [settings], { stdio: ["inherit", "ignore", "ignore"] });
163
+ }
164
+ },
165
+ };
166
+ }
167
+ // What marks a chat's own session, which a table started from inside a chat would hand down, and the host take for its own (Priya).
168
+ const sessionMarks = ["CLAUDECODE", "CLAUDE_CODE_SESSION_ID", "CLAUDE_CODE_CHILD_SESSION", "CLAUDE_CODE_SESSION_ATTENDED", "CLAUDE_PID", "CLAUDE_EFFORT", "AI_AGENT", "TRACEPARENT"];
169
+ const installHint = { claude: "https://claude.com/claude-code", codex: "npm install -g @openai/codex" };
170
+ /**
171
+ * The script behind npm's Windows shim of Codex, which Node runs as the shim would, without cmd and its quoting of the start message (Priya); none for
172
+ * another program, or when it is not beside the shim.
173
+ */
174
+ function npmScript(host, program, platform, exists) {
175
+ if (host !== "codex" || platform !== "win32" || !/\.cmd$/i.test(program)) {
176
+ return undefined;
177
+ }
178
+ const script = win32.join(win32.dirname(program), "node_modules", "@openai", "codex", "bin", "codex.js");
179
+ return exists(script) ? script : undefined;
180
+ }
@@ -0,0 +1,33 @@
1
+ /** The play process in the seat: its process id, its host, and the model and effort it started that host with, none for the host's own. */
2
+ export interface Seat {
3
+ readonly pid: number;
4
+ readonly host: string;
5
+ readonly model?: string | undefined;
6
+ readonly effort?: string | undefined;
7
+ }
8
+ /**
9
+ * Takes the seat for this play process, over a request left for one before it; it throws while another one holds it. A seat is held while its play
10
+ * refreshes it: one closed with its window, or stopped, lets it go within `heldFor`, whatever process takes its id after it.
11
+ */
12
+ export declare function takeSeat(folder: string, seat: Seat, now?: Date): void;
13
+ /** Tells the table this play process still holds the seat; nothing when it lost it. */
14
+ export declare function refreshSeat(folder: string, pid: number, now?: Date): void;
15
+ /** Leaves the seat, when this play process still holds it. */
16
+ export declare function leaveSeat(folder: string, pid: number): void;
17
+ /** Whether a play process holds the seat. */
18
+ export declare function seatTaken(folder: string, now?: Date): boolean;
19
+ /** The play process that holds the seat, with the model and effort it started its host with; none when no one holds it. */
20
+ export declare function seatHeld(folder: string, now?: Date): Seat | undefined;
21
+ /** Asks the play process in the seat to start its host again, or `again.host` in its place: whether one holds it, and was asked. */
22
+ export declare function askAgain(folder: string, now?: Date, again?: Again): boolean;
23
+ /** What the table asks the play process in the seat: to start its host again, or the host named in its place (D-107). */
24
+ export interface Again {
25
+ readonly host?: string;
26
+ }
27
+ /** What the table asked this play process, when it asked it to start its host again; the request is taken. */
28
+ export declare function askedAgain(folder: string, pid: number): Again | undefined;
29
+ /** How often a play process refreshes its seat, and how long the seat stays held without it. */
30
+ export declare const seatTimes: {
31
+ refresh: number;
32
+ heldFor: number;
33
+ };
@@ -0,0 +1,80 @@
1
+ // The seat `chesspal play` takes at the table (D-104): the play process notes itself in the table's folder while its host runs, one at a time, and the table
2
+ // asks it there to start its host again, a clean chat for a new game (D-57).
3
+ import { mkdirSync, readFileSync, rmSync, statSync, utimesSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { writeAtomically } from "../../../games/index.js";
6
+ /**
7
+ * Takes the seat for this play process, over a request left for one before it; it throws while another one holds it. A seat is held while its play
8
+ * refreshes it: one closed with its window, or stopped, lets it go within `heldFor`, whatever process takes its id after it.
9
+ */
10
+ export function takeSeat(folder, seat, now = new Date()) {
11
+ const held = seatHeld(folder, now);
12
+ if (held !== undefined && held.pid !== seat.pid) {
13
+ throw new Error(`A chat in ${held.host} already plays at the table, in another window: play there, or close it, then try again.`);
14
+ }
15
+ // chesspal play may come before the table has ever been opened.
16
+ mkdirSync(folder, { recursive: true });
17
+ rmSync(join(folder, againFile), { force: true });
18
+ writeAtomically(join(folder, seatFile), `${JSON.stringify(seat)}\n`);
19
+ }
20
+ /** Tells the table this play process still holds the seat; nothing when it lost it. */
21
+ export function refreshSeat(folder, pid, now = new Date()) {
22
+ if (seatIn(folder)?.pid === pid) {
23
+ utimesSync(join(folder, seatFile), now, now);
24
+ }
25
+ }
26
+ /** Leaves the seat, when this play process still holds it. */
27
+ export function leaveSeat(folder, pid) {
28
+ if (seatIn(folder)?.pid === pid) {
29
+ rmSync(join(folder, seatFile), { force: true });
30
+ }
31
+ }
32
+ /** Whether a play process holds the seat. */
33
+ export function seatTaken(folder, now = new Date()) {
34
+ return seatHeld(folder, now) !== undefined;
35
+ }
36
+ /** The play process that holds the seat, with the model and effort it started its host with; none when no one holds it. */
37
+ export function seatHeld(folder, now = new Date()) {
38
+ const seat = seatIn(folder);
39
+ // A file's time is finer than a Date's: it is compared to the millisecond, as the Date is.
40
+ const refreshed = statSync(join(folder, seatFile), { throwIfNoEntry: false })?.mtimeMs;
41
+ return seat !== undefined && refreshed !== undefined && now.getTime() - Math.floor(refreshed) < seatTimes.heldFor ? seat : undefined;
42
+ }
43
+ /** Asks the play process in the seat to start its host again, or `again.host` in its place: whether one holds it, and was asked. */
44
+ export function askAgain(folder, now = new Date(), again = {}) {
45
+ const held = seatHeld(folder, now);
46
+ if (held === undefined) {
47
+ return false;
48
+ }
49
+ writeAtomically(join(folder, againFile), `${JSON.stringify({ pid: held.pid, ...again })}\n`);
50
+ return true;
51
+ }
52
+ /** What the table asked this play process, when it asked it to start its host again; the request is taken. */
53
+ export function askedAgain(folder, pid) {
54
+ const asked = jsonIn(join(folder, againFile));
55
+ if (asked?.pid !== pid) {
56
+ return undefined;
57
+ }
58
+ rmSync(join(folder, againFile), { force: true });
59
+ return typeof asked.host === "string" ? { host: asked.host } : {};
60
+ }
61
+ /** How often a play process refreshes its seat, and how long the seat stays held without it. */
62
+ export const seatTimes = { refresh: 5_000, heldFor: 15_000 };
63
+ const seatFile = "play.json";
64
+ const againFile = "play-again.json";
65
+ function seatIn(folder) {
66
+ const seat = jsonIn(join(folder, seatFile));
67
+ if (typeof seat?.pid !== "number" || typeof seat.host !== "string") {
68
+ return undefined;
69
+ }
70
+ const text = (value) => (typeof value === "string" ? value : undefined);
71
+ return { pid: seat.pid, host: seat.host, model: text(seat.model), effort: text(seat.effort) };
72
+ }
73
+ function jsonIn(path) {
74
+ try {
75
+ return JSON.parse(readFileSync(path, "utf8"));
76
+ }
77
+ catch {
78
+ return undefined;
79
+ }
80
+ }
@@ -0,0 +1,6 @@
1
+ /** Opens a terminal window in `folder` running `command`, apart from the table, which it outlives: whether a terminal started. */
2
+ export declare function openTerminal(folder: string, command: readonly string[], platform: string, env?: NodeJS.ProcessEnv): Promise<boolean>;
3
+ /** The terminals to try, in order, each as the program and its arguments; those not installed are left out. */
4
+ export declare function terminals(folder: string, command: readonly string[], platform: string, env: NodeJS.ProcessEnv, exists?: (path: string) => boolean): string[][];
5
+ /** A word for cmd.exe's line, in double quotes when it holds a space or & | < > ^ ( ); cmd reads % and ! even inside them, which our words never hold. */
6
+ export declare function cmdQuoted(word: string): string;
@@ -0,0 +1,63 @@
1
+ // A terminal window the table opens for a chat (D-104), in a folder, running a command: Windows Terminal, else the console Windows starts; macOS's Terminal;
2
+ // on Linux the terminal the desktop names, else a common one.
3
+ import { spawn } from "node:child_process";
4
+ import { programPath } from "./find.js";
5
+ /** Opens a terminal window in `folder` running `command`, apart from the table, which it outlives: whether a terminal started. */
6
+ export async function openTerminal(folder, command, platform, env = process.env) {
7
+ for (const [program, ...args] of terminals(folder, command, platform, env)) {
8
+ if (program !== undefined && await started(program, args, folder)) {
9
+ return true;
10
+ }
11
+ }
12
+ return false;
13
+ }
14
+ /** The terminals to try, in order, each as the program and its arguments; those not installed are left out. */
15
+ export function terminals(folder, command, platform, env, exists) {
16
+ const installed = (name) => programPath(name, platform, env, exists);
17
+ if (platform === "win32") {
18
+ const windowsTerminal = installed("wt");
19
+ // start takes its first quoted word as the window's title, an empty one; cmd gets its line verbatim, each word quoted where it holds & or a space.
20
+ const windowsConsole = ["cmd.exe", "/d", "/s", "/c", `"start "" /D ${[folder, ...command].map(cmdQuoted).join(" ")}"`];
21
+ // wt splits its command line at every ;, and reads \; as one.
22
+ const wtWords = [folder, ...command].map((word) => word.replaceAll(";", "\\;"));
23
+ return windowsTerminal === undefined ? [windowsConsole] : [[windowsTerminal, "-w", "new", "-d", wtWords[0], "--title", "Chesspal", ...wtWords.slice(1)], windowsConsole];
24
+ }
25
+ if (platform === "darwin") {
26
+ const script = `cd ${shellQuoted(folder)} && exec ${command.map(shellQuoted).join(" ")}`;
27
+ return [["osascript", "-e", `tell application "Terminal" to do script "${appleQuoted(script)}"`, "-e", `tell application "Terminal" to activate`]];
28
+ }
29
+ const linux = [
30
+ ["x-terminal-emulator", "-e", ...command],
31
+ ["gnome-terminal", `--working-directory=${folder}`, "--", ...command],
32
+ ["konsole", "--workdir", folder, "-e", ...command],
33
+ ["xfce4-terminal", `--working-directory=${folder}`, "-x", ...command],
34
+ ["xterm", "-e", ...command],
35
+ ];
36
+ return linux.flatMap(([name, ...args]) => {
37
+ const path = installed(name ?? "");
38
+ return path === undefined ? [] : [[path, ...args]];
39
+ });
40
+ }
41
+ /** A word for cmd.exe's line, in double quotes when it holds a space or & | < > ^ ( ); cmd reads % and ! even inside them, which our words never hold. */
42
+ export function cmdQuoted(word) {
43
+ return /[\s&|<>^()!%]/.test(word) ? `"${word}"` : word;
44
+ }
45
+ // A program started apart from the table: whether it started. cmd's line is quoted already.
46
+ function started(program, args, folder) {
47
+ return new Promise((done) => {
48
+ const opened = spawn(program, args, { cwd: folder, detached: true, stdio: "ignore", windowsVerbatimArguments: program === "cmd.exe" });
49
+ opened.once("spawn", () => {
50
+ opened.unref();
51
+ done(true);
52
+ });
53
+ opened.once("error", () => done(false));
54
+ });
55
+ }
56
+ // A word for the shell, in single quotes, a quote in it closed and escaped.
57
+ function shellQuoted(word) {
58
+ return `'${word.replaceAll("'", "'\\''")}'`;
59
+ }
60
+ // Text inside an AppleScript string.
61
+ function appleQuoted(text) {
62
+ return text.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
63
+ }
@@ -0,0 +1,32 @@
1
+ /** What the icon starts: Node with the arguments of `chesspal` alone, in a folder. */
2
+ export interface IconCommand {
3
+ readonly node: string;
4
+ readonly args: readonly string[];
5
+ readonly folder: string;
6
+ }
7
+ /** The folders the icon goes in: the desktop and the system's menu of applications; the tests give their own. */
8
+ export interface IconPlaces {
9
+ readonly desktop: string;
10
+ readonly menu: string;
11
+ }
12
+ /**
13
+ * Puts Chesspal's icon on the desktop and in the menu, the system's own folders unless `places` gives others: the files made. It throws when the system
14
+ * refuses, with what went wrong.
15
+ */
16
+ export declare function makeIcons(platform: string, command: IconCommand, places?: IconPlaces): Promise<string[]>;
17
+ /**
18
+ * What the icon starts: the `chesspal` running now, the same Node and script, as `chesspal open`, in the data folder. Windows gives its hidden console a
19
+ * terminal, where `chesspal` alone could ask about the icon unseen; `open` never asks.
20
+ */
21
+ export declare function iconCommand(home: string): IconCommand;
22
+ /** Whether the command line asked already on this machine whether to put the icon there: once a machine, since the data folder may be on several. */
23
+ export declare function iconAsked(home: string, machine?: string): boolean;
24
+ /** Notes that the command line asked on this machine, whatever the answer. */
25
+ export declare function noteIconAsked(home: string, machine?: string): void;
26
+ /** Notes the icon made on this machine, its command and files, which asks no more there. */
27
+ export declare function noteIconMade(home: string, command: IconCommand, files: readonly string[], machine?: string): void;
28
+ /**
29
+ * Whether the icon made on this machine runs another Node or script than `command`, which a Node manager or an update may have taken away, and is still
30
+ * there: then it is made again. One the player removed stays removed.
31
+ */
32
+ export declare function iconToRemake(home: string, command: IconCommand, machine?: string, exists?: (path: string) => boolean): boolean;