@estebanforge/pi-antigravity-bridge 1.7.3 → 1.7.5
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 +27 -0
- package/README.md +6 -8
- package/extensions/index.ts +279 -14
- package/package.json +1 -1
- package/src/acp/driver.ts +3 -3
- package/src/agents.ts +64 -0
- package/src/agy-paths.ts +33 -0
- package/src/agy-version.ts +137 -0
- package/src/artifacts-ui.ts +86 -0
- package/src/artifacts.ts +144 -0
- package/src/ask-tool.ts +38 -7
- package/src/config.ts +10 -0
- package/src/driver-types.ts +8 -2
- package/src/driver.ts +68 -16
- package/src/models.ts +44 -9
- package/src/native-tools.ts +1 -1
- package/src/parked-turn.ts +125 -0
- package/src/provider.ts +14 -1
- package/src/skills.ts +0 -1
- package/src/stream-events.ts +1 -2
- package/src/subagent-roster.ts +245 -0
- package/src/tasks-ui.ts +88 -0
- package/src/tasks.ts +174 -0
- package/src/usage.ts +174 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.7.5] - 2026-09-25
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Exit 0 with no output is a failure, not a silent success.** A headless `agy -p` run can end cleanly with nothing on stdout when a delegated tool call hits a permission gate the headless run cannot prompt for. The plan-mode fix (withholding the skip flag in plan) makes the command gate the common case: the call is auto-denied, the reason goes only to stderr, agy exits 0, and the execute path fell through to success, returning just the conversation footer. The delegating model read that as "returned only a conversationId, zero output, no error" (silent-failure handoff, 2026-09-25). An empty answer now returns a loud failure note carrying the redacted stderr plus the remedy (allow-list the command under `permissions.allow` in the CLI settings, or rerun outside plan mode), the ask-end log marks the run warn with an `empty` flag so error-only default logging records what the chat saw, and a new `details.empty` marker flips the TUI result row to its error glyph instead of a green checkmark. Three new tests pin both stderr shapes and the glyph flip through the fake-binary stub.
|
|
10
|
+
|
|
11
|
+
## [1.7.4] - 2026-09-25
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`/agy agent <name|off>` — select a custom agy agent for stream-json turns.** The agent list comes from a one-shot `agy agent` spawn through the same bounded raw-spawn helper as the model catalog (8 s watchdog, 1 MiB cap, fail-closed to an empty list), parsed line-oriented: banner and help noise skipped, first whitespace token per line taken, filesystem-safe names validated, duplicates dropped. The choice persists in config with an `AGY_AGENT` env override, rides `--agent` on stream-json spawns, and recycles the process when it changes. ACP has no protocol slot for agent selection, so the command refuses it there with a stream-json-only message instead of silently ignoring the knob.
|
|
16
|
+
- **`/agy subagents` — live subagent roster.** agy's subagent steps were invisible from pi: spawn, message, and manage activity now folds from the engine-agnostic activity stream into an in-memory roster (spawn tools open entries, `send_message` counts messages, `manage_subagents` kills mark entries killed, done/error closes them). Entries key by tool-call id on ACP and step id on stream-json, with name, status, duration, message count, detail, and last error rendered on demand. Works on both engines; deliberately never persisted — agy's conversation database is the durable record, and telemetry must never be able to fail a turn.
|
|
17
|
+
- **`/agy quota` — subscription quota view.** One-shot `agy --print /usage` fetch rendered as bars per window, 5h ranked before weekly, with reset times. The fetch carries its own 30 s budget (the live call answers in ~10 s) and the wrapper watchdog is padded 5 s past agy's own `--print-timeout` so agy's clean self-timeout always wins the race over a SIGKILL. Quota reflects the CLI's Google account identity, so the reading is equally true under both engines; a CLI-less ACP-only install gets a fail-closed pointer to `/agy doctor` rather than a raw spawn error.
|
|
18
|
+
- **`/agy artifacts [open <n|name>]` — browse the conversation brain dir.** Non-recursive scan of the conversation root, `.tempmediaStorage`, and `.user_uploaded`, deduped by canonical path, newest first, with kind/size/last-write shown. `open` resolves by index, exact name, or unique substring and launches the OS opener detached. Path containment canonicalizes the scan root once and re-realpaths every entry before opening, so symlinked ancestors (macOS `/var` → `/private/var`, symlinked homes) cannot silently empty the listing — the first containment gate compared raw paths and did exactly that on such machines.
|
|
19
|
+
- **Parked-answer recovery on stream-json deadline guards.** When the stall or total-timeout guard fires, the driver now kills the child first (bumping the generation so a racing exit handler cannot settle the turn ERROR mid-probe), then probes the conversation's brain transcript: if the NEWEST step written during the turn is a clean, tool-free DONE MODEL response, the turn settles OK with the withheld answer instead of discarding finished work. The newest-step rule is the load-bearing part — a corpus sweep found 96.6% of PLANNER_RESPONSE transcript steps carry tool_calls; they are intermediate plan-and-act steps whose content is status text, not answers, and an older scan that accepted any matching step could settle a turn with "Running test..." as the answer. ACP reports turn completion itself, so the probe is stream-json-only by construction.
|
|
20
|
+
- **`/agy tasks [tail <id>]` — read-only background-task dashboard.** Scans the conversation's task logs (newest first, 128 cap, missing dirs degrade to an empty list) and marks a task ACTIVE while any process holds its log open, via one batched `lsof -nP -Fpn` call — a reader counts too, so the state is a watch, not a fact. Watch-only by design: modern agy pipes task output through itself, making log ownership unprovable, so nothing is ever stopped from here and a missing lsof shows `liveness?` instead of a guess. `tail <id>` resolves against the scanned list and prints the last 40 lines.
|
|
21
|
+
- **Agy CLI version awareness.** A warn-once session toast fires when the CLI reports older than the 1.1.22 stream-json floor; development builds pass, unparseable output classifies as invalid, and a failed spawn maps to a distinct unavailable status rather than a fake verdict. `/agy doctor` gained a CLI version line, labeled "(optional here; AskAntigravity delegations use the CLI)" on ACP-only installs because the CLI matters on both engines. Warn-only by design: a hard spawn gate would block turns on a slow `--version` in odd environments, and an old agy already fails turns with parse noise the warning explains. ACP server versions stay display-only — `agy_acp_server_1.1.1` and CLI `1.2.10` are unrelated streams, and no floor is enforced against the server until its versioning is understood.
|
|
22
|
+
|
|
23
|
+
### Removed
|
|
24
|
+
|
|
25
|
+
- **`/agy status`.** It was never a real subcommand — the old argument guard let the literal name fall through to the settings picker while the printed summary read as a broken second status surface. `/agy doctor` is now the single status surface: bridge state, driver counters, CLI and ACP versions, lifecycle, plus the settings rows folded in (nothing duplicated — rows doctor already rendered stay out of the shared block). Typing the retired name gets a one-line pointer to doctor; bare `/agy` keeps its settings picker.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **The agy models discovery spawn can no longer balloon memory.** It accumulated child stdout with no cap, so a runaway agy could exhaust the whole pi session. Output is now capped at 1 MiB (decoded-length proxy, documented); past the cap the child is SIGKILLed and the result fails closed to empty, which engages the baked-in fallback catalog — and a capped empty result never overwrites a good on-disk cache.
|
|
30
|
+
- **Plan mode never receives `--dangerously-skip-permissions`.** agy's plan-mode no-write gate IS a permission request ("File creation requires plan approval"), and the skip flag auto-approves every permission request — so passing the flag alongside `--mode plan` silently turned review-only into full write access inside the `--add-dir` grant. Probed both directions on 2026-09-25: with the flag a plan run wrote files; without it, file-write and run_command attempts end exit 0 in ~20-30 s with a "confirm plan" message — fail-visible, never the upstream #318 prompt-hang. Both spawn surfaces (the AskAntigravity one-shot and the stream-json driver) now withhold the flag in plan mode through one shared helper; the recycle profile stores the effective value and compares effective-vs-effective so plan sessions keep process reuse. README and ENGINES.md now state the enforcement instead of the aspirational claim.
|
|
31
|
+
|
|
5
32
|
## [1.7.3] - 2026-09-24
|
|
6
33
|
|
|
7
34
|
### Fixed
|
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ While agy is the active model it normally cannot see pi's universe of extensions
|
|
|
40
40
|
|
|
41
41
|
The bridge starts a localhost MCP server inside pi's process. The exposed catalog is computed live from pi's active tools on every `tools/list` and re-checked immediately before every `tools/call`: the `bridgeTools` mode, the session's `/agy tools` hidden set, and the bridge's own internal tools (`AskAntigravity`, the display-only `antigravity` wrapper, `activate_skill`, `bridge_poll_result`, the `agy_web_search`/`agy_read_url` wrappers) are filtered each time, and a call for anything outside the fresh set is rejected - a cached MCP catalog is not authorization. A call that passes routes into pi's own tool loop via the round-trip described below. Discovery has two layers. The provider's agy gets 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 driver passes that dir as an extra `--add-dir` when it spawns agy. A per-pid entry (`pi-bridge-<pid>`) is also registered in the user's global agy config (`~/.gemini/config/mcp_config.json`; foreign servers preserved, stale entries swept at start), so agy builds that read only the global config still find the bridge.
|
|
42
42
|
|
|
43
|
-
**
|
|
43
|
+
**Upstream pi APIs only.** 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.
|
|
44
44
|
|
|
45
45
|
**Long calls don't die.** agy's MCP client abandons a `tools/call` request at a flat ~180s, which used to kill any pi tool that ran longer (a long peer review, a build, a commit preview waiting for you). A call still running after ~20 seconds now settles its HTTP request with a `STILL RUNNING` answer carrying a `callId` while pi keeps executing; agy fetches the result through the bridge-local `bridge_poll_result` tool and polls until it lands. Escalated calls get their own 30-minute budget, so human-gated tools can take as long as the human takes. Fast calls stay fully synchronous and never see any of this. If a park does fail (abort, timeout, recycle), the late result is re-routed to agy as a follow-up prompt in the same conversation instead of being lost.
|
|
46
46
|
|
|
@@ -79,7 +79,7 @@ Full mechanics, configuration, and a sample gate extension: [docs/APPROVAL-GATE.
|
|
|
79
79
|
|
|
80
80
|
## Install
|
|
81
81
|
|
|
82
|
-
>
|
|
82
|
+
> The bridge runs on pi's public APIs; the extension never edits your pi install.
|
|
83
83
|
|
|
84
84
|
Install with pi's package manager:
|
|
85
85
|
|
|
@@ -132,9 +132,8 @@ The `activate_skill` catalog mirrors pi's directory-based skill discovery: the t
|
|
|
132
132
|
`/agy` configures the provider at runtime. Settings persist to `~/.pi/agent/antigravity-bridge/config.json` and take effect on the next turn.
|
|
133
133
|
|
|
134
134
|
```
|
|
135
|
-
/agy status, or open the full settings picker (TUI)
|
|
136
|
-
/agy
|
|
137
|
-
/agy doctor bridge state, driver counters, bridge port, last lifecycle events, log dir
|
|
135
|
+
/agy status summary, or open the full settings picker (TUI)
|
|
136
|
+
/agy doctor single status surface: bridge state, driver counters, versions, acp state, lifecycle, settings, log dir
|
|
138
137
|
/agy auth run the antigravity-acp sign-in now (engine acp): opens the Google login in your browser, shows the URL when no browser opens
|
|
139
138
|
/agy mode plan review-only: agy plans but writes nothing
|
|
140
139
|
/agy mode accept-edits agy applies edits directly (default)
|
|
@@ -150,7 +149,6 @@ The `activate_skill` catalog mirrors pi's directory-based skill discovery: the t
|
|
|
150
149
|
/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)
|
|
151
150
|
/agy engine acp|stream-json switch the turn engine (restart to apply; default stream-json; acp is beta and runs self-service setup: binary install + auth bootstrap). No arguments opens the engine picker modal (TUI)
|
|
152
151
|
/agy auth-manual manual ACP credential setup (fallback; auto-setup normally covers this; default login = your Antigravity subscription, same account as the agy CLI)
|
|
153
|
-
/agy patch-cleanup restore the original pi files if an older version patched them
|
|
154
152
|
/agy clear drop all session bindings (force fresh conversations)
|
|
155
153
|
```
|
|
156
154
|
|
|
@@ -158,9 +156,9 @@ The `activate_skill` catalog mirrors pi's directory-based skill discovery: the t
|
|
|
158
156
|
|
|
159
157
|
pi itself has no built-in approval gate. Unlike codex, claude, or agy running interactively, pi does not prompt you to confirm each tool action before it runs. That is the host environment this extension lives in.
|
|
160
158
|
|
|
161
|
-
Because agy runs non-interactively under this provider (nothing can answer a `y/n` prompt), this extension passes `--dangerously-skip-permissions` by default. It is technically necessary: `accept-edits` auto-approves file edits but not shell commands, so a `run_command` would otherwise hang forever waiting for a prompt nothing can answer (upstream [google-antigravity/antigravity-cli#318](https://github.com/google-antigravity/antigravity-cli/issues/318)). The net effect is that agy executes commands the same way pi already executes your other tools: without per-action review.
|
|
159
|
+
Because agy runs non-interactively under this provider (nothing can answer a `y/n` prompt), this extension passes `--dangerously-skip-permissions` by default on `accept-edits` runs. It is technically necessary there: `accept-edits` auto-approves file edits but not shell commands, so a `run_command` would otherwise hang forever waiting for a prompt nothing can answer (upstream [google-antigravity/antigravity-cli#318](https://github.com/google-antigravity/antigravity-cli/issues/318)). The net effect is that agy executes commands the same way pi already executes your other tools: without per-action review.
|
|
162
160
|
|
|
163
|
-
|
|
161
|
+
Plan mode never receives the flag: the flag auto-approves every permission request, including plan mode's own approval gate, which would silently turn review-only into full write access inside the `--add-dir` grant (probed 2026-09-25: with the flag a plan run wrote files; without it, file and command attempts end the run quickly with a "confirm plan" message). So `/agy mode plan` (and `AskAntigravity` with `mode: "plan"`) genuinely executes nothing. Do not combine `--sandbox` with skip-permissions ([#36](https://github.com/google-antigravity/antigravity-cli/issues/36)).
|
|
164
162
|
|
|
165
163
|
For per-action review of agy's mutating native tools (`run_command`, `create_file`, `edit_file`, ...), see the [Approval gate](docs/APPROVAL-GATE.md): with it on, the call must pass a pi-side approval (your permission extension, or the built-in ask/allow/deny fallback) before agy executes it.
|
|
166
164
|
|
package/extensions/index.ts
CHANGED
|
@@ -68,6 +68,16 @@ import { ensureAcpReady, inspectAcpSetup } from "../src/acp/setup.js";
|
|
|
68
68
|
import type { TurnDriver, TurnOutcome } from "../src/driver-types.js";
|
|
69
69
|
import { CONFIG_PATH, loadConfig, logsDir, MAX_TURN_CAP_MIN, parseCapMinutes, saveConfig, type AgyMode, type BridgeTools, type Engine, type ThinkingTier } from "../src/config.js";
|
|
70
70
|
import { agyMissingMessage, isAgyInstalled, savedEngineMessage, showEnginePicker, shouldOfferEnginePicker } from "../src/engine-picker.js";
|
|
71
|
+
import { checkAgyCliVersion, describeAgyVersionCheck, MIN_AGY_VERSION } from "../src/agy-version.js";
|
|
72
|
+
import { isValidAgyAgentName, listAgyAgents } from "../src/agents.js";
|
|
73
|
+
import { formatSubagentRoster, SubagentRoster } from "../src/subagent-roster.js";
|
|
74
|
+
import { spawn } from "node:child_process";
|
|
75
|
+
import { fetchAgyQuota, formatAgyQuotaReport } from "../src/usage.js";
|
|
76
|
+
import { artifactOpenCommand, listAgyArtifacts } from "../src/artifacts.js";
|
|
77
|
+
import { showArtifactsBrowser } from "../src/artifacts-ui.js";
|
|
78
|
+
import { listAgyTasks, tailAgyTaskLog } from "../src/tasks.js";
|
|
79
|
+
import { showTasksBrowser } from "../src/tasks-ui.js";
|
|
80
|
+
import { agyConversationDir } from "../src/agy-paths.js";
|
|
71
81
|
import { createDailyLogger, type DailyLogger } from "../src/daily-log.js";
|
|
72
82
|
import { registerAskAntigravityTool, toolModelsFromRaw } from "../src/ask-tool.js";
|
|
73
83
|
import { registerWebTools } from "../src/web-tools.js";
|
|
@@ -197,8 +207,10 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
197
207
|
// ACP self-heal runs once per process (session_start re-fires on /reload;
|
|
198
208
|
// a ready setup is two file stats, so re-running is harmless anyway).
|
|
199
209
|
let acpSelfHealRan = false;
|
|
200
|
-
// Warn-once-per-process
|
|
210
|
+
// Warn-once-per-process flags for the missing-agy-CLI toast and the
|
|
211
|
+
// too-old-agy version gate (both stream-json).
|
|
201
212
|
let agyMissingWarned = false;
|
|
213
|
+
let agyVersionWarned = false;
|
|
202
214
|
// OAuth URL capture: the server hands the login URL only to the
|
|
203
215
|
// browser-open call (nothing on stdio), so a BROWSER wrapper records it
|
|
204
216
|
// and the driver logs it as "auth-url". Local users keep the automatic
|
|
@@ -351,6 +363,9 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
351
363
|
),
|
|
352
364
|
);
|
|
353
365
|
const replay = new WrapperReplay();
|
|
366
|
+
// Subagent roster: folded from every driver activity (both engines) via
|
|
367
|
+
// the provider; /agy subagents renders it. In-memory, process-lifetime.
|
|
368
|
+
const roster = new SubagentRoster();
|
|
354
369
|
// ACP tool activity is display-only: durable Pi entries with native-looking
|
|
355
370
|
// titles and optional diffs. Unlike a synthetic Pi toolCall this does not
|
|
356
371
|
// re-execute edits, park the turn, or add tool results to model context.
|
|
@@ -460,6 +475,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
460
475
|
replay,
|
|
461
476
|
nativeActive,
|
|
462
477
|
onNativeEvent,
|
|
478
|
+
roster,
|
|
463
479
|
engine,
|
|
464
480
|
log: fileLog.log.bind(fileLog),
|
|
465
481
|
});
|
|
@@ -495,6 +511,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
495
511
|
hiddenBridgeTools,
|
|
496
512
|
acpLog,
|
|
497
513
|
fileLog,
|
|
514
|
+
roster,
|
|
498
515
|
authCapture: authCapture ?? null,
|
|
499
516
|
runAcpPickSetup,
|
|
500
517
|
});
|
|
@@ -641,6 +658,20 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
641
658
|
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
642
659
|
else console.error(`[antigravity-bridge] ${msg}`);
|
|
643
660
|
}
|
|
661
|
+
// Version gate (stream-json): an agy older than MIN_AGY_VERSION fails
|
|
662
|
+
// turns with raw stream errors that look like bridge bugs. Warn once
|
|
663
|
+
// with the fix in the message; doctor carries the full verdict.
|
|
664
|
+
// Development builds and unreadable output stay silent here (an agy
|
|
665
|
+
// from source works; doctor shows what we could not parse).
|
|
666
|
+
if (engine === "stream-json" && !agyVersionWarned && isAgyInstalled(binary)) {
|
|
667
|
+
void checkAgyCliVersion(binary).then((check) => {
|
|
668
|
+
if (agyVersionWarned || check.status !== "unsupported") return;
|
|
669
|
+
agyVersionWarned = true;
|
|
670
|
+
const msg = `agy ${check.version} is too old; install ${MIN_AGY_VERSION} or newer. Stream-json turns may fail in confusing ways until then (/agy doctor inspects).`;
|
|
671
|
+
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
672
|
+
else console.error(`[antigravity-bridge] ${msg}`);
|
|
673
|
+
});
|
|
674
|
+
}
|
|
644
675
|
// Legacy cleanup: users who ran the old consent-gated patcher still
|
|
645
676
|
// carry pi.invokeTool in their installed pi. Inert, but tell them once
|
|
646
677
|
// and offer /agy patch-cleanup. Never auto-edits the install.
|
|
@@ -1010,6 +1041,8 @@ interface AgyCommandCtx {
|
|
|
1010
1041
|
acpLog: (msg: string, data?: unknown) => void;
|
|
1011
1042
|
/** Daily file logger (src/daily-log.ts); command + doctor surfacing. */
|
|
1012
1043
|
fileLog: DailyLogger;
|
|
1044
|
+
/** Live subagent roster (folded in the provider); /agy subagents. */
|
|
1045
|
+
roster: SubagentRoster;
|
|
1013
1046
|
/** Wizard-pick follow-through (download now + chained sign-in); reused
|
|
1014
1047
|
* by /agy engine's no-args modal so both entry points behave alike. */
|
|
1015
1048
|
runAcpPickSetup: (cmdCtx: { ui: ExtensionUIContext }) => Promise<void>;
|
|
@@ -1031,30 +1064,39 @@ interface PendingConfig {
|
|
|
1031
1064
|
systemPrompt?: boolean;
|
|
1032
1065
|
}
|
|
1033
1066
|
|
|
1034
|
-
function
|
|
1067
|
+
function settingsRows(ctx: AgyCommandCtx): string[] {
|
|
1035
1068
|
const config = loadConfig();
|
|
1036
|
-
const source = ctx.usingFallback ? "fallback (agy models failed)" : "discovered";
|
|
1037
1069
|
const perm = config.skipPermissions ? "auto-approved (DANGEROUS)" : "prompt (hangs in -p)";
|
|
1038
1070
|
// padEnd keyed to the longest label ("AskAntigravity thinking:") so the
|
|
1039
|
-
// value column stays aligned as labels grow.
|
|
1071
|
+
// value column stays aligned as labels grow. Doctor-only state (engine,
|
|
1072
|
+
// models, sessions, config path) lives in /agy doctor, not here: one
|
|
1073
|
+
// surface per concern — this is the config surface, doctor is health.
|
|
1040
1074
|
const row = (label: string, value: string) => ` ${label.padEnd(24)} ${value}`;
|
|
1041
1075
|
return [
|
|
1042
|
-
"Antigravity bridge",
|
|
1043
|
-
row("engine:", `${config.engine}${config.engine === "acp" ? " (official server, opt-in)" : ""}`),
|
|
1044
|
-
row("models:", `${ctx.entries.length} ${source}`),
|
|
1045
1076
|
row("mode:", config.mode),
|
|
1046
1077
|
row("permissions:", perm),
|
|
1047
1078
|
row("AskAntigravity tool:", config.askTool ? "on" : "off"),
|
|
1048
1079
|
row("Web tools:", config.webTools ? "on" : "off"),
|
|
1049
1080
|
row("AskAntigravity model:", config.defaultModel),
|
|
1050
1081
|
row("AskAntigravity thinking:", config.defaultThinking),
|
|
1051
|
-
row("sessions:", `${ctx.store.size} bound`),
|
|
1052
|
-
row("config:", CONFIG_PATH),
|
|
1053
1082
|
row("bridge tools:", config.bridgeTools),
|
|
1054
1083
|
row("digest:", config.digest ? "on" : "off"),
|
|
1055
1084
|
row("system prompt:", config.systemPrompt ? "on" : "off"),
|
|
1085
|
+
];
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function statusText(ctx: AgyCommandCtx): string {
|
|
1089
|
+
const config = loadConfig();
|
|
1090
|
+
const source = ctx.usingFallback ? "fallback (agy models failed)" : "discovered";
|
|
1091
|
+
return [
|
|
1092
|
+
"Antigravity bridge",
|
|
1093
|
+
` engine: ${config.engine}${config.engine === "acp" ? " (official server, opt-in)" : ""}`,
|
|
1094
|
+
` models: ${ctx.entries.length} ${source}`,
|
|
1095
|
+
...settingsRows(ctx),
|
|
1096
|
+
` sessions: ${ctx.store.size} bound`,
|
|
1097
|
+
` config: ${CONFIG_PATH}`,
|
|
1056
1098
|
"",
|
|
1057
|
-
"Subcommands: /agy auth, /agy auth-manual, /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 tools [hide|show <name>|reset], /agy web on|off, /agy digest on|off, /agy system-prompt on|off, /agy acp-bin <path|auto>, /agy patch-cleanup, /agy clear",
|
|
1099
|
+
"Subcommands: /agy auth, /agy auth-manual, /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 agent <name|off>, /agy subagents, /agy quota, /agy artifacts [open <n|name>], /agy tasks [tail <id>], /agy bridge all|mcp|none, /agy tools [hide|show <name>|reset], /agy web on|off, /agy digest on|off, /agy system-prompt on|off, /agy timeout <1-1440|off>, /agy acp-bin <path|auto>, /agy patch-cleanup, /agy clear, /agy doctor",
|
|
1058
1100
|
].join("\n");
|
|
1059
1101
|
}
|
|
1060
1102
|
|
|
@@ -1062,7 +1104,7 @@ function statusText(ctx: AgyCommandCtx): string {
|
|
|
1062
1104
|
function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
1063
1105
|
pi.registerCommand("agy", {
|
|
1064
1106
|
description:
|
|
1065
|
-
"Antigravity provider:
|
|
1107
|
+
"Antigravity provider: doctor (health + settings), settings picker, clear sessions. Usage: /agy [doctor|auth|auth-manual|engine stream-json|acp|mode plan|accept-edits|permissions on|off|ask on|off|model <alias>|thinking low|medium|high|agent <name|off>|subagents|quota|artifacts [open <n|name>]|tasks [tail <id>]|bridge all|mcp|none|tools [hide|show <name>|reset]|web on|off|digest on|off|system-prompt on|off|timeout <1-1440|off>|acp-bin <path|auto>|patch-cleanup|clear]",
|
|
1066
1108
|
handler: async (args, cmdCtx: ExtensionCommandContext) => {
|
|
1067
1109
|
const ui = cmdCtx.ui;
|
|
1068
1110
|
if (ui) activeUi = ui;
|
|
@@ -1290,6 +1332,15 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
1290
1332
|
const engine = ctx.engine;
|
|
1291
1333
|
const snap = (engine === "acp" ? ctx.acpDriver : ctx.driver).snapshot();
|
|
1292
1334
|
const port = ctx.getMcpPort();
|
|
1335
|
+
// The CLI matters on both engines (AskAntigravity shells out to
|
|
1336
|
+
// `agy -p` even when turns ride ACP), but on an ACP session a
|
|
1337
|
+
// missing CLI is a supported shape, so label it optional there
|
|
1338
|
+
// instead of showing a bare failure line.
|
|
1339
|
+
const cliCheck = await checkAgyCliVersion(resolveAgyBinary());
|
|
1340
|
+
const cliSuffix =
|
|
1341
|
+
cliCheck.status === "unavailable" && engine === "acp"
|
|
1342
|
+
? " (optional here; AskAntigravity delegations use the CLI)"
|
|
1343
|
+
: "";
|
|
1293
1344
|
const lines = [
|
|
1294
1345
|
"Antigravity doctor (no tokens spent)",
|
|
1295
1346
|
` engine: ${engine}`,
|
|
@@ -1298,6 +1349,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
1298
1349
|
` 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})` : ""}`,
|
|
1299
1350
|
` sessions: ${ctx.store.size} bound`,
|
|
1300
1351
|
` models: ${ctx.entries.length} ${ctx.usingFallback ? "FALLBACK (agy models failed)" : "discovered"}`,
|
|
1352
|
+
` agy cli: ${describeAgyVersionCheck(cliCheck)} (min ${MIN_AGY_VERSION})${cliSuffix}`,
|
|
1301
1353
|
` config: ${CONFIG_PATH}`,
|
|
1302
1354
|
` logs: ${logsDir()} (attach recent days' files when reporting issues; set AGY_DEBUG=1 to capture details)`,
|
|
1303
1355
|
];
|
|
@@ -1326,6 +1378,9 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
1326
1378
|
` acp auth: ${setup.auth ?? "not configured (auto-setup bootstraps)"}`,
|
|
1327
1379
|
);
|
|
1328
1380
|
}
|
|
1381
|
+
// The config surface lives here too, so doctor is the ONE status
|
|
1382
|
+
// surface (former /agy status is gone).
|
|
1383
|
+
lines.push("", "settings:", ...settingsRows(ctx));
|
|
1329
1384
|
ui?.notify(lines.join("\n"), "info");
|
|
1330
1385
|
return;
|
|
1331
1386
|
}
|
|
@@ -1342,6 +1397,214 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
1342
1397
|
}
|
|
1343
1398
|
return;
|
|
1344
1399
|
}
|
|
1400
|
+
if (sub === "tasks") {
|
|
1401
|
+
const engine = ctx.engine;
|
|
1402
|
+
const snap = (engine === "acp" ? ctx.acpDriver : ctx.driver).snapshot();
|
|
1403
|
+
const conversationId = snap.conversationId ?? undefined;
|
|
1404
|
+
if (!conversationId) {
|
|
1405
|
+
ui?.notify("no Antigravity conversation bound yet; run a turn first.", "warning");
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
const dir = agyConversationDir(engine, conversationId);
|
|
1409
|
+
const rest = (args ?? "").trim().split(/\s+/);
|
|
1410
|
+
|
|
1411
|
+
// /agy tasks tail <id>: last bytes of one task log into chat.
|
|
1412
|
+
// Resolved against the scanned list, not a rebuilt filename:
|
|
1413
|
+
// the id->filename convention must not live in two places.
|
|
1414
|
+
if (rest[0]?.toLowerCase() === "tail") {
|
|
1415
|
+
const id = Number(rest[1]);
|
|
1416
|
+
if (!Number.isInteger(id) || id < 0) {
|
|
1417
|
+
ui?.notify("usage: /agy tasks tail <id>. /agy tasks lists them.", "warning");
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
const tasks = await listAgyTasks(dir);
|
|
1421
|
+
const task = tasks.find((t) => t.id === id);
|
|
1422
|
+
if (!task) {
|
|
1423
|
+
ui?.notify(`no task #${id} in this conversation. /agy tasks lists them.`, "warning");
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
const tail = await tailAgyTaskLog(task.logPath);
|
|
1427
|
+
if (tail === "") {
|
|
1428
|
+
ui?.notify(`task #${id} has no readable log.`, "warning");
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
const lines = tail.split("\n");
|
|
1432
|
+
const body = lines.length > 40 ? `…\n${lines.slice(-40).join("\n")}` : tail;
|
|
1433
|
+
ui?.notify(`task #${id} tail:\n${body}`, "info");
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// Dashboard form; r rescans by re-listing fresh.
|
|
1438
|
+
let tasks = await listAgyTasks(dir);
|
|
1439
|
+
if (tasks.length === 0) {
|
|
1440
|
+
ui?.notify("no background tasks recorded for this conversation.", "info");
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
if (!ui) {
|
|
1444
|
+
const lines = tasks.map((t) => {
|
|
1445
|
+
const state = t.livenessKnown ? (t.active ? "ACTIVE" : "idle") : "liveness?";
|
|
1446
|
+
return `#${t.id} ${state} ${t.bytes}B`;
|
|
1447
|
+
});
|
|
1448
|
+
console.log([`antigravity tasks (${tasks.length}):`, ...lines].join("\n"));
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
for (;;) {
|
|
1452
|
+
const action = await showTasksBrowser(ui, tasks, tasks[0]?.livenessKnown ?? false);
|
|
1453
|
+
if (action.type === "rescan") {
|
|
1454
|
+
tasks = await listAgyTasks(dir);
|
|
1455
|
+
if (tasks.length === 0) {
|
|
1456
|
+
ui?.notify("no background tasks recorded for this conversation.", "info");
|
|
1457
|
+
}
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
break;
|
|
1461
|
+
}
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
if (sub === "artifacts") {
|
|
1465
|
+
const engine = ctx.engine;
|
|
1466
|
+
const snap = (engine === "acp" ? ctx.acpDriver : ctx.driver).snapshot();
|
|
1467
|
+
const conversationId = snap.conversationId ?? undefined;
|
|
1468
|
+
if (!conversationId) {
|
|
1469
|
+
ui?.notify("no Antigravity conversation bound yet; run a turn first.", "warning");
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
const dir = agyConversationDir(engine, conversationId);
|
|
1473
|
+
const rest = (args ?? "").trim().split(/\s+/).slice(1);
|
|
1474
|
+
const openWord = rest[0]?.toLowerCase();
|
|
1475
|
+
|
|
1476
|
+
// /agy artifacts open <name|index>: direct open, no overlay.
|
|
1477
|
+
if (openWord === "open") {
|
|
1478
|
+
const artifacts = await listAgyArtifacts(dir);
|
|
1479
|
+
if (artifacts.length === 0) {
|
|
1480
|
+
ui?.notify("no artifacts in this conversation.", "info");
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
const target = rest.slice(1).join(" ").trim();
|
|
1484
|
+
if (target === "") {
|
|
1485
|
+
ui?.notify("usage: /agy artifacts open <name|index>. /agy artifacts lists them.", "warning");
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
const byIndex = /^\d+$/.test(target) ? Number(target) : undefined;
|
|
1489
|
+
const artifact =
|
|
1490
|
+
byIndex !== undefined
|
|
1491
|
+
? artifacts[byIndex]
|
|
1492
|
+
: artifacts.find((a) => a.name.toLowerCase() === target.toLowerCase()) ??
|
|
1493
|
+
(artifacts.filter((a) => a.name.toLowerCase().includes(target.toLowerCase())).length === 1
|
|
1494
|
+
? artifacts.find((a) => a.name.toLowerCase().includes(target.toLowerCase()))
|
|
1495
|
+
: undefined); // ambiguous or no match
|
|
1496
|
+
if (!artifact) {
|
|
1497
|
+
ui?.notify(`no unique artifact matches ${JSON.stringify(target)}. /agy artifacts lists them.`, "warning");
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
const openCmd = artifactOpenCommand();
|
|
1501
|
+
if (!openCmd) {
|
|
1502
|
+
ui?.notify(`no file-open handler for ${process.platform}; the file is at ${artifact.absolutePath}`, "warning");
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
spawn(openCmd.cmd, [artifact.absolutePath], { detached: true, stdio: "ignore", shell: false }).unref();
|
|
1506
|
+
ui?.notify(`opened ${artifact.name}`, "info");
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// Browser loop; r rescans by re-listing fresh. The first scan is
|
|
1511
|
+
// reused so the emptiness gate does not cost a second readdir pass.
|
|
1512
|
+
let artifacts = await listAgyArtifacts(dir);
|
|
1513
|
+
if (artifacts.length === 0) {
|
|
1514
|
+
ui?.notify("no artifacts in this conversation (media dirs appear once agy generates or receives files).", "info");
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
if (!ui) {
|
|
1518
|
+
// Headless: no overlay is possible; print the indexed list.
|
|
1519
|
+
const lines = artifacts.map((a, i) => `${i} ${a.name} (${a.kind}, ${a.bytes}B)`);
|
|
1520
|
+
console.log([`antigravity artifacts (${artifacts.length}):`, ...lines].join("\n"));
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
for (;;) {
|
|
1524
|
+
const action = await showArtifactsBrowser(ui, artifacts);
|
|
1525
|
+
if (action.type === "rescan") {
|
|
1526
|
+
artifacts = await listAgyArtifacts(dir);
|
|
1527
|
+
continue;
|
|
1528
|
+
}
|
|
1529
|
+
if (action.type === "open") {
|
|
1530
|
+
const openCmd = artifactOpenCommand();
|
|
1531
|
+
if (!openCmd) {
|
|
1532
|
+
ui.notify(`no file-open handler for ${process.platform}.`, "warning");
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
spawn(openCmd.cmd, [action.artifact.absolutePath], { detached: true, stdio: "ignore", shell: false }).unref();
|
|
1536
|
+
}
|
|
1537
|
+
break;
|
|
1538
|
+
}
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
if (sub === "quota") {
|
|
1542
|
+
// Quota lives in the CLI's Google identity. A pure-ACP install
|
|
1543
|
+
// without the CLI is a supported shape; say what is missing instead
|
|
1544
|
+
// of rendering a fetch failure.
|
|
1545
|
+
const bin = resolveAgyBinary();
|
|
1546
|
+
if (!isAgyInstalled(bin)) {
|
|
1547
|
+
ui?.notify(
|
|
1548
|
+
"quota comes from the agy CLI, which is not installed. ACP-only setups can install the CLI separately (same Google account) to see subscription quota (/agy doctor shows what IS available).",
|
|
1549
|
+
"warning",
|
|
1550
|
+
);
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
// /usage answers in ~10s live; surface the wait instead of a dead
|
|
1554
|
+
// command line.
|
|
1555
|
+
ui?.setStatus("agy-quota", "checking agy quota…");
|
|
1556
|
+
try {
|
|
1557
|
+
const report = await fetchAgyQuota(bin);
|
|
1558
|
+
if (report === undefined) {
|
|
1559
|
+
ui?.notify("quota unavailable: agy did not return a /usage payload (see /agy doctor for binary health).", "warning");
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
ui?.notify(formatAgyQuotaReport(report), "info");
|
|
1563
|
+
} finally {
|
|
1564
|
+
ui?.setStatus("agy-quota", undefined);
|
|
1565
|
+
}
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
if (sub === "subagents") {
|
|
1569
|
+
const entries = ctx.roster.snapshot();
|
|
1570
|
+
if (entries.length === 0) {
|
|
1571
|
+
ui?.notify("antigravity subagents: none tracked this session\n\nagy spawns subagents as ordinary tool steps; they appear here once a turn uses them.", "info");
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
ui?.notify(formatSubagentRoster(entries), "info");
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
if (sub === "agent") {
|
|
1578
|
+
// ACP has no agent slot in the protocol (RC01): refuse instead of
|
|
1579
|
+
// silently ignoring a configured agent.
|
|
1580
|
+
if (ctx.engine === "acp") {
|
|
1581
|
+
ui?.notify("the ACP engine has no agent selection (RC01). /agy engine stream-json first.", "warning");
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
const name = (args ?? "").trim().split(/\s+/).slice(1).join(" ").trim();
|
|
1585
|
+
if (!name) {
|
|
1586
|
+
const agents = await listAgyAgents(resolveAgyBinary());
|
|
1587
|
+
const current = loadConfig().agent;
|
|
1588
|
+
const agentList =
|
|
1589
|
+
agents.length > 0
|
|
1590
|
+
? agents.map((a) => `${a === current ? ">" : " "} ${a}`).join("\n")
|
|
1591
|
+
: "(no custom agents defined)";
|
|
1592
|
+
ui?.notify(`current agent: ${current ?? "(agy default)"}\n\n${agentList}\n\nusage: /agy agent <name|off>`, "info");
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
if (name === "off" || name === "none" || name === "default") {
|
|
1596
|
+
saveConfig({ agent: undefined });
|
|
1597
|
+
ui?.notify("agent cleared: agy default", "info");
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
if (!isValidAgyAgentName(name)) {
|
|
1601
|
+
ui?.notify(`invalid agent name: ${name} (use letters, digits, dot, dash, underscore)`, "warning");
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
saveConfig({ agent: name });
|
|
1605
|
+
ui?.notify(`agent set: ${name} (takes effect on the next stream-json turn)`, "info");
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1345
1608
|
if (sub === "permissions") {
|
|
1346
1609
|
if (val === "on" || val === "off") {
|
|
1347
1610
|
const next = saveConfig({ skipPermissions: val === "on" });
|
|
@@ -1461,9 +1724,11 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
|
|
|
1461
1724
|
}
|
|
1462
1725
|
|
|
1463
1726
|
|
|
1464
|
-
// No subcommand
|
|
1465
|
-
|
|
1466
|
-
|
|
1727
|
+
// No subcommand: print status, or open the picker in TUI. The old
|
|
1728
|
+
// "status" alias is retired; /agy doctor is the one status surface.
|
|
1729
|
+
if (sub) {
|
|
1730
|
+
const pointer = sub === "status" ? "/agy status was removed; use /agy doctor.\n" : "";
|
|
1731
|
+
ui?.notify(`${pointer}unknown subcommand: ${sub}\n${statusText(ctx)}`, "warning");
|
|
1467
1732
|
return;
|
|
1468
1733
|
}
|
|
1469
1734
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.5",
|
|
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/acp/driver.ts
CHANGED
|
@@ -438,7 +438,7 @@ export class AcpDriver implements TurnDriver {
|
|
|
438
438
|
case "tool_start": {
|
|
439
439
|
const entry = { name: mapped.name, args: mapped.args, diff: mapped.diff, mcpServer: mapped.mcpServer };
|
|
440
440
|
turn.toolCalls.set(mapped.toolCallId, entry);
|
|
441
|
-
this.#emit(turn, { type: "tool_start", name: mapped.name, args: mapped.args, mcpServer: mapped.mcpServer });
|
|
441
|
+
this.#emit(turn, { type: "tool_start", name: mapped.name, args: mapped.args, mcpServer: mapped.mcpServer, toolCallId: mapped.toolCallId });
|
|
442
442
|
return;
|
|
443
443
|
}
|
|
444
444
|
case "tool_done": {
|
|
@@ -457,14 +457,14 @@ export class AcpDriver implements TurnDriver {
|
|
|
457
457
|
const args = entry?.args ?? {};
|
|
458
458
|
// Native diff from the stored tool_call frame; the update's own
|
|
459
459
|
// diff (future builds) wins when present.
|
|
460
|
-
this.#emit(turn, { type: "tool_done", name, args, output: mapped.output, diff: mapped.diff ?? entry?.diff, mcpServer: mapped.mcpServer ?? entry?.mcpServer });
|
|
460
|
+
this.#emit(turn, { type: "tool_done", name, args, output: mapped.output, diff: mapped.diff ?? entry?.diff, mcpServer: mapped.mcpServer ?? entry?.mcpServer, toolCallId: mapped.toolCallId });
|
|
461
461
|
return;
|
|
462
462
|
}
|
|
463
463
|
case "tool_error": {
|
|
464
464
|
const entry = turn.toolCalls.get(mapped.toolCallId);
|
|
465
465
|
turn.toolCalls.delete(mapped.toolCallId);
|
|
466
466
|
const name = entry?.name ?? "tool";
|
|
467
|
-
this.#emit(turn, { type: "tool_error", name, message: mapped.message, mcpServer: mapped.mcpServer ?? entry?.mcpServer });
|
|
467
|
+
this.#emit(turn, { type: "tool_error", name, message: mapped.message, mcpServer: mapped.mcpServer ?? entry?.mcpServer, toolCallId: mapped.toolCallId });
|
|
468
468
|
return;
|
|
469
469
|
}
|
|
470
470
|
case "replay_user":
|
package/src/agents.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// agy custom-agent roster (stream-json engine only).
|
|
2
|
+
//
|
|
3
|
+
// `agy agent` (alias `agents`) lists the user's custom agents one per line.
|
|
4
|
+
// Live-verified against agy 1.2.10: bare names, no banner and no columns
|
|
5
|
+
// when agents exist; help text on bad flags; empty output when none. The
|
|
6
|
+
// parser still tolerates a name-plus-description column layout and banner
|
|
7
|
+
// lines so a future agy that annotates the listing keeps parsing.
|
|
8
|
+
//
|
|
9
|
+
// A picked name rides the turn request as `--agent` (same flag our web
|
|
10
|
+
// tools already pass for their throwaway agents). ACP has no agent slot in
|
|
11
|
+
// the protocol (RC01), so selection is refused at the command level there.
|
|
12
|
+
|
|
13
|
+
import { spawnAgyRaw } from "./models.js";
|
|
14
|
+
|
|
15
|
+
/** Agent names are agy agent-dir names: filesystem-safe tokens. */
|
|
16
|
+
export const AGENT_NAME_MAX = 128;
|
|
17
|
+
const AGENT_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
18
|
+
|
|
19
|
+
/** Parse raw `agy agent` output into agent names. Tolerant: skips empty and
|
|
20
|
+
* banner/help lines, takes the first whitespace-separated token per line
|
|
21
|
+
* (a future description column survives), rejects names that are not
|
|
22
|
+
* filesystem-safe tokens, dedupes preserving order. */
|
|
23
|
+
export function parseAgyAgentsRaw(text: string): string[] {
|
|
24
|
+
const seen = new Set<string>();
|
|
25
|
+
const agents: string[] = [];
|
|
26
|
+
for (const line of text.split("\n")) {
|
|
27
|
+
const trimmed = line.trim();
|
|
28
|
+
if (!trimmed) continue;
|
|
29
|
+
// Help output and older banner formats are noise, not agent names
|
|
30
|
+
// (checked on the whole line: "List available agents" has no single
|
|
31
|
+
// banner token).
|
|
32
|
+
if (/^(usage:|flags:|-h\b|list available|available agents)/i.test(trimmed)) continue;
|
|
33
|
+
const token = trimmed.split(/\s+/)[0] ?? "";
|
|
34
|
+
if (!AGENT_NAME_RE.test(token) || seen.has(token)) continue;
|
|
35
|
+
seen.add(token);
|
|
36
|
+
agents.push(token);
|
|
37
|
+
}
|
|
38
|
+
return agents;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** True when `name` is a valid agent-dir token (what `--agent` accepts). */
|
|
42
|
+
export function isValidAgyAgentName(name: string): boolean {
|
|
43
|
+
return AGENT_NAME_RE.test(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Normalize a configured agent value: trim, cap, strip control characters;
|
|
47
|
+
* empty result means "no agent configured". Kept permissive beyond the
|
|
48
|
+
* token regex so a user typo reaches agy's own validation (a visible
|
|
49
|
+
* turn error) instead of silently reverting to the default agent. */
|
|
50
|
+
export function normalizeAgyAgentName(value: string | undefined): string | undefined {
|
|
51
|
+
if (value === undefined) return undefined;
|
|
52
|
+
const cleaned = value
|
|
53
|
+
.trim()
|
|
54
|
+
.replace(/[\x00-\x1f\x7f]/g, "")
|
|
55
|
+
.slice(0, AGENT_NAME_MAX);
|
|
56
|
+
return cleaned === "" ? undefined : cleaned;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** List the custom agents `agy agent` reports. "" stdout (missing binary,
|
|
60
|
+
* failure, cap) parses to [], so callers can treat "no agents" and "could
|
|
61
|
+
* not ask" the same way the command layer renders it. */
|
|
62
|
+
export async function listAgyAgents(binary: string = "agy"): Promise<string[]> {
|
|
63
|
+
return parseAgyAgentsRaw(await spawnAgyRaw(binary, ["agent"]));
|
|
64
|
+
}
|
package/src/agy-paths.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// agy per-engine state dirs ("brain" trees).
|
|
2
|
+
//
|
|
3
|
+
// Both engines keep per-conversation state on disk; the roots differ
|
|
4
|
+
// (live-probed 2026-09-24, this machine):
|
|
5
|
+
// stream-json CLI: ~/.gemini/antigravity-cli/brain/<conversationId>/
|
|
6
|
+
// ACP server: ~/.gemini/antigravity-acp/brain/<conversationId>/
|
|
7
|
+
// The ACP tree mirrors the CLI layout (.system_generated/tasks/,
|
|
8
|
+
// .system_generated/steps/), so downstream scans (tasks, artifacts) work
|
|
9
|
+
// unchanged once the right root is picked. A third tree exists —
|
|
10
|
+
// ~/.gemini/antigravity-cli/conversations/<uuid>.db — but it serves
|
|
11
|
+
// one-shot print-mode calls (AskAntigravity), NOT these features; do not
|
|
12
|
+
// conflate the two. Conversation ids are the raw ids agy reports, no
|
|
13
|
+
// hashing; the ids our session store persists match these dir names.
|
|
14
|
+
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import type { Engine } from "./config.js";
|
|
18
|
+
|
|
19
|
+
/** Brain root for one engine. `home` injectable for tests. */
|
|
20
|
+
export function agyBrainRoot(engine: Engine, home: string = os.homedir()): string {
|
|
21
|
+
const scope = engine === "acp" ? "antigravity-acp" : "antigravity-cli";
|
|
22
|
+
return path.join(home, ".gemini", scope, "brain");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Per-conversation state dir. `conversationId` is the raw id agy reports
|
|
26
|
+
* (and what the session store persists). */
|
|
27
|
+
export function agyConversationDir(
|
|
28
|
+
engine: Engine,
|
|
29
|
+
conversationId: string,
|
|
30
|
+
home: string = os.homedir(),
|
|
31
|
+
): string {
|
|
32
|
+
return path.join(agyBrainRoot(engine, home), conversationId);
|
|
33
|
+
}
|