@estebanforge/pi-antigravity-bridge 1.4.1 → 1.4.3
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 +21 -0
- package/README.md +12 -6
- package/extensions/index.ts +196 -44
- package/package.json +1 -1
- package/src/config.ts +19 -4
- package/src/skills.ts +109 -32
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.4.3] - 2026-09-04
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- AskAntigravity tool toggle: `/agy ask on|off`, an "AskAntigravity tool" row in the bare-`/agy` picker, and the `askTool` config key (env `AGY_ASK_TOOL`). Default on; `off` skips registering the AskAntigravity one-shot delegation tool entirely, for users who want only the provider and its models and no delegation tool in the model's window context. When the separate pi-ask-antigravity package is also installed, `off` means no delegation tool from either package (no fallback). Takes effect on the next pi start (or /reload); `/agy status` shows the state.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- The bare-`/agy` settings picker gains a Turn engine row (`stream-json`/`acp`). Saving `acp` persists the switch, then runs the same self-service setup as `/agy engine acp` (binary install + auth bootstrap, `acp.bin` updated). The plan+acp RC01 guard now evaluates the effective engine AND mode after the save, so a config already in `plan` plus a fresh `acp` pick is refused instead of persisting an invalid combination (peer-review find).
|
|
15
|
+
- "Tool default model/thinking" renamed to "AskAntigravity model/thinking" across the picker, `/agy status`, and toasts; descriptions and README now state the values are fallbacks that callers may override per call.
|
|
16
|
+
- All ACP login-pending messages share one wording that names the antigravity-acp server and its place in Google's Antigravity suite (agy desktop, agy editor, agy cli, agy acp), and clarifies the login: same Google account as the agy CLI, separate login with its own token file.
|
|
17
|
+
|
|
18
|
+
## [1.4.2] - 2026-09-04
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- `bridgeTools` now defaults to `all` (every registered non-builtin pi tool) instead of `mcp`. The `mcp` surface filters to pi-mcp-adapter tools and serves an empty catalog on installs without that adapter, which left the bridge registered but tool-less from the model's point of view. `none` still opts out entirely; explicit `"bridgeTools": "mcp"` in an existing config keeps pinning the narrow surface.
|
|
23
|
+
- Skill discovery now mirrors pi's directory-based scan (docs/skills.md): global `~/.pi/agent/skills` AND `~/.agents/skills`, project `.pi/skills` plus `.agents/skills` in cwd and ancestors up to the git root (project dirs only when pi has trusted the project, same gate pi applies), recursive SKILL.md discovery with grouping folders, per-style root/`.md` rules, hidden entries skipped, and description-less skills dropped. Pi's other skill sources (`skills` settings array, `package.json`, `--skill`) are not mirrored. Previously only one flat level of two directories was scanned, and `~/.agents/skills` (where pi actually reads most skills) was missing entirely, so `activate_skill` never appeared.
|
|
24
|
+
- `/agy` now exposes the full runtime config surface: new `/agy bridge all|mcp|none` (which pi tools the MCP bridge hands to agy) and `/agy acp-bin <path|auto>` (target a specific ACP server binary; applies on the next ACP turn), plus Bridge tools, Context digest, and System prompt rows in the bare `/agy` settings picker. Usage strings and `/agy status` list every knob.
|
|
25
|
+
|
|
5
26
|
## [1.4.1] - 2026-09-04
|
|
6
27
|
|
|
7
28
|
### Added
|
package/README.md
CHANGED
|
@@ -104,27 +104,33 @@ If `agy models` fails at load (binary missing, auth not done, network stall), a
|
|
|
104
104
|
|
|
105
105
|
| Key | Values | Default |
|
|
106
106
|
| --- | --- | --- |
|
|
107
|
-
| `
|
|
107
|
+
| `askTool` | `on` (register the AskAntigravity delegation tool), `off` (no delegation tool; provider and models only) | `on` |
|
|
108
|
+
| `bridgeTools` | `none` (bridge off), `all` (every non-builtin tool, incl. other `Ask*` delegations), `mcp` (pi-mcp-adapter tools + skills bridge only) | `all` |
|
|
108
109
|
| `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` |
|
|
109
110
|
| `systemPrompt` | `on` (prepend pi's system prompt - operating instructions plus the global agent-dir `AGENTS.md` and ancestor `AGENTS.md`/`CLAUDE.md` - to the first prompt of each new agy conversation) or `off` (agy-native behavior) | `on` |
|
|
110
111
|
|
|
111
|
-
Env overrides: `AGY_BRIDGE_TOOLS`, `AGY_DIGEST`, `AGY_SYSTEM_PROMPT`. Env wins over the file, so while `AGY_DIGEST` or `AGY_SYSTEM_PROMPT` is set, the matching `/agy digest` or `/agy system-prompt` toggle persists a value that never takes effect.
|
|
112
|
+
Env overrides: `AGY_BRIDGE_TOOLS`, `AGY_ASK_TOOL`, `AGY_DIGEST`, `AGY_SYSTEM_PROMPT`. Env wins over the file, so while `AGY_DIGEST` or `AGY_SYSTEM_PROMPT` is set, the matching `/agy digest` or `/agy system-prompt` toggle persists a value that never takes effect.
|
|
113
|
+
|
|
114
|
+
The `activate_skill` catalog mirrors pi's directory-based skill discovery: the two global dirs plus project dirs, the latter only when pi has trusted the project (same gate pi itself applies). Pi's other skill sources - the `skills` settings array, `package.json` entries, and `--skill` CLI paths - are not mirrored and won't appear in the catalog.
|
|
112
115
|
|
|
113
116
|
### The /agy command
|
|
114
117
|
|
|
115
118
|
`/agy` configures the provider at runtime. Settings persist to `~/.pi/agent/antigravity-bridge/config.json` and take effect on the next turn.
|
|
116
119
|
|
|
117
120
|
```
|
|
118
|
-
/agy status, or open the
|
|
119
|
-
/agy status print current
|
|
121
|
+
/agy status, or open the full settings picker (TUI)
|
|
122
|
+
/agy status print current settings + session counts
|
|
120
123
|
/agy doctor bridge state, driver counters, bridge port, last lifecycle events
|
|
121
124
|
/agy mode plan review-only: agy plans but writes nothing
|
|
122
125
|
/agy mode accept-edits agy applies edits directly (default)
|
|
123
126
|
/agy permissions on|off auto-approve / prompt for tool calls (see warning)
|
|
124
|
-
/agy
|
|
125
|
-
/agy
|
|
127
|
+
/agy ask on|off register the AskAntigravity delegation tool (default on; off keeps only the provider and models, even with pi-ask-antigravity installed)
|
|
128
|
+
/agy model flash|pro|gemini fallback model for the AskAntigravity tool; callers may override per call
|
|
129
|
+
/agy thinking low|medium|high fallback thinking tier for the AskAntigravity tool; callers may override per call
|
|
126
130
|
/agy digest on|off inject pi-side context into agy prompts (default off; see table above)
|
|
127
131
|
/agy system-prompt on|off send pi's system prompt + AGENTS.md to new agy conversations (default on)
|
|
132
|
+
/agy bridge all|mcp|none which pi tools the MCP bridge exposes to agy (default all; none = bridge off)
|
|
133
|
+
/agy acp-bin <path|auto> point the ACP engine at a specific server binary (auto = setup installs, or AGY_ACP_BIN; applies on the next ACP turn)
|
|
128
134
|
/agy engine acp|stream-json switch the turn engine (restart to apply; default stream-json; acp runs self-service setup: binary install + auth bootstrap)
|
|
129
135
|
/agy acp-auth manual ACP credential setup (fallback; auto-setup normally covers this; default login = your Antigravity subscription, same account as the agy CLI)
|
|
130
136
|
/agy patch-cleanup restore the original pi files if an older version patched them
|
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,
|
|
@@ -207,8 +209,13 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
207
209
|
// AskAntigravity tool: one-shot delegation to agy (ported from
|
|
208
210
|
// pi-ask-antigravity). When both extensions are installed, the bridge wins
|
|
209
211
|
// and pi-ask-antigravity registers nothing (its load-time defer guard
|
|
210
|
-
// detects this package via import.meta.resolve).
|
|
211
|
-
|
|
212
|
+
// detects this package via import.meta.resolve). Opt-out: askTool=false
|
|
213
|
+
// (config file, AGY_ASK_TOOL, /agy ask, or the picker) skips registration
|
|
214
|
+
// entirely - users who want only the provider keep a clean tool list.
|
|
215
|
+
// Note: the active flag below is set regardless of askTool, so
|
|
216
|
+
// pi-ask-antigravity keeps deferring even then: off means NO delegation
|
|
217
|
+
// tool from either package, not a fallback to pi-ask-antigravity.
|
|
218
|
+
if (loadConfig().askTool) await registerAskAntigravityTool(pi, toolModels);
|
|
212
219
|
|
|
213
220
|
// Display-only wrapper tool: the provider emits mutating agy steps as
|
|
214
221
|
// toolCalls against it (never re-executed - execute() replays the output
|
|
@@ -263,8 +270,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
263
270
|
saveConfig({ acp: { bin: status.bin, permissions: loadConfig().acp.permissions } });
|
|
264
271
|
}
|
|
265
272
|
if (status.needsLogin) {
|
|
266
|
-
const msg =
|
|
267
|
-
"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.";
|
|
273
|
+
const msg = acpLoginPending("Your next antigravity message opens the browser;");
|
|
268
274
|
if (ctx.hasUI) ctx.ui.notify(msg, "info");
|
|
269
275
|
else console.error(`[antigravity-bridge] ${msg}`);
|
|
270
276
|
}
|
|
@@ -304,7 +310,9 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
304
310
|
if (bridgeMode === "none") return; // user opted out
|
|
305
311
|
if (mcpHandle) return; // already running (reload re-fires session_start)
|
|
306
312
|
const SKIP = new Set(["AskAntigravity"]);
|
|
307
|
-
|
|
313
|
+
// pi loads project skill locations only after the project is trusted;
|
|
314
|
+
// mirror that gate. Global skill dirs are always scanned.
|
|
315
|
+
const skills: SkillLite[] = scanSkills(ctx.isProjectTrusted() ? process.cwd() : undefined);
|
|
308
316
|
const getAll = (pi as unknown as {
|
|
309
317
|
getAllTools: () => Array<{ name: string; description?: string; parameters?: object; sourceInfo?: { source?: string } }>;
|
|
310
318
|
}).getAllTools.bind(pi);
|
|
@@ -375,6 +383,14 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
375
383
|
|
|
376
384
|
// --- /agy command -----------------------------------------------------------
|
|
377
385
|
|
|
386
|
+
/** Shared body for every ACP-login-pending moment (session_start self-heal,
|
|
387
|
+
* /agy engine acp, picker): names the component so users know WHAT they are
|
|
388
|
+
* logging into, and that it is the same Google account as the agy CLI with
|
|
389
|
+
* its own token file. `browserTrigger` says when the browser opens. */
|
|
390
|
+
function acpLoginPending(browserTrigger: string): string {
|
|
391
|
+
return `One-time Google login pending for the antigravity-acp server, Google's own ACP binary and a separate component of the Antigravity suite (agy desktop, agy editor, agy cli, agy acp). ${browserTrigger} Sign in with your Antigravity subscription account: the same Google account as your agy CLI login, but its own login and token file. One-time; tokens stay on your machine and this extension never sees them.`;
|
|
392
|
+
}
|
|
393
|
+
|
|
378
394
|
interface AgyCommandCtx {
|
|
379
395
|
entries: AgyModelEntry[];
|
|
380
396
|
store: SessionStore;
|
|
@@ -387,31 +403,40 @@ interface AgyCommandCtx {
|
|
|
387
403
|
}
|
|
388
404
|
|
|
389
405
|
interface PendingConfig {
|
|
406
|
+
engine?: Engine;
|
|
390
407
|
mode?: AgyMode;
|
|
391
408
|
skipPermissions?: boolean;
|
|
392
409
|
defaultModel?: string;
|
|
393
410
|
defaultThinking?: ThinkingTier;
|
|
411
|
+
askTool?: boolean;
|
|
412
|
+
bridgeTools?: BridgeTools;
|
|
413
|
+
digest?: boolean;
|
|
414
|
+
systemPrompt?: boolean;
|
|
394
415
|
}
|
|
395
416
|
|
|
396
417
|
function statusText(ctx: AgyCommandCtx): string {
|
|
397
418
|
const config = loadConfig();
|
|
398
419
|
const source = ctx.usingFallback ? "fallback (agy models failed)" : "discovered";
|
|
399
420
|
const perm = config.skipPermissions ? "auto-approved (DANGEROUS)" : "prompt (hangs in -p)";
|
|
421
|
+
// padEnd keyed to the longest label ("AskAntigravity thinking:") so the
|
|
422
|
+
// value column stays aligned as labels grow.
|
|
423
|
+
const row = (label: string, value: string) => ` ${label.padEnd(24)} ${value}`;
|
|
400
424
|
return [
|
|
401
425
|
"Antigravity bridge",
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
426
|
+
row("engine:", `${config.engine}${config.engine === "acp" ? " (official server, opt-in)" : ""}`),
|
|
427
|
+
row("models:", `${ctx.entries.length} ${source}`),
|
|
428
|
+
row("mode:", config.mode),
|
|
429
|
+
row("permissions:", perm),
|
|
430
|
+
row("AskAntigravity tool:", config.askTool ? "on" : "off"),
|
|
431
|
+
row("AskAntigravity model:", config.defaultModel),
|
|
432
|
+
row("AskAntigravity thinking:", config.defaultThinking),
|
|
433
|
+
row("sessions:", `${ctx.store.size} bound`),
|
|
434
|
+
row("config:", CONFIG_PATH),
|
|
435
|
+
row("bridge tools:", config.bridgeTools),
|
|
436
|
+
row("digest:", config.digest ? "on" : "off"),
|
|
437
|
+
row("system prompt:", config.systemPrompt ? "on" : "off"),
|
|
413
438
|
"",
|
|
414
|
-
"Subcommands: /agy engine stream-json|acp, /agy mode plan|accept-edits, /agy permissions on|off, /agy
|
|
439
|
+
"Subcommands: /agy engine stream-json|acp, /agy mode plan|accept-edits, /agy permissions on|off, /agy ask on|off, /agy model <alias>, /agy thinking low|medium|high, /agy bridge all|mcp|none, /agy digest on|off, /agy system-prompt on|off, /agy acp-bin <path|auto>, /agy acp-auth, /agy patch-cleanup, /agy clear",
|
|
415
440
|
].join("\n");
|
|
416
441
|
}
|
|
417
442
|
|
|
@@ -419,7 +444,7 @@ function statusText(ctx: AgyCommandCtx): string {
|
|
|
419
444
|
function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
420
445
|
pi.registerCommand("agy", {
|
|
421
446
|
description:
|
|
422
|
-
"Antigravity provider: status, doctor,
|
|
447
|
+
"Antigravity provider: status, doctor, settings picker, clear sessions. Usage: /agy [status|doctor|engine stream-json|acp|mode plan|accept-edits|permissions on|off|ask on|off|model <alias>|thinking low|medium|high|bridge all|mcp|none|digest on|off|system-prompt on|off|acp-bin <path|auto>|acp-auth|patch-cleanup|clear]",
|
|
423
448
|
handler: async (args, cmdCtx: ExtensionCommandContext) => {
|
|
424
449
|
const ui = cmdCtx.ui;
|
|
425
450
|
const mode = cmdCtx.mode;
|
|
@@ -478,7 +503,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
478
503
|
saveConfig({ acp: { bin: status.bin, permissions: loadConfig().acp.permissions } });
|
|
479
504
|
ui?.notify(
|
|
480
505
|
status.needsLogin
|
|
481
|
-
?
|
|
506
|
+
? `ACP engine ready. ${acpLoginPending("Your first ACP message (after the next pi start or /reload) opens the browser;")}`
|
|
482
507
|
: `ACP engine ready (auth: ${status.auth}). Takes effect on the next pi start (or /reload).`,
|
|
483
508
|
"info",
|
|
484
509
|
);
|
|
@@ -487,6 +512,24 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
487
512
|
}
|
|
488
513
|
return;
|
|
489
514
|
}
|
|
515
|
+
if (sub === "acp-bin") {
|
|
516
|
+
const rest = (args ?? "").trim().split(/\s+/).slice(1).join(" ");
|
|
517
|
+
if (rest.length > 0) {
|
|
518
|
+
// Only the keyword compares case-insensitively; the path keeps its case.
|
|
519
|
+
const bin = rest.toLowerCase() === "auto" ? "" : rest.replace(/^~(?=\/|$)/, os.homedir());
|
|
520
|
+
saveConfig({ acp: { bin, permissions: loadConfig().acp.permissions } });
|
|
521
|
+
ui?.notify(
|
|
522
|
+
bin
|
|
523
|
+
? `acp.bin set to ${bin}. The next ACP turn (re)connects with it.`
|
|
524
|
+
: "acp.bin cleared. Auto-setup (or AGY_ACP_BIN) picks the binary on the next ACP turn.",
|
|
525
|
+
"info",
|
|
526
|
+
);
|
|
527
|
+
} else {
|
|
528
|
+
const cur = loadConfig().acp.bin;
|
|
529
|
+
ui?.notify(`acp.bin: ${cur || "(auto: setup installs, or AGY_ACP_BIN)"}\nusage: /agy acp-bin <path|auto>`, "info");
|
|
530
|
+
}
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
490
533
|
if (sub === "acp-auth") {
|
|
491
534
|
ui?.notify(
|
|
492
535
|
[
|
|
@@ -575,12 +618,40 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
575
618
|
}
|
|
576
619
|
return;
|
|
577
620
|
}
|
|
621
|
+
if (sub === "bridge") {
|
|
622
|
+
if (val === "all" || val === "mcp" || val === "none") {
|
|
623
|
+
const next = saveConfig({ bridgeTools: val });
|
|
624
|
+
ui?.notify(
|
|
625
|
+
next.bridgeTools === "none"
|
|
626
|
+
? "bridge off. The MCP tool bridge will not start on the next pi start (or /reload)."
|
|
627
|
+
: `bridge tools set to ${next.bridgeTools}. The catalog rebuilds on the next pi start (or /reload).`,
|
|
628
|
+
"info",
|
|
629
|
+
);
|
|
630
|
+
} else {
|
|
631
|
+
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");
|
|
632
|
+
}
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
578
635
|
if (sub === "model") {
|
|
579
636
|
if (val && val.length > 0) {
|
|
580
637
|
const next = saveConfig({ defaultModel: val });
|
|
581
|
-
ui?.notify(`
|
|
638
|
+
ui?.notify(`AskAntigravity default model set to ${next.defaultModel}`, "info");
|
|
639
|
+
} else {
|
|
640
|
+
ui?.notify(`AskAntigravity model: ${loadConfig().defaultModel} (fallback; callers may override per call)\nusage: /agy model flash|pro|gemini|<exact>`, "info");
|
|
641
|
+
}
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (sub === "ask") {
|
|
645
|
+
if (val === "on" || val === "off") {
|
|
646
|
+
const next = saveConfig({ askTool: val === "on" });
|
|
647
|
+
ui?.notify(
|
|
648
|
+
next.askTool
|
|
649
|
+
? "AskAntigravity tool on. Registered on the next pi start (or /reload)."
|
|
650
|
+
: "AskAntigravity tool off. It is removed from the model's tool list on the next pi start (or /reload). Provider and models stay.",
|
|
651
|
+
"info",
|
|
652
|
+
);
|
|
582
653
|
} else {
|
|
583
|
-
ui?.notify(`tool
|
|
654
|
+
ui?.notify(`AskAntigravity tool: ${loadConfig().askTool ? "on" : "off"}\nusage: /agy ask on|off`, "info");
|
|
584
655
|
}
|
|
585
656
|
return;
|
|
586
657
|
}
|
|
@@ -616,9 +687,9 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
616
687
|
if (sub === "thinking") {
|
|
617
688
|
if (val === "low" || val === "medium" || val === "high") {
|
|
618
689
|
const next = saveConfig({ defaultThinking: val as ThinkingTier });
|
|
619
|
-
ui?.notify(`
|
|
690
|
+
ui?.notify(`AskAntigravity default thinking set to ${next.defaultThinking}`, "info");
|
|
620
691
|
} else {
|
|
621
|
-
ui?.notify(`
|
|
692
|
+
ui?.notify(`AskAntigravity thinking: ${loadConfig().defaultThinking} (fallback; callers may override per call)\nusage: /agy thinking low|medium|high`, "info");
|
|
622
693
|
}
|
|
623
694
|
return;
|
|
624
695
|
}
|
|
@@ -640,12 +711,23 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
640
711
|
});
|
|
641
712
|
}
|
|
642
713
|
|
|
643
|
-
/** Interactive settings picker (TUI only). Rows:
|
|
714
|
+
/** Interactive settings picker (TUI only). Rows: the full runtime config
|
|
715
|
+
* surface (engine, mode, permissions, model, thinking, bridge, digest,
|
|
716
|
+
* system prompt). Picking acp runs the same self-service setup as
|
|
717
|
+
* `/agy engine acp` (binary + auth) right after the save. */
|
|
644
718
|
async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promise<void> {
|
|
645
719
|
const config = loadConfig();
|
|
646
720
|
const pending: PendingConfig = {};
|
|
647
721
|
|
|
648
722
|
const items: SettingItem[] = [
|
|
723
|
+
{
|
|
724
|
+
id: "engine",
|
|
725
|
+
label: "Turn engine",
|
|
726
|
+
description:
|
|
727
|
+
"stream-json: the streaming engine (default, supported). acp: Google's official ACP server (experimental, opt-in). Switch takes effect on the next pi start (or /reload); acp also runs binary + auth setup on save.",
|
|
728
|
+
currentValue: config.engine,
|
|
729
|
+
values: ["stream-json", "acp"],
|
|
730
|
+
},
|
|
649
731
|
{
|
|
650
732
|
id: "mode",
|
|
651
733
|
label: "Execution mode",
|
|
@@ -662,22 +744,54 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
|
|
|
662
744
|
currentValue: config.skipPermissions ? "auto-approved" : "prompt",
|
|
663
745
|
values: ["auto-approved", "prompt"],
|
|
664
746
|
},
|
|
747
|
+
{
|
|
748
|
+
id: "ask",
|
|
749
|
+
label: "AskAntigravity tool",
|
|
750
|
+
description:
|
|
751
|
+
"Register the AskAntigravity one-shot delegation tool. off removes it from the model's tool list (provider and models stay, even with the separate pi-ask-antigravity package installed). Takes effect on the next pi start (or /reload).",
|
|
752
|
+
currentValue: config.askTool ? "on" : "off",
|
|
753
|
+
values: ["on", "off"],
|
|
754
|
+
},
|
|
665
755
|
{
|
|
666
756
|
id: "model",
|
|
667
|
-
label: "
|
|
757
|
+
label: "AskAntigravity model",
|
|
668
758
|
description:
|
|
669
|
-
"
|
|
759
|
+
"AskAntigravity one-shot delegation tool: model used when its caller omits the model param. Callers may override per call; this is only the fallback. flash/pro/gemini, or an exact id. Does not affect the provider model you pick in /model.",
|
|
670
760
|
currentValue: config.defaultModel,
|
|
671
761
|
values: ["flash", "pro", "gemini"],
|
|
672
762
|
},
|
|
673
763
|
{
|
|
674
764
|
id: "thinking",
|
|
675
|
-
label: "
|
|
765
|
+
label: "AskAntigravity thinking",
|
|
676
766
|
description:
|
|
677
|
-
"
|
|
767
|
+
"AskAntigravity one-shot delegation tool: thinking tier used when the call names none. Callers may override per call; this is only the fallback. Pro has no Medium; it falls back to nearest.",
|
|
678
768
|
currentValue: config.defaultThinking,
|
|
679
769
|
values: ["low", "medium", "high"],
|
|
680
770
|
},
|
|
771
|
+
{
|
|
772
|
+
id: "bridge",
|
|
773
|
+
label: "Bridge tools",
|
|
774
|
+
description:
|
|
775
|
+
"Which pi tools the MCP bridge exposes to agy. all: every non-builtin tool (default). mcp: pi-mcp-adapter tools + skills. none: bridge off.",
|
|
776
|
+
currentValue: config.bridgeTools,
|
|
777
|
+
values: ["all", "mcp", "none"],
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
id: "digest",
|
|
781
|
+
label: "Context digest",
|
|
782
|
+
description:
|
|
783
|
+
"Inject a delta of pi-side context into each agy prompt. Defeats agy's prompt cache (~25-30k tokens re-billed per turn).",
|
|
784
|
+
currentValue: config.digest ? "on" : "off",
|
|
785
|
+
values: ["on", "off"],
|
|
786
|
+
},
|
|
787
|
+
{
|
|
788
|
+
id: "system-prompt",
|
|
789
|
+
label: "System prompt",
|
|
790
|
+
description:
|
|
791
|
+
"Prepend pi's system prompt (incl. AGENTS.md files) to the first prompt of each new agy conversation.",
|
|
792
|
+
currentValue: config.systemPrompt ? "on" : "off",
|
|
793
|
+
values: ["on", "off"],
|
|
794
|
+
},
|
|
681
795
|
];
|
|
682
796
|
|
|
683
797
|
await ui.custom((tui, theme, _kb, done) => {
|
|
@@ -690,7 +804,9 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
|
|
|
690
804
|
Math.min(items.length + 4, 15),
|
|
691
805
|
getSettingsListTheme(),
|
|
692
806
|
(id, newValue) => {
|
|
693
|
-
if (id === "
|
|
807
|
+
if (id === "engine") {
|
|
808
|
+
pending.engine = newValue as Engine;
|
|
809
|
+
} else if (id === "mode") {
|
|
694
810
|
pending.mode = newValue as AgyMode;
|
|
695
811
|
} else if (id === "permissions") {
|
|
696
812
|
pending.skipPermissions = newValue === "auto-approved";
|
|
@@ -698,6 +814,14 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
|
|
|
698
814
|
pending.defaultModel = newValue;
|
|
699
815
|
} else if (id === "thinking") {
|
|
700
816
|
pending.defaultThinking = newValue as ThinkingTier;
|
|
817
|
+
} else if (id === "ask") {
|
|
818
|
+
pending.askTool = newValue === "on";
|
|
819
|
+
} else if (id === "bridge") {
|
|
820
|
+
pending.bridgeTools = newValue as BridgeTools;
|
|
821
|
+
} else if (id === "digest") {
|
|
822
|
+
pending.digest = newValue === "on";
|
|
823
|
+
} else if (id === "system-prompt") {
|
|
824
|
+
pending.systemPrompt = newValue === "on";
|
|
701
825
|
}
|
|
702
826
|
},
|
|
703
827
|
() => done(undefined),
|
|
@@ -714,37 +838,65 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
|
|
|
714
838
|
};
|
|
715
839
|
});
|
|
716
840
|
|
|
717
|
-
if (
|
|
718
|
-
pending.mode === undefined &&
|
|
719
|
-
pending.skipPermissions === undefined &&
|
|
720
|
-
pending.defaultModel === undefined &&
|
|
721
|
-
pending.defaultThinking === undefined
|
|
722
|
-
)
|
|
723
|
-
return;
|
|
841
|
+
if (Object.keys(pending).length === 0) return;
|
|
724
842
|
|
|
725
|
-
|
|
726
|
-
|
|
843
|
+
// The engine latches at load: a plan mode active while the effective
|
|
844
|
+
// engine is (or will be) acp would silently run non-plan. ACP has no
|
|
845
|
+
// review-only mode (RC01); refuse the combination. Both sides fall back
|
|
846
|
+
// to the on-disk config so a fresh engine pick over a saved plan (and
|
|
847
|
+
// vice versa) is caught too.
|
|
848
|
+
const nextEngine = pending.engine ?? ctx.engine;
|
|
849
|
+
const nextMode = pending.mode ?? config.mode;
|
|
850
|
+
if (nextMode === "plan" && nextEngine === "acp") {
|
|
851
|
+
ui.notify(
|
|
852
|
+
"plan + acp is not supported (RC01): the ACP engine has no review-only mode. Save mode accept-edits or pick the stream-json engine first.",
|
|
853
|
+
"warning",
|
|
854
|
+
);
|
|
727
855
|
return;
|
|
728
856
|
}
|
|
729
857
|
|
|
730
858
|
try {
|
|
731
859
|
const next = saveConfig(pending);
|
|
732
860
|
const changed = [
|
|
861
|
+
pending.engine !== undefined ? `engine=${next.engine}` : null,
|
|
733
862
|
pending.mode ? `mode=${next.mode}` : null,
|
|
734
863
|
pending.skipPermissions !== undefined
|
|
735
864
|
? `permissions=${next.skipPermissions ? "auto-approved" : "prompt"}`
|
|
736
865
|
: null,
|
|
737
|
-
pending.
|
|
738
|
-
pending.
|
|
866
|
+
pending.askTool !== undefined ? `AskAntigravity tool=${next.askTool ? "on" : "off"}` : null,
|
|
867
|
+
pending.defaultModel !== undefined ? `AskAntigravity model=${next.defaultModel}` : null,
|
|
868
|
+
pending.defaultThinking !== undefined ? `AskAntigravity thinking=${next.defaultThinking}` : null,
|
|
869
|
+
pending.bridgeTools !== undefined ? `bridge=${next.bridgeTools}` : null,
|
|
870
|
+
pending.digest !== undefined ? `digest=${next.digest ? "on" : "off"}` : null,
|
|
871
|
+
pending.systemPrompt !== undefined ? `system-prompt=${next.systemPrompt ? "on" : "off"}` : null,
|
|
739
872
|
]
|
|
740
873
|
.filter(Boolean)
|
|
741
874
|
.join(", ");
|
|
742
875
|
ui.notify(`Saved: ${changed}`, "info");
|
|
876
|
+
if (pending.engine === "acp") {
|
|
877
|
+
// Same flow as /agy engine acp: persist the switch, then prepare
|
|
878
|
+
// the server so the restart just works.
|
|
879
|
+
ui.notify("Preparing the ACP server (binary + auth)…", "info");
|
|
880
|
+
const status = await ensureAcpReady({
|
|
881
|
+
configBin: loadConfig().acp.bin,
|
|
882
|
+
onProgress: (m) => ui.notify(m, "info"),
|
|
883
|
+
});
|
|
884
|
+
if (status.ok) {
|
|
885
|
+
saveConfig({ acp: { bin: status.bin, permissions: loadConfig().acp.permissions } });
|
|
886
|
+
ui.notify(
|
|
887
|
+
status.needsLogin
|
|
888
|
+
? `ACP server ready. ${acpLoginPending("Your first ACP message (after the restart) opens the browser;")}`
|
|
889
|
+
: `ACP server ready (auth: ${status.auth}).`,
|
|
890
|
+
"info",
|
|
891
|
+
);
|
|
892
|
+
} else {
|
|
893
|
+
ui.notify(`ACP auto-setup failed (${status.error}).\n${status.manual}`, "warning");
|
|
894
|
+
}
|
|
895
|
+
}
|
|
743
896
|
} catch (err) {
|
|
744
897
|
ui.notify(
|
|
745
898
|
`Failed to save config: ${err instanceof Error ? err.message : String(err)}`,
|
|
746
899
|
"error",
|
|
747
900
|
);
|
|
748
901
|
}
|
|
749
|
-
void ctx;
|
|
750
902
|
}
|
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.3",
|
|
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",
|
package/src/config.ts
CHANGED
|
@@ -54,13 +54,20 @@ export interface AgyConfig {
|
|
|
54
54
|
defaultModel: string;
|
|
55
55
|
/** AskAntigravity tool: default thinking tier when the alias names none. */
|
|
56
56
|
defaultThinking: ThinkingTier;
|
|
57
|
+
/** Register the AskAntigravity one-shot delegation tool. Default on.
|
|
58
|
+
* off removes the tool entirely for users who want only the provider
|
|
59
|
+
* and models (no delegation tool in the model's window context).
|
|
60
|
+
* Takes effect at pi start (or /reload). */
|
|
61
|
+
askTool: boolean;
|
|
57
62
|
/** Set after the one-time notice about a leftover legacy invokeTool patch
|
|
58
63
|
* on the installed pi. The notice never repeats; /agy patch-cleanup is
|
|
59
64
|
* always available. */
|
|
60
65
|
patchCleanupNotified?: boolean;
|
|
61
66
|
/** Which pi tools the MCP bridge exposes to agy: "none" (bridge off),
|
|
62
|
-
* "
|
|
63
|
-
*
|
|
67
|
+
* "all" (every registered non-builtin tool incl. other Ask* delegations;
|
|
68
|
+
* default - users expect the bridge working out of the box, and the
|
|
69
|
+
* "mcp" surface serves an empty catalog on installs without
|
|
70
|
+
* pi-mcp-adapter), "mcp" (pi-mcp-adapter tools + skills bridge only). */
|
|
64
71
|
bridgeTools: BridgeTools;
|
|
65
72
|
/** Inject a delta digest of pi-side context (compaction summaries, turns
|
|
66
73
|
* handled by other providers or pi's own tools) into each agy prompt.
|
|
@@ -94,7 +101,8 @@ const DEFAULTS: AgyConfig = {
|
|
|
94
101
|
skipPermissions: true,
|
|
95
102
|
defaultModel: "flash",
|
|
96
103
|
defaultThinking: "medium",
|
|
97
|
-
|
|
104
|
+
askTool: true,
|
|
105
|
+
bridgeTools: "all",
|
|
98
106
|
digest: false,
|
|
99
107
|
systemPrompt: true,
|
|
100
108
|
acp: { bin: "", permissions: "auto" },
|
|
@@ -147,9 +155,15 @@ export function loadConfig(configPath: string = CONFIG_PATH): AgyConfig {
|
|
|
147
155
|
const defaultThinking: ThinkingTier =
|
|
148
156
|
thinkRaw === "low" || thinkRaw === "high" ? thinkRaw : "medium";
|
|
149
157
|
|
|
158
|
+
const askTool = process.env.AGY_ASK_TOOL !== undefined
|
|
159
|
+
? ["1", "true", "on"].includes(process.env.AGY_ASK_TOOL.toLowerCase())
|
|
160
|
+
: file.askTool ?? DEFAULTS.askTool;
|
|
161
|
+
|
|
150
162
|
const bridgeRaw = (process.env.AGY_BRIDGE_TOOLS ?? file.bridgeTools ?? DEFAULTS.bridgeTools).toLowerCase();
|
|
151
163
|
const bridgeTools: BridgeTools =
|
|
152
|
-
bridgeRaw === "none" || bridgeRaw === "all"
|
|
164
|
+
bridgeRaw === "none" || bridgeRaw === "all" || bridgeRaw === "mcp"
|
|
165
|
+
? bridgeRaw
|
|
166
|
+
: DEFAULTS.bridgeTools;
|
|
153
167
|
|
|
154
168
|
const digest = process.env.AGY_DIGEST !== undefined
|
|
155
169
|
? ["1", "true", "on"].includes(process.env.AGY_DIGEST.toLowerCase())
|
|
@@ -175,6 +189,7 @@ export function loadConfig(configPath: string = CONFIG_PATH): AgyConfig {
|
|
|
175
189
|
skipPermissions,
|
|
176
190
|
defaultModel,
|
|
177
191
|
defaultThinking,
|
|
192
|
+
askTool,
|
|
178
193
|
bridgeTools,
|
|
179
194
|
digest,
|
|
180
195
|
systemPrompt,
|
package/src/skills.ts
CHANGED
|
@@ -22,7 +22,9 @@ export interface SkillLite {
|
|
|
22
22
|
dir: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/** Parse `name:` / `description:` out of SKILL.md frontmatter.
|
|
25
|
+
/** Parse `name:` / `description:` out of SKILL.md frontmatter. Deliberately
|
|
26
|
+
* line-based, not YAML: multi-line block scalars (`description: >-`) keep
|
|
27
|
+
* only their first line. */
|
|
26
28
|
function parseFrontmatter(raw: string): { name?: string; description?: string } {
|
|
27
29
|
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
28
30
|
if (!m) return {};
|
|
@@ -36,48 +38,123 @@ function parseFrontmatter(raw: string): { name?: string; description?: string }
|
|
|
36
38
|
return out;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
// The rest of this module mirrors pi's own skill discovery (pi docs
|
|
42
|
+
// "docs/skills.md - Locations") so the bridge's activate_skill catalog
|
|
43
|
+
// matches the [Skills] list pi shows in its system prompt:
|
|
44
|
+
//
|
|
45
|
+
// - Global: ~/.pi/agent/skills (pi style) and ~/.agents/skills (agents style,
|
|
46
|
+
// the cross-harness standard).
|
|
47
|
+
// - Project: .pi/skills (pi style) and .agents/skills in cwd + ancestors up
|
|
48
|
+
// to the git repo root (agents style).
|
|
49
|
+
// - Both styles discover every directory holding a SKILL.md, recursively;
|
|
50
|
+
// grouping folders (no SKILL.md) are descended into and a skill's contents
|
|
51
|
+
// are freeform (no descent past its SKILL.md).
|
|
52
|
+
// - pi style also treats root .md files as individual skills; agents style
|
|
53
|
+
// ignores root .md but picks up frontmatter .md files inside grouping
|
|
54
|
+
// folders.
|
|
55
|
+
// - Skills without a description are not loaded (same as pi).
|
|
56
|
+
// - Name collisions keep the first skill found, pi global dirs before
|
|
57
|
+
// project dirs.
|
|
58
|
+
|
|
59
|
+
export type SkillStyle = "pi" | "agents";
|
|
60
|
+
|
|
61
|
+
export interface SkillLocation {
|
|
62
|
+
dir: string;
|
|
63
|
+
style: SkillStyle;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const MAX_DEPTH = 16; // symlink-loop insurance; real catalogs nest 1-2 levels
|
|
67
|
+
|
|
68
|
+
/** Ordered skill locations for a session: pi global dirs, then project dirs
|
|
69
|
+
* (project `.agents/skills` walks cwd ancestors up to the git root). */
|
|
70
|
+
export function skillLocations(projectDir?: string, home: string = os.homedir()): SkillLocation[] {
|
|
71
|
+
const out: SkillLocation[] = [
|
|
72
|
+
{ dir: path.join(home, ".pi", "agent", "skills"), style: "pi" },
|
|
73
|
+
{ dir: path.join(home, ".agents", "skills"), style: "agents" },
|
|
74
|
+
];
|
|
75
|
+
if (!projectDir) return out;
|
|
76
|
+
out.push({ dir: path.join(projectDir, ".pi", "skills"), style: "pi" });
|
|
77
|
+
let dir = path.resolve(projectDir);
|
|
78
|
+
for (;;) {
|
|
79
|
+
out.push({ dir: path.join(dir, ".agents", "skills"), style: "agents" });
|
|
80
|
+
if (path.dirname(dir) === dir || fs.existsSync(path.join(dir, ".git"))) break;
|
|
81
|
+
dir = path.dirname(dir);
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readSkillFile(file: string, fallbackName: string): SkillLite | null {
|
|
87
|
+
let raw = "";
|
|
41
88
|
try {
|
|
42
|
-
|
|
89
|
+
// Strip a UTF-8 BOM so editors that add one do not break the `---` anchor.
|
|
90
|
+
raw = fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "");
|
|
43
91
|
} catch {
|
|
44
|
-
return
|
|
92
|
+
return null;
|
|
45
93
|
}
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
94
|
+
const fm = parseFrontmatter(raw);
|
|
95
|
+
const description = (fm.description ?? "").trim();
|
|
96
|
+
// pi does not load skills without a description.
|
|
97
|
+
if (!description) return null;
|
|
98
|
+
return {
|
|
99
|
+
name: fm.name?.trim() || fallbackName,
|
|
100
|
+
description: description.replace(/\s+/g, " ").slice(0, 160),
|
|
101
|
+
filePath: file,
|
|
102
|
+
dir: path.dirname(file),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function scanLocation(loc: SkillLocation): SkillLite[] {
|
|
107
|
+
const walk = (dir: string, depth: number): SkillLite[] => {
|
|
108
|
+
if (depth > MAX_DEPTH) return [];
|
|
109
|
+
let entries: fs.Dirent[];
|
|
50
110
|
try {
|
|
51
|
-
|
|
111
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
52
112
|
} catch {
|
|
53
|
-
|
|
113
|
+
return [];
|
|
54
114
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
continue;
|
|
115
|
+
const found: SkillLite[] = [];
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
// Hidden entries are harness internals (e.g. ~/.agents/skills/.system);
|
|
118
|
+
// pi's own [Skills] list never includes them.
|
|
119
|
+
if (entry.name.startsWith(".")) continue;
|
|
120
|
+
const full = path.join(dir, entry.name);
|
|
121
|
+
let isDir: boolean;
|
|
122
|
+
try {
|
|
123
|
+
isDir = entry.isDirectory() || (entry.isSymbolicLink() && fs.statSync(full).isDirectory());
|
|
124
|
+
} catch {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (isDir) {
|
|
128
|
+
if (fs.existsSync(path.join(full, "SKILL.md"))) {
|
|
129
|
+
const skill = readSkillFile(path.join(full, "SKILL.md"), entry.name);
|
|
130
|
+
if (skill) found.push(skill);
|
|
131
|
+
} else {
|
|
132
|
+
found.push(...walk(full, depth + 1));
|
|
133
|
+
}
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (!entry.name.endsWith(".md") || entry.name === "SKILL.md") continue;
|
|
137
|
+
const standalone = loc.style === "pi" ? depth === 0 : depth > 0;
|
|
138
|
+
if (!standalone) continue;
|
|
139
|
+
const skill = readSkillFile(full, entry.name.replace(/\.md$/, ""));
|
|
140
|
+
if (skill) found.push(skill);
|
|
60
141
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
description: (fm.description ?? "").replace(/\s+/g, " ").slice(0, 160),
|
|
65
|
-
filePath: skillFile,
|
|
66
|
-
dir: skillDir,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
return found;
|
|
142
|
+
return found;
|
|
143
|
+
};
|
|
144
|
+
return walk(loc.dir, 0);
|
|
70
145
|
}
|
|
71
146
|
|
|
72
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
147
|
+
/** pi-faithful skill catalog: every skill pi itself would load, deduped by
|
|
148
|
+
* name (first wins). `home` is injectable for tests. */
|
|
149
|
+
export function scanSkills(projectDir?: string, home: string = os.homedir()): SkillLite[] {
|
|
75
150
|
const seen = new Set<string>();
|
|
76
151
|
const out: SkillLite[] = [];
|
|
77
|
-
for (const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
152
|
+
for (const loc of skillLocations(projectDir, home)) {
|
|
153
|
+
for (const skill of scanLocation(loc)) {
|
|
154
|
+
if (seen.has(skill.name)) continue;
|
|
155
|
+
seen.add(skill.name);
|
|
156
|
+
out.push(skill);
|
|
157
|
+
}
|
|
81
158
|
}
|
|
82
159
|
return out;
|
|
83
160
|
}
|