@estebanforge/pi-ask-antigravity 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  A [Pi](https://github.com/earendil-works/pi-coding-agent) extension that exposes the **`AskAntigravity`** tool: delegate a self-contained sub-task to Google Antigravity's `agy` CLI and stream its response back into the Pi session.
4
4
 
5
- It is the `AskClaude`-style delegation pattern, pointed at Gemini via `agy`. The tool answers to three names the CLI is known by — **gemini**, **antigravity**, and **agy** — surfaced in its description so the model maps any of them to this single tool.
5
+ It is the `AskClaude`-style delegation pattern (from pi-claude-bride extension), pointed at Gemini via `agy`. The tool answers to three names the CLI is known by — **gemini**, **antigravity**, and **agy** — surfaced in its description so the model maps any of them to this single tool.
6
+
7
+ <img width="1512" height="845" alt="image" src="https://github.com/user-attachments/assets/3e1a8f19-64d2-43ab-a30f-47f39daa151d" />
6
8
 
7
9
  ## Install
8
10
 
@@ -49,11 +51,40 @@ Verbose `agy models` strings (`Gemini 3.5 Flash (Medium)`) are hostile to natura
49
51
  | `pro low` | latest Pro, Low |
50
52
  | `3.5 flash` | pinned version, default tier |
51
53
  | `3.1 pro` | pinned Pro version (→ High) |
54
+ | `sonnet` | Claude Sonnet 4.6 (Thinking) |
55
+ | `opus` | Claude Opus 4.6 (Thinking) |
56
+ | `gpt-oss` | GPT-OSS 120B (Medium) |
52
57
  | `Gemini 3.5 Flash (Medium)` | exact passthrough |
53
58
 
54
59
  - **Latest** = highest version number available for the family.
55
60
  - **Default tier**: Flash → Medium, Pro → High. Overridable per-config (below).
56
61
  - When a tier is unavailable for a family, the nearest is chosen with ties broken toward the higher tier (so "latest and greatest" wins).
62
+ - The non-Gemini aliases (`sonnet`, `opus`, `gpt-oss`) are a static overlay merged with the live `agy models` catalog. Live entries always win on case-insensitive full-string equality, so an updated `agy` listing takes precedence over the hardcoded fallback.
63
+
64
+ ## Execution modes (`mode`)
65
+
66
+ | Mode | Flag | Use when |
67
+ | --- | --- | --- |
68
+ | `plan` | `--mode plan` | agy reviews and plans without writing. Use for cross-review and read-only tasks. |
69
+ | `accept-edits` (default) | `--mode accept-edits` | agy applies edits directly inside the workspace. |
70
+
71
+ For agy's orthogonal `--sandbox` shell-containment flag, set `AGY_EXTRA_ARGS=--sandbox` in your environment. The extension does not expose it as a `mode` value because `--sandbox` controls command containment, not edit persistence.
72
+
73
+ Recommended pairings:
74
+
75
+ ```text
76
+ # Gemini produces, Claude reviews
77
+ AskAntigravity prompt="Implement the approved refactor" mode=accept-edits
78
+ AskAntigravity prompt="Review the resulting diff" model=opus mode=plan digest=true
79
+
80
+ # Claude produces, Gemini reviews
81
+ AskAntigravity prompt="Fix the parser root cause" model=opus mode=accept-edits
82
+ AskAntigravity prompt="Review the resulting diff" model=pro mode=plan digest=true
83
+ ```
84
+
85
+ ## Compact output (`digest`)
86
+
87
+ Set `digest: true` to prefix the prompt with `(Use compact digests, not full file contents.)` so `agy` returns summaries instead of full file payloads. Defaults on for `plan`, off for `accept-edits`. Use `true` for any read-only call (review, exploration, planning) where full file contents are noise.
57
88
 
58
89
  ## Configuration
59
90
 
@@ -82,8 +113,9 @@ Interactive picker for the default model and default thinking. If the project co
82
113
  | `prompt` | yes | Self-contained task. agy cannot see the Pi conversation. |
83
114
  | `cwd` | no | Workspace agy runs in. Defaults to the project root. |
84
115
  | `model` | no | Alias or exact id. Omit for the configured default. |
116
+ | `mode` | no | `plan` (review-only) or `accept-edits` (agy applies edits, default). |
117
+ | `digest` | no | Prefix the prompt with `(Use compact digests, not full file contents.)`. Defaults on for `plan`, off for `accept-edits`. |
85
118
  | `conversationId` | no | Omit for a one-shot (agy starts fresh). Pass the id from a prior call's result (`details.conversationId`) to resume that agy conversation with full context. See [Two modes](#two-modes-one-shot-vs-continued-conversation). |
86
- | `skipPermissions` | no | Pass `--dangerously-skip-permissions`. Needed for mutating tasks. |
87
119
  | `timeoutMinutes` | no | Hard cap on the run. Default 10. |
88
120
 
89
121
  ## Environment
@@ -37,7 +37,7 @@ import {
37
37
  getSettingsListTheme,
38
38
  type ExtensionAPI,
39
39
  } from "@earendil-works/pi-coding-agent";
40
- import { StringEnum } from "@earendil-works/pi-ai";
40
+
41
41
  import { Container, SettingsList, Text, type SettingItem } from "@earendil-works/pi-tui";
42
42
  import { Type } from "typebox";
43
43
 
@@ -66,16 +66,70 @@ const FAMILY_DEFAULT_TIER: Record<Family, ThinkingTier> = {
66
66
 
67
67
  const TIER_RANK: Record<ThinkingTier, number> = { low: 0, medium: 1, high: 2 };
68
68
 
69
+ // Mode = which agy tool-loop policy to apply. Distinct from the alias layer.
70
+ // "plan" → --mode plan (no edits; review-only)
71
+ // "accept-edits" → --mode accept-edits (agy applies edits)
72
+ //
73
+ // Note: agy's --sandbox flag is an orthogonal shell-containment setting
74
+ // (not an "edit preview" mode), so it is not exposed here. Users who need
75
+ // it can pass it via the AGY_EXTRA_ARGS env var.
76
+ type Mode = "plan" | "accept-edits";
77
+
78
+ // Static alias overlay for non-Gemini models agy may or may not surface in
79
+ // `agy models` depending on plan. When the live catalog contains an entry
80
+ // whose full string equals the overlay target, the live entry wins. When it
81
+ // does not (older agy, missing model, plan gate), the overlay entry resolves
82
+ // the alias so the user can still type "sonnet" and get a working answer.
83
+ // "sonnet" -> Claude Sonnet 4.6 (Thinking)
84
+ // "opus" -> Claude Opus 4.6 (Thinking)
85
+ // "gpt-oss" -> GPT-OSS 120B (Medium)
86
+ const STATIC_ALIAS_OVERLAY: ReadonlyArray<ModelEntry> = [
87
+ { full: "Claude Sonnet 4.6 (Thinking)", family: "other", version: null, tier: null },
88
+ { full: "Claude Opus 4.6 (Thinking)", family: "other", version: null, tier: null },
89
+ { full: "GPT-OSS 120B (Medium)", family: "other", version: null, tier: null },
90
+ ];
91
+
92
+ // Short alias → overlay full string. Used by resolveModel to recognize
93
+ // friendly short names ("sonnet") that the family-parser (flash/pro) does
94
+ // not match. The overlay entries are also merged into the live catalog for
95
+ // exact-string passthrough, so this map only needs to cover the short names.
96
+ const STATIC_SHORT_ALIAS: ReadonlyMap<string, string> = new Map([
97
+ ["sonnet", "Claude Sonnet 4.6 (Thinking)"],
98
+ ["opus", "Claude Opus 4.6 (Thinking)"],
99
+ ["gpt-oss", "GPT-OSS 120B (Medium)"],
100
+ ]);
101
+
102
+ /** Merge the live catalog with the static alias overlay. Live entries win on
103
+ * case-insensitive full-string equality so an updated `agy models` listing
104
+ * always takes precedence over the hardcoded fallback. */
105
+ function mergeCatalog(live: ModelEntry[]): ModelEntry[] {
106
+ const seen = new Set(live.map((e) => e.full.toLowerCase()));
107
+ const merged = [...live];
108
+ for (const entry of STATIC_ALIAS_OVERLAY) {
109
+ if (!seen.has(entry.full.toLowerCase())) merged.push(entry);
110
+ }
111
+ return merged;
112
+ }
113
+
69
114
  // agy conversation ids are UUID DB-stems (e.g. "9e6fdc2f-f9f9-4096-95fc-7852528b50cc").
70
115
  // Reject anything that isn't, so a leading-dash value can't misbind on agy's
71
- // arg parser as the token after --conversation.
72
- const CONV_ID_RE = /^[A-Za-z0-9]{1,128}$/;
116
+ // arg parser as the token after --conversation. First char must be
117
+ // alphanumeric (rejects leading-dash flag injection); hyphens allowed in the
118
+ // body because real UUIDs contain them.
119
+ const CONV_ID_RE = /^[A-Za-z0-9][A-Za-z0-9-]{0,127}$/;
73
120
 
74
121
  const AGY_DESCRIPTION = `Delegate a self-contained sub-task to Google Antigravity. agy is the CLI for Gemini, so this tool is reached under three equivalent names the user may use interchangeably: **gemini**, **antigravity**, and **agy**. When the user says "ask gemini", "ask antigravity", "ask agy", or otherwise refers to any of these, call THIS tool. agy runs its OWN tool loop: it can read, write, edit, and execute inside the workspace, then returns its final answer. Use for a second opinion from a different model family, Gemini-specific reasoning, or isolated sub-tasks you do not need to drive step-by-step. Provide a complete, self-contained task description; agy will not see this conversation.
75
122
 
76
123
  TWO MODES (you choose):
77
124
  - **One-shot (isolated)**: omit conversationId. agy starts fresh with no memory of prior calls. Use for independent questions.
78
- - **Continued conversation**: pass the conversationId returned in the PREVIOUS call's details (details.conversationId). agy resumes that conversation with full context intact — use for follow-ups, multi-turn refinement, or when the user says "ask agy to follow up / continue / now do X based on what you just did". Thread the id from each result into the next call.`;
125
+ - **Continued conversation**: pass the conversationId returned in the PREVIOUS call's details (details.conversationId). agy resumes that conversation with full context intact — use for follow-ups, multi-turn refinement, or when the user says "ask agy to follow up / continue / now do X based on what you just did". Thread the id from each result into the next call.
126
+
127
+ EXECUTION MODES (param: mode):
128
+ - **plan**: agy reviews and plans without writing. Use for cross-review and read-only tasks.
129
+ - **accept-edits** (default): agy applies edits directly inside the workspace.
130
+ - For agy's orthogonal \`--sandbox\` shell-containment flag, set the \`AGY_EXTRA_ARGS=--sandbox\` env var.
131
+
132
+ COMPACT OUTPUT (param: digest): when true, the prompt is prefixed to request compact digests instead of full file contents. Defaults on for plan, off for accept-edits. Use true whenever you do not need full file payloads (review, exploration, planning).`;
79
133
 
80
134
  // --- Types -----------------------------------------------------------------
81
135
 
@@ -225,6 +279,22 @@ function resolveModel(
225
279
  const exact = entries.find((e) => e.full.toLowerCase() === lower);
226
280
  if (exact) return exact.full;
227
281
 
282
+ // 1b. Static short alias ("sonnet" / "opus" / "gpt-oss"). Checked
283
+ // before the family parser because none of these names contain
284
+ // "flash" or "pro" and would otherwise return null below. The
285
+ // resolved full string is then re-validated against the catalog
286
+ // in step 1's second pass on the next call, so renaming the
287
+ // overlay entry in code still wins on exact-string match.
288
+ // The case-insensitive lookup matches mergeCatalog's dedup logic
289
+ // so the "live entries win" guarantee holds even when agy lists
290
+ // the model under different casing than the overlay.
291
+ if (STATIC_SHORT_ALIAS.has(lower)) {
292
+ const target = STATIC_SHORT_ALIAS.get(lower) as string;
293
+ const targetLower = target.toLowerCase();
294
+ const fromCatalog = entries.find((e) => e.full.toLowerCase() === targetLower);
295
+ return fromCatalog ? fromCatalog.full : target;
296
+ }
297
+
228
298
  // 2. Parse the alias.
229
299
  let family: Family | null = lower.includes("flash")
230
300
  ? "flash"
@@ -252,14 +322,27 @@ function resolveModel(
252
322
  const versioned = candidates.filter((e) => e.version === version);
253
323
  if (versioned.length > 0) candidates = versioned;
254
324
  } else {
255
- const versions = candidates
256
- .map((e) => e.version)
257
- .filter((v): v is string => v !== null)
258
- .sort(compareVersionsDesc);
259
- if (versions.length > 0) {
260
- const top = versions[0];
261
- const latest = candidates.filter((e) => e.version === top || e.version === null);
262
- if (latest.length > 0) candidates = latest;
325
+ // Prefer Google's official `gemini-*-latest` aliases (entries with no
326
+ // parseable version in their name, e.g. "Gemini Flash Latest") when
327
+ // the user did NOT pin a specific version. The alias is the
328
+ // versionless pointer Google intends for "the current release" and
329
+ // hot-swaps on every release, while versioned entries like
330
+ // "Gemini 3.6 Flash (Medium)" stay available via explicit pinning
331
+ // (e.g. "3.6 flash medium"). Falls back to the highest versioned
332
+ // entry if no alias is present in the catalog.
333
+ const aliases = candidates.filter((e) => e.version === null);
334
+ if (aliases.length > 0) {
335
+ candidates = aliases;
336
+ } else {
337
+ const versions = candidates
338
+ .map((e) => e.version)
339
+ .filter((v): v is string => v !== null)
340
+ .sort(compareVersionsDesc);
341
+ if (versions.length > 0) {
342
+ const top = versions[0];
343
+ const latest = candidates.filter((e) => e.version === top);
344
+ if (latest.length > 0) candidates = latest;
345
+ }
263
346
  }
264
347
  }
265
348
 
@@ -372,6 +455,8 @@ function newConversationId(dir: string, before: Set<string>): string | null {
372
455
  interface AgyDetails {
373
456
  model: string | null;
374
457
  resolvedModel: string | null;
458
+ mode: Mode;
459
+ digest: boolean;
375
460
  conversationId: string | null;
376
461
  exitCode: number;
377
462
  aborted: boolean;
@@ -382,9 +467,12 @@ interface AgyDetails {
382
467
 
383
468
  export default async function (pi: ExtensionAPI) {
384
469
  const binary = resolveAgy();
385
- // Discovered once at load; frozen for the session (including the
386
- // StringEnum model param). Run /reload after an `agy update` to refresh.
387
- const discovered = await discoverModels(binary).catch(() => []);
470
+ // Discovered once at load; frozen for the session. Run /reload after an
471
+ // `agy update` to refresh. Failure is non-fatal: resolveModel falls back
472
+ // to passthrough so exact slugs typed by the user still work. The static
473
+ // alias overlay (sonnet / opus / gpt-oss) is merged on top so those
474
+ // aliases resolve even when agy doesn't surface them in the live catalog.
475
+ const discovered = mergeCatalog(await discoverModels(binary).catch(() => []));
388
476
 
389
477
  // --- /agy: view / change default model + thinking ---------------------
390
478
 
@@ -498,27 +586,17 @@ export default async function (pi: ExtensionAPI) {
498
586
 
499
587
  // --- Tool registration -------------------------------------------------
500
588
 
501
- // Model param: free string (friendly alias OR exact). We surface the
502
- // discovered models as a StringEnum when available so the model gets a
503
- // bounded list, but also accept arbitrary input that resolves through
504
- // the alias map. Description carries the alias grammar.
505
- const modelParam = discovered.length
506
- ? Type.Optional(
507
- StringEnum(
508
- // Offer friendly aliases plus the full exact strings.
509
- [...new Set([...MODEL_OPTIONS, ...discovered.map((e) => e.full)])],
510
- {
511
- description:
512
- "Model alias or exact id. Friendly: 'flash' (latest Flash), 'pro' (latest Pro), 'gemini' (=flash). Add a tier: 'flash high', 'pro low'. Pin a version: '3.5 flash'. Exact: 'Gemini 3.5 Flash (Medium)'. Omit for the configured default.",
513
- },
514
- ),
515
- )
516
- : Type.Optional(
517
- Type.String({
518
- description:
519
- "Model alias or exact id. Friendly: 'flash', 'pro', 'gemini'. Add tier: 'flash high'. Omit for default.",
520
- }),
521
- );
589
+ // Model param: free string (friendly alias OR exact). Previously this
590
+ // was a StringEnum built from the live catalog; that made tiered/pinned
591
+ // aliases ("flash high", "3.5 flash") fail AJV validation before
592
+ // resolveModel ever saw them. Type.String() lets the resolver handle
593
+ // every documented form and falls back to agy for unknown slugs.
594
+ const modelParam = Type.Optional(
595
+ Type.String({
596
+ description:
597
+ "Model alias or exact id. Friendly: 'flash' (latest Flash, prefers the gemini-flash-latest alias), 'pro' (latest Pro, prefers gemini-pro-latest), 'gemini' (=flash). Add a tier: 'flash high', 'pro low'. Pin a version: '3.5 flash'. Exact: 'Gemini 3.5 Flash (Medium)'. Omit for the configured default.",
598
+ }),
599
+ );
522
600
 
523
601
  pi.registerTool({
524
602
  name: "AskAntigravity",
@@ -535,10 +613,23 @@ export default async function (pi: ExtensionAPI) {
535
613
  }),
536
614
  ),
537
615
  model: modelParam,
538
- skipPermissions: Type.Optional(
616
+ mode: Type.Optional(
617
+ Type.Union(
618
+ [
619
+ Type.Literal("plan"),
620
+ Type.Literal("accept-edits"),
621
+ ],
622
+ {
623
+ description:
624
+ "agy execution mode. 'plan' = review-only, no edits (--mode plan). 'accept-edits' = agy applies edits directly (--mode accept-edits, default). For agy's orthogonal --sandbox shell-containment flag, set the AGY_EXTRA_ARGS env var.",
625
+ default: "accept-edits",
626
+ },
627
+ ),
628
+ ),
629
+ digest: Type.Optional(
539
630
  Type.Boolean({
540
631
  description:
541
- "Pass --dangerously-skip-permissions so agy auto-approves its own write/edit/exec tool calls. Required for tasks that mutate files. Use with care.",
632
+ "Request compact digests instead of full file contents. When true, the prompt is prefixed with '(Use compact digests, not full file contents.)'. Defaults on for plan, off for accept-edits.",
542
633
  }),
543
634
  ),
544
635
  conversationId: Type.Optional(
@@ -569,6 +660,8 @@ export default async function (pi: ExtensionAPI) {
569
660
  details: {
570
661
  model: null,
571
662
  resolvedModel: null,
663
+ mode: "accept-edits",
664
+ digest: false,
572
665
  conversationId: null,
573
666
  exitCode: 0,
574
667
  aborted: false,
@@ -581,6 +674,21 @@ export default async function (pi: ExtensionAPI) {
581
674
 
582
675
  const config = loadConfig();
583
676
  const requestedModel = (params.model as string | undefined) ?? config.defaultModel;
677
+ // Defensive: reject leading-dash model values that could misbind
678
+ // on agy's arg parser when spliced as the `--model` value. Same
679
+ // threat model as CONV_ID_RE — a leading-dash value can't be a
680
+ // model id, so refuse it instead of letting it reach argv.
681
+ if (typeof params.model === "string" && params.model.trim().startsWith("-")) {
682
+ return {
683
+ content: [
684
+ {
685
+ type: "text",
686
+ text: `model value "${params.model}" starts with "-" — not a valid model id. Use a friendly alias (e.g. "flash", "pro", "gemini") or a known exact id (e.g. "Gemini 3.5 Flash (Medium)").`,
687
+ },
688
+ ],
689
+ details: emptyDetails(requestedModel, null),
690
+ };
691
+ }
584
692
  const resolved =
585
693
  resolveModel(requestedModel, discovered, config.defaultThinking) ?? requestedModel;
586
694
 
@@ -617,18 +725,32 @@ export default async function (pi: ExtensionAPI) {
617
725
  typeof rawConvId === "string" && rawConvId.length > 0 && CONV_ID_RE.test(rawConvId);
618
726
  const snapshot = isContinuation ? null : snapshotConversations(CONVERSATIONS_DIR);
619
727
 
728
+ const mode: Mode = (params.mode as Mode | undefined) ?? "accept-edits";
729
+ // digest default: on for plan (review-only contexts where full file
730
+ // contents are noise), off for accept-edits (agy applies edits and
731
+ // may need richer context for diffs).
732
+ const useDigest: boolean =
733
+ typeof params.digest === "boolean"
734
+ ? params.digest
735
+ : mode === "plan";
736
+ const finalPrompt: string = useDigest
737
+ ? `(Use compact digests, not full file contents.)\n${params.prompt}`
738
+ : params.prompt;
739
+
620
740
  const args: string[] = ["--add-dir", cwd];
621
741
  const extra = extraArgs();
622
742
  if (extra.length) args.push(...extra);
623
743
  if (resolved) args.push("--model", resolved);
624
- if (params.skipPermissions) args.push("--dangerously-skip-permissions");
744
+ args.push("--mode", mode);
625
745
  if (isContinuation) args.push("--conversation", rawConvId as string);
626
746
  args.push("--print-timeout", `${timeoutMin}m`);
627
- args.push("-p", params.prompt);
747
+ args.push("-p", finalPrompt);
628
748
 
629
749
  const details: AgyDetails = {
630
750
  model: requestedModel,
631
751
  resolvedModel: resolved,
752
+ mode,
753
+ digest: useDigest,
632
754
  conversationId: isContinuation ? (rawConvId as string) : null,
633
755
  exitCode: 0,
634
756
  aborted: false,
@@ -847,6 +969,8 @@ function emptyDetails(model: string | null, resolvedModel: string | null): AgyDe
847
969
  return {
848
970
  model,
849
971
  resolvedModel,
972
+ mode: "accept-edits",
973
+ digest: false,
850
974
  conversationId: null,
851
975
  exitCode: 0,
852
976
  aborted: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estebanforge/pi-ask-antigravity",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Pi tool that delegates a self-contained sub-task to Google Antigravity's agy CLI (the CLI for Gemini). Friendly model aliases (flash/pro/gemini), configurable default model + thinking, and the AskAntigravity delegation tool.",
5
5
  "keywords": [
6
6
  "pi-package",