@estebanforge/pi-ask-antigravity 1.2.1 → 1.2.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.
Files changed (2) hide show
  1. package/extensions/index.ts +254 -37
  2. package/package.json +1 -1
@@ -34,11 +34,15 @@ import * as os from "node:os";
34
34
  import * as path from "node:path";
35
35
  import {
36
36
  type AgentToolResult,
37
+ buildSessionContext,
38
+ getAgentDir,
37
39
  getSettingsListTheme,
40
+ keyHint,
38
41
  type ExtensionAPI,
39
42
  } from "@earendil-works/pi-coding-agent";
40
43
 
41
44
  import { Container, SettingsList, Text, type SettingItem } from "@earendil-works/pi-tui";
45
+ import { contentText } from "@earendil-works/pi-ai";
42
46
  import { Type } from "typebox";
43
47
 
44
48
  // --- Constants -------------------------------------------------------------
@@ -52,6 +56,10 @@ const DISCOVERY_POLL_ATTEMPTS = 5;
52
56
  const DISCOVERY_POLL_MS = 100;
53
57
  const GLOBAL_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "ask-antigravity.json");
54
58
 
59
+ // renderCall / renderResult preview limits (match pi-claude-bridge).
60
+ const PREVIEW_MAX_CHARS = 1000;
61
+ const PREVIEW_MAX_LINES = 6;
62
+
55
63
  const DEFAULT_MODEL = "flash";
56
64
  const DEFAULT_THINKING = "medium";
57
65
  // Default on: without --dangerously-skip-permissions, any run_command hangs in
@@ -83,13 +91,13 @@ type Mode = "plan" | "accept-edits";
83
91
  // whose full string equals the overlay target, the live entry wins. When it
84
92
  // does not (older agy, missing model, plan gate), the overlay entry resolves
85
93
  // the alias so the user can still type "sonnet" and get a working answer.
86
- // "sonnet" -> Claude Sonnet 4.6 (Thinking)
87
- // "opus" -> Claude Opus 4.6 (Thinking)
88
- // "gpt-oss" -> GPT-OSS 120B (Medium)
94
+ // "sonnet" -> claude-sonnet-4-6
95
+ // "opus" -> claude-opus-4-6-thinking
96
+ // "gpt-oss" -> gpt-oss-120b-medium
89
97
  const STATIC_ALIAS_OVERLAY: ReadonlyArray<ModelEntry> = [
90
- { full: "Claude Sonnet 4.6 (Thinking)", family: "other", version: null, tier: null },
91
- { full: "Claude Opus 4.6 (Thinking)", family: "other", version: null, tier: null },
92
- { full: "GPT-OSS 120B (Medium)", family: "other", version: null, tier: null },
98
+ { full: "claude-sonnet-4-6", family: "other", version: null, tier: null },
99
+ { full: "claude-opus-4-6-thinking", family: "other", version: null, tier: null },
100
+ { full: "gpt-oss-120b-medium", family: "other", version: null, tier: null },
93
101
  ];
94
102
 
95
103
  // Short alias → overlay full string. Used by resolveModel to recognize
@@ -97,15 +105,15 @@ const STATIC_ALIAS_OVERLAY: ReadonlyArray<ModelEntry> = [
97
105
  // not match. The overlay entries are also merged into the live catalog for
98
106
  // exact-string passthrough, so this map only needs to cover the short names.
99
107
  const STATIC_SHORT_ALIAS: ReadonlyMap<string, string> = new Map([
100
- ["sonnet", "Claude Sonnet 4.6 (Thinking)"],
101
- ["opus", "Claude Opus 4.6 (Thinking)"],
102
- ["gpt-oss", "GPT-OSS 120B (Medium)"],
108
+ ["sonnet", "claude-sonnet-4-6"],
109
+ ["opus", "claude-opus-4-6-thinking"],
110
+ ["gpt-oss", "gpt-oss-120b-medium"],
103
111
  ]);
104
112
 
105
113
  /** Merge the live catalog with the static alias overlay. Live entries win on
106
114
  * case-insensitive full-string equality so an updated `agy models` listing
107
115
  * always takes precedence over the hardcoded fallback. */
108
- function mergeCatalog(live: ModelEntry[]): ModelEntry[] {
116
+ export function mergeCatalog(live: ModelEntry[]): ModelEntry[] {
109
117
  const seen = new Set(live.map((e) => e.full.toLowerCase()));
110
118
  const merged = [...live];
111
119
  for (const entry of STATIC_ALIAS_OVERLAY) {
@@ -139,13 +147,22 @@ COMPACT OUTPUT (param: digest): when true, the prompt is prefixed to request com
139
147
  type ThinkingTier = "low" | "medium" | "high";
140
148
  type Family = "flash" | "pro" | "other";
141
149
 
142
- interface ModelEntry {
143
- full: string; // exact agy string, e.g. "Gemini 3.5 Flash (Medium)"
150
+ export interface ModelEntry {
151
+ full: string; // exact agy slug, e.g. "gemini-3.6-flash-medium"
144
152
  family: Family;
145
- version: string | null; // "3.5"
153
+ version: string | null; // "3.6"
146
154
  tier: ThinkingTier | null;
147
155
  }
148
156
 
157
+ /** Argv-facing model resolution: the exact --model slug plus an optional
158
+ * --effort tier. Gemini bases split the tier out (the base slug alone is
159
+ * invalid without --effort); fixed-thinking families keep agy's exact slug
160
+ * and carry no effort. */
161
+ export interface ResolvedModel {
162
+ model: string;
163
+ effort?: ThinkingTier;
164
+ }
165
+
149
166
  interface Config {
150
167
  defaultModel: string;
151
168
  defaultThinking: ThinkingTier;
@@ -243,8 +260,11 @@ function saveConfig(patch: Partial<Config>): SaveResult {
243
260
  // --- Model parsing + alias resolution --------------------------------------
244
261
 
245
262
  /** Parse one `agy models` line into a structured entry. */
246
- function parseModelLine(line: string): ModelEntry | null {
247
- const full = line.trim();
263
+ export function parseModelLine(line: string): ModelEntry | null {
264
+ // agy prints TWO columns: "<slug> <display label>". --model takes only the
265
+ // slug (col 1), so split it off; the label is display-only. A bare-slug line
266
+ // (no whitespace) splits to itself.
267
+ const full = line.trim().split(/\s+/)[0] ?? "";
248
268
  if (!full) return null;
249
269
 
250
270
  const lower = full.toLowerCase();
@@ -257,7 +277,7 @@ function parseModelLine(line: string): ModelEntry | null {
257
277
  const versionMatch = lower.match(/(\d+\.\d+)/);
258
278
  const version = versionMatch ? versionMatch[1] : null;
259
279
 
260
- const tierMatch = lower.match(/\((low|medium|high)\)/);
280
+ const tierMatch = lower.match(/-(low|medium|high)$/);
261
281
  const tier = tierMatch ? (tierMatch[1] as ThinkingTier) : null;
262
282
 
263
283
  return { full, family, version, tier };
@@ -276,21 +296,34 @@ function nearestTier(available: ThinkingTier[], preferred: ThinkingTier): Thinki
276
296
  return sorted[0] ?? preferred;
277
297
  }
278
298
 
299
+ /** Build the argv-facing resolution from a picked catalog entry. Gemini bases
300
+ * (slugs starting "gemini-") accept a separate --effort, so split the tier
301
+ * suffix out of the slug: the base alone (gemini-3.6-flash) is what --model
302
+ * wants, and the tier goes to --effort. Fixed-thinking families keep agy's
303
+ * exact slug even when it carries a -medium suffix (gpt-oss-120b-medium):
304
+ * agy rejects --effort for them, so the suffix stays part of the slug. */
305
+ function toResolved(full: string, tier: ThinkingTier | null): ResolvedModel {
306
+ if (tier && full.toLowerCase().startsWith("gemini-")) {
307
+ return { model: full.replace(/-(low|medium|high)$/, ""), effort: tier };
308
+ }
309
+ return { model: full };
310
+ }
311
+
279
312
  /**
280
- * Resolve a friendly alias / partial name to an exact agy model string.
281
- * Returns null if resolution is not possible (caller passes input through
282
- * to agy, which may resolve or fail on its own).
313
+ * Resolve a friendly alias / partial name to an argv-facing {model, effort?}.
314
+ * Returns null only when the family is unrecognized; the caller then passes
315
+ * the raw input straight to agy.
283
316
  */
284
- function resolveModel(
317
+ export function resolveModel(
285
318
  input: string,
286
319
  entries: ModelEntry[],
287
320
  defaultThinking: ThinkingTier,
288
- ): string | null {
321
+ ): ResolvedModel | null {
289
322
  const lower = input.toLowerCase().trim();
290
323
 
291
324
  // 1. Exact full-string match (case-insensitive).
292
325
  const exact = entries.find((e) => e.full.toLowerCase() === lower);
293
- if (exact) return exact.full;
326
+ if (exact) return toResolved(exact.full, exact.tier);
294
327
 
295
328
  // 1b. Static short alias ("sonnet" / "opus" / "gpt-oss"). Checked
296
329
  // before the family parser because none of these names contain
@@ -303,9 +336,8 @@ function resolveModel(
303
336
  // the model under different casing than the overlay.
304
337
  if (STATIC_SHORT_ALIAS.has(lower)) {
305
338
  const target = STATIC_SHORT_ALIAS.get(lower) as string;
306
- const targetLower = target.toLowerCase();
307
- const fromCatalog = entries.find((e) => e.full.toLowerCase() === targetLower);
308
- return fromCatalog ? fromCatalog.full : target;
339
+ const fromCatalog = entries.find((e) => e.full.toLowerCase() === target.toLowerCase());
340
+ return toResolved(fromCatalog?.full ?? target, fromCatalog?.tier ?? null);
309
341
  }
310
342
 
311
343
  // 2. Parse the alias.
@@ -365,13 +397,14 @@ function resolveModel(
365
397
  const familyTiers = new Set(
366
398
  candidates.map((e) => e.tier).filter((t): t is ThinkingTier => t !== null),
367
399
  );
368
- if (familyTiers.size === 0) return candidates[0].full; // no tiers on any entry
400
+ if (familyTiers.size === 0) return toResolved(candidates[0].full, null); // no tiers on any entry
369
401
 
370
402
  const preferred =
371
403
  tier ??
372
404
  (familyTiers.has(defaultThinking) ? defaultThinking : FAMILY_DEFAULT_TIER[family]);
373
405
  const chosenTier = nearestTier([...familyTiers], preferred);
374
- return (candidates.find((e) => e.tier === chosenTier) ?? candidates[0]).full;
406
+ const picked = candidates.find((e) => e.tier === chosenTier) ?? candidates[0];
407
+ return toResolved(picked.full, picked.tier);
375
408
  }
376
409
 
377
410
  // --- agy process helpers ---------------------------------------------------
@@ -620,14 +653,82 @@ function newConversationId(
620
653
  return null;
621
654
  }
622
655
 
656
+ // --- Full-context export (opt-in includeContext) --------------------------
657
+ // NOTE: duplicated per pi-ask-* package (each is self-contained). Duck-typed
658
+ // over role/content to tolerate AgentMessage's union + custom message types.
659
+
660
+ // Tool-call inputs and tool-result bodies are clamped so the exported
661
+ // transcript stays reviewable; the agent can re-read any source file by path.
662
+ // User/assistant prose is kept in full (that IS the conversation).
663
+ const CONTEXT_BLOCK_MAX_CHARS = 2000;
664
+
665
+ function clampBlock(text: unknown, limit = CONTEXT_BLOCK_MAX_CHARS): string {
666
+ const t = String(text ?? "");
667
+ return t.length > limit ? `${t.slice(0, limit)}\n…[truncated, ${t.length - limit} more chars]` : t;
668
+ }
669
+
670
+ /** Render resolved pi AgentMessages to a readable markdown transcript.
671
+ * Pure: no IO. Caller writes the returned string to a temp file. */
672
+ export function renderAgentMessagesMarkdown(messages: readonly unknown[]): string {
673
+ const lines: string[] = [
674
+ "# Pi conversation context",
675
+ "",
676
+ `_Exported for full-context delegation. ${messages.length} message(s)._`,
677
+ "",
678
+ ];
679
+ for (const raw of messages) {
680
+ const m = raw as { role?: string; content?: unknown };
681
+ const role = m.role ?? "message";
682
+ const content = m.content;
683
+ if (role === "assistant") {
684
+ const blocks = (Array.isArray(content) ? content : []) as ReadonlyArray<{
685
+ type: string;
686
+ text?: string;
687
+ name?: string;
688
+ input?: unknown;
689
+ }>;
690
+ const text = blocks
691
+ .filter((b) => b.type === "text")
692
+ .map((b) => b.text ?? "")
693
+ .join("\n");
694
+ if (text.trim()) lines.push("## Assistant", "", text, "");
695
+ for (const b of blocks) {
696
+ if (b.type === "toolCall" || b.type === "tool_use") {
697
+ const input = clampBlock(
698
+ typeof b.input === "string" ? b.input : JSON.stringify(b.input ?? ""),
699
+ 500,
700
+ );
701
+ lines.push(`> tool call: ${b.name ?? "(unknown)"}(${input})`, "");
702
+ }
703
+ }
704
+ } else if (role === "toolResult" || role === "tool_result" || role === "tool") {
705
+ const text = contentText(content as any);
706
+ if (text.trim()) lines.push("## Tool result", "", clampBlock(text), "");
707
+ } else {
708
+ const text = contentText(content as any);
709
+ if (text.trim()) lines.push(`## ${role}`, "", clampBlock(text), "");
710
+ }
711
+ }
712
+ return lines.join("\n");
713
+ }
714
+
715
+ /** Centralized scratch dir for full-context exports, following the
716
+ * ~/.pi/extensions-data/<author>/<extension>/ convention (see pi-token-cost-ledger).
717
+ * Derived from getAgentDir() so rebranded distros resolve correctly. */
718
+ function askContextDir(): string {
719
+ return path.join(path.dirname(getAgentDir()), "extensions-data", "estebanforge", "pi-ask-antigravity");
720
+ }
721
+
623
722
  // --- Extension -------------------------------------------------------------
624
723
 
625
724
  interface AgyDetails {
626
725
  model: string | null;
627
726
  resolvedModel: string | null;
727
+ thinking: ThinkingTier | null;
628
728
  mode: Mode;
629
729
  digest: boolean;
630
730
  conversationId: string | null;
731
+ includeContext: boolean;
631
732
  exitCode: number;
632
733
  aborted: boolean;
633
734
  timedOut: boolean;
@@ -671,7 +772,7 @@ export default async function (pi: ExtensionAPI) {
671
772
  ` defaultModel: ${config.defaultModel}`,
672
773
  ` defaultThinking: ${config.defaultThinking}`,
673
774
  ` permissions: ${config.skipPermissions ? "auto-approved" : "prompt"}`,
674
- ` resolved: ${resolveModel(config.defaultModel, discovered, config.defaultThinking) ?? "(agy default)"}`,
775
+ ` resolved: ${resolveModel(config.defaultModel, discovered, config.defaultThinking)?.model ?? "(agy default)"}`,
675
776
  ``,
676
777
  `Edit: ~/.pi/agent/ask-antigravity.json`,
677
778
  ].join("\n"),
@@ -682,7 +783,7 @@ export default async function (pi: ExtensionAPI) {
682
783
 
683
784
  // Resolve the display string for the current default model.
684
785
  const currentResolved =
685
- resolveModel(config.defaultModel, discovered, config.defaultThinking) ?? config.defaultModel;
786
+ resolveModel(config.defaultModel, discovered, config.defaultThinking)?.model ?? config.defaultModel;
686
787
 
687
788
  const items: SettingItem[] = [
688
789
  {
@@ -692,7 +793,7 @@ export default async function (pi: ExtensionAPI) {
692
793
  "Friendly alias resolved to the latest matching agy model. 'flash' = latest Flash, 'pro' = latest Pro, 'gemini' = latest Flash.",
693
794
  currentValue: `${config.defaultModel} → ${currentResolved}`,
694
795
  values: MODEL_OPTIONS.map((m) => {
695
- const r = resolveModel(m, discovered, config.defaultThinking) ?? m;
796
+ const r = resolveModel(m, discovered, config.defaultThinking)?.model ?? m;
696
797
  return `${m} → ${r}`;
697
798
  }),
698
799
  },
@@ -830,7 +931,80 @@ export default async function (pi: ExtensionAPI) {
830
931
  description: `Hard cap on the agy run in minutes. Default ${DEFAULT_TIMEOUT_MIN}.`,
831
932
  }),
832
933
  ),
934
+ includeContext: Type.Optional(
935
+ Type.Boolean({
936
+ description:
937
+ "When true, export the current pi conversation (resolved, as markdown) to a temp file inside the workspace and tell agy to read it first. Default false (isolated one-shot). Opt in only when the user explicitly wants agy to see the full conversation; it costs agy tokens to read.",
938
+ }),
939
+ ),
833
940
  }),
941
+ renderCall(args, theme, _context) {
942
+ // Show RESOLVED model/thinking/mode (config defaults applied) so the
943
+ // row identifies what will actually run, not just explicit args.
944
+ // agy folds the thinking tier into the model alias or --effort; we
945
+ // surface the resolved tier separately for identification.
946
+ const cfg = loadConfig();
947
+ const requestedModel = (args.model as string | undefined)?.trim() || cfg.defaultModel;
948
+ const resolved =
949
+ resolveModel(requestedModel, discovered, cfg.defaultThinking) ?? { model: requestedModel };
950
+ const thinking: ThinkingTier = resolved.effort ?? cfg.defaultThinking;
951
+ const mode: Mode = (args.mode as Mode | undefined) ?? "accept-edits";
952
+ const useDigest = typeof args.digest === "boolean" ? args.digest : mode === "plan";
953
+ const isContinue =
954
+ typeof args.conversationId === "string" && CONV_ID_RE.test(args.conversationId);
955
+
956
+ const tags: string[] = [`model=${resolved.model}`, `thinking=${thinking}`];
957
+ if (mode !== "accept-edits") tags.push(`mode=${mode}`);
958
+ if (useDigest) tags.push("digest");
959
+ if (isContinue) tags.push("continue");
960
+ if (args.includeContext) tags.push("context=full");
961
+
962
+ let text = theme.fg("mdLink", theme.bold("AskAntigravity "));
963
+ text += `${theme.fg("accent", `[${tags.join(", ")}]`)} `;
964
+
965
+ const prompt = String(args.prompt ?? "");
966
+ const truncated = prompt.length > PREVIEW_MAX_CHARS ? prompt.slice(0, PREVIEW_MAX_CHARS) : prompt;
967
+ const lines = truncated.split("\n").slice(0, PREVIEW_MAX_LINES);
968
+ text += theme.fg("muted", `"${lines.join("\n")}"`);
969
+ if (prompt.length > PREVIEW_MAX_CHARS || prompt.split("\n").length > PREVIEW_MAX_LINES) {
970
+ text += theme.fg("dim", " …");
971
+ }
972
+ return new Text(text, 0, 0);
973
+ },
974
+ renderResult(result, { expanded, isPartial }, theme) {
975
+ const d = result.details as AgyDetails | undefined;
976
+ if (isPartial) {
977
+ const status = result.content[0]?.type === "text" ? result.content[0].text : "working...";
978
+ return new Text(theme.fg("mdLink", "◉ AskAntigravity ") + theme.fg("muted", status), 0, 0);
979
+ }
980
+
981
+ const body = result.content[0]?.type === "text" ? result.content[0].text : "";
982
+ const errored = d?.exitCode !== 0 || !!d?.aborted || !!d?.timedOut;
983
+
984
+ let text = errored
985
+ ? theme.fg("error", "✗ AskAntigravity error")
986
+ : theme.fg("mdLink", "✓ AskAntigravity");
987
+
988
+ const rTags: string[] = [];
989
+ if (d?.resolvedModel || d?.model) rTags.push(`model=${d?.resolvedModel ?? d?.model}`);
990
+ if (d?.thinking) rTags.push(`thinking=${d.thinking}`);
991
+ if (d?.mode && d.mode !== "accept-edits") rTags.push(`mode=${d.mode}`);
992
+ if (d?.includeContext) rTags.push("context=full");
993
+ if (rTags.length) text += ` ${theme.fg("accent", `[${rTags.join(", ")}]`)}`;
994
+ if (d?.durationMs) text += ` ${theme.fg("dim", `${(d.durationMs / 1000).toFixed(1)}s`)}`;
995
+
996
+ if (expanded) {
997
+ if (body) text += `\n${theme.fg("toolOutput", body)}`;
998
+ } else {
999
+ const truncated = body.length > PREVIEW_MAX_CHARS ? body.slice(0, PREVIEW_MAX_CHARS) : body;
1000
+ const lines = truncated.split("\n").slice(0, PREVIEW_MAX_LINES);
1001
+ if (lines.length) text += `\n${theme.fg("toolOutput", lines.join("\n"))}`;
1002
+ if (body.length > PREVIEW_MAX_CHARS || body.split("\n").length > PREVIEW_MAX_LINES) {
1003
+ text += `\n${theme.fg("dim", `… (${keyHint("app.tools.expand", "to expand")})`)}`;
1004
+ }
1005
+ }
1006
+ return new Text(text, 0, 0);
1007
+ },
834
1008
  async execute(toolCallId, params, signal, onUpdate, ctx) {
835
1009
  // Circular-delegation guard (best-effort). This extension registers
836
1010
  // NO provider, so the check only fires if a future agy-as-provider
@@ -877,7 +1051,9 @@ export default async function (pi: ExtensionAPI) {
877
1051
  };
878
1052
  }
879
1053
  const resolved =
880
- resolveModel(requestedModel, discovered, config.defaultThinking) ?? requestedModel;
1054
+ resolveModel(requestedModel, discovered, config.defaultThinking) ?? {
1055
+ model: requestedModel,
1056
+ };
881
1057
 
882
1058
  const start = Date.now();
883
1059
  const cwd = params.cwd || ctx.cwd || process.cwd();
@@ -888,13 +1064,13 @@ export default async function (pi: ExtensionAPI) {
888
1064
  if (!stat.isDirectory()) {
889
1065
  return {
890
1066
  content: [{ type: "text", text: `cwd is not a directory: ${cwd}` }],
891
- details: emptyDetails(requestedModel, resolved),
1067
+ details: emptyDetails(requestedModel, resolved.model),
892
1068
  };
893
1069
  }
894
1070
  } catch {
895
1071
  return {
896
1072
  content: [{ type: "text", text: `cwd does not exist: ${cwd}` }],
897
- details: emptyDetails(requestedModel, resolved),
1073
+ details: emptyDetails(requestedModel, resolved.model),
898
1074
  };
899
1075
  }
900
1076
 
@@ -924,10 +1100,34 @@ export default async function (pi: ExtensionAPI) {
924
1100
  ? `(Use compact digests, not full file contents.)\n${params.prompt}`
925
1101
  : params.prompt;
926
1102
 
1103
+ // Opt-in full-context export (isolated stays the default).
1104
+ let contextFile: string | null = null;
1105
+ if (params.includeContext) {
1106
+ try {
1107
+ const { messages } = buildSessionContext(ctx.sessionManager.getBranch());
1108
+ if (messages.length) {
1109
+ const md = renderAgentMessagesMarkdown(messages);
1110
+ const ctxDir = askContextDir();
1111
+ fs.mkdirSync(ctxDir, { recursive: true });
1112
+ contextFile = path.join(
1113
+ ctxDir,
1114
+ `.ask-context-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.md`,
1115
+ );
1116
+ fs.writeFileSync(contextFile, md, { mode: 0o600 });
1117
+ }
1118
+ } catch {
1119
+ contextFile = null;
1120
+ }
1121
+ }
1122
+ const effectivePrompt = contextFile
1123
+ ? `The full pi conversation context (as markdown) is at: ${contextFile}\nRead that file first for context, then do the task below.\n\n---\n\n${finalPrompt}`
1124
+ : finalPrompt;
1125
+
927
1126
  const args: string[] = ["--add-dir", cwd];
928
1127
  const extra = extraArgs();
929
1128
  if (extra.length) args.push(...extra);
930
- if (resolved) args.push("--model", resolved);
1129
+ if (resolved.model) args.push("--model", resolved.model);
1130
+ if (resolved.effort) args.push("--effort", resolved.effort);
931
1131
  args.push("--mode", mode);
932
1132
  // accept-edits auto-approves file edits but NOT shell commands, so a
933
1133
  // run_command would hang on an unanswerable y/n prompt in non-interactive
@@ -935,14 +1135,17 @@ export default async function (pi: ExtensionAPI) {
935
1135
  if (config.skipPermissions) args.push("--dangerously-skip-permissions");
936
1136
  if (isContinuation) args.push("--conversation", rawConvId as string);
937
1137
  args.push("--print-timeout", `${timeoutMin}m`);
938
- args.push("-p", finalPrompt);
1138
+ if (contextFile) args.push("--add-dir", askContextDir());
1139
+ args.push("-p", effectivePrompt);
939
1140
 
940
1141
  const details: AgyDetails = {
941
1142
  model: requestedModel,
942
- resolvedModel: resolved,
1143
+ resolvedModel: resolved.model,
1144
+ thinking: resolved.effort ?? config.defaultThinking,
943
1145
  mode,
944
1146
  digest: useDigest,
945
1147
  conversationId: isContinuation ? (rawConvId as string) : null,
1148
+ includeContext: contextFile !== null,
946
1149
  exitCode: 0,
947
1150
  aborted: false,
948
1151
  timedOut: false,
@@ -1183,16 +1386,30 @@ export default async function (pi: ExtensionAPI) {
1183
1386
  details,
1184
1387
  };
1185
1388
  }
1389
+ finally {
1390
+ if (contextFile) {
1391
+ try {
1392
+ fs.unlinkSync(contextFile);
1393
+ } catch {}
1394
+ }
1395
+ }
1186
1396
  },
1187
1397
  });
1188
1398
  }
1189
1399
 
1190
- function emptyDetails(model: string | null, resolvedModel: string | null): AgyDetails {
1400
+ function emptyDetails(
1401
+ model: string | null,
1402
+ resolvedModel: string | null,
1403
+ thinking: ThinkingTier | null = null,
1404
+ includeContext: boolean = false,
1405
+ ): AgyDetails {
1191
1406
  return {
1192
1407
  model,
1193
1408
  resolvedModel,
1409
+ thinking,
1194
1410
  mode: "accept-edits",
1195
1411
  digest: false,
1412
+ includeContext,
1196
1413
  conversationId: null,
1197
1414
  exitCode: 0,
1198
1415
  aborted: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estebanforge/pi-ask-antigravity",
3
- "version": "1.2.1",
3
+ "version": "1.2.4",
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",