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
@@ -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
+ }
@@ -31,7 +31,10 @@ export declare function runHost(launch: HostLaunch, again: () => boolean, platfo
31
31
  code: number;
32
32
  again: boolean;
33
33
  }>;
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. */
34
+ /**
35
+ * 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,
36
+ * saying how to install it, when it is not there.
37
+ */
35
38
  export declare function hostLaunch(context: Context, host: HostName, env?: NodeJS.ProcessEnv, exists?: (path: string) => boolean): HostLaunch;
36
39
  /**
37
40
  * 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,21 +128,25 @@ 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 args = hostArgs(host, startMessage(languageIn(games, context.language), analysisAsked(games)), choice, folder, choices);
145
150
  // 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
151
  const effort = host === "claude" && choice.effort !== undefined ? { CLAUDE_CODE_EFFORT_LEVEL: undefined } : {};
147
152
  // 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.
@@ -163,7 +168,7 @@ function analysisAsked(folder) {
163
168
  return false;
164
169
  }
165
170
  }
166
- const hostTitle = { claude: "Claude Code", codex: "Codex" };
171
+ const hostTitle = { claude: "Claude Code", codex: "Codex", antigravity: "Antigravity" };
167
172
  // What the player is told before Codex asks about the hooks Chesspal wrote: the first time, and when they changed.
168
173
  const hooksWords = {
169
174
  new: {
@@ -256,7 +261,11 @@ function savedTerminal(platform) {
256
261
  }
257
262
  // 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
263
  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" };
264
+ const installHint = {
265
+ claude: "https://claude.com/claude-code",
266
+ codex: "npm install -g @openai/codex",
267
+ antigravity: "https://antigravity.google/docs/getting-started",
268
+ };
260
269
  /**
261
270
  * 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
271
  * another program, or when it is not beside the shim.
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chesspal",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Play chess against an AI with a character, and learn with an AI coach: Chesspal keeps the rules, the board, the games and the memory; the AI plays, reasons and talks.",
5
5
  "type": "module",
6
6
  "bin": {