chesspal 0.4.1 → 0.6.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.
Files changed (57) hide show
  1. package/README.md +37 -8
  2. package/dist/cli/chat-hook.d.ts +4 -1
  3. package/dist/cli/chat-hook.js +12 -5
  4. package/dist/cli/cli.js +3 -3
  5. package/dist/cli/command-line.js +9 -9
  6. package/dist/commands/game/new.js +3 -5
  7. package/dist/commands/registry/context.d.ts +5 -0
  8. package/dist/commands/registry/context.js +2 -1
  9. package/dist/commands/setup/init.d.ts +4 -1
  10. package/dist/commands/setup/init.js +30 -10
  11. package/dist/core/review.d.ts +2 -2
  12. package/dist/core/review.js +7 -3
  13. package/dist/host-files/host-files.d.ts +10 -6
  14. package/dist/host-files/host-files.js +57 -9
  15. package/dist/host-files/index.d.ts +1 -1
  16. package/dist/host-files/index.js +1 -1
  17. package/dist/page/app/app.js +112 -25
  18. package/dist/page/app/keys.js +16 -1
  19. package/dist/page/app/title.js +6 -3
  20. package/dist/page/board/board.css +6 -0
  21. package/dist/page/game/column.js +3 -3
  22. package/dist/page/game/game.css +121 -1
  23. package/dist/page/game/move-list.js +48 -10
  24. package/dist/page/game/variation.js +65 -0
  25. package/dist/page/guide/en.md +3 -2
  26. package/dist/page/guide/it.md +3 -2
  27. package/dist/page/profile/settings.js +2 -0
  28. package/dist/page/table/action.js +19 -7
  29. package/dist/page/table/window.js +7 -5
  30. package/dist/page/text.js +4 -2
  31. package/dist/server/bell/antigravity.d.ts +10 -0
  32. package/dist/server/bell/antigravity.js +75 -0
  33. package/dist/server/bell/bell.d.ts +10 -2
  34. package/dist/server/bell/bell.js +33 -10
  35. package/dist/server/bell/index.d.ts +1 -1
  36. package/dist/server/bell/index.js +2 -1
  37. package/dist/server/mcp/caller.js +26 -1
  38. package/dist/server/page/game.js +4 -3
  39. package/dist/server/page/page.js +1 -0
  40. package/dist/server/page/table.d.ts +14 -2
  41. package/dist/server/page/table.js +17 -5
  42. package/dist/server/table/hosts/chats.d.ts +4 -2
  43. package/dist/server/table/hosts/chats.js +24 -11
  44. package/dist/server/table/hosts/find.d.ts +6 -0
  45. package/dist/server/table/hosts/find.js +19 -1
  46. package/dist/server/table/hosts/hooks.d.ts +5 -2
  47. package/dist/server/table/hosts/hooks.js +6 -3
  48. package/dist/server/table/hosts/hosts.d.ts +19 -10
  49. package/dist/server/table/hosts/hosts.js +34 -16
  50. package/dist/server/table/hosts/models.d.ts +19 -0
  51. package/dist/server/table/hosts/models.js +95 -0
  52. package/dist/server/table/hosts/play.d.ts +7 -2
  53. package/dist/server/table/hosts/play.js +22 -12
  54. package/dist/server/table/hosts/seat.d.ts +5 -1
  55. package/dist/server/table/hosts/seat.js +1 -1
  56. package/dist/server/table/table.js +1 -1
  57. package/package.json +1 -1
@@ -62,22 +62,25 @@ export function answerTable(request, response, url, served) {
62
62
  }
63
63
  /**
64
64
  * Whether a new game starts the chat at the table again, clean (D-57): `chesspal play` holds it, it waits for the bell, no analyst of it is at work, and it
65
- * has seen a game move since it was first shown one; one that only greeted the player stays, as one opened from the chat's window before a new game.
65
+ * has seen a game move since it was first shown one; one that only greeted the player stays, as one opened from the chat's window before a new game. One
66
+ * `chesspal play` started for the analysis starts again all the same, at rest or still opening, or its first turn would analyse a game no longer there.
66
67
  */
67
68
  export function chatToRelaunch(served) {
68
69
  const { positionSeen, positionFirstSeen } = served.chat;
69
70
  const played = positionSeen?.started !== undefined && (positionFirstSeen === undefined || !samePosition(positionSeen, positionFirstSeen));
70
- return played && heldChat(served) !== undefined && chatAtRest(served);
71
+ return chatForAnalysis(served) ? served.attached === undefined || chatAtRest(served) : played && heldChat(served) !== undefined && chatAtRest(served);
71
72
  }
72
73
  /**
73
74
  * Asks `chesspal play` to start the chat at the table again: once the new game has started, or with another model or host in its place (D-107). Whether
74
75
  * it was asked; the pages then show it opening.
75
76
  */
76
77
  export function relaunchChat(served, how = "again", host) {
77
- const folder = served.attached?.chat.folder;
78
+ // One started for the analysis is asked again while it is still opening, in the table's folder.
79
+ const opening = served.attached === undefined && how === "again" && chatForAnalysis(served);
80
+ const folder = opening ? served.context.folder : served.attached?.chat.folder;
78
81
  // Another host may first ask whether it trusts the table's folder: the pages say so, as for a chat opened.
79
82
  const switching = how === "restart" && host !== undefined && host !== hostAtTable(served);
80
- if (folder === undefined || heldChat(served) === undefined || served.table?.chats?.relaunch(folder, host) !== true) {
83
+ if (folder === undefined || (!opening && heldChat(served) === undefined) || served.table?.chats?.relaunch(folder, host) !== true) {
81
84
  return false;
82
85
  }
83
86
  noteOpening(served, switching ? "chat" : how);
@@ -90,6 +93,15 @@ export function relaunchChat(served, how = "again", host) {
90
93
  export function chatToWaitFor(served) {
91
94
  return heldChat(served) !== undefined && !chatAtRest(served) && !analystAtWork(served);
92
95
  }
96
+ /**
97
+ * Whether the chat `chesspal play` holds was started for the analysis on the page: the one at the table, or, with none at it yet, the one still opening.
98
+ * A chat opened by hand never is.
99
+ */
100
+ export function chatForAnalysis(served) {
101
+ const { attached, context, table } = served;
102
+ const held = table?.chats?.held(attached?.chat.folder ?? context.folder);
103
+ return held?.analysis === true && (attached === undefined || held.seat === attached.chat.seat);
104
+ }
93
105
  /**
94
106
  * Whether the chat at the table has played the game `current` at length (D-57), counted from the first position it was shown of it. A chat the table
95
107
  * started again for a game began after it, and new_game takes it for one cleared since: it holds the whole game all the same.
@@ -287,7 +299,7 @@ function hostOffered(served, host) {
287
299
  function hostsOffered(served) {
288
300
  return `Name a host the table offers and this computer has: ${served.table?.chats?.hosts().filter((one) => one.installed).map((one) => one.name).join(", ") || "none"}.`;
289
301
  }
290
- // The host of the chat at the table, among those offered: it says its name as its MCP client does, «claude-code» or «codex-mcp-client».
302
+ // The host of the chat at the table, among those offered: it says its name as its MCP client does, «claude-code», «codex-mcp-client» or «antigravity-client».
291
303
  function hostAtTable(served) {
292
304
  const said = served.chat.host()?.toLowerCase() ?? "";
293
305
  return served.attached === undefined ? undefined : served.table?.chats?.hosts().find((one) => said.includes(one.name))?.name;
@@ -1,10 +1,12 @@
1
1
  import type { Context } from "../../../commands/index.ts";
2
2
  import type { TableChats } from "../../page/index.ts";
3
+ import { type RunProgram } from "./models.ts";
3
4
  /**
4
5
  * 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
5
- * terminal opened with `chesspal play`, run by `chesspal`, the same Node and script as the table's.
6
+ * terminal opened with `chesspal play`, run by `chesspal`, the same Node and script as the table's. Antigravity's models are read with `run` as soon
7
+ * as it is found, and again when it is looked for after a reading that failed.
6
8
  */
7
- export declare function terminalChats(context: Context, chesspal?: readonly string[], env?: NodeJS.ProcessEnv): TableChats;
9
+ export declare function terminalChats(context: Context, chesspal?: readonly string[], env?: NodeJS.ProcessEnv, run?: RunProgram): TableChats;
8
10
  /**
9
11
  * What the terminal runs: `chesspal play <host>`, with the table's data folder when it has one of its own, as a playtest's: Terminal.app and Windows
10
12
  * Terminal start the chat without the table's environment.
@@ -1,27 +1,40 @@
1
1
  import { samePath, tableFolder } from "../../../home/index.js";
2
- import { programPath } from "./find.js";
2
+ import { hostProgramPath } from "./find.js";
3
3
  import { hostChoices, hostNames, isHostName } from "./hosts.js";
4
+ import { antigravityModels } from "./models.js";
4
5
  import { askAgain, seatHeld } from "./seat.js";
5
6
  import { openTerminal } from "./terminal.js";
6
7
  /**
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
8
- * terminal opened with `chesspal play`, run by `chesspal`, the same Node and script as the table's.
9
+ * terminal opened with `chesspal play`, run by `chesspal`, the same Node and script as the table's. Antigravity's models are read with `run` as soon
10
+ * as it is found, and again when it is looked for after a reading that failed.
9
11
  */
10
- export function terminalChats(context, chesspal = [process.execPath, ...process.execArgv, process.argv[1] ?? ""], env = process.env) {
12
+ export function terminalChats(context, chesspal = [process.execPath, ...process.execArgv, process.argv[1] ?? ""], env = process.env, run) {
11
13
  let found;
14
+ const models = antigravityModels(tableFolder(context.home()), run);
15
+ const programs = () => {
16
+ const now = context.now().getTime();
17
+ if (found === undefined || now - found.at >= lookAgain) {
18
+ found = { programs: Object.fromEntries(hostNames.map((name) => [name, hostProgramPath(name, context.platform, env)])), at: now };
19
+ const agy = found.programs.antigravity;
20
+ if (agy !== undefined) {
21
+ models.read(agy);
22
+ }
23
+ }
24
+ return found.programs;
25
+ };
26
+ programs();
12
27
  return {
13
28
  hosts: () => {
14
- const now = context.now().getTime();
15
- if (found === undefined || now - found.at >= lookAgain) {
16
- found = { hosts: hostNames.map((name) => chatHost(name, context.platform, env)), at: now };
17
- }
18
- return found.hosts;
29
+ const found = programs();
30
+ return hostNames.map((name) => chatHost(name, found[name] !== undefined, name === "antigravity" ? models.choices() : hostChoices[name]));
19
31
  },
32
+ watch: (changed) => models.watch(changed),
20
33
  open: async (host) => isHostName(host) && await openTerminal(tableFolder(context.home()), playCommand(chesspal, host, env), context.platform, env),
21
34
  // Only a chat in the table's folder can be the one chesspal play holds.
22
35
  held: (folder) => {
23
36
  const seat = samePath(folder, tableFolder(context.home())) ? seatHeld(folder, context.now()) : undefined;
24
- return seat === undefined ? undefined : { model: seat.model, effort: seat.effort, seat: seat.pid };
37
+ return seat === undefined ? undefined : { model: seat.model, effort: seat.effort, seat: seat.pid, ...(seat.analysis === true ? { analysis: true } : {}) };
25
38
  },
26
39
  relaunch: (folder, host, updated) => samePath(folder, tableFolder(context.home())) && askAgain(folder, context.now(), { ...(host === undefined ? {} : { host }), ...(updated === true ? { updated } : {}) }),
27
40
  };
@@ -36,6 +49,6 @@ export function playCommand(chesspal, host, env) {
36
49
  }
37
50
  // Looking on the PATH takes a few milliseconds, and the page asks at every change of the game.
38
51
  const lookAgain = 10_000;
39
- function chatHost(name, platform, env) {
40
- return { name, installed: programPath(name, platform, env) !== undefined, ...hostChoices[name], command: `chesspal play ${name}` };
52
+ function chatHost(name, installed, choices) {
53
+ return { name, installed, ...choices, command: `chesspal play ${name}` };
41
54
  }
@@ -1,2 +1,8 @@
1
+ import { type HostName } from "./hosts.ts";
2
+ /**
3
+ * The host's program: on the PATH of `env`, else where its installer puts it, as Antigravity's, which a terminal opened before it was installed does not
4
+ * find; none when it is not installed.
5
+ */
6
+ export declare function hostProgramPath(host: HostName, platform: string, env: NodeJS.ProcessEnv, exists?: (path: string) => boolean): string | undefined;
1
7
  /** The program's path on the PATH of `env`, the first found; none when it is not installed there. */
2
8
  export declare function programPath(name: string, platform: string, env: NodeJS.ProcessEnv, exists?: (path: string) => boolean): string | undefined;
@@ -1,6 +1,24 @@
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.
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
+ // a host's, also where its installer puts it.
2
3
  import { existsSync, lstatSync } from "node:fs";
3
4
  import { posix, win32 } from "node:path";
5
+ import { hostPrograms } from "./hosts.js";
6
+ /**
7
+ * The host's program: on the PATH of `env`, else where its installer puts it, as Antigravity's, which a terminal opened before it was installed does not
8
+ * find; none when it is not installed.
9
+ */
10
+ export function hostProgramPath(host, platform, env, exists = present) {
11
+ const found = programPath(hostPrograms[host], platform, env, exists);
12
+ if (found !== undefined || host !== "antigravity") {
13
+ return found;
14
+ }
15
+ const base = platform === "win32" ? env.LOCALAPPDATA : env.HOME;
16
+ if (base === undefined || base.trim() === "") {
17
+ return undefined;
18
+ }
19
+ const installed = platform === "win32" ? win32.join(base, "agy", "bin", "agy.exe") : posix.join(base, ".local", "bin", "agy");
20
+ return exists(installed) ? installed : undefined;
21
+ }
4
22
  /** The program's path on the PATH of `env`, the first found; none when it is not installed there. */
5
23
  export function programPath(name, platform, env, exists = present) {
6
24
  const windows = platform === "win32";
@@ -1,6 +1,9 @@
1
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
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;
3
+ /**
4
+ * Sets the table's folder up as init does, Antigravity's settings in the user's home too when given, noting when Chesspal wrote Codex's hooks: Codex
5
+ * then asks the player to trust them.
6
+ */
7
+ export declare function setUpTable(folder: string, platform: string, userHome?: string): void;
5
8
  /** 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
9
  export declare function takeHooksNews(folder: string): HooksNews | undefined;
@@ -4,11 +4,14 @@ import { existsSync, readFileSync, rmSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { writeAtomically } from "../../../games/index.js";
6
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) {
7
+ /**
8
+ * Sets the table's folder up as init does, Antigravity's settings in the user's home too when given, noting when Chesspal wrote Codex's hooks: Codex
9
+ * then asks the player to trust them.
10
+ */
11
+ export function setUpTable(folder, platform, userHome) {
9
12
  const hooks = codexHooksPath(folder);
10
13
  const hadHooks = existsSync(hooks);
11
- if (setUpHosts(folder, platform).includes(hooks)) {
14
+ if (setUpHosts(folder, platform, userHome).includes(hooks)) {
12
15
  // Hooks never trusted stay new, whatever Chesspal wrote over them before Codex asked.
13
16
  const news = hadHooks && readNote(folder) !== "new" ? "changed" : "new";
14
17
  writeAtomically(join(folder, hooksNoteFile), `${JSON.stringify({ hooks: news })}\n`);
@@ -1,28 +1,35 @@
1
- /** The hosts the page offers, by their command's name. */
2
- export declare const hostNames: readonly ["claude", "codex"];
1
+ /** The hosts the page offers, by the name `chesspal play` takes. */
2
+ export declare const hostNames: readonly ["claude", "codex", "antigravity"];
3
3
  export type HostName = typeof hostNames[number];
4
+ /** Each host's program, as its installer names it. */
5
+ export declare const hostPrograms: Readonly<Record<HostName, string>>;
4
6
  /**
5
7
  * 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.
8
+ * `model` and `effort` a chat starts with until the player chooses, the middle ones. `modelEfforts`, where the models differ, the efforts of each,
9
+ * none for a model without; else every model has `efforts`.
7
10
  */
8
11
  export interface HostChoices {
9
12
  readonly models: readonly string[];
10
13
  readonly efforts: readonly string[];
11
14
  readonly model: string;
12
15
  readonly effort: string;
16
+ readonly modelEfforts?: Readonly<Record<string, readonly string[]>>;
13
17
  }
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. */
18
+ /** 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; no effort for a model without. */
15
19
  export interface HostChoice {
16
20
  readonly model?: string | undefined;
17
21
  readonly effort?: string | undefined;
18
22
  }
19
23
  /**
20
24
  * 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
21
- * models a ChatGPT account lists, which change with its releases.
25
+ * models a ChatGPT account lists, which change with its releases; Antigravity's, the Gemini Flash its free plan gives, its effort apart (D-115), until
26
+ * the table has read the models of the player's account.
22
27
  */
23
28
  export declare const hostChoices: Readonly<Record<HostName, HostChoices>>;
24
29
  /** Whether a word names a host the page offers. */
25
30
  export declare function isHostName(name: unknown): name is HostName;
31
+ /** The efforts a host offers for one of its models: none for a model that has no effort to choose. */
32
+ export declare function effortsOf(choices: HostChoices, model: string): readonly string[];
26
33
  /**
27
34
  * 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
35
  * `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.
@@ -30,13 +37,15 @@ export declare function isHostName(name: unknown): name is HostName;
30
37
  export declare function startMessage(language: string | undefined, toAnalyse?: boolean): string;
31
38
  /**
32
39
  * 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
33
- * subcommand, kept from its update screen, which an Enter would accept.
40
+ * subcommand, kept from its update screen, which an Enter would accept; for Antigravity after -i, which keeps the chat open, with Chesspal's tools
41
+ * allowed and the table's `folder` trusted for this chat alone, never saved in its settings: a folder trusted only at its first chat's dialog leaves that
42
+ * chat without its hooks. A model with no effort to choose is given none. `choices` are those the host offers, as read for Antigravity.
34
43
  */
35
- export declare function hostArgs(host: HostName, message: string, choice?: HostChoice): string[];
36
- /** The settings' keys that keep a host's choice in the profile: claudeModel, claudeEffort, codexModel, codexEffort. */
44
+ export declare function hostArgs(host: HostName, message: string, choice?: HostChoice, folder?: string, choices?: HostChoices): string[];
45
+ /** The settings' keys that keep a host's choice in the profile: claudeModel, claudeEffort, codexModel, codexEffort, antigravityModel, antigravityEffort. */
37
46
  export declare function choiceKeys(host: HostName): {
38
47
  model: string;
39
48
  effort: string;
40
49
  };
41
- /** A host's choice as the profile's settings keep it: the values the host offers alone, the rest is its middle one. */
42
- export declare function choiceIn(settings: Readonly<Record<string, unknown>> | undefined, host: HostName): HostChoice;
50
+ /** A host's choice as the profile's settings keep it: the values the host offers alone, among `choices`, the rest is its middle one. */
51
+ export declare function choiceIn(settings: Readonly<Record<string, unknown>> | undefined, host: HostName, choices?: HostChoices): HostChoice;
@@ -1,20 +1,28 @@
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
1
+ // The AI hosts the table opens a chat in (D-104, D-115): Claude Code, Codex and Antigravity, each with the start message, and the model and effort the player chose on the page
2
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
- /** The hosts the page offers, by their command's name. */
5
- export const hostNames = ["claude", "codex"];
4
+ /** The hosts the page offers, by the name `chesspal play` takes. */
5
+ export const hostNames = ["claude", "codex", "antigravity"];
6
+ /** Each host's program, as its installer names it. */
7
+ export const hostPrograms = { claude: "claude", codex: "codex", antigravity: "agy" };
6
8
  /**
7
9
  * 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.
10
+ * models a ChatGPT account lists, which change with its releases; Antigravity's, the Gemini Flash its free plan gives, its effort apart (D-115), until
11
+ * the table has read the models of the player's account.
9
12
  */
10
13
  export const hostChoices = {
11
14
  claude: { models: ["fable", "opus", "sonnet", "haiku"], efforts: ["low", "medium", "high", "xhigh", "max"], model: "sonnet", effort: "medium" },
12
15
  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" },
16
+ antigravity: { models: ["gemini-3.8-flash"], efforts: ["low", "medium", "high"], model: "gemini-3.8-flash", effort: "medium" },
13
17
  };
14
18
  /** Whether a word names a host the page offers. */
15
19
  export function isHostName(name) {
16
20
  return hostNames.includes(name);
17
21
  }
22
+ /** The efforts a host offers for one of its models: none for a model that has no effort to choose. */
23
+ export function effortsOf(choices, model) {
24
+ return choices.modelEfforts?.[model] ?? choices.efforts;
25
+ }
18
26
  /**
19
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
20
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.
@@ -27,29 +35,39 @@ export function startMessage(language, toAnalyse = false) {
27
35
  }
28
36
  /**
29
37
  * 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
30
- * subcommand, kept from its update screen, which an Enter would accept.
38
+ * subcommand, kept from its update screen, which an Enter would accept; for Antigravity after -i, which keeps the chat open, with Chesspal's tools
39
+ * allowed and the table's `folder` trusted for this chat alone, never saved in its settings: a folder trusted only at its first chat's dialog leaves that
40
+ * chat without its hooks. A model with no effort to choose is given none. `choices` are those the host offers, as read for Antigravity.
31
41
  */
32
- export function hostArgs(host, message, choice = {}) {
33
- const { model, effort } = offered(host, choice);
42
+ export function hostArgs(host, message, choice = {}, folder, choices = hostChoices[host]) {
43
+ const { model, effort } = offered(choices, choice);
34
44
  if (host === "claude") {
35
45
  return [message, ...(model === undefined ? [] : ["--model", model]), ...(effort === undefined ? [] : ["--effort", effort])];
36
46
  }
47
+ if (host === "antigravity") {
48
+ const settings = { ...antigravityAllowed, ...(folder === undefined ? {} : { trustedWorkspaces: [folder] }) };
49
+ return ["-i", message, ...(model === undefined ? [] : ["--model", model]), ...(effort === undefined ? [] : ["--effort", effort]), "--settings", JSON.stringify(settings)];
50
+ }
37
51
  return ["-c", "check_for_update_on_startup=false", ...(model === undefined ? [] : ["-m", model]), ...(effort === undefined ? [] : ["-c", `model_reasoning_effort=${effort}`]), message];
38
52
  }
39
- /** The settings' keys that keep a host's choice in the profile: claudeModel, claudeEffort, codexModel, codexEffort. */
53
+ /** The settings' keys that keep a host's choice in the profile: claudeModel, claudeEffort, codexModel, codexEffort, antigravityModel, antigravityEffort. */
40
54
  export function choiceKeys(host) {
41
55
  return { model: `${host}Model`, effort: `${host}Effort` };
42
56
  }
43
- /** A host's choice as the profile's settings keep it: the values the host offers alone, the rest is its middle one. */
44
- export function choiceIn(settings, host) {
57
+ /** A host's choice as the profile's settings keep it: the values the host offers alone, among `choices`, the rest is its middle one. */
58
+ export function choiceIn(settings, host, choices = hostChoices[host]) {
45
59
  const keys = choiceKeys(host);
46
60
  const text = (value) => (typeof value === "string" ? value : undefined);
47
- return offered(host, { model: text(settings?.[keys.model]), effort: text(settings?.[keys.effort]) });
61
+ return offered(choices, { model: text(settings?.[keys.model]), effort: text(settings?.[keys.effort]) });
48
62
  }
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.
50
- function offered(host, choice) {
51
- const choices = hostChoices[host];
63
+ // Antigravity's settings for one chat: Chesspal's tools allowed, which it would otherwise ask for at each first use.
64
+ const antigravityAllowed = { permissions: { allow: ["mcp(chesspal/*)"] } };
65
+ // The choice kept to the values the host offers: its middle model where none is chosen, or one it does not offer; the model's middle effort likewise,
66
+ // none for a model without.
67
+ function offered(choices, choice) {
52
68
  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;
54
- return { model, effort };
69
+ const efforts = effortsOf(choices, model);
70
+ const middle = efforts.includes(choices.effort) ? choices.effort : efforts[Math.floor((efforts.length - 1) / 2)];
71
+ const effort = choice.effort !== undefined && efforts.includes(choice.effort) ? choice.effort : middle;
72
+ return effort === undefined ? { model } : { model, effort };
55
73
  }
@@ -0,0 +1,19 @@
1
+ import { type HostChoices, type HostName } from "./hosts.ts";
2
+ /** Runs a program with its arguments: what it wrote on stdout; it fails when the program fails, or has not ended within `timeout` milliseconds. */
3
+ export type RunProgram = (program: string, args: readonly string[], timeout: number) => Promise<string>;
4
+ /** Antigravity's models as the table reads them: the choices offered now, a reading started, and who is told when they come. */
5
+ export interface AntigravityModels {
6
+ choices(): HostChoices;
7
+ /** Reads the models with the program, once: again only after a reading that failed. */
8
+ read(program: string): void;
9
+ watch(changed: () => void): void;
10
+ }
11
+ /** Antigravity's models read with `run` for the table in `folder`, which keeps the list read for `chesspal play`. */
12
+ export declare function antigravityModels(folder: string, run?: RunProgram): AntigravityModels;
13
+ /**
14
+ * The choices in what `agy models` lists, an id and a name on each line: an id ending in an effort is that model with that effort, one without a model with
15
+ * no effort to choose. A Gemini Flash is the middle model where there is one, else the first; none when no line names a model.
16
+ */
17
+ export declare function antigravityChoices(listed: string): HostChoices | undefined;
18
+ /** The choices a host offers at the table in `folder`: Antigravity's as the table last read them, the others' always the same. */
19
+ export declare function tableChoices(folder: string, host: HostName): HostChoices;
@@ -0,0 +1,95 @@
1
+ // Antigravity's models, as `agy models` lists them for the account signed in: Gemini Flash on a free plan, more with a subscription. The table reads them
2
+ // once, without waiting for them, and keeps the list in its folder for `chesspal play`; until it comes, or when it cannot be read, Gemini Flash alone.
3
+ import { execFile } from "node:child_process";
4
+ import { readFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { writeAtomically } from "../../../games/index.js";
7
+ import { hostChoices } from "./hosts.js";
8
+ /** Antigravity's models read with `run` for the table in `folder`, which keeps the list read for `chesspal play`. */
9
+ export function antigravityModels(folder, run = runProgram) {
10
+ let read;
11
+ let reading = false;
12
+ const watchers = [];
13
+ return {
14
+ choices: () => read ?? hostChoices.antigravity,
15
+ read: (program) => {
16
+ if (read !== undefined || reading) {
17
+ return;
18
+ }
19
+ reading = true;
20
+ run(program, ["models"], readTime).then((listed) => {
21
+ reading = false;
22
+ read = antigravityChoices(listed);
23
+ if (read !== undefined) {
24
+ keepList(folder, listed);
25
+ watchers.forEach((changed) => changed());
26
+ }
27
+ }, () => {
28
+ reading = false;
29
+ });
30
+ },
31
+ watch: (changed) => {
32
+ watchers.push(changed);
33
+ },
34
+ };
35
+ }
36
+ /**
37
+ * The choices in what `agy models` lists, an id and a name on each line: an id ending in an effort is that model with that effort, one without a model with
38
+ * no effort to choose. A Gemini Flash is the middle model where there is one, else the first; none when no line names a model.
39
+ */
40
+ export function antigravityChoices(listed) {
41
+ const modelEfforts = {};
42
+ for (const line of listed.split(/\r?\n/)) {
43
+ const id = line.split("\t")[0].trim();
44
+ if (!modelId.test(id)) {
45
+ continue;
46
+ }
47
+ const [, model = id, effort] = effortEnd.exec(id) ?? [];
48
+ const efforts = (modelEfforts[model] ??= []);
49
+ if (effort !== undefined && !efforts.includes(effort)) {
50
+ efforts.push(effort);
51
+ efforts.sort((one, other) => effortScale.indexOf(one) - effortScale.indexOf(other));
52
+ }
53
+ }
54
+ const models = Object.keys(modelEfforts);
55
+ if (models.length === 0) {
56
+ return undefined;
57
+ }
58
+ const efforts = effortScale.filter((effort) => models.some((model) => modelEfforts[model].includes(effort)));
59
+ const model = models.find((one) => /^gemini-.*flash/.test(one)) ?? models[0];
60
+ return { models, efforts, model, effort: "medium", modelEfforts };
61
+ }
62
+ /** The choices a host offers at the table in `folder`: Antigravity's as the table last read them, the others' always the same. */
63
+ export function tableChoices(folder, host) {
64
+ if (host !== "antigravity") {
65
+ return hostChoices[host];
66
+ }
67
+ try {
68
+ return antigravityChoices(readFileSync(join(folder, listFile), "utf8")) ?? hostChoices.antigravity;
69
+ }
70
+ catch {
71
+ return hostChoices.antigravity;
72
+ }
73
+ }
74
+ // `agy models` asks Google's servers for the account's models: about a second, at times more.
75
+ const readTime = 15_000;
76
+ const listFile = "antigravity-models.txt";
77
+ // The efforts Antigravity's ids end in, from the least.
78
+ const effortScale = ["low", "medium", "high", "xhigh", "max"];
79
+ const effortEnd = new RegExp(`^(.+)-(${effortScale.join("|")})$`);
80
+ // An id as Antigravity writes them, which a header or a message never is.
81
+ const modelId = /^[a-z0-9][a-z0-9._-]*$/;
82
+ function runProgram(program, args, timeout) {
83
+ return new Promise((done, failed) => {
84
+ execFile(program, args, { timeout, windowsHide: true, encoding: "utf8" }, (error, stdout) => (error === null ? done(stdout) : failed(error)));
85
+ });
86
+ }
87
+ // A list that cannot be written leaves `chesspal play` with Gemini Flash: the chat starts all the same.
88
+ function keepList(folder, listed) {
89
+ try {
90
+ writeAtomically(join(folder, listFile), listed);
91
+ }
92
+ catch {
93
+ // Nothing to do: see above.
94
+ }
95
+ }
@@ -4,7 +4,7 @@ import { type HooksNews } from "./hooks.ts";
4
4
  import { type HostChoice, type HostName } from "./hosts.ts";
5
5
  /**
6
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
7
- * model and effort it is given, which the seat notes for the page (D-107).
7
+ * model and effort it is given, which the seat notes for the page (D-107), as it notes a chat started for the game in analysis.
8
8
  */
9
9
  export interface HostLaunch {
10
10
  readonly program: string;
@@ -13,6 +13,8 @@ export interface HostLaunch {
13
13
  readonly env: NodeJS.ProcessEnv;
14
14
  readonly shell: boolean;
15
15
  readonly choice: HostChoice;
16
+ /** Started for the game in analysis on the page, with no game going on: a new game from the page starts it again. */
17
+ readonly analysis: boolean;
16
18
  /** Chesspal wrote Codex's hooks, the first time or again: Codex runs them only once the player trusts them. */
17
19
  readonly hooks?: HooksNews;
18
20
  }
@@ -31,7 +33,10 @@ export declare function runHost(launch: HostLaunch, again: () => boolean, platfo
31
33
  code: number;
32
34
  again: boolean;
33
35
  }>;
34
- /** 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. */
36
+ /**
37
+ * How the host starts for the profile in use: found on the PATH, or where its installer puts it, with its choice among those the table offers; it throws,
38
+ * saying how to install it, when it is not there.
39
+ */
35
40
  export declare function hostLaunch(context: Context, host: HostName, env?: NodeJS.ProcessEnv, exists?: (path: string) => boolean): HostLaunch;
36
41
  /**
37
42
  * Ends a process and what it started, a host's MCP server among them: on Windows its whole tree, which taskkill ends, never the console (Priya); else the
@@ -7,9 +7,10 @@ import { speaksItalian } from "../../../commands/index.js";
7
7
  import { endOf, gameInAnalysis, loadGame } from "../../../games/index.js";
8
8
  import { tableFolder } from "../../../home/index.js";
9
9
  import { activeProfile, gameFolder, languageIn, profileSettings } from "../../../profiles/index.js";
10
- import { programPath } from "./find.js";
10
+ import { hostProgramPath } from "./find.js";
11
11
  import { setUpTable, takeHooksNews } from "./hooks.js";
12
12
  import { choiceIn, hostArgs, isHostName, startMessage } from "./hosts.js";
13
+ import { tableChoices } from "./models.js";
13
14
  import { askedAgain, leaveSeat, refreshSeat, seatTimes, takeSeat } from "./seat.js";
14
15
  import { cmdQuoted } from "./terminal.js";
15
16
  /**
@@ -127,30 +128,35 @@ export function runHost(launch, again, platform = process.platform, every = 500)
127
128
  });
128
129
  });
129
130
  }
130
- /** 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. */
131
+ /**
132
+ * How the host starts for the profile in use: found on the PATH, or where its installer puts it, with its choice among those the table offers; it throws,
133
+ * saying how to install it, when it is not there.
134
+ */
131
135
  export function hostLaunch(context, host, env = process.env, exists) {
132
136
  const home = context.home();
133
137
  const folder = tableFolder(home);
134
- const found = programPath(host, context.platform, env, exists);
138
+ const found = hostProgramPath(host, context.platform, env, exists);
135
139
  if (found === undefined) {
136
140
  throw new Error(`${hostTitle[host]} is not installed on this computer, or not on its PATH: install it (${installHint[host]}), then try again.`);
137
141
  }
138
- setUpTable(folder, context.platform);
142
+ setUpTable(folder, context.platform, context.userHome?.());
139
143
  // 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
144
  const news = host === "codex" ? takeHooksNews(folder) : undefined;
141
145
  const hooks = news === undefined ? {} : { hooks: news };
142
- const choice = choiceIn(profileSettings(home, activeProfile(home).id), host);
146
+ const choices = tableChoices(folder, host);
147
+ const choice = choiceIn(profileSettings(home, activeProfile(home).id), host, choices);
143
148
  const games = gameFolder(home);
144
- const args = hostArgs(host, startMessage(languageIn(games, context.language), analysisAsked(games)), choice);
149
+ const analysis = analysisAsked(games);
150
+ const args = hostArgs(host, startMessage(languageIn(games, context.language), analysis), choice, folder, choices);
145
151
  // 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.
146
152
  const effort = host === "claude" && choice.effort !== undefined ? { CLAUDE_CODE_EFFORT_LEVEL: undefined } : {};
147
153
  // 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.
148
154
  const childEnv = { ...env, ...Object.fromEntries(sessionMarks.map((name) => [name, undefined])), ...effort, CHESSPAL_SEAT: String(process.pid) };
149
155
  const script = npmScript(host, found, context.platform, exists ?? existsSync);
150
156
  if (script !== undefined) {
151
- return { program: process.execPath, args: [script, ...args], folder, env: childEnv, shell: false, choice, ...hooks };
157
+ return { program: process.execPath, args: [script, ...args], folder, env: childEnv, shell: false, choice, analysis, ...hooks };
152
158
  }
153
- return { program: found, args, folder, env: childEnv, shell: context.platform === "win32" && /\.(cmd|bat)$/i.test(found), choice, ...hooks };
159
+ return { program: found, args, folder, env: childEnv, shell: context.platform === "win32" && /\.(cmd|bat)$/i.test(found), choice, analysis, ...hooks };
154
160
  }
155
161
  /** 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
162
  function analysisAsked(folder) {
@@ -163,7 +169,7 @@ function analysisAsked(folder) {
163
169
  return false;
164
170
  }
165
171
  }
166
- const hostTitle = { claude: "Claude Code", codex: "Codex" };
172
+ const hostTitle = { claude: "Claude Code", codex: "Codex", antigravity: "Antigravity" };
167
173
  // What the player is told before Codex asks about the hooks Chesspal wrote: the first time, and when they changed.
168
174
  const hooksWords = {
169
175
  new: {
@@ -180,10 +186,10 @@ const closedAtStartWords = {
180
186
  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
187
  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."],
182
188
  };
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.
189
+ // Runs the host as runHost does, the seat noting the model and effort it is given, and whether for the analysis; one not installed, or that could not start, fails saying so.
184
190
  async function started(context, host, again) {
185
191
  const launch = hostLaunch(context, host);
186
- takeSeat(launch.folder, { pid: process.pid, host, ...launch.choice });
192
+ takeSeat(launch.folder, { pid: process.pid, host, ...launch.choice, ...(launch.analysis ? { analysis: true } : {}) });
187
193
  // Codex asks the player to trust the hooks Chesspal wrote: said here, above its question, as init says it.
188
194
  if (launch.hooks !== undefined) {
189
195
  const words = hooksWords[launch.hooks];
@@ -256,7 +262,11 @@ function savedTerminal(platform) {
256
262
  }
257
263
  // 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).
258
264
  const sessionMarks = ["CLAUDECODE", "CLAUDE_CODE_SESSION_ID", "CLAUDE_CODE_CHILD_SESSION", "CLAUDE_CODE_SESSION_ATTENDED", "CLAUDE_PID", "CLAUDE_EFFORT", "AI_AGENT", "TRACEPARENT"];
259
- const installHint = { claude: "https://claude.com/claude-code", codex: "npm install -g @openai/codex" };
265
+ const installHint = {
266
+ claude: "https://claude.com/claude-code",
267
+ codex: "npm install -g @openai/codex",
268
+ antigravity: "https://antigravity.google/docs/getting-started",
269
+ };
260
270
  /**
261
271
  * 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
262
272
  * another program, or when it is not beside the shim.
@@ -1,9 +1,13 @@
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. */
1
+ /**
2
+ * 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; `analysis` when
3
+ * it started it for the game in analysis on the page, which a new game then starts again.
4
+ */
2
5
  export interface Seat {
3
6
  readonly pid: number;
4
7
  readonly host: string;
5
8
  readonly model?: string | undefined;
6
9
  readonly effort?: string | undefined;
10
+ readonly analysis?: true;
7
11
  }
8
12
  /**
9
13
  * 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
@@ -70,7 +70,7 @@ function seatIn(folder) {
70
70
  return undefined;
71
71
  }
72
72
  const text = (value) => (typeof value === "string" ? value : undefined);
73
- return { pid: seat.pid, host: seat.host, model: text(seat.model), effort: text(seat.effort) };
73
+ return { pid: seat.pid, host: seat.host, model: text(seat.model), effort: text(seat.effort), ...(seat.analysis === true ? { analysis: true } : {}) };
74
74
  }
75
75
  function jsonIn(path) {
76
76
  try {
@@ -14,7 +14,7 @@ export const tableTimes = { idle: 5 * 60_000, every: 15_000 };
14
14
  */
15
15
  export async function startTable(context, times = tableTimes, chats, updater) {
16
16
  const folder = tableFolder(context.home());
17
- setUpTable(folder, context.platform);
17
+ setUpTable(folder, context.platform, context.userHome?.());
18
18
  let quit = () => undefined;
19
19
  const quitting = new Promise((done) => {
20
20
  quit = done;