pitroom 0.23.0 → 0.24.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/dist/lib.mjs CHANGED
@@ -6,56 +6,848 @@ var UserError = class extends Error {
6
6
  }
7
7
  };
8
8
 
9
- // src/backends/claude/index.ts
9
+ // src/backends/antigravity/index.ts
10
+ import crypto3 from "node:crypto";
10
11
  import { spawnSync } from "node:child_process";
11
- import fs2 from "node:fs";
12
- import os2 from "node:os";
13
- import path2 from "node:path";
12
+ import fs6 from "node:fs";
13
+ import os3 from "node:os";
14
+ import path5 from "node:path";
14
15
 
15
- // src/backends/exec.ts
16
+ // src/core/secrets.ts
17
+ import { execFileSync } from "node:child_process";
16
18
  import fs from "node:fs";
17
- import os from "node:os";
18
19
  import path from "node:path";
20
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
21
+ var MAX_VISITED = 2e4;
22
+ var MAX_DEPTH = 4;
23
+ var TEMPLATE = /\.(example|sample|template|dist|defaults?|tpl)$/i;
24
+ function looksSecret(file) {
25
+ const name = file.toLowerCase();
26
+ if (TEMPLATE.test(name)) return false;
27
+ if (name === ".env" || name.startsWith(".env.") || name.endsWith(".env")) return true;
28
+ return /\.(pem|p12|pfx|key|jks|keystore)$/.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || name === ".netrc" || name === "credentials.json" || /^secrets?\.(json|ya?ml|toml)$/.test(name);
29
+ }
30
+ function findSecretFiles(dir) {
31
+ const found = [];
32
+ let visited = 0;
33
+ const walk = (d, depth) => {
34
+ if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
35
+ let entries;
36
+ try {
37
+ entries = fs.readdirSync(d, { withFileTypes: true });
38
+ } catch {
39
+ return;
40
+ }
41
+ for (const e of entries) {
42
+ visited++;
43
+ if (e.isDirectory()) {
44
+ if (!SKIP_DIRS.has(e.name)) walk(path.join(d, e.name), depth + 1);
45
+ } else if (e.isFile() && looksSecret(e.name)) {
46
+ found.push(slash(path.relative(dir, path.join(d, e.name))));
47
+ }
48
+ }
49
+ };
50
+ walk(dir, 0);
51
+ return found.sort();
52
+ }
53
+ function findSecretFilesInTree(root, dir) {
54
+ try {
55
+ const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
56
+ return out.split("\0").filter((f) => f && looksSecret(path.basename(f)) && !path.relative(dir, path.join(root, f)).startsWith("..")).map((f) => slash(path.relative(dir, path.join(root, f)))).sort();
57
+ } catch {
58
+ return [];
59
+ }
60
+ }
61
+ function secretWarning(files, mode) {
62
+ if (!files.length) return void 0;
63
+ const shown = files.slice(0, 3).join(", ") + (files.length > 3 ? `, +${files.length - 3} more` : "");
64
+ const where = mode === "isolate" ? "would be copied into the isolated copy" : "sit in the directory the worker runs in";
65
+ const fix = mode === "isolate" ? "add them to .gitignore, or" : "use an isolated copy (-i, git-ignored files are left out) or a clean checkout, or";
66
+ return `secret-looking files ${where} (${shown}): the worker is told not to read them, but nothing stops it, and its model may be hosted by a third party; ${fix} set PITROOM_NO_SECRET_WARNING=1 to silence this`;
67
+ }
68
+ var slash = (p) => p.split(path.sep).join("/");
69
+
70
+ // src/core/store.ts
71
+ import fs3 from "node:fs";
72
+ import os from "node:os";
73
+ import path2 from "node:path";
74
+ import crypto from "node:crypto";
75
+
76
+ // src/core/fs-atomic.ts
77
+ import fs2 from "node:fs";
78
+ var BUSY = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
79
+ function renameOver(tmp, file) {
80
+ for (let attempt = 0; ; attempt++) {
81
+ try {
82
+ fs2.renameSync(tmp, file);
83
+ return;
84
+ } catch (e) {
85
+ if (process.platform !== "win32" || !BUSY.has(e.code ?? "") || attempt >= 40) throw e;
86
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
87
+ }
88
+ }
89
+ }
90
+
91
+ // src/core/store.ts
92
+ var TERMINAL = ["done", "failed", "timeout", "stopped"];
93
+ var isActive = (s) => !TERMINAL.includes(s);
94
+ function home() {
95
+ if (process.env.PITROOM_HOME) return path2.resolve(process.env.PITROOM_HOME);
96
+ if (process.platform === "win32") {
97
+ return path2.join(process.env.LOCALAPPDATA ?? path2.join(os.homedir(), "AppData", "Local"), "pitroom");
98
+ }
99
+ return path2.join(process.env.XDG_STATE_HOME ?? path2.join(os.homedir(), ".local", "state"), "pitroom");
100
+ }
101
+ var runsDir = () => path2.join(home(), "runs");
102
+ var ledgerFile = () => path2.join(home(), "ledger.jsonl");
103
+ var runDir = (id2) => path2.join(runsDir(), id2);
104
+ var runFile = (id2, name) => path2.join(runDir(id2), name);
105
+ function newRunId(now = /* @__PURE__ */ new Date()) {
106
+ const p = (n) => String(n).padStart(2, "0");
107
+ const stamp2 = `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
108
+ return `${stamp2}-${crypto.randomBytes(2).toString("hex")}`;
109
+ }
110
+ var onFinished;
111
+ var archive;
112
+ function writeMeta(meta) {
113
+ fs3.mkdirSync(runDir(meta.id), { recursive: true });
114
+ const file = runFile(meta.id, "meta.json");
115
+ const tmp = `${file}.${process.pid}.tmp`;
116
+ fs3.writeFileSync(tmp, JSON.stringify(meta, null, 2));
117
+ renameOver(tmp, file);
118
+ if (TERMINAL.includes(meta.state)) {
119
+ try {
120
+ onFinished?.(meta);
121
+ } catch {
122
+ }
123
+ }
124
+ }
125
+ function readMeta(id2) {
126
+ const file = runFile(id2, "meta.json");
127
+ if (!fs3.existsSync(file)) {
128
+ const kept = archive?.meta(id2);
129
+ if (kept) return upgrade(kept);
130
+ }
131
+ return upgrade(JSON.parse(fs3.readFileSync(file, "utf8")));
132
+ }
133
+ function upgrade(raw) {
134
+ if (!raw.worker) raw.worker = raw.model ? { backend: "opencode", model: raw.model } : { backend: "opencode" };
135
+ if (!Array.isArray(raw.fallback)) {
136
+ raw.fallback = (raw.fallbackModels ?? []).map((m) => ({ backend: "opencode", model: m }));
137
+ }
138
+ raw.warnings ??= [];
139
+ raw.files ??= [];
140
+ raw.link ??= [];
141
+ return raw;
142
+ }
143
+ function listRunIds() {
144
+ if (!fs3.existsSync(runsDir())) return [];
145
+ return fs3.readdirSync(runsDir()).filter((d) => fs3.existsSync(runFile(d, "meta.json"))).sort();
146
+ }
147
+ function resolveRun(ref) {
148
+ const ids = listRunIds();
149
+ if (!ref || ref === "latest" || ref === "last") {
150
+ if (!ids.length) throw new UserError("no runs yet");
151
+ return ids[ids.length - 1];
152
+ }
153
+ if (ids.includes(ref)) return ref;
154
+ const hits = ids.filter((id2) => id2.startsWith(ref) || id2.endsWith(ref));
155
+ if (hits.length === 1) return hits[0];
156
+ if (!hits.length) {
157
+ const kept = archive?.id(ref);
158
+ if (kept) return kept;
159
+ }
160
+ throw new UserError(hits.length ? `ambiguous run "${ref}": ${hits.join(", ")}` : ids.length ? `unknown run "${ref}"` : "no runs yet");
161
+ }
162
+ function isAlive(pid) {
163
+ if (!pid) return false;
164
+ try {
165
+ process.kill(pid, 0);
166
+ return true;
167
+ } catch (e) {
168
+ return e.code === "EPERM";
169
+ }
170
+ }
171
+ var STOP_FILE = "stop-requested";
172
+ var requestStop = (id2) => fs3.writeFileSync(runFile(id2, STOP_FILE), "");
173
+ function freshMeta(id2) {
174
+ const meta = readMeta(id2);
175
+ if (!TERMINAL.includes(meta.state) && meta.pid && !isAlive(meta.pid)) {
176
+ const latest = readMeta(id2);
177
+ if (TERMINAL.includes(latest.state)) return latest;
178
+ const stopped = latest.stopRequested || fs3.existsSync(runFile(id2, STOP_FILE));
179
+ latest.state = stopped ? "stopped" : "failed";
180
+ if (!stopped) latest.error ??= "worker process exited unexpectedly";
181
+ latest.endedAt ??= (/* @__PURE__ */ new Date()).toISOString();
182
+ writeMeta(latest);
183
+ return latest;
184
+ }
185
+ return meta;
186
+ }
187
+
188
+ // src/backends/deny.ts
189
+ var FORBIDDEN_COMMANDS = [
190
+ "git commit",
191
+ "git push",
192
+ "git pull",
193
+ "git reset",
194
+ "git checkout",
195
+ "git restore",
196
+ "git clean",
197
+ "git stash",
198
+ "git rebase",
199
+ "git merge",
200
+ "git switch",
201
+ "git branch",
202
+ "git tag",
203
+ "git rm",
204
+ "git worktree",
205
+ "git update-ref",
206
+ "git config",
207
+ "git remote",
208
+ "git cherry-pick",
209
+ "git revert",
210
+ "git am",
211
+ "git add",
212
+ "rm -rf",
213
+ "rm -fr",
214
+ "rm -r",
215
+ "rm -R",
216
+ "rm --recursive",
217
+ "sudo",
218
+ "chown",
219
+ "kill",
220
+ "pkill",
221
+ "killall",
222
+ "gh",
223
+ "npm publish",
224
+ "pitroom",
225
+ "opencode",
226
+ "claude",
227
+ "codex",
228
+ "gemini",
229
+ "qwen",
230
+ "kilo",
231
+ "kilocode",
232
+ "agy"
233
+ ];
234
+
235
+ // src/backends/exec.ts
236
+ import fs4 from "node:fs";
237
+ import os2 from "node:os";
238
+ import path3 from "node:path";
19
239
  function findBinary(name, envVar, known = []) {
20
240
  const override = process.env[envVar];
21
241
  if (override) return override;
22
242
  const exts = process.platform === "win32" ? [".exe", ".cmd", ""] : [""];
23
- for (const dir of (process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
243
+ for (const dir of (process.env.PATH ?? "").split(path3.delimiter).filter(Boolean)) {
24
244
  for (const ext of exts) {
25
- const p = path.join(dir, `${name}${ext}`);
245
+ const p = path3.join(dir, `${name}${ext}`);
26
246
  if (isFile(p)) return p;
27
247
  }
28
248
  }
29
- return known.map((k) => k.replace(/^~/, os.homedir())).find(isFile) ?? name;
249
+ return known.map((k) => k.replace(/^~/, os2.homedir())).find(isFile) ?? name;
250
+ }
251
+ function isFile(p) {
252
+ try {
253
+ return fs4.statSync(p).isFile();
254
+ } catch {
255
+ return false;
256
+ }
257
+ }
258
+ function resolveCommand(bin) {
259
+ try {
260
+ const real = fs4.realpathSync(bin);
261
+ const fd = fs4.openSync(real, "r");
262
+ const head = Buffer.alloc(128);
263
+ const n = fs4.readSync(fd, head, 0, 128, 0);
264
+ fs4.closeSync(fd);
265
+ const firstLine = head.subarray(0, n).toString("utf8").split("\n")[0] ?? "";
266
+ if (/^#!.*\bnode\b/.test(firstLine)) return { command: process.execPath, prefix: [real] };
267
+ } catch {
268
+ }
269
+ return { command: bin, prefix: [] };
270
+ }
271
+
272
+ // src/backends/home.ts
273
+ import crypto2 from "node:crypto";
274
+ import fs5 from "node:fs";
275
+ import path4 from "node:path";
276
+ function writeIfChanged(file, body) {
277
+ try {
278
+ if (fs5.readFileSync(file, "utf8") === body) return;
279
+ } catch {
280
+ }
281
+ fs5.mkdirSync(path4.dirname(file), { recursive: true });
282
+ const tmp = `${file}.${process.pid}-${crypto2.randomBytes(3).toString("hex")}.tmp`;
283
+ fs5.writeFileSync(tmp, body);
284
+ for (let tries = 0; ; tries++) {
285
+ try {
286
+ fs5.renameSync(tmp, file);
287
+ return;
288
+ } catch (e) {
289
+ const busy = ["EPERM", "EBUSY", "EACCES"].includes(e.code ?? "");
290
+ if (busy && tries < 10) {
291
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);
292
+ continue;
293
+ }
294
+ fs5.rmSync(tmp, { force: true });
295
+ if (!busy) throw e;
296
+ fs5.writeFileSync(file, body);
297
+ return;
298
+ }
299
+ }
300
+ }
301
+ function linkInto(dir, from) {
302
+ const to = path4.join(dir, path4.basename(from));
303
+ try {
304
+ let st;
305
+ try {
306
+ st = fs5.lstatSync(to);
307
+ } catch {
308
+ }
309
+ if (st && !st.isSymbolicLink()) return;
310
+ if (st) {
311
+ if (fs5.readlinkSync(to) === from && fs5.existsSync(from)) return;
312
+ fs5.unlinkSync(to);
313
+ }
314
+ if (fs5.existsSync(from)) fs5.symlinkSync(from, to);
315
+ } catch {
316
+ }
317
+ }
318
+
319
+ // src/backends/text.ts
320
+ var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
321
+ function num(v) {
322
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
323
+ }
324
+ function oneLine(s, max = 80) {
325
+ return clip(s.replace(/\s+/g, " ").trim(), max);
326
+ }
327
+ function splitModel(model) {
328
+ if (!model) return {};
329
+ const [m, effort] = model.split("#");
330
+ return { model: m || void 0, effort: effort || void 0 };
331
+ }
332
+
333
+ // src/backends/types.ts
334
+ var MAX_STEPS = 400;
335
+
336
+ // src/backends/antigravity/events.ts
337
+ var EDIT_TOOLS = /* @__PURE__ */ new Set(["write_to_file", "replace_file_content", "multi_replace_file_content", "edit_file", "write_file"]);
338
+ var SHELL = "run_command";
339
+ var DENIED = /soft-denied|permission|denied|not allowed|blocked|refus|requires approval/i;
340
+ var FAILED = /error|fail|denied|cancel|reject/i;
341
+ var OK = /^(success|succeeded|ok|done|completed?)$/i;
342
+ function parseEvents(jsonl) {
343
+ const usage = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0, steps: 0, toolCalls: 0, denied: 0 };
344
+ const stepUsage = [];
345
+ const usageSeen = /* @__PURE__ */ new Set();
346
+ const tools = {};
347
+ const edits = [];
348
+ const timeline = [];
349
+ const said = /* @__PURE__ */ new Map();
350
+ const stepOf = /* @__PURE__ */ new Map();
351
+ const toolSeen = /* @__PURE__ */ new Set();
352
+ const done = /* @__PURE__ */ new Set();
353
+ let sessionId;
354
+ let model;
355
+ let lastText = "";
356
+ let response;
357
+ let lastActivity;
358
+ let error;
359
+ let streamError;
360
+ let resultSeen = false;
361
+ let haveResultUsage = false;
362
+ const open = /* @__PURE__ */ new Map();
363
+ let unnamed = 0;
364
+ let turns = 0;
365
+ let inTurn = false;
366
+ const startTurn = () => {
367
+ if (!inTurn) turns++;
368
+ inTurn = true;
369
+ };
370
+ for (const line of jsonl.split("\n")) {
371
+ if (!line.trim()) continue;
372
+ let e;
373
+ try {
374
+ e = JSON.parse(line);
375
+ } catch {
376
+ continue;
377
+ }
378
+ if (!e || typeof e !== "object") continue;
379
+ const type = String(e.event ?? e.type ?? "").toLowerCase();
380
+ const body = (name) => e[name] && typeof e[name] === "object" ? e[name] : e;
381
+ if (type === "init") {
382
+ sessionId ??= id(e.conversation_id ?? e.session_id ?? body("init").conversation_id);
383
+ model ??= id(body("init").model);
384
+ } else if (type === "step_update") {
385
+ const s = body("step_update");
386
+ sessionId ??= id(s.conversation_id);
387
+ const kind2 = String(s.step_type ?? "").toLowerCase();
388
+ const state = String(s.state ?? "").toUpperCase();
389
+ const failedStep = state === "ERROR";
390
+ const finished = state === "DONE" || failedStep;
391
+ let key = s.step_index ?? s.step_id;
392
+ if (key === void 0 || key === null) {
393
+ key = open.get(kind2) ?? `${kind2}#${unnamed++}`;
394
+ if (finished) open.delete(kind2);
395
+ else open.set(kind2, key);
396
+ }
397
+ key = String(key);
398
+ if (kind2 === "agent_response") {
399
+ startTurn();
400
+ const text = (said.get(key) ?? "") + str(s.text_delta);
401
+ said.set(key, text);
402
+ if (finished && !done.has(key)) {
403
+ done.add(key);
404
+ const t = text.trim();
405
+ if (t) {
406
+ lastText = t;
407
+ lastActivity = `says: ${oneLine(t)}`;
408
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(t, 600) });
409
+ }
410
+ }
411
+ } else if (kind2 === "tool") {
412
+ const info = s.tool_info && typeof s.tool_info === "object" ? s.tool_info : {};
413
+ const name = String(info.name ?? info.tool_name ?? s.tool_name ?? "tool");
414
+ const p = info.parameters ?? info.args ?? info.arguments ?? s.parameters ?? {};
415
+ if (!toolSeen.has(key)) {
416
+ toolSeen.add(key);
417
+ startTurn();
418
+ lastText = "";
419
+ usage.toolCalls++;
420
+ tools[name] = (tools[name] ?? 0) + 1;
421
+ const target = p.CommandLine ?? p.command ?? p.TargetFile ?? p.AbsolutePath ?? p.file_path ?? p.path ?? p.SearchPath ?? p.Query ?? p.query ?? p.Url ?? p.url;
422
+ lastActivity = `${name} ${target ? oneLine(String(target), 60) : ""}`.trim();
423
+ if (timeline.length < MAX_STEPS) {
424
+ const step = { kind: EDIT_TOOLS.has(name) ? "edit" : name === SHELL ? "shell" : "tool", name, text: clip(String(target ?? ""), 240) };
425
+ timeline.push(step);
426
+ stepOf.set(key, step);
427
+ }
428
+ }
429
+ if (finished && !done.has(key)) {
430
+ done.add(key);
431
+ inTurn = false;
432
+ const problem = `${errText(info.error)} ${str(info.status ?? s.status)}`.trim();
433
+ const ok = !failedStep && (!problem || OK.test(problem) || !FAILED.test(problem));
434
+ const step = stepOf.get(key);
435
+ if (step) step.ok = ok;
436
+ if (!ok && DENIED.test(`${problem} ${str(info.output)}`)) usage.denied++;
437
+ const file = p.TargetFile ?? p.AbsolutePath ?? p.file_path ?? p.path;
438
+ if (ok && EDIT_TOOLS.has(name) && file) edits.push(String(file));
439
+ }
440
+ }
441
+ if (finished && s.usage && typeof s.usage === "object" && !usageSeen.has(`${kind2}:${key}`)) {
442
+ usageSeen.add(`${kind2}:${key}`);
443
+ stepUsage.push(s.usage);
444
+ }
445
+ } else if (type === "result") {
446
+ const r = body("result");
447
+ sessionId ??= id(r.conversation_id);
448
+ model ??= id(r.model);
449
+ if (typeof r.response === "string") response = r.response.trim();
450
+ if (r.usage && typeof r.usage === "object") {
451
+ add(usage, r.usage);
452
+ haveResultUsage = true;
453
+ }
454
+ if (Array.isArray(r.denied_actions)) usage.denied = Math.max(usage.denied, r.denied_actions.length);
455
+ resultSeen = true;
456
+ const status = str(r.status);
457
+ const message = errText(r.error);
458
+ if (status ? !OK.test(status) : !!message) {
459
+ error ??= message || (/^waiting$/i.test(status) ? "agy stopped waiting for an approval a headless run cannot give" : `agy ended with status ${status}`);
460
+ }
461
+ } else if (type === "error") {
462
+ streamError ??= typeof e.message === "string" && e.message || errText(e.error) || "Antigravity CLI error";
463
+ }
464
+ }
465
+ if (streamError && (!resultSeen || !response)) error ??= streamError;
466
+ if (!haveResultUsage) for (const u of stepUsage) add(usage, u);
467
+ usage.steps = turns;
468
+ const finalText = error ? "" : response || lastText;
469
+ return { sessionId, model, finalText, usage, tools, edits, lastActivity, timeline, error };
470
+ }
471
+ function add(u, r) {
472
+ const cached3 = num(r.cache_read_tokens ?? r.cached_tokens);
473
+ const input = num(r.input_tokens);
474
+ u.cacheRead += cached3;
475
+ u.input += Math.max(0, input - cached3);
476
+ u.output += num(r.output_tokens);
477
+ u.reasoning += num(r.thinking_tokens ?? r.reasoning_tokens);
478
+ u.total += num(r.total_tokens) || input + num(r.output_tokens) + num(r.thinking_tokens ?? r.reasoning_tokens);
479
+ }
480
+ var id = (v) => typeof v === "string" && v ? v : void 0;
481
+ function errText(v) {
482
+ if (typeof v === "string") return v;
483
+ if (!v || typeof v !== "object" || !Object.keys(v).length) return "";
484
+ const code = typeof v.code === "string" || typeof v.code === "number" ? String(v.code) : "";
485
+ const message = typeof v.message === "string" ? v.message : typeof v.reason === "string" ? v.reason : "";
486
+ return [code, message].filter(Boolean).join(": ") || JSON.stringify(v);
487
+ }
488
+ var str = (v) => typeof v === "string" ? v : v === void 0 || v === null ? "" : typeof v === "object" ? JSON.stringify(v) : String(v);
489
+
490
+ // src/backends/antigravity/index.ts
491
+ var binary = () => findBinary("agy", "PITROOM_ANTIGRAVITY_BIN", ["~/.local/bin/agy", "~/.antigravity/bin/agy"]);
492
+ function agy(args, timeout = 3e4) {
493
+ const { command, prefix } = resolveCommand(binary());
494
+ const r = spawnSync(command, [...prefix, ...args], { encoding: "utf8", timeout, stdio: ["ignore", "pipe", "pipe"] });
495
+ return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
496
+ }
497
+ var dataDir = (h) => path5.join(h, ".gemini", "antigravity-cli");
498
+ var userDir = () => dataDir(os3.homedir());
499
+ var TOKEN = "antigravity-oauth-token";
500
+ var HOME_SECRETS = [".ssh", ".aws", ".gnupg", ".netrc", ".docker", ".kube", ".config/gcloud", ".config/gh", ".gemini", "Library/Keychains"];
501
+ function denyCommandRules() {
502
+ const subcommands = FORBIDDEN_COMMANDS.filter((c) => c.startsWith("git ")).map((c) => c.slice(4));
503
+ const anywhere = (cmd, option) => [0, 1, 2, 3, 4, 5].map((n) => `${cmd} ${".* ".repeat(n)}${option}`);
504
+ return [
505
+ ...FORBIDDEN_COMMANDS.map((c) => `command(${c})`),
506
+ ...FORBIDDEN_COMMANDS.filter((c) => !c.startsWith("git ")).map((c) => `command(regex:.*/${c})`),
507
+ // git by path would bypass the git guard's PATH shim; -C, -c, --git-dir and --work-tree point it elsewhere
508
+ "command(regex:.*/git)",
509
+ "command(regex:git (-C|-c|--git-dir.*|--work-tree.*))",
510
+ `command(regex:git -.* (${subcommands.join("|")}))`,
511
+ `command(regex:git -.* -.* (${subcommands.join("|")}))`,
512
+ // a recursive delete however its options are spelled or placed (rm -Rf, rm -r -f, rm dir -rf, rm --recursive)
513
+ ...["rm", ".*/rm"].flatMap((rm) => [...anywhere(rm, "-[a-zA-Z]*[rR][a-zA-Z]*"), ...anywhere(rm, "--recursive")]).map((r) => `command(regex:${r})`),
514
+ // find runs what follows -exec itself, and agy does not take it apart; such a rule only holds when it spans the
515
+ // whole command (a prefix of it was seen not to match), so find with -exec, -ok or -delete is refused outright,
516
+ // up to six words before and after (grep -r does what find -exec grep did)
517
+ ...["find", ".*/find"].flatMap((find) => [0, 1, 2, 3, 4, 5, 6].flatMap((before) => [0, 1, 2, 3, 4, 5, 6].flatMap((after) => {
518
+ const words = (n) => " .*".repeat(n);
519
+ return [
520
+ ...after ? [`${find}${words(before)} -(exec|execdir|ok|okdir)${words(after)}`] : [],
521
+ `${find}${words(before)} -delete${words(after)}`
522
+ ];
523
+ }))).map((r) => `command(regex:${r})`),
524
+ // xargs runs its arguments as a command, and agy does not take it apart either; with a `{}` in it a rule has
525
+ // to span the whole command again, so the words after the command are spelled out too
526
+ ...["xargs", ".*/xargs"].flatMap((xargs) => [0, 1, 2, 3].flatMap((n) => [0, 1, 2, 3].flatMap((after) => [
527
+ `${xargs}${" .*".repeat(n)} (.*/)?(${FORBIDDEN_COMMANDS.filter((c) => !c.includes(" ")).join("|")})${" .*".repeat(after)}`,
528
+ `${xargs}${" .*".repeat(n)} (.*/)?git (${subcommands.join("|")})${" .*".repeat(after)}`,
529
+ `${xargs}${" .*".repeat(n)} (.*/)?rm -[a-zA-Z]*[rR][a-zA-Z]*${" .*".repeat(after)}`
530
+ ]))).map((r) => `command(regex:${r})`),
531
+ // a command line with a `{}` in it is not split into commands: a rule is then matched against the whole line,
532
+ // so these find xargs and find wherever they stand in it
533
+ `command(regex:.*git (${subcommands.join("|")}).*)`,
534
+ "command(regex:.*rm -[a-zA-Z]*[rR].*)",
535
+ `command(regex:.*xargs .*rm -[a-zA-Z]*[rR].*)`,
536
+ `command(regex:.*xargs .*git .*(${subcommands.join("|")}).*)`,
537
+ "command(regex:.*find .*-(exec|execdir|ok|okdir) .*)",
538
+ "command(regex:.*find .*-delete.*)",
539
+ // a command inside a string is not matched above
540
+ "command(regex:(ba|z|da|k)?sh -[a-zA-Z]*c[a-zA-Z]*)",
541
+ "command(regex:.*/(ba|z|da|k)?sh -[a-zA-Z]*c[a-zA-Z]*)",
542
+ "command(eval)"
543
+ ];
544
+ }
545
+ function workerSettings(workspace, workerHome3, reads = []) {
546
+ const real = (p) => {
547
+ try {
548
+ return fs6.realpathSync.native(p);
549
+ } catch {
550
+ return p;
551
+ }
552
+ };
553
+ const both = (p) => [.../* @__PURE__ */ new Set([p, real(p)])];
554
+ const secrets = [
555
+ ...findSecretFiles(workspace).map((f) => path5.join(workspace, f)),
556
+ ...reads.flatMap((r) => r.secrets),
557
+ ...HOME_SECRETS.map((f) => path5.join(os3.homedir(), f)).filter((p) => fs6.existsSync(p)),
558
+ // the keychain link of the worker's own home
559
+ path5.join(workerHome3, "Library")
560
+ ];
561
+ return {
562
+ ...baseSettings(),
563
+ permissions: {
564
+ // plan mode writes its plan into agy's own data folder, which is outside the workspace; the user's toolchains
565
+ // under their home stay usable in the sandbox (read-only), which sees only what is granted besides the system
566
+ allow: [
567
+ "command(*)",
568
+ ...both(path5.join(dataDir(workerHome3), "brain")).map((p) => `write_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)})`)
572
+ ],
573
+ deny: [
574
+ ...secrets.flatMap(both).map((p) => `read_file(${rulePath(p)})`),
575
+ ...denyCommandRules(),
576
+ "mcp(*)",
577
+ // No web for agy workers: the rule is in the settings file, so --web cannot switch it on per run.
578
+ "read_url(*)",
579
+ "execute_url(*)"
580
+ ]
581
+ }
582
+ };
583
+ }
584
+ function toolchains() {
585
+ const homeDir = os3.homedir();
586
+ const inHome = (d) => {
587
+ const rel = path5.relative(homeDir, d);
588
+ return !!rel && !rel.startsWith("..") && !path5.isAbsolute(rel);
589
+ };
590
+ const dirs = (process.env.PATH ?? "").split(path5.delimiter).filter((d) => d && path5.isAbsolute(d) && inHome(d) && fs6.existsSync(d));
591
+ const install = (d) => path5.basename(d) === "bin" && /[\\/](versions|toolchains)[\\/]/.test(d) ? path5.dirname(d) : d;
592
+ return [...new Set(dirs.map(install))].filter((d) => !inHome(home()) || path5.relative(home(), d).startsWith(".."));
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
+ }
684
+ function baseSettings() {
685
+ return {
686
+ enableTerminalSandbox: true,
687
+ toolPermission: "proceed-in-sandbox",
688
+ artifactReviewPolicy: "always-proceed",
689
+ allowNonWorkspaceAccess: false
690
+ };
691
+ }
692
+ var rulePath = (p) => p.replace(/^[A-Za-z]:(?=[\\/])/, "").replace(/\\/g, "/");
693
+ var HOMES_KEPT_DAYS = 14;
694
+ function workerHome(workspace, reads = []) {
695
+ const root = path5.join(home(), "antigravity-home");
696
+ const id2 = crypto3.createHash("sha256").update(path5.resolve(workspace)).digest("hex").slice(0, 16);
697
+ const dir = path5.join(root, id2);
698
+ fs6.mkdirSync(dataDir(dir), { recursive: true });
699
+ writeIfChanged(path5.join(dataDir(dir), "settings.json"), `${JSON.stringify(workerSettings(workspace, dir, reads), null, 2)}
700
+ `);
701
+ if (process.platform === "darwin") {
702
+ fs6.mkdirSync(path5.join(dir, "Library"), { recursive: true });
703
+ linkInto(path5.join(dir, "Library"), path5.join(os3.homedir(), "Library", "Keychains"));
704
+ } else {
705
+ linkInto(dataDir(dir), path5.join(userDir(), TOKEN));
706
+ }
707
+ const now = /* @__PURE__ */ new Date();
708
+ fs6.utimesSync(dir, now, now);
709
+ pruneHomes(root, dir);
710
+ return dir;
711
+ }
712
+ function pruneHomes(root, keep) {
713
+ const cutoff = Date.now() - HOMES_KEPT_DAYS * 864e5;
714
+ try {
715
+ for (const name of fs6.readdirSync(root)) {
716
+ const dir = path5.join(root, name);
717
+ if (dir === keep || !/^[0-9a-f]{16}$/.test(name)) continue;
718
+ if (fs6.statSync(dir).mtimeMs < cutoff) fs6.rmSync(dir, { recursive: true, force: true });
719
+ }
720
+ } catch {
721
+ }
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
+ }
736
+ function invocation(req) {
737
+ const prompt = inCopy(req.prompt, req.project);
738
+ const args = [
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.
740
+ "-p",
741
+ prompt.startsWith("-") ? ` ${prompt}` : prompt,
742
+ "--output-format",
743
+ "stream-json",
744
+ "--mode",
745
+ req.mode === "read" ? "plan" : "accept-edits",
746
+ "--sandbox",
747
+ // agy's own limit (5m by default) would end a long run with partial output and exit 0; Pitroom's timeout governs.
748
+ "--print-timeout",
749
+ "24h"
750
+ ];
751
+ if (req.sessionId) args.push("--conversation", req.sessionId);
752
+ const { model, effort } = splitModel(req.model);
753
+ if (model) args.push("--model", model);
754
+ if (effort) args.push("--effort", effort);
755
+ const { command, prefix } = resolveCommand(binary());
756
+ const workerDir = workerHome(req.cwd, outsideReads(prompt, req.cwd, req.project?.root));
757
+ const env = { HOME: workerDir };
758
+ if (process.platform === "win32") env.USERPROFILE = workerDir;
759
+ return { command, args: [...prefix, ...args], env };
760
+ }
761
+ function classify(message) {
762
+ if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|capacity/i.test(message)) return "rate-limited";
763
+ if (/not (signed|logged) in|\bsign.?in\b|\blog ?in\b|authenticat|PERMISSION_DENIED|oauth|credential|keyring|keychain|\b401\b|unauthori[sz]ed/i.test(message)) {
764
+ return "auth";
765
+ }
766
+ if (/model.{0,80}(not found|not available|no longer available|does not exist|unknown|unsupported|invalid)|unknown model|invalid model|NOT_FOUND|\b404\b/i.test(message)) {
767
+ return "model-unavailable";
768
+ }
769
+ return "other";
30
770
  }
31
- function isFile(p) {
771
+ var TIMED_OUT = /print timeout after/i;
772
+ var BLOCKED = /^This request was blocked by \S+ filters\b/;
773
+ function failure(run, stderr, exitCode) {
774
+ const lines = stderr.trim().split("\n").map((l) => l.trim()).filter(Boolean);
775
+ const envelope = lines.find((l) => l.startsWith("AGY_ERROR"));
776
+ const detail = (envelope && fromEnvelope(envelope)) ?? lines.find((l) => /RESOURCE_EXHAUSTED|error|failed|quota|exhausted|denied|not found|\bsign.?in\b/i.test(l) && !TIMED_OUT.test(l));
777
+ if (exitCode === 0 && !run.error) {
778
+ if (BLOCKED.test(run.finalText)) return { kind: "other", message: `the model's filter blocked the request: ${run.finalText.split("\n")[0]}` };
779
+ if (run.finalText) return void 0;
780
+ if (detail) return { kind: classify(detail), message: detail };
781
+ if (lines.some((l) => TIMED_OUT.test(l))) return { kind: "other", message: "agy stopped at its own --print-timeout before answering" };
782
+ if (run.usage.denied) return { kind: "other", message: "agy ended its turn without an answer when a tool call was refused" };
783
+ if (!run.sessionId && !stderr.trim()) return { kind: "other", message: "agy printed nothing (some agy versions write no output to a pipe); update agy" };
784
+ return { kind: "other", message: "agy ended without an answer (a refused tool call ends its turn with no other sign)" };
785
+ }
786
+ const message = run.error ?? detail ?? lines.at(-1) ?? `agy exited with code ${exitCode}`;
787
+ return { kind: classify(`${message} ${detail ?? ""}`), message };
788
+ }
789
+ function warnings(_run, stderr) {
790
+ const lost = /conversation "([^"]+)" not found/i.exec(stderr);
791
+ return lost ? [`agy did not find the earlier conversation (${lost[1]}) and started a new one: this answer does not know the earlier turns`] : [];
792
+ }
793
+ function fromEnvelope(line) {
32
794
  try {
33
- return fs.statSync(p).isFile();
795
+ const e = JSON.parse(line.slice(line.indexOf("{")));
796
+ if (typeof e.short_error === "string" && e.short_error) return e.short_error;
797
+ const text = [e.status ?? e.code, e.message ?? e.error].filter((v) => typeof v === "string" && v).join(": ");
798
+ return text || void 0;
34
799
  } catch {
35
- return false;
800
+ return line.replace(/^AGY_ERROR:?\s*/, "") || void 0;
36
801
  }
37
802
  }
38
- function resolveCommand(bin) {
39
- try {
40
- const real = fs.realpathSync(bin);
41
- const fd = fs.openSync(real, "r");
42
- const head = Buffer.alloc(128);
43
- const n = fs.readSync(fd, head, 0, 128, 0);
44
- fs.closeSync(fd);
45
- const firstLine = head.subarray(0, n).toString("utf8").split("\n")[0] ?? "";
46
- if (/^#!.*\bnode\b/.test(firstLine)) return { command: process.execPath, prefix: [real] };
47
- } catch {
803
+ function doctor({ models, hasFallback }) {
804
+ const version = agy(["--version"]);
805
+ if (version.missing || !version.ok) {
806
+ return [{
807
+ level: "fail",
808
+ message: `agy not runnable (${binary()}). Install the Antigravity CLI (curl -fsSL https://antigravity.google/cli/install.sh | bash), or set PITROOM_ANTIGRAVITY_BIN`
809
+ }];
810
+ }
811
+ const checks = [
812
+ { level: "ok", message: `Antigravity CLI ${version.out.trim().split("\n")[0]} at ${binary()}` },
813
+ {
814
+ level: "warn",
815
+ message: "the Antigravity worker is in beta (checked against agy 1.3.5). agy has no read-only mode for headless runs, so its read runs always read a snapshot of the project; prefer -i for changes"
816
+ }
817
+ ];
818
+ if (process.platform === "win32") {
819
+ checks.push({ level: "warn", message: "agy's permission rules and terminal sandbox are for macOS and Linux; on Windows run the Antigravity worker only with -i (an isolated copy)" });
48
820
  }
49
- return { command: bin, prefix: [] };
821
+ if (process.platform === "linux" && !fs6.existsSync(path5.join(userDir(), TOKEN))) {
822
+ checks.push({ level: "warn", message: "no Antigravity sign-in file found: if a run fails to sign in, run `agy` once and sign in with your Google account" });
823
+ }
824
+ for (const m of models) checks.push({ level: "ok", message: m ? `Antigravity model: ${m}` : "Antigravity model: agy's own choice (`agy models` lists yours)" });
825
+ checks.push({ level: "ok", message: "Antigravity workers have no web access (--web does not apply to them)" });
826
+ if (!hasFallback) checks.push({ level: "warn", message: "no fallback workers configured for Antigravity runs (its quota is per account and can run out for hours)" });
827
+ return checks;
50
828
  }
829
+ var antigravity = {
830
+ id: "antigravity",
831
+ name: "Antigravity CLI",
832
+ capabilities: { readOnly: "snapshot", resume: "by-id", reportsCost: false, attachFiles: false },
833
+ binary,
834
+ invocation,
835
+ parse: parseEvents,
836
+ failure,
837
+ warnings,
838
+ doctor
839
+ };
51
840
 
52
- // src/backends/types.ts
53
- var MAX_STEPS = 400;
841
+ // src/backends/claude/index.ts
842
+ import { spawnSync as spawnSync2 } from "node:child_process";
843
+ import fs7 from "node:fs";
844
+ import os4 from "node:os";
845
+ import path6 from "node:path";
54
846
 
55
847
  // src/backends/claude/events.ts
56
- var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
57
- var DENIED = /permission|not allowed|denied|blocked|disallowed/i;
58
- function parseEvents(jsonl) {
848
+ var EDIT_TOOLS2 = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
849
+ var DENIED2 = /permission|not allowed|denied|blocked|disallowed/i;
850
+ function parseEvents2(jsonl) {
59
851
  const usage = {
60
852
  input: 0,
61
853
  output: 0,
@@ -108,9 +900,9 @@ function parseEvents(jsonl) {
108
900
  tools[c.name] = (tools[c.name] ?? 0) + 1;
109
901
  const target = c.input?.file_path ?? c.input?.path ?? c.input?.pattern ?? c.input?.command ?? c.input?.url;
110
902
  lastActivity = `${c.name} ${target ? oneLine(String(target), 60) : ""}`.trim();
111
- if (EDIT_TOOLS.has(c.name) && c.input?.file_path) pendingEdits.set(c.id, String(c.input.file_path));
903
+ if (EDIT_TOOLS2.has(c.name) && c.input?.file_path) pendingEdits.set(c.id, String(c.input.file_path));
112
904
  if (timeline.length < MAX_STEPS) {
113
- const step = { kind: EDIT_TOOLS.has(c.name) ? "edit" : c.name === "Bash" ? "shell" : "tool", name: c.name, text: clip(String(target ?? ""), 240) };
905
+ const step = { kind: EDIT_TOOLS2.has(c.name) ? "edit" : c.name === "Bash" ? "shell" : "tool", name: c.name, text: clip(String(target ?? ""), 240) };
114
906
  timeline.push(step);
115
907
  stepOf.set(String(c.id), step);
116
908
  }
@@ -123,7 +915,7 @@ function parseEvents(jsonl) {
123
915
  const step = stepOf.get(String(c.tool_use_id));
124
916
  if (step) step.ok = !c.is_error;
125
917
  if (c.is_error) {
126
- if (DENIED.test(text)) usage.denied++;
918
+ if (DENIED2.test(text)) usage.denied++;
127
919
  } else if (pendingEdits.has(c.tool_use_id)) {
128
920
  edits.push(pendingEdits.get(c.tool_use_id));
129
921
  }
@@ -149,20 +941,12 @@ function parseEvents(jsonl) {
149
941
  usage.steps = steps.size;
150
942
  return { sessionId, model, finalText: finalText ?? (error ? "" : lastText), usage, tools, edits, lastActivity, timeline, error };
151
943
  }
152
- var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
153
- function num(v) {
154
- return typeof v === "number" && Number.isFinite(v) ? v : 0;
155
- }
156
- function oneLine(s, max = 80) {
157
- const t = s.replace(/\s+/g, " ").trim();
158
- return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
159
- }
160
944
 
161
945
  // src/backends/claude/index.ts
162
- var binary = () => findBinary("claude", "PITROOM_CLAUDE_BIN", ["~/.local/bin/claude", "~/.claude/local/claude"]);
946
+ var binary2 = () => findBinary("claude", "PITROOM_CLAUDE_BIN", ["~/.local/bin/claude", "~/.claude/local/claude"]);
163
947
  function cl(args, timeout = 6e4) {
164
- const { command, prefix } = resolveCommand(binary());
165
- const r = spawnSync(command, [...prefix, ...args], {
948
+ const { command, prefix } = resolveCommand(binary2());
949
+ const r = spawnSync2(command, [...prefix, ...args], {
166
950
  encoding: "utf8",
167
951
  timeout,
168
952
  stdio: ["ignore", "pipe", "pipe"],
@@ -173,52 +957,12 @@ function cl(args, timeout = 6e4) {
173
957
  var READ_TOOLS = ["Read", "Grep", "Glob"];
174
958
  var WRITE_TOOLS = [...READ_TOOLS, "Edit", "Write", "Bash"];
175
959
  var WEB_TOOLS = ["WebFetch", "WebSearch"];
176
- var DENY_BASH = [
177
- "git commit",
178
- "git push",
179
- "git pull",
180
- "git reset",
181
- "git checkout",
182
- "git restore",
183
- "git clean",
184
- "git stash",
185
- "git rebase",
186
- "git merge",
187
- "git switch",
188
- "git branch",
189
- "git tag",
190
- "git rm",
191
- "git worktree",
192
- "git update-ref",
193
- "git config",
194
- "git remote",
195
- "git cherry-pick",
196
- "git revert",
197
- "git am",
198
- "git add",
199
- "rm -rf",
200
- "rm -fr",
201
- "sudo",
202
- "chown",
203
- "kill",
204
- "pkill",
205
- "killall",
206
- "gh",
207
- "npm publish",
208
- "pitroom",
209
- "opencode",
210
- "claude",
211
- "codex",
212
- "gemini",
213
- "qwen",
214
- "kilo",
215
- "kilocode"
216
- ].map((c) => `Bash(${c}:*)`);
960
+ var DENY_BASH = FORBIDDEN_COMMANDS.map((c) => `Bash(${c}:*)`);
217
961
  var SECRETS = ["**/.env", "**/.env.*", "**/*.pem", "**/id_rsa*", "**/id_ed25519*"].map((p) => `Read(${p})`);
218
962
  function toolsFor(mode, web2) {
219
963
  return [...mode === "read" ? READ_TOOLS : WRITE_TOOLS, ...web2 ? WEB_TOOLS : []];
220
964
  }
221
- function invocation(req) {
965
+ function invocation2(req) {
222
966
  const tools = toolsFor(req.mode, req.web).join(",");
223
967
  const args = [
224
968
  "-p",
@@ -246,10 +990,10 @@ function invocation(req) {
246
990
  if (effort) args.push("--effort", effort);
247
991
  if (req.sessionId) args.push("--resume", req.sessionId);
248
992
  args.push("--", req.prompt);
249
- const { command, prefix } = resolveCommand(binary());
993
+ const { command, prefix } = resolveCommand(binary2());
250
994
  return { command, args: [...prefix, ...args], env: { DISABLE_AUTOUPDATER: "1" } };
251
995
  }
252
- function classify(message) {
996
+ function classify2(message) {
253
997
  if (/authentication_failed|failed to authenticate|oauth|not logged in|\/login|invalid api key|\b40[13]\b|unauthori[sz]ed/i.test(message)) {
254
998
  return "auth";
255
999
  }
@@ -257,26 +1001,26 @@ function classify(message) {
257
1001
  if (/model[^.]*(not found|does not exist|invalid|not available)|invalid model|not_found_error/i.test(message)) return "model-unavailable";
258
1002
  return "other";
259
1003
  }
260
- function failure(run, stderr, exitCode) {
1004
+ function failure2(run, stderr, exitCode) {
261
1005
  if (exitCode === 0 && !run.error) return void 0;
262
1006
  const detail = stderr.trim().split("\n").filter(Boolean).pop();
263
1007
  const message = run.error ?? detail ?? `claude exited with code ${exitCode}`;
264
- return { kind: classify(message), message };
1008
+ return { kind: classify2(message), message };
265
1009
  }
266
1010
  function defaultModel() {
267
1011
  try {
268
- const settings2 = JSON.parse(fs2.readFileSync(path2.join(os2.homedir(), ".claude", "settings.json"), "utf8"));
1012
+ const settings2 = JSON.parse(fs7.readFileSync(path6.join(os4.homedir(), ".claude", "settings.json"), "utf8"));
269
1013
  return typeof settings2.model === "string" ? settings2.model : void 0;
270
1014
  } catch {
271
1015
  return void 0;
272
1016
  }
273
1017
  }
274
- function doctor({ models, hasFallback }) {
1018
+ function doctor2({ models, hasFallback }) {
275
1019
  const version = cl(["--version"]);
276
1020
  if (version.missing || !version.ok) {
277
- return [{ level: "fail", message: `claude not runnable (${binary()}). Install Claude Code, or set PITROOM_CLAUDE_BIN` }];
1021
+ return [{ level: "fail", message: `claude not runnable (${binary2()}). Install Claude Code, or set PITROOM_CLAUDE_BIN` }];
278
1022
  }
279
- const checks = [{ level: "ok", message: `Claude Code ${version.out.trim()} at ${binary()}` }];
1023
+ const checks = [{ level: "ok", message: `Claude Code ${version.out.trim()} at ${binary2()}` }];
280
1024
  let loggedIn = false;
281
1025
  try {
282
1026
  loggedIn = JSON.parse(cl(["auth", "status"]).out).loggedIn === true;
@@ -299,7 +1043,7 @@ function doctor({ models, hasFallback }) {
299
1043
  var CLAUDE_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
300
1044
  function catalog() {
301
1045
  const def = defaultModel();
302
- const models = ["haiku", "sonnet", "opus"].map((id) => ({ id, efforts: CLAUDE_EFFORTS }));
1046
+ const models = ["haiku", "sonnet", "opus"].map((id2) => ({ id: id2, efforts: CLAUDE_EFFORTS }));
303
1047
  if (def && !models.some((m) => m.id === def)) models.push({ id: def, efforts: CLAUDE_EFFORTS });
304
1048
  return { models, source: "Claude Code aliases (latest of each size) and --effort levels from `claude --help`" };
305
1049
  }
@@ -307,25 +1051,25 @@ var claude = {
307
1051
  id: "claude",
308
1052
  name: "Claude Code",
309
1053
  capabilities: { readOnly: "tool-allowlist", resume: "by-id", reportsCost: true, attachFiles: false },
310
- binary,
1054
+ binary: binary2,
311
1055
  catalog,
312
- invocation,
313
- parse: parseEvents,
314
- failure,
1056
+ invocation: invocation2,
1057
+ parse: parseEvents2,
1058
+ failure: failure2,
315
1059
  defaultModel,
316
- doctor
1060
+ doctor: doctor2
317
1061
  };
318
1062
 
319
1063
  // src/backends/codex/index.ts
320
- import { spawnSync as spawnSync2 } from "node:child_process";
321
- import fs3 from "node:fs";
322
- import os3 from "node:os";
323
- import path3 from "node:path";
1064
+ import { spawnSync as spawnSync3 } from "node:child_process";
1065
+ import fs8 from "node:fs";
1066
+ import os5 from "node:os";
1067
+ import path7 from "node:path";
324
1068
 
325
1069
  // src/backends/codex/events.ts
326
1070
  var WORK = /* @__PURE__ */ new Set(["agent_message", "reasoning", "command_execution", "file_change", "mcp_tool_call", "web_search", "todo_list"]);
327
1071
  var TOOLS = /* @__PURE__ */ new Set(["command_execution", "file_change", "mcp_tool_call", "web_search"]);
328
- function parseEvents2(jsonl) {
1072
+ function parseEvents3(jsonl) {
329
1073
  const usage = {
330
1074
  input: 0,
331
1075
  output: 0,
@@ -366,31 +1110,31 @@ function parseEvents2(jsonl) {
366
1110
  }
367
1111
  if (it.type === "agent_message" && String(it.text ?? "").trim()) {
368
1112
  finalText = String(it.text).trim();
369
- lastActivity = `says: ${oneLine2(finalText)}`;
370
- if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip2(finalText, 600) });
1113
+ lastActivity = `says: ${oneLine(finalText)}`;
1114
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(finalText, 600) });
371
1115
  } else if (it.type === "command_execution") {
372
1116
  if (it.status === "declined" || /operation not permitted|sandbox/i.test(String(it.aggregated_output ?? ""))) usage.denied++;
373
- lastActivity = `shell ${oneLine2(String(it.command ?? ""), 60)}`;
374
- if (timeline.length < MAX_STEPS) timeline.push({ kind: "shell", name: "shell", text: clip2(String(it.command ?? "").replace(/^\/bin\/\w+ -lc /, ""), 240), ok: it.exit_code === 0 });
1117
+ lastActivity = `shell ${oneLine(String(it.command ?? ""), 60)}`;
1118
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "shell", name: "shell", text: clip(String(it.command ?? "").replace(/^\/bin\/\w+ -lc /, ""), 240), ok: it.exit_code === 0 });
375
1119
  } else if (it.type === "file_change") {
376
1120
  const paths = (it.changes ?? []).map((c) => String(c.path));
377
1121
  if (it.status !== "failed" && it.status !== "declined") edits.push(...paths);
378
1122
  else usage.denied++;
379
- lastActivity = `edit ${oneLine2(paths.join(", "), 60)}`;
380
- if (timeline.length < MAX_STEPS) timeline.push({ kind: "edit", name: "edit", text: clip2(paths.join(", "), 240), ok: it.status !== "failed" && it.status !== "declined" });
1123
+ lastActivity = `edit ${oneLine(paths.join(", "), 60)}`;
1124
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "edit", name: "edit", text: clip(paths.join(", "), 240), ok: it.status !== "failed" && it.status !== "declined" });
381
1125
  }
382
1126
  break;
383
1127
  }
384
1128
  case "turn.completed": {
385
1129
  const u = e.usage ?? {};
386
- const cached3 = num2(u.cached_input_tokens);
387
- const reasoning = num2(u.reasoning_output_tokens);
388
- usage.input += Math.max(0, num2(u.input_tokens) - cached3);
1130
+ const cached3 = num(u.cached_input_tokens);
1131
+ const reasoning = num(u.reasoning_output_tokens);
1132
+ usage.input += Math.max(0, num(u.input_tokens) - cached3);
389
1133
  usage.cacheRead += cached3;
390
- usage.cacheWrite += num2(u.cache_write_input_tokens);
391
- usage.output += Math.max(0, num2(u.output_tokens) - reasoning);
1134
+ usage.cacheWrite += num(u.cache_write_input_tokens);
1135
+ usage.output += Math.max(0, num(u.output_tokens) - reasoning);
392
1136
  usage.reasoning += reasoning;
393
- usage.total += num2(u.input_tokens) + num2(u.output_tokens);
1137
+ usage.total += num(u.input_tokens) + num(u.output_tokens);
394
1138
  break;
395
1139
  }
396
1140
  case "error":
@@ -414,21 +1158,13 @@ function describe(raw) {
414
1158
  return s;
415
1159
  }
416
1160
  }
417
- function num2(v) {
418
- return typeof v === "number" && Number.isFinite(v) ? v : 0;
419
- }
420
- function oneLine2(s, max = 80) {
421
- const t = s.replace(/\s+/g, " ").trim();
422
- return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
423
- }
424
- var clip2 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
425
1161
 
426
1162
  // src/backends/codex/index.ts
427
- var binary2 = () => findBinary("codex", "PITROOM_CODEX_BIN");
428
- var codexHome = () => process.env.CODEX_HOME ?? path3.join(os3.homedir(), ".codex");
1163
+ var binary3 = () => findBinary("codex", "PITROOM_CODEX_BIN");
1164
+ var codexHome = () => process.env.CODEX_HOME ?? path7.join(os5.homedir(), ".codex");
429
1165
  function cx(args, timeout = 6e4) {
430
- const { command, prefix } = resolveCommand(binary2());
431
- const r = spawnSync2(command, [...prefix, ...args], {
1166
+ const { command, prefix } = resolveCommand(binary3());
1167
+ const r = spawnSync3(command, [...prefix, ...args], {
432
1168
  encoding: "utf8",
433
1169
  timeout,
434
1170
  stdio: ["ignore", "pipe", "pipe"],
@@ -438,12 +1174,7 @@ function cx(args, timeout = 6e4) {
438
1174
  }
439
1175
  var SANDBOX = { read: "read-only", write: "workspace-write", isolate: "workspace-write" };
440
1176
  var toml = (s) => JSON.stringify(s);
441
- function splitModel(model) {
442
- if (!model) return {};
443
- const [m, effort] = model.split("#");
444
- return { model: m || void 0, effort: effort || void 0 };
445
- }
446
- function invocation2(req) {
1177
+ function invocation3(req) {
447
1178
  const { model, effort } = splitModel(req.model);
448
1179
  const args = ["exec"];
449
1180
  if (req.sessionId) args.push("resume", req.sessionId);
@@ -460,10 +1191,10 @@ function invocation2(req) {
460
1191
  if (model) args.push("--model", model);
461
1192
  if (effort) args.push("-c", `model_reasoning_effort=${toml(effort)}`);
462
1193
  args.push(req.prompt);
463
- const { command, prefix } = resolveCommand(binary2());
1194
+ const { command, prefix } = resolveCommand(binary3());
464
1195
  return { command, args: [...prefix, ...args], env: {} };
465
1196
  }
466
- function classify2(message) {
1197
+ function classify3(message) {
467
1198
  if (/not supported|model[^.]*(not found|does not exist|unknown|unavailable)|unsupported model|no such model/i.test(message)) {
468
1199
  return "model-unavailable";
469
1200
  }
@@ -471,37 +1202,37 @@ function classify2(message) {
471
1202
  if (/unauthori[sz]ed|\b40[13]\b|not logged in|log ?in|api.?key|credits?\b|billing|subscription/i.test(message)) return "auth";
472
1203
  return "other";
473
1204
  }
474
- function failure2(run, stderr, exitCode) {
1205
+ function failure3(run, stderr, exitCode) {
475
1206
  if (exitCode === 0 && !run.error) return void 0;
476
1207
  const detail = stderr.split("\n").find((l) => /^Error[: ]|ERROR/.test(l) && !/rmcp|models cache/.test(l));
477
1208
  let message = run.error ?? (detail ? detail.trim() : `codex exited with code ${exitCode}`);
478
1209
  if (/not supported when using Codex/i.test(message)) message += " (an outdated Codex CLI says this too: npm i -g @openai/codex@latest)";
479
- return { kind: classify2(message), message };
1210
+ return { kind: classify3(message), message };
480
1211
  }
481
1212
  function resolveModel(sessionId) {
482
- const root = path3.join(codexHome(), "sessions");
483
- const day = (d) => path3.join(root, String(d.getFullYear()), String(d.getMonth() + 1).padStart(2, "0"), String(d.getDate()).padStart(2, "0"));
1213
+ const root = path7.join(codexHome(), "sessions");
1214
+ const day = (d) => path7.join(root, String(d.getFullYear()), String(d.getMonth() + 1).padStart(2, "0"), String(d.getDate()).padStart(2, "0"));
484
1215
  const now = /* @__PURE__ */ new Date();
485
1216
  for (const dir of [day(now), day(new Date(now.getTime() - 864e5))]) {
486
1217
  let files = [];
487
1218
  try {
488
- files = fs3.readdirSync(dir).filter((f) => f.includes(sessionId));
1219
+ files = fs8.readdirSync(dir).filter((f) => f.includes(sessionId));
489
1220
  } catch {
490
1221
  continue;
491
1222
  }
492
1223
  for (const f of files) {
493
- const m = /"model":"([^"]+)"/.exec(fs3.readFileSync(path3.join(dir, f), "utf8"));
1224
+ const m = /"model":"([^"]+)"/.exec(fs8.readFileSync(path7.join(dir, f), "utf8"));
494
1225
  if (m) return m[1];
495
1226
  }
496
1227
  }
497
1228
  return void 0;
498
1229
  }
499
- function doctor2({ models, hasFallback }) {
1230
+ function doctor3({ models, hasFallback }) {
500
1231
  const version = cx(["--version"]);
501
1232
  if (version.missing || !version.ok) {
502
- return [{ level: "fail", message: `codex not runnable (${binary2()}). Install: npm i -g @openai/codex, or set PITROOM_CODEX_BIN` }];
1233
+ return [{ level: "fail", message: `codex not runnable (${binary3()}). Install: npm i -g @openai/codex, or set PITROOM_CODEX_BIN` }];
503
1234
  }
504
- const checks = [{ level: "ok", message: `${version.out.trim()} at ${binary2()}` }];
1235
+ const checks = [{ level: "ok", message: `${version.out.trim()} at ${binary3()}` }];
505
1236
  const login = cx(["login", "status"]);
506
1237
  checks.push(
507
1238
  login.ok && /logged in/i.test(login.out) ? { level: "ok", message: `Codex: ${login.out.trim().split("\n")[0]}` } : { level: "fail", message: "Codex is not logged in: run `codex login`" }
@@ -517,9 +1248,9 @@ function doctor2({ models, hasFallback }) {
517
1248
  return checks;
518
1249
  }
519
1250
  function catalog2() {
520
- const file = path3.join(codexHome(), "models_cache.json");
1251
+ const file = path7.join(codexHome(), "models_cache.json");
521
1252
  try {
522
- const raw = JSON.parse(fs3.readFileSync(file, "utf8"));
1253
+ const raw = JSON.parse(fs8.readFileSync(file, "utf8"));
523
1254
  const models = (raw.models ?? []).filter((m) => m.visibility !== "hide" && typeof m.slug === "string").map((m) => ({
524
1255
  id: m.slug,
525
1256
  efforts: (m.supported_reasoning_levels ?? []).map(
@@ -537,143 +1268,25 @@ var codex = {
537
1268
  name: "Codex",
538
1269
  catalog: catalog2,
539
1270
  capabilities: { readOnly: "os-sandbox", resume: "by-id", reportsCost: false, attachFiles: false },
540
- binary: binary2,
541
- invocation: invocation2,
542
- parse: parseEvents2,
543
- failure: failure2,
544
- resolveModel,
545
- doctor: doctor2
546
- };
547
-
548
- // src/backends/gemini/index.ts
549
- import { spawnSync as spawnSync3 } from "node:child_process";
550
- import fs6 from "node:fs";
551
- import os5 from "node:os";
552
- import path5 from "node:path";
553
- import { fileURLToPath } from "node:url";
554
-
555
- // src/core/store.ts
556
- import fs5 from "node:fs";
557
- import os4 from "node:os";
558
- import path4 from "node:path";
559
- import crypto from "node:crypto";
560
-
561
- // src/core/fs-atomic.ts
562
- import fs4 from "node:fs";
563
- var BUSY = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
564
- function renameOver(tmp, file) {
565
- for (let attempt = 0; ; attempt++) {
566
- try {
567
- fs4.renameSync(tmp, file);
568
- return;
569
- } catch (e) {
570
- if (process.platform !== "win32" || !BUSY.has(e.code ?? "") || attempt >= 40) throw e;
571
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
572
- }
573
- }
574
- }
575
-
576
- // src/core/store.ts
577
- var TERMINAL = ["done", "failed", "timeout", "stopped"];
578
- var isActive = (s) => !TERMINAL.includes(s);
579
- function home() {
580
- if (process.env.PITROOM_HOME) return path4.resolve(process.env.PITROOM_HOME);
581
- if (process.platform === "win32") {
582
- return path4.join(process.env.LOCALAPPDATA ?? path4.join(os4.homedir(), "AppData", "Local"), "pitroom");
583
- }
584
- return path4.join(process.env.XDG_STATE_HOME ?? path4.join(os4.homedir(), ".local", "state"), "pitroom");
585
- }
586
- var runsDir = () => path4.join(home(), "runs");
587
- var ledgerFile = () => path4.join(home(), "ledger.jsonl");
588
- var runDir = (id) => path4.join(runsDir(), id);
589
- var runFile = (id, name) => path4.join(runDir(id), name);
590
- function newRunId(now = /* @__PURE__ */ new Date()) {
591
- const p = (n) => String(n).padStart(2, "0");
592
- const stamp2 = `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
593
- return `${stamp2}-${crypto.randomBytes(2).toString("hex")}`;
594
- }
595
- var onFinished;
596
- var archive;
597
- function writeMeta(meta) {
598
- fs5.mkdirSync(runDir(meta.id), { recursive: true });
599
- const file = runFile(meta.id, "meta.json");
600
- const tmp = `${file}.${process.pid}.tmp`;
601
- fs5.writeFileSync(tmp, JSON.stringify(meta, null, 2));
602
- renameOver(tmp, file);
603
- if (TERMINAL.includes(meta.state)) {
604
- try {
605
- onFinished?.(meta);
606
- } catch {
607
- }
608
- }
609
- }
610
- function readMeta(id) {
611
- const file = runFile(id, "meta.json");
612
- if (!fs5.existsSync(file)) {
613
- const kept = archive?.meta(id);
614
- if (kept) return upgrade(kept);
615
- }
616
- return upgrade(JSON.parse(fs5.readFileSync(file, "utf8")));
617
- }
618
- function upgrade(raw) {
619
- if (!raw.worker) raw.worker = raw.model ? { backend: "opencode", model: raw.model } : { backend: "opencode" };
620
- if (!Array.isArray(raw.fallback)) {
621
- raw.fallback = (raw.fallbackModels ?? []).map((m) => ({ backend: "opencode", model: m }));
622
- }
623
- raw.warnings ??= [];
624
- raw.files ??= [];
625
- raw.link ??= [];
626
- return raw;
627
- }
628
- function listRunIds() {
629
- if (!fs5.existsSync(runsDir())) return [];
630
- return fs5.readdirSync(runsDir()).filter((d) => fs5.existsSync(runFile(d, "meta.json"))).sort();
631
- }
632
- function resolveRun(ref) {
633
- const ids = listRunIds();
634
- if (!ref || ref === "latest" || ref === "last") {
635
- if (!ids.length) throw new UserError("no runs yet");
636
- return ids[ids.length - 1];
637
- }
638
- if (ids.includes(ref)) return ref;
639
- const hits = ids.filter((id) => id.startsWith(ref) || id.endsWith(ref));
640
- if (hits.length === 1) return hits[0];
641
- if (!hits.length) {
642
- const kept = archive?.id(ref);
643
- if (kept) return kept;
644
- }
645
- throw new UserError(hits.length ? `ambiguous run "${ref}": ${hits.join(", ")}` : ids.length ? `unknown run "${ref}"` : "no runs yet");
646
- }
647
- function isAlive(pid) {
648
- if (!pid) return false;
649
- try {
650
- process.kill(pid, 0);
651
- return true;
652
- } catch (e) {
653
- return e.code === "EPERM";
654
- }
655
- }
656
- var STOP_FILE = "stop-requested";
657
- var requestStop = (id) => fs5.writeFileSync(runFile(id, STOP_FILE), "");
658
- function freshMeta(id) {
659
- const meta = readMeta(id);
660
- if (!TERMINAL.includes(meta.state) && meta.pid && !isAlive(meta.pid)) {
661
- const latest = readMeta(id);
662
- if (TERMINAL.includes(latest.state)) return latest;
663
- const stopped = latest.stopRequested || fs5.existsSync(runFile(id, STOP_FILE));
664
- latest.state = stopped ? "stopped" : "failed";
665
- if (!stopped) latest.error ??= "worker process exited unexpectedly";
666
- latest.endedAt ??= (/* @__PURE__ */ new Date()).toISOString();
667
- writeMeta(latest);
668
- return latest;
669
- }
670
- return meta;
671
- }
1271
+ binary: binary3,
1272
+ invocation: invocation3,
1273
+ parse: parseEvents3,
1274
+ failure: failure3,
1275
+ resolveModel,
1276
+ doctor: doctor3
1277
+ };
1278
+
1279
+ // src/backends/gemini/index.ts
1280
+ import { spawnSync as spawnSync4 } from "node:child_process";
1281
+ import fs9 from "node:fs";
1282
+ import os6 from "node:os";
1283
+ import path8 from "node:path";
1284
+ import { fileURLToPath } from "node:url";
672
1285
 
673
1286
  // src/backends/gemini/events.ts
674
- var EDIT_TOOLS2 = /* @__PURE__ */ new Set(["replace", "write_file", "edit"]);
675
- var DENIED2 = /policy|permission|not allowed|denied|blocked|disallowed|refus/i;
676
- function parseEvents3(jsonl) {
1287
+ var EDIT_TOOLS3 = /* @__PURE__ */ new Set(["replace", "write_file", "edit"]);
1288
+ var DENIED3 = /policy|permission|not allowed|denied|blocked|disallowed|refus/i;
1289
+ function parseEvents4(jsonl) {
677
1290
  const usage = {
678
1291
  input: 0,
679
1292
  output: 0,
@@ -703,8 +1316,8 @@ function parseEvents3(jsonl) {
703
1316
  text = "";
704
1317
  if (!said) return;
705
1318
  lastText = said;
706
- lastActivity = `says: ${oneLine3(said)}`;
707
- if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip3(said, 600) });
1319
+ lastActivity = `says: ${oneLine(said)}`;
1320
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(said, 600) });
708
1321
  };
709
1322
  const startTurn = () => {
710
1323
  if (!inTurn) turns++;
@@ -736,11 +1349,11 @@ function parseEvents3(jsonl) {
736
1349
  usage.toolCalls++;
737
1350
  tools[name] = (tools[name] ?? 0) + 1;
738
1351
  const target = p.file_path ?? p.absolute_path ?? p.path ?? p.dir_path ?? p.pattern ?? p.command ?? p.query ?? p.url ?? p.prompt;
739
- lastActivity = `${name} ${target ? oneLine3(String(target), 60) : ""}`.trim();
1352
+ lastActivity = `${name} ${target ? oneLine(String(target), 60) : ""}`.trim();
740
1353
  const file = p.file_path ?? p.absolute_path ?? p.path;
741
- if (EDIT_TOOLS2.has(name) && file) pendingEdits.set(String(e.tool_id), String(file));
1354
+ if (EDIT_TOOLS3.has(name) && file) pendingEdits.set(String(e.tool_id), String(file));
742
1355
  if (timeline.length < MAX_STEPS) {
743
- const step = { kind: EDIT_TOOLS2.has(name) ? "edit" : name === "run_shell_command" ? "shell" : "tool", name, text: clip3(String(target ?? ""), 240) };
1356
+ const step = { kind: EDIT_TOOLS3.has(name) ? "edit" : name === "run_shell_command" ? "shell" : "tool", name, text: clip(String(target ?? ""), 240) };
744
1357
  timeline.push(step);
745
1358
  stepOf.set(String(e.tool_id), step);
746
1359
  }
@@ -749,18 +1362,18 @@ function parseEvents3(jsonl) {
749
1362
  const ok = e.status === "success";
750
1363
  const step = stepOf.get(String(e.tool_id));
751
1364
  if (step) step.ok = ok;
752
- if (!ok && DENIED2.test(`${e.error?.type ?? ""} ${e.error?.message ?? ""}`)) usage.denied++;
1365
+ if (!ok && DENIED3.test(`${e.error?.type ?? ""} ${e.error?.message ?? ""}`)) usage.denied++;
753
1366
  if (ok && pendingEdits.has(String(e.tool_id))) edits.push(pendingEdits.get(String(e.tool_id)));
754
1367
  } else if (e.type === "error") {
755
1368
  if (e.severity === "error") error ??= String(e.message ?? "Gemini CLI error");
756
1369
  } else if (e.type === "result") {
757
1370
  const s = e.stats ?? {};
758
- const cached3 = num3(s.cached);
1371
+ const cached3 = num(s.cached);
759
1372
  usage.cacheRead += cached3;
760
- usage.input += s.input !== void 0 ? num3(s.input) : Math.max(0, num3(s.input_tokens) - cached3);
761
- usage.output += num3(s.output_tokens);
762
- usage.total += num3(s.total_tokens) || num3(s.input_tokens) + num3(s.output_tokens);
763
- usage.toolCalls = Math.max(usage.toolCalls, num3(s.tool_calls));
1373
+ usage.input += s.input !== void 0 ? num(s.input) : Math.max(0, num(s.input_tokens) - cached3);
1374
+ usage.output += num(s.output_tokens);
1375
+ usage.total += num(s.total_tokens) || num(s.input_tokens) + num(s.output_tokens);
1376
+ usage.toolCalls = Math.max(usage.toolCalls, num(s.tool_calls));
764
1377
  model ??= Object.keys(s.models ?? {})[0];
765
1378
  if (e.status === "error") error ??= String(e.error?.message ?? e.error?.type ?? "Gemini CLI error");
766
1379
  }
@@ -769,20 +1382,12 @@ function parseEvents3(jsonl) {
769
1382
  usage.steps = turns;
770
1383
  return { sessionId, model, finalText: error ? "" : lastText, usage, tools, edits, lastActivity, timeline, error };
771
1384
  }
772
- var clip3 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
773
- function num3(v) {
774
- return typeof v === "number" && Number.isFinite(v) ? v : 0;
775
- }
776
- function oneLine3(s, max = 80) {
777
- const t = s.replace(/\s+/g, " ").trim();
778
- return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
779
- }
780
1385
 
781
1386
  // src/backends/gemini/index.ts
782
- var binary3 = () => findBinary("gemini", "PITROOM_GEMINI_BIN", ["~/.local/bin/gemini"]);
1387
+ var binary4 = () => findBinary("gemini", "PITROOM_GEMINI_BIN", ["~/.local/bin/gemini"]);
783
1388
  function gem(args, timeout = 6e4) {
784
- const { command, prefix } = resolveCommand(binary3());
785
- const r = spawnSync3(command, [...prefix, ...args], {
1389
+ const { command, prefix } = resolveCommand(binary4());
1390
+ const r = spawnSync4(command, [...prefix, ...args], {
786
1391
  encoding: "utf8",
787
1392
  timeout,
788
1393
  stdio: ["ignore", "pipe", "pipe"],
@@ -790,32 +1395,19 @@ function gem(args, timeout = 6e4) {
790
1395
  });
791
1396
  return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
792
1397
  }
793
- var policyDir = () => path5.resolve(path5.dirname(fileURLToPath(import.meta.url)), "..", "policies", "gemini");
794
- function workerHome() {
795
- const root = path5.join(home(), "gemini-home");
796
- const dir = path5.join(root, ".gemini");
797
- const base = JSON.parse(fs6.readFileSync(path5.join(policyDir(), "worker-settings.json"), "utf8"));
1398
+ var policyDir = () => path8.resolve(path8.dirname(fileURLToPath(import.meta.url)), "..", "policies", "gemini");
1399
+ function workerHome2() {
1400
+ const root = path8.join(home(), "gemini-home");
1401
+ const dir = path8.join(root, ".gemini");
1402
+ const base = JSON.parse(fs9.readFileSync(path8.join(policyDir(), "worker-settings.json"), "utf8"));
798
1403
  const auth = settings().security?.auth;
799
1404
  const body = `${JSON.stringify(auth ? { ...base, security: { ...base.security, auth } } : base, null, 2)}
800
1405
  `;
801
- const file = path5.join(dir, "settings.json");
802
- try {
803
- if (fs6.readFileSync(file, "utf8") === body) return root;
804
- } catch {
805
- }
806
- fs6.mkdirSync(dir, { recursive: true });
807
- fs6.writeFileSync(file, body);
808
- for (const name of ["oauth_creds.json", "google_accounts.json"]) {
809
- const from = path5.join(geminiHome(), name);
810
- const to = path5.join(dir, name);
811
- try {
812
- if (fs6.existsSync(from) && !fs6.existsSync(to)) fs6.symlinkSync(from, to);
813
- } catch {
814
- }
815
- }
1406
+ writeIfChanged(path8.join(dir, "settings.json"), body);
1407
+ for (const name of ["oauth_creds.json", "google_accounts.json"]) linkInto(dir, path8.join(geminiHome(), name));
816
1408
  return root;
817
1409
  }
818
- function invocation3(req) {
1410
+ function invocation4(req) {
819
1411
  const dir = policyDir();
820
1412
  const policies = ["base.toml", ...req.web ? [] : ["no-web.toml"], ...req.mode === "read" ? [] : ["shell.toml"]];
821
1413
  const args = [
@@ -829,18 +1421,18 @@ function invocation3(req) {
829
1421
  "-e",
830
1422
  "none"
831
1423
  ];
832
- for (const p of policies) args.push("--policy", path5.join(dir, p));
1424
+ for (const p of policies) args.push("--policy", path8.join(dir, p));
833
1425
  if (req.sessionId) args.push("--resume", req.sessionId);
834
1426
  if (process.env.PITROOM_GEMINI_TRUST === "1") args.push("--skip-trust");
835
1427
  const [model] = (req.model ?? "").split("#");
836
1428
  if (model) args.push("--model", model);
837
- const { command, prefix } = resolveCommand(binary3());
838
- const env = { GEMINI_CLI_HOME: workerHome() };
839
- const trusted = path5.join(geminiHome(), "trustedFolders.json");
840
- if (fs6.existsSync(trusted)) env.GEMINI_CLI_TRUSTED_FOLDERS_PATH = trusted;
1429
+ const { command, prefix } = resolveCommand(binary4());
1430
+ const env = { GEMINI_CLI_HOME: workerHome2() };
1431
+ const trusted = path8.join(geminiHome(), "trustedFolders.json");
1432
+ if (fs9.existsSync(trusted)) env.GEMINI_CLI_TRUSTED_FOLDERS_PATH = trusted;
841
1433
  return { command, args: [...prefix, ...args], env };
842
1434
  }
843
- function classify3(message) {
1435
+ function classify4(message) {
844
1436
  if (/IneligibleTier|no longer supported for Gemini|error authenticating|not logged in|please (log ?in|sign in)|credentials|api key|UNAUTHENTICATED|PERMISSION_DENIED|\b40[13]\b|unauthori[sz]ed/i.test(message)) {
845
1437
  return "auth";
846
1438
  }
@@ -849,20 +1441,20 @@ function classify3(message) {
849
1441
  return "other";
850
1442
  }
851
1443
  var UNTRUSTED = "Gemini CLI does not trust this folder: open `gemini` in it once and trust it, or set PITROOM_GEMINI_TRUST=1 (the folder's own Gemini settings, hooks included, then load)";
852
- function failure3(run, stderr, exitCode) {
1444
+ function failure4(run, stderr, exitCode) {
853
1445
  if (exitCode === 0 && !run.error) return void 0;
854
1446
  const lines = stderr.trim().split("\n").map((l) => l.trim()).filter(Boolean);
855
1447
  const detail = lines.find((l) => /error|failed|exceeded|quota|exhausted|unsupported/i.test(l) && !/^at /.test(l)) ?? lines.at(-1);
856
1448
  const raw = run.error ?? detail ?? `gemini exited with code ${exitCode}`;
857
1449
  const message = /not running in a trusted directory/i.test(`${stderr} ${raw}`) ? UNTRUSTED : raw;
858
- return { kind: classify3(message), message };
1450
+ return { kind: classify4(message), message };
859
1451
  }
860
1452
  function geminiHome() {
861
- return path5.join(process.env.GEMINI_CLI_HOME ?? os5.homedir(), ".gemini");
1453
+ return path8.join(process.env.GEMINI_CLI_HOME ?? os6.homedir(), ".gemini");
862
1454
  }
863
1455
  function settings() {
864
1456
  try {
865
- return JSON.parse(fs6.readFileSync(path5.join(geminiHome(), "settings.json"), "utf8"));
1457
+ return JSON.parse(fs9.readFileSync(path8.join(geminiHome(), "settings.json"), "utf8"));
866
1458
  } catch {
867
1459
  return {};
868
1460
  }
@@ -873,15 +1465,15 @@ function defaultModel2() {
873
1465
  const name = typeof m === "string" ? m : m?.name;
874
1466
  return typeof name === "string" ? name : void 0;
875
1467
  }
876
- function doctor3({ models, hasFallback }) {
1468
+ function doctor4({ models, hasFallback }) {
877
1469
  const version = gem(["--version"]);
878
1470
  if (version.missing || !version.ok) {
879
- return [{ level: "fail", message: `gemini not runnable (${binary3()}). Install Gemini CLI (npm i -g @google/gemini-cli), or set PITROOM_GEMINI_BIN` }];
1471
+ return [{ level: "fail", message: `gemini not runnable (${binary4()}). Install Gemini CLI (npm i -g @google/gemini-cli), or set PITROOM_GEMINI_BIN` }];
880
1472
  }
881
- const checks = [{ level: "ok", message: `Gemini CLI ${version.out.trim()} at ${binary3()}` }];
1473
+ const checks = [{ level: "ok", message: `Gemini CLI ${version.out.trim()} at ${binary4()}` }];
882
1474
  const key = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
883
1475
  const vertex = process.env.GOOGLE_GENAI_USE_VERTEXAI === "true" || process.env.GOOGLE_GENAI_USE_VERTEXAI === "1";
884
- const signedIn = fs6.existsSync(path5.join(geminiHome(), "oauth_creds.json"));
1476
+ const signedIn = fs9.existsSync(path8.join(geminiHome(), "oauth_creds.json"));
885
1477
  const type = settings().security?.auth?.selectedType;
886
1478
  if (key || vertex) {
887
1479
  checks.push({ level: "ok", message: `Gemini CLI: ${key ? "an API key is set" : "Vertex AI is set up"}` });
@@ -910,7 +1502,7 @@ function doctor3({ models, hasFallback }) {
910
1502
  }
911
1503
  function catalog3() {
912
1504
  const def = defaultModel2();
913
- const models = ["gemini-3.8-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview", "gemini-3-flash-preview"].map((id) => ({ id }));
1505
+ const models = ["gemini-3.8-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview", "gemini-3-flash-preview"].map((id2) => ({ id: id2 }));
914
1506
  if (def && !models.some((m) => m.id === def)) models.push({ id: def });
915
1507
  return { models, source: "Gemini CLI's built-in model names (it cannot list models); the effort suffix is not used" };
916
1508
  }
@@ -918,26 +1510,26 @@ var gemini = {
918
1510
  id: "gemini",
919
1511
  name: "Gemini CLI",
920
1512
  capabilities: { readOnly: "approval-mode", resume: "by-id", reportsCost: false, attachFiles: false },
921
- binary: binary3,
1513
+ binary: binary4,
922
1514
  catalog: catalog3,
923
- invocation: invocation3,
924
- parse: parseEvents3,
925
- failure: failure3,
1515
+ invocation: invocation4,
1516
+ parse: parseEvents4,
1517
+ failure: failure4,
926
1518
  defaultModel: defaultModel2,
927
- doctor: doctor3
1519
+ doctor: doctor4
928
1520
  };
929
1521
 
930
1522
  // src/backends/kilo/index.ts
931
- import { spawnSync as spawnSync5 } from "node:child_process";
932
- import path7 from "node:path";
1523
+ import { spawnSync as spawnSync6 } from "node:child_process";
1524
+ import path10 from "node:path";
933
1525
 
934
1526
  // src/backends/opencode/index.ts
935
- import { spawnSync as spawnSync4 } from "node:child_process";
1527
+ import { spawnSync as spawnSync5 } from "node:child_process";
936
1528
 
937
1529
  // src/backends/opencode/events.ts
938
- var EDIT_TOOLS3 = /* @__PURE__ */ new Set(["edit", "write", "patch", "multiedit", "apply_patch"]);
939
- var DENIED3 = /rule which prevents you|permission denied|permission\.rejected/i;
940
- function parseEvents4(ndjson) {
1530
+ var EDIT_TOOLS4 = /* @__PURE__ */ new Set(["edit", "write", "patch", "multiedit", "apply_patch"]);
1531
+ var DENIED4 = /rule which prevents you|permission denied|permission\.rejected/i;
1532
+ function parseEvents5(ndjson) {
941
1533
  const usage = {
942
1534
  input: 0,
943
1535
  output: 0,
@@ -974,8 +1566,8 @@ function parseEvents4(ndjson) {
974
1566
  const key = String(p.messageID ?? "");
975
1567
  if (!textByMessage.has(key)) textByMessage.set(key, []);
976
1568
  textByMessage.get(key).push(text);
977
- lastActivity = `says: ${oneLine4(text)}`;
978
- if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip4(text.trim(), 600), at: stamp(e.timestamp) });
1569
+ lastActivity = `says: ${oneLine(text)}`;
1570
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(text.trim(), 600), at: stamp(e.timestamp) });
979
1571
  break;
980
1572
  }
981
1573
  case "tool_use": {
@@ -983,28 +1575,28 @@ function parseEvents4(ndjson) {
983
1575
  const st = p.state ?? {};
984
1576
  usage.toolCalls++;
985
1577
  tools[name] = (tools[name] ?? 0) + 1;
986
- if (st.status === "error" && DENIED3.test(String(st.error ?? ""))) usage.denied++;
987
- if (st.status === "completed" && EDIT_TOOLS3.has(name)) {
1578
+ if (st.status === "error" && DENIED4.test(String(st.error ?? ""))) usage.denied++;
1579
+ if (st.status === "completed" && EDIT_TOOLS4.has(name)) {
988
1580
  edits.push(String(st.input?.filePath ?? st.input?.path ?? name));
989
1581
  }
990
1582
  lastActivity = `${name} ${describe2(st.input)}`.trim();
991
1583
  if (timeline.length < MAX_STEPS) {
992
- const kind2 = EDIT_TOOLS3.has(name) ? "edit" : /^(bash|shell)$/.test(name) ? "shell" : "tool";
1584
+ const kind2 = EDIT_TOOLS4.has(name) ? "edit" : /^(bash|shell)$/.test(name) ? "shell" : "tool";
993
1585
  const text = kind2 === "shell" ? String(st.input?.command ?? "") : String(st.input?.filePath ?? st.input?.path ?? st.input?.pattern ?? st.input?.url ?? "") || describe2(st.input);
994
- timeline.push({ kind: kind2, name, text: clip4(text, 240), ok: st.status === "completed" ? true : st.status === "error" ? false : void 0, at: stamp(e.timestamp) });
1586
+ timeline.push({ kind: kind2, name, text: clip(text, 240), ok: st.status === "completed" ? true : st.status === "error" ? false : void 0, at: stamp(e.timestamp) });
995
1587
  }
996
1588
  break;
997
1589
  }
998
1590
  case "step_finish": {
999
1591
  const t = p.tokens ?? {};
1000
1592
  usage.steps++;
1001
- usage.input += num4(t.input);
1002
- usage.output += num4(t.output);
1003
- usage.reasoning += num4(t.reasoning);
1004
- usage.cacheRead += num4(t.cache?.read);
1005
- usage.cacheWrite += num4(t.cache?.write);
1006
- usage.total += num4(t.total) || num4(t.input) + num4(t.output) + num4(t.reasoning) + num4(t.cache?.read);
1007
- usage.cost = (usage.cost ?? 0) + num4(p.cost);
1593
+ usage.input += num(t.input);
1594
+ usage.output += num(t.output);
1595
+ usage.reasoning += num(t.reasoning);
1596
+ usage.cacheRead += num(t.cache?.read);
1597
+ usage.cacheWrite += num(t.cache?.write);
1598
+ usage.total += num(t.total) || num(t.input) + num(t.output) + num(t.reasoning) + num(t.cache?.read);
1599
+ usage.cost = (usage.cost ?? 0) + num(p.cost);
1008
1600
  break;
1009
1601
  }
1010
1602
  case "error": {
@@ -1028,25 +1620,17 @@ function describeError(err) {
1028
1620
  const tags = [err.type, inner, err.status && `HTTP ${err.status}`].filter(Boolean);
1029
1621
  return tags.length ? `${message} [${tags.join(", ")}]` : message;
1030
1622
  }
1031
- var clip4 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
1032
1623
  var stamp = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
1033
- function num4(v) {
1034
- return typeof v === "number" && Number.isFinite(v) ? v : 0;
1035
- }
1036
- function oneLine4(s, max = 80) {
1037
- const t = s.replace(/\s+/g, " ").trim();
1038
- return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
1039
- }
1040
1624
  function describe2(input) {
1041
1625
  if (!input || typeof input !== "object") return "";
1042
1626
  const v = input.filePath ?? input.path ?? input.pattern ?? input.command ?? input.url ?? input.query;
1043
- return v ? oneLine4(String(v), 60) : "";
1627
+ return v ? oneLine(String(v), 60) : "";
1044
1628
  }
1045
1629
 
1046
1630
  // src/backends/opencode/profiles.ts
1047
- import fs7 from "node:fs";
1048
- import os6 from "node:os";
1049
- import path6 from "node:path";
1631
+ import fs10 from "node:fs";
1632
+ import os7 from "node:os";
1633
+ import path9 from "node:path";
1050
1634
  var AGENT = { read: "pitroom-read", write: "pitroom-write" };
1051
1635
  var READ_FILES = {
1052
1636
  "*": "allow",
@@ -1142,22 +1726,23 @@ var FORBIDDEN_BASH = [
1142
1726
  "opencode*",
1143
1727
  "kilo *",
1144
1728
  "kilocode*",
1729
+ "agy *",
1145
1730
  "pitroom *",
1146
1731
  "*pitroom*"
1147
1732
  ];
1148
1733
  function scratchDirs(app) {
1149
- const data = process.env.XDG_DATA_HOME ?? path6.join(os6.homedir(), ".local", "share");
1734
+ const data = process.env.XDG_DATA_HOME ?? path9.join(os7.homedir(), ".local", "share");
1150
1735
  const rules = {
1151
1736
  "*": "deny",
1152
- [path6.join(data, app, "tool-output", "*")]: "allow",
1153
- [path6.join(data, app, "shell", "*", "*")]: "allow"
1737
+ [path9.join(data, app, "tool-output", "*")]: "allow",
1738
+ [path9.join(data, app, "shell", "*", "*")]: "allow"
1154
1739
  };
1155
- for (const tmp of /* @__PURE__ */ new Set([os6.tmpdir(), realpath(os6.tmpdir())])) rules[path6.join(tmp, app, "*")] = "allow";
1740
+ for (const tmp of /* @__PURE__ */ new Set([os7.tmpdir(), realpath(os7.tmpdir())])) rules[path9.join(tmp, app, "*")] = "allow";
1156
1741
  return rules;
1157
1742
  }
1158
1743
  function realpath(p) {
1159
1744
  try {
1160
- return fs7.realpathSync(p);
1745
+ return fs10.realpathSync(p);
1161
1746
  } catch {
1162
1747
  return p;
1163
1748
  }
@@ -1246,10 +1831,10 @@ function isObject(v) {
1246
1831
  }
1247
1832
 
1248
1833
  // src/backends/opencode/index.ts
1249
- var binary4 = () => findBinary("opencode", "PITROOM_OPENCODE_BIN", ["~/.opencode/bin/opencode"]);
1834
+ var binary5 = () => findBinary("opencode", "PITROOM_OPENCODE_BIN", ["~/.opencode/bin/opencode"]);
1250
1835
  function oc(args, opts = {}) {
1251
- const { command, prefix } = resolveCommand(binary4());
1252
- const r = spawnSync4(command, [...prefix, ...args], {
1836
+ const { command, prefix } = resolveCommand(binary5());
1837
+ const r = spawnSync5(command, [...prefix, ...args], {
1253
1838
  encoding: "utf8",
1254
1839
  timeout: opts.timeout ?? 6e4,
1255
1840
  env: opts.env ?? process.env,
@@ -1259,7 +1844,7 @@ function oc(args, opts = {}) {
1259
1844
  return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
1260
1845
  }
1261
1846
  var jsonIn = (s) => JSON.parse(s.slice(s.indexOf("{")));
1262
- function invocation4(req) {
1847
+ function invocation5(req) {
1263
1848
  const args = [
1264
1849
  "run",
1265
1850
  "--standalone",
@@ -1278,7 +1863,7 @@ function invocation4(req) {
1278
1863
  if (req.sessionId) args.push("--session", req.sessionId);
1279
1864
  for (const f of req.files) args.push("--file", f);
1280
1865
  args.push(req.prompt);
1281
- const { command, prefix } = resolveCommand(binary4());
1866
+ const { command, prefix } = resolveCommand(binary5());
1282
1867
  return {
1283
1868
  command,
1284
1869
  args: [...prefix, ...args],
@@ -1293,7 +1878,7 @@ function majorVersion(output) {
1293
1878
  const m = /(\d+)\.\d+\.\d+/.exec(output);
1294
1879
  return m ? Number(m[1]) : void 0;
1295
1880
  }
1296
- function classify4(message) {
1881
+ function classify5(message) {
1297
1882
  if (/model not found|ModelNotFound|unknown model|no such model|deprecated/i.test(message)) return "model-unavailable";
1298
1883
  if (/rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|unavailable|capacity/i.test(message)) return "rate-limited";
1299
1884
  if (/provider\.auth|FreeTierError|free tier|unauthori[sz]ed|\b40[13]\b|api.?key|insufficient|credits?\b|billing|payment/i.test(message)) {
@@ -1301,17 +1886,17 @@ function classify4(message) {
1301
1886
  }
1302
1887
  return "other";
1303
1888
  }
1304
- function failure4(run, stderr, exitCode) {
1889
+ function failure5(run, stderr, exitCode) {
1305
1890
  if (exitCode === 0 && !run.error) return void 0;
1306
1891
  const detail = stderr.match(/error="([^"]+)"/g)?.pop()?.slice(7, -1);
1307
1892
  const raw = run.error ?? `opencode exited with code ${exitCode}`;
1308
1893
  const message = detail && !raw.includes(detail) ? `${raw}: ${detail}` : raw;
1309
- return { kind: classify4(message), message };
1894
+ return { kind: classify5(message), message };
1310
1895
  }
1311
1896
  function modelId(m) {
1312
1897
  if (typeof m === "string") return m;
1313
- const id = m?.model ?? m?.id;
1314
- return m?.providerID && id ? `${m.providerID}/${id}` : void 0;
1898
+ const id2 = m?.model ?? m?.id;
1899
+ return m?.providerID && id2 ? `${m.providerID}/${id2}` : void 0;
1315
1900
  }
1316
1901
  function defaultModel3() {
1317
1902
  try {
@@ -1341,25 +1926,25 @@ function resolveModel2(sessionId) {
1341
1926
  function catalog4() {
1342
1927
  const ids = listModels();
1343
1928
  return {
1344
- models: (ids.length ? ids : listModels()).map((id) => ({ id })),
1929
+ models: (ids.length ? ids : listModels()).map((id2) => ({ id: id2 })),
1345
1930
  source: "`opencode models` (reasoning variants are provider-specific: provider/model#variant)"
1346
1931
  };
1347
1932
  }
1348
1933
  function listModels() {
1349
1934
  return oc(["models"]).out.split("\n").map((s) => s.trim()).filter(Boolean);
1350
1935
  }
1351
- function doctor4({ models, hasFallback }) {
1936
+ function doctor5({ models, hasFallback }) {
1352
1937
  const checks = [];
1353
1938
  const version = oc(["--version"]);
1354
1939
  if (version.missing || !version.ok) {
1355
- return [{ level: "fail", message: `opencode not runnable (${binary4()}). Install: https://opencode.ai or set PITROOM_OPENCODE_BIN` }];
1940
+ return [{ level: "fail", message: `opencode not runnable (${binary5()}). Install: https://opencode.ai or set PITROOM_OPENCODE_BIN` }];
1356
1941
  }
1357
1942
  const v = version.out.trim().replace(/^opencode\s+/i, "");
1358
1943
  const major = majorVersion(v);
1359
1944
  if (major !== void 0 && major < 2) {
1360
1945
  return [{ level: "fail", message: `OpenCode ${v} is too old: Pitroom needs OpenCode v2 or newer (run \`opencode upgrade\`)` }];
1361
1946
  }
1362
- checks.push({ level: "ok", message: `opencode ${v} at ${binary4()} (private --standalone server per run)` });
1947
+ checks.push({ level: "ok", message: `opencode ${v} at ${binary5()} (private --standalone server per run)` });
1363
1948
  const known = new Set(listModels());
1364
1949
  const def = defaultModel3();
1365
1950
  for (const m of models) {
@@ -1387,25 +1972,25 @@ var opencode = {
1387
1972
  id: "opencode",
1388
1973
  name: "OpenCode",
1389
1974
  capabilities: { readOnly: "permission-rules", resume: "by-id", reportsCost: true, attachFiles: true },
1390
- binary: binary4,
1391
- invocation: invocation4,
1392
- parse: parseEvents4,
1393
- failure: failure4,
1975
+ binary: binary5,
1976
+ invocation: invocation5,
1977
+ parse: parseEvents5,
1978
+ failure: failure5,
1394
1979
  defaultModel: defaultModel3,
1395
1980
  resolveModel: resolveModel2,
1396
1981
  listModels,
1397
1982
  catalog: catalog4,
1398
- doctor: doctor4
1983
+ doctor: doctor5
1399
1984
  };
1400
1985
 
1401
1986
  // src/backends/kilo/index.ts
1402
- var binary5 = () => findBinary("kilo", "PITROOM_KILO_BIN", ["~/.kilo/bin/kilo"]);
1987
+ var binary6 = () => findBinary("kilo", "PITROOM_KILO_BIN", ["~/.kilo/bin/kilo"]);
1403
1988
  function kilo(args, timeout = 6e4) {
1404
- const { command, prefix } = resolveCommand(binary5());
1405
- const r = spawnSync5(command, [...prefix, ...args], { encoding: "utf8", timeout, stdio: ["ignore", "pipe", "pipe"], maxBuffer: 64 * 1024 * 1024 });
1989
+ const { command, prefix } = resolveCommand(binary6());
1990
+ const r = spawnSync6(command, [...prefix, ...args], { encoding: "utf8", timeout, stdio: ["ignore", "pipe", "pipe"], maxBuffer: 64 * 1024 * 1024 });
1406
1991
  return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
1407
1992
  }
1408
- function invocation5(req) {
1993
+ function invocation6(req) {
1409
1994
  const args = [
1410
1995
  "run",
1411
1996
  "--agent",
@@ -1423,7 +2008,7 @@ function invocation5(req) {
1423
2008
  if (req.sessionId) args.push("--session", req.sessionId);
1424
2009
  for (const f of req.files) args.push("--file", f);
1425
2010
  args.push(req.prompt);
1426
- const { command, prefix } = resolveCommand(binary5());
2011
+ const { command, prefix } = resolveCommand(binary6());
1427
2012
  return {
1428
2013
  command,
1429
2014
  args: [...prefix, ...args],
@@ -1431,16 +2016,16 @@ function invocation5(req) {
1431
2016
  // A worker run must not upgrade Kilo underneath the user.
1432
2017
  KILO_CONFIG_CONTENT: configContent(process.env.KILO_CONFIG_CONTENT, req.web, "kilo", { autoupdate: false }),
1433
2018
  // No daemon to attach to: a private server per run (see above).
1434
- XDG_STATE_HOME: path7.join(home(), "kilo-state")
2019
+ XDG_STATE_HOME: path10.join(home(), "kilo-state")
1435
2020
  }
1436
2021
  };
1437
2022
  }
1438
- function failure5(run, stderr, exitCode) {
2023
+ function failure6(run, stderr, exitCode) {
1439
2024
  if (exitCode === 0 && !run.error) return void 0;
1440
2025
  const detail = stderr.match(/error="([^"]+)"/g)?.pop()?.slice(7, -1);
1441
2026
  const raw = run.error ?? `kilo exited with code ${exitCode}`;
1442
2027
  const message = detail && !raw.includes(detail) ? `${raw}: ${detail}` : raw;
1443
- return { kind: classify4(message), message };
2028
+ return { kind: classify5(message), message };
1444
2029
  }
1445
2030
  function listModels2() {
1446
2031
  return kilo(["models"]).out.split("\n").map((s) => s.trim()).filter((s) => /^[\w.-]+\/\S+$/.test(s));
@@ -1455,14 +2040,14 @@ function defaultModel4() {
1455
2040
  }
1456
2041
  }
1457
2042
  function catalog5() {
1458
- return { models: listModels2().map((id) => ({ id })), source: "`kilo models` (reasoning variants are provider-specific: provider/model#variant)" };
2043
+ return { models: listModels2().map((id2) => ({ id: id2 })), source: "`kilo models` (reasoning variants are provider-specific: provider/model#variant)" };
1459
2044
  }
1460
- function doctor5({ models, hasFallback }) {
2045
+ function doctor6({ models, hasFallback }) {
1461
2046
  const version = kilo(["--version"]);
1462
2047
  if (version.missing || !version.ok) {
1463
- return [{ level: "fail", message: `kilo not runnable (${binary5()}). Install Kilo Code's CLI (npm i -g @kilocode/cli), or set PITROOM_KILO_BIN` }];
2048
+ return [{ level: "fail", message: `kilo not runnable (${binary6()}). Install Kilo Code's CLI (npm i -g @kilocode/cli), or set PITROOM_KILO_BIN` }];
1464
2049
  }
1465
- const checks = [{ level: "ok", message: `Kilo Code ${version.out.trim().split("\n").pop()} at ${binary5()} (beta worker: checked against 7.8)` }];
2050
+ const checks = [{ level: "ok", message: `Kilo Code ${version.out.trim().split("\n").pop()} at ${binary6()} (beta worker: checked against 7.8)` }];
1466
2051
  const def = defaultModel4();
1467
2052
  for (const m of models) {
1468
2053
  const model = m ?? def;
@@ -1476,29 +2061,29 @@ var kiloBackend = {
1476
2061
  id: "kilo",
1477
2062
  name: "Kilo Code",
1478
2063
  capabilities: { readOnly: "permission-rules", resume: "by-id", reportsCost: true, attachFiles: true },
1479
- binary: binary5,
1480
- invocation: invocation5,
1481
- parse: parseEvents4,
1482
- failure: failure5,
2064
+ binary: binary6,
2065
+ invocation: invocation6,
2066
+ parse: parseEvents5,
2067
+ failure: failure6,
1483
2068
  defaultModel: defaultModel4,
1484
2069
  listModels: listModels2,
1485
2070
  catalog: catalog5,
1486
- doctor: doctor5
2071
+ doctor: doctor6
1487
2072
  };
1488
2073
 
1489
2074
  // src/backends/qwen/index.ts
1490
- import { spawnSync as spawnSync6 } from "node:child_process";
1491
- import fs8 from "node:fs";
1492
- import os7 from "node:os";
1493
- import path8 from "node:path";
2075
+ import { spawnSync as spawnSync7 } from "node:child_process";
2076
+ import fs11 from "node:fs";
2077
+ import os8 from "node:os";
2078
+ import path11 from "node:path";
1494
2079
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1495
2080
 
1496
2081
  // src/backends/qwen/events.ts
1497
- var EDIT_TOOLS4 = /* @__PURE__ */ new Set(["write_file", "edit", "notebook_edit"]);
1498
- var SHELL = "run_shell_command";
1499
- var DENIED4 = /permission was declined|denied by permission rules|requires permission|not allowed/i;
2082
+ var EDIT_TOOLS5 = /* @__PURE__ */ new Set(["write_file", "edit", "notebook_edit"]);
2083
+ var SHELL2 = "run_shell_command";
2084
+ var DENIED5 = /permission was declined|denied by permission rules|requires permission|not allowed/i;
1500
2085
  var API_ERROR = /^\[API Error: /;
1501
- function parseEvents5(jsonl) {
2086
+ function parseEvents6(jsonl) {
1502
2087
  const usage = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0, steps: 0, toolCalls: 0, denied: 0 };
1503
2088
  const tools = {};
1504
2089
  const edits = [];
@@ -1535,16 +2120,16 @@ function parseEvents5(jsonl) {
1535
2120
  for (const c of content) {
1536
2121
  if (c.type === "text" && String(c.text ?? "").trim()) {
1537
2122
  lastText = String(c.text).trim();
1538
- lastActivity = `says: ${oneLine5(lastText)}`;
1539
- if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip5(lastText, 600) });
2123
+ lastActivity = `says: ${oneLine(lastText)}`;
2124
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(lastText, 600) });
1540
2125
  } else if (c.type === "tool_use") {
1541
2126
  usage.toolCalls++;
1542
2127
  tools[c.name] = (tools[c.name] ?? 0) + 1;
1543
2128
  const target = c.input?.file_path ?? c.input?.path ?? c.input?.pattern ?? c.input?.command ?? c.input?.url;
1544
- lastActivity = `${c.name} ${target ? oneLine5(String(target), 60) : ""}`.trim();
1545
- if (EDIT_TOOLS4.has(c.name) && c.input?.file_path) pendingEdits.set(String(c.id), String(c.input.file_path));
2129
+ lastActivity = `${c.name} ${target ? oneLine(String(target), 60) : ""}`.trim();
2130
+ if (EDIT_TOOLS5.has(c.name) && c.input?.file_path) pendingEdits.set(String(c.id), String(c.input.file_path));
1546
2131
  if (timeline.length < MAX_STEPS) {
1547
- const step = { kind: EDIT_TOOLS4.has(c.name) ? "edit" : c.name === SHELL ? "shell" : "tool", name: c.name, text: clip5(String(target ?? ""), 240) };
2132
+ const step = { kind: EDIT_TOOLS5.has(c.name) ? "edit" : c.name === SHELL2 ? "shell" : "tool", name: c.name, text: clip(String(target ?? ""), 240) };
1548
2133
  timeline.push(step);
1549
2134
  stepOf.set(String(c.id), step);
1550
2135
  }
@@ -1557,17 +2142,17 @@ function parseEvents5(jsonl) {
1557
2142
  const step = stepOf.get(String(c.tool_use_id));
1558
2143
  if (step) step.ok = !c.is_error;
1559
2144
  if (c.is_error) {
1560
- if (DENIED4.test(text)) usage.denied++;
2145
+ if (DENIED5.test(text)) usage.denied++;
1561
2146
  } else if (pendingEdits.has(String(c.tool_use_id))) {
1562
2147
  edits.push(pendingEdits.get(String(c.tool_use_id)));
1563
2148
  }
1564
2149
  }
1565
2150
  } else if (e.type === "result") {
1566
2151
  const u = e.usage ?? {};
1567
- usage.input += Math.max(0, num5(u.input_tokens) - num5(u.cache_read_input_tokens));
1568
- usage.cacheRead += num5(u.cache_read_input_tokens);
1569
- usage.output += num5(u.output_tokens);
1570
- usage.total += num5(u.total_tokens) || num5(u.input_tokens) + num5(u.output_tokens);
2152
+ usage.input += Math.max(0, num(u.input_tokens) - num(u.cache_read_input_tokens));
2153
+ usage.cacheRead += num(u.cache_read_input_tokens);
2154
+ usage.output += num(u.output_tokens);
2155
+ usage.total += num(u.total_tokens) || num(u.input_tokens) + num(u.output_tokens);
1571
2156
  usage.denied = Math.max(usage.denied, Array.isArray(e.permission_denials) ? e.permission_denials.length : 0);
1572
2157
  if (e.is_error) {
1573
2158
  error ??= apiError(String(e.error?.message ?? e.result ?? e.subtype ?? "Qwen Code error"));
@@ -1579,16 +2164,10 @@ function parseEvents5(jsonl) {
1579
2164
  return { sessionId, model, finalText: finalText ?? (error ? "" : lastText), usage, tools, edits, lastActivity, timeline, error };
1580
2165
  }
1581
2166
  var apiError = (text) => text.replace(API_ERROR, "").replace(/\](?=\s*(\n|$))/, "").replace(/\s+/g, " ").trim() || text;
1582
- var clip5 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
1583
- var num5 = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
1584
- var oneLine5 = (s, max = 80) => {
1585
- const t = s.replace(/\s+/g, " ").trim();
1586
- return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
1587
- };
1588
2167
 
1589
2168
  // src/backends/qwen/index.ts
1590
- var binary6 = () => findBinary("qwen", "PITROOM_QWEN_BIN", ["~/.local/bin/qwen", "~/.npm-global/bin/qwen"]);
1591
- var policyDir2 = () => path8.resolve(path8.dirname(fileURLToPath2(import.meta.url)), "..", "policies", "qwen");
2169
+ var binary7 = () => findBinary("qwen", "PITROOM_QWEN_BIN", ["~/.local/bin/qwen", "~/.npm-global/bin/qwen"]);
2170
+ var policyDir2 = () => path11.resolve(path11.dirname(fileURLToPath2(import.meta.url)), "..", "policies", "qwen");
1592
2171
  var EXCLUDED = [
1593
2172
  "agent",
1594
2173
  "send_message",
@@ -1613,48 +2192,8 @@ var EXCLUDED = [
1613
2192
  ];
1614
2193
  var WEB = ["web_fetch", "web_search"];
1615
2194
  var EDITS_HERE = ["Edit(./**)", "write_file(./**)", "notebook_edit(./**)"];
1616
- var DENY_SHELL = [
1617
- "git commit",
1618
- "git push",
1619
- "git pull",
1620
- "git reset",
1621
- "git checkout",
1622
- "git restore",
1623
- "git clean",
1624
- "git stash",
1625
- "git rebase",
1626
- "git merge",
1627
- "git switch",
1628
- "git branch",
1629
- "git tag",
1630
- "git rm",
1631
- "git worktree",
1632
- "git update-ref",
1633
- "git config",
1634
- "git remote",
1635
- "git cherry-pick",
1636
- "git revert",
1637
- "git am",
1638
- "git add",
1639
- "rm -rf",
1640
- "rm -fr",
1641
- "sudo",
1642
- "chown",
1643
- "kill",
1644
- "pkill",
1645
- "killall",
1646
- "gh",
1647
- "npm publish",
1648
- "pitroom",
1649
- "opencode",
1650
- "claude",
1651
- "codex",
1652
- "gemini",
1653
- "qwen",
1654
- "kilo",
1655
- "kilocode"
1656
- ].map((c) => `run_shell_command(${c})`);
1657
- function invocation6(req) {
2195
+ var DENY_SHELL = FORBIDDEN_COMMANDS.map((c) => `run_shell_command(${c})`);
2196
+ function invocation7(req) {
1658
2197
  const read = req.mode === "read";
1659
2198
  const exclude = [...EXCLUDED, ...req.web ? [] : WEB, ...read ? [] : DENY_SHELL];
1660
2199
  const args = [
@@ -1673,32 +2212,32 @@ function invocation6(req) {
1673
2212
  const model = (req.model ?? "").split("#")[0];
1674
2213
  if (model) args.push("--model", model);
1675
2214
  if (req.sessionId) args.push("--resume", req.sessionId);
1676
- const { command, prefix } = resolveCommand(binary6());
2215
+ const { command, prefix } = resolveCommand(binary7());
1677
2216
  return {
1678
2217
  command,
1679
2218
  args: [...prefix, ...args],
1680
2219
  env: {
1681
- QWEN_CODE_SYSTEM_DEFAULTS_PATH: path8.join(policyDir2(), "worker-defaults.json"),
2220
+ QWEN_CODE_SYSTEM_DEFAULTS_PATH: path11.join(policyDir2(), "worker-defaults.json"),
1682
2221
  QWEN_CODE_DISABLE_CRON: "1",
1683
2222
  QWEN_DISABLE_AUTO_TITLE: "1"
1684
2223
  }
1685
2224
  };
1686
2225
  }
1687
- function classify5(message) {
2226
+ function classify6(message) {
1688
2227
  if (/\b40[13]\b|incorrect api key|invalid api key|unauthori[sz]ed|authenticat|not logged in|no auth/i.test(message)) return "auth";
1689
2228
  if (/\b429\b|rate.?limit|too many requests|quota|capacity|overloaded|\b529\b/i.test(message)) return "rate-limited";
1690
2229
  if (/model[^.]*(not found|does not exist|not available|not supported)|invalid model|model_not_found|\b404\b/i.test(message)) return "model-unavailable";
1691
2230
  return "other";
1692
2231
  }
1693
- function failure6(run, stderr, exitCode) {
2232
+ function failure7(run, stderr, exitCode) {
1694
2233
  if (exitCode === 0 && !run.error) return void 0;
1695
2234
  const detail = stderr.trim().split("\n").filter((l) => l.trim() && !/SAFE MODE/.test(l)).pop();
1696
2235
  const message = run.error ?? detail ?? `qwen exited with code ${exitCode}`;
1697
- return { kind: classify5(message), message };
2236
+ return { kind: classify6(message), message };
1698
2237
  }
1699
2238
  function userSettings() {
1700
2239
  try {
1701
- return JSON.parse(fs8.readFileSync(path8.join(process.env.QWEN_HOME ?? path8.join(os7.homedir(), ".qwen"), "settings.json"), "utf8"));
2240
+ return JSON.parse(fs11.readFileSync(path11.join(process.env.QWEN_HOME ?? path11.join(os8.homedir(), ".qwen"), "settings.json"), "utf8"));
1702
2241
  } catch {
1703
2242
  return {};
1704
2243
  }
@@ -1707,11 +2246,11 @@ function defaultModel5() {
1707
2246
  const name = userSettings().model?.name;
1708
2247
  return typeof name === "string" && name ? name : void 0;
1709
2248
  }
1710
- function doctor6({ models, hasFallback }) {
1711
- const { command, prefix } = resolveCommand(binary6());
1712
- const r = spawnSync6(command, [...prefix, "--version"], { encoding: "utf8", timeout: 6e4, stdio: ["ignore", "pipe", "pipe"] });
1713
- if (r.error || r.status !== 0) return [{ level: "fail", message: `qwen not runnable (${binary6()}). Install Qwen Code (npm i -g @qwen-code/qwen-code), or set PITROOM_QWEN_BIN` }];
1714
- const checks = [{ level: "ok", message: `Qwen Code ${r.stdout.trim()} at ${binary6()} (beta worker: checked against Qwen Code 0.25)` }];
2249
+ function doctor7({ models, hasFallback }) {
2250
+ const { command, prefix } = resolveCommand(binary7());
2251
+ const r = spawnSync7(command, [...prefix, "--version"], { encoding: "utf8", timeout: 6e4, stdio: ["ignore", "pipe", "pipe"] });
2252
+ if (r.error || r.status !== 0) return [{ level: "fail", message: `qwen not runnable (${binary7()}). Install Qwen Code (npm i -g @qwen-code/qwen-code), or set PITROOM_QWEN_BIN` }];
2253
+ const checks = [{ level: "ok", message: `Qwen Code ${r.stdout.trim()} at ${binary7()} (beta worker: checked against Qwen Code 0.25)` }];
1715
2254
  const auth = userSettings().security?.auth?.selectedType;
1716
2255
  const key = process.env.OPENAI_API_KEY || process.env.DASHSCOPE_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.GEMINI_API_KEY;
1717
2256
  checks.push(
@@ -1728,27 +2267,27 @@ var qwen = {
1728
2267
  id: "qwen",
1729
2268
  name: "Qwen Code",
1730
2269
  capabilities: { readOnly: "approval-mode", resume: "by-id", reportsCost: false, attachFiles: false },
1731
- binary: binary6,
1732
- invocation: invocation6,
1733
- parse: parseEvents5,
1734
- failure: failure6,
2270
+ binary: binary7,
2271
+ invocation: invocation7,
2272
+ parse: parseEvents6,
2273
+ failure: failure7,
1735
2274
  defaultModel: defaultModel5,
1736
- doctor: doctor6
2275
+ doctor: doctor7
1737
2276
  };
1738
2277
 
1739
2278
  // src/backends/index.ts
1740
- var REGISTRY = new Map([opencode, codex, claude, gemini, qwen, kiloBackend].map((b) => [b.id, b]));
2279
+ var REGISTRY = new Map([opencode, codex, claude, gemini, qwen, kiloBackend, antigravity].map((b) => [b.id, b]));
1741
2280
  var DEFAULT_BACKEND = opencode.id;
1742
2281
  var PLANNED = ["hermes"];
1743
2282
  var backendIds = () => [...REGISTRY.keys()];
1744
2283
  var allBackends = () => [...REGISTRY.values()];
1745
- var isBackendId = (id) => REGISTRY.has(id) || PLANNED.includes(id);
1746
- function getBackend(id) {
1747
- const backend = REGISTRY.get(id);
2284
+ var isBackendId = (id2) => REGISTRY.has(id2) || PLANNED.includes(id2);
2285
+ function getBackend(id2) {
2286
+ const backend = REGISTRY.get(id2);
1748
2287
  if (backend) return backend;
1749
2288
  const available = backendIds().join(", ");
1750
2289
  throw new UserError(
1751
- PLANNED.includes(id) ? `the "${id}" worker is not supported yet (available: ${available})` : `unknown worker "${id}" (available: ${available})`,
2290
+ PLANNED.includes(id2) ? `the "${id2}" worker is not supported yet (available: ${available})` : `unknown worker "${id2}" (available: ${available})`,
1752
2291
  3
1753
2292
  );
1754
2293
  }
@@ -1769,12 +2308,12 @@ var formatTarget = (t) => t.model ? `${t.backend}:${t.model}` : t.backend;
1769
2308
  var describeTarget = (t) => t.model ? formatTarget(t) : `${t.backend} (default model)`;
1770
2309
 
1771
2310
  // src/core/refs.ts
1772
- import fs9 from "node:fs";
1773
- import path9 from "node:path";
2311
+ import fs12 from "node:fs";
2312
+ import path12 from "node:path";
1774
2313
  var FILE_EXTENSIONS = new Set(
1775
2314
  "ts tsx mts cts js jsx mjs cjs json jsonc json5 md mdx txt rst py pyi rb go rs java kt kts swift c h cc cpp cxx hpp hh cs fs php lua r jl sh bash zsh fish ps1 bat yml yaml toml ini cfg conf env html htm css scss sass less vue svelte astro sql graphql gql proto lock xml svg csv tsv gradle tf hcl dart ex exs erl hs ml scala clj vim el mk cmake dockerfile gitignore gitattributes editorconfig npmrc nvmrc".split(" ")
1776
2315
  );
1777
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
2316
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
1778
2317
  var MAX_INDEXED = 6e4;
1779
2318
  var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
1780
2319
  var REF = new RegExp(
@@ -1821,13 +2360,13 @@ function extractRefs(answer) {
1821
2360
  return [...seen.values()];
1822
2361
  }
1823
2362
  function verifyRefs(refs, dirs) {
1824
- const roots = [...new Set(dirs.filter(Boolean).map((d) => path9.resolve(d)))];
2363
+ const roots = [...new Set(dirs.filter(Boolean).map((d) => path12.resolve(d)))];
1825
2364
  const invalid = [];
1826
2365
  const lineCache = /* @__PURE__ */ new Map();
1827
2366
  const load = (file) => {
1828
2367
  if (!lineCache.has(file)) {
1829
- const size = fs9.statSync(file).size;
1830
- const lines = size > MAX_BYTES ? null : fs9.readFileSync(file, "utf8").split("\n");
2368
+ const size = fs12.statSync(file).size;
2369
+ const lines = size > MAX_BYTES ? null : fs12.readFileSync(file, "utf8").split("\n");
1831
2370
  if (lines && lines[lines.length - 1] === "") lines.pop();
1832
2371
  lineCache.set(file, lines);
1833
2372
  }
@@ -1838,9 +2377,9 @@ function verifyRefs(refs, dirs) {
1838
2377
  const candidates = (ref) => {
1839
2378
  const direct = resolve(ref.file, roots);
1840
2379
  if (direct) return [direct];
1841
- if (path9.isAbsolute(ref.file)) return [];
2380
+ if (path12.isAbsolute(ref.file)) return [];
1842
2381
  const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
1843
- return (byName().get(path9.basename(wanted)) ?? []).filter((f) => f.split(path9.sep).join("/").endsWith(`/${wanted}`) || path9.basename(f) === wanted).slice(0, 20);
2382
+ return (byName().get(path12.basename(wanted)) ?? []).filter((f) => f.split(path12.sep).join("/").endsWith(`/${wanted}`) || path12.basename(f) === wanted).slice(0, 20);
1844
2383
  };
1845
2384
  const check = (file, ref) => {
1846
2385
  const lines = load(file);
@@ -1855,7 +2394,7 @@ function verifyRefs(refs, dirs) {
1855
2394
  if (!files.length) {
1856
2395
  if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
1857
2396
  total++;
1858
- invalid.push({ ref: ref.text, reason: path9.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
2397
+ invalid.push({ ref: ref.text, reason: path12.isAbsolute(ref.file) && !inside2(ref.file, roots) ? "outside the project" : "file not found" });
1859
2398
  continue;
1860
2399
  }
1861
2400
  total++;
@@ -1876,7 +2415,7 @@ function enclosedBy(lines, ref) {
1876
2415
  return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
1877
2416
  }
1878
2417
  var isFileName = (name) => {
1879
- const base = path9.basename(name);
2418
+ const base = path12.basename(name);
1880
2419
  return !base.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base) || FILE_EXTENSIONS.has(base.split(".").pop().toLowerCase());
1881
2420
  };
1882
2421
  function indexFiles(roots) {
@@ -1885,19 +2424,19 @@ function indexFiles(roots) {
1885
2424
  const walk = (dir) => {
1886
2425
  let entries;
1887
2426
  try {
1888
- entries = fs9.readdirSync(dir, { withFileTypes: true });
2427
+ entries = fs12.readdirSync(dir, { withFileTypes: true });
1889
2428
  } catch {
1890
2429
  return;
1891
2430
  }
1892
2431
  for (const e of entries) {
1893
2432
  if (count >= MAX_INDEXED) return;
1894
2433
  if (e.isDirectory()) {
1895
- if (!SKIP_DIRS.has(e.name)) walk(path9.join(dir, e.name));
2434
+ if (!SKIP_DIRS2.has(e.name)) walk(path12.join(dir, e.name));
1896
2435
  } else if (e.isFile()) {
1897
2436
  count++;
1898
2437
  const list2 = index.get(e.name);
1899
- if (list2) list2.push(path9.join(dir, e.name));
1900
- else index.set(e.name, [path9.join(dir, e.name)]);
2438
+ if (list2) list2.push(path12.join(dir, e.name));
2439
+ else index.set(e.name, [path12.join(dir, e.name)]);
1901
2440
  }
1902
2441
  }
1903
2442
  };
@@ -1905,32 +2444,32 @@ function indexFiles(roots) {
1905
2444
  return index;
1906
2445
  }
1907
2446
  function resolve(ref, roots) {
1908
- const candidates = path9.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path9.join(r, ref));
2447
+ const candidates = path12.isAbsolute(ref) ? inside2(ref, roots) ? [ref] : [] : roots.map((r) => path12.join(r, ref));
1909
2448
  return candidates.find((c) => {
1910
2449
  try {
1911
- return fs9.statSync(c).isFile() && inside(c, roots);
2450
+ return fs12.statSync(c).isFile() && inside2(c, roots);
1912
2451
  } catch {
1913
2452
  return false;
1914
2453
  }
1915
2454
  });
1916
2455
  }
1917
- function inside(file, roots) {
2456
+ function inside2(file, roots) {
1918
2457
  return roots.some((r) => {
1919
- const rel = path9.relative(r, path9.resolve(file));
1920
- return rel !== "" && !rel.startsWith("..") && !path9.isAbsolute(rel);
2458
+ const rel = path12.relative(r, path12.resolve(file));
2459
+ return rel !== "" && !rel.startsWith("..") && !path12.isAbsolute(rel);
1921
2460
  });
1922
2461
  }
1923
2462
  var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1924
2463
 
1925
2464
  // src/core/templates.ts
1926
- import fs10 from "node:fs";
1927
- import path11 from "node:path";
2465
+ import fs13 from "node:fs";
2466
+ import path14 from "node:path";
1928
2467
 
1929
2468
  // src/core/install.ts
1930
- import path10 from "node:path";
2469
+ import path13 from "node:path";
1931
2470
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1932
2471
  function packageRoot() {
1933
- return path10.resolve(path10.dirname(fileURLToPath3(import.meta.url)), "..");
2472
+ return path13.resolve(path13.dirname(fileURLToPath3(import.meta.url)), "..");
1934
2473
  }
1935
2474
 
1936
2475
  // src/core/templates.ts
@@ -1941,9 +2480,9 @@ var FILES = {
1941
2480
  "code-reviewer": "pitroom-review/code-reviewer.md"
1942
2481
  };
1943
2482
  function loadTemplate(name, root = packageRoot()) {
1944
- const file = path11.join(root, "skills", FILES[name]);
1945
- if (!fs10.existsSync(file)) throw new UserError(`template missing: ${file} (broken install? run pitroom doctor)`, 3);
1946
- return fs10.readFileSync(file, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
2483
+ const file = path14.join(root, "skills", FILES[name]);
2484
+ if (!fs13.existsSync(file)) throw new UserError(`template missing: ${file} (broken install? run pitroom doctor)`, 3);
2485
+ return fs13.readFileSync(file, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
1947
2486
  }
1948
2487
  function fill(template, values) {
1949
2488
  const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
@@ -2000,9 +2539,9 @@ function parseAudit(text) {
2000
2539
  }
2001
2540
 
2002
2541
  // src/core/config.ts
2003
- import fs11 from "node:fs";
2004
- import os8 from "node:os";
2005
- import path12 from "node:path";
2542
+ import fs14 from "node:fs";
2543
+ import os9 from "node:os";
2544
+ import path15 from "node:path";
2006
2545
  var SCHEMA = {
2007
2546
  worker: "string",
2008
2547
  fallback: "string[]",
@@ -2033,37 +2572,37 @@ var SCHEMA = {
2033
2572
  rankWorkers: "boolean"
2034
2573
  };
2035
2574
  function configPath() {
2036
- if (process.env.PITROOM_CONFIG) return path12.resolve(process.env.PITROOM_CONFIG);
2037
- const base = process.platform === "win32" ? process.env.APPDATA ?? path12.join(os8.homedir(), "AppData", "Roaming") : process.env.XDG_CONFIG_HOME ?? path12.join(os8.homedir(), ".config");
2038
- return path12.join(base, "pitroom", "config.json");
2575
+ if (process.env.PITROOM_CONFIG) return path15.resolve(process.env.PITROOM_CONFIG);
2576
+ const base = process.platform === "win32" ? process.env.APPDATA ?? path15.join(os9.homedir(), "AppData", "Roaming") : process.env.XDG_CONFIG_HOME ?? path15.join(os9.homedir(), ".config");
2577
+ return path15.join(base, "pitroom", "config.json");
2039
2578
  }
2040
2579
  var cached;
2041
2580
  function loadConfig() {
2042
2581
  if (cached) return cached;
2043
2582
  const file = configPath();
2044
2583
  const config = {};
2045
- const warnings = [];
2046
- if (fs11.existsSync(file)) {
2584
+ const warnings2 = [];
2585
+ if (fs14.existsSync(file)) {
2047
2586
  let raw;
2048
2587
  try {
2049
- raw = JSON.parse(fs11.readFileSync(file, "utf8"));
2588
+ raw = JSON.parse(fs14.readFileSync(file, "utf8"));
2050
2589
  } catch (e) {
2051
- warnings.push(`${file}: invalid JSON (${e.message}); ignored`);
2590
+ warnings2.push(`${file}: invalid JSON (${e.message}); ignored`);
2052
2591
  }
2053
2592
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
2054
2593
  for (const [key, value] of Object.entries(raw)) {
2055
2594
  const type = SCHEMA[key];
2056
2595
  if (!type) {
2057
- warnings.push(`${file}: unknown key "${key}" ignored`);
2596
+ warnings2.push(`${file}: unknown key "${key}" ignored`);
2058
2597
  } else if (matches(value, type)) {
2059
2598
  config[key] = value;
2060
2599
  } else {
2061
- warnings.push(`${file}: "${key}" must be ${type}; ignored`);
2600
+ warnings2.push(`${file}: "${key}" must be ${type}; ignored`);
2062
2601
  }
2063
2602
  }
2064
2603
  }
2065
2604
  }
2066
- cached = { config, warnings };
2605
+ cached = { config, warnings: warnings2 };
2067
2606
  return cached;
2068
2607
  }
2069
2608
  function matches(v, type) {
@@ -2199,22 +2738,22 @@ function rankChain(chain, recordOf) {
2199
2738
  }
2200
2739
 
2201
2740
  // src/core/receipt.ts
2202
- import fs13 from "node:fs";
2741
+ import fs16 from "node:fs";
2203
2742
 
2204
2743
  // src/core/prices.ts
2205
- import fs12 from "node:fs";
2206
- import path13 from "node:path";
2744
+ import fs15 from "node:fs";
2745
+ import path16 from "node:path";
2207
2746
  var RETRY_AFTER_FAILURE_MS = 60 * 6e4;
2208
- var catalogFile = () => path13.join(home(), "prices.json");
2747
+ var catalogFile = () => path16.join(home(), "prices.json");
2209
2748
  function trim(raw) {
2210
2749
  const out = {};
2211
2750
  if (!raw || typeof raw !== "object") return out;
2212
2751
  for (const [provider, p] of Object.entries(raw)) {
2213
- for (const [id, m] of Object.entries(p?.models ?? {})) {
2752
+ for (const [id2, m] of Object.entries(p?.models ?? {})) {
2214
2753
  const c = m?.cost;
2215
2754
  const ok = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
2216
2755
  if (!c || !ok(c.input) || !ok(c.output)) continue;
2217
- out[`${provider}/${id}`] = [c.input, c.output, ok(c.cache_read) ? c.cache_read : c.input / 10];
2756
+ out[`${provider}/${id2}`] = [c.input, c.output, ok(c.cache_read) ? c.cache_read : c.input / 10];
2218
2757
  }
2219
2758
  }
2220
2759
  return out;
@@ -2223,9 +2762,9 @@ var cached2;
2223
2762
  function readCatalog() {
2224
2763
  const file = catalogFile();
2225
2764
  try {
2226
- const { mtimeMs } = fs12.statSync(file);
2765
+ const { mtimeMs } = fs15.statSync(file);
2227
2766
  if (cached2?.file === file && cached2.mtimeMs === mtimeMs) return cached2.catalog;
2228
- const parsed = JSON.parse(fs12.readFileSync(file, "utf8"));
2767
+ const parsed = JSON.parse(fs15.readFileSync(file, "utf8"));
2229
2768
  const catalog6 = parsed && typeof parsed.models === "object" && parsed.models && Number.isFinite(Date.parse(parsed.fetchedAt)) ? parsed : void 0;
2230
2769
  cached2 = { file, mtimeMs, catalog: catalog6 };
2231
2770
  return catalog6;
@@ -2238,10 +2777,10 @@ function catalogPrice(backend, ...models) {
2238
2777
  const catalog6 = readCatalog();
2239
2778
  if (!catalog6) return void 0;
2240
2779
  for (const raw of models) {
2241
- const id = raw?.split("#")[0];
2242
- if (!id) continue;
2243
- const keys = [id, ...[...OWN[backend] ?? [], "anthropic", "openai", "google"].map((p) => `${p}/${id}`)];
2244
- const any = keys.find((k) => catalog6.models[k]) ?? Object.keys(catalog6.models).filter((k) => k.endsWith(`/${id}`)).sort()[0];
2780
+ const id2 = raw?.split("#")[0];
2781
+ if (!id2) continue;
2782
+ const keys = [id2, ...[...OWN[backend] ?? [], "anthropic", "openai", "google"].map((p) => `${p}/${id2}`)];
2783
+ const any = keys.find((k) => catalog6.models[k]) ?? Object.keys(catalog6.models).filter((k) => k.endsWith(`/${id2}`)).sort()[0];
2245
2784
  const triple = any ? catalog6.models[any] : void 0;
2246
2785
  if (any && triple && triple.length === 3 && triple.every((n) => Number.isFinite(n) && n >= 0)) {
2247
2786
  const [input, output, cachedInput] = triple;
@@ -2283,8 +2822,8 @@ function savedUsd(usage, returnedTokens, price = primaryPrice()) {
2283
2822
  return Math.max(0, costAt(usage, price) - (workerCostOf(usage) ?? 0) - readingTheReport);
2284
2823
  }
2285
2824
  function readLedger(sinceMs) {
2286
- if (!fs13.existsSync(ledgerFile())) return [];
2287
- return fs13.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
2825
+ if (!fs16.existsSync(ledgerFile())) return [];
2826
+ return fs16.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
2288
2827
  try {
2289
2828
  return [JSON.parse(l)];
2290
2829
  } catch {
@@ -2362,8 +2901,8 @@ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.rou
2362
2901
  var WEEK_MS = 7 * 24 * 3600 * 1e3;
2363
2902
 
2364
2903
  // src/core/plan.ts
2365
- import fs14 from "node:fs";
2366
- import path14 from "node:path";
2904
+ import fs17 from "node:fs";
2905
+ import path17 from "node:path";
2367
2906
  var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
2368
2907
  var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
2369
2908
  function structure(lines) {
@@ -2410,9 +2949,9 @@ function parsePlan(text, file = "") {
2410
2949
  return { file, title: titleHeading?.text ?? "", header, constraints, tasks };
2411
2950
  }
2412
2951
  function loadPlan(file) {
2413
- const abs = path14.resolve(file);
2414
- if (!fs14.existsSync(abs) || !fs14.statSync(abs).isFile()) throw new UserError(`plan not found: ${file}`);
2415
- const plan = parsePlan(fs14.readFileSync(abs, "utf8"), fs14.realpathSync(abs));
2952
+ const abs = path17.resolve(file);
2953
+ if (!fs17.existsSync(abs) || !fs17.statSync(abs).isFile()) throw new UserError(`plan not found: ${file}`);
2954
+ const plan = parsePlan(fs17.readFileSync(abs, "utf8"), fs17.realpathSync(abs));
2416
2955
  if (!plan.tasks.length) throw new UserError(`${file} has no "Task N" headings (see pitroom-writing-plans)`);
2417
2956
  return plan;
2418
2957
  }
@@ -2431,7 +2970,7 @@ function brief(plan, task) {
2431
2970
  ].filter(Boolean).join("\n\n")}
2432
2971
  `;
2433
2972
  }
2434
- var planName = (file) => path14.basename(file).replace(/\.md$/i, "");
2973
+ var planName = (file) => path17.basename(file).replace(/\.md$/i, "");
2435
2974
 
2436
2975
  // src/core/style.ts
2437
2976
  var enabled = () => {
@@ -2463,13 +3002,13 @@ function retryCommands(metas, shell = retryShell()) {
2463
3002
  }
2464
3003
 
2465
3004
  // src/core/eval.ts
2466
- import { spawnSync as spawnSync7 } from "node:child_process";
2467
- import fs15 from "node:fs";
3005
+ import { spawnSync as spawnSync8 } from "node:child_process";
3006
+ import fs18 from "node:fs";
2468
3007
  var KINDS = ["definition", "count", "set"];
2469
3008
  function loadEval(file) {
2470
3009
  let raw;
2471
3010
  try {
2472
- raw = JSON.parse(fs15.readFileSync(file, "utf8"));
3011
+ raw = JSON.parse(fs18.readFileSync(file, "utf8"));
2473
3012
  } catch (e) {
2474
3013
  throw new UserError(`cannot read ${file}: ${e.message}`);
2475
3014
  }
@@ -2479,24 +3018,24 @@ function loadEval(file) {
2479
3018
  return list2.map((q, i) => {
2480
3019
  const where = `${file}: question ${i + 1}`;
2481
3020
  if (!q || typeof q !== "object") throw new UserError(`${where} is not an object`);
2482
- const id = typeof q.id === "string" && q.id.trim() ? q.id.trim() : `q${i + 1}`;
2483
- if (ids.has(id)) throw new UserError(`${where}: the id "${id}" is used twice`);
2484
- ids.add(id);
2485
- if (typeof q.task !== "string" || !q.task.trim()) throw new UserError(`${where} (${id}) has no task`);
2486
- if (!KINDS.includes(q.kind)) throw new UserError(`${where} (${id}): kind must be one of ${KINDS.join(", ")}`);
3021
+ const id2 = typeof q.id === "string" && q.id.trim() ? q.id.trim() : `q${i + 1}`;
3022
+ if (ids.has(id2)) throw new UserError(`${where}: the id "${id2}" is used twice`);
3023
+ ids.add(id2);
3024
+ if (typeof q.task !== "string" || !q.task.trim()) throw new UserError(`${where} (${id2}) has no task`);
3025
+ if (!KINDS.includes(q.kind)) throw new UserError(`${where} (${id2}): kind must be one of ${KINDS.join(", ")}`);
2487
3026
  const t = q.truth;
2488
3027
  const grep = t && typeof t === "object" && !Array.isArray(t) && typeof t.grep === "string" && t.grep;
2489
3028
  const literal = q.kind === "definition" && typeof t === "string" && /:\d+$/.test(t) || q.kind === "count" && typeof t === "number" && Number.isInteger(t) && t >= 0 || q.kind === "set" && Array.isArray(t) && t.every((s) => typeof s === "string");
2490
3029
  if (!grep && !literal) {
2491
3030
  const want = { definition: '"path:line"', count: "a whole number", set: "a list of paths" }[q.kind];
2492
- throw new UserError(`${where} (${id}): truth must be ${want} or {"grep": "pattern", \u2026}`);
3031
+ throw new UserError(`${where} (${id2}): truth must be ${want} or {"grep": "pattern", \u2026}`);
2493
3032
  }
2494
- return { id, task: q.task.trim(), kind: q.kind, truth: t };
3033
+ return { id: id2, task: q.task.trim(), kind: q.kind, truth: t };
2495
3034
  });
2496
3035
  }
2497
3036
  var clean = (p) => p.replace(/\\/g, "/").replace(/^\.?\//, "");
2498
3037
  function gitGrep(root, flags, t) {
2499
- const r = spawnSync7("git", ["-C", root, "grep", ...flags, t.regex ? "-E" : "-F", ...t.word ? ["-w"] : [], "-e", t.grep, "--", t.dir ?? "."], {
3038
+ const r = spawnSync8("git", ["-C", root, "grep", ...flags, t.regex ? "-E" : "-F", ...t.word ? ["-w"] : [], "-e", t.grep, "--", t.dir ?? "."], {
2500
3039
  encoding: "utf8",
2501
3040
  maxBuffer: 64 * 1024 * 1024
2502
3041
  });
@@ -2531,7 +3070,7 @@ function answerParts(answer) {
2531
3070
  const summary = s ? s[1] : text.split(/^\s*(?:DETAILS|FILES CHANGED|VERIFICATION|OPEN ISSUES):/m)[0];
2532
3071
  return { summary: summary.trim(), details: (d?.[1] ?? "").trim() };
2533
3072
  }
2534
- function pathsIn(text) {
3073
+ function pathsIn2(text) {
2535
3074
  const found = text.replace(/\\/g, "/").match(/[A-Za-z0-9_.@\-/]+\.[A-Za-z0-9]{1,8}(?::\d+)?/g) ?? [];
2536
3075
  return [...new Set(found.map((p) => clean(p.replace(/:\d+$/, ""))))];
2537
3076
  }
@@ -2543,7 +3082,7 @@ function scoreAnswer(q, truth, answer, root) {
2543
3082
  if ("line" in truth) {
2544
3083
  const flat = summary.replace(/\s+/g, " ");
2545
3084
  if (new RegExp(`(^|[^\\w./-])\\.?/?${escape2(truth.path)}:${truth.line}(?!\\d)`).test(flat)) return { score: 1, got: `${truth.path}:${truth.line}` };
2546
- const named = pathsIn(flat).includes(truth.path);
3085
+ const named = pathsIn2(flat).includes(truth.path);
2547
3086
  return { score: named ? 0.5 : 0, got: flat.slice(0, 120) };
2548
3087
  }
2549
3088
  if ("count" in truth) {
@@ -2551,8 +3090,8 @@ function scoreAnswer(q, truth, answer, root) {
2551
3090
  const got2 = n ? Number(n[0].replace(/,/g, "")) : void 0;
2552
3091
  return { score: got2 === truth.count ? 1 : 0, got: got2 === void 0 ? "(no number)" : String(got2) };
2553
3092
  }
2554
- let got = pathsIn(summary);
2555
- if (!got.length) got = pathsIn(details);
3093
+ let got = pathsIn2(summary);
3094
+ if (!got.length) got = pathsIn2(details);
2556
3095
  const hit = got.filter((p) => truth.files.includes(p)).length;
2557
3096
  const precision = got.length ? hit / got.length : 0;
2558
3097
  const recall = truth.files.length ? hit / truth.files.length : got.length ? 0 : 1;
@@ -2580,8 +3119,8 @@ function summarize(rows) {
2580
3119
  }
2581
3120
 
2582
3121
  // src/vcs/guard.ts
2583
- import fs16 from "node:fs";
2584
- import path15 from "node:path";
3122
+ import fs19 from "node:fs";
3123
+ import path18 from "node:path";
2585
3124
  var VERSION = 2;
2586
3125
  var WINDOWS = process.platform === "win32";
2587
3126
  var fwd = (p) => WINDOWS ? p.replace(/\\/g, "/") : p;
@@ -2658,29 +3197,29 @@ esac
2658
3197
  exec "$real" "$@"
2659
3198
  `;
2660
3199
  function shimDir() {
2661
- return path15.join(home(), "shim", `v${VERSION}`);
3200
+ return path18.join(home(), "shim", `v${VERSION}`);
2662
3201
  }
2663
3202
  function findRealGit(skip) {
2664
3203
  const same = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
2665
- for (const dir of (process.env.PATH ?? "").split(path15.delimiter).filter(Boolean)) {
2666
- if (same(path15.resolve(dir), skip)) continue;
2667
- const p = path15.join(dir, WINDOWS ? "git.exe" : "git");
3204
+ for (const dir of (process.env.PATH ?? "").split(path18.delimiter).filter(Boolean)) {
3205
+ if (same(path18.resolve(dir), skip)) continue;
3206
+ const p = path18.join(dir, WINDOWS ? "git.exe" : "git");
2668
3207
  try {
2669
- fs16.accessSync(p, fs16.constants.X_OK);
2670
- if (fs16.statSync(p).isFile()) return p;
3208
+ fs19.accessSync(p, fs19.constants.X_OK);
3209
+ if (fs19.statSync(p).isFile()) return p;
2671
3210
  } catch {
2672
3211
  }
2673
3212
  }
2674
3213
  return void 0;
2675
3214
  }
2676
3215
  function findGitSh(realGit) {
2677
- let dir = path15.dirname(realGit);
3216
+ let dir = path18.dirname(realGit);
2678
3217
  for (let i = 0; i < 4; i++) {
2679
3218
  for (const rel of ["bin/sh.exe", "usr/bin/sh.exe"]) {
2680
- const p = path15.join(dir, rel);
2681
- if (fs16.existsSync(p)) return p;
3219
+ const p = path18.join(dir, rel);
3220
+ if (fs19.existsSync(p)) return p;
2682
3221
  }
2683
- const up = path15.dirname(dir);
3222
+ const up = path18.dirname(dir);
2684
3223
  if (up === dir) break;
2685
3224
  dir = up;
2686
3225
  }
@@ -2698,12 +3237,12 @@ var REF_HOOK = `#!/bin/sh
2698
3237
  echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
2699
3238
  exit 1
2700
3239
  `;
2701
- var hooksDir = () => path15.join(home(), "git-hooks", `v${HOOK_VERSION}`);
2702
- function writeIfChanged(file, content) {
2703
- if (fs16.existsSync(file) && fs16.readFileSync(file, "utf8") === content) return;
2704
- fs16.mkdirSync(path15.dirname(file), { recursive: true });
3240
+ var hooksDir = () => path18.join(home(), "git-hooks", `v${HOOK_VERSION}`);
3241
+ function writeIfChanged2(file, content) {
3242
+ if (fs19.existsSync(file) && fs19.readFileSync(file, "utf8") === content) return;
3243
+ fs19.mkdirSync(path18.dirname(file), { recursive: true });
2705
3244
  const tmp = `${file}.${process.pid}.tmp`;
2706
- fs16.writeFileSync(tmp, content, { mode: 493 });
3245
+ fs19.writeFileSync(tmp, content, { mode: 493 });
2707
3246
  renameOver(tmp, file);
2708
3247
  }
2709
3248
  function pathKey(env) {
@@ -2735,7 +3274,7 @@ function guardEnv(env) {
2735
3274
  GIT_PAGER: "cat",
2736
3275
  PAGER: "cat"
2737
3276
  };
2738
- writeIfChanged(path15.join(hooksDir(), "reference-transaction"), REF_HOOK);
3277
+ writeIfChanged2(path18.join(hooksDir(), "reference-transaction"), REF_HOOK);
2739
3278
  env = withGitConfig(onePath(env), [
2740
3279
  ["core.hooksPath", fwd(hooksDir())],
2741
3280
  ["url.pitroom-push-blocked://.pushInsteadOf", ""]
@@ -2747,11 +3286,11 @@ function guardEnv(env) {
2747
3286
  if (WINDOWS) {
2748
3287
  sh = findGitSh(real);
2749
3288
  if (!sh) return { ...env, ...quiet };
2750
- writeIfChanged(path15.join(dir, "git.cmd"), cmdShim(sh));
3289
+ writeIfChanged2(path18.join(dir, "git.cmd"), cmdShim(sh));
2751
3290
  }
2752
- writeIfChanged(path15.join(dir, "git"), SHIM);
3291
+ writeIfChanged2(path18.join(dir, "git"), SHIM);
2753
3292
  const key = pathKey(env);
2754
- return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${path15.delimiter}${env[key] ?? ""}` };
3293
+ return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${path18.delimiter}${env[key] ?? ""}` };
2755
3294
  }
2756
3295
  function shimReady() {
2757
3296
  const env = guardEnv({ ...process.env });
@@ -2759,11 +3298,11 @@ function shimReady() {
2759
3298
  }
2760
3299
 
2761
3300
  // src/core/slots.ts
2762
- import crypto2 from "node:crypto";
2763
- import fs17 from "node:fs";
2764
- import path16 from "node:path";
2765
- var slotsDir = () => path16.join(home(), "slots");
2766
- var locksDir = () => path16.join(home(), "locks");
3301
+ import crypto4 from "node:crypto";
3302
+ import fs20 from "node:fs";
3303
+ import path19 from "node:path";
3304
+ var slotsDir = () => path19.join(home(), "slots");
3305
+ var locksDir = () => path19.join(home(), "locks");
2767
3306
  var STARTUP_GRACE_MS = 3e4;
2768
3307
  function holderActive(runId, self) {
2769
3308
  if (!runId || runId === self) return false;
@@ -2777,23 +3316,23 @@ function holderActive(runId, self) {
2777
3316
  }
2778
3317
  }
2779
3318
  function tryClaim(file, runId) {
2780
- fs17.mkdirSync(path16.dirname(file), { recursive: true });
3319
+ fs20.mkdirSync(path19.dirname(file), { recursive: true });
2781
3320
  try {
2782
- fs17.writeFileSync(file, runId, { flag: "wx" });
3321
+ fs20.writeFileSync(file, runId, { flag: "wx" });
2783
3322
  return true;
2784
3323
  } catch (e) {
2785
3324
  if (e.code !== "EEXIST") throw e;
2786
3325
  }
2787
3326
  let owner = "";
2788
3327
  try {
2789
- owner = fs17.readFileSync(file, "utf8").trim();
3328
+ owner = fs20.readFileSync(file, "utf8").trim();
2790
3329
  } catch {
2791
3330
  }
2792
3331
  if (owner === runId) return true;
2793
3332
  if (holderActive(owner, runId)) return false;
2794
- fs17.rmSync(file, { force: true });
3333
+ fs20.rmSync(file, { force: true });
2795
3334
  try {
2796
- fs17.writeFileSync(file, runId, { flag: "wx" });
3335
+ fs20.writeFileSync(file, runId, { flag: "wx" });
2797
3336
  return true;
2798
3337
  } catch {
2799
3338
  return false;
@@ -2802,101 +3341,47 @@ function tryClaim(file, runId) {
2802
3341
  function releaseIfOwner(file, runId) {
2803
3342
  if (!file) return;
2804
3343
  try {
2805
- if (fs17.readFileSync(file, "utf8").trim() === runId) fs17.rmSync(file, { force: true });
3344
+ if (fs20.readFileSync(file, "utf8").trim() === runId) fs20.rmSync(file, { force: true });
2806
3345
  } catch {
2807
3346
  }
2808
3347
  }
2809
3348
  function tryAcquireSlot(runId, maxParallel) {
2810
3349
  for (let n = 0; n < Math.max(1, maxParallel); n++) {
2811
- const file = path16.join(slotsDir(), `slot-${n}`);
3350
+ const file = path19.join(slotsDir(), `slot-${n}`);
2812
3351
  if (tryClaim(file, runId)) return file;
2813
3352
  }
2814
3353
  return void 0;
2815
3354
  }
2816
3355
  var releaseSlot = (file, runId) => releaseIfOwner(file, runId);
2817
3356
  function slotHolders() {
2818
- if (!fs17.existsSync(slotsDir())) return [];
2819
- return fs17.readdirSync(slotsDir()).map((f) => {
3357
+ if (!fs20.existsSync(slotsDir())) return [];
3358
+ return fs20.readdirSync(slotsDir()).map((f) => {
2820
3359
  try {
2821
- return fs17.readFileSync(path16.join(slotsDir(), f), "utf8").trim();
3360
+ return fs20.readFileSync(path19.join(slotsDir(), f), "utf8").trim();
2822
3361
  } catch {
2823
3362
  return "";
2824
3363
  }
2825
- }).filter((id) => holderActive(id, ""));
3364
+ }).filter((id2) => holderActive(id2, ""));
2826
3365
  }
2827
- var lockFile = (repoRoot) => path16.join(locksDir(), `write-${crypto2.createHash("sha1").update(path16.resolve(repoRoot)).digest("hex").slice(0, 16)}`);
3366
+ var lockFile = (repoRoot) => path19.join(locksDir(), `write-${crypto4.createHash("sha1").update(path19.resolve(repoRoot)).digest("hex").slice(0, 16)}`);
2828
3367
  function acquireWriteLock(repoRoot, runId) {
2829
3368
  const file = lockFile(repoRoot);
2830
3369
  if (tryClaim(file, runId)) return;
2831
3370
  let owner = "";
2832
3371
  try {
2833
- owner = fs17.readFileSync(file, "utf8").trim();
3372
+ owner = fs20.readFileSync(file, "utf8").trim();
2834
3373
  } catch {
2835
3374
  }
2836
3375
  throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
2837
3376
  }
2838
3377
  var releaseWriteLock = (repoRoot, runId) => releaseIfOwner(lockFile(repoRoot), runId);
2839
3378
 
2840
- // src/core/secrets.ts
2841
- import { execFileSync } from "node:child_process";
2842
- import fs18 from "node:fs";
2843
- import path17 from "node:path";
2844
- var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
2845
- var MAX_VISITED = 2e4;
2846
- var MAX_DEPTH = 4;
2847
- var TEMPLATE = /\.(example|sample|template|dist|defaults?|tpl)$/i;
2848
- function looksSecret(file) {
2849
- const name = file.toLowerCase();
2850
- if (TEMPLATE.test(name)) return false;
2851
- if (name === ".env" || name.startsWith(".env.") || name.endsWith(".env")) return true;
2852
- return /\.(pem|p12|pfx|key|jks|keystore)$/.test(name) || /^id_(rsa|dsa|ecdsa|ed25519)$/.test(name) || name === ".netrc" || name === "credentials.json" || /^secrets?\.(json|ya?ml|toml)$/.test(name);
2853
- }
2854
- function findSecretFiles(dir) {
2855
- const found = [];
2856
- let visited = 0;
2857
- const walk = (d, depth) => {
2858
- if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
2859
- let entries;
2860
- try {
2861
- entries = fs18.readdirSync(d, { withFileTypes: true });
2862
- } catch {
2863
- return;
2864
- }
2865
- for (const e of entries) {
2866
- visited++;
2867
- if (e.isDirectory()) {
2868
- if (!SKIP_DIRS2.has(e.name)) walk(path17.join(d, e.name), depth + 1);
2869
- } else if (e.isFile() && looksSecret(e.name)) {
2870
- found.push(slash(path17.relative(dir, path17.join(d, e.name))));
2871
- }
2872
- }
2873
- };
2874
- walk(dir, 0);
2875
- return found.sort();
2876
- }
2877
- function findSecretFilesInTree(root, dir) {
2878
- try {
2879
- const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
2880
- return out.split("\0").filter((f) => f && looksSecret(path17.basename(f)) && !path17.relative(dir, path17.join(root, f)).startsWith("..")).map((f) => slash(path17.relative(dir, path17.join(root, f)))).sort();
2881
- } catch {
2882
- return [];
2883
- }
2884
- }
2885
- function secretWarning(files, mode) {
2886
- if (!files.length) return void 0;
2887
- const shown = files.slice(0, 3).join(", ") + (files.length > 3 ? `, +${files.length - 3} more` : "");
2888
- const where = mode === "isolate" ? "would be copied into the isolated copy" : "sit in the directory the worker runs in";
2889
- const fix = mode === "isolate" ? "add them to .gitignore, or" : "use an isolated copy (-i, git-ignored files are left out) or a clean checkout, or";
2890
- return `secret-looking files ${where} (${shown}): the worker is told not to read them, but nothing stops it, and its model may be hosted by a third party; ${fix} set PITROOM_NO_SECRET_WARNING=1 to silence this`;
2891
- }
2892
- var slash = (p) => p.split(path17.sep).join("/");
2893
-
2894
3379
  // src/core/process.ts
2895
3380
  import { spawn } from "node:child_process";
2896
- import fs19 from "node:fs";
3381
+ import fs21 from "node:fs";
2897
3382
  async function spawnWorker(inv, opts) {
2898
- const out = fs19.openSync(opts.stdoutFile, "w");
2899
- const err = fs19.openSync(opts.stderrFile, "w");
3383
+ const out = fs21.openSync(opts.stdoutFile, "w");
3384
+ const err = fs21.openSync(opts.stderrFile, "w");
2900
3385
  const res = { code: null, timedOut: false, stopped: false };
2901
3386
  const child = spawn(inv.command, inv.args, {
2902
3387
  cwd: opts.cwd,
@@ -2919,7 +3404,7 @@ async function spawnWorker(inv, opts) {
2919
3404
  }, opts.timeoutSec * 1e3);
2920
3405
  res.code = await new Promise((resolve2) => {
2921
3406
  child.on("error", (e) => {
2922
- res.spawnError = e.code === "ENOENT" ? fs19.existsSync(opts.cwd) ? `${inv.command} not found` : `the working directory ${opts.cwd} does not exist` : e.message;
3407
+ res.spawnError = e.code === "ENOENT" ? fs21.existsSync(opts.cwd) ? `${inv.command} not found` : `the working directory ${opts.cwd} does not exist` : e.message;
2923
3408
  resolve2(127);
2924
3409
  });
2925
3410
  child.on("close", (c) => resolve2(c));
@@ -2928,13 +3413,14 @@ async function spawnWorker(inv, opts) {
2928
3413
  if (killer) clearTimeout(killer);
2929
3414
  process.off("SIGINT", stop);
2930
3415
  process.off("SIGTERM", stop);
2931
- fs19.closeSync(out);
2932
- fs19.closeSync(err);
3416
+ fs21.closeSync(out);
3417
+ fs21.closeSync(err);
2933
3418
  return res;
2934
3419
  }
2935
3420
  export {
2936
3421
  ASKS_FOR_LIST,
2937
3422
  DEFAULT_BACKEND,
3423
+ FORBIDDEN_COMMANDS,
2938
3424
  STOP_FILE,
2939
3425
  TERMINAL,
2940
3426
  acquireWriteLock,
@@ -2963,6 +3449,7 @@ export {
2963
3449
  home,
2964
3450
  isActive,
2965
3451
  isAlive,
3452
+ linkInto,
2966
3453
  listRunIds,
2967
3454
  loadEval,
2968
3455
  loadPlan,
@@ -2975,7 +3462,7 @@ export {
2975
3462
  parseStatus,
2976
3463
  parseTarget,
2977
3464
  parseVerdict,
2978
- pathsIn,
3465
+ pathsIn2 as pathsIn,
2979
3466
  planName,
2980
3467
  planTask,
2981
3468
  primaryPrice,
@@ -3008,5 +3495,6 @@ export {
3008
3495
  usd,
3009
3496
  verifyRefs,
3010
3497
  workerCostOf,
3498
+ writeIfChanged,
3011
3499
  writeMeta
3012
3500
  };