@estebanforge/pi-antigravity-bridge 1.4.0 → 1.4.2
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/CHANGELOG.md +22 -0
- package/README.md +11 -7
- package/docs/ACP-ADOPTION-PLAN.md +33 -33
- package/docs/ACP-PROTOCOL-REFERENCE.md +1 -1
- package/extensions/index.ts +207 -49
- package/package.json +5 -4
- package/src/acp/connection.ts +22 -8
- package/src/acp/driver.ts +14 -10
- package/src/acp/setup.ts +355 -0
- package/src/config.ts +8 -4
- package/src/driver.ts +20 -1
- package/src/provider.ts +8 -0
- package/src/skills.ts +109 -32
package/extensions/index.ts
CHANGED
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
// ("[agy tool: editing foo.ts]") for visibility, but the edits already landed
|
|
12
12
|
// on disk and pi's inline diff review does not engage.
|
|
13
13
|
//
|
|
14
|
-
// /agy command:
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// /agy command: full runtime config surface (engine, mode, permissions,
|
|
15
|
+
// bridge tools, model, thinking, digest, system prompt, acp binary) plus
|
|
16
|
+
// doctor, auth, patch-cleanup, and session clear. Config persists to
|
|
17
|
+
// ~/.pi/agent/antigravity-bridge/config.json so toggles survive restarts.
|
|
17
18
|
|
|
19
|
+
import os from "node:os";
|
|
18
20
|
import {
|
|
19
21
|
type ExtensionAPI,
|
|
20
22
|
type ExtensionCommandContext,
|
|
@@ -38,7 +40,7 @@ import { SessionStore } from "../src/sessions.js";
|
|
|
38
40
|
import { ToolRoundTrips, WrapperReplay, createStreamSimple } from "../src/provider.js";
|
|
39
41
|
import { AgyDriver } from "../src/driver.js";
|
|
40
42
|
import { AcpDriver } from "../src/acp/driver.js";
|
|
41
|
-
import {
|
|
43
|
+
import { ensureAcpReady, inspectAcpSetup } from "../src/acp/setup.js";
|
|
42
44
|
import type { TurnDriver } from "../src/driver-types.js";
|
|
43
45
|
import { CONFIG_PATH, loadConfig, saveConfig, type AgyMode, type BridgeTools, type Engine, type ThinkingTier } from "../src/config.js";
|
|
44
46
|
import { registerAskAntigravityTool, toolModelsFromRaw } from "../src/ask-tool.js";
|
|
@@ -97,14 +99,32 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
97
99
|
// MCP bridge handle, declared early: the ACP engine reads the bridge port
|
|
98
100
|
// at session/new / session/load time.
|
|
99
101
|
let mcpHandle: McpServerHandle | null = null;
|
|
102
|
+
// ACP self-heal runs once per process (session_start re-fires on /reload;
|
|
103
|
+
// a ready setup is two file stats, so re-running is harmless anyway).
|
|
104
|
+
let acpSelfHealRan = false;
|
|
100
105
|
// Two turn engines behind one contract (plan §9): stream-json (tested
|
|
101
106
|
// default) and the official ACP server (opt-in via config.engine, off by
|
|
102
107
|
// default). Neither spawns anything until its first turn.
|
|
108
|
+
// ACP log routing: only genuine failures reach stderr. Routine lifecycle
|
|
109
|
+
// (driver-created, spawn, session-new, ...) stays in the driver's
|
|
110
|
+
// #lifecycle ring buffer, visible via /agy doctor. An unfiltered sink fired
|
|
111
|
+
// console.error at extension load ("driver-created"), before any UI exists,
|
|
112
|
+
// and leaked raw driver lines into the terminal on every startup.
|
|
113
|
+
const acpFailures = new Set([
|
|
114
|
+
"start-failed", "spawn-error", "parse-error", "write-failed",
|
|
115
|
+
"mode-apply-failed", "timeout", "stall", "auth-required",
|
|
116
|
+
"session-load-failed-creating-fresh", "connection-exited", "cancel-failed",
|
|
117
|
+
"unsupported-server-request",
|
|
118
|
+
]);
|
|
103
119
|
const legacyDriver = new AgyDriver();
|
|
104
120
|
const acpDriver = new AcpDriver({
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
121
|
+
// Resolved per connection: the setup flow can install the binary and
|
|
122
|
+
// update acp.bin mid-session; the next turn picks it up (no restart).
|
|
123
|
+
bin: () => loadConfig().acp.bin,
|
|
124
|
+
log: (msg, data) => {
|
|
125
|
+
if (!acpFailures.has(msg)) return;
|
|
126
|
+
console.error(`[antigravity-bridge acp] ${msg}${data !== undefined ? " " + JSON.stringify(data) : ""}`);
|
|
127
|
+
},
|
|
108
128
|
mcpServers: () => {
|
|
109
129
|
const handle = mcpHandle;
|
|
110
130
|
if (!handle) return [];
|
|
@@ -233,29 +253,52 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
233
253
|
} catch {
|
|
234
254
|
/* detection is best-effort */
|
|
235
255
|
}
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
256
|
+
// ACP self-heal: engine=acp needs a server binary + auth. Silent when
|
|
257
|
+
// everything is ready; installs from the registry and bootstraps auth
|
|
258
|
+
// otherwise; manual instructions only on failure. Fire-and-forget: it
|
|
259
|
+
// must not delay session start (and nothing spawns until the first turn).
|
|
260
|
+
if (engine === "acp" && !acpSelfHealRan) {
|
|
261
|
+
acpSelfHealRan = true;
|
|
262
|
+
void ensureAcpReady({ configBin: loadConfig().acp.bin }).then((status) => {
|
|
263
|
+
if (status.ok) {
|
|
264
|
+
if (status.binarySource === "installed" || status.binarySource === "existing") {
|
|
265
|
+
saveConfig({ acp: { bin: status.bin, permissions: loadConfig().acp.permissions } });
|
|
266
|
+
}
|
|
267
|
+
if (status.needsLogin) {
|
|
268
|
+
const msg =
|
|
269
|
+
"ACP: one-time Google login pending. Your next antigravity message opens the browser; sign in with your Antigravity subscription account (the same login as the agy CLI). Tokens stay on your machine.";
|
|
270
|
+
if (ctx.hasUI) ctx.ui.notify(msg, "info");
|
|
271
|
+
else console.error(`[antigravity-bridge] ${msg}`);
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const msg = `ACP auto-setup failed (${status.error}).\n${status.manual}`;
|
|
276
|
+
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
277
|
+
else console.error(`[antigravity-bridge] ${msg}`);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
// Bridge failure logger. Routine lifecycle (listening,
|
|
281
|
+
// bridge-config-written/removed, closed) is normal startup/teardown
|
|
282
|
+
// traffic: toasting it every session, or pinning it via stderr in
|
|
283
|
+
// headless mode, was noise. Only genuine failures surface - as a
|
|
284
|
+
// warning toast (ctx.ui.notify, ephemeral) or stderr when headless.
|
|
285
|
+
// Per-turn success events (list-tools / call-tool) stay silent.
|
|
243
286
|
const mcpLog = (s: string, d?: unknown) => {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
"
|
|
287
|
+
// Routine abort traffic: failAll fires on turn end / session shutdown
|
|
288
|
+
// and the bridge answers every parked call with an error. Not a fault.
|
|
289
|
+
if (s === "call-tool-fail") {
|
|
290
|
+
const detail = (d as { msg?: string } | undefined)?.msg ?? "";
|
|
291
|
+
if (detail.includes("unresolved pi tool call") || detail.includes("session shut down")) return;
|
|
292
|
+
}
|
|
293
|
+
const failures = new Set([
|
|
294
|
+
"http-error", "bridge-config-write-failed", "call-tool-fail",
|
|
295
|
+
"transport-error", "handleRequest-error", "request-error",
|
|
296
|
+
"request-handler-error", "unauthorized",
|
|
249
297
|
]);
|
|
250
|
-
if (!
|
|
298
|
+
if (!failures.has(s)) return;
|
|
251
299
|
const msg = `[antigravity-bridge mcp] ${s}${d !== undefined ? " " + JSON.stringify(d) : ""}`;
|
|
252
|
-
if (ctx.hasUI)
|
|
253
|
-
|
|
254
|
-
|| s === "bridge-config-removed" || s === "closed";
|
|
255
|
-
ctx.ui.notify(msg, ok ? "info" : "warning");
|
|
256
|
-
} else {
|
|
257
|
-
console.error(msg);
|
|
258
|
-
}
|
|
300
|
+
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
301
|
+
else console.error(msg);
|
|
259
302
|
};
|
|
260
303
|
// Start the bridge unless the user turned it off. No patch gate, no
|
|
261
304
|
// consent flow: calls route through pi's normal toolUse loop.
|
|
@@ -263,7 +306,9 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
263
306
|
if (bridgeMode === "none") return; // user opted out
|
|
264
307
|
if (mcpHandle) return; // already running (reload re-fires session_start)
|
|
265
308
|
const SKIP = new Set(["AskAntigravity"]);
|
|
266
|
-
|
|
309
|
+
// pi loads project skill locations only after the project is trusted;
|
|
310
|
+
// mirror that gate. Global skill dirs are always scanned.
|
|
311
|
+
const skills: SkillLite[] = scanSkills(ctx.isProjectTrusted() ? process.cwd() : undefined);
|
|
267
312
|
const getAll = (pi as unknown as {
|
|
268
313
|
getAllTools: () => Array<{ name: string; description?: string; parameters?: object; sourceInfo?: { source?: string } }>;
|
|
269
314
|
}).getAllTools.bind(pi);
|
|
@@ -350,6 +395,9 @@ interface PendingConfig {
|
|
|
350
395
|
skipPermissions?: boolean;
|
|
351
396
|
defaultModel?: string;
|
|
352
397
|
defaultThinking?: ThinkingTier;
|
|
398
|
+
bridgeTools?: BridgeTools;
|
|
399
|
+
digest?: boolean;
|
|
400
|
+
systemPrompt?: boolean;
|
|
353
401
|
}
|
|
354
402
|
|
|
355
403
|
function statusText(ctx: AgyCommandCtx): string {
|
|
@@ -370,7 +418,7 @@ function statusText(ctx: AgyCommandCtx): string {
|
|
|
370
418
|
` digest: ${config.digest ? "on" : "off"}`,
|
|
371
419
|
` system prompt: ${config.systemPrompt ? "on" : "off"}`,
|
|
372
420
|
"",
|
|
373
|
-
"Subcommands: /agy engine stream-json|acp, /agy mode plan|accept-edits, /agy permissions on|off, /agy
|
|
421
|
+
"Subcommands: /agy engine stream-json|acp, /agy mode plan|accept-edits, /agy permissions on|off, /agy bridge all|mcp|none, /agy model <alias>, /agy thinking low|medium|high, /agy digest on|off, /agy system-prompt on|off, /agy acp-bin <path|auto>, /agy acp-auth, /agy patch-cleanup, /agy clear",
|
|
374
422
|
].join("\n");
|
|
375
423
|
}
|
|
376
424
|
|
|
@@ -378,7 +426,7 @@ function statusText(ctx: AgyCommandCtx): string {
|
|
|
378
426
|
function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
379
427
|
pi.registerCommand("agy", {
|
|
380
428
|
description:
|
|
381
|
-
"Antigravity provider: status, doctor,
|
|
429
|
+
"Antigravity provider: status, doctor, settings picker, clear sessions. Usage: /agy [status|doctor|engine stream-json|acp|mode plan|accept-edits|permissions on|off|bridge all|mcp|none|model <alias>|thinking low|medium|high|digest on|off|system-prompt on|off|acp-bin <path|auto>|acp-auth|patch-cleanup|clear]",
|
|
382
430
|
handler: async (args, cmdCtx: ExtensionCommandContext) => {
|
|
383
431
|
const ui = cmdCtx.ui;
|
|
384
432
|
const mode = cmdCtx.mode;
|
|
@@ -413,9 +461,32 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
413
461
|
}
|
|
414
462
|
if (sub === "engine") {
|
|
415
463
|
if (val === "acp" || val === "stream-json") {
|
|
464
|
+
if (val === "acp" && loadConfig().mode === "plan") {
|
|
465
|
+
ui?.notify("mode is plan; the ACP engine has no plan mode. /agy mode accept-edits first.", "warning");
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
416
468
|
const next = saveConfig({ engine: val });
|
|
469
|
+
if (next.engine !== "acp") {
|
|
470
|
+
ui?.notify("engine set to stream-json. Takes effect on the next pi start (or /reload).", "info");
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
// Self-service setup: install the server from the official
|
|
474
|
+
// registry and bootstrap auth now, so the restart just works.
|
|
475
|
+
// Manual instructions only when a step fails.
|
|
476
|
+
ui?.notify("engine set to acp. Preparing the server (binary + auth)…", "info");
|
|
477
|
+
const status = await ensureAcpReady({
|
|
478
|
+
configBin: loadConfig().acp.bin,
|
|
479
|
+
onProgress: (m) => ui?.notify(m, "info"),
|
|
480
|
+
});
|
|
481
|
+
if (!status.ok) {
|
|
482
|
+
ui?.notify(`ACP auto-setup failed (${status.error}).\n${status.manual}`, "warning");
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
saveConfig({ acp: { bin: status.bin, permissions: loadConfig().acp.permissions } });
|
|
417
486
|
ui?.notify(
|
|
418
|
-
|
|
487
|
+
status.needsLogin
|
|
488
|
+
? "ACP engine ready. Your first ACP message opens the Google login in your browser: sign in with the account of your Antigravity subscription (the same login as the Antigravity CLI, agy). One-time; tokens stay on your machine and this extension never sees them. Takes effect on the next pi start (or /reload)."
|
|
489
|
+
: `ACP engine ready (auth: ${status.auth}). Takes effect on the next pi start (or /reload).`,
|
|
419
490
|
"info",
|
|
420
491
|
);
|
|
421
492
|
} else {
|
|
@@ -423,20 +494,47 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
423
494
|
}
|
|
424
495
|
return;
|
|
425
496
|
}
|
|
497
|
+
if (sub === "acp-bin") {
|
|
498
|
+
const rest = (args ?? "").trim().split(/\s+/).slice(1).join(" ");
|
|
499
|
+
if (rest.length > 0) {
|
|
500
|
+
// Only the keyword compares case-insensitively; the path keeps its case.
|
|
501
|
+
const bin = rest.toLowerCase() === "auto" ? "" : rest.replace(/^~(?=\/|$)/, os.homedir());
|
|
502
|
+
saveConfig({ acp: { bin, permissions: loadConfig().acp.permissions } });
|
|
503
|
+
ui?.notify(
|
|
504
|
+
bin
|
|
505
|
+
? `acp.bin set to ${bin}. The next ACP turn (re)connects with it.`
|
|
506
|
+
: "acp.bin cleared. Auto-setup (or AGY_ACP_BIN) picks the binary on the next ACP turn.",
|
|
507
|
+
"info",
|
|
508
|
+
);
|
|
509
|
+
} else {
|
|
510
|
+
const cur = loadConfig().acp.bin;
|
|
511
|
+
ui?.notify(`acp.bin: ${cur || "(auto: setup installs, or AGY_ACP_BIN)"}\nusage: /agy acp-bin <path|auto>`, "info");
|
|
512
|
+
}
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
426
515
|
if (sub === "acp-auth") {
|
|
427
516
|
ui?.notify(
|
|
428
517
|
[
|
|
429
|
-
"ACP engine authentication (one-time
|
|
430
|
-
"
|
|
431
|
-
"
|
|
432
|
-
"
|
|
433
|
-
|
|
434
|
-
"
|
|
435
|
-
"
|
|
436
|
-
"
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
"
|
|
518
|
+
"ACP engine authentication (one-time; usually automatic -",
|
|
519
|
+
"/agy engine acp and session start set this up for you):",
|
|
520
|
+
"",
|
|
521
|
+
"The server is Google's official Antigravity ACP, installed from Google's",
|
|
522
|
+
"own registry. Logging in uses your Antigravity subscription: the same",
|
|
523
|
+
"Google account and plan as the Antigravity CLI (agy). It is no different",
|
|
524
|
+
"from logging into the CLI; the server just keeps its own token file on",
|
|
525
|
+
"your machine, like any Google tool. This extension never sees your",
|
|
526
|
+
"credentials.",
|
|
527
|
+
"",
|
|
528
|
+
"1. Server binary: auto-setup installs it. Manual: agy_acp_server.par from",
|
|
529
|
+
" the antigravity-acp registry; point acp.bin or AGY_ACP_BIN at it.",
|
|
530
|
+
'2. Default: put {"auth":{"type":"oauth-personal"}} in',
|
|
531
|
+
" ~/.gemini/antigravity-acp/settings.json, run one turn, and complete the",
|
|
532
|
+
" Google login that opens in your browser (headless: tunnel 127.0.0.1:<port>",
|
|
533
|
+
" over ssh, then open the URL on your machine).",
|
|
534
|
+
' Headless alternative: GEMINI_API_KEY + {"auth":{"type":"gemini-api-key"}}',
|
|
535
|
+
" (metered paid API - not your Antigravity plan). The key is used only",
|
|
536
|
+
" when that type is selected; with the default oauth-personal in place,",
|
|
537
|
+
" an exported key is ignored.",
|
|
440
538
|
"3. Run one turn; /agy doctor shows the server version when auth is OK.",
|
|
441
539
|
].join("\n"),
|
|
442
540
|
"info",
|
|
@@ -469,11 +567,22 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
469
567
|
lines.push(" lifecycle (last 5):");
|
|
470
568
|
for (const entry of snap.lifecycle.slice(-5)) lines.push(` ${entry}`);
|
|
471
569
|
}
|
|
570
|
+
if (engine === "acp") {
|
|
571
|
+
const setup = inspectAcpSetup({ configBin: config.acp.bin });
|
|
572
|
+
lines.push(
|
|
573
|
+
` acp binary: ${setup.bin ?? "not found (auto-setup offers install)"}${setup.source ? ` (${setup.source})` : ""}`,
|
|
574
|
+
` acp auth: ${setup.auth ?? "not configured (auto-setup bootstraps)"}`,
|
|
575
|
+
);
|
|
576
|
+
}
|
|
472
577
|
ui?.notify(lines.join("\n"), "info");
|
|
473
578
|
return;
|
|
474
579
|
}
|
|
475
580
|
if (sub === "mode") {
|
|
476
581
|
if (val === "plan" || val === "accept-edits") {
|
|
582
|
+
if (val === "plan" && ctx.engine === "acp") {
|
|
583
|
+
ui?.notify("the ACP engine has no plan mode (RC01). /agy engine stream-json first, or /agy mode accept-edits.", "warning");
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
477
586
|
const next = saveConfig({ mode: val as AgyMode });
|
|
478
587
|
ui?.notify(`mode set to ${next.mode}`, "info");
|
|
479
588
|
} else {
|
|
@@ -491,6 +600,20 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
491
600
|
}
|
|
492
601
|
return;
|
|
493
602
|
}
|
|
603
|
+
if (sub === "bridge") {
|
|
604
|
+
if (val === "all" || val === "mcp" || val === "none") {
|
|
605
|
+
const next = saveConfig({ bridgeTools: val });
|
|
606
|
+
ui?.notify(
|
|
607
|
+
next.bridgeTools === "none"
|
|
608
|
+
? "bridge off. The MCP tool bridge will not start on the next pi start (or /reload)."
|
|
609
|
+
: `bridge tools set to ${next.bridgeTools}. The catalog rebuilds on the next pi start (or /reload).`,
|
|
610
|
+
"info",
|
|
611
|
+
);
|
|
612
|
+
} else {
|
|
613
|
+
ui?.notify(`bridge: ${loadConfig().bridgeTools}\nusage: /agy bridge all|mcp|none\n all: every non-builtin pi tool (default). mcp: pi-mcp-adapter tools + skills only. none: bridge off.`, "info");
|
|
614
|
+
}
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
494
617
|
if (sub === "model") {
|
|
495
618
|
if (val && val.length > 0) {
|
|
496
619
|
const next = saveConfig({ defaultModel: val });
|
|
@@ -556,7 +679,10 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
556
679
|
});
|
|
557
680
|
}
|
|
558
681
|
|
|
559
|
-
/** Interactive settings picker (TUI only). Rows:
|
|
682
|
+
/** Interactive settings picker (TUI only). Rows: the full runtime config
|
|
683
|
+
* surface (mode, permissions, model, thinking, bridge, digest, system
|
|
684
|
+
* prompt). Engine stays a subcommand: switching runs install + auth setup
|
|
685
|
+
* and needs a restart. */
|
|
560
686
|
async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promise<void> {
|
|
561
687
|
const config = loadConfig();
|
|
562
688
|
const pending: PendingConfig = {};
|
|
@@ -594,6 +720,30 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
|
|
|
594
720
|
currentValue: config.defaultThinking,
|
|
595
721
|
values: ["low", "medium", "high"],
|
|
596
722
|
},
|
|
723
|
+
{
|
|
724
|
+
id: "bridge",
|
|
725
|
+
label: "Bridge tools",
|
|
726
|
+
description:
|
|
727
|
+
"Which pi tools the MCP bridge exposes to agy. all: every non-builtin tool (default). mcp: pi-mcp-adapter tools + skills. none: bridge off.",
|
|
728
|
+
currentValue: config.bridgeTools,
|
|
729
|
+
values: ["all", "mcp", "none"],
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
id: "digest",
|
|
733
|
+
label: "Context digest",
|
|
734
|
+
description:
|
|
735
|
+
"Inject a delta of pi-side context into each agy prompt. Defeats agy's prompt cache (~25-30k tokens re-billed per turn).",
|
|
736
|
+
currentValue: config.digest ? "on" : "off",
|
|
737
|
+
values: ["on", "off"],
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
id: "system-prompt",
|
|
741
|
+
label: "System prompt",
|
|
742
|
+
description:
|
|
743
|
+
"Prepend pi's system prompt (incl. AGENTS.md files) to the first prompt of each new agy conversation.",
|
|
744
|
+
currentValue: config.systemPrompt ? "on" : "off",
|
|
745
|
+
values: ["on", "off"],
|
|
746
|
+
},
|
|
597
747
|
];
|
|
598
748
|
|
|
599
749
|
await ui.custom((tui, theme, _kb, done) => {
|
|
@@ -614,6 +764,12 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
|
|
|
614
764
|
pending.defaultModel = newValue;
|
|
615
765
|
} else if (id === "thinking") {
|
|
616
766
|
pending.defaultThinking = newValue as ThinkingTier;
|
|
767
|
+
} else if (id === "bridge") {
|
|
768
|
+
pending.bridgeTools = newValue as BridgeTools;
|
|
769
|
+
} else if (id === "digest") {
|
|
770
|
+
pending.digest = newValue === "on";
|
|
771
|
+
} else if (id === "system-prompt") {
|
|
772
|
+
pending.systemPrompt = newValue === "on";
|
|
617
773
|
}
|
|
618
774
|
},
|
|
619
775
|
() => done(undefined),
|
|
@@ -630,13 +786,12 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
|
|
|
630
786
|
};
|
|
631
787
|
});
|
|
632
788
|
|
|
633
|
-
if (
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
pending.defaultThinking === undefined
|
|
638
|
-
)
|
|
789
|
+
if (Object.keys(pending).length === 0) return;
|
|
790
|
+
|
|
791
|
+
if (pending.mode === "plan" && ctx.engine === "acp") {
|
|
792
|
+
ui.notify("the ACP engine has no plan mode (RC01). Switch /agy engine stream-json first.", "warning");
|
|
639
793
|
return;
|
|
794
|
+
}
|
|
640
795
|
|
|
641
796
|
try {
|
|
642
797
|
const next = saveConfig(pending);
|
|
@@ -647,6 +802,9 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
|
|
|
647
802
|
: null,
|
|
648
803
|
pending.defaultModel !== undefined ? `tool model=${next.defaultModel}` : null,
|
|
649
804
|
pending.defaultThinking !== undefined ? `tool thinking=${next.defaultThinking}` : null,
|
|
805
|
+
pending.bridgeTools !== undefined ? `bridge=${next.bridgeTools}` : null,
|
|
806
|
+
pending.digest !== undefined ? `digest=${next.digest ? "on" : "off"}` : null,
|
|
807
|
+
pending.systemPrompt !== undefined ? `system-prompt=${next.systemPrompt ? "on" : "off"}` : null,
|
|
650
808
|
]
|
|
651
809
|
.filter(Boolean)
|
|
652
810
|
.join(", ");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "Gemini provider for Pi on the Antigravity ACP server (official Google ACP) or the stream-json agy CLI. antigravity/* models in Pi's /model picker, no-patch MCP bridge: agy runs Pi's tools. ToS safe to use.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -59,9 +59,10 @@
|
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@earendil-works/pi-ai": "^0.
|
|
63
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
64
|
-
"@earendil-works/pi-
|
|
62
|
+
"@earendil-works/pi-ai": "^0.85.0",
|
|
63
|
+
"@earendil-works/pi-coding-agent": "^0.85.0",
|
|
64
|
+
"@earendil-works/pi-server": "^0.85.0",
|
|
65
|
+
"@earendil-works/pi-tui": "^0.85.0",
|
|
65
66
|
"@types/node": "^22.0.0",
|
|
66
67
|
"tsx": "^4.19.0",
|
|
67
68
|
"typebox": "^1.1.38",
|
package/src/acp/connection.ts
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
// the effort tier baked in, e.g. gemini-3.8-flash-low)
|
|
12
12
|
// - session/cancel returns -32601 on RC01 (not implemented) — the driver
|
|
13
13
|
// treats that as "cancel unsupported" and falls back to teardown+kill
|
|
14
|
-
// - session/request_permission is answered in-connection:
|
|
15
|
-
//
|
|
14
|
+
// - session/request_permission is answered in-connection: policy per turn
|
|
15
|
+
// (skipPermissions on -> first allow option; off -> first reject option,
|
|
16
|
+
// fail-closed)
|
|
16
17
|
// - session/load replays history as notifications BEFORE its response; the
|
|
17
18
|
// driver suppresses updates while the load is in flight
|
|
18
19
|
// - mcpServers entries: {name, type:"http", url, headers:[]} — headers is a
|
|
@@ -58,6 +59,10 @@ export interface AcpConnectionOptions {
|
|
|
58
59
|
/** mcpServers entries for session/new AND session/load (run 6: load takes
|
|
59
60
|
* the same param). Evaluated lazily per call. */
|
|
60
61
|
mcpServers?: () => AcpMcpServer[];
|
|
62
|
+
/** Permission policy for session/request_permission, evaluated per request:
|
|
63
|
+
* "auto" selects the first allow option; "deny" fail-closes to the first
|
|
64
|
+
* reject option. Absent = deny (fail closed). */
|
|
65
|
+
permissions?: () => "auto" | "deny";
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
const INIT_TIMEOUT_MS = 30_000;
|
|
@@ -114,6 +119,13 @@ export class AcpConnection {
|
|
|
114
119
|
this.#child = child;
|
|
115
120
|
child.stdout?.setEncoding("utf8");
|
|
116
121
|
child.stderr?.setEncoding("utf8");
|
|
122
|
+
// Pipe failures arrive asynchronously as stream 'error' events; the sync
|
|
123
|
+
// try/catch in #write cannot see them. Without this listener an EPIPE
|
|
124
|
+
// (server died mid-handshake) is uncaught and kills pi.
|
|
125
|
+
child.stdin?.on("error", (err) => {
|
|
126
|
+
this.#opts.log("stdin-error", { message: err.message });
|
|
127
|
+
if (!this.#exited && !this.#killed) this.#finish(err.message);
|
|
128
|
+
});
|
|
117
129
|
|
|
118
130
|
const rpc = new JsonRpcSession({
|
|
119
131
|
send: (frame) => this.#write(frame),
|
|
@@ -253,9 +265,10 @@ export class AcpConnection {
|
|
|
253
265
|
await this.guarded("session/close", { sessionId }, SESSION_OP_TIMEOUT_MS);
|
|
254
266
|
}
|
|
255
267
|
|
|
256
|
-
/** Protocol-level
|
|
257
|
-
* first allow option
|
|
258
|
-
* the
|
|
268
|
+
/** Protocol-level permission answering: policy from the driver ("auto"
|
|
269
|
+
* selects the first allow option; "deny" fail-closes to the first reject
|
|
270
|
+
* option). Never hangs: options are data from the server and the answer
|
|
271
|
+
* is computed synchronously. */
|
|
259
272
|
#onServerRequest(method: string, params: unknown): Promise<unknown> {
|
|
260
273
|
if (method === "session/request_permission") {
|
|
261
274
|
const options = (
|
|
@@ -263,9 +276,10 @@ export class AcpConnection {
|
|
|
263
276
|
) as Array<{ optionId?: string; kind?: string }> | undefined;
|
|
264
277
|
const allow = options?.find((o) => typeof o.kind === "string" && o.kind.startsWith("allow"));
|
|
265
278
|
const deny = options?.find((o) => typeof o.kind === "string" && o.kind.startsWith("reject"));
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
279
|
+
const policy = this.#opts.permissions?.() ?? "deny";
|
|
280
|
+
const chosen = policy === "auto" ? (allow ?? deny ?? options?.[0]) : (deny ?? options?.[0]);
|
|
281
|
+
this.#opts.log("permission", { optionId: chosen?.optionId, policy });
|
|
282
|
+
return Promise.resolve({ outcome: { outcome: "selected", optionId: chosen?.optionId } });
|
|
269
283
|
}
|
|
270
284
|
// fs/* and terminal/* are declined: our client capabilities are off and
|
|
271
285
|
// agy keeps executing its own tools (plan §8 capability posture).
|
package/src/acp/driver.ts
CHANGED
|
@@ -34,8 +34,10 @@ import type {
|
|
|
34
34
|
const LIFECYCLE_LIMIT = 24;
|
|
35
35
|
|
|
36
36
|
export interface AcpDriverOptions {
|
|
37
|
-
/** Config acp.bin value (may be empty). Env AGY_ACP_BIN wins.
|
|
38
|
-
|
|
37
|
+
/** Config acp.bin value (may be empty). Env AGY_ACP_BIN wins. A function
|
|
38
|
+
* is resolved per connection: setup can install the binary and update
|
|
39
|
+
* config mid-session, and the next turn picks it up without a restart. */
|
|
40
|
+
bin: string | (() => string);
|
|
39
41
|
/** Extra argv for the binary (tests: node + fake-server script). */
|
|
40
42
|
binArgs?: string[];
|
|
41
43
|
extraEnv?: Record<string, string>;
|
|
@@ -88,11 +90,11 @@ export function acpModelSlug(model: string, effort?: string): string {
|
|
|
88
90
|
return `${model}-${effort}`;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
/** Map our config knobs onto ACP session modes.
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
93
|
+
/** Map our config knobs onto ACP session modes. skipPermissions=false also
|
|
94
|
+
* fail-closes the in-connection permission handler (reject options), so the
|
|
95
|
+
* modes keep their server-side meaning. Known gap: the CLI's `--mode plan`
|
|
96
|
+
* has no ACP equivalent (review 4, finding 4) — plan + acp is refused at the
|
|
97
|
+
* command level and fails the turn visibly. */
|
|
96
98
|
export function acpMode(mode: string, skipPermissions: boolean): string {
|
|
97
99
|
if (skipPermissions) return "yolo";
|
|
98
100
|
return mode === "plan" ? "default" : "auto_edit";
|
|
@@ -122,7 +124,7 @@ export class AcpDriver implements TurnDriver {
|
|
|
122
124
|
|
|
123
125
|
constructor(opts: AcpDriverOptions) {
|
|
124
126
|
this.#opts = opts;
|
|
125
|
-
this.#log("driver-created", { bin: resolveAcpBinary(opts.bin) });
|
|
127
|
+
this.#log("driver-created", { bin: typeof opts.bin === "function" ? "(resolved per turn)" : resolveAcpBinary(opts.bin) });
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
get state(): DriverState {
|
|
@@ -417,17 +419,19 @@ export class AcpDriver implements TurnDriver {
|
|
|
417
419
|
this.#state = "starting";
|
|
418
420
|
this.#stats.spawns += 1;
|
|
419
421
|
const conn = new AcpConnection({
|
|
420
|
-
bin: resolveAcpBinary(this.#opts.bin),
|
|
422
|
+
bin: resolveAcpBinary(typeof this.#opts.bin === "function" ? this.#opts.bin() : this.#opts.bin),
|
|
421
423
|
binArgs: this.#opts.binArgs,
|
|
422
424
|
extraEnv: this.#opts.extraEnv,
|
|
423
425
|
cwd: request.cwd,
|
|
424
426
|
mcpServers: this.#opts.mcpServers,
|
|
425
427
|
log: (msg, data) => this.#log(msg, data),
|
|
428
|
+
// Fail-closed permissions: only turns with skipPermissions answer allow.
|
|
429
|
+
permissions: () => (this.#active?.request.skipPermissions ? "auto" : "deny"),
|
|
426
430
|
onUpdate: (sessionId, update) => this.#onConnectionUpdate(sessionId, update),
|
|
427
431
|
onExit: (info) => this.#onConnectionExit(conn, info),
|
|
428
432
|
});
|
|
429
433
|
this.#conn = conn;
|
|
430
|
-
this.#log("spawn", { bin: resolveAcpBinary(this.#opts.bin) });
|
|
434
|
+
this.#log("spawn", { bin: resolveAcpBinary(typeof this.#opts.bin === "function" ? this.#opts.bin() : this.#opts.bin) });
|
|
431
435
|
return conn
|
|
432
436
|
.start()
|
|
433
437
|
.then(() => {
|