@estebanforge/pi-antigravity-bridge 1.9.1 → 1.9.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 CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.9.3] - 2026-11-06
6
+
7
+ ### Fixed
8
+
9
+ - **A silently dying ACP server now reports its exit code and signal.** The exit handler received the child's code and signal but dropped them (hardcoded null), so a `.par` that died before answering initialize surfaced only as "process gone" with no cause (issue #9). The code and signal now reach the abort reason and the user-facing mid-turn failure, and the `connection-exited` log line records them alongside the stderr tail.
10
+
11
+ - **The registry's declared `distribution.binary.args` are now honored.** The installer read only `archive` and `cmd`, so a Linux-only arg like `--uid=` was silently dropped and the server never received it (issue #9). Args are validated as a flat string array, persisted to a sidecar `args.json` next to `zip.sha256` (so the already-installed early-return path recovers them), and resolved by path identity against the managed `<root>/current` binary: setup persists that path into `acp.bin`, so a source-label check would wrongly treat it as a user override on the next turn. Env and config overrides (a user shim such as `agy acp`) own their argv and get no registry args. Both `/agy auth` flows and the per-connection driver receive them.
12
+
13
+ - **A ToS-disabled Google account is detected at sign-in instead of on the first real prompt.** The credential probe used only `session/new`, which a disabled account passes, so it wrote a sticky verified marker and `/agy doctor` kept reporting healthy (issue #9). The probe now sends one bounded `session/prompt` and scans the streamed reply for the disablement notice (Google returns it as normal content, not a -32000 auth error). A disabled account fails auth immediately with appeal guidance and writes no marker; the probe is time-capped so a hollow or hung server fails rather than hanging `/agy auth`, and the reply head is logged so a change in Google's wording is diagnosable. The probe is scoped to the token-absent branch to avoid a model call on every normal browser sign-in.
14
+
15
+ ## [1.9.2] - 2026-11-02
16
+
17
+ ### Fixed
18
+ - The AskAntigravity tool's expand hint now comes from the render context pi injects (`context.keyHint`) instead of the package import. pi 1.5 blinds an imported `keyHint` to the host keybindings (app.* ids format as ""), so the hint rendered as "to expand" with no key. Peer floor moves to `^1.5.0` accordingly.
19
+
5
20
  ## [1.9.1] - 2026-10-29
6
21
 
7
22
  ### Fixed
@@ -62,7 +62,7 @@ import { AcpDriver } from "../src/acp/driver.js";
62
62
  import { runAcpAuth } from "../src/acp/auth.js";
63
63
  import { setAuthInvalidatedHook } from "../src/acp/connection.js";
64
64
  import { setupAuthUrlCapture } from "../src/acp/browser-capture.js";
65
- import { clearAcpAuthVerified, ensureAcpReady, inspectAcpSetup } from "../src/acp/setup.js";
65
+ import { clearAcpAuthVerified, ensureAcpReady, inspectAcpSetup, resolveInstalledArgs } from "../src/acp/setup.js";
66
66
  import { ensureQueuedMessagesDefaults } from "../src/queued-messages.js";
67
67
  import type { TurnDriver, TurnOutcome } from "../src/driver-types.js";
68
68
  import { CONFIG_PATH, loadConfig, logsDir, MAX_TURN_CAP_MIN, parseCapMinutes, saveConfig, type AgyMode, type BridgeTools, type Engine, type ThinkingTier } from "../src/config.js";
@@ -313,6 +313,10 @@ export default async function (pi: ExtensionAPI): Promise<void> {
313
313
  // Resolved per connection: the setup flow can install the binary and
314
314
  // update acp.bin mid-session; the next turn picks it up (no restart).
315
315
  bin: () => loadConfig().acp.bin,
316
+ // Registry-declared argv for the managed binary (e.g. linux --uid=).
317
+ // Empty for env/config overrides, which own their argv. Resolved per
318
+ // connection alongside bin so a mid-session install picks them up.
319
+ binArgs: () => resolveInstalledArgs({ configBin: loadConfig().acp.bin }),
316
320
  // Resolved per turn: /agy and AGY_USAGE_ESTIMATE changes apply without
317
321
  // a restart. Without this the driver defaults to "estimate" and the
318
322
  // config knob (incl. "off") is dead.
@@ -656,6 +660,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
656
660
  );
657
661
  const r = await runAcpAuth({
658
662
  bin: status.bin,
663
+ binArgs: status.args,
659
664
  ...(authCapture ? { extraEnv: authCapture.browserEnv, authUrlFile: authCapture.file } : {}),
660
665
  log: acpLog,
661
666
  });
@@ -1319,6 +1324,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1319
1324
  );
1320
1325
  const r = await runAcpAuth({
1321
1326
  bin: status.bin,
1327
+ binArgs: status.args,
1322
1328
  extraEnv: ctx.authCapture?.browserEnv,
1323
1329
  authUrlFile: ctx.authCapture?.file,
1324
1330
  log: ctx.acpLog,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estebanforge/pi-antigravity-bridge",
3
- "version": "1.9.1",
3
+ "version": "1.9.3",
4
4
  "description": "Gemini provider for Pi on the Antigravity ACP server (official Google ACP) or the stream-json agy CLI. Use antigravity/* models in Pi. Full /model picker support. Models runs Pi's tools. ToS safe to use.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -46,7 +46,7 @@
46
46
  ]
47
47
  },
48
48
  "peerDependencies": {
49
- "@earendil-works/pi-coding-agent": "^1.0.0"
49
+ "@earendil-works/pi-coding-agent": "^1.5.0"
50
50
  },
51
51
  "peerDependenciesMeta": {
52
52
  "@earendil-works/pi-coding-agent": {
package/src/acp/auth.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { AcpConnection } from "./connection.js";
2
+ import { mapUpdate } from "./events.js";
2
3
  import { hasAcpToken, writeAcpAuthVerified } from "./setup.js";
3
4
 
4
5
  // Explicit sign-in (/agy auth): spawn a short-lived server, send
@@ -53,23 +54,62 @@ export interface AcpAuthResult {
53
54
  error?: string;
54
55
  }
55
56
 
56
- /** Live credential probe for the token-absent branch: a server with usable
57
- * credentials answers session/new; a hollow accept starts (or wants) a
58
- * browser flow and the request times out or errors. Closes the probe
59
- * session; the caller kills the connection either way. */
60
- async function probeUsableCredentials(conn: AcpConnection, cwd?: string): Promise<boolean> {
57
+ /** Google returns account-disablement as a normal session/prompt reply
58
+ * (stopReason end_turn) carrying this text, not a -32000 auth error, so a
59
+ * session/new probe alone cannot tell "authenticated" from "entitled". */
60
+ const ACCOUNT_DISABLED_RE = /disabled in this account|violation of\s+Terms of Service|submit an appeal/i;
61
+
62
+ /** Bounds the probe's session/prompt. A real first token normally lands well
63
+ * under this; a hollow or hung server hits the cap and fails auth. */
64
+ const PROBE_PROMPT_TIMEOUT_MS = 30_000;
65
+
66
+ /** Live credential probe for the token-absent branch. session/new alone is
67
+ * too shallow: a ToS-disabled account passes it. So the probe sends one
68
+ * trivial session/prompt and inspects the streamed reply (collected by the
69
+ * caller's onUpdate into `sink`): the disablement notice means the account
70
+ * is blocked (distinct from a hollow accept). Returns "usable" only when
71
+ * the prompt completes without that notice. Closes the probe session; the
72
+ * caller kills the connection either way. */
73
+ async function probeUsableCredentials(
74
+ conn: AcpConnection,
75
+ sink: { text: string },
76
+ log: (event: string, data?: unknown) => void,
77
+ cwd?: string,
78
+ ): Promise<"usable" | "disabled" | "unusable"> {
79
+ let timer: NodeJS.Timeout | undefined;
61
80
  try {
62
81
  const s = await conn.newSession(cwd ?? process.cwd());
63
- await conn.closeSession(s.sessionId).catch(() => {});
64
- return true;
82
+ try {
83
+ sink.text = "";
84
+ // conn.prompt has no timeout by design (driver timers govern real
85
+ // turns). The probe runs during interactive auth, so bound it: a
86
+ // hollow server that accepts session/new but never answers the
87
+ // prompt must fail, not hang /agy auth and the authInFlight lock.
88
+ await Promise.race([
89
+ conn.prompt(s.sessionId, "ping"),
90
+ new Promise<never>((_, reject) => {
91
+ timer = setTimeout(() => reject(new Error("probe prompt timed out")), PROBE_PROMPT_TIMEOUT_MS);
92
+ }),
93
+ ]);
94
+ } finally {
95
+ clearTimeout(timer);
96
+ await conn.closeSession(s.sessionId).catch(() => {});
97
+ }
98
+ // Log the reply head so a change in Google's disablement wording (which
99
+ // would silently regress the regex to "usable") is diagnosable.
100
+ log("auth-probe-reply", { head: sink.text.slice(0, 200) });
101
+ return ACCOUNT_DISABLED_RE.test(sink.text) ? "disabled" : "usable";
65
102
  } catch {
66
- return false;
103
+ return "unusable";
67
104
  }
68
105
  }
69
106
 
70
107
  let authInFlight: Promise<AcpAuthResult> | null = null;
71
108
 
72
109
  export function runAcpAuth(opts: AcpAuthOptions): Promise<AcpAuthResult> {
110
+ // Collects streamed reply text so the credential probe can detect an
111
+ // account-disablement notice (which arrives as normal prompt content).
112
+ const probeSink = { text: "" };
73
113
  const conn = new AcpConnection({
74
114
  bin: opts.bin,
75
115
  binArgs: opts.binArgs,
@@ -77,7 +117,10 @@ export function runAcpAuth(opts: AcpAuthOptions): Promise<AcpAuthResult> {
77
117
  extraEnv: opts.extraEnv,
78
118
  authUrlFile: opts.authUrlFile,
79
119
  log: opts.log ?? (() => {}),
80
- onUpdate: () => {},
120
+ onUpdate: (_sessionId, update) => {
121
+ const mapped = mapUpdate(update);
122
+ if (mapped?.kind === "text") probeSink.text += mapped.delta;
123
+ },
81
124
  onExit: () => {},
82
125
  });
83
126
  const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
@@ -91,8 +134,15 @@ export function runAcpAuth(opts: AcpAuthOptions): Promise<AcpAuthResult> {
91
134
  if (!hasAcpToken(opts.tokenDir)) {
92
135
  // authenticate replied but no token file: either the server
93
136
  // authenticated from artifacts outside antigravity-acp, or the
94
- // reply is hollow. A live session/new separates the two.
95
- if (!(await probeUsableCredentials(conn, opts.cwd))) {
137
+ // reply is hollow. A live session prompt separates the two.
138
+ const probe = await probeUsableCredentials(conn, probeSink, opts.log ?? (() => {}), opts.cwd);
139
+ if (probe === "disabled") {
140
+ return {
141
+ ok: false,
142
+ error: "this Google account is disabled for the Antigravity service (Terms of Service). Signing in again will not help; submit an appeal with Google to restore access.",
143
+ };
144
+ }
145
+ if (probe !== "usable") {
96
146
  return {
97
147
  ok: false,
98
148
  error: `the server accepted the sign-in but holds no usable credentials (session probe failed; token file absent in ${opts.tokenDir ?? "~/.gemini/antigravity-acp"}). Finish the Google sign-in if a browser opened, then retry /agy auth.`,
@@ -101,6 +151,11 @@ export function runAcpAuth(opts: AcpAuthOptions): Promise<AcpAuthResult> {
101
151
  writeAcpAuthVerified(opts.authVerifiedFile);
102
152
  return { ok: true, note: "the server authenticated from existing credentials; no browser sign-in was needed" };
103
153
  }
154
+ // Token present (normal browser sign-in): no entitlement probe. A
155
+ // ToS-disabled account that completes a real sign-in still verifies
156
+ // here and surfaces the disablement on its first real prompt. The
157
+ // prompt probe is scoped to the token-absent branch (issue #9's
158
+ // `agy acp` shim path) to avoid a model call on every sign-in.
104
159
  writeAcpAuthVerified(opts.authVerifiedFile);
105
160
  return { ok: true };
106
161
  } catch (err) {
@@ -246,8 +246,13 @@ export class AcpConnection {
246
246
  child.on("exit", (code, signal) => {
247
247
  this.#opts.log("exit", { code: code ?? signal ?? "?" });
248
248
  // The reason rides into pending-request rejections, so it must be
249
- // redacted here (server stderr can carry auth material).
250
- this.#finish(redactText(this.#stderrTail.trim()));
249
+ // redacted here (server stderr can carry auth material). When the
250
+ // server dies with no output the exit code/signal is the only clue,
251
+ // so it goes into the reason ahead of the stderr tail.
252
+ const where = signal ? `signal ${signal}` : code !== null ? `code ${code}` : "";
253
+ const tail = redactText(this.#stderrTail.trim());
254
+ const detail = [where, tail || "process gone"].filter(Boolean).join(": ");
255
+ this.#finish(detail, code, signal);
251
256
  });
252
257
 
253
258
  const result = (await this.request(
@@ -582,7 +587,7 @@ export class AcpConnection {
582
587
  this.#opts.log("auth-url", { url, port: parseAuthPort(url) });
583
588
  }
584
589
 
585
- #finish(reason: string): void {
590
+ #finish(reason: string, code: number | null = null, signal: NodeJS.Signals | null = null): void {
586
591
  if (this.#exited) return;
587
592
  this.#exited = true;
588
593
  this.#authUrlWatcher?.close();
@@ -593,7 +598,7 @@ export class AcpConnection {
593
598
  this.#permissionPending.clear();
594
599
  for (const deny of pendings) deny();
595
600
  this.abortAll(`connection exited: ${reason || "process gone"}`);
596
- this.#opts.onExit({ code: null, signal: null, stderrTail: redactText(this.#stderrTail) });
601
+ this.#opts.onExit({ code, signal, stderrTail: redactText(this.#stderrTail) });
597
602
  }
598
603
  }
599
604
 
package/src/acp/driver.ts CHANGED
@@ -45,8 +45,10 @@ export interface AcpDriverOptions {
45
45
  * is resolved per connection: setup can install the binary and update
46
46
  * config mid-session, and the next turn picks it up without a restart. */
47
47
  bin: string | (() => string);
48
- /** Extra argv for the binary (tests: node + fake-server script). */
49
- binArgs?: string[];
48
+ /** Extra argv for the binary (tests: node + fake-server script). A
49
+ * function is resolved per connection, like bin, so registry args from a
50
+ * mid-session managed install apply without a restart. */
51
+ binArgs?: string[] | (() => string[]);
50
52
  extraEnv?: Record<string, string>;
51
53
  /** Record file for BROWSER-captured OAuth URLs; passed to the connection,
52
54
  * which logs "auth-url" when the server tries to open a login. */
@@ -472,7 +474,7 @@ export class AcpDriver implements TurnDriver {
472
474
  }
473
475
  }
474
476
 
475
- #onConnectionExit(conn: AcpConnection, info: { stderrTail: string }): void {
477
+ #onConnectionExit(conn: AcpConnection, info: { code: number | null; signal: string | null; stderrTail: string }): void {
476
478
  if (this.#conn !== conn) {
477
479
  // A replaced connection reporting its death late (RC01's signal
478
480
  // handler intercepts SIGTERM and can outlive its replacement by
@@ -489,7 +491,12 @@ export class AcpDriver implements TurnDriver {
489
491
  // mid-flight is user-reportable, and it surfaces as a normal turn
490
492
  // error, not as raw console noise.
491
493
  const turnActive = Boolean(turn && !turn.closed && !turn.aborted && !turn.sawResult);
492
- this.#log("connection-exited", { expected: !turnActive, tail: info.stderrTail.slice(-200) });
494
+ this.#log("connection-exited", {
495
+ expected: !turnActive,
496
+ code: info.code,
497
+ signal: info.signal,
498
+ tail: info.stderrTail.slice(-200),
499
+ });
493
500
  if (!turn || turn.closed) return;
494
501
  if (turn.aborted || turn.sawResult) {
495
502
  this.#settle(turn, {
@@ -501,7 +508,10 @@ export class AcpDriver implements TurnDriver {
501
508
  });
502
509
  return;
503
510
  }
504
- this.#failTurn(turn, "Antigravity session exited unexpectedly; it restarts on the next turn");
511
+ // A silent death (no stderr) leaves only the exit code/signal as a
512
+ // clue; surface it so the failure is not a bare "exited unexpectedly".
513
+ const where = info.signal ? ` (signal ${info.signal})` : info.code !== null ? ` (code ${info.code})` : "";
514
+ this.#failTurn(turn, `Antigravity session exited unexpectedly${where}; it restarts on the next turn`);
505
515
  }
506
516
 
507
517
  #ensureConnection(request: DriverTurnRequest): Promise<AcpConnection> {
@@ -511,7 +521,7 @@ export class AcpDriver implements TurnDriver {
511
521
  this.#stats.spawns += 1;
512
522
  const conn = new AcpConnection({
513
523
  bin: resolveAcpBinary(typeof this.#opts.bin === "function" ? this.#opts.bin() : this.#opts.bin),
514
- binArgs: this.#opts.binArgs,
524
+ binArgs: typeof this.#opts.binArgs === "function" ? this.#opts.binArgs() : this.#opts.binArgs,
515
525
  extraEnv: this.#opts.extraEnv,
516
526
  authUrlFile: this.#opts.authUrlFile,
517
527
  cwd: request.cwd,
package/src/acp/setup.ts CHANGED
@@ -52,6 +52,8 @@ export type AcpSetupStatus =
52
52
  ok: true;
53
53
  bin: string;
54
54
  binarySource: "env" | "config" | "installed" | "existing";
55
+ /** Registry argv for the managed binary; empty for env/config overrides. */
56
+ args: string[];
55
57
  auth: string;
56
58
  /** True when auth was just set to oauth-personal: /agy auth opens
57
59
  * the Google login in the browser. */
@@ -121,6 +123,15 @@ export function executableFile(p: string): boolean {
121
123
  interface RegistryBinary {
122
124
  archive: string;
123
125
  cmd?: string;
126
+ /** Platform-specific argv the server needs to boot (e.g. linux `--uid=`).
127
+ * Untrusted registry data: validated as a string array before use. */
128
+ args?: unknown;
129
+ }
130
+
131
+ /** Registry args are untrusted; accept only a flat string array. */
132
+ function normalizeRegistryArgs(raw: unknown): string[] {
133
+ if (!Array.isArray(raw)) return [];
134
+ return raw.filter((a): a is string => typeof a === "string");
124
135
  }
125
136
 
126
137
  async function fetchRegistryBinary(opts: SetupOptions, key: string): Promise<RegistryBinary> {
@@ -180,6 +191,8 @@ export interface InstallResult {
180
191
  build: string;
181
192
  sha256: string;
182
193
  downloaded: boolean;
194
+ /** Registry-declared argv for the managed binary (may be empty). */
195
+ args: string[];
183
196
  }
184
197
 
185
198
  /** Fetch the pinned registry build for this platform and install it under
@@ -193,8 +206,11 @@ export async function installAcpBinary(opts: SetupOptions = {}): Promise<Install
193
206
  const cmd = (entry.cmd ?? "./agy_acp_server.par").replace(/^\.\//, "");
194
207
  const dir = path.join(root, build);
195
208
  const bin = path.join(dir, cmd);
209
+ const args = normalizeRegistryArgs(entry.args);
196
210
  if (executableFile(bin)) {
197
- return { bin, build, sha256: readRecordedSha(dir), downloaded: false };
211
+ // Already installed: recover any recorded args so the early-return
212
+ // path does not silently drop them.
213
+ return { bin, build, sha256: readRecordedSha(dir), downloaded: false, args: readRecordedArgs(dir) };
198
214
  }
199
215
 
200
216
  fs.mkdirSync(root, { recursive: true });
@@ -209,13 +225,14 @@ export async function installAcpBinary(opts: SetupOptions = {}): Promise<Install
209
225
  if (!executableFile(bin)) throw new Error(`${cmd} not found in the archive after unpacking`);
210
226
  fs.chmodSync(bin, 0o755);
211
227
  fs.writeFileSync(path.join(dir, "zip.sha256"), `${sha} ${path.basename(entry.archive)}\n`);
228
+ if (args.length > 0) fs.writeFileSync(path.join(dir, "args.json"), JSON.stringify(args));
212
229
 
213
230
  // Re-point `current` at the new build. The link is ours (managed layout).
214
231
  const link = path.join(root, "current");
215
232
  fs.rmSync(link, { force: true });
216
233
  fs.symlinkSync(build, link);
217
234
  opts.onProgress?.("ACP server installed.");
218
- return { bin, build, sha256: sha, downloaded: true };
235
+ return { bin, build, sha256: sha, downloaded: true, args };
219
236
  }
220
237
 
221
238
  function readRecordedSha(dir: string): string {
@@ -226,6 +243,14 @@ function readRecordedSha(dir: string): string {
226
243
  }
227
244
  }
228
245
 
246
+ function readRecordedArgs(dir: string): string[] {
247
+ try {
248
+ return normalizeRegistryArgs(JSON.parse(fs.readFileSync(path.join(dir, "args.json"), "utf8")));
249
+ } catch {
250
+ return [];
251
+ }
252
+ }
253
+
229
254
  export interface AuthState {
230
255
  configured: boolean;
231
256
  /** settings.json auth.type, "token" (acp_token.json present), or undefined. */
@@ -343,6 +368,25 @@ export function resolveInstalledBinary(opts: SetupOptions = {}): BinaryResolutio
343
368
  return { bin: null, source: null };
344
369
  }
345
370
 
371
+ /** Registry argv for the managed install only. Env/config overrides (a user
372
+ * shim like `agy acp`, or AGY_ACP_BIN) own their argv, so they get none.
373
+ * Identity is by PATH, not by which candidate won: setup persists the managed
374
+ * path into acp.bin, so after install the config candidate resolves to the
375
+ * managed binary too. Compare realpaths against `<root>/current/...` (the
376
+ * symlink target carries the args.json sidecar). Empty when not managed. */
377
+ export function resolveInstalledArgs(opts: SetupOptions = {}): string[] {
378
+ const binary = resolveInstalledBinary(opts);
379
+ if (!binary.bin) return [];
380
+ const root = opts.installRoot ?? defaultInstallRoot();
381
+ const managed = path.join(root, "current", "agy_acp_server.par");
382
+ try {
383
+ if (fs.realpathSync(binary.bin) !== fs.realpathSync(managed)) return [];
384
+ } catch {
385
+ return [];
386
+ }
387
+ return readRecordedArgs(path.dirname(fs.realpathSync(managed)));
388
+ }
389
+
346
390
  /** Read-only setup state (for /agy doctor): no installs, no writes. */
347
391
  export function inspectAcpSetup(opts: SetupOptions = {}): { bin: string | null; source: string | null; auth: string | null } {
348
392
  const binary = resolveInstalledBinary(opts);
@@ -367,6 +411,7 @@ export async function ensureAcpReady(opts: SetupOptions = {}): Promise<AcpSetupS
367
411
 
368
412
  // 1. Binary.
369
413
  let binary = resolveInstalledBinary(opts);
414
+ let args = resolveInstalledArgs(opts);
370
415
  if (!binary.bin) {
371
416
  if (opts.install === false) {
372
417
  return { ok: false, stage: "install", error: "no ACP server binary found", manual: MANUAL_SETUP };
@@ -374,6 +419,7 @@ export async function ensureAcpReady(opts: SetupOptions = {}): Promise<AcpSetupS
374
419
  try {
375
420
  const installed = await installAcpBinary(opts);
376
421
  binary = { bin: installed.bin, source: "installed" };
422
+ args = installed.args;
377
423
  actions.push(installed.downloaded ? `installed ACP server build ${installed.build}` : `found installed ACP server build ${installed.build}`);
378
424
  } catch (err) {
379
425
  return {
@@ -397,7 +443,7 @@ export async function ensureAcpReady(opts: SetupOptions = {}): Promise<AcpSetupS
397
443
  try {
398
444
  writeAuthType("oauth-personal", gdir);
399
445
  actions.push("configured oauth-personal auth (your Antigravity subscription login)");
400
- return { ok: true, bin: binary.bin!, binarySource: binary.source!, auth: "oauth-personal", needsLogin: true, actions };
446
+ return { ok: true, bin: binary.bin!, binarySource: binary.source!, args, auth: "oauth-personal", needsLogin: true, actions };
401
447
  } catch (err) {
402
448
  return {
403
449
  ok: false,
@@ -419,6 +465,7 @@ export async function ensureAcpReady(opts: SetupOptions = {}): Promise<AcpSetupS
419
465
  ok: true,
420
466
  bin: binary.bin!,
421
467
  binarySource: binary.source!,
468
+ args,
422
469
  auth: auth.type ?? "token",
423
470
  needsLogin,
424
471
  actions,
package/src/ask-tool.ts CHANGED
@@ -16,7 +16,7 @@ import { randomBytes } from "node:crypto";
16
16
  import * as fs from "node:fs";
17
17
  import * as path from "node:path";
18
18
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
- import { buildSessionContext, getAgentDir, keyHint } from "@earendil-works/pi-coding-agent";
19
+ import { buildSessionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
20
20
  import { Text } from "@earendil-works/pi-tui";
21
21
  import { contentText, type ThinkingLevel } from "@earendil-works/pi-ai";
22
22
  import { Type } from "typebox";
@@ -759,7 +759,7 @@ export async function registerAskAntigravityTool(
759
759
  }
760
760
  return new Text(text, 0, 0);
761
761
  },
762
- renderResult(result, { expanded, isPartial }, theme) {
762
+ renderResult(result, { expanded, isPartial }, theme, context) {
763
763
  const d = result.details as AgyDetails | undefined;
764
764
  if (isPartial) {
765
765
  const status = result.content[0]?.type === "text" ? result.content[0].text : "working...";
@@ -789,7 +789,7 @@ export async function registerAskAntigravityTool(
789
789
  const lines = truncated.split("\n").slice(0, PREVIEW_MAX_LINES);
790
790
  if (lines.length) text += `\n${theme.fg("toolOutput", lines.join("\n"))}`;
791
791
  if (body.length > PREVIEW_MAX_CHARS || body.split("\n").length > PREVIEW_MAX_LINES) {
792
- text += `\n${theme.fg("dim", `… (${keyHint("app.tools.expand", "to expand")})`)}`;
792
+ text += `\n${theme.fg("dim", `… (${context.keyHint("app.tools.expand", "to expand")})`)}`;
793
793
  }
794
794
  }
795
795
  return new Text(text, 0, 0);