pitroom 0.24.0 → 0.25.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +11 -0
- package/dist/lib.mjs +123 -16
- package/dist/pitroom.mjs +141 -25
- package/dist/ui/app.css +1 -1
- package/dist/ui/app.js +31 -31
- package/docs/backends.md +1 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pitroom",
|
|
3
3
|
"description": "A free pit crew for your expensive coding agent: the superpowers development workflow, run by cheap workers. Delegate research, fixes, tests, reviews and whole plans from Claude Code, Codex or any agent to OpenCode, Codex or Claude Code workers, with receipts that prove the savings.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.25.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TahaAtas"
|
|
7
7
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pitroom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "A free pit crew for your expensive coding agent: delegate research, fixes, tests, reviews and parallel investigations to cheap worker agents running your own models, with verified answers, exact diffs and cost receipts.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TahaAtas"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.25.0
|
|
4
|
+
|
|
5
|
+
### Dashboard
|
|
6
|
+
- **"Finished" no longer goes down as you start runs.** It counted the finished runs among the ones on the page (the newest 40), so each new run pushed a finished one off and the number dropped. It now counts every finished run of the period, from the ledger the savings come from (a picked group's from its own runs); audits are left out, as in the savings.
|
|
7
|
+
- **A period on the Finished and Est. saved cards**: 24 hours, 7 days (the default), 30 days, 6 months or all time, picked on each card and remembered in the browser.
|
|
8
|
+
|
|
9
|
+
## 0.24.1
|
|
10
|
+
|
|
11
|
+
### Workers
|
|
12
|
+
- **Antigravity: a task that names files outside the project no longer stops halfway.** agy's file tools stay in the workspace, and a headless agy cannot ask for more, so a read outside it ("copy the components from /tmp/ui-source") was refused and ended the turn without an answer. The folders and files a task names by absolute path (or `~/…`) that exist outside the workspace are now granted read-only (the sandbox mounts them read-only too), with every secret-looking file in them denied (found at any depth and in every folder; a folder too big to check, with a link leading out of it or nowhere, or with a folder that cannot be read, is not granted, and a named path counts where its links lead). Never the root or a top-level folder such as `/tmp`, your home itself or a folder above it, a folder holding the workspace or the project, a dot-folder in your home (`~/.config` and the like) or `~/Library`, Pitroom's own state or a secret-looking file, and nothing on Windows (agy compares paths there without their drive letter); the secrets in your home stay denied. In a snapshot or an isolated copy the project's own paths in the task are turned into the copy's (links resolved, so `/tmp/…` and `/private/tmp/…` alike): the worker reads and edits its copy instead of being refused at the project.
|
|
13
|
+
|
|
3
14
|
## 0.24.0
|
|
4
15
|
|
|
5
16
|
### Workers
|
package/dist/lib.mjs
CHANGED
|
@@ -542,10 +542,10 @@ function denyCommandRules() {
|
|
|
542
542
|
"command(eval)"
|
|
543
543
|
];
|
|
544
544
|
}
|
|
545
|
-
function workerSettings(workspace, workerHome3) {
|
|
545
|
+
function workerSettings(workspace, workerHome3, reads = []) {
|
|
546
546
|
const real = (p) => {
|
|
547
547
|
try {
|
|
548
|
-
return fs6.realpathSync(p);
|
|
548
|
+
return fs6.realpathSync.native(p);
|
|
549
549
|
} catch {
|
|
550
550
|
return p;
|
|
551
551
|
}
|
|
@@ -553,6 +553,7 @@ function workerSettings(workspace, workerHome3) {
|
|
|
553
553
|
const both = (p) => [.../* @__PURE__ */ new Set([p, real(p)])];
|
|
554
554
|
const secrets = [
|
|
555
555
|
...findSecretFiles(workspace).map((f) => path5.join(workspace, f)),
|
|
556
|
+
...reads.flatMap((r) => r.secrets),
|
|
556
557
|
...HOME_SECRETS.map((f) => path5.join(os3.homedir(), f)).filter((p) => fs6.existsSync(p)),
|
|
557
558
|
// the keychain link of the worker's own home
|
|
558
559
|
path5.join(workerHome3, "Library")
|
|
@@ -565,7 +566,9 @@ function workerSettings(workspace, workerHome3) {
|
|
|
565
566
|
allow: [
|
|
566
567
|
"command(*)",
|
|
567
568
|
...both(path5.join(dataDir(workerHome3), "brain")).map((p) => `write_file(${rulePath(p)})`),
|
|
568
|
-
...toolchains().flatMap(both).map((p) => `read_file(${rulePath(p)})`)
|
|
569
|
+
...toolchains().flatMap(both).map((p) => `read_file(${rulePath(p)})`),
|
|
570
|
+
// what the task names outside the workspace: a headless agy cannot ask, and would end the turn unanswered
|
|
571
|
+
...reads.map((r) => r.path).flatMap(both).map((p) => `read_file(${rulePath(p)})`)
|
|
569
572
|
],
|
|
570
573
|
deny: [
|
|
571
574
|
...secrets.flatMap(both).map((p) => `read_file(${rulePath(p)})`),
|
|
@@ -588,6 +591,96 @@ function toolchains() {
|
|
|
588
591
|
const install = (d) => path5.basename(d) === "bin" && /[\\/](versions|toolchains)[\\/]/.test(d) ? path5.dirname(d) : d;
|
|
589
592
|
return [...new Set(dirs.map(install))].filter((d) => !inHome(home()) || path5.relative(home(), d).startsWith(".."));
|
|
590
593
|
}
|
|
594
|
+
var isDir = (p) => {
|
|
595
|
+
try {
|
|
596
|
+
return fs6.statSync(p).isDirectory();
|
|
597
|
+
} catch {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
var MAX_READS = 20;
|
|
602
|
+
function pathsIn(prompt) {
|
|
603
|
+
const out = [];
|
|
604
|
+
for (const m of prompt.matchAll(/(^|[\s"'`(\[<{=])((?:~|[A-Za-z]:)?[\\/][^\s"'`)\]>}]*)/g)) {
|
|
605
|
+
const raw = m[2].replace(/[.,;:!?]+$/, "");
|
|
606
|
+
const abs = raw.startsWith("~") ? path5.join(os3.homedir(), raw.slice(1)) : raw;
|
|
607
|
+
if (raw && path5.isAbsolute(abs)) out.push({ raw, at: m.index + m[1].length, abs });
|
|
608
|
+
}
|
|
609
|
+
return out;
|
|
610
|
+
}
|
|
611
|
+
var realOf = (p) => {
|
|
612
|
+
try {
|
|
613
|
+
return fs6.realpathSync.native(p);
|
|
614
|
+
} catch {
|
|
615
|
+
return void 0;
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
function resolved(p) {
|
|
619
|
+
let dir = path5.resolve(p);
|
|
620
|
+
const rest = [];
|
|
621
|
+
for (; ; ) {
|
|
622
|
+
const r = realOf(dir);
|
|
623
|
+
if (r) return path5.join(r, ...rest);
|
|
624
|
+
const up = path5.dirname(dir);
|
|
625
|
+
if (up === dir) return path5.resolve(p);
|
|
626
|
+
rest.unshift(path5.basename(dir));
|
|
627
|
+
dir = up;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
var inside = (p, dir) => {
|
|
631
|
+
const rel = path5.relative(dir, p);
|
|
632
|
+
return !rel || !rel.startsWith("..") && !path5.isAbsolute(rel);
|
|
633
|
+
};
|
|
634
|
+
var MAX_SCANNED = 2e4;
|
|
635
|
+
function allSecrets(dir) {
|
|
636
|
+
const root = realOf(dir) ?? dir;
|
|
637
|
+
const found = [];
|
|
638
|
+
let seen = 0;
|
|
639
|
+
const stack = [dir];
|
|
640
|
+
while (stack.length) {
|
|
641
|
+
const d = stack.pop();
|
|
642
|
+
let entries;
|
|
643
|
+
try {
|
|
644
|
+
entries = fs6.readdirSync(d, { withFileTypes: true });
|
|
645
|
+
} catch {
|
|
646
|
+
return void 0;
|
|
647
|
+
}
|
|
648
|
+
for (const e of entries) {
|
|
649
|
+
if (++seen > MAX_SCANNED) return void 0;
|
|
650
|
+
const p = path5.join(d, e.name);
|
|
651
|
+
if (e.isSymbolicLink()) {
|
|
652
|
+
const target = realOf(p);
|
|
653
|
+
if (!target || !inside(target, root)) return void 0;
|
|
654
|
+
if (looksSecret(e.name)) found.push(p);
|
|
655
|
+
} else if (e.isDirectory()) stack.push(p);
|
|
656
|
+
else if (looksSecret(e.name)) found.push(p);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return found;
|
|
660
|
+
}
|
|
661
|
+
function outsideReads(prompt, workspace, project) {
|
|
662
|
+
if (process.platform === "win32") return [];
|
|
663
|
+
const homeDir = realOf(os3.homedir()) ?? os3.homedir();
|
|
664
|
+
const ws = realOf(workspace) ?? path5.resolve(workspace);
|
|
665
|
+
const proj = project ? resolved(project) : void 0;
|
|
666
|
+
const own = realOf(home()) ?? home();
|
|
667
|
+
const found = /* @__PURE__ */ new Map();
|
|
668
|
+
for (const { abs } of pathsIn(prompt)) {
|
|
669
|
+
const p = realOf(abs);
|
|
670
|
+
if (!p || found.has(p) || inside(p, ws) || inside(p, own)) continue;
|
|
671
|
+
if (inside(homeDir, p) || inside(ws, p) || proj && inside(proj, p)) continue;
|
|
672
|
+
if (p.split(/[\\/]/).filter(Boolean).length < 2) continue;
|
|
673
|
+
if (inside(p, homeDir) && /^(\.|Library([\\/]|$))/.test(path5.relative(homeDir, p))) continue;
|
|
674
|
+
if (!isDir(p)) {
|
|
675
|
+
if (!looksSecret(path5.basename(p))) found.set(p, { path: p, secrets: [] });
|
|
676
|
+
} else {
|
|
677
|
+
const secrets = allSecrets(p);
|
|
678
|
+
if (secrets) found.set(p, { path: p, secrets });
|
|
679
|
+
}
|
|
680
|
+
if (found.size >= MAX_READS) break;
|
|
681
|
+
}
|
|
682
|
+
return [...found.values()];
|
|
683
|
+
}
|
|
591
684
|
function baseSettings() {
|
|
592
685
|
return {
|
|
593
686
|
enableTerminalSandbox: true,
|
|
@@ -598,12 +691,12 @@ function baseSettings() {
|
|
|
598
691
|
}
|
|
599
692
|
var rulePath = (p) => p.replace(/^[A-Za-z]:(?=[\\/])/, "").replace(/\\/g, "/");
|
|
600
693
|
var HOMES_KEPT_DAYS = 14;
|
|
601
|
-
function workerHome(workspace) {
|
|
694
|
+
function workerHome(workspace, reads = []) {
|
|
602
695
|
const root = path5.join(home(), "antigravity-home");
|
|
603
696
|
const id2 = crypto3.createHash("sha256").update(path5.resolve(workspace)).digest("hex").slice(0, 16);
|
|
604
697
|
const dir = path5.join(root, id2);
|
|
605
698
|
fs6.mkdirSync(dataDir(dir), { recursive: true });
|
|
606
|
-
writeIfChanged(path5.join(dataDir(dir), "settings.json"), `${JSON.stringify(workerSettings(workspace, dir), null, 2)}
|
|
699
|
+
writeIfChanged(path5.join(dataDir(dir), "settings.json"), `${JSON.stringify(workerSettings(workspace, dir, reads), null, 2)}
|
|
607
700
|
`);
|
|
608
701
|
if (process.platform === "darwin") {
|
|
609
702
|
fs6.mkdirSync(path5.join(dir, "Library"), { recursive: true });
|
|
@@ -627,11 +720,25 @@ function pruneHomes(root, keep) {
|
|
|
627
720
|
} catch {
|
|
628
721
|
}
|
|
629
722
|
}
|
|
723
|
+
function inCopy(prompt, project) {
|
|
724
|
+
if (!project || project.root === project.copy) return prompt;
|
|
725
|
+
const root = resolved(project.root);
|
|
726
|
+
let out = "";
|
|
727
|
+
let from = 0;
|
|
728
|
+
for (const { raw, at, abs } of pathsIn(prompt)) {
|
|
729
|
+
const p = resolved(abs);
|
|
730
|
+
if (!inside(p, root)) continue;
|
|
731
|
+
out += prompt.slice(from, at) + path5.join(project.copy, path5.relative(root, p));
|
|
732
|
+
from = at + raw.length;
|
|
733
|
+
}
|
|
734
|
+
return out + prompt.slice(from);
|
|
735
|
+
}
|
|
630
736
|
function invocation(req) {
|
|
737
|
+
const prompt = inCopy(req.prompt, req.project);
|
|
631
738
|
const args = [
|
|
632
739
|
// -p takes the prompt as its value; one that starts with "-" would be read as an option: a leading space keeps it a value.
|
|
633
740
|
"-p",
|
|
634
|
-
|
|
741
|
+
prompt.startsWith("-") ? ` ${prompt}` : prompt,
|
|
635
742
|
"--output-format",
|
|
636
743
|
"stream-json",
|
|
637
744
|
"--mode",
|
|
@@ -646,7 +753,7 @@ function invocation(req) {
|
|
|
646
753
|
if (model) args.push("--model", model);
|
|
647
754
|
if (effort) args.push("--effort", effort);
|
|
648
755
|
const { command, prefix } = resolveCommand(binary());
|
|
649
|
-
const workerDir = workerHome(req.cwd);
|
|
756
|
+
const workerDir = workerHome(req.cwd, outsideReads(prompt, req.cwd, req.project?.root));
|
|
650
757
|
const env = { HOME: workerDir };
|
|
651
758
|
if (process.platform === "win32") env.USERPROFILE = workerDir;
|
|
652
759
|
return { command, args: [...prefix, ...args], env };
|
|
@@ -2287,7 +2394,7 @@ function verifyRefs(refs, dirs) {
|
|
|
2287
2394
|
if (!files.length) {
|
|
2288
2395
|
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
2289
2396
|
total++;
|
|
2290
|
-
invalid.push({ ref: ref.text, reason: path12.isAbsolute(ref.file) && !
|
|
2397
|
+
invalid.push({ ref: ref.text, reason: path12.isAbsolute(ref.file) && !inside2(ref.file, roots) ? "outside the project" : "file not found" });
|
|
2291
2398
|
continue;
|
|
2292
2399
|
}
|
|
2293
2400
|
total++;
|
|
@@ -2337,16 +2444,16 @@ function indexFiles(roots) {
|
|
|
2337
2444
|
return index;
|
|
2338
2445
|
}
|
|
2339
2446
|
function resolve(ref, roots) {
|
|
2340
|
-
const candidates = path12.isAbsolute(ref) ?
|
|
2447
|
+
const candidates = path12.isAbsolute(ref) ? inside2(ref, roots) ? [ref] : [] : roots.map((r) => path12.join(r, ref));
|
|
2341
2448
|
return candidates.find((c) => {
|
|
2342
2449
|
try {
|
|
2343
|
-
return fs12.statSync(c).isFile() &&
|
|
2450
|
+
return fs12.statSync(c).isFile() && inside2(c, roots);
|
|
2344
2451
|
} catch {
|
|
2345
2452
|
return false;
|
|
2346
2453
|
}
|
|
2347
2454
|
});
|
|
2348
2455
|
}
|
|
2349
|
-
function
|
|
2456
|
+
function inside2(file, roots) {
|
|
2350
2457
|
return roots.some((r) => {
|
|
2351
2458
|
const rel = path12.relative(r, path12.resolve(file));
|
|
2352
2459
|
return rel !== "" && !rel.startsWith("..") && !path12.isAbsolute(rel);
|
|
@@ -2963,7 +3070,7 @@ function answerParts(answer) {
|
|
|
2963
3070
|
const summary = s ? s[1] : text.split(/^\s*(?:DETAILS|FILES CHANGED|VERIFICATION|OPEN ISSUES):/m)[0];
|
|
2964
3071
|
return { summary: summary.trim(), details: (d?.[1] ?? "").trim() };
|
|
2965
3072
|
}
|
|
2966
|
-
function
|
|
3073
|
+
function pathsIn2(text) {
|
|
2967
3074
|
const found = text.replace(/\\/g, "/").match(/[A-Za-z0-9_.@\-/]+\.[A-Za-z0-9]{1,8}(?::\d+)?/g) ?? [];
|
|
2968
3075
|
return [...new Set(found.map((p) => clean(p.replace(/:\d+$/, ""))))];
|
|
2969
3076
|
}
|
|
@@ -2975,7 +3082,7 @@ function scoreAnswer(q, truth, answer, root) {
|
|
|
2975
3082
|
if ("line" in truth) {
|
|
2976
3083
|
const flat = summary.replace(/\s+/g, " ");
|
|
2977
3084
|
if (new RegExp(`(^|[^\\w./-])\\.?/?${escape2(truth.path)}:${truth.line}(?!\\d)`).test(flat)) return { score: 1, got: `${truth.path}:${truth.line}` };
|
|
2978
|
-
const named =
|
|
3085
|
+
const named = pathsIn2(flat).includes(truth.path);
|
|
2979
3086
|
return { score: named ? 0.5 : 0, got: flat.slice(0, 120) };
|
|
2980
3087
|
}
|
|
2981
3088
|
if ("count" in truth) {
|
|
@@ -2983,8 +3090,8 @@ function scoreAnswer(q, truth, answer, root) {
|
|
|
2983
3090
|
const got2 = n ? Number(n[0].replace(/,/g, "")) : void 0;
|
|
2984
3091
|
return { score: got2 === truth.count ? 1 : 0, got: got2 === void 0 ? "(no number)" : String(got2) };
|
|
2985
3092
|
}
|
|
2986
|
-
let got =
|
|
2987
|
-
if (!got.length) got =
|
|
3093
|
+
let got = pathsIn2(summary);
|
|
3094
|
+
if (!got.length) got = pathsIn2(details);
|
|
2988
3095
|
const hit = got.filter((p) => truth.files.includes(p)).length;
|
|
2989
3096
|
const precision = got.length ? hit / got.length : 0;
|
|
2990
3097
|
const recall = truth.files.length ? hit / truth.files.length : got.length ? 0 : 1;
|
|
@@ -3355,7 +3462,7 @@ export {
|
|
|
3355
3462
|
parseStatus,
|
|
3356
3463
|
parseTarget,
|
|
3357
3464
|
parseVerdict,
|
|
3358
|
-
pathsIn,
|
|
3465
|
+
pathsIn2 as pathsIn,
|
|
3359
3466
|
planName,
|
|
3360
3467
|
planTask,
|
|
3361
3468
|
primaryPrice,
|
package/dist/pitroom.mjs
CHANGED
|
@@ -558,10 +558,10 @@ function denyCommandRules() {
|
|
|
558
558
|
"command(eval)"
|
|
559
559
|
];
|
|
560
560
|
}
|
|
561
|
-
function workerSettings(workspace, workerHome3) {
|
|
561
|
+
function workerSettings(workspace, workerHome3, reads = []) {
|
|
562
562
|
const real = (p) => {
|
|
563
563
|
try {
|
|
564
|
-
return fs6.realpathSync(p);
|
|
564
|
+
return fs6.realpathSync.native(p);
|
|
565
565
|
} catch {
|
|
566
566
|
return p;
|
|
567
567
|
}
|
|
@@ -569,6 +569,7 @@ function workerSettings(workspace, workerHome3) {
|
|
|
569
569
|
const both = (p) => [.../* @__PURE__ */ new Set([p, real(p)])];
|
|
570
570
|
const secrets = [
|
|
571
571
|
...findSecretFiles(workspace).map((f) => path5.join(workspace, f)),
|
|
572
|
+
...reads.flatMap((r) => r.secrets),
|
|
572
573
|
...HOME_SECRETS.map((f) => path5.join(os3.homedir(), f)).filter((p) => fs6.existsSync(p)),
|
|
573
574
|
// the keychain link of the worker's own home
|
|
574
575
|
path5.join(workerHome3, "Library")
|
|
@@ -581,7 +582,9 @@ function workerSettings(workspace, workerHome3) {
|
|
|
581
582
|
allow: [
|
|
582
583
|
"command(*)",
|
|
583
584
|
...both(path5.join(dataDir(workerHome3), "brain")).map((p) => `write_file(${rulePath(p)})`),
|
|
584
|
-
...toolchains().flatMap(both).map((p) => `read_file(${rulePath(p)})`)
|
|
585
|
+
...toolchains().flatMap(both).map((p) => `read_file(${rulePath(p)})`),
|
|
586
|
+
// what the task names outside the workspace: a headless agy cannot ask, and would end the turn unanswered
|
|
587
|
+
...reads.map((r) => r.path).flatMap(both).map((p) => `read_file(${rulePath(p)})`)
|
|
585
588
|
],
|
|
586
589
|
deny: [
|
|
587
590
|
...secrets.flatMap(both).map((p) => `read_file(${rulePath(p)})`),
|
|
@@ -604,6 +607,96 @@ function toolchains() {
|
|
|
604
607
|
const install2 = (d) => path5.basename(d) === "bin" && /[\\/](versions|toolchains)[\\/]/.test(d) ? path5.dirname(d) : d;
|
|
605
608
|
return [...new Set(dirs.map(install2))].filter((d) => !inHome(home()) || path5.relative(home(), d).startsWith(".."));
|
|
606
609
|
}
|
|
610
|
+
var isDir = (p) => {
|
|
611
|
+
try {
|
|
612
|
+
return fs6.statSync(p).isDirectory();
|
|
613
|
+
} catch {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
var MAX_READS = 20;
|
|
618
|
+
function pathsIn(prompt) {
|
|
619
|
+
const out = [];
|
|
620
|
+
for (const m of prompt.matchAll(/(^|[\s"'`(\[<{=])((?:~|[A-Za-z]:)?[\\/][^\s"'`)\]>}]*)/g)) {
|
|
621
|
+
const raw = m[2].replace(/[.,;:!?]+$/, "");
|
|
622
|
+
const abs = raw.startsWith("~") ? path5.join(os3.homedir(), raw.slice(1)) : raw;
|
|
623
|
+
if (raw && path5.isAbsolute(abs)) out.push({ raw, at: m.index + m[1].length, abs });
|
|
624
|
+
}
|
|
625
|
+
return out;
|
|
626
|
+
}
|
|
627
|
+
var realOf = (p) => {
|
|
628
|
+
try {
|
|
629
|
+
return fs6.realpathSync.native(p);
|
|
630
|
+
} catch {
|
|
631
|
+
return void 0;
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
function resolved(p) {
|
|
635
|
+
let dir = path5.resolve(p);
|
|
636
|
+
const rest = [];
|
|
637
|
+
for (; ; ) {
|
|
638
|
+
const r = realOf(dir);
|
|
639
|
+
if (r) return path5.join(r, ...rest);
|
|
640
|
+
const up = path5.dirname(dir);
|
|
641
|
+
if (up === dir) return path5.resolve(p);
|
|
642
|
+
rest.unshift(path5.basename(dir));
|
|
643
|
+
dir = up;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
var inside = (p, dir) => {
|
|
647
|
+
const rel = path5.relative(dir, p);
|
|
648
|
+
return !rel || !rel.startsWith("..") && !path5.isAbsolute(rel);
|
|
649
|
+
};
|
|
650
|
+
var MAX_SCANNED = 2e4;
|
|
651
|
+
function allSecrets(dir) {
|
|
652
|
+
const root = realOf(dir) ?? dir;
|
|
653
|
+
const found = [];
|
|
654
|
+
let seen = 0;
|
|
655
|
+
const stack = [dir];
|
|
656
|
+
while (stack.length) {
|
|
657
|
+
const d = stack.pop();
|
|
658
|
+
let entries;
|
|
659
|
+
try {
|
|
660
|
+
entries = fs6.readdirSync(d, { withFileTypes: true });
|
|
661
|
+
} catch {
|
|
662
|
+
return void 0;
|
|
663
|
+
}
|
|
664
|
+
for (const e of entries) {
|
|
665
|
+
if (++seen > MAX_SCANNED) return void 0;
|
|
666
|
+
const p = path5.join(d, e.name);
|
|
667
|
+
if (e.isSymbolicLink()) {
|
|
668
|
+
const target = realOf(p);
|
|
669
|
+
if (!target || !inside(target, root)) return void 0;
|
|
670
|
+
if (looksSecret(e.name)) found.push(p);
|
|
671
|
+
} else if (e.isDirectory()) stack.push(p);
|
|
672
|
+
else if (looksSecret(e.name)) found.push(p);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return found;
|
|
676
|
+
}
|
|
677
|
+
function outsideReads(prompt, workspace, project) {
|
|
678
|
+
if (process.platform === "win32") return [];
|
|
679
|
+
const homeDir = realOf(os3.homedir()) ?? os3.homedir();
|
|
680
|
+
const ws = realOf(workspace) ?? path5.resolve(workspace);
|
|
681
|
+
const proj = project ? resolved(project) : void 0;
|
|
682
|
+
const own = realOf(home()) ?? home();
|
|
683
|
+
const found = /* @__PURE__ */ new Map();
|
|
684
|
+
for (const { abs } of pathsIn(prompt)) {
|
|
685
|
+
const p = realOf(abs);
|
|
686
|
+
if (!p || found.has(p) || inside(p, ws) || inside(p, own)) continue;
|
|
687
|
+
if (inside(homeDir, p) || inside(ws, p) || proj && inside(proj, p)) continue;
|
|
688
|
+
if (p.split(/[\\/]/).filter(Boolean).length < 2) continue;
|
|
689
|
+
if (inside(p, homeDir) && /^(\.|Library([\\/]|$))/.test(path5.relative(homeDir, p))) continue;
|
|
690
|
+
if (!isDir(p)) {
|
|
691
|
+
if (!looksSecret(path5.basename(p))) found.set(p, { path: p, secrets: [] });
|
|
692
|
+
} else {
|
|
693
|
+
const secrets = allSecrets(p);
|
|
694
|
+
if (secrets) found.set(p, { path: p, secrets });
|
|
695
|
+
}
|
|
696
|
+
if (found.size >= MAX_READS) break;
|
|
697
|
+
}
|
|
698
|
+
return [...found.values()];
|
|
699
|
+
}
|
|
607
700
|
function baseSettings() {
|
|
608
701
|
return {
|
|
609
702
|
enableTerminalSandbox: true,
|
|
@@ -614,12 +707,12 @@ function baseSettings() {
|
|
|
614
707
|
}
|
|
615
708
|
var rulePath = (p) => p.replace(/^[A-Za-z]:(?=[\\/])/, "").replace(/\\/g, "/");
|
|
616
709
|
var HOMES_KEPT_DAYS = 14;
|
|
617
|
-
function workerHome(workspace) {
|
|
710
|
+
function workerHome(workspace, reads = []) {
|
|
618
711
|
const root = path5.join(home(), "antigravity-home");
|
|
619
712
|
const id2 = crypto3.createHash("sha256").update(path5.resolve(workspace)).digest("hex").slice(0, 16);
|
|
620
713
|
const dir = path5.join(root, id2);
|
|
621
714
|
fs6.mkdirSync(dataDir(dir), { recursive: true });
|
|
622
|
-
writeIfChanged(path5.join(dataDir(dir), "settings.json"), `${JSON.stringify(workerSettings(workspace, dir), null, 2)}
|
|
715
|
+
writeIfChanged(path5.join(dataDir(dir), "settings.json"), `${JSON.stringify(workerSettings(workspace, dir, reads), null, 2)}
|
|
623
716
|
`);
|
|
624
717
|
if (process.platform === "darwin") {
|
|
625
718
|
fs6.mkdirSync(path5.join(dir, "Library"), { recursive: true });
|
|
@@ -643,11 +736,25 @@ function pruneHomes(root, keep) {
|
|
|
643
736
|
} catch {
|
|
644
737
|
}
|
|
645
738
|
}
|
|
739
|
+
function inCopy(prompt, project) {
|
|
740
|
+
if (!project || project.root === project.copy) return prompt;
|
|
741
|
+
const root = resolved(project.root);
|
|
742
|
+
let out = "";
|
|
743
|
+
let from = 0;
|
|
744
|
+
for (const { raw, at, abs } of pathsIn(prompt)) {
|
|
745
|
+
const p = resolved(abs);
|
|
746
|
+
if (!inside(p, root)) continue;
|
|
747
|
+
out += prompt.slice(from, at) + path5.join(project.copy, path5.relative(root, p));
|
|
748
|
+
from = at + raw.length;
|
|
749
|
+
}
|
|
750
|
+
return out + prompt.slice(from);
|
|
751
|
+
}
|
|
646
752
|
function invocation(req) {
|
|
753
|
+
const prompt = inCopy(req.prompt, req.project);
|
|
647
754
|
const args = [
|
|
648
755
|
// -p takes the prompt as its value; one that starts with "-" would be read as an option: a leading space keeps it a value.
|
|
649
756
|
"-p",
|
|
650
|
-
|
|
757
|
+
prompt.startsWith("-") ? ` ${prompt}` : prompt,
|
|
651
758
|
"--output-format",
|
|
652
759
|
"stream-json",
|
|
653
760
|
"--mode",
|
|
@@ -662,7 +769,7 @@ function invocation(req) {
|
|
|
662
769
|
if (model) args.push("--model", model);
|
|
663
770
|
if (effort) args.push("--effort", effort);
|
|
664
771
|
const { command, prefix } = resolveCommand(binary());
|
|
665
|
-
const workerDir = workerHome(req.cwd);
|
|
772
|
+
const workerDir = workerHome(req.cwd, outsideReads(prompt, req.cwd, req.project?.root));
|
|
666
773
|
const env = { HOME: workerDir };
|
|
667
774
|
if (process.platform === "win32") env.USERPROFILE = workerDir;
|
|
668
775
|
return { command, args: [...prefix, ...args], env };
|
|
@@ -4986,7 +5093,7 @@ function verifyRefs(refs, dirs) {
|
|
|
4986
5093
|
if (!files.length) {
|
|
4987
5094
|
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
4988
5095
|
total++;
|
|
4989
|
-
invalid.push({ ref: ref.text, reason: path26.isAbsolute(ref.file) && !
|
|
5096
|
+
invalid.push({ ref: ref.text, reason: path26.isAbsolute(ref.file) && !inside2(ref.file, roots) ? "outside the project" : "file not found" });
|
|
4990
5097
|
continue;
|
|
4991
5098
|
}
|
|
4992
5099
|
total++;
|
|
@@ -5036,16 +5143,16 @@ function indexFiles(roots) {
|
|
|
5036
5143
|
return index;
|
|
5037
5144
|
}
|
|
5038
5145
|
function resolve(ref, roots) {
|
|
5039
|
-
const candidates = path26.isAbsolute(ref) ?
|
|
5146
|
+
const candidates = path26.isAbsolute(ref) ? inside2(ref, roots) ? [ref] : [] : roots.map((r) => path26.join(r, ref));
|
|
5040
5147
|
return candidates.find((c) => {
|
|
5041
5148
|
try {
|
|
5042
|
-
return fs30.statSync(c).isFile() &&
|
|
5149
|
+
return fs30.statSync(c).isFile() && inside2(c, roots);
|
|
5043
5150
|
} catch {
|
|
5044
5151
|
return false;
|
|
5045
5152
|
}
|
|
5046
5153
|
});
|
|
5047
5154
|
}
|
|
5048
|
-
function
|
|
5155
|
+
function inside2(file2, roots) {
|
|
5049
5156
|
return roots.some((r) => {
|
|
5050
5157
|
const rel = path26.relative(r, path26.resolve(file2));
|
|
5051
5158
|
return rel !== "" && !rel.startsWith("..") && !path26.isAbsolute(rel);
|
|
@@ -5054,7 +5161,7 @@ function inside(file2, roots) {
|
|
|
5054
5161
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5055
5162
|
|
|
5056
5163
|
// src/core/run.ts
|
|
5057
|
-
var VERSION2 = true ? "0.
|
|
5164
|
+
var VERSION2 = true ? "0.25.0" : "0.0.0-dev";
|
|
5058
5165
|
function planWork(o) {
|
|
5059
5166
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
5060
5167
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -5471,7 +5578,8 @@ function attempt(meta, backend, target) {
|
|
|
5471
5578
|
sessionId: meta.sessionId,
|
|
5472
5579
|
files: meta.files,
|
|
5473
5580
|
web: !!meta.web,
|
|
5474
|
-
title: `pitroom ${meta.mode}: ${meta.task.replace(/\s+/g, " ").slice(0, 60)}
|
|
5581
|
+
title: `pitroom ${meta.mode}: ${meta.task.replace(/\s+/g, " ").slice(0, 60)}`,
|
|
5582
|
+
project: meta.repoRoot && (meta.snapshot?.dir ?? meta.worktree) && meta.cwd !== meta.dir ? { root: meta.repoRoot, copy: meta.snapshot?.dir ?? meta.worktree } : void 0
|
|
5475
5583
|
});
|
|
5476
5584
|
return spawnWorker(inv, {
|
|
5477
5585
|
cwd: meta.cwd,
|
|
@@ -6587,7 +6695,8 @@ var MISSING = `<!doctype html><meta charset="utf-8"><title>Pitroom</title><body
|
|
|
6587
6695
|
|
|
6588
6696
|
// src/core/dash.ts
|
|
6589
6697
|
var DEFAULT_PORT = 7878;
|
|
6590
|
-
var
|
|
6698
|
+
var DAY_MS = 24 * 3600 * 1e3;
|
|
6699
|
+
var PERIODS = { day: 1, week: 7, month: 30, half: 182, all: Infinity };
|
|
6591
6700
|
var SCAN = 400;
|
|
6592
6701
|
var RUN_ID3 = /^\d{8}-\d{6}-[0-9a-f]{4}$/;
|
|
6593
6702
|
var LOCAL_HOST = /^(127\.0\.0\.1|localhost|\[::1\]):\d+$/i;
|
|
@@ -6659,10 +6768,17 @@ function dashState(opts = {}) {
|
|
|
6659
6768
|
}
|
|
6660
6769
|
const whole = new Set(metas.slice(0, limit).flatMap((m) => m.group ? [m.group] : []));
|
|
6661
6770
|
const runs = metas.filter((m, i) => i < limit || m.group && whole.has(m.group)).map(toRun);
|
|
6771
|
+
const now = Date.now();
|
|
6772
|
+
const ledger = readLedger();
|
|
6773
|
+
const within = (at, days2) => !!at && Date.parse(at) >= now - days2 * DAY_MS;
|
|
6774
|
+
const per = (f) => Object.fromEntries(Object.entries(PERIODS).map(([p, days2]) => [p, f(days2)]));
|
|
6662
6775
|
return {
|
|
6663
6776
|
price: primaryPrice().name,
|
|
6664
6777
|
running: runs.filter((r) => r.state === "running" || r.state === "queued").length,
|
|
6665
|
-
|
|
6778
|
+
finished: per(
|
|
6779
|
+
(days2) => opts.group ? metas.filter((m) => m.state === "done" && !m.auditOf && within(m.endedAt, days2)).length : ledger.filter((e) => e.state === "done" && within(e.at, days2)).length
|
|
6780
|
+
),
|
|
6781
|
+
saved: per((days2) => totals(ledger.filter((e) => within(e.at, days2))).saved),
|
|
6666
6782
|
groups: [...groups].sort(),
|
|
6667
6783
|
runs
|
|
6668
6784
|
};
|
|
@@ -7696,7 +7812,7 @@ function answerParts(answer) {
|
|
|
7696
7812
|
const summary = s ? s[1] : text.split(/^\s*(?:DETAILS|FILES CHANGED|VERIFICATION|OPEN ISSUES):/m)[0];
|
|
7697
7813
|
return { summary: summary.trim(), details: (d?.[1] ?? "").trim() };
|
|
7698
7814
|
}
|
|
7699
|
-
function
|
|
7815
|
+
function pathsIn2(text) {
|
|
7700
7816
|
const found = text.replace(/\\/g, "/").match(/[A-Za-z0-9_.@\-/]+\.[A-Za-z0-9]{1,8}(?::\d+)?/g) ?? [];
|
|
7701
7817
|
return [...new Set(found.map((p) => clean(p.replace(/:\d+$/, ""))))];
|
|
7702
7818
|
}
|
|
@@ -7708,7 +7824,7 @@ function scoreAnswer(q, truth, answer, root) {
|
|
|
7708
7824
|
if ("line" in truth) {
|
|
7709
7825
|
const flat = summary.replace(/\s+/g, " ");
|
|
7710
7826
|
if (new RegExp(`(^|[^\\w./-])\\.?/?${escape2(truth.path)}:${truth.line}(?!\\d)`).test(flat)) return { score: 1, got: `${truth.path}:${truth.line}` };
|
|
7711
|
-
const named =
|
|
7827
|
+
const named = pathsIn2(flat).includes(truth.path);
|
|
7712
7828
|
return { score: named ? 0.5 : 0, got: flat.slice(0, 120) };
|
|
7713
7829
|
}
|
|
7714
7830
|
if ("count" in truth) {
|
|
@@ -7716,8 +7832,8 @@ function scoreAnswer(q, truth, answer, root) {
|
|
|
7716
7832
|
const got2 = n ? Number(n[0].replace(/,/g, "")) : void 0;
|
|
7717
7833
|
return { score: got2 === truth.count ? 1 : 0, got: got2 === void 0 ? "(no number)" : String(got2) };
|
|
7718
7834
|
}
|
|
7719
|
-
let got =
|
|
7720
|
-
if (!got.length) got =
|
|
7835
|
+
let got = pathsIn2(summary);
|
|
7836
|
+
if (!got.length) got = pathsIn2(details);
|
|
7721
7837
|
const hit = got.filter((p) => truth.files.includes(p)).length;
|
|
7722
7838
|
const precision = got.length ? hit / got.length : 0;
|
|
7723
7839
|
const recall = truth.files.length ? hit / truth.files.length : got.length ? 0 : 1;
|
|
@@ -7880,9 +7996,9 @@ function writePackage(job) {
|
|
|
7880
7996
|
const root = repoRoot(job.dir);
|
|
7881
7997
|
if (!g || !root) throw new UserError(`not a git repository: ${job.dir}`);
|
|
7882
7998
|
const name = `review-${crypto13.randomBytes(4).toString("hex")}.md`;
|
|
7883
|
-
const
|
|
7884
|
-
const file2 =
|
|
7885
|
-
if (!
|
|
7999
|
+
const inside3 = !path35.relative(fs40.realpathSync(root), fs40.realpathSync(g)).startsWith("..");
|
|
8000
|
+
const file2 = inside3 ? path35.join(g, "pitroom", name) : path35.join(root, ".pitroom", name);
|
|
8001
|
+
if (!inside3) ignoreInGit(root, "/.pitroom/");
|
|
7886
8002
|
fs40.mkdirSync(path35.dirname(file2), { recursive: true });
|
|
7887
8003
|
fs40.writeFileSync(file2, job.package);
|
|
7888
8004
|
return file2;
|
|
@@ -8615,9 +8731,9 @@ function doctor8(probe2) {
|
|
|
8615
8731
|
section2("Worker chain");
|
|
8616
8732
|
const chain = [];
|
|
8617
8733
|
try {
|
|
8618
|
-
const
|
|
8619
|
-
chain.push(
|
|
8620
|
-
for (const w of
|
|
8734
|
+
const resolved2 = resolveChain();
|
|
8735
|
+
chain.push(resolved2.worker, ...resolved2.fallback);
|
|
8736
|
+
for (const w of resolved2.warnings) add2("warn", w);
|
|
8621
8737
|
} catch (e) {
|
|
8622
8738
|
add2("fail", e.message);
|
|
8623
8739
|
}
|