chesspal 0.4.1 → 0.5.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 (47) 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 +1 -1
  18. package/dist/page/guide/en.md +2 -2
  19. package/dist/page/guide/it.md +2 -2
  20. package/dist/page/profile/settings.js +2 -0
  21. package/dist/page/table/action.js +19 -7
  22. package/dist/page/table/window.js +7 -5
  23. package/dist/page/text.js +2 -2
  24. package/dist/server/bell/antigravity.d.ts +10 -0
  25. package/dist/server/bell/antigravity.js +75 -0
  26. package/dist/server/bell/bell.d.ts +10 -2
  27. package/dist/server/bell/bell.js +33 -10
  28. package/dist/server/bell/index.d.ts +1 -1
  29. package/dist/server/bell/index.js +2 -1
  30. package/dist/server/mcp/caller.js +26 -1
  31. package/dist/server/page/page.js +1 -0
  32. package/dist/server/page/table.d.ts +2 -0
  33. package/dist/server/page/table.js +1 -1
  34. package/dist/server/table/hosts/chats.d.ts +4 -2
  35. package/dist/server/table/hosts/chats.js +23 -10
  36. package/dist/server/table/hosts/find.d.ts +6 -0
  37. package/dist/server/table/hosts/find.js +19 -1
  38. package/dist/server/table/hosts/hooks.d.ts +5 -2
  39. package/dist/server/table/hosts/hooks.js +6 -3
  40. package/dist/server/table/hosts/hosts.d.ts +19 -10
  41. package/dist/server/table/hosts/hosts.js +34 -16
  42. package/dist/server/table/hosts/models.d.ts +19 -0
  43. package/dist/server/table/hosts/models.js +95 -0
  44. package/dist/server/table/hosts/play.d.ts +4 -1
  45. package/dist/server/table/hosts/play.js +17 -8
  46. package/dist/server/table/table.js +1 -1
  47. package/package.json +1 -1
@@ -2,6 +2,7 @@
2
2
  export const choiceKeys = {
3
3
  claude: { model: "claudeModel", effort: "claudeEffort" },
4
4
  codex: { model: "codexModel", effort: "codexEffort" },
5
+ antigravity: { model: "antigravityModel", effort: "antigravityEffort" },
5
6
  };
6
7
  /** The hosts the window offers: those installed whose choices the settings keep. */
7
8
  export function offeredHosts(table) {
@@ -13,14 +14,21 @@ export function firstHost(table, settings) {
13
14
  const atTable = offered.find((host) => table.chat && host.name === table.host);
14
15
  return (atTable ?? offered.find((host) => host.name === settings.chatHost) ?? offered[0])?.name;
15
16
  }
16
- /** A host's model or effort as the settings keep it: the host's middle one for none, and for a choice it no longer offers. */
17
+ /** The efforts a host offers for one of its models: none for a model that has no effort to choose. */
18
+ export function effortsOf(host, model) {
19
+ return host.modelEfforts?.[model] ?? host.efforts;
20
+ }
21
+ /**
22
+ * A host's model or effort as the settings keep it: the host's middle one for none, and for a choice it no longer offers; the effort among the chosen
23
+ * model's, none for a model without.
24
+ */
17
25
  export function chosen(host, kind, settings) {
18
- const key = choiceKeys[host.name][kind];
19
- return offeredOr(host, kind, settings[key]);
26
+ const keys = choiceKeys[host.name];
27
+ return offered(host, settings[keys.model], settings[keys.effort])[kind];
20
28
  }
21
29
  /** What the chat held started with: the host's middle one where the table does not say, or says one it no longer offers. */
22
30
  export function startedWith(table, host, kind) {
23
- return offeredOr(host, kind, table.started?.[kind]);
31
+ return offered(host, table.started?.model, table.started?.effort)[kind];
24
32
  }
25
33
  /** What the button does with the host selected, none without one: with no chat, open one, to review the game in `review`; else start the chat again, in another host when it is not the one at the table. */
26
34
  export function chatAction(table, host, review) {
@@ -36,7 +44,11 @@ export function chatAction(table, host, review) {
36
44
  export function canAct(table) {
37
45
  return table.opening === undefined && table.waiting === undefined && (!table.chat || table.held);
38
46
  }
39
- function offeredOr(host, kind, value) {
40
- const offered = kind === "model" ? host.models : host.efforts;
41
- return value !== undefined && offered.includes(value) ? value : host[kind];
47
+ // The choice kept to what the host offers, as the table keeps it when it starts the chat.
48
+ function offered(host, model, effort) {
49
+ const kept = model !== undefined && host.models.includes(model) ? model : host.model;
50
+ const efforts = effortsOf(host, kept);
51
+ const middle = efforts.includes(host.effort) ? host.effort : efforts[Math.floor((efforts.length - 1) / 2)];
52
+ const keptEffort = effort !== undefined && efforts.includes(effort) ? effort : middle;
53
+ return keptEffort === undefined ? { model: kept } : { model: kept, effort: keptEffort };
42
54
  }
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-ru
2
2
  import { useEffect, useState } from "preact/hooks";
3
3
  import { Dialog } from "../dialogs/dialog.js";
4
4
  import { checkIcon } from "../dialogs/profiles.js";
5
- import { canAct, chatAction, choiceKeys, chosen, firstHost, offeredHosts, startedWith } from "./action.js";
5
+ import { canAct, chatAction, choiceKeys, chosen, effortsOf, firstHost, offeredHosts, startedWith } from "./action.js";
6
6
  /** The chat's window: closed once a chat opens or starts again, the page then saying where it stands; `review` when it opened to have a game analysed. */
7
7
  export function ChatDialog({ open, review = false, table, settings, onChange, name, onClose, text }) {
8
8
  return (_jsx(Dialog, { open: open, title: review ? text.table.reviewTitle : text.table.title, onClose: onClose, text: text, children: _jsx(ChatForm, { review: review, table: table, settings: settings, onChange: onChange, name: name, onClose: onClose, text: text }) }));
@@ -41,7 +41,7 @@ export function chatState(table, name, text) {
41
41
  }
42
42
  return table.opening === "chat" ? words.opening : table.opening === "again" ? words.again(name) : table.opening === "restart" ? words.restart : undefined;
43
43
  }
44
- const hostTitles = { claude: "Claude Code", codex: "Codex" };
44
+ const hostTitles = { claude: "Claude Code", codex: "Codex", antigravity: "Antigravity" };
45
45
  /** The ↻ of a chat that starts again. */
46
46
  const restartIcon = (_jsx("svg", { class: "icon in-use", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M19.5 12a7.5 7.5 0 1 1-2.2-5.3M19.5 4.5v4h-4" }) }));
47
47
  /** The window's form, made again at each opening with the host at the table selected, else the last the button used, else the first. */
@@ -94,17 +94,19 @@ function submitOnEnter(event) {
94
94
  /** A host's model or effort as a row of choices, its middle one chosen until the player chooses; the one the chat at the table `started` with bears a dot. */
95
95
  function Choices({ host, kind, settings, started, onChange, text }) {
96
96
  const key = choiceKeys[host.name][kind];
97
- const offered = kind === "model" ? host.models : host.efforts;
97
+ // The efforts of the model chosen: none, and no row, for a model without.
98
+ const offered = kind === "model" ? host.models : effortsOf(host, chosen(host, "model", settings) ?? host.model);
98
99
  const value = chosen(host, kind, settings);
99
100
  if (offered.length === 0) {
100
101
  return null;
101
102
  }
102
103
  return (_jsxs("fieldset", { children: [_jsx("legend", { children: text.table[kind] }), _jsx("div", { class: "chat-choices", children: offered.map((one) => {
103
104
  const words = one + (one === started ? ` · ${text.table.atTable}` : "");
104
- return (_jsxs("label", { class: one === started ? "level started" : "level", title: words, children: [_jsx("input", { type: "radio", name: `chat-${kind}`, checked: value === one, onChange: () => onChange({ ...settings, [key]: one }), "aria-label": words }), _jsx("span", { children: shortName(one) })] }, one));
105
+ return (_jsxs("label", { class: one === started ? "level started" : "level", title: words, children: [_jsx("input", { type: "radio", name: `chat-${kind}`, checked: value === one, onChange: () => onChange({ ...settings, [key]: one }), "aria-label": words }), _jsx("span", { children: host.name === "codex" ? shortName(one) : one })] }, one));
105
106
  }) })] }));
106
107
  }
107
- // Codex's models without the "gpt-" they all start with, so the choice stays narrow; the title keeps the whole name.
108
+ // Codex's models without the "gpt-" they all start with, so the choice stays narrow; the title keeps the whole name. Another host's keep it: Antigravity's
109
+ // GPT-OSS would read as "oss".
108
110
  function shortName(value) {
109
111
  return value.replace(/^gpt-/, "");
110
112
  }
package/dist/page/text.js CHANGED
@@ -182,7 +182,7 @@ const english = {
182
182
  byHand: (name) => `You opened this chat yourself: change ${name}'s model with /model in its window, or close it and open one from here, with the choices below.`,
183
183
  noChat: "No chat at the table: choose one",
184
184
  change: "Change the chat or its model",
185
- install: (name) => `To play with ${name}, install Claude Code or Codex, then open this page again.`,
185
+ install: (name) => `To play with ${name}, install Claude Code, Codex or Antigravity, then open this page again.`,
186
186
  chat: "Chat",
187
187
  model: "Model",
188
188
  effort: "Effort",
@@ -621,7 +621,7 @@ const italian = {
621
621
  byHand: (name) => `Questa chat l'hai aperta tu: cambia il modello di ${name} con /model nella sua finestra, oppure chiudila e aprine una da qui, con le scelte qui sotto.`,
622
622
  noChat: "Nessuna chat al tavolo: scegline una",
623
623
  change: "Cambia la chat o il modello",
624
- install: (name) => `Per giocare con ${name}, installa Claude Code o Codex, poi apri di nuovo questa pagina.`,
624
+ install: (name) => `Per giocare con ${name}, installa Claude Code, Codex o Antigravity, poi apri di nuovo questa pagina.`,
625
625
  chat: "Chat",
626
626
  model: "Modello",
627
627
  effort: "Ragionamento",
@@ -0,0 +1,10 @@
1
+ /** Antigravity's own folder, where its command line keeps its history and its log. */
2
+ export declare function antigravityFolder(userHome?: string): string;
3
+ /**
4
+ * Watches, from now on, whether the player starts a new turn in the conversation: writes in the chat, or stops it with Esc. `started` resolves then, never
5
+ * otherwise; `stop` ends the watch.
6
+ */
7
+ export declare function watchNewTurn(conversation: string, folder?: string, every?: number): {
8
+ started: Promise<void>;
9
+ stop: () => void;
10
+ };
@@ -0,0 +1,75 @@
1
+ // Antigravity's Stop hook holds the chat until it ends: a message the player writes meanwhile waits behind it, and Esc leaves it running. Antigravity's own
2
+ // files tell either: the history of what the player typed, with the conversation it went to, and its log, which notes the conversation stopped.
3
+ import { closeSync, openSync, readSync, statSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ /** Antigravity's own folder, where its command line keeps its history and its log. */
7
+ export function antigravityFolder(userHome = homedir()) {
8
+ return join(userHome, ".gemini", "antigravity-cli");
9
+ }
10
+ /**
11
+ * Watches, from now on, whether the player starts a new turn in the conversation: writes in the chat, or stops it with Esc. `started` resolves then, never
12
+ * otherwise; `stop` ends the watch.
13
+ */
14
+ export function watchNewTurn(conversation, folder = antigravityFolder(), every = 500) {
15
+ const history = join(folder, "history.jsonl");
16
+ const log = join(folder, "cli.log");
17
+ const read = { [history]: sizeOf(history), [log]: sizeOf(log) };
18
+ let timer;
19
+ const started = new Promise((done) => {
20
+ timer = setInterval(() => {
21
+ if (wrote(newText(history, read), conversation) || stopped(newText(log, read), conversation)) {
22
+ clearInterval(timer);
23
+ done();
24
+ }
25
+ }, every);
26
+ timer.unref();
27
+ });
28
+ return { started, stop: () => clearInterval(timer) };
29
+ }
30
+ /** Whether these lines of Antigravity's history hold something the player typed in the conversation: a message, or a command such as /clear. */
31
+ function wrote(lines, conversation) {
32
+ return lines.split("\n").some((line) => {
33
+ try {
34
+ return JSON.parse(line).conversationId === conversation;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ });
40
+ }
41
+ /** Whether these lines of Antigravity's log say the conversation was stopped, as Esc does. */
42
+ function stopped(lines, conversation) {
43
+ return lines.includes(`Cancelling conversation ${conversation}`);
44
+ }
45
+ /** What a file gained since it was last read, as `read` keeps its size; a file that shrank, written anew, is read from its start. */
46
+ function newText(file, read) {
47
+ const size = sizeOf(file);
48
+ const from = size < (read[file] ?? 0) ? 0 : read[file] ?? 0;
49
+ read[file] = size;
50
+ if (size <= from) {
51
+ return "";
52
+ }
53
+ try {
54
+ const descriptor = openSync(file, "r");
55
+ try {
56
+ const buffer = Buffer.alloc(size - from);
57
+ const length = readSync(descriptor, buffer, 0, buffer.length, from);
58
+ return buffer.subarray(0, length).toString("utf8");
59
+ }
60
+ finally {
61
+ closeSync(descriptor);
62
+ }
63
+ }
64
+ catch {
65
+ return "";
66
+ }
67
+ }
68
+ function sizeOf(file) {
69
+ try {
70
+ return statSync(file).size;
71
+ }
72
+ catch {
73
+ return 0;
74
+ }
75
+ }
@@ -65,11 +65,19 @@ export declare function notePagePort(home: string, folder: string, port: number,
65
65
  export declare function forgetPagePort(home: string, folder: string, port: number): void;
66
66
  /** What Codex's Stop hook answers to wake the chat: its turn goes on with the doorbell's line. */
67
67
  export declare const codexBellAnswer: string;
68
+ /** What Antigravity's Stop hook answers to wake the chat: its turn goes on, the doorbell's line given as a system message (D-115). */
69
+ export declare const antigravityBellAnswer: string;
70
+ /** The host whose Stop hook runs `chesspal listen`, as its flag names it: Claude Code with none. */
71
+ export type ListeningHost = "claude" | "codex" | "antigravity";
68
72
  /**
69
73
  * Reads the hook's input, waits for the bell, and ends the process as the host reads it: for Claude Code's asyncRewake, 2 with the line on stderr; for
70
- * Codex's Stop hook, 0 with its answer on stdout, since Windows PowerShell, which runs Codex's hooks, turns an exit code 2 into 1. Else 0, saying nothing.
74
+ * Codex's and Antigravity's Stop hooks, 0 with their answer on stdout, since Windows PowerShell, which runs Codex's hooks, turns an exit code 2 into 1.
75
+ * Else 0, saying nothing, or for Antigravity an empty answer, which lets its chat stop. Antigravity's hook holds the chat: it lets it go as soon as the
76
+ * player writes in the chat or stops it, without the bell.
71
77
  */
72
- export declare function listen(codex?: boolean): Promise<void>;
78
+ export declare function listen(host?: ListeningHost): Promise<void>;
79
+ /** What the hook says as it ends, by its host: the doorbell's line or answer when it rang; else nothing, but for Antigravity, which reads an answer. */
80
+ export declare function hookAnswer(host: ListeningHost, rang: boolean): string | undefined;
73
81
  /**
74
82
  * Waits at the page of the hook's chat until the player moves there: true then; false when there is no page to wait at, or another hook takes its place.
75
83
  * The chat's own page is the one with its session; after /clear, which gives the hook a new one, the only page of its folder.
@@ -1,4 +1,4 @@
1
- // The doorbell for the page (D-47): `chesspal listen`, the Stop hook init writes for Claude Code and Codex, waits at the page of its chat and, when
1
+ // The doorbell for the page (D-47): `chesspal listen`, the Stop hook init writes for Claude Code, Codex and Antigravity, waits at the page of its chat and, when
2
2
  // the player moves or writes there, tells the chat to call status. It never carries the move, the words or the position: status reads them fresh.
3
3
  import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
4
4
  import { dirname, join, resolve } from "node:path";
@@ -6,6 +6,7 @@ import { positionShownNote, writeAtomically } from "../../games/index.js";
6
6
  import { samePath } from "../../home/index.js";
7
7
  import { analystAgent } from "../../host-files/index.js";
8
8
  import { askPage, chatFolder, findChatPage, notedFolder, notedPagePorts, pageNote, probeOf, probeWait } from "../ports/index.js";
9
+ import { watchNewTurn } from "./antigravity.js";
9
10
  /** What the chat hears from the doorbell: one line for whatever the player did, never what, only where to read it. */
10
11
  export const bellLine = "The player acted on the board page: call status to see it.";
11
12
  /**
@@ -185,31 +186,51 @@ export function forgetPagePort(home, folder, port) {
185
186
  }
186
187
  /** What Codex's Stop hook answers to wake the chat: its turn goes on with the doorbell's line. */
187
188
  export const codexBellAnswer = JSON.stringify({ decision: "block", reason: bellLine });
189
+ /** What Antigravity's Stop hook answers to wake the chat: its turn goes on, the doorbell's line given as a system message (D-115). */
190
+ export const antigravityBellAnswer = JSON.stringify({ decision: "continue", reason: bellLine });
188
191
  /**
189
192
  * Reads the hook's input, waits for the bell, and ends the process as the host reads it: for Claude Code's asyncRewake, 2 with the line on stderr; for
190
- * Codex's Stop hook, 0 with its answer on stdout, since Windows PowerShell, which runs Codex's hooks, turns an exit code 2 into 1. Else 0, saying nothing.
193
+ * Codex's and Antigravity's Stop hooks, 0 with their answer on stdout, since Windows PowerShell, which runs Codex's hooks, turns an exit code 2 into 1.
194
+ * Else 0, saying nothing, or for Antigravity an empty answer, which lets its chat stop. Antigravity's hook holds the chat: it lets it go as soon as the
195
+ * player writes in the chat or stops it, without the bell.
191
196
  */
192
- export async function listen(codex = false) {
197
+ export async function listen(host = "claude") {
193
198
  let input = "";
194
199
  for await (const chunk of process.stdin) {
195
200
  input += String(chunk);
196
201
  }
197
- const heard = await bellHeard(input).catch((error) => ({ folder: chatFolder(hookInput(input)), rang: false, why: `failed: ${error.message}` }));
202
+ const conversation = host === "antigravity" ? hookInput(input).session : undefined;
203
+ const turn = conversation === undefined ? undefined : watchNewTurn(conversation);
204
+ const waited = bellHeard(input).catch((error) => ({ folder: chatFolder(hookInput(input)), rang: false, why: `failed: ${error.message}` }));
205
+ const newTurn = turn?.started.then(() => ({ folder: chatFolder(hookInput(input)), rang: false, why: "the player started a new turn" }));
206
+ const heard = await (newTurn === undefined ? waited : Promise.race([waited, newTurn]));
207
+ turn?.stop();
198
208
  // Codex's turn ends with a hook that heard no bell, and the player's next move on the page wakes nobody: the log keeps why.
199
- if (codex && !heard.rang) {
209
+ if (host === "codex" && !heard.rang) {
200
210
  noteSilence(heard.folder, heard.why);
201
211
  }
202
212
  // It ends as soon as it knows: the chat hears the hook only once it ends.
203
- if (!heard.rang) {
213
+ const answer = hookAnswer(host, heard.rang);
214
+ if (answer === undefined) {
204
215
  process.exit(0);
205
216
  }
206
- else if (codex) {
207
- process.stdout.write(`${codexBellAnswer}\n`, () => process.exit(0));
217
+ else if (host === "claude") {
218
+ process.stderr.write(`${answer}\n`, () => process.exit(2));
208
219
  }
209
220
  else {
210
- process.stderr.write(`${bellLine}\n`, () => process.exit(2));
221
+ process.stdout.write(`${answer}\n`, () => process.exit(0));
211
222
  }
212
223
  }
224
+ /** What the hook says as it ends, by its host: the doorbell's line or answer when it rang; else nothing, but for Antigravity, which reads an answer. */
225
+ export function hookAnswer(host, rang) {
226
+ if (host === "antigravity") {
227
+ return rang ? antigravityBellAnswer : "{}";
228
+ }
229
+ if (!rang) {
230
+ return undefined;
231
+ }
232
+ return host === "codex" ? codexBellAnswer : bellLine;
233
+ }
213
234
  /**
214
235
  * Waits at the page of the hook's chat until the player moves there: true then; false when there is no page to wait at, or another hook takes its place.
215
236
  * The chat's own page is the one with its session; after /clear, which gives the hook a new one, the only page of its folder.
@@ -264,11 +285,13 @@ export function noteSilence(folder, why, now = new Date()) {
264
285
  // The log only explains; the hook ends the same.
265
286
  }
266
287
  }
288
+ /** What the hook reads of its host's input: the chat's session, Antigravity's conversation, its folder, and whether its analyst has gone. */
267
289
  function hookInput(input) {
268
290
  try {
269
291
  const parsed = JSON.parse(input);
292
+ const session = typeof parsed.session_id === "string" ? parsed.session_id : parsed.conversationId;
270
293
  return {
271
- ...(typeof parsed.session_id === "string" ? { session: parsed.session_id } : {}),
294
+ ...(typeof session === "string" ? { session } : {}),
272
295
  ...(typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {}),
273
296
  ...(analystGone(parsed.background_tasks) ? { analystGone: true } : {}),
274
297
  };
@@ -1,2 +1,2 @@
1
- export { Bell, forgetPagePort, listen, notePagePort } from "./bell.ts";
1
+ export { Bell, forgetPagePort, listen, type ListeningHost, notePagePort } from "./bell.ts";
2
2
  export { chatInterrupted } from "./interrupted.ts";
@@ -1,3 +1,4 @@
1
- // The doorbell that wakes a chat when the player acts on the page (D-47), and the notes of the ports a folder's page took off its usual ones.
1
+ // The doorbell that wakes a chat when the player acts on the page (D-47), Antigravity's included (D-115), and the notes of the ports a folder's page took
2
+ // off its usual ones.
2
3
  export { Bell, forgetPagePort, listen, notePagePort } from "./bell.js";
3
4
  export { chatInterrupted } from "./interrupted.js";
@@ -1,5 +1,6 @@
1
1
  // Who makes a tool call (D-81): only what the host ties to that call, never what the AI says of itself nor a setting the chat may have changed.
2
- // Codex sends the model and the effort with each call; Claude Code writes them into the chat's transcript, on the line of the call.
2
+ // Codex sends the model and the effort with each call; Claude Code writes them into the chat's transcript, on the line of the call; Antigravity sends the
3
+ // conversation, whose transcript names the model with its effort on each of the AI's steps (D-115).
3
4
  import { closeSync, openSync, readdirSync, readFileSync, readSync, statSync } from "node:fs";
4
5
  import { homedir } from "node:os";
5
6
  import { basename, dirname, join } from "node:path";
@@ -18,6 +19,13 @@ export async function callerOf(meta, client, folder, wait = 50) {
18
19
  return found;
19
20
  }
20
21
  }
22
+ const conversation = meta?.["antigravity.google/artifacts_dir"];
23
+ if (typeof conversation === "string" && conversation !== "") {
24
+ const found = await antigravityModel(join(conversation, ".system_generated", "logs", "transcript_full.jsonl"), wait);
25
+ if (found !== undefined) {
26
+ return found;
27
+ }
28
+ }
21
29
  return client === undefined ? undefined : { annotator: client.title ?? client.name, hostOnly: true };
22
30
  }
23
31
  /**
@@ -91,6 +99,23 @@ function callerIn(text, call) {
91
99
  }
92
100
  return undefined;
93
101
  }
102
+ /**
103
+ * The model of the AI's last step in an Antigravity conversation, as its transcript names it, the effort after it (gemini-3.8-flash-medium); the step of
104
+ * the call is written after it, but the chat's steps before it had the same model. None when no step names one in time.
105
+ */
106
+ async function antigravityModel(transcript, wait) {
107
+ for (let attempt = 0; attempt < attempts; attempt++) {
108
+ const model = tailOf(transcript).split("\n").reverse().map((line) => objectOf(line)).find((entry) => entry?.type === "PLANNER_RESPONSE" && typeof entry.model === "string")?.model;
109
+ if (typeof model === "string") {
110
+ const [, name, effort] = /^(.+)-(low|medium|high|xhigh|max)$/.exec(model) ?? [];
111
+ return name === undefined ? { annotator: model } : withEffort(name, effort);
112
+ }
113
+ if (attempt < attempts - 1) {
114
+ await new Promise((done) => setTimeout(done, wait));
115
+ }
116
+ }
117
+ return undefined;
118
+ }
94
119
  /**
95
120
  * Who in Codex made a call: the chat, or an agent by the role its session notes first, before its first call (Priya). An agent started with the chat's
96
121
  * own turns sees the opponent's plan, so it counts as the chat (D-88).
@@ -60,6 +60,7 @@ export async function startPage(context, options = {}) {
60
60
  const noted = offPorts ? notePort(served) : undefined;
61
61
  const stopWatchingGame = watchGame(served);
62
62
  const stopAskingNpm = watchForUpdates(served);
63
+ table?.chats?.watch?.(() => tellWatchers(watchers));
63
64
  // A chat's process ends without closing its page: the note goes with it.
64
65
  const forgetPort = () => {
65
66
  if (noted !== undefined) {
@@ -19,6 +19,8 @@ export interface TableChats {
19
19
  held(folder: string): HeldChat | undefined;
20
20
  /** Asks for the chat again: with another host, or `updated`, from the Chesspal just installed (D-108). */
21
21
  relaunch(folder: string, host?: string, updated?: boolean): boolean;
22
+ /** Tells `changed` when the hosts' choices change: Antigravity's models, read from the player's account. */
23
+ watch?(changed: () => void): void;
22
24
  }
23
25
  /** The chat `chesspal play` holds: the model and effort its host started with, none for the host's own, and the seat, the play's process. */
24
26
  export interface HeldChat {
@@ -287,7 +287,7 @@ function hostOffered(served, host) {
287
287
  function hostsOffered(served) {
288
288
  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
289
  }
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».
290
+ // 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
291
  function hostAtTable(served) {
292
292
  const said = served.chat.host()?.toLowerCase() ?? "";
293
293
  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,22 +1,35 @@
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) => {
@@ -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;