@estebanforge/pi-ask-antigravity 1.2.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 +147 -5
- package/package.json +14 -5
package/extensions/index.ts
CHANGED
|
@@ -500,13 +500,124 @@ function snapshotConversations(dir: string): Set<string> {
|
|
|
500
500
|
return out;
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
-
/**
|
|
504
|
-
*
|
|
505
|
-
|
|
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 {
|
|
506
610
|
const created = [...snapshotConversations(dir)].filter((id) => !before.has(id));
|
|
507
611
|
if (created.length === 0) return null;
|
|
508
|
-
if (created.length
|
|
509
|
-
|
|
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;
|
|
510
621
|
}
|
|
511
622
|
|
|
512
623
|
// --- Extension -------------------------------------------------------------
|
|
@@ -859,6 +970,11 @@ export default async function (pi: ExtensionAPI) {
|
|
|
859
970
|
: null;
|
|
860
971
|
|
|
861
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();
|
|
862
978
|
const outcome = await new Promise<{
|
|
863
979
|
exitCode: number;
|
|
864
980
|
aborted: boolean;
|
|
@@ -886,6 +1002,28 @@ export default async function (pi: ExtensionAPI) {
|
|
|
886
1002
|
details.stderr += d;
|
|
887
1003
|
});
|
|
888
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
|
+
|
|
889
1027
|
let sigkillTimer: ReturnType<typeof setTimeout> | undefined;
|
|
890
1028
|
let watchdog: ReturnType<typeof setTimeout> | undefined;
|
|
891
1029
|
let settled = false;
|
|
@@ -952,6 +1090,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
952
1090
|
|
|
953
1091
|
if (statusInterval) clearInterval(statusInterval);
|
|
954
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
|
+
|
|
955
1097
|
details.exitCode = outcome.exitCode;
|
|
956
1098
|
details.aborted = outcome.aborted;
|
|
957
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.2.
|
|
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
|
}
|