@estebanforge/pi-antigravity-bridge 1.0.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/CHANGELOG.md +125 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/docs/ARCHITECTURE.md +48 -0
- package/docs/DEVELOPMENT.md +64 -0
- package/docs/PI-BRIDGE-GAPS.md +186 -0
- package/docs/PI-INVOKETOOL-PATCH.md +227 -0
- package/extensions/index.ts +474 -0
- package/package.json +69 -0
- package/src/ask-tool.ts +579 -0
- package/src/config.ts +119 -0
- package/src/diff-render.ts +190 -0
- package/src/discovery.ts +199 -0
- package/src/mcp-server.ts +443 -0
- package/src/models.ts +261 -0
- package/src/patcher.ts +571 -0
- package/src/poller.ts +202 -0
- package/src/protobuf.ts +184 -0
- package/src/provider.ts +502 -0
- package/src/runner.ts +386 -0
- package/src/sessions.ts +159 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
// pi-antigravity-bridge - extension entry point.
|
|
2
|
+
//
|
|
3
|
+
// Registers Gemini (via the agy CLI) as a pi model provider so it shows up in
|
|
4
|
+
// the /model picker as antigravity/gemini-*. When selected, pi routes each turn
|
|
5
|
+
// through streamSimple, which spawns `agy -p`, polls the conversation SQLite DB
|
|
6
|
+
// agy writes, decodes the protobuf step payloads, and streams the agent text
|
|
7
|
+
// back into pi's TUI.
|
|
8
|
+
//
|
|
9
|
+
// Architectural wall (cannot be worked around - see PLAN.md):
|
|
10
|
+
// agy runs its OWN closed tool loop against --add-dir. pi's read/write/edit/
|
|
11
|
+
// bash tools never fire. Tool activity is surfaced as thinking events
|
|
12
|
+
// ("[agy tool: editing foo.ts]") for visibility, but the edits already landed
|
|
13
|
+
// on disk and pi's inline diff review does not engage.
|
|
14
|
+
//
|
|
15
|
+
// /agy command: status, mode picker (plan / accept-edits),
|
|
16
|
+
// and session clear. Config persists to ~/.pi/agent/antigravity-bridge/
|
|
17
|
+
// config.json so toggles survive restarts.
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
type ExtensionAPI,
|
|
21
|
+
type ExtensionCommandContext,
|
|
22
|
+
type ExtensionUIContext,
|
|
23
|
+
getSettingsListTheme,
|
|
24
|
+
} from "@earendil-works/pi-coding-agent";
|
|
25
|
+
import {
|
|
26
|
+
Container,
|
|
27
|
+
SettingsList,
|
|
28
|
+
Text,
|
|
29
|
+
type SettingItem,
|
|
30
|
+
} from "@earendil-works/pi-tui";
|
|
31
|
+
import {
|
|
32
|
+
entriesFromRaw,
|
|
33
|
+
FALLBACK_MODELS,
|
|
34
|
+
loadModelCatalogRaw,
|
|
35
|
+
toPiModel,
|
|
36
|
+
type AgyModelEntry,
|
|
37
|
+
} from "../src/models.js";
|
|
38
|
+
import { SessionStore } from "../src/sessions.js";
|
|
39
|
+
import { createStreamSimple } from "../src/provider.js";
|
|
40
|
+
import { CONFIG_PATH, loadConfig, saveConfig, type AgyMode, type ThinkingTier } from "../src/config.js";
|
|
41
|
+
import { registerAskAntigravityTool, toolModelsFromRaw } from "../src/ask-tool.js";
|
|
42
|
+
import { hasInvokeTool, startMcpServer, type McpServerHandle } from "../src/mcp-server.js";
|
|
43
|
+
import { applyInvokeToolPatch, decidePatchAction, patchStatus, restorePatch } from "../src/patcher.js";
|
|
44
|
+
|
|
45
|
+
function resolveAgyBinary(): string {
|
|
46
|
+
return process.env.AGY_BIN || "agy";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default async function (pi: ExtensionAPI): Promise<void> {
|
|
50
|
+
// Claim the AskAntigravity tool for this process. pi-ask-antigravity (if also
|
|
51
|
+
// installed) checks this in-process flag OR the bridge's package.json on disk
|
|
52
|
+
// and defers. See that extension's isBridgeInstalled().
|
|
53
|
+
(globalThis as Record<symbol, unknown>)[Symbol.for("pi-antigravity-bridge:active")] = true;
|
|
54
|
+
|
|
55
|
+
const binary = resolveAgyBinary();
|
|
56
|
+
|
|
57
|
+
// Discover once at load. Failure is non-fatal: FALLBACK_MODELS keeps the
|
|
58
|
+
// picker populated so the user gets a clear runtime error from agy rather
|
|
59
|
+
// than an empty model list. /reload re-runs this and refreshes after an
|
|
60
|
+
// `agy update`.
|
|
61
|
+
// loadModelCatalogRaw serves a short-TTL cache (~/.pi/agent/antigravity-bridge/
|
|
62
|
+
// models-cache.json) so reloads are instant and only re-spawn in the
|
|
63
|
+
// background when stale. Derive both catalogs from the same raw text
|
|
64
|
+
// (provider's slugified Gemini entries + the tool's family/version/tier
|
|
65
|
+
// entries).
|
|
66
|
+
const raw = await loadModelCatalogRaw(binary);
|
|
67
|
+
const discovered = entriesFromRaw(raw);
|
|
68
|
+
const toolModels = toolModelsFromRaw(raw);
|
|
69
|
+
const usingFallback = discovered.length === 0;
|
|
70
|
+
const entries: AgyModelEntry[] = usingFallback ? FALLBACK_MODELS : discovered;
|
|
71
|
+
const models = entries.map(toPiModel);
|
|
72
|
+
|
|
73
|
+
const store = new SessionStore();
|
|
74
|
+
const streamSimple = createStreamSimple({ entries, store });
|
|
75
|
+
|
|
76
|
+
pi.registerProvider("antigravity", {
|
|
77
|
+
name: "Antigravity (agy)",
|
|
78
|
+
baseUrl: "agy-bridge://antigravity",
|
|
79
|
+
apiKey: "not-used",
|
|
80
|
+
api: "agy-bridge",
|
|
81
|
+
models: models.map((m) => ({
|
|
82
|
+
id: m.id,
|
|
83
|
+
name: m.name,
|
|
84
|
+
api: m.api,
|
|
85
|
+
reasoning: m.reasoning,
|
|
86
|
+
input: m.input,
|
|
87
|
+
cost: m.cost,
|
|
88
|
+
contextWindow: m.contextWindow,
|
|
89
|
+
maxTokens: m.maxTokens,
|
|
90
|
+
})),
|
|
91
|
+
streamSimple,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
registerAgyCommand(pi, { entries, store, usingFallback });
|
|
95
|
+
|
|
96
|
+
// AskAntigravity tool: one-shot delegation to agy (ported from
|
|
97
|
+
// pi-ask-antigravity). When both extensions are installed, the bridge wins
|
|
98
|
+
// and pi-ask-antigravity registers nothing (its load-time defer guard
|
|
99
|
+
// detects this package via import.meta.resolve).
|
|
100
|
+
await registerAskAntigravityTool(pi, toolModels);
|
|
101
|
+
|
|
102
|
+
// MCP tool bridge: expose pi's tools to agy over localhost Streamable HTTP,
|
|
103
|
+
// backed by pi.invokeTool (local patch). Capability-gated: if the running pi
|
|
104
|
+
// lacks invokeTool, startMcpServer returns { ok: false } and the bridge runs
|
|
105
|
+
// unchanged (other instances do not break). Started on session_start so it is
|
|
106
|
+
// live before the provider's first turn; torn down on session_shutdown.
|
|
107
|
+
let mcpHandle: McpServerHandle | null = null;
|
|
108
|
+
const mcpLog = (s: string, d?: unknown) => {
|
|
109
|
+
// Quiet by default: surface only lifecycle/error events to stderr.
|
|
110
|
+
// Lifecycle + error events only. Per-turn success events (list-tools /
|
|
111
|
+
// call-tool) are deliberately NOT surfaced: writing to stderr during an
|
|
112
|
+
// active turn corrupts the pi TUI / construct daemon rendering (spinner
|
|
113
|
+
// gets stuck, hint text leaks into the display). Use a status-bar API for
|
|
114
|
+
// in-turn visibility instead.
|
|
115
|
+
const surfaced = new Set([
|
|
116
|
+
"listening", "capability-missing", "http-error", "closed",
|
|
117
|
+
"bridge-config-written", "bridge-config-removed", "bridge-config-write-failed",
|
|
118
|
+
"call-tool-fail", "transport-error", "handleRequest-error",
|
|
119
|
+
"request-error", "request-handler-error", "unauthorized", "self-patch-error",
|
|
120
|
+
]);
|
|
121
|
+
if (surfaced.has(s)) {
|
|
122
|
+
console.error(`[antigravity-bridge mcp] ${s}${d !== undefined ? " " + JSON.stringify(d) : ""}`);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
126
|
+
// Decide the pi.invokeTool patch + MCP tool bridge path. The patch is only
|
|
127
|
+
// needed for the bridge; the provider and AskAntigravity tool always work.
|
|
128
|
+
// The bridge can only start when the patch is LIVE in this process; a
|
|
129
|
+
// just-applied patch needs a full pi restart (not /reload) to go live.
|
|
130
|
+
const live = hasInvokeTool(pi);
|
|
131
|
+
const action = live
|
|
132
|
+
? ({ kind: "proceed" } as const)
|
|
133
|
+
: decidePatchAction(live, patchStatus().present, !!loadConfig().invokeToolPatchDeclined, !!ctx.hasUI);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
switch (action.kind) {
|
|
137
|
+
case "proceed": {
|
|
138
|
+
if (loadConfig().invokeToolPatchDeclined) {
|
|
139
|
+
saveConfig({ invokeToolPatchDeclined: false });
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case "notify-restart": {
|
|
144
|
+
ctx.ui.notify(
|
|
145
|
+
"The pi.invokeTool patch is present on disk but not loaded in this pi session. Fully RESTART pi (quit + relaunch) to start the MCP tool bridge.",
|
|
146
|
+
"warning",
|
|
147
|
+
);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
case "silent": {
|
|
151
|
+
// Previously declined. Stay quiet; bridge stays off until the user
|
|
152
|
+
// runs /agy patch apply or the patch becomes live.
|
|
153
|
+
mcpLog("patch-declined");
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case "ask": {
|
|
157
|
+
const apply = await ctx.ui.confirm(
|
|
158
|
+
"Apply the pi.invokeTool patch?",
|
|
159
|
+
"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.",
|
|
160
|
+
// Bound the wait so a non-confirm-capable RPC client (hasUI is always
|
|
161
|
+
// true in RPC) can't hang session_start. Timeout resolves like "no"
|
|
162
|
+
// (declined persists); reversible via /agy patch apply.
|
|
163
|
+
{ timeout: 60_000 },
|
|
164
|
+
);
|
|
165
|
+
if (apply) {
|
|
166
|
+
const res = applyInvokeToolPatch({ log: mcpLog });
|
|
167
|
+
if (res.patched) {
|
|
168
|
+
saveConfig({ invokeToolPatchDeclined: false });
|
|
169
|
+
ctx.ui.notify(
|
|
170
|
+
`Applied the pi.invokeTool patch to ${res.root} (pi ${res.version}). Fully RESTART pi (quit + relaunch) to start the MCP tool bridge.`,
|
|
171
|
+
"warning",
|
|
172
|
+
);
|
|
173
|
+
} else if (res.errors.length > 0) {
|
|
174
|
+
ctx.ui.notify(`pi.invokeTool patch failed: ${res.errors[0]}`, "error");
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
saveConfig({ invokeToolPatchDeclined: true });
|
|
178
|
+
ctx.ui.notify(
|
|
179
|
+
"Skipped. The MCP tool bridge stays off. To enable it later, run /agy patch apply. Then restart pi.",
|
|
180
|
+
"info",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case "headless-skip": {
|
|
186
|
+
console.error(
|
|
187
|
+
"[antigravity-bridge] pi.invokeTool patch missing; MCP tool bridge off. Apply interactively (/agy patch apply) or re-run pi in a TUI.",
|
|
188
|
+
);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} catch (e) {
|
|
193
|
+
mcpLog("self-patch-error", e instanceof Error ? e.message : String(e));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (action.kind !== "proceed") return; // bridge can't start without a live patch
|
|
197
|
+
if (mcpHandle) return; // already running (reload re-fires session_start)
|
|
198
|
+
const r = await startMcpServer(pi, { log: mcpLog });
|
|
199
|
+
if (r.ok && r.handle) {
|
|
200
|
+
mcpHandle = r.handle;
|
|
201
|
+
} else {
|
|
202
|
+
console.error(`[antigravity-bridge] MCP tool bridge disabled: ${r.reason}`);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
pi.on("session_shutdown", async () => {
|
|
206
|
+
const h = mcpHandle;
|
|
207
|
+
mcpHandle = null;
|
|
208
|
+
await h?.close();
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// --- /agy command -----------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
interface AgyCommandCtx {
|
|
215
|
+
entries: AgyModelEntry[];
|
|
216
|
+
store: SessionStore;
|
|
217
|
+
usingFallback: boolean;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
interface PendingConfig {
|
|
221
|
+
mode?: AgyMode;
|
|
222
|
+
skipPermissions?: boolean;
|
|
223
|
+
defaultModel?: string;
|
|
224
|
+
defaultThinking?: ThinkingTier;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function statusText(ctx: AgyCommandCtx): string {
|
|
228
|
+
const config = loadConfig();
|
|
229
|
+
const source = ctx.usingFallback ? "fallback (agy models failed)" : "discovered";
|
|
230
|
+
const perm = config.skipPermissions ? "auto-approved (DANGEROUS)" : "prompt (hangs in -p)";
|
|
231
|
+
return [
|
|
232
|
+
"Antigravity bridge",
|
|
233
|
+
` models: ${ctx.entries.length} ${source}`,
|
|
234
|
+
` mode: ${config.mode}`,
|
|
235
|
+
` permissions: ${perm}`,
|
|
236
|
+
` tool model: ${config.defaultModel}`,
|
|
237
|
+
` tool thinking: ${config.defaultThinking}`,
|
|
238
|
+
` sessions: ${ctx.store.size} bound`,
|
|
239
|
+
` config: ${CONFIG_PATH}`,
|
|
240
|
+
` invokeTool: ${patchStateLabel()}`,
|
|
241
|
+
"",
|
|
242
|
+
"Subcommands: /agy mode plan|accept-edits, /agy permissions on|off, /agy model flash|pro|gemini, /agy thinking low|medium|high, /agy patch status|apply|restore, /agy clear",
|
|
243
|
+
].join("\n");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function patchStateLabel(): string {
|
|
247
|
+
const s = patchStatus();
|
|
248
|
+
if (s.present) return "patched";
|
|
249
|
+
if (!s.root) return "MISSING (pi root not found)";
|
|
250
|
+
if (loadConfig().invokeToolPatchDeclined) return `declined (pi ${s.version}). Resume: /agy patch apply`;
|
|
251
|
+
return `MISSING (pi ${s.version}). Apply it: /agy patch apply`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
255
|
+
pi.registerCommand("agy", {
|
|
256
|
+
description:
|
|
257
|
+
"Antigravity provider: status, mode picker, patch, clear sessions. Usage: /agy [status|mode [plan|accept-edits]|patch [status|apply|restore]|clear]",
|
|
258
|
+
handler: async (args, cmdCtx: ExtensionCommandContext) => {
|
|
259
|
+
const ui = cmdCtx.ui;
|
|
260
|
+
const mode = cmdCtx.mode;
|
|
261
|
+
const sub = (args ?? "").trim().split(/\s+/)[0]?.toLowerCase();
|
|
262
|
+
const val = (args ?? "").trim().split(/\s+/)[1]?.toLowerCase();
|
|
263
|
+
|
|
264
|
+
// Direct subcommands work everywhere (headless + TUI).
|
|
265
|
+
if (sub === "clear") {
|
|
266
|
+
ctx.store.clear();
|
|
267
|
+
ui?.notify("Cleared all antigravity session bindings.", "info");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (sub === "mode") {
|
|
271
|
+
if (val === "plan" || val === "accept-edits") {
|
|
272
|
+
const next = saveConfig({ mode: val as AgyMode });
|
|
273
|
+
ui?.notify(`mode set to ${next.mode}`, "info");
|
|
274
|
+
} else {
|
|
275
|
+
ui?.notify(`current mode: ${loadConfig().mode}\nusage: /agy mode plan|accept-edits`, "info");
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (sub === "permissions") {
|
|
280
|
+
if (val === "on" || val === "off") {
|
|
281
|
+
const next = saveConfig({ skipPermissions: val === "on" });
|
|
282
|
+
const warn = next.skipPermissions ? "\nWARNING: agy can now run arbitrary commands without review." : "";
|
|
283
|
+
ui?.notify(`permissions: ${next.skipPermissions ? "auto-approved (DANGEROUS)" : "prompt"}${warn}`, next.skipPermissions ? "warning" : "info");
|
|
284
|
+
} else {
|
|
285
|
+
ui?.notify(`permissions: ${loadConfig().skipPermissions ? "auto-approved (DANGEROUS)" : "prompt"}\nusage: /agy permissions on|off\n(off hangs any run_command in non-interactive mode)`, "info");
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (sub === "model") {
|
|
290
|
+
if (val && val.length > 0) {
|
|
291
|
+
const next = saveConfig({ defaultModel: val });
|
|
292
|
+
ui?.notify(`tool default model set to ${next.defaultModel}`, "info");
|
|
293
|
+
} else {
|
|
294
|
+
ui?.notify(`tool model: ${loadConfig().defaultModel}\nusage: /agy model flash|pro|gemini|<exact>`, "info");
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (sub === "thinking") {
|
|
299
|
+
if (val === "low" || val === "medium" || val === "high") {
|
|
300
|
+
const next = saveConfig({ defaultThinking: val as ThinkingTier });
|
|
301
|
+
ui?.notify(`tool default thinking set to ${next.defaultThinking}`, "info");
|
|
302
|
+
} else {
|
|
303
|
+
ui?.notify(`tool thinking: ${loadConfig().defaultThinking}\nusage: /agy thinking low|medium|high`, "info");
|
|
304
|
+
}
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (sub === "patch") {
|
|
309
|
+
if (val === "restore") {
|
|
310
|
+
const r = restorePatch();
|
|
311
|
+
ui?.notify(
|
|
312
|
+
r.ok
|
|
313
|
+
? `Restored ${r.restoredFiles.length} file(s) from ${r.backupDir}. Restart pi to take effect.`
|
|
314
|
+
: `restore failed: ${r.reason}`,
|
|
315
|
+
r.ok ? "info" : "error",
|
|
316
|
+
);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (val === "apply") {
|
|
320
|
+
const r = applyInvokeToolPatch();
|
|
321
|
+
if (r.patched || r.alreadyPresent) {
|
|
322
|
+
saveConfig({ invokeToolPatchDeclined: false });
|
|
323
|
+
}
|
|
324
|
+
const msg = r.patched
|
|
325
|
+
? `Applied patch to ${r.changedFiles.length} file(s) in ${r.root} (pi ${r.version}). Restart pi to activate.`
|
|
326
|
+
: r.alreadyPresent
|
|
327
|
+
? `Patch already present in ${r.root} (pi ${r.version}).`
|
|
328
|
+
: `apply failed: ${r.errors[0] ?? "unknown error"}`;
|
|
329
|
+
ui?.notify(msg, r.patched || r.alreadyPresent ? "info" : "error");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
// status (default)
|
|
333
|
+
const s = patchStatus();
|
|
334
|
+
if (!s.root) {
|
|
335
|
+
ui?.notify("patch status: could not locate the pi package root.", "warning");
|
|
336
|
+
} else {
|
|
337
|
+
ui?.notify(
|
|
338
|
+
[
|
|
339
|
+
`pi.invokeTool patch: ${s.present ? "PRESENT" : "MISSING"}`,
|
|
340
|
+
` root: ${s.root}`,
|
|
341
|
+
` version: ${s.version}`,
|
|
342
|
+
s.missing.length ? ` missing: ${s.missing.length} site(s)` : null,
|
|
343
|
+
loadConfig().invokeToolPatchDeclined ? " consent: declined. Resume: /agy patch apply" : null,
|
|
344
|
+
s.backupDir ? ` backup: ${s.backupDir} (v${s.backupVersion})` : " backup: none",
|
|
345
|
+
"",
|
|
346
|
+
"Usage: /agy patch [status|apply|restore]",
|
|
347
|
+
]
|
|
348
|
+
.filter(Boolean)
|
|
349
|
+
.join("\n"),
|
|
350
|
+
"info",
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// No subcommand (or "status"): print status, or open the picker in TUI.
|
|
357
|
+
if (sub && sub !== "status") {
|
|
358
|
+
ui?.notify(`unknown subcommand: ${sub}\n${statusText(ctx)}`, "warning");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (mode !== "tui" || !ui) {
|
|
363
|
+
ui?.notify(statusText(ctx), "info");
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
await openAgyPicker(ui, ctx);
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Interactive settings picker (TUI only). Rows: mode + permissions + model + thinking. */
|
|
373
|
+
async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promise<void> {
|
|
374
|
+
const config = loadConfig();
|
|
375
|
+
const pending: PendingConfig = {};
|
|
376
|
+
|
|
377
|
+
const items: SettingItem[] = [
|
|
378
|
+
{
|
|
379
|
+
id: "mode",
|
|
380
|
+
label: "Execution mode",
|
|
381
|
+
description:
|
|
382
|
+
"accept-edits: agy applies edits directly. plan: review-only, no writes. Takes effect next turn.",
|
|
383
|
+
currentValue: config.mode,
|
|
384
|
+
values: ["accept-edits", "plan"],
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
id: "permissions",
|
|
388
|
+
label: "Permissions",
|
|
389
|
+
description:
|
|
390
|
+
"auto-approved: --dangerously-skip-permissions (required so commands don't hang in -p mode). prompt: agy asks y/n (hangs non-interactively).",
|
|
391
|
+
currentValue: config.skipPermissions ? "auto-approved" : "prompt",
|
|
392
|
+
values: ["auto-approved", "prompt"],
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
id: "model",
|
|
396
|
+
label: "Tool default model",
|
|
397
|
+
description:
|
|
398
|
+
"Alias used when the AskAntigravity tool omits its model param. flash/pro/gemini, or an exact id.",
|
|
399
|
+
currentValue: config.defaultModel,
|
|
400
|
+
values: ["flash", "pro", "gemini"],
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
id: "thinking",
|
|
404
|
+
label: "Tool default thinking",
|
|
405
|
+
description:
|
|
406
|
+
"Thinking tier used when the model alias names none. Pro has no Medium; it falls back to nearest.",
|
|
407
|
+
currentValue: config.defaultThinking,
|
|
408
|
+
values: ["low", "medium", "high"],
|
|
409
|
+
},
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
await ui.custom((tui, theme, _kb, done) => {
|
|
413
|
+
const container = new Container();
|
|
414
|
+
container.addChild(
|
|
415
|
+
new Text(theme.fg("accent", theme.bold("Antigravity provider")), 1, 1),
|
|
416
|
+
);
|
|
417
|
+
const settingsList = new SettingsList(
|
|
418
|
+
items,
|
|
419
|
+
Math.min(items.length + 4, 15),
|
|
420
|
+
getSettingsListTheme(),
|
|
421
|
+
(id, newValue) => {
|
|
422
|
+
if (id === "mode") {
|
|
423
|
+
pending.mode = newValue as AgyMode;
|
|
424
|
+
} else if (id === "permissions") {
|
|
425
|
+
pending.skipPermissions = newValue === "auto-approved";
|
|
426
|
+
} else if (id === "model") {
|
|
427
|
+
pending.defaultModel = newValue;
|
|
428
|
+
} else if (id === "thinking") {
|
|
429
|
+
pending.defaultThinking = newValue as ThinkingTier;
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
() => done(undefined),
|
|
433
|
+
);
|
|
434
|
+
container.addChild(settingsList);
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
render: (w: number) => container.render(w),
|
|
438
|
+
invalidate: () => container.invalidate(),
|
|
439
|
+
handleInput: (data: string) => {
|
|
440
|
+
settingsList.handleInput?.(data);
|
|
441
|
+
tui.requestRender();
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
if (
|
|
447
|
+
pending.mode === undefined &&
|
|
448
|
+
pending.skipPermissions === undefined &&
|
|
449
|
+
pending.defaultModel === undefined &&
|
|
450
|
+
pending.defaultThinking === undefined
|
|
451
|
+
)
|
|
452
|
+
return;
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
const next = saveConfig(pending);
|
|
456
|
+
const changed = [
|
|
457
|
+
pending.mode ? `mode=${next.mode}` : null,
|
|
458
|
+
pending.skipPermissions !== undefined
|
|
459
|
+
? `permissions=${next.skipPermissions ? "auto-approved" : "prompt"}`
|
|
460
|
+
: null,
|
|
461
|
+
pending.defaultModel !== undefined ? `tool model=${next.defaultModel}` : null,
|
|
462
|
+
pending.defaultThinking !== undefined ? `tool thinking=${next.defaultThinking}` : null,
|
|
463
|
+
]
|
|
464
|
+
.filter(Boolean)
|
|
465
|
+
.join(", ");
|
|
466
|
+
ui.notify(`Saved: ${changed}`, "info");
|
|
467
|
+
} catch (err) {
|
|
468
|
+
ui.notify(
|
|
469
|
+
`Failed to save config: ${err instanceof Error ? err.message : String(err)}`,
|
|
470
|
+
"error",
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
void ctx;
|
|
474
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Streaming Gemini provider for pi, built on the agy CLI. Registers antigravity/* models in pi's /model picker via SQLite polling + protobuf decode of agy's conversation DBs.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"antigravity",
|
|
9
|
+
"agy",
|
|
10
|
+
"gemini",
|
|
11
|
+
"google",
|
|
12
|
+
"provider",
|
|
13
|
+
"streaming"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "EstebanForge",
|
|
18
|
+
"email": "esteban@attitude.cl",
|
|
19
|
+
"url": "https://actitud.xyz"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/EstebanForge/pi-antigravity-bridge.git"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"type": "module",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc --noEmit",
|
|
31
|
+
"test": "tsx --test tests/*.test.ts",
|
|
32
|
+
"prepublishOnly": "npm run build && npm test",
|
|
33
|
+
"run-agy": "tsx scripts/run-agy.ts",
|
|
34
|
+
"decode-db": "tsx scripts/decode-db.ts",
|
|
35
|
+
"smoke:pi": "bash scripts/smoke-in-pi.sh"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"extensions",
|
|
39
|
+
"src",
|
|
40
|
+
"docs",
|
|
41
|
+
"README.md",
|
|
42
|
+
"CHANGELOG.md"
|
|
43
|
+
],
|
|
44
|
+
"pi": {
|
|
45
|
+
"extensions": [
|
|
46
|
+
"./extensions"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"@earendil-works/pi-coding-agent": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@earendil-works/pi-ai": "^0.82.1",
|
|
59
|
+
"@earendil-works/pi-coding-agent": "^0.82.1",
|
|
60
|
+
"@earendil-works/pi-tui": "^0.82.1",
|
|
61
|
+
"@types/node": "^22.0.0",
|
|
62
|
+
"tsx": "^4.19.0",
|
|
63
|
+
"typebox": "^1.1.38",
|
|
64
|
+
"typescript": "^5.8.0"
|
|
65
|
+
},
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
68
|
+
}
|
|
69
|
+
}
|