@estebanforge/pi-ask-antigravity 1.2.3 → 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.
- package/extensions/index.ts +191 -2
- package/package.json +1 -1
package/extensions/index.ts
CHANGED
|
@@ -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
|
|
@@ -645,14 +653,82 @@ function newConversationId(
|
|
|
645
653
|
return null;
|
|
646
654
|
}
|
|
647
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
|
+
|
|
648
722
|
// --- Extension -------------------------------------------------------------
|
|
649
723
|
|
|
650
724
|
interface AgyDetails {
|
|
651
725
|
model: string | null;
|
|
652
726
|
resolvedModel: string | null;
|
|
727
|
+
thinking: ThinkingTier | null;
|
|
653
728
|
mode: Mode;
|
|
654
729
|
digest: boolean;
|
|
655
730
|
conversationId: string | null;
|
|
731
|
+
includeContext: boolean;
|
|
656
732
|
exitCode: number;
|
|
657
733
|
aborted: boolean;
|
|
658
734
|
timedOut: boolean;
|
|
@@ -855,7 +931,80 @@ export default async function (pi: ExtensionAPI) {
|
|
|
855
931
|
description: `Hard cap on the agy run in minutes. Default ${DEFAULT_TIMEOUT_MIN}.`,
|
|
856
932
|
}),
|
|
857
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
|
+
),
|
|
858
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
|
+
},
|
|
859
1008
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
860
1009
|
// Circular-delegation guard (best-effort). This extension registers
|
|
861
1010
|
// NO provider, so the check only fires if a future agy-as-provider
|
|
@@ -951,6 +1100,29 @@ export default async function (pi: ExtensionAPI) {
|
|
|
951
1100
|
? `(Use compact digests, not full file contents.)\n${params.prompt}`
|
|
952
1101
|
: params.prompt;
|
|
953
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
|
+
|
|
954
1126
|
const args: string[] = ["--add-dir", cwd];
|
|
955
1127
|
const extra = extraArgs();
|
|
956
1128
|
if (extra.length) args.push(...extra);
|
|
@@ -963,14 +1135,17 @@ export default async function (pi: ExtensionAPI) {
|
|
|
963
1135
|
if (config.skipPermissions) args.push("--dangerously-skip-permissions");
|
|
964
1136
|
if (isContinuation) args.push("--conversation", rawConvId as string);
|
|
965
1137
|
args.push("--print-timeout", `${timeoutMin}m`);
|
|
966
|
-
args.push("-
|
|
1138
|
+
if (contextFile) args.push("--add-dir", askContextDir());
|
|
1139
|
+
args.push("-p", effectivePrompt);
|
|
967
1140
|
|
|
968
1141
|
const details: AgyDetails = {
|
|
969
1142
|
model: requestedModel,
|
|
970
1143
|
resolvedModel: resolved.model,
|
|
1144
|
+
thinking: resolved.effort ?? config.defaultThinking,
|
|
971
1145
|
mode,
|
|
972
1146
|
digest: useDigest,
|
|
973
1147
|
conversationId: isContinuation ? (rawConvId as string) : null,
|
|
1148
|
+
includeContext: contextFile !== null,
|
|
974
1149
|
exitCode: 0,
|
|
975
1150
|
aborted: false,
|
|
976
1151
|
timedOut: false,
|
|
@@ -1211,16 +1386,30 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1211
1386
|
details,
|
|
1212
1387
|
};
|
|
1213
1388
|
}
|
|
1389
|
+
finally {
|
|
1390
|
+
if (contextFile) {
|
|
1391
|
+
try {
|
|
1392
|
+
fs.unlinkSync(contextFile);
|
|
1393
|
+
} catch {}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1214
1396
|
},
|
|
1215
1397
|
});
|
|
1216
1398
|
}
|
|
1217
1399
|
|
|
1218
|
-
function emptyDetails(
|
|
1400
|
+
function emptyDetails(
|
|
1401
|
+
model: string | null,
|
|
1402
|
+
resolvedModel: string | null,
|
|
1403
|
+
thinking: ThinkingTier | null = null,
|
|
1404
|
+
includeContext: boolean = false,
|
|
1405
|
+
): AgyDetails {
|
|
1219
1406
|
return {
|
|
1220
1407
|
model,
|
|
1221
1408
|
resolvedModel,
|
|
1409
|
+
thinking,
|
|
1222
1410
|
mode: "accept-edits",
|
|
1223
1411
|
digest: false,
|
|
1412
|
+
includeContext,
|
|
1224
1413
|
conversationId: null,
|
|
1225
1414
|
exitCode: 0,
|
|
1226
1415
|
aborted: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-ask-antigravity",
|
|
3
|
-
"version": "1.2.
|
|
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",
|