@estebanforge/pi-antigravity-bridge 1.2.6 → 1.3.1
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 +35 -0
- package/README.md +40 -32
- package/docs/ANTIGRAVITY-INTEGRATIONS.md +2 -0
- package/docs/ARCHITECTURE.md +58 -15
- package/docs/DEVELOPMENT.md +17 -8
- package/docs/PI-BRIDGE-GAPS.md +65 -82
- package/extensions/index.ts +199 -139
- package/package.json +2 -2
- package/src/config.ts +55 -5
- package/src/driver.ts +644 -0
- package/src/mcp-server.ts +28 -48
- package/src/native-tools.ts +104 -0
- package/src/{patcher.ts → patch-cleanup.ts} +25 -197
- package/src/provider.ts +447 -24
- package/src/skills.ts +116 -0
- package/src/stream-events.ts +123 -0
- package/docs/PI-INVOKETOOL-PATCH.md +0 -254
package/extensions/index.ts
CHANGED
|
@@ -36,11 +36,23 @@ import {
|
|
|
36
36
|
type AgyModelEntry,
|
|
37
37
|
} from "../src/models.js";
|
|
38
38
|
import { SessionStore } from "../src/sessions.js";
|
|
39
|
-
import { createStreamSimple } from "../src/provider.js";
|
|
40
|
-
import {
|
|
39
|
+
import { ToolRoundTrips, WrapperReplay, createStreamSimple } from "../src/provider.js";
|
|
40
|
+
import { AgyDriver } from "../src/driver.js";
|
|
41
|
+
import { CONFIG_PATH, loadConfig, saveConfig, type AgyMode, type BridgeTools, type ThinkingTier } from "../src/config.js";
|
|
41
42
|
import { registerAskAntigravityTool, toolModelsFromRaw } from "../src/ask-tool.js";
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
43
|
+
import { startMcpServer, type McpServerHandle } from "../src/mcp-server.js";
|
|
44
|
+
import {
|
|
45
|
+
ACTIVATE_SKILL_TOOL_NAME,
|
|
46
|
+
activateSkillSchema,
|
|
47
|
+
catalogSummary,
|
|
48
|
+
findSkillByName,
|
|
49
|
+
readSkillBody,
|
|
50
|
+
scanSkills,
|
|
51
|
+
type SkillLite,
|
|
52
|
+
} from "../src/skills.js";
|
|
53
|
+
import { mapAgyToolToNative } from "../src/native-tools.js";
|
|
54
|
+
import { Type } from "typebox";
|
|
55
|
+
import { patchStatus, restorePatch } from "../src/patch-cleanup.js";
|
|
44
56
|
|
|
45
57
|
function resolveAgyBinary(): string {
|
|
46
58
|
return process.env.AGY_BIN || "agy";
|
|
@@ -71,7 +83,26 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
71
83
|
const models = entries.map(toPiModel);
|
|
72
84
|
|
|
73
85
|
const store = new SessionStore();
|
|
74
|
-
|
|
86
|
+
// Persistent stream-json engine + the no-patch pi-tool round-trip store.
|
|
87
|
+
// The MCP bridge parks calls here; the provider emits them as real pi
|
|
88
|
+
// toolUse turns and completes them from the next call's toolResult.
|
|
89
|
+
const driver = new AgyDriver();
|
|
90
|
+
const roundTrips = new ToolRoundTrips(driver);
|
|
91
|
+
const replay = new WrapperReplay();
|
|
92
|
+
// Native re-exec only emits for builtins actually active in the session;
|
|
93
|
+
// anything else (or an unknown name) falls back to the wrapper card.
|
|
94
|
+
const nativeActive = (name: string): boolean => {
|
|
95
|
+
try {
|
|
96
|
+
const getAll = (pi as unknown as { getAllTools: () => Array<{ name: string }> }).getAllTools.bind(pi);
|
|
97
|
+
return getAll().some((t) => t.name === name);
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
// A settled turn cannot answer its parked calls; the driver never sees
|
|
103
|
+
// ToolRoundTrips, so the provider bridges the two here.
|
|
104
|
+
driver.onTurnEnd = () => roundTrips.failAll("antigravity turn ended with an unresolved pi tool call");
|
|
105
|
+
const streamSimple = createStreamSimple({ entries, store, driver, roundTrips, replay, nativeActive });
|
|
75
106
|
|
|
76
107
|
pi.registerProvider("antigravity", {
|
|
77
108
|
name: "Antigravity (agy)",
|
|
@@ -92,7 +123,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
92
123
|
streamSimple,
|
|
93
124
|
});
|
|
94
125
|
|
|
95
|
-
registerAgyCommand(pi, { entries, store, usingFallback });
|
|
126
|
+
registerAgyCommand(pi, { entries, store, usingFallback, driver, getMcpPort: () => mcpHandle?.port ?? null });
|
|
96
127
|
|
|
97
128
|
// AskAntigravity tool: one-shot delegation to agy (ported from
|
|
98
129
|
// pi-ask-antigravity). When both extensions are installed, the bridge wins
|
|
@@ -100,13 +131,48 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
100
131
|
// detects this package via import.meta.resolve).
|
|
101
132
|
await registerAskAntigravityTool(pi, toolModels);
|
|
102
133
|
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
134
|
+
// Display-only wrapper tool: the provider emits mutating agy steps as
|
|
135
|
+
// toolCalls against it (never re-executed - execute() replays the output
|
|
136
|
+
// agy already recorded). Empty description on purpose: no model should
|
|
137
|
+
// call it, it exists so pi renders proper toolCall/toolResult cards.
|
|
138
|
+
pi.registerTool({
|
|
139
|
+
name: "antigravity",
|
|
140
|
+
label: "Antigravity",
|
|
141
|
+
description: "",
|
|
142
|
+
parameters: Type.Object({
|
|
143
|
+
tool: Type.String({ description: "agy tool name that produced this step." }),
|
|
144
|
+
key: Type.String({ description: "Internal replay key. Do not fabricate." }),
|
|
145
|
+
}),
|
|
146
|
+
execute: async (_toolCallId, params) => {
|
|
147
|
+
const key = (params as { key?: string }).key ?? "";
|
|
148
|
+
const output = replay.take(key) ?? `(no recorded output for ${key})`;
|
|
149
|
+
return { content: [{ type: "text", text: output }], details: { replay: true } };
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// MCP tool bridge: expose pi's tools to agy over localhost Streamable HTTP.
|
|
154
|
+
// Calls park in the provider's round-trip store and complete through pi's
|
|
155
|
+
// normal toolUse loop (native cards, permissions, hooks) - no patch, no
|
|
156
|
+
// privileged API. Started on session_start, torn down on session_shutdown.
|
|
108
157
|
let mcpHandle: McpServerHandle | null = null;
|
|
109
158
|
pi.on("session_start", async (_event, ctx) => {
|
|
159
|
+
// Legacy cleanup: users who ran the old consent-gated patcher still
|
|
160
|
+
// carry pi.invokeTool in their installed pi. Inert, but tell them once
|
|
161
|
+
// and offer /agy patch-cleanup. Never auto-edits the install.
|
|
162
|
+
try {
|
|
163
|
+
if (!loadConfig().patchCleanupNotified && patchStatus().present) {
|
|
164
|
+
// Flag after surfacing, not before: headless sessions log to
|
|
165
|
+
// stderr (ctx.ui.notify is a no-op without a UI), so the notice
|
|
166
|
+
// is never silently dropped.
|
|
167
|
+
const msg =
|
|
168
|
+
"Your pi install still carries the old pi.invokeTool patch. It is unused and harmless; a pi update also removes it. To restore the original files from the backup now: /agy patch-cleanup";
|
|
169
|
+
if (ctx.hasUI) ctx.ui.notify(msg, "info");
|
|
170
|
+
else console.error(`[antigravity-bridge] ${msg}`);
|
|
171
|
+
saveConfig({ patchCleanupNotified: true });
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
/* detection is best-effort */
|
|
175
|
+
}
|
|
110
176
|
// Bridge lifecycle/error logger. Routes through ctx.ui.notify (an
|
|
111
177
|
// ephemeral toast that fades) instead of stderr: pi's TUI captures stderr
|
|
112
178
|
// and pins it above the input for the whole session, which left the
|
|
@@ -131,79 +197,65 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
131
197
|
console.error(msg);
|
|
132
198
|
}
|
|
133
199
|
};
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
case "silent": {
|
|
159
|
-
// Previously declined. Stay quiet; bridge stays off until the user
|
|
160
|
-
// runs /agy patch apply or the patch becomes live.
|
|
161
|
-
mcpLog("patch-declined");
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
case "ask": {
|
|
165
|
-
const apply = await ctx.ui.confirm(
|
|
166
|
-
"Apply the pi.invokeTool patch?",
|
|
167
|
-
"Enables the MCP tool bridge. Edits one method into your installed @earendil-works/pi-coding-agent/dist/ (reversible via /agy patch restore), and takes effect after a full pi restart.",
|
|
168
|
-
// Bound the wait so a non-confirm-capable RPC client (hasUI is always
|
|
169
|
-
// true in RPC) can't hang session_start. Timeout resolves like "no"
|
|
170
|
-
// (declined persists); reversible via /agy patch apply.
|
|
171
|
-
{ timeout: 60_000 },
|
|
172
|
-
);
|
|
173
|
-
if (apply) {
|
|
174
|
-
const res = applyInvokeToolPatch({ log: mcpLog });
|
|
175
|
-
if (res.patched) {
|
|
176
|
-
saveConfig({ invokeToolPatchDeclined: false });
|
|
177
|
-
ctx.ui.notify(
|
|
178
|
-
`Applied the pi.invokeTool patch to ${res.root} (pi ${res.version}). Fully RESTART pi (quit + relaunch) to start the MCP tool bridge.`,
|
|
179
|
-
"warning",
|
|
180
|
-
);
|
|
181
|
-
} else if (res.errors.length > 0) {
|
|
182
|
-
ctx.ui.notify(`pi.invokeTool patch failed: ${res.errors[0]}`, "error");
|
|
183
|
-
}
|
|
184
|
-
} else {
|
|
185
|
-
saveConfig({ invokeToolPatchDeclined: true });
|
|
186
|
-
ctx.ui.notify(
|
|
187
|
-
"Skipped. The MCP tool bridge stays off. To enable it later, run /agy patch apply. Then restart pi.",
|
|
188
|
-
"info",
|
|
189
|
-
);
|
|
200
|
+
// Start the bridge unless the user turned it off. No patch gate, no
|
|
201
|
+
// consent flow: calls route through pi's normal toolUse loop.
|
|
202
|
+
const bridgeMode: BridgeTools = loadConfig().bridgeTools;
|
|
203
|
+
if (bridgeMode === "none") return; // user opted out
|
|
204
|
+
if (mcpHandle) return; // already running (reload re-fires session_start)
|
|
205
|
+
const SKIP = new Set(["AskAntigravity"]);
|
|
206
|
+
const skills: SkillLite[] = scanSkills(process.cwd());
|
|
207
|
+
const getAll = (pi as unknown as {
|
|
208
|
+
getAllTools: () => Array<{ name: string; description?: string; parameters?: object; sourceInfo?: { source?: string } }>;
|
|
209
|
+
}).getAllTools.bind(pi);
|
|
210
|
+
const listTools = () => {
|
|
211
|
+
const all = getAll();
|
|
212
|
+
const filtered =
|
|
213
|
+
bridgeMode === "mcp"
|
|
214
|
+
? all.filter((t) => /pi-mcp-adapter/.test(t.sourceInfo?.source ?? ""))
|
|
215
|
+
: all.filter((t) => t.sourceInfo?.source !== "builtin");
|
|
216
|
+
const tools = filtered
|
|
217
|
+
.filter((t) => !SKIP.has(t.name))
|
|
218
|
+
.map((t) => {
|
|
219
|
+
let inputSchema: object = { type: "object", properties: {}, additionalProperties: true };
|
|
220
|
+
try {
|
|
221
|
+
if (t.parameters) inputSchema = JSON.parse(JSON.stringify(t.parameters)) as object;
|
|
222
|
+
} catch {
|
|
223
|
+
/* keep default schema */
|
|
190
224
|
}
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
}
|
|
225
|
+
return { name: t.name, description: t.description ?? t.name, inputSchema };
|
|
226
|
+
});
|
|
227
|
+
if (skills.length > 0) {
|
|
228
|
+
tools.push({
|
|
229
|
+
name: ACTIVATE_SKILL_TOOL_NAME,
|
|
230
|
+
description: `Activate a pi Agent Skill by name. Catalog:\n${catalogSummary(skills)}`,
|
|
231
|
+
inputSchema: activateSkillSchema(skills) as object,
|
|
232
|
+
});
|
|
199
233
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
234
|
+
return tools;
|
|
235
|
+
};
|
|
236
|
+
// activate_skill never round-trips through pi: the bridge answers it
|
|
237
|
+
// directly by reading the SKILL.md (pi has no skill tool to execute).
|
|
238
|
+
const bridgeOnToolCall = (
|
|
239
|
+
callId: string,
|
|
240
|
+
name: string,
|
|
241
|
+
args: Record<string, unknown>,
|
|
242
|
+
signal: AbortSignal,
|
|
243
|
+
) => {
|
|
244
|
+
if (name !== ACTIVATE_SKILL_TOOL_NAME) return roundTrips.onToolCall(callId, name, args, signal);
|
|
245
|
+
const wanted = typeof args.name === "string" ? args.name : "";
|
|
246
|
+
const skill = findSkillByName(skills, wanted);
|
|
247
|
+
const body = skill ? readSkillBody(skill) : `unknown skill: ${wanted || "(none given)"}`;
|
|
248
|
+
return Promise.resolve({
|
|
249
|
+
content: [
|
|
250
|
+
{
|
|
251
|
+
type: "text",
|
|
252
|
+
text: skill ? `${body}\n\n[skill resources dir: ${skill.dir}]` : `Error: ${body}`,
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
isError: !skill,
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
const r = await startMcpServer({ listTools, onToolCall: bridgeOnToolCall }, { log: mcpLog });
|
|
207
259
|
if (r.ok && r.handle) {
|
|
208
260
|
mcpHandle = r.handle;
|
|
209
261
|
} else {
|
|
@@ -214,6 +266,8 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
214
266
|
const h = mcpHandle;
|
|
215
267
|
mcpHandle = null;
|
|
216
268
|
await h?.close();
|
|
269
|
+
roundTrips.failAll("antigravity session shut down");
|
|
270
|
+
await driver.close("shutdown");
|
|
217
271
|
});
|
|
218
272
|
}
|
|
219
273
|
|
|
@@ -223,6 +277,8 @@ interface AgyCommandCtx {
|
|
|
223
277
|
entries: AgyModelEntry[];
|
|
224
278
|
store: SessionStore;
|
|
225
279
|
usingFallback: boolean;
|
|
280
|
+
driver: AgyDriver;
|
|
281
|
+
getMcpPort: () => number | null;
|
|
226
282
|
}
|
|
227
283
|
|
|
228
284
|
interface PendingConfig {
|
|
@@ -245,24 +301,19 @@ function statusText(ctx: AgyCommandCtx): string {
|
|
|
245
301
|
` tool thinking: ${config.defaultThinking}`,
|
|
246
302
|
` sessions: ${ctx.store.size} bound`,
|
|
247
303
|
` config: ${CONFIG_PATH}`,
|
|
248
|
-
`
|
|
304
|
+
` engine: ${config.engine}`,
|
|
305
|
+
` bridge tools: ${config.bridgeTools}`,
|
|
306
|
+
` digest: ${config.digest ? "on" : "off"}`,
|
|
249
307
|
"",
|
|
250
|
-
"Subcommands: /agy mode plan|accept-edits, /agy permissions on|off, /agy model flash|pro|gemini, /agy thinking low|medium|high, /agy
|
|
308
|
+
"Subcommands: /agy mode plan|accept-edits, /agy permissions on|off, /agy model flash|pro|gemini, /agy thinking low|medium|high, /agy clear",
|
|
251
309
|
].join("\n");
|
|
252
310
|
}
|
|
253
311
|
|
|
254
|
-
function patchStateLabel(): string {
|
|
255
|
-
const s = patchStatus();
|
|
256
|
-
if (s.present) return "patched";
|
|
257
|
-
if (!s.root) return "MISSING (pi root not found)";
|
|
258
|
-
if (loadConfig().invokeToolPatchDeclined) return `declined (pi ${s.version}). Resume: /agy patch apply`;
|
|
259
|
-
return `MISSING (pi ${s.version}). Apply it: /agy patch apply`;
|
|
260
|
-
}
|
|
261
312
|
|
|
262
313
|
function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
263
314
|
pi.registerCommand("agy", {
|
|
264
315
|
description:
|
|
265
|
-
"Antigravity provider: status, mode picker,
|
|
316
|
+
"Antigravity provider: status, doctor, mode picker, clear sessions. Usage: /agy [status|doctor|mode [plan|accept-edits]|digest on|off|patch-cleanup|clear]",
|
|
266
317
|
handler: async (args, cmdCtx: ExtensionCommandContext) => {
|
|
267
318
|
const ui = cmdCtx.ui;
|
|
268
319
|
const mode = cmdCtx.mode;
|
|
@@ -275,6 +326,47 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
275
326
|
ui?.notify("Cleared all antigravity session bindings.", "info");
|
|
276
327
|
return;
|
|
277
328
|
}
|
|
329
|
+
if (sub === "patch-cleanup") {
|
|
330
|
+
const st = patchStatus();
|
|
331
|
+
if (!st.present) {
|
|
332
|
+
ui?.notify(
|
|
333
|
+
st.root
|
|
334
|
+
? `No invokeTool patch detected on pi ${st.version}. Nothing to clean.`
|
|
335
|
+
: "Could not locate the installed pi package. Nothing cleaned.",
|
|
336
|
+
"info",
|
|
337
|
+
);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const r = restorePatch();
|
|
341
|
+
ui?.notify(
|
|
342
|
+
r.ok
|
|
343
|
+
? `Restored ${r.restoredFiles.length} file(s) from ${r.backupDir}. The running session is unaffected; the files on disk are clean again.`
|
|
344
|
+
: `patch-cleanup failed: ${r.reason}`,
|
|
345
|
+
r.ok ? "info" : "error",
|
|
346
|
+
);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (sub === "doctor") {
|
|
350
|
+
const config = loadConfig();
|
|
351
|
+
const snap = ctx.driver.snapshot();
|
|
352
|
+
const port = ctx.getMcpPort();
|
|
353
|
+
const lines = [
|
|
354
|
+
"Antigravity doctor (no tokens spent)",
|
|
355
|
+
` engine: ${config.engine}`,
|
|
356
|
+
` bridge: ${config.bridgeTools}${port ? ` (port ${port})` : " (not running)"}`,
|
|
357
|
+
` driver: ${snap.state}${snap.pid ? ` pid=${snap.pid}` : ""}${snap.conversationId ? ` conv=${snap.conversationId.slice(0, 8)}` : ""}`,
|
|
358
|
+
` driver stats: spawns=${snap.stats.spawns} turns=${snap.stats.turns} reused=${snap.stats.reused} recycles=${snap.stats.recycles}${snap.stats.lastRecycleReason ? ` (last: ${snap.stats.lastRecycleReason})` : ""}`,
|
|
359
|
+
` sessions: ${ctx.store.size} bound`,
|
|
360
|
+
` models: ${ctx.entries.length} ${ctx.usingFallback ? "FALLBACK (agy models failed)" : "discovered"}`,
|
|
361
|
+
` config: ${CONFIG_PATH}`,
|
|
362
|
+
];
|
|
363
|
+
if (snap.lifecycle.length > 0) {
|
|
364
|
+
lines.push(" lifecycle (last 5):");
|
|
365
|
+
for (const entry of snap.lifecycle.slice(-5)) lines.push(` ${entry}`);
|
|
366
|
+
}
|
|
367
|
+
ui?.notify(lines.join("\n"), "info");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
278
370
|
if (sub === "mode") {
|
|
279
371
|
if (val === "plan" || val === "accept-edits") {
|
|
280
372
|
const next = saveConfig({ mode: val as AgyMode });
|
|
@@ -303,6 +395,21 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
303
395
|
}
|
|
304
396
|
return;
|
|
305
397
|
}
|
|
398
|
+
if (sub === "digest") {
|
|
399
|
+
if (val === "on" || val === "off") {
|
|
400
|
+
const next = saveConfig({ digest: val === "on" });
|
|
401
|
+
ui?.notify(
|
|
402
|
+
next.digest
|
|
403
|
+
? "digest on. pi-side context (compaction summaries, other-provider turns) is injected into each agy prompt. Note: this defeats agy's prompt cache (~25-30k tokens re-billed per turn)."
|
|
404
|
+
: "digest off. agy prompts contain only your message; agy's prompt cache stays stable. Enable when mixing providers in one session and agy must see pi-side context.",
|
|
405
|
+
"info",
|
|
406
|
+
);
|
|
407
|
+
} else {
|
|
408
|
+
ui?.notify(`digest: ${loadConfig().digest ? "on" : "off"}\nusage: /agy digest on|off`, "info");
|
|
409
|
+
}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
306
413
|
if (sub === "thinking") {
|
|
307
414
|
if (val === "low" || val === "medium" || val === "high") {
|
|
308
415
|
const next = saveConfig({ defaultThinking: val as ThinkingTier });
|
|
@@ -313,53 +420,6 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
313
420
|
return;
|
|
314
421
|
}
|
|
315
422
|
|
|
316
|
-
if (sub === "patch") {
|
|
317
|
-
if (val === "restore") {
|
|
318
|
-
const r = restorePatch();
|
|
319
|
-
ui?.notify(
|
|
320
|
-
r.ok
|
|
321
|
-
? `Restored ${r.restoredFiles.length} file(s) from ${r.backupDir}. Restart pi to take effect.`
|
|
322
|
-
: `restore failed: ${r.reason}`,
|
|
323
|
-
r.ok ? "info" : "error",
|
|
324
|
-
);
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
if (val === "apply") {
|
|
328
|
-
const r = applyInvokeToolPatch();
|
|
329
|
-
if (r.patched || r.alreadyPresent) {
|
|
330
|
-
saveConfig({ invokeToolPatchDeclined: false });
|
|
331
|
-
}
|
|
332
|
-
const msg = r.patched
|
|
333
|
-
? `Applied patch to ${r.changedFiles.length} file(s) in ${r.root} (pi ${r.version}). Restart pi to activate.`
|
|
334
|
-
: r.alreadyPresent
|
|
335
|
-
? `Patch already present in ${r.root} (pi ${r.version}).`
|
|
336
|
-
: `apply failed: ${r.errors[0] ?? "unknown error"}`;
|
|
337
|
-
ui?.notify(msg, r.patched || r.alreadyPresent ? "info" : "error");
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
// status (default)
|
|
341
|
-
const s = patchStatus();
|
|
342
|
-
if (!s.root) {
|
|
343
|
-
ui?.notify("patch status: could not locate the pi package root.", "warning");
|
|
344
|
-
} else {
|
|
345
|
-
ui?.notify(
|
|
346
|
-
[
|
|
347
|
-
`pi.invokeTool patch: ${s.present ? "PRESENT" : "MISSING"}`,
|
|
348
|
-
` root: ${s.root}`,
|
|
349
|
-
` version: ${s.version}`,
|
|
350
|
-
s.missing.length ? ` missing: ${s.missing.length} site(s)` : null,
|
|
351
|
-
loadConfig().invokeToolPatchDeclined ? " consent: declined. Resume: /agy patch apply" : null,
|
|
352
|
-
s.backupDir ? ` backup: ${s.backupDir} (v${s.backupVersion})` : " backup: none",
|
|
353
|
-
"",
|
|
354
|
-
"Usage: /agy patch [status|apply|restore]",
|
|
355
|
-
]
|
|
356
|
-
.filter(Boolean)
|
|
357
|
-
.join("\n"),
|
|
358
|
-
"info",
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
423
|
|
|
364
424
|
// No subcommand (or "status"): print status, or open the picker in TUI.
|
|
365
425
|
if (sub && sub !== "status") {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Streaming Gemini provider for pi, built on the agy CLI. Registers antigravity/* models in pi's /model picker
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"description": "Streaming Gemini provider for pi, built on the agy CLI. Registers antigravity/* models in pi's /model picker; drives agy through its stream-json protocol (persistent process, tool round-trips, live usage).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
7
7
|
"pi-extension",
|
package/src/config.ts
CHANGED
|
@@ -24,6 +24,8 @@ const CONFIG_PATH = path.join(
|
|
|
24
24
|
|
|
25
25
|
export type AgyMode = "accept-edits" | "plan";
|
|
26
26
|
export type ThinkingTier = "low" | "medium" | "high";
|
|
27
|
+
export type AgyEngine = "stream-json" | "legacy-sqlite";
|
|
28
|
+
export type BridgeTools = "none" | "mcp" | "all";
|
|
27
29
|
|
|
28
30
|
export interface AgyConfig {
|
|
29
31
|
mode: AgyMode;
|
|
@@ -37,10 +39,33 @@ export interface AgyConfig {
|
|
|
37
39
|
defaultModel: string;
|
|
38
40
|
/** AskAntigravity tool: default thinking tier when the alias names none. */
|
|
39
41
|
defaultThinking: ThinkingTier;
|
|
40
|
-
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
42
|
+
/** Turn engine. "stream-json" (default): one persistent agy process fed
|
|
43
|
+
* NDJSON user events; enables live toolUse round-trips, native usage, and
|
|
44
|
+
* conversation binding from the init event. "legacy-sqlite": the old
|
|
45
|
+
* spawn-`agy -p`-and-poll-SQLite path, kept as a fallback for one release. */
|
|
46
|
+
engine: AgyEngine;
|
|
47
|
+
/** Set after the one-time notice about a leftover legacy invokeTool patch
|
|
48
|
+
* on the installed pi. The notice never repeats; /agy patch-cleanup is
|
|
49
|
+
* always available. */
|
|
50
|
+
patchCleanupNotified?: boolean;
|
|
51
|
+
/** Which pi tools the MCP bridge exposes to agy: "none" (bridge off),
|
|
52
|
+
* "mcp" (pi-mcp-adapter tools + skills bridge; default), "all" (every
|
|
53
|
+
* registered non-builtin tool incl. other Ask* delegations). */
|
|
54
|
+
bridgeTools: BridgeTools;
|
|
55
|
+
/** Inject a delta digest of pi-side context (compaction summaries, turns
|
|
56
|
+
* handled by other providers or pi's own tools) into each agy prompt.
|
|
57
|
+
*
|
|
58
|
+
* Default OFF. The digest changes every turn, which defeats agy's
|
|
59
|
+
* server-side prompt cache: every turn re-bills the full context
|
|
60
|
+
* (~25-30k tokens observed). With it off, prompts stay stable and the
|
|
61
|
+
* cache hits.
|
|
62
|
+
*
|
|
63
|
+
* Enable when you mix providers in one pi session (Claude turns, pi-side
|
|
64
|
+
* tool runs, or a compaction that agy should know about) and you value
|
|
65
|
+
* agy seeing that context over the cache re-billing. Pure antigravity
|
|
66
|
+
* sessions gain nothing: agy already keeps its own history, and bridge
|
|
67
|
+
* round-trips deliver tool results through the bridge, not the digest. */
|
|
68
|
+
digest: boolean;
|
|
44
69
|
}
|
|
45
70
|
|
|
46
71
|
const DEFAULTS: AgyConfig = {
|
|
@@ -48,6 +73,9 @@ const DEFAULTS: AgyConfig = {
|
|
|
48
73
|
skipPermissions: true,
|
|
49
74
|
defaultModel: "flash",
|
|
50
75
|
defaultThinking: "medium",
|
|
76
|
+
engine: "stream-json",
|
|
77
|
+
bridgeTools: "mcp",
|
|
78
|
+
digest: false,
|
|
51
79
|
};
|
|
52
80
|
|
|
53
81
|
/** Load config merged over defaults. Env vars override the file when set. */
|
|
@@ -92,7 +120,29 @@ export function loadConfig(configPath: string = CONFIG_PATH): AgyConfig {
|
|
|
92
120
|
const defaultThinking: ThinkingTier =
|
|
93
121
|
thinkRaw === "low" || thinkRaw === "high" ? thinkRaw : "medium";
|
|
94
122
|
|
|
95
|
-
|
|
123
|
+
const engine: AgyEngine =
|
|
124
|
+
process.env.AGY_ENGINE === "legacy-sqlite" || file.engine === "legacy-sqlite"
|
|
125
|
+
? "legacy-sqlite"
|
|
126
|
+
: "stream-json";
|
|
127
|
+
|
|
128
|
+
const bridgeRaw = (process.env.AGY_BRIDGE_TOOLS ?? file.bridgeTools ?? DEFAULTS.bridgeTools).toLowerCase();
|
|
129
|
+
const bridgeTools: BridgeTools =
|
|
130
|
+
bridgeRaw === "none" || bridgeRaw === "all" ? bridgeRaw : "mcp";
|
|
131
|
+
|
|
132
|
+
const digest = process.env.AGY_DIGEST !== undefined
|
|
133
|
+
? ["1", "true", "on"].includes(process.env.AGY_DIGEST.toLowerCase())
|
|
134
|
+
: file.digest ?? false;
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
mode,
|
|
138
|
+
skipPermissions,
|
|
139
|
+
defaultModel,
|
|
140
|
+
defaultThinking,
|
|
141
|
+
engine,
|
|
142
|
+
bridgeTools,
|
|
143
|
+
digest,
|
|
144
|
+
patchCleanupNotified: file.patchCleanupNotified === true,
|
|
145
|
+
};
|
|
96
146
|
}
|
|
97
147
|
|
|
98
148
|
/** Atomically persist a config patch (temp + rename). */
|