@estebanforge/pi-antigravity-bridge 1.4.2 → 1.4.4

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,35 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.4.4] - 2026-09-04
6
+
7
+ ### Added
8
+
9
+ - The Google sign-in URL now surfaces in pi. The ACP server hands the OAuth URL only to the browser-open call (nothing on stdout/stderr, headless or not), so a `BROWSER` wrapper script records the URL and forwards to the real opener; the connection watches the record file and logs it as an `auth-url` event. The extension toasts it at warning level with the ssh port-forward command for the redirect port, so logins work from SSH sessions on remote machines; local users keep the automatic browser open and get the URL as a fallback. An existing `BROWSER` setting is chained, not replaced.
10
+
11
+ ### Fixed
12
+
13
+ - ACP login pending was reported only at the moment setup wrote `settings.json`, so after the restart, with `settings.json` in place but the browser login never completed, every check went silent: no toast, no hint, while the first ACP message had no token to use. `needsLogin` now tracks the token file (stat only, never read): `oauth-personal` without `acp_token.json` is login-pending, so the switch-time toast, the picker, and the `session_start` self-heal keep saying so on every start until the login is done.
14
+
15
+ ### Changed
16
+
17
+ - Engine switching is command-only now: `/agy engine stream-json|acp`. The bare-`/agy` settings picker no longer carries an Engine row (nor the post-save setup run), so the switch surface cannot be hit accidentally; `/agy engine acp` keeps the self-service setup (binary + auth) and the login warning. The picker's plan+acp (RC01) guard stays in a narrower form: the mode row alone can still produce `plan` while the engine is `acp`.
18
+ - ACP login-pending messages rewritten for end users: what happens (the server opens the Google sign-in page in your browser on the first Antigravity message), when (after the restart or immediately, per moment), and which account (your Antigravity subscription, the same Google account as the `agy` CLI). No URL to open manually. All remaining moments (engine switch, session start) now toast at warning level so the pending action stands out.
19
+ - The "Turn engine" wording became "Engine" in the README env table, the config comment, and the architecture doc heading; the picker row itself is gone (engine switching is command-only, see above).
20
+
21
+ ## [1.4.3] - 2026-09-04
22
+
23
+ ### Added
24
+
25
+ - 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.
26
+
27
+ ### Changed
28
+ ### Changed
29
+
30
+ - 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).
31
+ - "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.
32
+ - 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.
33
+
5
34
  ## [1.4.2] - 2026-09-04
6
35
 
7
36
  ### Changed
package/README.md CHANGED
@@ -25,7 +25,7 @@ Turns run through one of two engines behind the same provider surface (`config.e
25
25
 
26
26
  Engine-dependent features: pi image attachments ride natively only on the ACP engine (the picker offers image attach automatically when `config.engine` is `acp`; the stream-json CLI prompt is text-only). With the optional G1 digest enabled, its delivery also differs: ACP ships it as a native `embeddedContext` resource block, stream-json prepends it to the prompt text.
27
27
 
28
- Switch with `/agy engine acp|stream-json` (takes effect on restart). Setup is automatic: switching to `acp` installs Google's official ACP server binary from the [antigravity-acp registry entry](https://github.com/agentclientprotocol/registry) (`~/.local/opt/agy-acp/<build>/` + a `current` symlink, zip sha256 recorded; layout and pinning in [docs/ACP-ADOPTION-PLAN.md](docs/ACP-ADOPTION-PLAN.md)) and prepares the login. The login is your Antigravity subscription: on your first ACP message the server opens the Google login in your browser, and you sign in with the same account and plan you use for the Antigravity CLI (`agy`). It is no different from logging into the CLI; the server just keeps its own token file on your machine, like any Google tool, and this extension never sees your credentials. If you also export `GEMINI_API_KEY`, it is ignored: the server uses the auth type in settings.json, and setup always writes `oauth-personal`. A session start self-heals the same way, silently when everything is ready. Manual instructions (`/agy acp-auth`) surface only when a step fails. Sessions are engine-scoped, so switching engines never crosses conversations.
28
+ Switch with `/agy engine acp|stream-json` (takes effect on restart). Setup is automatic: switching to `acp` installs Google's official ACP server binary from the [antigravity-acp registry entry](https://github.com/agentclientprotocol/registry) (`~/.local/opt/agy-acp/<build>/` + a `current` symlink, zip sha256 recorded; layout and pinning in [docs/ACP-ADOPTION-PLAN.md](docs/ACP-ADOPTION-PLAN.md)) and prepares the login. The login is your Antigravity subscription: on your first ACP message the server opens the Google login in your browser, and you sign in with the same account and plan you use for the Antigravity CLI (`agy`). If no browser is available (an SSH session on a remote machine), pi shows the sign-in URL to copy, plus the ssh port-forward command for the login redirect. It is no different from logging into the CLI; the server just keeps its own token file on your machine, like any Google tool, and this extension never sees your credentials. If you also export `GEMINI_API_KEY`, it is ignored: the server uses the auth type in settings.json, and setup always writes `oauth-personal`. A session start self-heals the same way, silently when everything is ready. Manual instructions (`/agy acp-auth`) surface only when a step fails. Sessions are engine-scoped, so switching engines never crosses conversations.
29
29
 
30
30
  ## What it cannot do
31
31
 
@@ -104,11 +104,12 @@ If `agy models` fails at load (binary missing, auth not done, network stall), a
104
104
 
105
105
  | Key | Values | Default |
106
106
  | --- | --- | --- |
107
+ | `askTool` | `on` (register the AskAntigravity delegation tool), `off` (no delegation tool; provider and models only) | `on` |
107
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.
112
113
 
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.
114
115
 
@@ -123,8 +124,9 @@ The `activate_skill` catalog mirrors pi's directory-based skill discovery: the t
123
124
  /agy mode plan review-only: agy plans but writes nothing
124
125
  /agy mode accept-edits agy applies edits directly (default)
125
126
  /agy permissions on|off auto-approve / prompt for tool calls (see warning)
126
- /agy model flash|pro|gemini default model alias for the AskAntigravity tool
127
- /agy thinking low|medium|high default thinking tier for the AskAntigravity tool
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
128
130
  /agy digest on|off inject pi-side context into agy prompts (default off; see table above)
129
131
  /agy system-prompt on|off send pi's system prompt + AGENTS.md to new agy conversations (default on)
130
132
  /agy bridge all|mcp|none which pi tools the MCP bridge exposes to agy (default all; none = bridge off)
@@ -152,7 +154,7 @@ For isolation when running any agent that executes commands without a confirmati
152
154
  | Variable | Purpose |
153
155
  | --- | --- |
154
156
  | `AGY_BIN` | Path to the agy binary. Defaults to `agy` on PATH. |
155
- | `AGY_ENGINE` | Turn engine: `stream-json` (default) or `acp`. Wins over the config file. |
157
+ | `AGY_ENGINE` | Engine: `stream-json` (default) or `acp`. Wins over the config file. |
156
158
  | `AGY_ACP_BIN` | Path to the ACP server binary (`agy_acp_server.par`). Defaults to `agy_acp_server.par` on PATH. Wins over `config.acp.bin`. When neither points at a binary, auto-setup installs one. |
157
159
  | `AGY_EXTRA_ARGS` | Extra args appended to every invocation. Whitespace-split. |
158
160
  | `AGY_CONVERSATIONS_DIR` | Override the conversations DB directory. |
@@ -2,7 +2,7 @@
2
2
 
3
3
  How the provider works internally. For build/test/debug workflow see [DEVELOPMENT.md](./DEVELOPMENT.md).
4
4
 
5
- ## Turn engine
5
+ ## Engine
6
6
 
7
7
  The provider ships two turn engines behind one contract (`TurnDriver`,
8
8
  `src/driver-types.ts`): the default **stream-json engine** (below) and the
@@ -40,6 +40,7 @@ import { SessionStore } from "../src/sessions.js";
40
40
  import { ToolRoundTrips, WrapperReplay, createStreamSimple } from "../src/provider.js";
41
41
  import { AgyDriver } from "../src/driver.js";
42
42
  import { AcpDriver } from "../src/acp/driver.js";
43
+ import { setupAuthUrlCapture } from "../src/acp/browser-capture.js";
43
44
  import { ensureAcpReady, inspectAcpSetup } from "../src/acp/setup.js";
44
45
  import type { TurnDriver } from "../src/driver-types.js";
45
46
  import { CONFIG_PATH, loadConfig, saveConfig, type AgyMode, type BridgeTools, type Engine, type ThinkingTier } from "../src/config.js";
@@ -58,6 +59,12 @@ import { mapAgyToolToNative } from "../src/native-tools.js";
58
59
  import { Type } from "typebox";
59
60
  import { patchStatus, restorePatch } from "../src/patch-cleanup.js";
60
61
 
62
+ // Last UI seen (session_start / /agy commands). The ACP login URL arrives
63
+ // via the driver log sink, which has no command context; the stash lets that
64
+ // sink toast instead of only logging to stderr. Module scope: both the
65
+ // default export (session_start, log sink) and registerAgyCommand assign it.
66
+ let activeUi: ExtensionUIContext | null = null;
67
+
61
68
  function resolveAgyBinary(): string {
62
69
  return process.env.AGY_BIN || "agy";
63
70
  }
@@ -102,6 +109,17 @@ export default async function (pi: ExtensionAPI): Promise<void> {
102
109
  // ACP self-heal runs once per process (session_start re-fires on /reload;
103
110
  // a ready setup is two file stats, so re-running is harmless anyway).
104
111
  let acpSelfHealRan = false;
112
+ // OAuth URL capture: the server hands the login URL only to the
113
+ // browser-open call (nothing on stdio), so a BROWSER wrapper records it
114
+ // and the driver logs it as "auth-url". Local users keep the automatic
115
+ // browser open; over SSH the URL surfaces for copy-paste with the
116
+ // port-forward command.
117
+ const authCapture = setupAuthUrlCapture();
118
+ if (!authCapture && process.platform !== "win32") {
119
+ // Rare (unwritable data dir). Surfacing the diagnostic: without it,
120
+ // login URLs would silently stop appearing on headless boxes.
121
+ console.error("[antigravity-bridge] OAuth URL capture unavailable; login URL surfacing is off (setup failed).");
122
+ }
105
123
  // Two turn engines behind one contract (plan §9): stream-json (tested
106
124
  // default) and the official ACP server (opt-in via config.engine, off by
107
125
  // default). Neither spawns anything until its first turn.
@@ -121,7 +139,17 @@ export default async function (pi: ExtensionAPI): Promise<void> {
121
139
  // Resolved per connection: the setup flow can install the binary and
122
140
  // update acp.bin mid-session; the next turn picks it up (no restart).
123
141
  bin: () => loadConfig().acp.bin,
142
+ ...(authCapture ? { extraEnv: authCapture.browserEnv, authUrlFile: authCapture.file } : {}),
124
143
  log: (msg, data) => {
144
+ if (msg === "auth-url") {
145
+ const { url, port } = (data ?? {}) as { url?: string; port?: number | null };
146
+ if (!url) return;
147
+ const ssh = port ? `\nSSH session? Forward the port on your machine first:\n ssh -N -L ${port}:127.0.0.1:${port} <user@host>` : "";
148
+ const text = `Google sign-in URL for the ACP engine:\n${url}${ssh}`;
149
+ if (activeUi) activeUi.notify(text, "warning");
150
+ else console.error(`[antigravity-bridge acp] ${text}`);
151
+ return;
152
+ }
125
153
  if (!acpFailures.has(msg)) return;
126
154
  console.error(`[antigravity-bridge acp] ${msg}${data !== undefined ? " " + JSON.stringify(data) : ""}`);
127
155
  },
@@ -209,8 +237,13 @@ export default async function (pi: ExtensionAPI): Promise<void> {
209
237
  // AskAntigravity tool: one-shot delegation to agy (ported from
210
238
  // pi-ask-antigravity). When both extensions are installed, the bridge wins
211
239
  // and pi-ask-antigravity registers nothing (its load-time defer guard
212
- // detects this package via import.meta.resolve).
213
- await registerAskAntigravityTool(pi, toolModels);
240
+ // detects this package via import.meta.resolve). Opt-out: askTool=false
241
+ // (config file, AGY_ASK_TOOL, /agy ask, or the picker) skips registration
242
+ // entirely - users who want only the provider keep a clean tool list.
243
+ // Note: the active flag below is set regardless of askTool, so
244
+ // pi-ask-antigravity keeps deferring even then: off means NO delegation
245
+ // tool from either package, not a fallback to pi-ask-antigravity.
246
+ if (loadConfig().askTool) await registerAskAntigravityTool(pi, toolModels);
214
247
 
215
248
  // Display-only wrapper tool: the provider emits mutating agy steps as
216
249
  // toolCalls against it (never re-executed - execute() replays the output
@@ -236,6 +269,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
236
269
  // normal toolUse loop (native cards, permissions, hooks) - no patch, no
237
270
  // privileged API. Started on session_start, torn down on session_shutdown.
238
271
  pi.on("session_start", async (_event, ctx) => {
272
+ if (ctx.hasUI) activeUi = ctx.ui;
239
273
  // Legacy cleanup: users who ran the old consent-gated patcher still
240
274
  // carry pi.invokeTool in their installed pi. Inert, but tell them once
241
275
  // and offer /agy patch-cleanup. Never auto-edits the install.
@@ -265,9 +299,8 @@ export default async function (pi: ExtensionAPI): Promise<void> {
265
299
  saveConfig({ acp: { bin: status.bin, permissions: loadConfig().acp.permissions } });
266
300
  }
267
301
  if (status.needsLogin) {
268
- const msg =
269
- "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.";
270
- if (ctx.hasUI) ctx.ui.notify(msg, "info");
302
+ const msg = acpLoginPending("Your next Antigravity message opens the Google sign-in page in your browser.");
303
+ if (ctx.hasUI) ctx.ui.notify(msg, "warning");
271
304
  else console.error(`[antigravity-bridge] ${msg}`);
272
305
  }
273
306
  return;
@@ -368,6 +401,9 @@ export default async function (pi: ExtensionAPI): Promise<void> {
368
401
  }
369
402
  });
370
403
  pi.on("session_shutdown", async () => {
404
+ // The UI is going away; a later auth-url must fall back to stderr
405
+ // instead of toasting into a dead UI (where it would be lost).
406
+ activeUi = null;
371
407
  const h = mcpHandle;
372
408
  mcpHandle = null;
373
409
  await h?.close();
@@ -379,6 +415,14 @@ export default async function (pi: ExtensionAPI): Promise<void> {
379
415
 
380
416
  // --- /agy command -----------------------------------------------------------
381
417
 
418
+ /** Shared body for every ACP-login-pending moment (session_start self-heal,
419
+ * /agy engine acp, picker). End-user simple: what happens, when, which
420
+ * account. The server opens the browser itself, so there is no URL to
421
+ * open manually. `browserTrigger` says when that happens. */
422
+ function acpLoginPending(browserTrigger: string): string {
423
+ return `One-time sign-in needed to finish ACP setup. ${browserTrigger} Use the Google account of your Antigravity subscription (the same account as your agy CLI login). If no browser opens, pi shows the sign-in URL to copy. The token stays on your machine; this extension never sees it.`;
424
+ }
425
+
382
426
  interface AgyCommandCtx {
383
427
  entries: AgyModelEntry[];
384
428
  store: SessionStore;
@@ -395,6 +439,7 @@ interface PendingConfig {
395
439
  skipPermissions?: boolean;
396
440
  defaultModel?: string;
397
441
  defaultThinking?: ThinkingTier;
442
+ askTool?: boolean;
398
443
  bridgeTools?: BridgeTools;
399
444
  digest?: boolean;
400
445
  systemPrompt?: boolean;
@@ -404,21 +449,25 @@ function statusText(ctx: AgyCommandCtx): string {
404
449
  const config = loadConfig();
405
450
  const source = ctx.usingFallback ? "fallback (agy models failed)" : "discovered";
406
451
  const perm = config.skipPermissions ? "auto-approved (DANGEROUS)" : "prompt (hangs in -p)";
452
+ // padEnd keyed to the longest label ("AskAntigravity thinking:") so the
453
+ // value column stays aligned as labels grow.
454
+ const row = (label: string, value: string) => ` ${label.padEnd(24)} ${value}`;
407
455
  return [
408
456
  "Antigravity bridge",
409
- ` engine: ${config.engine}${config.engine === "acp" ? " (official server, opt-in)" : ""}`,
410
- ` models: ${ctx.entries.length} ${source}`,
411
- ` mode: ${config.mode}`,
412
- ` permissions: ${perm}`,
413
- ` tool model: ${config.defaultModel}`,
414
- ` tool thinking: ${config.defaultThinking}`,
415
- ` sessions: ${ctx.store.size} bound`,
416
- ` config: ${CONFIG_PATH}`,
417
- ` bridge tools: ${config.bridgeTools}`,
418
- ` digest: ${config.digest ? "on" : "off"}`,
419
- ` system prompt: ${config.systemPrompt ? "on" : "off"}`,
457
+ row("engine:", `${config.engine}${config.engine === "acp" ? " (official server, opt-in)" : ""}`),
458
+ row("models:", `${ctx.entries.length} ${source}`),
459
+ row("mode:", config.mode),
460
+ row("permissions:", perm),
461
+ row("AskAntigravity tool:", config.askTool ? "on" : "off"),
462
+ row("AskAntigravity model:", config.defaultModel),
463
+ row("AskAntigravity thinking:", config.defaultThinking),
464
+ row("sessions:", `${ctx.store.size} bound`),
465
+ row("config:", CONFIG_PATH),
466
+ row("bridge tools:", config.bridgeTools),
467
+ row("digest:", config.digest ? "on" : "off"),
468
+ row("system prompt:", config.systemPrompt ? "on" : "off"),
420
469
  "",
421
- "Subcommands: /agy engine stream-json|acp, /agy mode plan|accept-edits, /agy permissions on|off, /agy bridge all|mcp|none, /agy model <alias>, /agy thinking low|medium|high, /agy digest on|off, /agy system-prompt on|off, /agy acp-bin <path|auto>, /agy acp-auth, /agy patch-cleanup, /agy clear",
470
+ "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",
422
471
  ].join("\n");
423
472
  }
424
473
 
@@ -426,9 +475,10 @@ function statusText(ctx: AgyCommandCtx): string {
426
475
  function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
427
476
  pi.registerCommand("agy", {
428
477
  description:
429
- "Antigravity provider: status, doctor, settings picker, clear sessions. Usage: /agy [status|doctor|engine stream-json|acp|mode plan|accept-edits|permissions on|off|bridge all|mcp|none|model <alias>|thinking low|medium|high|digest on|off|system-prompt on|off|acp-bin <path|auto>|acp-auth|patch-cleanup|clear]",
478
+ "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]",
430
479
  handler: async (args, cmdCtx: ExtensionCommandContext) => {
431
480
  const ui = cmdCtx.ui;
481
+ if (ui) activeUi = ui;
432
482
  const mode = cmdCtx.mode;
433
483
  const sub = (args ?? "").trim().split(/\s+/)[0]?.toLowerCase();
434
484
  const val = (args ?? "").trim().split(/\s+/)[1]?.toLowerCase();
@@ -483,12 +533,14 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
483
533
  return;
484
534
  }
485
535
  saveConfig({ acp: { bin: status.bin, permissions: loadConfig().acp.permissions } });
486
- ui?.notify(
487
- status.needsLogin
488
- ? "ACP engine ready. Your first ACP message opens the Google login in your browser: sign in with the account of your Antigravity subscription (the same login as the Antigravity CLI, agy). One-time; tokens stay on your machine and this extension never sees them. Takes effect on the next pi start (or /reload)."
489
- : `ACP engine ready (auth: ${status.auth}). Takes effect on the next pi start (or /reload).`,
490
- "info",
491
- );
536
+ if (status.needsLogin) {
537
+ ui?.notify(
538
+ `ACP engine set. ${acpLoginPending("After the restart (pi restart or /reload), your first Antigravity message opens the Google sign-in page in your browser.")}`,
539
+ "warning",
540
+ );
541
+ } else {
542
+ ui?.notify(`ACP engine ready (auth: ${status.auth}). Takes effect on the next pi start (or /reload).`, "info");
543
+ }
492
544
  } else {
493
545
  ui?.notify(`current engine: ${loadConfig().engine}\nusage: /agy engine stream-json|acp`, "info");
494
546
  }
@@ -529,8 +581,10 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
529
581
  " the antigravity-acp registry; point acp.bin or AGY_ACP_BIN at it.",
530
582
  '2. Default: put {"auth":{"type":"oauth-personal"}} in',
531
583
  " ~/.gemini/antigravity-acp/settings.json, run one turn, and complete the",
532
- " Google login that opens in your browser (headless: tunnel 127.0.0.1:<port>",
533
- " over ssh, then open the URL on your machine).",
584
+ " Google login that opens in your browser. No browser (SSH session)?",
585
+ " pi shows the sign-in URL to copy; forward the redirect port over ssh",
586
+ " (ssh -N -L <port>:127.0.0.1:<port> <user@host>), then open the URL",
587
+ " on your machine.",
534
588
  ' Headless alternative: GEMINI_API_KEY + {"auth":{"type":"gemini-api-key"}}',
535
589
  " (metered paid API - not your Antigravity plan). The key is used only",
536
590
  " when that type is selected; with the default oauth-personal in place,",
@@ -617,9 +671,23 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
617
671
  if (sub === "model") {
618
672
  if (val && val.length > 0) {
619
673
  const next = saveConfig({ defaultModel: val });
620
- ui?.notify(`tool default model set to ${next.defaultModel}`, "info");
674
+ ui?.notify(`AskAntigravity default model set to ${next.defaultModel}`, "info");
621
675
  } else {
622
- ui?.notify(`tool model: ${loadConfig().defaultModel}\nusage: /agy model flash|pro|gemini|<exact>`, "info");
676
+ ui?.notify(`AskAntigravity model: ${loadConfig().defaultModel} (fallback; callers may override per call)\nusage: /agy model flash|pro|gemini|<exact>`, "info");
677
+ }
678
+ return;
679
+ }
680
+ if (sub === "ask") {
681
+ if (val === "on" || val === "off") {
682
+ const next = saveConfig({ askTool: val === "on" });
683
+ ui?.notify(
684
+ next.askTool
685
+ ? "AskAntigravity tool on. Registered on the next pi start (or /reload)."
686
+ : "AskAntigravity tool off. It is removed from the model's tool list on the next pi start (or /reload). Provider and models stay.",
687
+ "info",
688
+ );
689
+ } else {
690
+ ui?.notify(`AskAntigravity tool: ${loadConfig().askTool ? "on" : "off"}\nusage: /agy ask on|off`, "info");
623
691
  }
624
692
  return;
625
693
  }
@@ -655,9 +723,9 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
655
723
  if (sub === "thinking") {
656
724
  if (val === "low" || val === "medium" || val === "high") {
657
725
  const next = saveConfig({ defaultThinking: val as ThinkingTier });
658
- ui?.notify(`tool default thinking set to ${next.defaultThinking}`, "info");
726
+ ui?.notify(`AskAntigravity default thinking set to ${next.defaultThinking}`, "info");
659
727
  } else {
660
- ui?.notify(`tool thinking: ${loadConfig().defaultThinking}\nusage: /agy thinking low|medium|high`, "info");
728
+ ui?.notify(`AskAntigravity thinking: ${loadConfig().defaultThinking} (fallback; callers may override per call)\nusage: /agy thinking low|medium|high`, "info");
661
729
  }
662
730
  return;
663
731
  }
@@ -679,10 +747,10 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
679
747
  });
680
748
  }
681
749
 
682
- /** Interactive settings picker (TUI only). Rows: the full runtime config
683
- * surface (mode, permissions, model, thinking, bridge, digest, system
684
- * prompt). Engine stays a subcommand: switching runs install + auth setup
685
- * and needs a restart. */
750
+ /** Interactive settings picker (TUI only). Rows: the runtime config surface
751
+ * (mode, permissions, model, thinking, bridge, digest, system prompt).
752
+ * Engine switching is command-only: /agy engine stream-json|acp (the
753
+ * command also runs self-service binary + auth setup for acp). */
686
754
  async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promise<void> {
687
755
  const config = loadConfig();
688
756
  const pending: PendingConfig = {};
@@ -704,19 +772,27 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
704
772
  currentValue: config.skipPermissions ? "auto-approved" : "prompt",
705
773
  values: ["auto-approved", "prompt"],
706
774
  },
775
+ {
776
+ id: "ask",
777
+ label: "AskAntigravity tool",
778
+ description:
779
+ "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).",
780
+ currentValue: config.askTool ? "on" : "off",
781
+ values: ["on", "off"],
782
+ },
707
783
  {
708
784
  id: "model",
709
- label: "Tool default model",
785
+ label: "AskAntigravity model",
710
786
  description:
711
- "Alias used when the AskAntigravity tool omits its model param. flash/pro/gemini, or an exact id.",
787
+ "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.",
712
788
  currentValue: config.defaultModel,
713
789
  values: ["flash", "pro", "gemini"],
714
790
  },
715
791
  {
716
792
  id: "thinking",
717
- label: "Tool default thinking",
793
+ label: "AskAntigravity thinking",
718
794
  description:
719
- "Thinking tier used when the model alias names none. Pro has no Medium; it falls back to nearest.",
795
+ "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.",
720
796
  currentValue: config.defaultThinking,
721
797
  values: ["low", "medium", "high"],
722
798
  },
@@ -764,6 +840,8 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
764
840
  pending.defaultModel = newValue;
765
841
  } else if (id === "thinking") {
766
842
  pending.defaultThinking = newValue as ThinkingTier;
843
+ } else if (id === "ask") {
844
+ pending.askTool = newValue === "on";
767
845
  } else if (id === "bridge") {
768
846
  pending.bridgeTools = newValue as BridgeTools;
769
847
  } else if (id === "digest") {
@@ -788,8 +866,15 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
788
866
 
789
867
  if (Object.keys(pending).length === 0) return;
790
868
 
791
- if (pending.mode === "plan" && ctx.engine === "acp") {
792
- ui.notify("the ACP engine has no plan mode (RC01). Switch /agy engine stream-json first.", "warning");
869
+ // The picker cannot switch engines (command-only: /agy engine), but the
870
+ // mode row can still produce plan while the latched engine is acp. ACP
871
+ // has no review-only mode (RC01); refuse the combination.
872
+ const nextMode = pending.mode ?? config.mode;
873
+ if (nextMode === "plan" && ctx.engine === "acp") {
874
+ ui.notify(
875
+ "plan + acp is not supported (RC01): the ACP engine has no review-only mode. /agy engine stream-json first, or /agy mode accept-edits.",
876
+ "warning",
877
+ );
793
878
  return;
794
879
  }
795
880
 
@@ -800,8 +885,9 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
800
885
  pending.skipPermissions !== undefined
801
886
  ? `permissions=${next.skipPermissions ? "auto-approved" : "prompt"}`
802
887
  : null,
803
- pending.defaultModel !== undefined ? `tool model=${next.defaultModel}` : null,
804
- pending.defaultThinking !== undefined ? `tool thinking=${next.defaultThinking}` : null,
888
+ pending.askTool !== undefined ? `AskAntigravity tool=${next.askTool ? "on" : "off"}` : null,
889
+ pending.defaultModel !== undefined ? `AskAntigravity model=${next.defaultModel}` : null,
890
+ pending.defaultThinking !== undefined ? `AskAntigravity thinking=${next.defaultThinking}` : null,
805
891
  pending.bridgeTools !== undefined ? `bridge=${next.bridgeTools}` : null,
806
892
  pending.digest !== undefined ? `digest=${next.digest ? "on" : "off"}` : null,
807
893
  pending.systemPrompt !== undefined ? `system-prompt=${next.systemPrompt ? "on" : "off"}` : null,
@@ -815,5 +901,4 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
815
901
  "error",
816
902
  );
817
903
  }
818
- void ctx;
819
904
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estebanforge/pi-antigravity-bridge",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
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",
@@ -0,0 +1,104 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ // The ACP server hands the Google OAuth URL ONLY to the browser-open call:
6
+ // nothing on stdout or stderr, headless or not (verified per
7
+ // docs/ACP-PROTOCOL-REFERENCE.md). The capture makes the URL visible:
8
+ // BROWSER points at a wrapper that appends the URL to a record file and then
9
+ // execs the real opener, and the connection watches the record file to log
10
+ // "auth-url". Local users keep the automatic browser open; over SSH the user
11
+ // copies the URL and forwards the redirect port.
12
+
13
+ export interface AuthUrlCapture {
14
+ /** Spawn env for the ACP server (BROWSER -> wrapper script). */
15
+ browserEnv: Record<string, string>;
16
+ /** Record file the connection watches. */
17
+ file: string;
18
+ /** Most recent URL in the record file, or null. */
19
+ lastUrl(): string | null;
20
+ }
21
+
22
+ /** First existing executable named `name` on `env.PATH`, or null. */
23
+ export function findOnPath(name: string, env: NodeJS.ProcessEnv = process.env): string | null {
24
+ for (const dir of (env.PATH ?? "").split(path.delimiter)) {
25
+ if (!dir) continue;
26
+ const candidate = path.join(dir, name);
27
+ try {
28
+ const st = fs.statSync(candidate);
29
+ if (st.isFile() && (st.mode & 0o111) !== 0) return candidate;
30
+ } catch {
31
+ /* not here */
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+
37
+ /** Last http(s) line of `file`, or null (absent/unreadable counts as empty). */
38
+ export function readLastUrl(file: string): string | null {
39
+ try {
40
+ const lines = fs.readFileSync(file, "utf8").split("\n").filter((l) => /^https?:\/\//.test(l));
41
+ return lines.at(-1) ?? null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ /** Port of the OAuth loopback redirect (redirect_uri param), or null. */
48
+ export function parseAuthPort(url: string): number | null {
49
+ try {
50
+ const redirect = new URL(url).searchParams.get("redirect_uri");
51
+ if (!redirect) return null;
52
+ const port = new URL(redirect).port;
53
+ return port ? Number(port) : null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ const shQuote = (s: string): string => `'${s.replace(/'/g, "'\\''")}'`;
60
+
61
+ /** Create the capture (record file + wrapper script) and return the spawn
62
+ * env. Idempotent: the wrapper is rewritten only when content changes. The
63
+ * opener to forward to is resolved once, at write time: an existing BROWSER
64
+ * wins, then xdg-open, then macOS open; with none available the wrapper
65
+ * records the URL and exits (headless: capture without an opener). The
66
+ * forward word-splits the BROWSER value, so flag forms (`open -a Chrome`)
67
+ * work; a BROWSER path containing spaces does not (rare, and BROWSER lists
68
+ * are not honored either - the first entry is what chains). Windows browser
69
+ * resolution differs; no capture there. */
70
+ export function setupAuthUrlCapture(dataDir = path.join(os.homedir(), ".pi", "agent", "antigravity-bridge"), env: NodeJS.ProcessEnv = process.env): AuthUrlCapture | null {
71
+ if (process.platform === "win32") return null;
72
+ const file = path.join(dataDir, "acp-auth-urls.log");
73
+ const wrapper = path.join(dataDir, "acp-browser-wrapper.sh");
74
+ try {
75
+ fs.mkdirSync(dataDir, { recursive: true });
76
+ // Truncate: the file mirrors the current login attempt only. OAuth
77
+ // URLs carry state + PKCE challenges, so keep the record private.
78
+ fs.writeFileSync(file, "", { mode: 0o600 });
79
+ const opener = env.BROWSER?.trim() || findOnPath("xdg-open", env) || findOnPath("open", env) || "";
80
+ const body = [
81
+ "#!/usr/bin/env sh",
82
+ "# Antigravity bridge: records the Google OAuth URL the ACP server hands",
83
+ "# to the browser, then forwards to the real opener. The bridge watches",
84
+ "# the record file and shows the URL in pi (copy it when logging in over",
85
+ "# SSH; forward the redirect port first).",
86
+ `printf '%s\\n' "$@" >> ${shQuote(file)}`,
87
+ `REAL=${shQuote(opener)}`,
88
+ 'if [ -n "$REAL" ]; then set -f; exec $REAL "$@"; fi',
89
+ "",
90
+ ].join("\n");
91
+ const write = (p: string, content: string, mode: number): void => {
92
+ try {
93
+ if (fs.readFileSync(p, "utf8") === content) return;
94
+ } catch {
95
+ /* new file */
96
+ }
97
+ fs.writeFileSync(p, content, { mode });
98
+ };
99
+ write(wrapper, body, 0o755);
100
+ return { browserEnv: { BROWSER: wrapper }, file, lastUrl: () => readLastUrl(file) };
101
+ } catch {
102
+ return null; // capture is best effort; login still works without it
103
+ }
104
+ }
@@ -20,8 +20,10 @@
20
20
  // LIST; a dead URL is accepted at session time (lazy connect)
21
21
 
22
22
  import { spawn, type ChildProcess } from "node:child_process";
23
+ import fs from "node:fs";
23
24
  import os from "node:os";
24
25
  import path from "node:path";
26
+ import { parseAuthPort, readLastUrl } from "./browser-capture.js";
25
27
  import { JsonRpcResponseError, JsonRpcSession } from "./jsonrpc.js";
26
28
 
27
29
  export interface AcpMcpServer {
@@ -63,6 +65,11 @@ export interface AcpConnectionOptions {
63
65
  * "auto" selects the first allow option; "deny" fail-closes to the first
64
66
  * reject option. Absent = deny (fail closed). */
65
67
  permissions?: () => "auto" | "deny";
68
+ /** Record file for BROWSER-captured OAuth URLs (src/acp/browser-capture.ts).
69
+ * The server hands the login URL only to the browser-open call, so the
70
+ * wrapper records it there; the connection watches the file and logs
71
+ * "auth-url" {url, port} for every new URL. */
72
+ authUrlFile?: string;
66
73
  }
67
74
 
68
75
  const INIT_TIMEOUT_MS = 30_000;
@@ -80,6 +87,8 @@ export class AcpConnection {
80
87
  agentInfo: Record<string, unknown> | undefined;
81
88
  /** Last known mode/model config echo from set_config_option results. */
82
89
  lastConfigOptions: unknown = undefined;
90
+ #authUrlWatcher: fs.FSWatcher | undefined;
91
+ #lastAuthUrl: string | null = null;
83
92
 
84
93
  constructor(opts: AcpConnectionOptions) {
85
94
  this.#opts = opts;
@@ -117,6 +126,7 @@ export class AcpConnection {
117
126
  ...(this.#opts.extraEnv ? { env: { ...process.env, ...this.#opts.extraEnv } } : {}),
118
127
  });
119
128
  this.#child = child;
129
+ if (this.#opts.authUrlFile) this.#watchAuthUrl();
120
130
  child.stdout?.setEncoding("utf8");
121
131
  child.stderr?.setEncoding("utf8");
122
132
  // Pipe failures arrive asynchronously as stream 'error' events; the sync
@@ -373,9 +383,36 @@ export class AcpConnection {
373
383
  }
374
384
  }
375
385
 
386
+ /** Surface OAuth login URLs captured by the BROWSER wrapper: every new
387
+ * URL in the record file is logged once as "auth-url" {url, port}. Best
388
+ * effort only - a broken watch never affects turns. */
389
+ #watchAuthUrl(): void {
390
+ const file = this.#opts.authUrlFile!;
391
+ try {
392
+ const watcher = fs.watch(file, () => this.#emitAuthUrl());
393
+ // Without a listener, a watch 'error' event escapes as uncaught.
394
+ watcher.on("error", () => {
395
+ /* record file gone: login URL surfacing is off until respawn */
396
+ });
397
+ this.#authUrlWatcher = watcher;
398
+ } catch {
399
+ /* no watch; the one-shot check below still covers earlier writes */
400
+ }
401
+ this.#emitAuthUrl(); // URLs recorded before the watch started
402
+ }
403
+
404
+ #emitAuthUrl(): void {
405
+ const url = readLastUrl(this.#opts.authUrlFile!);
406
+ if (!url || url === this.#lastAuthUrl) return;
407
+ this.#lastAuthUrl = url;
408
+ this.#opts.log("auth-url", { url, port: parseAuthPort(url) });
409
+ }
410
+
376
411
  #finish(reason: string): void {
377
412
  if (this.#exited) return;
378
413
  this.#exited = true;
414
+ this.#authUrlWatcher?.close();
415
+ this.#authUrlWatcher = undefined;
379
416
  this.abortAll(`connection exited: ${reason || "process gone"}`);
380
417
  this.#opts.onExit({ code: null, signal: null, stderrTail: this.#stderrTail });
381
418
  }
package/src/acp/driver.ts CHANGED
@@ -41,6 +41,9 @@ export interface AcpDriverOptions {
41
41
  /** Extra argv for the binary (tests: node + fake-server script). */
42
42
  binArgs?: string[];
43
43
  extraEnv?: Record<string, string>;
44
+ /** Record file for BROWSER-captured OAuth URLs; passed to the connection,
45
+ * which logs "auth-url" when the server tries to open a login. */
46
+ authUrlFile?: string;
44
47
  /** Bridge registration for session/new AND session/load. */
45
48
  mcpServers?: () => AcpMcpServer[];
46
49
  log?: (msg: string, data?: unknown) => void;
@@ -422,6 +425,7 @@ export class AcpDriver implements TurnDriver {
422
425
  bin: resolveAcpBinary(typeof this.#opts.bin === "function" ? this.#opts.bin() : this.#opts.bin),
423
426
  binArgs: this.#opts.binArgs,
424
427
  extraEnv: this.#opts.extraEnv,
428
+ authUrlFile: this.#opts.authUrlFile,
425
429
  cwd: request.cwd,
426
430
  mcpServers: this.#opts.mcpServers,
427
431
  log: (msg, data) => this.#log(msg, data),
package/src/acp/setup.ts CHANGED
@@ -227,6 +227,18 @@ export interface AuthState {
227
227
  type?: string;
228
228
  }
229
229
 
230
+ /** Token presence WITHOUT reading any credential value: acp_token.json is
231
+ * stat()ed, never opened. The token only exists after the browser login
232
+ * round-trip, so this is the real proof of a completed login. */
233
+ function tokenPresent(target: string): boolean {
234
+ try {
235
+ fs.statSync(path.join(target, "acp_token.json"));
236
+ return true;
237
+ } catch {
238
+ return false;
239
+ }
240
+ }
241
+
230
242
  /** Auth state WITHOUT reading any credential value: settings.json carries
231
243
  * only auth.type (+ non-secret gcp placement), acp_token.json is stat()ed. */
232
244
  export function readAuthState(dir?: string): AuthState {
@@ -240,12 +252,7 @@ export function readAuthState(dir?: string): AuthState {
240
252
  } catch {
241
253
  /* absent or garbage = unconfigured */
242
254
  }
243
- try {
244
- fs.statSync(path.join(target, "acp_token.json"));
245
- return { configured: true, type: "token" };
246
- } catch {
247
- return { configured: false };
248
- }
255
+ return tokenPresent(target) ? { configured: true, type: "token" } : { configured: false };
249
256
  }
250
257
 
251
258
  /** Write a minimal auth block into settings.json. The server reads this file
@@ -344,12 +351,18 @@ export async function ensureAcpReady(opts: SetupOptions = {}): Promise<AcpSetupS
344
351
  };
345
352
  }
346
353
  }
354
+ // settings.json alone proves nothing: setup writes it before the login,
355
+ // the token file only appears after the browser round-trip. oauth-personal
356
+ // without a token = login still pending, and every caller (switch toast,
357
+ // session_start self-heal) must keep saying so until the token exists.
358
+ // gemini-api-key has no browser login; a present token file means done.
359
+ const needsLogin = auth.type !== "gemini-api-key" && !tokenPresent(gdir);
347
360
  return {
348
361
  ok: true,
349
362
  bin: binary.bin!,
350
363
  binarySource: binary.source!,
351
364
  auth: auth.type ?? "token",
352
- needsLogin: false,
365
+ needsLogin,
353
366
  actions,
354
367
  };
355
368
  }
package/src/config.ts CHANGED
@@ -39,7 +39,7 @@ export interface AcpConfig {
39
39
  }
40
40
 
41
41
  export interface AgyConfig {
42
- /** Turn engine. Switching requires a pi restart (drivers wire at load). */
42
+ /** Engine. Switching requires a pi restart (drivers wire at load). */
43
43
  engine: Engine;
44
44
  /** Official-server ACP engine options (used when engine = "acp"). */
45
45
  acp: AcpConfig;
@@ -54,6 +54,11 @@ 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. */
@@ -96,6 +101,7 @@ const DEFAULTS: AgyConfig = {
96
101
  skipPermissions: true,
97
102
  defaultModel: "flash",
98
103
  defaultThinking: "medium",
104
+ askTool: true,
99
105
  bridgeTools: "all",
100
106
  digest: false,
101
107
  systemPrompt: true,
@@ -149,6 +155,10 @@ export function loadConfig(configPath: string = CONFIG_PATH): AgyConfig {
149
155
  const defaultThinking: ThinkingTier =
150
156
  thinkRaw === "low" || thinkRaw === "high" ? thinkRaw : "medium";
151
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
+
152
162
  const bridgeRaw = (process.env.AGY_BRIDGE_TOOLS ?? file.bridgeTools ?? DEFAULTS.bridgeTools).toLowerCase();
153
163
  const bridgeTools: BridgeTools =
154
164
  bridgeRaw === "none" || bridgeRaw === "all" || bridgeRaw === "mcp"
@@ -179,6 +189,7 @@ export function loadConfig(configPath: string = CONFIG_PATH): AgyConfig {
179
189
  skipPermissions,
180
190
  defaultModel,
181
191
  defaultThinking,
192
+ askTool,
182
193
  bridgeTools,
183
194
  digest,
184
195
  systemPrompt,