chesspal 0.4.0 → 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.
- 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/index.d.ts +1 -1
- package/dist/commands/game/index.js +1 -1
- package/dist/commands/game/new.d.ts +2 -0
- package/dist/commands/game/new.js +4 -6
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.js +1 -1
- 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 +13 -9
- package/dist/page/app/phase.js +4 -0
- package/dist/page/dialogs/settings.js +6 -7
- package/dist/page/guide/en.md +3 -3
- package/dist/page/guide/it.md +3 -3
- 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/talk/talk.css +13 -0
- package/dist/page/talk/talk.js +3 -3
- 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 +22 -5
- package/dist/server/bell/bell.js +67 -13
- package/dist/server/bell/index.d.ts +2 -1
- package/dist/server/bell/index.js +3 -1
- package/dist/server/bell/interrupted.d.ts +2 -0
- package/dist/server/bell/interrupted.js +56 -0
- package/dist/server/mcp/caller.js +26 -1
- package/dist/server/page/game.js +6 -1
- package/dist/server/page/page.js +3 -2
- package/dist/server/page/table.d.ts +9 -0
- package/dist/server/page/table.js +13 -2
- package/dist/server/table/hosts/chats.d.ts +4 -2
- package/dist/server/table/hosts/chats.js +23 -10
- 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 +4 -1
- package/dist/server/table/hosts/play.js +17 -8
- package/dist/server/table/hosts/seat.js +2 -0
- package/dist/server/table/table.js +1 -1
- package/package.json +1 -1
package/dist/server/bell/bell.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The doorbell for the page (D-47): `chesspal listen`, the Stop hook init writes for Claude Code and
|
|
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
|
/**
|
|
@@ -22,17 +23,22 @@ export class Bell {
|
|
|
22
23
|
woken = false;
|
|
23
24
|
// A hook has waited for the bell since the chat came: its host runs Chesspal's hooks, and only they tell where a turn ends.
|
|
24
25
|
hooked = false;
|
|
26
|
+
// Looks again, while a ring waits for a chat in a turn, whether the chat was stopped meanwhile.
|
|
27
|
+
stopWatch;
|
|
25
28
|
news;
|
|
26
29
|
changed;
|
|
30
|
+
stopped;
|
|
27
31
|
/**
|
|
28
32
|
* `news` says whether a ring no hook heard is still worth waking the chat for; `changed` is told when what the page shows of the chat may change: it
|
|
29
|
-
* starts or stops listening, the doorbell wakes it, its turn ends
|
|
33
|
+
* starts or stops listening, the doorbell wakes it, its turn ends; `stopped` whether the host says the chat was stopped in its turn (Esc), which no
|
|
34
|
+
* hook tells.
|
|
30
35
|
*/
|
|
31
|
-
constructor(news = () => true, changed = () => undefined) {
|
|
36
|
+
constructor(news = () => true, changed = () => undefined, stopped = () => false) {
|
|
32
37
|
this.news = news;
|
|
33
38
|
this.changed = changed;
|
|
39
|
+
this.stopped = stopped;
|
|
34
40
|
}
|
|
35
|
-
/** Whether the chat is in a turn, as far as the doorbell knows: a chat stopped with Esc stays in it until its next tool. */
|
|
41
|
+
/** Whether the chat is in a turn, as far as the doorbell knows: a chat stopped with Esc stays in it until its next tool, or until a ring finds it stopped. */
|
|
36
42
|
get inTurn() {
|
|
37
43
|
return this.busy;
|
|
38
44
|
}
|
|
@@ -68,6 +74,7 @@ export class Bell {
|
|
|
68
74
|
/**
|
|
69
75
|
* The player acted on the page: the hook waiting hears it now, or the next one as soon as it comes. In a host that runs no hook nothing tells the end of
|
|
70
76
|
* a turn: an act the chat waits for finds it out of its turn, and nobody hears the player, unless the chat `mayBeAtWork` on the AI's move meanwhile.
|
|
77
|
+
* A chat stopped in its turn ends no turn either: while the ring waits, the doorbell looks every `stopCheck` whether the host says so.
|
|
71
78
|
*/
|
|
72
79
|
ring(mayBeAtWork = false) {
|
|
73
80
|
if (this.listener === undefined) {
|
|
@@ -76,6 +83,9 @@ export class Bell {
|
|
|
76
83
|
this.busy = false;
|
|
77
84
|
this.changed();
|
|
78
85
|
}
|
|
86
|
+
else {
|
|
87
|
+
this.watchForStop();
|
|
88
|
+
}
|
|
79
89
|
return;
|
|
80
90
|
}
|
|
81
91
|
this.listener.end("ring");
|
|
@@ -128,7 +138,29 @@ export class Bell {
|
|
|
128
138
|
this.changed();
|
|
129
139
|
}
|
|
130
140
|
}
|
|
141
|
+
/** A ring waits for a chat in its turn: a chat stopped there, which nobody hears, is out of it now, and the page says where to wake it. */
|
|
142
|
+
watchForStop() {
|
|
143
|
+
if (this.stopWatch !== undefined || !this.waitsInTurn()) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (this.stopped()) {
|
|
147
|
+
this.busy = false;
|
|
148
|
+
this.woken = false;
|
|
149
|
+
this.changed();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this.stopWatch = setTimeout(() => {
|
|
153
|
+
this.stopWatch = undefined;
|
|
154
|
+
this.watchForStop();
|
|
155
|
+
}, stopCheck);
|
|
156
|
+
this.stopWatch.unref();
|
|
157
|
+
}
|
|
158
|
+
waitsInTurn() {
|
|
159
|
+
return this.pending && this.busy && this.listener === undefined;
|
|
160
|
+
}
|
|
131
161
|
}
|
|
162
|
+
/** How often the doorbell looks whether a chat a ring waits for was stopped in its turn. */
|
|
163
|
+
export const stopCheck = 2_000;
|
|
132
164
|
/**
|
|
133
165
|
* Notes in the data folder the port a chat's page took off its folder's usual ports, one file per page, for the hook to find it there; then forgets, off
|
|
134
166
|
* the caller's clock, the notes of its folder whose page is gone: nobody listens at the port, or another folder's page does. A page that answers late
|
|
@@ -154,30 +186,50 @@ export function forgetPagePort(home, folder, port) {
|
|
|
154
186
|
}
|
|
155
187
|
/** What Codex's Stop hook answers to wake the chat: its turn goes on with the doorbell's line. */
|
|
156
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 });
|
|
157
191
|
/**
|
|
158
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
|
|
159
|
-
* Codex's Stop
|
|
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.
|
|
160
196
|
*/
|
|
161
|
-
export async function listen(
|
|
197
|
+
export async function listen(host = "claude") {
|
|
162
198
|
let input = "";
|
|
163
199
|
for await (const chunk of process.stdin) {
|
|
164
200
|
input += String(chunk);
|
|
165
201
|
}
|
|
166
|
-
const
|
|
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();
|
|
167
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.
|
|
168
|
-
if (codex && !heard.rang) {
|
|
209
|
+
if (host === "codex" && !heard.rang) {
|
|
169
210
|
noteSilence(heard.folder, heard.why);
|
|
170
211
|
}
|
|
171
212
|
// It ends as soon as it knows: the chat hears the hook only once it ends.
|
|
172
|
-
|
|
213
|
+
const answer = hookAnswer(host, heard.rang);
|
|
214
|
+
if (answer === undefined) {
|
|
173
215
|
process.exit(0);
|
|
174
216
|
}
|
|
175
|
-
else if (
|
|
176
|
-
process.
|
|
217
|
+
else if (host === "claude") {
|
|
218
|
+
process.stderr.write(`${answer}\n`, () => process.exit(2));
|
|
177
219
|
}
|
|
178
220
|
else {
|
|
179
|
-
process.
|
|
221
|
+
process.stdout.write(`${answer}\n`, () => process.exit(0));
|
|
222
|
+
}
|
|
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;
|
|
180
231
|
}
|
|
232
|
+
return host === "codex" ? codexBellAnswer : bellLine;
|
|
181
233
|
}
|
|
182
234
|
/**
|
|
183
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.
|
|
@@ -233,11 +285,13 @@ export function noteSilence(folder, why, now = new Date()) {
|
|
|
233
285
|
// The log only explains; the hook ends the same.
|
|
234
286
|
}
|
|
235
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. */
|
|
236
289
|
function hookInput(input) {
|
|
237
290
|
try {
|
|
238
291
|
const parsed = JSON.parse(input);
|
|
292
|
+
const session = typeof parsed.session_id === "string" ? parsed.session_id : parsed.conversationId;
|
|
239
293
|
return {
|
|
240
|
-
...(typeof
|
|
294
|
+
...(typeof session === "string" ? { session } : {}),
|
|
241
295
|
...(typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {}),
|
|
242
296
|
...(analystGone(parsed.background_tasks) ? { analystGone: true } : {}),
|
|
243
297
|
};
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { Bell, forgetPagePort, listen, notePagePort } from "./bell.ts";
|
|
1
|
+
export { Bell, forgetPagePort, listen, type ListeningHost, notePagePort } from "./bell.ts";
|
|
2
|
+
export { chatInterrupted } from "./interrupted.ts";
|
|
@@ -1,2 +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
|
|
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";
|
|
4
|
+
export { chatInterrupted } from "./interrupted.js";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// A chat stopped with Esc: Claude Code runs no Stop hook then, so no hook waits for the bell, and the doorbell would think the chat still at work. Its
|
|
2
|
+
// transcript says it: the last line of the conversation is the player's interruption.
|
|
3
|
+
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
4
|
+
import { lastChatStart } from "../../games/index.js";
|
|
5
|
+
/** Whether the chat last started in this folder was stopped in the middle of its turn, as Claude Code writes it in the chat's transcript; false when it cannot be told. */
|
|
6
|
+
export function chatInterrupted(folder) {
|
|
7
|
+
const transcript = lastChatStart(folder)?.transcript;
|
|
8
|
+
return transcript !== undefined && endsInterrupted(tailOf(transcript));
|
|
9
|
+
}
|
|
10
|
+
/** Whether the last line of the conversation in these lines of a transcript is the player's interruption; the lines of agents and of the host's own notes are skipped. */
|
|
11
|
+
function endsInterrupted(lines) {
|
|
12
|
+
for (const line of lines.split("\n").reverse()) {
|
|
13
|
+
const entry = objectOf(line);
|
|
14
|
+
if (entry === undefined || entry.isSidechain === true || (entry.type !== "user" && entry.type !== "assistant")) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const content = entry.message?.content;
|
|
18
|
+
const parts = typeof content === "string" ? [{ type: "text", text: content }] : Array.isArray(content) ? content : [];
|
|
19
|
+
return entry.type === "user" && parts.some((part) => {
|
|
20
|
+
const { type, text } = (part ?? {});
|
|
21
|
+
return type === "text" && typeof text === "string" && text.startsWith(interruption);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
// What Claude Code writes as the player's line when Esc stops the chat, with " for tool use]" or "]" after it.
|
|
27
|
+
const interruption = "[Request interrupted by user";
|
|
28
|
+
// The conversation's last line is near the end of the file.
|
|
29
|
+
const tailBytes = 64 * 1024;
|
|
30
|
+
function tailOf(file) {
|
|
31
|
+
try {
|
|
32
|
+
const descriptor = openSync(file, "r");
|
|
33
|
+
try {
|
|
34
|
+
const size = statSync(file).size;
|
|
35
|
+
const length = Math.min(size, tailBytes);
|
|
36
|
+
const buffer = Buffer.alloc(length);
|
|
37
|
+
readSync(descriptor, buffer, 0, length, size - length);
|
|
38
|
+
return buffer.toString("utf8");
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
closeSync(descriptor);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function objectOf(line) {
|
|
49
|
+
try {
|
|
50
|
+
const value = JSON.parse(line);
|
|
51
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -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).
|
package/dist/server/page/game.js
CHANGED
|
@@ -7,7 +7,7 @@ import { asTheChatSees } from "./chat.js";
|
|
|
7
7
|
import { tellWatchers } from "./events.js";
|
|
8
8
|
import { bodyOf, bytesOf, isJson, json, jsonOf, runRoute, send, textOf } from "./http.js";
|
|
9
9
|
import { profilesShown } from "./profiles.js";
|
|
10
|
-
import { analystAtWork, chatToRelaunch, chatToWaitFor, refuseWaited, relaunchChat, tableShown } from "./table.js";
|
|
10
|
+
import { analystAtWork, chatPlayedAtLength, chatToRelaunch, chatToWaitFor, refuseWaited, relaunchChat, tableShown } from "./table.js";
|
|
11
11
|
import { updateShown } from "./update.js";
|
|
12
12
|
/** Answers the page's requests about the game: true when the request was one. */
|
|
13
13
|
export function answerGame(request, response, url, served) {
|
|
@@ -152,6 +152,11 @@ async function newGameFromPage(request, response, served) {
|
|
|
152
152
|
}
|
|
153
153
|
// The game starts from the page, open: its clocks wait for the player's Start there. A chat the table starts again is clean: no /clear is asked.
|
|
154
154
|
relaunching = chatToRelaunch(served);
|
|
155
|
+
if (!relaunching && chatToWaitFor(served) && chatPlayedAtLength(served, before)) {
|
|
156
|
+
waitForChat(served, input.data);
|
|
157
|
+
send(response, 200, json, JSON.stringify({ waiting: true }));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
155
160
|
const sight = relaunching ? served.context : asTheChatSees(served.context, served.chat);
|
|
156
161
|
chosen = input.data;
|
|
157
162
|
rerunIfChanged(() => startNewGame(input.data, { ...sight, pageOpen: () => true }, "page"));
|
package/dist/server/page/page.js
CHANGED
|
@@ -8,7 +8,7 @@ import { aiNameOf } from "../../core/index.js";
|
|
|
8
8
|
import { forgetAnalyst, gameStamp, tableTalkStamp } from "../../games/index.js";
|
|
9
9
|
import { outdatedHostFiles } from "../../host-files/index.js";
|
|
10
10
|
import { gameFolder } from "../../profiles/index.js";
|
|
11
|
-
import { Bell, forgetPagePort, notePagePort } from "../bell/index.js";
|
|
11
|
+
import { Bell, chatInterrupted, forgetPagePort, notePagePort } from "../bell/index.js";
|
|
12
12
|
import { bellProbe, pagePorts } from "../ports/index.js";
|
|
13
13
|
import { analysesOf, answerAnalyses, sendSiteGames } from "./analyses.js";
|
|
14
14
|
import { answerChat } from "./attach.js";
|
|
@@ -41,7 +41,7 @@ export async function startPage(context, options = {}) {
|
|
|
41
41
|
startWhenAtRest(served);
|
|
42
42
|
updateWhenAtRest(served);
|
|
43
43
|
tellWatchers(watchers);
|
|
44
|
-
}),
|
|
44
|
+
}, () => chatInterrupted(folder)),
|
|
45
45
|
watchers,
|
|
46
46
|
port: 0,
|
|
47
47
|
outdated: outdatedHostFiles(folder, context.platform).length > 0,
|
|
@@ -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 {
|
|
@@ -80,6 +82,13 @@ export declare function relaunchChat(served: Served, how?: "again" | "restart",
|
|
|
80
82
|
* it, it is at work, and no analyst of it is; with an analyst at work the player is asked for /clear, as with a chat opened by hand.
|
|
81
83
|
*/
|
|
82
84
|
export declare function chatToWaitFor(served: Served): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* 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
|
|
87
|
+
* 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.
|
|
88
|
+
*/
|
|
89
|
+
export declare function chatPlayedAtLength(served: Served, current: {
|
|
90
|
+
readonly started: string;
|
|
91
|
+
} | undefined): boolean;
|
|
83
92
|
/**
|
|
84
93
|
* Does what the page asked while the chat was at work, now that it has ended its answer. A chat gone meanwhile takes a start again with it; a new game
|
|
85
94
|
* starts without it, and loses nothing: the next chat at the table is a clean one.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
-
import { analystOf, gameAt, samePosition } from "../../commands/index.js";
|
|
2
|
+
import { analystOf, gameAt, pliesBeforeClearing, samePosition } from "../../commands/index.js";
|
|
3
3
|
import { endOf, gameInAnalysis } from "../../games/index.js";
|
|
4
4
|
import { gameFolder } from "../../profiles/index.js";
|
|
5
5
|
import { tellWatchers } from "./events.js";
|
|
@@ -90,6 +90,17 @@ export function relaunchChat(served, how = "again", host) {
|
|
|
90
90
|
export function chatToWaitFor(served) {
|
|
91
91
|
return heldChat(served) !== undefined && !chatAtRest(served) && !analystAtWork(served);
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* 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
|
+
* 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.
|
|
96
|
+
*/
|
|
97
|
+
export function chatPlayedAtLength(served, current) {
|
|
98
|
+
const { positionSeen: seen, positionFirstSeen: first } = served.chat;
|
|
99
|
+
if (current === undefined || seen?.started !== current.started) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return seen.plies - (first?.started === seen.started ? first.plies : 0) >= pliesBeforeClearing;
|
|
103
|
+
}
|
|
93
104
|
/**
|
|
94
105
|
* Does what the page asked while the chat was at work, now that it has ended its answer. A chat gone meanwhile takes a start again with it; a new game
|
|
95
106
|
* starts without it, and loses nothing: the next chat at the table is a clean one.
|
|
@@ -276,7 +287,7 @@ function hostOffered(served, host) {
|
|
|
276
287
|
function hostsOffered(served) {
|
|
277
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"}.`;
|
|
278
289
|
}
|
|
279
|
-
// The host of the chat at the table, among those offered: it says its name as its MCP client does, «claude-code
|
|
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».
|
|
280
291
|
function hostAtTable(served) {
|
|
281
292
|
const said = served.chat.host()?.toLowerCase() ?? "";
|
|
282
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 {
|
|
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) => {
|
|
@@ -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;
|