@estebanforge/pi-antigravity-bridge 1.2.5 → 1.3.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 +38 -0
- package/README.md +30 -2
- package/docs/ARCHITECTURE.md +0 -1
- package/extensions/index.ts +199 -139
- package/package.json +1 -1
- package/src/config.ts +55 -5
- package/src/driver.ts +644 -0
- package/src/mcp-server.ts +28 -48
- package/src/models.ts +1 -1
- 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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.3.0] - 2026-08-31
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Stream-json engine: one persistent `agy --input-format stream-json` process per provider; conversation binding from the `init` event (no more SQLite snapshot diffing), native tool-step events (no protobuf decoding), and token usage mapped onto pi's usage when agy reports it. `AGY_ENGINE=legacy-sqlite` keeps the old engine for one release.
|
|
10
|
+
- No-patch MCP tool bridge: bridge calls park in a round-trip store; the provider emits them as real pi `toolUse` turns, pi executes with native cards/permissions/hooks, and the toolResult completes the parked MCP response on the next stream call. `bridgeTools` config selects the surface: `none` / `mcp` (default) / `all`.
|
|
11
|
+
- Native re-execution of agy read-only tools as real pi builtins (native cards, live output).
|
|
12
|
+
- Display-only `antigravity` wrapper tool: mutating agy steps land as real toolCall/toolResult pairs via recorded-output replay.
|
|
13
|
+
- Skills bridge: `activate_skill` tool exposing the pi Agent Skills catalog to agy, answered by the bridge directly.
|
|
14
|
+
- `/agy doctor`: engine, bridge, driver counters, and lifecycle tail, zero tokens.
|
|
15
|
+
- Legacy patch cleanup: `src/patch-cleanup.ts` detects a leftover invokeTool patch, one-time notice on session start, and `/agy patch-cleanup` restores the original files from the versioned backup.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- The MCP tool bridge no longer patches pi. The `pi.invokeTool` round-trip is replaced by the provider-owned park/emit/resolve flow above.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Live stream-json protocol shapes against real agy: terminal status is `SUCCESS` (not `OK`) and agent text arrives as `text_delta`. The first burn-in turn failed on both; both are pinned by a regression test.
|
|
24
|
+
- Native re-exec tool calls include the `reasoning` argument pi requires on read/edit-class builtins; without it pi rejected every native `read` card at validation.
|
|
25
|
+
- Peer-review round 2 (engine): parked bridge calls suspend the stdout idle timer (a >5-minute permission prompt no longer kills the turn); turn lifetimes are serialized (a second `run()` can no longer orphan an open turn); the cumulative-text guard points the right direction; a settled turn fails round-trips parked against it.
|
|
26
|
+
- Peer-review round 2 (cleanup): backup selection prefers an exact version match over newest-by-mtime (stacked multi-version backups made legitimate restores refuse); `WrapperReplay` entries are single-use (no unbounded growth, no enumerable stale outputs); `rt`-kind round-trips are removed on turn death; the one-time leftover-patch notice is headless-safe (`ctx.hasUI` gate with stderr fallback) and set after surfacing, not before.
|
|
27
|
+
|
|
28
|
+
### Removed
|
|
29
|
+
|
|
30
|
+
- `pi.invokeTool` patch: `src/patcher.ts`, the load-time consent prompt, `/agy patch` subcommands, `docs/PI-INVOKETOOL-PATCH.md`, and the `invokeToolPatchDeclined` config flag.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## [1.2.6] - 2026-08-28
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **Offline fallback Flash bumped to Gemini 3.7.** `FALLBACK_MODELS`
|
|
38
|
+
(served only when `agy models` fails or is unauthenticated at load) now
|
|
39
|
+
offers `gemini-3.7-flash` instead of `gemini-3.6-flash`. Live discovery
|
|
40
|
+
always overrides the fallback, so this only affects the picker when agy
|
|
41
|
+
is missing or broken.
|
|
42
|
+
|
|
5
43
|
## [1.2.5] - 2026-08-24
|
|
6
44
|
|
|
7
45
|
### Fixed
|
package/README.md
CHANGED
|
@@ -26,9 +26,9 @@ Residual limits (with or without the bridge):
|
|
|
26
26
|
|
|
27
27
|
While agy is the active model it normally cannot see pi's universe of extensions: agentmemory, codegraph, web search, slack/asana, the `Ask*` delegations, and any other installed pi tool. This extension optionally bridges that gap.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
The bridge starts a localhost MCP server inside pi's process. `tools/list` returns pi's registered tools (built-in file/shell tools and `AskAntigravity` are filtered out), and `tools/call` executes them via `pi.invokeTool()`. agy discovers the server through a per-invocation config: the bridge writes `.agents/mcp_config.json` into a bridge-controlled dir (`~/.pi/agent/antigravity-bridge/agy-mcp-<pid>/`) and the provider passes that dir as an extra `--add-dir` when it spawns agy. The user's global agy config (`~/.gemini/config/mcp_config.json`) is never touched, so standalone agy outside pi is unaffected.
|
|
30
30
|
|
|
31
|
-
**
|
|
31
|
+
**No patch required.** Bridge calls park in the provider's round-trip store; the provider ends the pi assistant message with a `toolUse` stop reason for the real pi tool, pi executes it in its own loop (native cards, permissions, hooks), and the toolResult completes the parked MCP response on the next stream call. This is the same mechanism tianzuo/pi-antigravity uses; upstream pi APIs only.
|
|
32
32
|
|
|
33
33
|
**Recursion safety.** Only the provider's agy receives the extra `--add-dir`. The `AskAntigravity` tool spawns its own agy with just the workspace, so that inner agy starts plain (no pi tools) and cannot re-enter. `AskAntigravity` is also filtered from the exposed tool list. Standalone agy is unaffected because nothing is written to its global config.
|
|
34
34
|
|
|
@@ -36,6 +36,22 @@ When the capability is present the bridge starts a localhost MCP server inside p
|
|
|
36
36
|
|
|
37
37
|
**Security.** The MCP server binds to `127.0.0.1` only and requires a per-session shared-secret header (`x-bridge-token`) that agy sends from the bridge config; browsers cannot set custom headers on a simple cross-origin POST, so this blocks web CSRF against the loopback server. Request bodies are size-capped. This is intended for single-user developer machines: any local process running as the same user can read the token from the per-pid config and call the exposed tools, so do not run it on a shared host where you do not trust other same-user processes.
|
|
38
38
|
|
|
39
|
+
### Native cards, wrapper replay, and skills (stream-json engine)
|
|
40
|
+
|
|
41
|
+
Read-only agy steps (view_file, list_dir, grep_search, find_by_name) re-run as
|
|
42
|
+
real pi builtins (`read`, `ls`, `grep`, `find`) when those builtins are active,
|
|
43
|
+
so their cards render with pi's own renderers. Mutating and agy-specialty steps
|
|
44
|
+
render through a display-only `antigravity` wrapper tool: its `execute()`
|
|
45
|
+
replays the output agy already recorded, so the transcript gets proper
|
|
46
|
+
toolCall/toolResult pairs without any double execution. Neither path re-runs
|
|
47
|
+
anything with side effects.
|
|
48
|
+
|
|
49
|
+
When the bridge is on, agy also gets one `activate_skill` tool whose enum is
|
|
50
|
+
your pi Agent Skills catalog; calling it returns the SKILL.md body. The bridge
|
|
51
|
+
answers it directly, no pi round-trip. `/agy doctor` prints engine state,
|
|
52
|
+
driver counters, bridge port, and the last lifecycle events without spending
|
|
53
|
+
tokens.
|
|
54
|
+
|
|
39
55
|
## Install
|
|
40
56
|
|
|
41
57
|
> ⚠️ **Heads-up: this extension patches your `pi` install.** When it loads and
|
|
@@ -92,6 +108,18 @@ Model ids are slugified from the `agy models` output (`Gemini 3.6 Flash (Medium)
|
|
|
92
108
|
|
|
93
109
|
If `agy models` fails at load (binary missing, auth not done, network stall), a fallback catalog still populates the picker so you get a clear runtime error instead of an empty list.
|
|
94
110
|
|
|
111
|
+
### Engine and bridge surface
|
|
112
|
+
|
|
113
|
+
`config.json` selects the turn engine and the bridge surface:
|
|
114
|
+
|
|
115
|
+
| Key | Values | Default |
|
|
116
|
+
| --- | --- | --- |
|
|
117
|
+
| `engine` | `stream-json` (persistent agy process, toolUse round-trips, live usage) or `legacy-sqlite` (spawn `agy -p`, poll its SQLite) | `stream-json` |
|
|
118
|
+
| `bridgeTools` | `none` (bridge off), `mcp` (pi-mcp-adapter tools), `all` (every non-builtin tool, incl. other `Ask*` delegations) | `mcp` |
|
|
119
|
+
| `digest` | `off` (stable prompts; agy's prompt cache hits) or `on` (inject a delta of pi-side context - compaction summaries, other-provider turns - into each agy prompt; the delta changes every turn, so agy re-bills the full context). Enable for mixed-provider sessions where agy must see pi-side context | `off` |
|
|
120
|
+
|
|
121
|
+
Env overrides: `AGY_ENGINE`, `AGY_BRIDGE_TOOLS`, `AGY_DIGEST`. The `legacy-sqlite` engine is kept as a fallback and is scheduled for removal once the stream-json engine has burned in.
|
|
122
|
+
|
|
95
123
|
### The /agy command
|
|
96
124
|
|
|
97
125
|
`/agy` configures the provider at runtime. Settings persist to `~/.pi/agent/antigravity-bridge/config.json` and take effect on the next turn.
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -16,7 +16,6 @@ src/sessions.ts atomic JSON store: pi session -> agy conversation + last s
|
|
|
16
16
|
src/config.ts persisted runtime config (mode, permissions, model/thinking defaults)
|
|
17
17
|
src/ask-tool.ts the AskAntigravity one-shot delegation tool (model/thinking defaults)
|
|
18
18
|
src/mcp-server.ts MCP tool bridge: exposes pi's tools to agy over Streamable HTTP
|
|
19
|
-
src/patcher.ts Auto-applies the pi.invokeTool local patch to enable the MCP tool bridge
|
|
20
19
|
src/diff-render.ts render agy's file edits as git diffs in pi's thinking stream
|
|
21
20
|
```
|
|
22
21
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
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). */
|