linkshell-cli 0.10.31 → 0.10.33
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/dist/cli/src/auth.js +40 -30
- package/dist/cli/src/auth.js.map +1 -1
- package/dist/cli/src/commands/host.js +3 -2
- package/dist/cli/src/commands/host.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/gateway/src/relay.js +14 -3
- package/dist/gateway/src/relay.js.map +1 -1
- package/dist/host/src/drivers/acp/antigravity.d.ts +49 -4
- package/dist/host/src/drivers/acp/antigravity.js +690 -12
- package/dist/host/src/drivers/acp/antigravity.js.map +1 -1
- package/dist/host/src/drivers/acp/connection.js +4 -4
- package/dist/host/src/drivers/acp/connection.js.map +1 -1
- package/dist/host/src/drivers/codex/desktop-ipc.d.ts +3 -0
- package/dist/host/src/drivers/codex/desktop-ipc.js +10 -1
- package/dist/host/src/drivers/codex/desktop-ipc.js.map +1 -1
- package/dist/host/src/drivers/codex/driver.js +19 -7
- package/dist/host/src/drivers/codex/driver.js.map +1 -1
- package/package.json +3 -3
- package/src/auth.ts +44 -35
- package/src/commands/host.ts +3 -2
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { accessSync, constants, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
3
|
import { homedir, tmpdir } from "node:os";
|
|
3
4
|
import { basename, delimiter, dirname, isAbsolute, join } from "node:path";
|
|
5
|
+
import { DatabaseSync } from "node:sqlite";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { RpcError, } from "@linkshell/wire";
|
|
4
9
|
import { AcpDriver } from "./driver.js";
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
5
11
|
/**
|
|
6
12
|
* Official Google Antigravity ACP distribution metadata (from the ACP Registry:
|
|
7
13
|
* `antigravity-acp/agent.json`).
|
|
@@ -74,7 +80,6 @@ function findInManagedToolRoots(roots, binaryName) {
|
|
|
74
80
|
for (const root of roots) {
|
|
75
81
|
if (!existsSync(root))
|
|
76
82
|
continue;
|
|
77
|
-
// Check `<root>/<version>/<platform>-<arch>/<binaryName>` and `<root>/<binaryName>`
|
|
78
83
|
const direct = join(root, binaryName);
|
|
79
84
|
if (isExecutable(direct))
|
|
80
85
|
return { path: direct, version: ANTIGRAVITY_ACP_VERSION };
|
|
@@ -162,6 +167,63 @@ export function resolveAntigravityLaunch(env = process.env) {
|
|
|
162
167
|
version,
|
|
163
168
|
};
|
|
164
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Resolves an installed Antigravity Desktop app (`/Applications/Antigravity.app`)
|
|
172
|
+
* and its conversation store (`~/.gemini/antigravity`).
|
|
173
|
+
*/
|
|
174
|
+
export function resolveAntigravityDesktop(env = process.env) {
|
|
175
|
+
if (env.PATH === "" && !env.ANTIGRAVITY_APP_DATA_DIR)
|
|
176
|
+
return undefined;
|
|
177
|
+
const home = env.HOME || homedir();
|
|
178
|
+
const dataCandidates = [
|
|
179
|
+
env.ANTIGRAVITY_APP_DATA_DIR?.trim(),
|
|
180
|
+
join(home, ".gemini", "antigravity"),
|
|
181
|
+
join(home, ".gemini", "antigravity-ide"),
|
|
182
|
+
].filter((entry) => Boolean(entry));
|
|
183
|
+
const dataDir = dataCandidates.find((dir) => existsSync(join(dir, "conversation_summaries.db")) || existsSync(join(dir, "brain")));
|
|
184
|
+
const lsCandidates = process.platform === "darwin"
|
|
185
|
+
? [
|
|
186
|
+
"/Applications/Antigravity.app/Contents/Resources/bin/language_server",
|
|
187
|
+
join(home, "Applications", "Antigravity.app", "Contents", "Resources", "bin", "language_server"),
|
|
188
|
+
]
|
|
189
|
+
: [];
|
|
190
|
+
const languageServerPath = lsCandidates.find((candidate) => isExecutable(candidate));
|
|
191
|
+
if (!dataDir && !languageServerPath)
|
|
192
|
+
return undefined;
|
|
193
|
+
const logCandidates = process.platform === "darwin"
|
|
194
|
+
? [join(home, "Library", "Logs", "Antigravity", "language_server.log")]
|
|
195
|
+
: [join(home, ".config", "Antigravity", "logs", "language_server.log")];
|
|
196
|
+
const logPath = logCandidates.find((candidate) => existsSync(candidate));
|
|
197
|
+
let version = "desktop";
|
|
198
|
+
if (logPath) {
|
|
199
|
+
try {
|
|
200
|
+
const head = readFileSync(logPath, "utf8").slice(0, 2048);
|
|
201
|
+
const match = /Language server version:\s*([^\s\r\n]+)/i.exec(head);
|
|
202
|
+
if (match?.[1])
|
|
203
|
+
version = match[1];
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// ignore unreadable log
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (version === "desktop" && process.platform === "darwin") {
|
|
210
|
+
try {
|
|
211
|
+
const plist = readFileSync("/Applications/Antigravity.app/Contents/Info.plist", "utf8");
|
|
212
|
+
const match = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]+)<\/string>/.exec(plist);
|
|
213
|
+
if (match?.[1])
|
|
214
|
+
version = match[1].trim();
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// ignore unreadable plist
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
dataDir: dataDir ?? join(home, ".gemini", "antigravity"),
|
|
222
|
+
languageServerPath,
|
|
223
|
+
logPath,
|
|
224
|
+
version,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
165
227
|
const AUTH_URL_PATTERN = /Open the following link to authenticate the ACP server:\s*(https?:\/\/\S+)/i;
|
|
166
228
|
export function extractAntigravityAuthUrl(line) {
|
|
167
229
|
const match = AUTH_URL_PATTERN.exec(line.trim());
|
|
@@ -187,12 +249,269 @@ export function antigravitySpec() {
|
|
|
187
249
|
authStatus: antigravityAuthStatus,
|
|
188
250
|
};
|
|
189
251
|
}
|
|
252
|
+
function unquoteArg(value) {
|
|
253
|
+
if (typeof value !== "string")
|
|
254
|
+
return value;
|
|
255
|
+
const trimmed = value.trim();
|
|
256
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
257
|
+
(trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
258
|
+
(trimmed.startsWith("[") && trimmed.endsWith("]")) ||
|
|
259
|
+
trimmed === "true" ||
|
|
260
|
+
trimmed === "false" ||
|
|
261
|
+
/^-?\d+(?:\.\d+)?$/.test(trimmed)) {
|
|
262
|
+
try {
|
|
263
|
+
return JSON.parse(trimmed);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
function parseToolArgs(raw) {
|
|
272
|
+
if (!raw || typeof raw !== "object")
|
|
273
|
+
return {};
|
|
274
|
+
const result = {};
|
|
275
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
276
|
+
result[k] = unquoteArg(v);
|
|
277
|
+
}
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
export function cleanAntigravityUserInput(content) {
|
|
281
|
+
const requestMatch = /<USER_REQUEST>\s*([\s\S]*?)\s*<\/USER_REQUEST>/i.exec(content);
|
|
282
|
+
if (requestMatch)
|
|
283
|
+
return requestMatch[1].trim();
|
|
284
|
+
return content
|
|
285
|
+
.replace(/<(?:ADDITIONAL_METADATA|USER_SETTINGS_CHANGE|SYSTEM_MESSAGE|CONTEXT_SUMMARY)>[\s\S]*?<\/(?:ADDITIONAL_METADATA|USER_SETTINGS_CHANGE|SYSTEM_MESSAGE|CONTEXT_SUMMARY)>/gi, "")
|
|
286
|
+
.trim();
|
|
287
|
+
}
|
|
288
|
+
function mapAntigravityToolCall(stepIndex, callIndex, call, outputText) {
|
|
289
|
+
const name = call.name ?? "tool";
|
|
290
|
+
const args = parseToolArgs(call.args);
|
|
291
|
+
const summary = (typeof args.toolSummary === "string" && args.toolSummary.trim()) ||
|
|
292
|
+
(typeof args.toolAction === "string" && args.toolAction.trim()) ||
|
|
293
|
+
name;
|
|
294
|
+
const toolCallId = call.id || `agy:tc:${stepIndex}:${callIndex}`;
|
|
295
|
+
let kind = "other";
|
|
296
|
+
let rawInput = args;
|
|
297
|
+
let locations;
|
|
298
|
+
if (name === "run_command") {
|
|
299
|
+
kind = "execute";
|
|
300
|
+
rawInput = {
|
|
301
|
+
command: typeof args.CommandLine === "string" ? args.CommandLine : summary,
|
|
302
|
+
cwd: typeof args.Cwd === "string" ? args.Cwd : undefined,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
else if (name === "view_file") {
|
|
306
|
+
kind = "read";
|
|
307
|
+
const path = typeof args.AbsolutePath === "string" ? args.AbsolutePath : undefined;
|
|
308
|
+
const line = typeof args.StartLine === "number" ? args.StartLine : undefined;
|
|
309
|
+
if (path) {
|
|
310
|
+
rawInput = { file_path: path };
|
|
311
|
+
locations = [{ path, ...(line ? { line } : {}) }];
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else if (name === "replace_file_content" || name === "multi_replace_file_content" || name === "write_to_file") {
|
|
315
|
+
kind = "edit";
|
|
316
|
+
const path = typeof args.TargetFile === "string" ? args.TargetFile : undefined;
|
|
317
|
+
const line = typeof args.StartLine === "number" ? args.StartLine : undefined;
|
|
318
|
+
if (path) {
|
|
319
|
+
rawInput = { file_path: path };
|
|
320
|
+
locations = [{ path, ...(line ? { line } : {}) }];
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else if (name === "search_web") {
|
|
324
|
+
kind = "search";
|
|
325
|
+
rawInput = { query: typeof args.query === "string" ? args.query : summary };
|
|
326
|
+
}
|
|
327
|
+
else if (name === "read_url_content") {
|
|
328
|
+
kind = "fetch";
|
|
329
|
+
rawInput = { url: typeof args.Url === "string" ? args.Url : summary };
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
sessionUpdate: "tool_call",
|
|
333
|
+
toolCallId,
|
|
334
|
+
title: summary,
|
|
335
|
+
name,
|
|
336
|
+
kind,
|
|
337
|
+
status: "completed",
|
|
338
|
+
rawInput,
|
|
339
|
+
...(outputText ? { rawOutput: { output: outputText } } : {}),
|
|
340
|
+
...(locations ? { locations } : {}),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Converts an Antigravity Desktop `transcript.jsonl` file into LinkShell's
|
|
345
|
+
* `HistoryItem[]` list.
|
|
346
|
+
*/
|
|
347
|
+
export function parseAntigravityTranscript(transcriptPath, fromLine = 0) {
|
|
348
|
+
if (!existsSync(transcriptPath))
|
|
349
|
+
return { items: [], nextLine: fromLine };
|
|
350
|
+
let lines;
|
|
351
|
+
try {
|
|
352
|
+
lines = readFileSync(transcriptPath, "utf8").split("\n").filter((line) => line.trim().length > 0);
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return { items: [], nextLine: fromLine };
|
|
356
|
+
}
|
|
357
|
+
if (fromLine >= lines.length)
|
|
358
|
+
return { items: [], nextLine: lines.length };
|
|
359
|
+
const parsed = [];
|
|
360
|
+
for (let i = fromLine; i < lines.length; i++) {
|
|
361
|
+
try {
|
|
362
|
+
const step = JSON.parse(lines[i]);
|
|
363
|
+
parsed.push({ index: step.step_index ?? i, step });
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
// skip partial trailing JSON line
|
|
367
|
+
return { items: [], nextLine: i };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const items = [];
|
|
371
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
372
|
+
const { index: stepIndex, step } = parsed[i];
|
|
373
|
+
if (step.type === "USER_INPUT") {
|
|
374
|
+
const text = typeof step.content === "string" ? cleanAntigravityUserInput(step.content) : "";
|
|
375
|
+
if (!text)
|
|
376
|
+
continue;
|
|
377
|
+
const messageId = `agy:user:${stepIndex}`;
|
|
378
|
+
items.push({
|
|
379
|
+
itemId: messageId,
|
|
380
|
+
updates: [
|
|
381
|
+
{
|
|
382
|
+
sessionUpdate: "user_message_chunk",
|
|
383
|
+
messageId,
|
|
384
|
+
content: { type: "text", text },
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
else if (step.type === "PLANNER_RESPONSE") {
|
|
390
|
+
const updates = [];
|
|
391
|
+
if (typeof step.thinking === "string" && step.thinking.trim()) {
|
|
392
|
+
const thoughtId = `agy:thought:${stepIndex}`;
|
|
393
|
+
updates.push({
|
|
394
|
+
sessionUpdate: "agent_thought_chunk",
|
|
395
|
+
messageId: thoughtId,
|
|
396
|
+
content: { type: "text", text: step.thinking.trim() },
|
|
397
|
+
}, {
|
|
398
|
+
sessionUpdate: "ls_message_done",
|
|
399
|
+
messageId: thoughtId,
|
|
400
|
+
role: "thought",
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
const calls = Array.isArray(step.tool_calls) ? step.tool_calls : [];
|
|
404
|
+
if (calls.length > 0) {
|
|
405
|
+
// Subsequent GENERIC steps hold the outputs of these tool calls in order.
|
|
406
|
+
const outputs = [];
|
|
407
|
+
let lookahead = i + 1;
|
|
408
|
+
while (lookahead < parsed.length && parsed[lookahead].step.type === "GENERIC" && outputs.length < calls.length) {
|
|
409
|
+
outputs.push(typeof parsed[lookahead].step.content === "string" ? parsed[lookahead].step.content : "");
|
|
410
|
+
lookahead++;
|
|
411
|
+
}
|
|
412
|
+
calls.forEach((call, callIdx) => {
|
|
413
|
+
updates.push(mapAntigravityToolCall(stepIndex, callIdx, call, outputs[callIdx]));
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
if (typeof step.content === "string" && step.content.trim()) {
|
|
417
|
+
const messageId = `agy:msg:${stepIndex}`;
|
|
418
|
+
updates.push({
|
|
419
|
+
sessionUpdate: "agent_message_chunk",
|
|
420
|
+
messageId,
|
|
421
|
+
content: { type: "text", text: step.content.trim() },
|
|
422
|
+
}, {
|
|
423
|
+
sessionUpdate: "ls_message_done",
|
|
424
|
+
messageId,
|
|
425
|
+
role: "agent",
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
if (updates.length > 0) {
|
|
429
|
+
items.push({
|
|
430
|
+
itemId: `agy:step:${stepIndex}`,
|
|
431
|
+
updates,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return { items, nextLine: lines.length };
|
|
437
|
+
}
|
|
438
|
+
async function findRunningLanguageServer(desktop) {
|
|
439
|
+
if (!desktop?.logPath || !existsSync(desktop.logPath))
|
|
440
|
+
return undefined;
|
|
441
|
+
let httpPort;
|
|
442
|
+
try {
|
|
443
|
+
const head = readFileSync(desktop.logPath, "utf8").slice(0, 4096);
|
|
444
|
+
const match = /listening on \w+ port at (\d+) for HTTP\b/i.exec(head);
|
|
445
|
+
if (match?.[1])
|
|
446
|
+
httpPort = Number(match[1]);
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
451
|
+
if (!httpPort)
|
|
452
|
+
return undefined;
|
|
453
|
+
try {
|
|
454
|
+
const { stdout } = await execFileAsync("ps", ["-ax", "-o", "args="], { timeout: 3000 });
|
|
455
|
+
for (const line of stdout.split("\n")) {
|
|
456
|
+
if (!line.includes("language_server") || !line.includes("--csrf_token"))
|
|
457
|
+
continue;
|
|
458
|
+
const tokenMatch = /--csrf_token[=\s]+([^\s]+)/.exec(line);
|
|
459
|
+
if (tokenMatch?.[1]) {
|
|
460
|
+
return { httpPort, csrfToken: tokenMatch[1] };
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
// ignore ps errors
|
|
466
|
+
}
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
async function callLanguageServer(conn, method, body) {
|
|
470
|
+
const response = await fetch(`http://127.0.0.1:${conn.httpPort}/exa.language_server_pb.LanguageServerService/${method}`, {
|
|
471
|
+
method: "POST",
|
|
472
|
+
headers: {
|
|
473
|
+
"Content-Type": "application/json",
|
|
474
|
+
"x-codeium-csrf-token": conn.csrfToken,
|
|
475
|
+
},
|
|
476
|
+
body: JSON.stringify(body),
|
|
477
|
+
signal: AbortSignal.timeout(10_000),
|
|
478
|
+
});
|
|
479
|
+
if (!response.ok) {
|
|
480
|
+
const text = await response.text().catch(() => "");
|
|
481
|
+
throw new Error(`Antigravity Desktop RPC ${method} failed (${response.status}): ${text.slice(0, 200)}`);
|
|
482
|
+
}
|
|
483
|
+
return (await response.json());
|
|
484
|
+
}
|
|
485
|
+
function parseWorkspaceCwd(workspaceUrisJson, fallbackCwd) {
|
|
486
|
+
if (!workspaceUrisJson)
|
|
487
|
+
return fallbackCwd;
|
|
488
|
+
try {
|
|
489
|
+
const uris = JSON.parse(workspaceUrisJson);
|
|
490
|
+
if (Array.isArray(uris) && typeof uris[0] === "string") {
|
|
491
|
+
const first = uris[0];
|
|
492
|
+
if (first.startsWith("file://"))
|
|
493
|
+
return fileURLToPath(first);
|
|
494
|
+
if (isAbsolute(first))
|
|
495
|
+
return first;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
// ignore invalid JSON
|
|
500
|
+
}
|
|
501
|
+
return fallbackCwd;
|
|
502
|
+
}
|
|
190
503
|
/**
|
|
191
|
-
* Specialized
|
|
192
|
-
*
|
|
193
|
-
*
|
|
504
|
+
* Specialized driver for Google Antigravity:
|
|
505
|
+
* - Drives `agy_acp_server.par` / `agy-acp` over ACP when installed.
|
|
506
|
+
* - Also discovers and interacts with Antigravity Desktop (`/Applications/Antigravity.app`,
|
|
507
|
+
* `~/.gemini/antigravity/conversation_summaries.db`, `brain/<id>/.system_generated/logs/transcript.jsonl`,
|
|
508
|
+
* and its local `language_server` Connect-RPC service) so users with only the Desktop app
|
|
509
|
+
* get full session visibility, history, model selection, and messaging.
|
|
194
510
|
*/
|
|
195
511
|
export class AntigravityDriver extends AcpDriver {
|
|
512
|
+
desktopMode = false;
|
|
513
|
+
desktopWatchers = new Map();
|
|
514
|
+
desktopModelsCache;
|
|
196
515
|
constructor(options) {
|
|
197
516
|
super(antigravitySpec(), options);
|
|
198
517
|
}
|
|
@@ -204,21 +523,380 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
204
523
|
...resolved?.env,
|
|
205
524
|
};
|
|
206
525
|
}
|
|
526
|
+
async authStatus() {
|
|
527
|
+
const base = await super.authStatus();
|
|
528
|
+
if (base.state !== "unknown")
|
|
529
|
+
return base;
|
|
530
|
+
const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
|
|
531
|
+
if (desktop && (existsSync(join(desktop.dataDir, "antigravity_state.pbtxt")) || existsSync(join(desktop.dataDir, "conversation_summaries.db")))) {
|
|
532
|
+
return { state: "ok", method: "antigravity-desktop" };
|
|
533
|
+
}
|
|
534
|
+
return base;
|
|
535
|
+
}
|
|
207
536
|
async detectVersion() {
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
537
|
+
const env = this.options.env ?? process.env;
|
|
538
|
+
const resolved = resolveAntigravityLaunch(env);
|
|
539
|
+
if (resolved) {
|
|
540
|
+
this.desktopMode = false;
|
|
541
|
+
this.spec.command = resolved.command;
|
|
542
|
+
this.spec.args = resolved.args;
|
|
543
|
+
return resolved.version;
|
|
544
|
+
}
|
|
545
|
+
const desktop = resolveAntigravityDesktop(env);
|
|
546
|
+
if (desktop) {
|
|
547
|
+
this.desktopMode = true;
|
|
548
|
+
return desktop.version;
|
|
549
|
+
}
|
|
550
|
+
return undefined;
|
|
214
551
|
}
|
|
215
552
|
async boot() {
|
|
216
|
-
const
|
|
553
|
+
const env = this.options.env ?? process.env;
|
|
554
|
+
const resolved = resolveAntigravityLaunch(env);
|
|
217
555
|
if (resolved) {
|
|
556
|
+
this.desktopMode = false;
|
|
218
557
|
this.spec.command = resolved.command;
|
|
219
558
|
this.spec.args = resolved.args;
|
|
559
|
+
await super.boot();
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const desktop = resolveAntigravityDesktop(env);
|
|
563
|
+
if (desktop) {
|
|
564
|
+
this.desktopMode = true;
|
|
565
|
+
this.capabilityState = {
|
|
566
|
+
...this.capabilityState,
|
|
567
|
+
images: true,
|
|
568
|
+
models: true,
|
|
569
|
+
};
|
|
570
|
+
this.current = { installed: true, version: desktop.version };
|
|
571
|
+
return;
|
|
220
572
|
}
|
|
221
573
|
await super.boot();
|
|
222
574
|
}
|
|
575
|
+
async stop() {
|
|
576
|
+
for (const watcher of this.desktopWatchers.values())
|
|
577
|
+
clearInterval(watcher.timer);
|
|
578
|
+
this.desktopWatchers.clear();
|
|
579
|
+
await super.stop();
|
|
580
|
+
}
|
|
581
|
+
transcriptPathFor(nativeId) {
|
|
582
|
+
const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
|
|
583
|
+
if (!desktop)
|
|
584
|
+
return undefined;
|
|
585
|
+
return join(desktop.dataDir, "brain", nativeId, ".system_generated", "logs", "transcript.jsonl");
|
|
586
|
+
}
|
|
587
|
+
isConversationRunningInDesktop(nativeId, statusCol) {
|
|
588
|
+
const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
|
|
589
|
+
if (!desktop)
|
|
590
|
+
return false;
|
|
591
|
+
const lockPath = join(desktop.dataDir, "presence", `${nativeId}.lock`);
|
|
592
|
+
if (statusCol && statusCol !== "CASCADE_RUN_STATUS_RUNNING")
|
|
593
|
+
return false;
|
|
594
|
+
return existsSync(lockPath) && statusCol === "CASCADE_RUN_STATUS_RUNNING";
|
|
595
|
+
}
|
|
596
|
+
listDesktopSessions(limit) {
|
|
597
|
+
const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
|
|
598
|
+
if (!desktop)
|
|
599
|
+
return [];
|
|
600
|
+
const dbPath = join(desktop.dataDir, "conversation_summaries.db");
|
|
601
|
+
const home = (this.options.env ?? process.env).HOME || homedir();
|
|
602
|
+
const results = [];
|
|
603
|
+
if (existsSync(dbPath)) {
|
|
604
|
+
let db;
|
|
605
|
+
try {
|
|
606
|
+
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
607
|
+
const rows = db
|
|
608
|
+
.prepare(`SELECT conversation_id, title, last_modified_time, workspace_uris, status, parent_conversation_id, killed
|
|
609
|
+
FROM conversation_summaries
|
|
610
|
+
WHERE (parent_conversation_id = '' OR parent_conversation_id IS NULL) AND killed = 0
|
|
611
|
+
ORDER BY last_modified_time DESC
|
|
612
|
+
LIMIT ?`)
|
|
613
|
+
.all(limit);
|
|
614
|
+
for (const row of rows) {
|
|
615
|
+
if (!row.conversation_id)
|
|
616
|
+
continue;
|
|
617
|
+
const cwd = parseWorkspaceCwd(row.workspace_uris, home);
|
|
618
|
+
const updatedAt = Date.parse(row.last_modified_time) || Date.now();
|
|
619
|
+
let title = row.title?.trim() || undefined;
|
|
620
|
+
if (!title) {
|
|
621
|
+
const transcriptPath = join(desktop.dataDir, "brain", row.conversation_id, ".system_generated", "logs", "transcript.jsonl");
|
|
622
|
+
const { items } = parseAntigravityTranscript(transcriptPath, 0);
|
|
623
|
+
const firstUser = items.find((entry) => entry.updates.some((u) => u.sessionUpdate === "user_message_chunk"));
|
|
624
|
+
const chunk = firstUser?.updates.find((u) => u.sessionUpdate === "user_message_chunk");
|
|
625
|
+
if (chunk?.sessionUpdate === "user_message_chunk" && chunk.content.type === "text") {
|
|
626
|
+
title = chunk.content.text.split("\n")[0]?.slice(0, 100).trim() || undefined;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const running = this.isConversationRunningInDesktop(row.conversation_id, row.status);
|
|
630
|
+
results.push({
|
|
631
|
+
nativeId: row.conversation_id,
|
|
632
|
+
cwd,
|
|
633
|
+
title,
|
|
634
|
+
createdAt: updatedAt,
|
|
635
|
+
updatedAt,
|
|
636
|
+
state: running ? "running" : "idle",
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
// fallback to brain directory scan below if sqlite table is locked or unreadable
|
|
642
|
+
}
|
|
643
|
+
finally {
|
|
644
|
+
try {
|
|
645
|
+
db?.close();
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
// ignore close errors
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (results.length === 0 && existsSync(join(desktop.dataDir, "brain"))) {
|
|
653
|
+
try {
|
|
654
|
+
const dirs = readdirSync(join(desktop.dataDir, "brain"));
|
|
655
|
+
for (const id of dirs) {
|
|
656
|
+
const transcriptPath = join(desktop.dataDir, "brain", id, ".system_generated", "logs", "transcript.jsonl");
|
|
657
|
+
if (!existsSync(transcriptPath))
|
|
658
|
+
continue;
|
|
659
|
+
const stat = statSync(transcriptPath);
|
|
660
|
+
const { items } = parseAntigravityTranscript(transcriptPath, 0);
|
|
661
|
+
const firstUser = items.find((entry) => entry.updates.some((u) => u.sessionUpdate === "user_message_chunk"));
|
|
662
|
+
const chunk = firstUser?.updates.find((u) => u.sessionUpdate === "user_message_chunk");
|
|
663
|
+
const title = chunk?.sessionUpdate === "user_message_chunk" && chunk.content.type === "text"
|
|
664
|
+
? chunk.content.text.split("\n")[0]?.slice(0, 100).trim()
|
|
665
|
+
: undefined;
|
|
666
|
+
results.push({
|
|
667
|
+
nativeId: id,
|
|
668
|
+
cwd: home,
|
|
669
|
+
title,
|
|
670
|
+
createdAt: stat.mtimeMs,
|
|
671
|
+
updatedAt: stat.mtimeMs,
|
|
672
|
+
state: "idle",
|
|
673
|
+
});
|
|
674
|
+
if (results.length >= limit)
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
// ignore unreadable brain dir
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return results;
|
|
683
|
+
}
|
|
684
|
+
async listSessions(limit) {
|
|
685
|
+
const acpSessions = !this.desktopMode && this.connection?.alive ? await super.listSessions(limit).catch(() => []) : [];
|
|
686
|
+
const desktopSessions = this.listDesktopSessions(limit);
|
|
687
|
+
const merged = new Map();
|
|
688
|
+
for (const session of [...acpSessions, ...desktopSessions]) {
|
|
689
|
+
if (!merged.has(session.nativeId))
|
|
690
|
+
merged.set(session.nativeId, session);
|
|
691
|
+
}
|
|
692
|
+
return [...merged.values()].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, limit);
|
|
693
|
+
}
|
|
694
|
+
async loadDesktopModels() {
|
|
695
|
+
if (this.desktopModelsCache?.length)
|
|
696
|
+
return this.desktopModelsCache;
|
|
697
|
+
const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
|
|
698
|
+
const conn = await findRunningLanguageServer(desktop);
|
|
699
|
+
if (!conn)
|
|
700
|
+
return [];
|
|
701
|
+
try {
|
|
702
|
+
const data = await callLanguageServer(conn, "GetCascadeModelConfigData", {});
|
|
703
|
+
const models = (data.clientModelConfigs ?? [])
|
|
704
|
+
.map((entry) => {
|
|
705
|
+
const value = entry.modelOrAlias?.model ?? entry.modelOrAlias?.alias;
|
|
706
|
+
const name = entry.label?.trim();
|
|
707
|
+
if (!value || !name)
|
|
708
|
+
return undefined;
|
|
709
|
+
return {
|
|
710
|
+
value,
|
|
711
|
+
name,
|
|
712
|
+
...(entry.tagTitle ? { description: entry.tagTitle } : {}),
|
|
713
|
+
};
|
|
714
|
+
})
|
|
715
|
+
.filter((entry) => Boolean(entry));
|
|
716
|
+
if (models.length > 0)
|
|
717
|
+
this.desktopModelsCache = models;
|
|
718
|
+
return models;
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
return [];
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
async emitDesktopConfig(nativeId, selectedModel) {
|
|
725
|
+
const models = await this.loadDesktopModels();
|
|
726
|
+
if (models.length === 0)
|
|
727
|
+
return;
|
|
728
|
+
let defaultModel = selectedModel;
|
|
729
|
+
if (!defaultModel) {
|
|
730
|
+
const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
|
|
731
|
+
if (desktop && existsSync(join(desktop.dataDir, "antigravity_state.pbtxt"))) {
|
|
732
|
+
try {
|
|
733
|
+
const stateText = readFileSync(join(desktop.dataDir, "antigravity_state.pbtxt"), "utf8");
|
|
734
|
+
const match = /last_selected_agent_model:\s*(\S+)/.exec(stateText);
|
|
735
|
+
if (match?.[1] && models.some((m) => m.value === match[1]))
|
|
736
|
+
defaultModel = match[1];
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
// ignore
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
const current = defaultModel && models.some((m) => m.value === defaultModel) ? defaultModel : models[0].value;
|
|
744
|
+
const options = [
|
|
745
|
+
{
|
|
746
|
+
id: "model",
|
|
747
|
+
name: "Model",
|
|
748
|
+
category: "model",
|
|
749
|
+
current,
|
|
750
|
+
values: models,
|
|
751
|
+
},
|
|
752
|
+
];
|
|
753
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_config", options });
|
|
754
|
+
}
|
|
755
|
+
startWatchingDesktopTranscript(nativeId, transcriptPath, initialLine) {
|
|
756
|
+
const existing = this.desktopWatchers.get(nativeId);
|
|
757
|
+
if (existing)
|
|
758
|
+
clearInterval(existing.timer);
|
|
759
|
+
const lastSize = existsSync(transcriptPath) ? statSync(transcriptPath).size : 0;
|
|
760
|
+
const watcher = {
|
|
761
|
+
nextLine: initialLine,
|
|
762
|
+
lastSize,
|
|
763
|
+
running: false,
|
|
764
|
+
selectedModel: existing?.selectedModel,
|
|
765
|
+
timer: setInterval(() => {
|
|
766
|
+
if (!existsSync(transcriptPath))
|
|
767
|
+
return;
|
|
768
|
+
let stat;
|
|
769
|
+
try {
|
|
770
|
+
stat = statSync(transcriptPath);
|
|
771
|
+
}
|
|
772
|
+
catch {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (stat.size <= watcher.lastSize)
|
|
776
|
+
return;
|
|
777
|
+
watcher.lastSize = stat.size;
|
|
778
|
+
const { items, nextLine } = parseAntigravityTranscript(transcriptPath, watcher.nextLine);
|
|
779
|
+
watcher.nextLine = nextLine;
|
|
780
|
+
for (const item of items) {
|
|
781
|
+
item.updates.forEach((update, idx) => {
|
|
782
|
+
this.host?.update(this.id, nativeId, update, idx === item.updates.length - 1 ? item.itemId : undefined);
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}, 1500),
|
|
786
|
+
};
|
|
787
|
+
watcher.timer.unref?.();
|
|
788
|
+
this.desktopWatchers.set(nativeId, watcher);
|
|
789
|
+
}
|
|
790
|
+
async attach(nativeId, context) {
|
|
791
|
+
const transcriptPath = this.transcriptPathFor(nativeId);
|
|
792
|
+
if (this.desktopMode || (transcriptPath && existsSync(transcriptPath))) {
|
|
793
|
+
await this.ensureStarted();
|
|
794
|
+
const { items, nextLine } = transcriptPath ? parseAntigravityTranscript(transcriptPath, 0) : { items: [], nextLine: 0 };
|
|
795
|
+
if (transcriptPath) {
|
|
796
|
+
this.startWatchingDesktopTranscript(nativeId, transcriptPath, nextLine);
|
|
797
|
+
}
|
|
798
|
+
const running = this.isConversationRunningInDesktop(nativeId, "CASCADE_RUN_STATUS_RUNNING");
|
|
799
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: running ? "running" : "idle", turnActive: running });
|
|
800
|
+
void this.emitDesktopConfig(nativeId, this.desktopWatchers.get(nativeId)?.selectedModel);
|
|
801
|
+
return items;
|
|
802
|
+
}
|
|
803
|
+
return super.attach(nativeId, context);
|
|
804
|
+
}
|
|
805
|
+
async detach(nativeId) {
|
|
806
|
+
const watcher = this.desktopWatchers.get(nativeId);
|
|
807
|
+
if (watcher) {
|
|
808
|
+
clearInterval(watcher.timer);
|
|
809
|
+
this.desktopWatchers.delete(nativeId);
|
|
810
|
+
}
|
|
811
|
+
await super.detach(nativeId);
|
|
812
|
+
}
|
|
813
|
+
async createSession(options) {
|
|
814
|
+
if (!this.desktopMode)
|
|
815
|
+
return super.createSession(options);
|
|
816
|
+
await this.ensureStarted();
|
|
817
|
+
const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
|
|
818
|
+
const conn = await findRunningLanguageServer(desktop);
|
|
819
|
+
if (!conn) {
|
|
820
|
+
throw RpcError.app("agent_unavailable", "请先在电脑上打开 Antigravity 桌面端应用");
|
|
821
|
+
}
|
|
822
|
+
const response = await callLanguageServer(conn, "StartCascade", {
|
|
823
|
+
workspaceUris: [pathToFileURL(options.cwd).href],
|
|
824
|
+
overrideWorkspaceUris: true,
|
|
825
|
+
...(options.model ? { requestedModel: options.model } : {}),
|
|
826
|
+
});
|
|
827
|
+
const nativeId = response.cascadeId;
|
|
828
|
+
if (!nativeId)
|
|
829
|
+
throw RpcError.app("agent_unavailable", "Antigravity 未返回新会话 ID");
|
|
830
|
+
const transcriptPath = this.transcriptPathFor(nativeId);
|
|
831
|
+
if (transcriptPath)
|
|
832
|
+
this.startWatchingDesktopTranscript(nativeId, transcriptPath, 0);
|
|
833
|
+
if (options.model) {
|
|
834
|
+
const watcher = this.desktopWatchers.get(nativeId);
|
|
835
|
+
if (watcher)
|
|
836
|
+
watcher.selectedModel = options.model;
|
|
837
|
+
}
|
|
838
|
+
void this.emitDesktopConfig(nativeId, options.model);
|
|
839
|
+
const now = Date.now();
|
|
840
|
+
return { nativeId, cwd: options.cwd, createdAt: now, updatedAt: now, state: "idle" };
|
|
841
|
+
}
|
|
842
|
+
async prompt(nativeId, content, clientMessageId, context) {
|
|
843
|
+
const transcriptPath = this.transcriptPathFor(nativeId);
|
|
844
|
+
if (!this.desktopMode && !(transcriptPath && existsSync(transcriptPath))) {
|
|
845
|
+
return super.prompt(nativeId, content, clientMessageId, context);
|
|
846
|
+
}
|
|
847
|
+
const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
|
|
848
|
+
const conn = await findRunningLanguageServer(desktop);
|
|
849
|
+
if (!conn) {
|
|
850
|
+
throw RpcError.app("agent_unavailable", "请先在电脑上打开 Antigravity 桌面端应用,再发送消息");
|
|
851
|
+
}
|
|
852
|
+
const text = content
|
|
853
|
+
.map((block) => (block.type === "text" ? block.text : ""))
|
|
854
|
+
.join("\n")
|
|
855
|
+
.trim();
|
|
856
|
+
if (!text)
|
|
857
|
+
throw RpcError.app("invalid_params", "nothing to send");
|
|
858
|
+
const fullText = context ? `${context}\n\n${text}` : text;
|
|
859
|
+
const selectedModel = this.desktopWatchers.get(nativeId)?.selectedModel;
|
|
860
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "started" });
|
|
861
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "running", turnActive: true });
|
|
862
|
+
await callLanguageServer(conn, "SendUserCascadeMessage", {
|
|
863
|
+
cascadeId: nativeId,
|
|
864
|
+
items: [{ text: fullText }],
|
|
865
|
+
...(selectedModel ? { cascadeConfig: { plannerConfig: { requestedModel: { model: selectedModel } } } } : {}),
|
|
866
|
+
});
|
|
867
|
+
if (transcriptPath && !this.desktopWatchers.has(nativeId)) {
|
|
868
|
+
const { nextLine } = parseAntigravityTranscript(transcriptPath, 0);
|
|
869
|
+
this.startWatchingDesktopTranscript(nativeId, transcriptPath, nextLine);
|
|
870
|
+
}
|
|
871
|
+
return "started";
|
|
872
|
+
}
|
|
873
|
+
async cancel(nativeId) {
|
|
874
|
+
if (this.desktopMode || this.desktopWatchers.has(nativeId)) {
|
|
875
|
+
const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
|
|
876
|
+
const conn = await findRunningLanguageServer(desktop);
|
|
877
|
+
if (conn) {
|
|
878
|
+
await callLanguageServer(conn, "CancelCascadeInvocation", {
|
|
879
|
+
cascadeId: nativeId,
|
|
880
|
+
killBackgroundTasks: false,
|
|
881
|
+
}).catch(() => { });
|
|
882
|
+
}
|
|
883
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason: "cancelled" });
|
|
884
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "idle", turnActive: false });
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
await super.cancel(nativeId);
|
|
888
|
+
}
|
|
889
|
+
async setConfig(nativeId, optionId, value) {
|
|
890
|
+
if (this.desktopMode || this.desktopWatchers.has(nativeId)) {
|
|
891
|
+
if (optionId !== "model")
|
|
892
|
+
throw RpcError.app("not_supported", `Antigravity 暂不支持设置 ${optionId}`);
|
|
893
|
+
const watcher = this.desktopWatchers.get(nativeId);
|
|
894
|
+
if (watcher)
|
|
895
|
+
watcher.selectedModel = value;
|
|
896
|
+
await this.emitDesktopConfig(nativeId, value);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
await super.setConfig(nativeId, optionId, value);
|
|
900
|
+
}
|
|
223
901
|
}
|
|
224
902
|
//# sourceMappingURL=antigravity.js.map
|