@estebanforge/pi-ask-antigravity 1.1.0 → 1.2.1
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 +227 -5
- package/package.json +14 -5
package/extensions/index.ts
CHANGED
|
@@ -54,6 +54,9 @@ const GLOBAL_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "ask-antigrav
|
|
|
54
54
|
|
|
55
55
|
const DEFAULT_MODEL = "flash";
|
|
56
56
|
const DEFAULT_THINKING = "medium";
|
|
57
|
+
// Default on: without --dangerously-skip-permissions, any run_command hangs in
|
|
58
|
+
// non-interactive -p mode (accept-edits auto-approves edits, NOT commands).
|
|
59
|
+
const DEFAULT_SKIP_PERMISSIONS = true;
|
|
57
60
|
|
|
58
61
|
// Per-family fallback tier when none is specified and no config default.
|
|
59
62
|
// Flash defaults to medium (per spec); Pro only ships Low/High, so "latest
|
|
@@ -146,6 +149,9 @@ interface ModelEntry {
|
|
|
146
149
|
interface Config {
|
|
147
150
|
defaultModel: string;
|
|
148
151
|
defaultThinking: ThinkingTier;
|
|
152
|
+
/** Pass --dangerously-skip-permissions so commands don't hang on an
|
|
153
|
+
* unanswerable y/n prompt in non-interactive -p mode. Default true. */
|
|
154
|
+
skipPermissions: boolean;
|
|
149
155
|
}
|
|
150
156
|
|
|
151
157
|
// --- Version helpers -------------------------------------------------------
|
|
@@ -188,9 +194,16 @@ function loadConfig(): Config {
|
|
|
188
194
|
const thinking: ThinkingTier =
|
|
189
195
|
thinkingRaw === "low" || thinkingRaw === "high" ? thinkingRaw : "medium";
|
|
190
196
|
|
|
197
|
+
const envPerm = process.env.AGY_SKIP_PERMISSIONS;
|
|
198
|
+
const skipPermissions =
|
|
199
|
+
envPerm !== undefined
|
|
200
|
+
? envPerm === "1" || envPerm.toLowerCase() === "true"
|
|
201
|
+
: merged.skipPermissions === false ? false : DEFAULT_SKIP_PERMISSIONS;
|
|
202
|
+
|
|
191
203
|
return {
|
|
192
204
|
defaultModel: String(merged.defaultModel ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL,
|
|
193
205
|
defaultThinking: thinking,
|
|
206
|
+
skipPermissions,
|
|
194
207
|
};
|
|
195
208
|
}
|
|
196
209
|
|
|
@@ -367,6 +380,52 @@ function resolveAgy(): string {
|
|
|
367
380
|
return process.env.AGY_BIN || "agy";
|
|
368
381
|
}
|
|
369
382
|
|
|
383
|
+
// Defer to pi-antigravity-bridge when it is installed: the bridge provides BOTH
|
|
384
|
+
// the streaming antigravity provider AND the AskAntigravity tool (same shape as
|
|
385
|
+
// pi-claude-bridge). Registering the tool here too would create a duplicate.
|
|
386
|
+
//
|
|
387
|
+
// Detection does NOT rely on module resolution: pi loads each package with a
|
|
388
|
+
// separate module root (docs/packages.md), so require.resolve from here never
|
|
389
|
+
// reaches a sibling package. Instead we check:
|
|
390
|
+
// 1. An in-process Symbol.for flag the bridge sets at its load (fast path
|
|
391
|
+
// when the bridge loaded earlier this session).
|
|
392
|
+
// 2. The bridge's package.json at pi's documented install locations
|
|
393
|
+
// (~/.pi/agent/npm|git/... and project .pi/npm|git/...).
|
|
394
|
+
//
|
|
395
|
+
// Coverage: (2) is order-independent for npm/git installs - installation is a
|
|
396
|
+
// fact on disk. For LOCAL/source installs (`pi install <path>`) the package
|
|
397
|
+
// lives at its original checkout, not under .pi/npm|git/, so (2) sees nothing;
|
|
398
|
+
// there only (1) helps, and only if the bridge loads first. Recommendation for
|
|
399
|
+
// dev with both repos checked out side-by-side: install the bridge first so it
|
|
400
|
+
// is earlier in settings.json (pi loads extensions in that order). If the
|
|
401
|
+
// clash still occurs, both tools are functionally identical, so the only
|
|
402
|
+
// symptom is a duplicate-name TUI warning - not broken behavior.
|
|
403
|
+
const BRIDGE_FLAG = Symbol.for("pi-antigravity-bridge:active");
|
|
404
|
+
|
|
405
|
+
function bridgeInstallPaths(): string[] {
|
|
406
|
+
const npmPkg = path.join("@estebanforge", "pi-antigravity-bridge", "package.json");
|
|
407
|
+
const gitPkg = path.join("EstebanForge", "pi-antigravity-bridge", "package.json");
|
|
408
|
+
const home = os.homedir();
|
|
409
|
+
const cwd = process.cwd();
|
|
410
|
+
return [
|
|
411
|
+
path.join(home, ".pi", "agent", "npm", "node_modules", npmPkg),
|
|
412
|
+
path.join(cwd, ".pi", "npm", "node_modules", npmPkg),
|
|
413
|
+
path.join(home, ".pi", "agent", "git", "github.com", gitPkg),
|
|
414
|
+
path.join(cwd, ".pi", "git", "github.com", gitPkg),
|
|
415
|
+
];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const isBridgeInstalled = (): boolean => {
|
|
419
|
+
if ((globalThis as Record<symbol, unknown>)[BRIDGE_FLAG]) return true;
|
|
420
|
+
return bridgeInstallPaths().some((p) => {
|
|
421
|
+
try {
|
|
422
|
+
return fs.existsSync(p);
|
|
423
|
+
} catch {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
};
|
|
428
|
+
|
|
370
429
|
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
|
371
430
|
|
|
372
431
|
/** Query `agy models`. Returns [] on any failure (non-fatal). */
|
|
@@ -441,13 +500,124 @@ function snapshotConversations(dir: string): Set<string> {
|
|
|
441
500
|
return out;
|
|
442
501
|
}
|
|
443
502
|
|
|
444
|
-
/**
|
|
445
|
-
*
|
|
446
|
-
|
|
503
|
+
/** Resolve which of the `candidates` DB ids is held open by the process tree
|
|
504
|
+
* rooted at `rootPid`. Used to disambiguate concurrent agy runs. Returns the
|
|
505
|
+
* single matching id, or null when none/several are open or /proc is
|
|
506
|
+
* unavailable. Ported from pi-antigravity-bridge src/discovery.ts; keep in
|
|
507
|
+
* sync. */
|
|
508
|
+
type OpenDbResolver = (
|
|
509
|
+
rootPid: number,
|
|
510
|
+
dir: string,
|
|
511
|
+
candidates: Set<string>,
|
|
512
|
+
) => string | null;
|
|
513
|
+
|
|
514
|
+
function readProcStat(pid: number): { pid: number; ppid: number } | null {
|
|
515
|
+
let raw: string;
|
|
516
|
+
try {
|
|
517
|
+
raw = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
518
|
+
} catch {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
const closeParen = raw.lastIndexOf(")");
|
|
522
|
+
if (closeParen < 0) return null;
|
|
523
|
+
const fields = raw.slice(closeParen + 2).trim().split(/\s+/);
|
|
524
|
+
const ppid = Number(fields[1]);
|
|
525
|
+
if (!Number.isFinite(ppid)) return null;
|
|
526
|
+
return { pid, ppid };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function collectDescendants(rootPid: number): Set<number> {
|
|
530
|
+
const out = new Set<number>([rootPid]);
|
|
531
|
+
let entries: string[];
|
|
532
|
+
try {
|
|
533
|
+
entries = fs.readdirSync("/proc");
|
|
534
|
+
} catch {
|
|
535
|
+
return out;
|
|
536
|
+
}
|
|
537
|
+
const ppidOf = new Map<number, number>();
|
|
538
|
+
for (const e of entries) {
|
|
539
|
+
if (!/^\d+$/.test(e)) continue;
|
|
540
|
+
const s = readProcStat(Number(e));
|
|
541
|
+
if (s) ppidOf.set(s.pid, s.ppid);
|
|
542
|
+
}
|
|
543
|
+
let changed = true;
|
|
544
|
+
while (changed) {
|
|
545
|
+
changed = false;
|
|
546
|
+
for (const [pid, ppid] of ppidOf) {
|
|
547
|
+
if (out.has(pid)) continue;
|
|
548
|
+
if (out.has(ppid)) {
|
|
549
|
+
out.add(pid);
|
|
550
|
+
changed = true;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return out;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function safeRealpath(p: string): string | null {
|
|
558
|
+
try {
|
|
559
|
+
return fs.realpathSync(p);
|
|
560
|
+
} catch {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const procTreeOpenDbResolver: OpenDbResolver = (rootPid, dir, candidates) => {
|
|
566
|
+
if (candidates.size <= 1) return null;
|
|
567
|
+
if (process.platform !== "linux") return null;
|
|
568
|
+
const dirResolved = safeRealpath(dir);
|
|
569
|
+
const tree = collectDescendants(rootPid);
|
|
570
|
+
const found = new Set<string>();
|
|
571
|
+
for (const pid of tree) {
|
|
572
|
+
let fds: string[];
|
|
573
|
+
try {
|
|
574
|
+
fds = fs.readdirSync(`/proc/${pid}/fd`);
|
|
575
|
+
} catch {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
for (const fd of fds) {
|
|
579
|
+
let target: string;
|
|
580
|
+
try {
|
|
581
|
+
target = fs.readlinkSync(`/proc/${pid}/fd/${fd}`);
|
|
582
|
+
} catch {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
const base = path.basename(target);
|
|
586
|
+
if (!base.endsWith(".db")) continue;
|
|
587
|
+
if (dirResolved && safeRealpath(path.dirname(target)) !== dirResolved) continue;
|
|
588
|
+
const id = base.slice(0, -3);
|
|
589
|
+
if (candidates.has(id)) found.add(id);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (found.size === 1) return [...found][0] ?? null;
|
|
593
|
+
return null;
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
interface BindOptions {
|
|
597
|
+
pid?: number;
|
|
598
|
+
resolveOpenDb?: OpenDbResolver;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** Find the conversation id created since `before`. Returns null when none
|
|
602
|
+
* appeared, or when several appeared and we cannot tie one to our process.
|
|
603
|
+
* Pass `opts.pid` (the spawned agy) to enable concurrent-run disambiguation
|
|
604
|
+
* via the process-tree FD scan. */
|
|
605
|
+
function newConversationId(
|
|
606
|
+
dir: string,
|
|
607
|
+
before: Set<string>,
|
|
608
|
+
opts: BindOptions = {},
|
|
609
|
+
): string | null {
|
|
447
610
|
const created = [...snapshotConversations(dir)].filter((id) => !before.has(id));
|
|
448
611
|
if (created.length === 0) return null;
|
|
449
|
-
if (created.length
|
|
450
|
-
|
|
612
|
+
if (created.length === 1) return created[0] ?? null;
|
|
613
|
+
// Ambiguous: try to authoritatively identify ours via the spawned
|
|
614
|
+
// process's open files. Fail safe to null when we can't pick exactly one.
|
|
615
|
+
if (opts.pid !== undefined) {
|
|
616
|
+
const resolve = opts.resolveOpenDb ?? procTreeOpenDbResolver;
|
|
617
|
+
const hit = resolve(opts.pid, dir, new Set(created));
|
|
618
|
+
if (hit && created.includes(hit)) return hit;
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
451
621
|
}
|
|
452
622
|
|
|
453
623
|
// --- Extension -------------------------------------------------------------
|
|
@@ -466,6 +636,12 @@ interface AgyDetails {
|
|
|
466
636
|
}
|
|
467
637
|
|
|
468
638
|
export default async function (pi: ExtensionAPI) {
|
|
639
|
+
// If pi-antigravity-bridge is installed, it owns the AskAntigravity tool
|
|
640
|
+
// (and the provider). Stay silent and register nothing to avoid a
|
|
641
|
+
// duplicate-tool clash. Without the bridge, this extension behaves as
|
|
642
|
+
// before (standalone tool).
|
|
643
|
+
if (isBridgeInstalled()) return;
|
|
644
|
+
|
|
469
645
|
const binary = resolveAgy();
|
|
470
646
|
// Discovered once at load; frozen for the session. Run /reload after an
|
|
471
647
|
// `agy update` to refresh. Failure is non-fatal: resolveModel falls back
|
|
@@ -494,6 +670,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
494
670
|
`AskAntigravity config`,
|
|
495
671
|
` defaultModel: ${config.defaultModel}`,
|
|
496
672
|
` defaultThinking: ${config.defaultThinking}`,
|
|
673
|
+
` permissions: ${config.skipPermissions ? "auto-approved" : "prompt"}`,
|
|
497
674
|
` resolved: ${resolveModel(config.defaultModel, discovered, config.defaultThinking) ?? "(agy default)"}`,
|
|
498
675
|
``,
|
|
499
676
|
`Edit: ~/.pi/agent/ask-antigravity.json`,
|
|
@@ -527,6 +704,14 @@ export default async function (pi: ExtensionAPI) {
|
|
|
527
704
|
currentValue: config.defaultThinking,
|
|
528
705
|
values: THINKING_OPTIONS,
|
|
529
706
|
},
|
|
707
|
+
{
|
|
708
|
+
id: "permissions",
|
|
709
|
+
label: "Permissions",
|
|
710
|
+
description:
|
|
711
|
+
"auto-approved: --dangerously-skip-permissions (required so run_command doesn't hang in -p mode). prompt: agy asks y/n (hangs non-interactively).",
|
|
712
|
+
currentValue: config.skipPermissions ? "auto-approved" : "prompt",
|
|
713
|
+
values: ["auto-approved", "prompt"],
|
|
714
|
+
},
|
|
530
715
|
];
|
|
531
716
|
|
|
532
717
|
const pending: Partial<Config> = {};
|
|
@@ -548,6 +733,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
548
733
|
pending.defaultModel = alias;
|
|
549
734
|
} else if (id === "defaultThinking") {
|
|
550
735
|
pending.defaultThinking = newValue as ThinkingTier;
|
|
736
|
+
} else if (id === "permissions") {
|
|
737
|
+
pending.skipPermissions = newValue === "auto-approved";
|
|
551
738
|
}
|
|
552
739
|
},
|
|
553
740
|
() => done(undefined),
|
|
@@ -742,6 +929,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
742
929
|
if (extra.length) args.push(...extra);
|
|
743
930
|
if (resolved) args.push("--model", resolved);
|
|
744
931
|
args.push("--mode", mode);
|
|
932
|
+
// accept-edits auto-approves file edits but NOT shell commands, so a
|
|
933
|
+
// run_command would hang on an unanswerable y/n prompt in non-interactive
|
|
934
|
+
// -p mode. Honor the shared permissions setting (same knob as the bridge).
|
|
935
|
+
if (config.skipPermissions) args.push("--dangerously-skip-permissions");
|
|
745
936
|
if (isContinuation) args.push("--conversation", rawConvId as string);
|
|
746
937
|
args.push("--print-timeout", `${timeoutMin}m`);
|
|
747
938
|
args.push("-p", finalPrompt);
|
|
@@ -779,6 +970,11 @@ export default async function (pi: ExtensionAPI) {
|
|
|
779
970
|
: null;
|
|
780
971
|
|
|
781
972
|
try {
|
|
973
|
+
// Bind the conversation id DURING the run (agy is alive then) so the
|
|
974
|
+
// pid-based /proc FD resolver can disambiguate when a concurrent agy
|
|
975
|
+
// also drops a new .db. Awaited after the run; the post-exit loop
|
|
976
|
+
// below is the fallback for runs that exit before the poll binds.
|
|
977
|
+
let bindDuringRun: Promise<void> = Promise.resolve();
|
|
782
978
|
const outcome = await new Promise<{
|
|
783
979
|
exitCode: number;
|
|
784
980
|
aborted: boolean;
|
|
@@ -806,6 +1002,28 @@ export default async function (pi: ExtensionAPI) {
|
|
|
806
1002
|
details.stderr += d;
|
|
807
1003
|
});
|
|
808
1004
|
|
|
1005
|
+
// Concurrent bind: poll for the new id while agy is alive. The FD
|
|
1006
|
+
// resolver needs a live process tree, so this stops (and the
|
|
1007
|
+
// post-exit fallback below takes over) once agy has exited.
|
|
1008
|
+
if (!isContinuation && snapshot && proc.pid) {
|
|
1009
|
+
bindDuringRun = (async () => {
|
|
1010
|
+
for (let attempt = 0; attempt < DISCOVERY_POLL_ATTEMPTS; attempt++) {
|
|
1011
|
+
if (details.conversationId) return;
|
|
1012
|
+
if (proc.exitCode !== null) return; // agy gone: scan useless now
|
|
1013
|
+
const found = newConversationId(CONVERSATIONS_DIR, snapshot, {
|
|
1014
|
+
pid: proc.pid,
|
|
1015
|
+
});
|
|
1016
|
+
if (found) {
|
|
1017
|
+
details.conversationId = found;
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
await sleep(DISCOVERY_POLL_MS);
|
|
1021
|
+
}
|
|
1022
|
+
})().catch(() => {
|
|
1023
|
+
/* best-effort: a bind error must never fail an otherwise-OK turn */
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
|
|
809
1027
|
let sigkillTimer: ReturnType<typeof setTimeout> | undefined;
|
|
810
1028
|
let watchdog: ReturnType<typeof setTimeout> | undefined;
|
|
811
1029
|
let settled = false;
|
|
@@ -872,6 +1090,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
872
1090
|
|
|
873
1091
|
if (statusInterval) clearInterval(statusInterval);
|
|
874
1092
|
|
|
1093
|
+
// Let the during-run bind poll finish (it bails immediately once agy
|
|
1094
|
+
// has exited, so this rarely blocks).
|
|
1095
|
+
await bindDuringRun;
|
|
1096
|
+
|
|
875
1097
|
details.exitCode = outcome.exitCode;
|
|
876
1098
|
details.aborted = outcome.aborted;
|
|
877
1099
|
details.timedOut = outcome.timedOut;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-ask-antigravity",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
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",
|
|
@@ -22,7 +22,15 @@
|
|
|
22
22
|
"type": "git",
|
|
23
23
|
"url": "git+https://github.com/EstebanForge/pi-ask-antigravity.git"
|
|
24
24
|
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
25
28
|
"type": "module",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"prepublishOnly": "npm run typecheck && npm test"
|
|
33
|
+
},
|
|
26
34
|
"files": [
|
|
27
35
|
"extensions",
|
|
28
36
|
"README.md"
|
|
@@ -41,11 +49,12 @@
|
|
|
41
49
|
}
|
|
42
50
|
},
|
|
43
51
|
"devDependencies": {
|
|
44
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
45
|
-
"@earendil-works/pi-ai": "^0.
|
|
46
|
-
"@earendil-works/pi-tui": "^0.
|
|
52
|
+
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
53
|
+
"@earendil-works/pi-ai": "^0.84.0",
|
|
54
|
+
"@earendil-works/pi-tui": "^0.84.0",
|
|
47
55
|
"@types/node": "^22.0.0",
|
|
48
56
|
"typebox": "^1.1.38",
|
|
49
|
-
"typescript": "^5.8.0"
|
|
57
|
+
"typescript": "^5.8.0",
|
|
58
|
+
"vitest": "^4.1.7"
|
|
50
59
|
}
|
|
51
60
|
}
|