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.
- package/README.md +37 -8
- package/dist/cli/chat-hook.d.ts +4 -1
- package/dist/cli/chat-hook.js +12 -5
- package/dist/cli/cli.js +3 -3
- package/dist/cli/command-line.js +9 -9
- package/dist/commands/game/new.js +3 -5
- package/dist/commands/registry/context.d.ts +5 -0
- package/dist/commands/registry/context.js +2 -1
- package/dist/commands/setup/init.d.ts +4 -1
- package/dist/commands/setup/init.js +30 -10
- package/dist/core/review.d.ts +2 -2
- package/dist/core/review.js +7 -3
- package/dist/host-files/host-files.d.ts +10 -6
- package/dist/host-files/host-files.js +57 -9
- package/dist/host-files/index.d.ts +1 -1
- package/dist/host-files/index.js +1 -1
- package/dist/page/app/app.js +112 -25
- package/dist/page/app/keys.js +16 -1
- package/dist/page/app/title.js +6 -3
- package/dist/page/board/board.css +6 -0
- package/dist/page/game/column.js +3 -3
- package/dist/page/game/game.css +121 -1
- package/dist/page/game/move-list.js +48 -10
- package/dist/page/game/variation.js +65 -0
- package/dist/page/guide/en.md +3 -2
- package/dist/page/guide/it.md +3 -2
- package/dist/page/profile/settings.js +2 -0
- package/dist/page/table/action.js +19 -7
- package/dist/page/table/window.js +7 -5
- package/dist/page/text.js +4 -2
- package/dist/server/bell/antigravity.d.ts +10 -0
- package/dist/server/bell/antigravity.js +75 -0
- package/dist/server/bell/bell.d.ts +10 -2
- package/dist/server/bell/bell.js +33 -10
- package/dist/server/bell/index.d.ts +1 -1
- package/dist/server/bell/index.js +2 -1
- package/dist/server/mcp/caller.js +26 -1
- package/dist/server/page/game.js +4 -3
- package/dist/server/page/page.js +1 -0
- package/dist/server/page/table.d.ts +14 -2
- package/dist/server/page/table.js +17 -5
- package/dist/server/table/hosts/chats.d.ts +4 -2
- package/dist/server/table/hosts/chats.js +24 -11
- package/dist/server/table/hosts/find.d.ts +6 -0
- package/dist/server/table/hosts/find.js +19 -1
- package/dist/server/table/hosts/hooks.d.ts +5 -2
- package/dist/server/table/hosts/hooks.js +6 -3
- package/dist/server/table/hosts/hosts.d.ts +19 -10
- package/dist/server/table/hosts/hosts.js +34 -16
- package/dist/server/table/hosts/models.d.ts +19 -0
- package/dist/server/table/hosts/models.js +95 -0
- package/dist/server/table/hosts/play.d.ts +7 -2
- package/dist/server/table/hosts/play.js +22 -12
- package/dist/server/table/hosts/seat.d.ts +5 -1
- package/dist/server/table/hosts/seat.js +1 -1
- package/dist/server/table/table.js +1 -1
- 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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
15
|
-
|
|
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,
|
|
40
|
-
return { name, installed
|
|
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
|
-
/**
|
|
4
|
-
|
|
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
|
-
/**
|
|
8
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
61
|
+
return offered(choices, { model: text(settings?.[keys.model]), effort: text(settings?.[keys.effort]) });
|
|
48
62
|
}
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
54
|
-
|
|
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
|
-
/**
|
|
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 {
|
|
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
|
-
/**
|
|
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 =
|
|
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
|
|
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
|
|
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 = {
|
|
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
|
-
/**
|
|
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;
|