pitroom 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib.mjs CHANGED
@@ -6,56 +6,741 @@ 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) {
546
+ const real = (p) => {
547
+ try {
548
+ return fs6.realpathSync(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
+ ...HOME_SECRETS.map((f) => path5.join(os3.homedir(), f)).filter((p) => fs6.existsSync(p)),
557
+ // the keychain link of the worker's own home
558
+ path5.join(workerHome3, "Library")
559
+ ];
560
+ return {
561
+ ...baseSettings(),
562
+ permissions: {
563
+ // plan mode writes its plan into agy's own data folder, which is outside the workspace; the user's toolchains
564
+ // under their home stay usable in the sandbox (read-only), which sees only what is granted besides the system
565
+ allow: [
566
+ "command(*)",
567
+ ...both(path5.join(dataDir(workerHome3), "brain")).map((p) => `write_file(${rulePath(p)})`),
568
+ ...toolchains().flatMap(both).map((p) => `read_file(${rulePath(p)})`)
569
+ ],
570
+ deny: [
571
+ ...secrets.flatMap(both).map((p) => `read_file(${rulePath(p)})`),
572
+ ...denyCommandRules(),
573
+ "mcp(*)",
574
+ // No web for agy workers: the rule is in the settings file, so --web cannot switch it on per run.
575
+ "read_url(*)",
576
+ "execute_url(*)"
577
+ ]
578
+ }
579
+ };
580
+ }
581
+ function toolchains() {
582
+ const homeDir = os3.homedir();
583
+ const inHome = (d) => {
584
+ const rel = path5.relative(homeDir, d);
585
+ return !!rel && !rel.startsWith("..") && !path5.isAbsolute(rel);
586
+ };
587
+ const dirs = (process.env.PATH ?? "").split(path5.delimiter).filter((d) => d && path5.isAbsolute(d) && inHome(d) && fs6.existsSync(d));
588
+ const install = (d) => path5.basename(d) === "bin" && /[\\/](versions|toolchains)[\\/]/.test(d) ? path5.dirname(d) : d;
589
+ return [...new Set(dirs.map(install))].filter((d) => !inHome(home()) || path5.relative(home(), d).startsWith(".."));
590
+ }
591
+ function baseSettings() {
592
+ return {
593
+ enableTerminalSandbox: true,
594
+ toolPermission: "proceed-in-sandbox",
595
+ artifactReviewPolicy: "always-proceed",
596
+ allowNonWorkspaceAccess: false
597
+ };
598
+ }
599
+ var rulePath = (p) => p.replace(/^[A-Za-z]:(?=[\\/])/, "").replace(/\\/g, "/");
600
+ var HOMES_KEPT_DAYS = 14;
601
+ function workerHome(workspace) {
602
+ const root = path5.join(home(), "antigravity-home");
603
+ const id2 = crypto3.createHash("sha256").update(path5.resolve(workspace)).digest("hex").slice(0, 16);
604
+ const dir = path5.join(root, id2);
605
+ fs6.mkdirSync(dataDir(dir), { recursive: true });
606
+ writeIfChanged(path5.join(dataDir(dir), "settings.json"), `${JSON.stringify(workerSettings(workspace, dir), null, 2)}
607
+ `);
608
+ if (process.platform === "darwin") {
609
+ fs6.mkdirSync(path5.join(dir, "Library"), { recursive: true });
610
+ linkInto(path5.join(dir, "Library"), path5.join(os3.homedir(), "Library", "Keychains"));
611
+ } else {
612
+ linkInto(dataDir(dir), path5.join(userDir(), TOKEN));
613
+ }
614
+ const now = /* @__PURE__ */ new Date();
615
+ fs6.utimesSync(dir, now, now);
616
+ pruneHomes(root, dir);
617
+ return dir;
618
+ }
619
+ function pruneHomes(root, keep) {
620
+ const cutoff = Date.now() - HOMES_KEPT_DAYS * 864e5;
621
+ try {
622
+ for (const name of fs6.readdirSync(root)) {
623
+ const dir = path5.join(root, name);
624
+ if (dir === keep || !/^[0-9a-f]{16}$/.test(name)) continue;
625
+ if (fs6.statSync(dir).mtimeMs < cutoff) fs6.rmSync(dir, { recursive: true, force: true });
626
+ }
627
+ } catch {
628
+ }
629
+ }
630
+ function invocation(req) {
631
+ const args = [
632
+ // -p takes the prompt as its value; one that starts with "-" would be read as an option: a leading space keeps it a value.
633
+ "-p",
634
+ req.prompt.startsWith("-") ? ` ${req.prompt}` : req.prompt,
635
+ "--output-format",
636
+ "stream-json",
637
+ "--mode",
638
+ req.mode === "read" ? "plan" : "accept-edits",
639
+ "--sandbox",
640
+ // agy's own limit (5m by default) would end a long run with partial output and exit 0; Pitroom's timeout governs.
641
+ "--print-timeout",
642
+ "24h"
643
+ ];
644
+ if (req.sessionId) args.push("--conversation", req.sessionId);
645
+ const { model, effort } = splitModel(req.model);
646
+ if (model) args.push("--model", model);
647
+ if (effort) args.push("--effort", effort);
648
+ const { command, prefix } = resolveCommand(binary());
649
+ const workerDir = workerHome(req.cwd);
650
+ const env = { HOME: workerDir };
651
+ if (process.platform === "win32") env.USERPROFILE = workerDir;
652
+ return { command, args: [...prefix, ...args], env };
653
+ }
654
+ function classify(message) {
655
+ if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|capacity/i.test(message)) return "rate-limited";
656
+ 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)) {
657
+ return "auth";
658
+ }
659
+ 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)) {
660
+ return "model-unavailable";
661
+ }
662
+ return "other";
30
663
  }
31
- function isFile(p) {
664
+ var TIMED_OUT = /print timeout after/i;
665
+ var BLOCKED = /^This request was blocked by \S+ filters\b/;
666
+ function failure(run, stderr, exitCode) {
667
+ const lines = stderr.trim().split("\n").map((l) => l.trim()).filter(Boolean);
668
+ const envelope = lines.find((l) => l.startsWith("AGY_ERROR"));
669
+ 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));
670
+ if (exitCode === 0 && !run.error) {
671
+ if (BLOCKED.test(run.finalText)) return { kind: "other", message: `the model's filter blocked the request: ${run.finalText.split("\n")[0]}` };
672
+ if (run.finalText) return void 0;
673
+ if (detail) return { kind: classify(detail), message: detail };
674
+ if (lines.some((l) => TIMED_OUT.test(l))) return { kind: "other", message: "agy stopped at its own --print-timeout before answering" };
675
+ if (run.usage.denied) return { kind: "other", message: "agy ended its turn without an answer when a tool call was refused" };
676
+ if (!run.sessionId && !stderr.trim()) return { kind: "other", message: "agy printed nothing (some agy versions write no output to a pipe); update agy" };
677
+ return { kind: "other", message: "agy ended without an answer (a refused tool call ends its turn with no other sign)" };
678
+ }
679
+ const message = run.error ?? detail ?? lines.at(-1) ?? `agy exited with code ${exitCode}`;
680
+ return { kind: classify(`${message} ${detail ?? ""}`), message };
681
+ }
682
+ function warnings(_run, stderr) {
683
+ const lost = /conversation "([^"]+)" not found/i.exec(stderr);
684
+ return lost ? [`agy did not find the earlier conversation (${lost[1]}) and started a new one: this answer does not know the earlier turns`] : [];
685
+ }
686
+ function fromEnvelope(line) {
32
687
  try {
33
- return fs.statSync(p).isFile();
688
+ const e = JSON.parse(line.slice(line.indexOf("{")));
689
+ if (typeof e.short_error === "string" && e.short_error) return e.short_error;
690
+ const text = [e.status ?? e.code, e.message ?? e.error].filter((v) => typeof v === "string" && v).join(": ");
691
+ return text || void 0;
34
692
  } catch {
35
- return false;
693
+ return line.replace(/^AGY_ERROR:?\s*/, "") || void 0;
36
694
  }
37
695
  }
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 {
696
+ function doctor({ models, hasFallback }) {
697
+ const version = agy(["--version"]);
698
+ if (version.missing || !version.ok) {
699
+ return [{
700
+ level: "fail",
701
+ message: `agy not runnable (${binary()}). Install the Antigravity CLI (curl -fsSL https://antigravity.google/cli/install.sh | bash), or set PITROOM_ANTIGRAVITY_BIN`
702
+ }];
703
+ }
704
+ const checks = [
705
+ { level: "ok", message: `Antigravity CLI ${version.out.trim().split("\n")[0]} at ${binary()}` },
706
+ {
707
+ level: "warn",
708
+ 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"
709
+ }
710
+ ];
711
+ if (process.platform === "win32") {
712
+ 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
713
  }
49
- return { command: bin, prefix: [] };
714
+ if (process.platform === "linux" && !fs6.existsSync(path5.join(userDir(), TOKEN))) {
715
+ 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" });
716
+ }
717
+ for (const m of models) checks.push({ level: "ok", message: m ? `Antigravity model: ${m}` : "Antigravity model: agy's own choice (`agy models` lists yours)" });
718
+ checks.push({ level: "ok", message: "Antigravity workers have no web access (--web does not apply to them)" });
719
+ 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)" });
720
+ return checks;
50
721
  }
722
+ var antigravity = {
723
+ id: "antigravity",
724
+ name: "Antigravity CLI",
725
+ capabilities: { readOnly: "snapshot", resume: "by-id", reportsCost: false, attachFiles: false },
726
+ binary,
727
+ invocation,
728
+ parse: parseEvents,
729
+ failure,
730
+ warnings,
731
+ doctor
732
+ };
51
733
 
52
- // src/backends/types.ts
53
- var MAX_STEPS = 400;
734
+ // src/backends/claude/index.ts
735
+ import { spawnSync as spawnSync2 } from "node:child_process";
736
+ import fs7 from "node:fs";
737
+ import os4 from "node:os";
738
+ import path6 from "node:path";
54
739
 
55
740
  // 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) {
741
+ var EDIT_TOOLS2 = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
742
+ var DENIED2 = /permission|not allowed|denied|blocked|disallowed/i;
743
+ function parseEvents2(jsonl) {
59
744
  const usage = {
60
745
  input: 0,
61
746
  output: 0,
@@ -108,9 +793,9 @@ function parseEvents(jsonl) {
108
793
  tools[c.name] = (tools[c.name] ?? 0) + 1;
109
794
  const target = c.input?.file_path ?? c.input?.path ?? c.input?.pattern ?? c.input?.command ?? c.input?.url;
110
795
  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));
796
+ if (EDIT_TOOLS2.has(c.name) && c.input?.file_path) pendingEdits.set(c.id, String(c.input.file_path));
112
797
  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) };
798
+ const step = { kind: EDIT_TOOLS2.has(c.name) ? "edit" : c.name === "Bash" ? "shell" : "tool", name: c.name, text: clip(String(target ?? ""), 240) };
114
799
  timeline.push(step);
115
800
  stepOf.set(String(c.id), step);
116
801
  }
@@ -123,7 +808,7 @@ function parseEvents(jsonl) {
123
808
  const step = stepOf.get(String(c.tool_use_id));
124
809
  if (step) step.ok = !c.is_error;
125
810
  if (c.is_error) {
126
- if (DENIED.test(text)) usage.denied++;
811
+ if (DENIED2.test(text)) usage.denied++;
127
812
  } else if (pendingEdits.has(c.tool_use_id)) {
128
813
  edits.push(pendingEdits.get(c.tool_use_id));
129
814
  }
@@ -149,20 +834,12 @@ function parseEvents(jsonl) {
149
834
  usage.steps = steps.size;
150
835
  return { sessionId, model, finalText: finalText ?? (error ? "" : lastText), usage, tools, edits, lastActivity, timeline, error };
151
836
  }
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
837
 
161
838
  // src/backends/claude/index.ts
162
- var binary = () => findBinary("claude", "PITROOM_CLAUDE_BIN", ["~/.local/bin/claude", "~/.claude/local/claude"]);
839
+ var binary2 = () => findBinary("claude", "PITROOM_CLAUDE_BIN", ["~/.local/bin/claude", "~/.claude/local/claude"]);
163
840
  function cl(args, timeout = 6e4) {
164
- const { command, prefix } = resolveCommand(binary());
165
- const r = spawnSync(command, [...prefix, ...args], {
841
+ const { command, prefix } = resolveCommand(binary2());
842
+ const r = spawnSync2(command, [...prefix, ...args], {
166
843
  encoding: "utf8",
167
844
  timeout,
168
845
  stdio: ["ignore", "pipe", "pipe"],
@@ -173,52 +850,12 @@ function cl(args, timeout = 6e4) {
173
850
  var READ_TOOLS = ["Read", "Grep", "Glob"];
174
851
  var WRITE_TOOLS = [...READ_TOOLS, "Edit", "Write", "Bash"];
175
852
  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}:*)`);
853
+ var DENY_BASH = FORBIDDEN_COMMANDS.map((c) => `Bash(${c}:*)`);
217
854
  var SECRETS = ["**/.env", "**/.env.*", "**/*.pem", "**/id_rsa*", "**/id_ed25519*"].map((p) => `Read(${p})`);
218
855
  function toolsFor(mode, web2) {
219
856
  return [...mode === "read" ? READ_TOOLS : WRITE_TOOLS, ...web2 ? WEB_TOOLS : []];
220
857
  }
221
- function invocation(req) {
858
+ function invocation2(req) {
222
859
  const tools = toolsFor(req.mode, req.web).join(",");
223
860
  const args = [
224
861
  "-p",
@@ -246,10 +883,10 @@ function invocation(req) {
246
883
  if (effort) args.push("--effort", effort);
247
884
  if (req.sessionId) args.push("--resume", req.sessionId);
248
885
  args.push("--", req.prompt);
249
- const { command, prefix } = resolveCommand(binary());
886
+ const { command, prefix } = resolveCommand(binary2());
250
887
  return { command, args: [...prefix, ...args], env: { DISABLE_AUTOUPDATER: "1" } };
251
888
  }
252
- function classify(message) {
889
+ function classify2(message) {
253
890
  if (/authentication_failed|failed to authenticate|oauth|not logged in|\/login|invalid api key|\b40[13]\b|unauthori[sz]ed/i.test(message)) {
254
891
  return "auth";
255
892
  }
@@ -257,26 +894,26 @@ function classify(message) {
257
894
  if (/model[^.]*(not found|does not exist|invalid|not available)|invalid model|not_found_error/i.test(message)) return "model-unavailable";
258
895
  return "other";
259
896
  }
260
- function failure(run, stderr, exitCode) {
897
+ function failure2(run, stderr, exitCode) {
261
898
  if (exitCode === 0 && !run.error) return void 0;
262
899
  const detail = stderr.trim().split("\n").filter(Boolean).pop();
263
900
  const message = run.error ?? detail ?? `claude exited with code ${exitCode}`;
264
- return { kind: classify(message), message };
901
+ return { kind: classify2(message), message };
265
902
  }
266
903
  function defaultModel() {
267
904
  try {
268
- const settings2 = JSON.parse(fs2.readFileSync(path2.join(os2.homedir(), ".claude", "settings.json"), "utf8"));
905
+ const settings2 = JSON.parse(fs7.readFileSync(path6.join(os4.homedir(), ".claude", "settings.json"), "utf8"));
269
906
  return typeof settings2.model === "string" ? settings2.model : void 0;
270
907
  } catch {
271
908
  return void 0;
272
909
  }
273
910
  }
274
- function doctor({ models, hasFallback }) {
911
+ function doctor2({ models, hasFallback }) {
275
912
  const version = cl(["--version"]);
276
913
  if (version.missing || !version.ok) {
277
- return [{ level: "fail", message: `claude not runnable (${binary()}). Install Claude Code, or set PITROOM_CLAUDE_BIN` }];
914
+ return [{ level: "fail", message: `claude not runnable (${binary2()}). Install Claude Code, or set PITROOM_CLAUDE_BIN` }];
278
915
  }
279
- const checks = [{ level: "ok", message: `Claude Code ${version.out.trim()} at ${binary()}` }];
916
+ const checks = [{ level: "ok", message: `Claude Code ${version.out.trim()} at ${binary2()}` }];
280
917
  let loggedIn = false;
281
918
  try {
282
919
  loggedIn = JSON.parse(cl(["auth", "status"]).out).loggedIn === true;
@@ -299,7 +936,7 @@ function doctor({ models, hasFallback }) {
299
936
  var CLAUDE_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
300
937
  function catalog() {
301
938
  const def = defaultModel();
302
- const models = ["haiku", "sonnet", "opus"].map((id) => ({ id, efforts: CLAUDE_EFFORTS }));
939
+ const models = ["haiku", "sonnet", "opus"].map((id2) => ({ id: id2, efforts: CLAUDE_EFFORTS }));
303
940
  if (def && !models.some((m) => m.id === def)) models.push({ id: def, efforts: CLAUDE_EFFORTS });
304
941
  return { models, source: "Claude Code aliases (latest of each size) and --effort levels from `claude --help`" };
305
942
  }
@@ -307,25 +944,25 @@ var claude = {
307
944
  id: "claude",
308
945
  name: "Claude Code",
309
946
  capabilities: { readOnly: "tool-allowlist", resume: "by-id", reportsCost: true, attachFiles: false },
310
- binary,
947
+ binary: binary2,
311
948
  catalog,
312
- invocation,
313
- parse: parseEvents,
314
- failure,
949
+ invocation: invocation2,
950
+ parse: parseEvents2,
951
+ failure: failure2,
315
952
  defaultModel,
316
- doctor
953
+ doctor: doctor2
317
954
  };
318
955
 
319
956
  // 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";
957
+ import { spawnSync as spawnSync3 } from "node:child_process";
958
+ import fs8 from "node:fs";
959
+ import os5 from "node:os";
960
+ import path7 from "node:path";
324
961
 
325
962
  // src/backends/codex/events.ts
326
963
  var WORK = /* @__PURE__ */ new Set(["agent_message", "reasoning", "command_execution", "file_change", "mcp_tool_call", "web_search", "todo_list"]);
327
964
  var TOOLS = /* @__PURE__ */ new Set(["command_execution", "file_change", "mcp_tool_call", "web_search"]);
328
- function parseEvents2(jsonl) {
965
+ function parseEvents3(jsonl) {
329
966
  const usage = {
330
967
  input: 0,
331
968
  output: 0,
@@ -366,31 +1003,31 @@ function parseEvents2(jsonl) {
366
1003
  }
367
1004
  if (it.type === "agent_message" && String(it.text ?? "").trim()) {
368
1005
  finalText = String(it.text).trim();
369
- lastActivity = `says: ${oneLine2(finalText)}`;
370
- if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip2(finalText, 600) });
1006
+ lastActivity = `says: ${oneLine(finalText)}`;
1007
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(finalText, 600) });
371
1008
  } else if (it.type === "command_execution") {
372
1009
  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 });
1010
+ lastActivity = `shell ${oneLine(String(it.command ?? ""), 60)}`;
1011
+ 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
1012
  } else if (it.type === "file_change") {
376
1013
  const paths = (it.changes ?? []).map((c) => String(c.path));
377
1014
  if (it.status !== "failed" && it.status !== "declined") edits.push(...paths);
378
1015
  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" });
1016
+ lastActivity = `edit ${oneLine(paths.join(", "), 60)}`;
1017
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "edit", name: "edit", text: clip(paths.join(", "), 240), ok: it.status !== "failed" && it.status !== "declined" });
381
1018
  }
382
1019
  break;
383
1020
  }
384
1021
  case "turn.completed": {
385
1022
  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);
1023
+ const cached3 = num(u.cached_input_tokens);
1024
+ const reasoning = num(u.reasoning_output_tokens);
1025
+ usage.input += Math.max(0, num(u.input_tokens) - cached3);
389
1026
  usage.cacheRead += cached3;
390
- usage.cacheWrite += num2(u.cache_write_input_tokens);
391
- usage.output += Math.max(0, num2(u.output_tokens) - reasoning);
1027
+ usage.cacheWrite += num(u.cache_write_input_tokens);
1028
+ usage.output += Math.max(0, num(u.output_tokens) - reasoning);
392
1029
  usage.reasoning += reasoning;
393
- usage.total += num2(u.input_tokens) + num2(u.output_tokens);
1030
+ usage.total += num(u.input_tokens) + num(u.output_tokens);
394
1031
  break;
395
1032
  }
396
1033
  case "error":
@@ -414,21 +1051,13 @@ function describe(raw) {
414
1051
  return s;
415
1052
  }
416
1053
  }
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
1054
 
426
1055
  // 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");
1056
+ var binary3 = () => findBinary("codex", "PITROOM_CODEX_BIN");
1057
+ var codexHome = () => process.env.CODEX_HOME ?? path7.join(os5.homedir(), ".codex");
429
1058
  function cx(args, timeout = 6e4) {
430
- const { command, prefix } = resolveCommand(binary2());
431
- const r = spawnSync2(command, [...prefix, ...args], {
1059
+ const { command, prefix } = resolveCommand(binary3());
1060
+ const r = spawnSync3(command, [...prefix, ...args], {
432
1061
  encoding: "utf8",
433
1062
  timeout,
434
1063
  stdio: ["ignore", "pipe", "pipe"],
@@ -438,12 +1067,7 @@ function cx(args, timeout = 6e4) {
438
1067
  }
439
1068
  var SANDBOX = { read: "read-only", write: "workspace-write", isolate: "workspace-write" };
440
1069
  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) {
1070
+ function invocation3(req) {
447
1071
  const { model, effort } = splitModel(req.model);
448
1072
  const args = ["exec"];
449
1073
  if (req.sessionId) args.push("resume", req.sessionId);
@@ -460,10 +1084,10 @@ function invocation2(req) {
460
1084
  if (model) args.push("--model", model);
461
1085
  if (effort) args.push("-c", `model_reasoning_effort=${toml(effort)}`);
462
1086
  args.push(req.prompt);
463
- const { command, prefix } = resolveCommand(binary2());
1087
+ const { command, prefix } = resolveCommand(binary3());
464
1088
  return { command, args: [...prefix, ...args], env: {} };
465
1089
  }
466
- function classify2(message) {
1090
+ function classify3(message) {
467
1091
  if (/not supported|model[^.]*(not found|does not exist|unknown|unavailable)|unsupported model|no such model/i.test(message)) {
468
1092
  return "model-unavailable";
469
1093
  }
@@ -471,37 +1095,37 @@ function classify2(message) {
471
1095
  if (/unauthori[sz]ed|\b40[13]\b|not logged in|log ?in|api.?key|credits?\b|billing|subscription/i.test(message)) return "auth";
472
1096
  return "other";
473
1097
  }
474
- function failure2(run, stderr, exitCode) {
1098
+ function failure3(run, stderr, exitCode) {
475
1099
  if (exitCode === 0 && !run.error) return void 0;
476
1100
  const detail = stderr.split("\n").find((l) => /^Error[: ]|ERROR/.test(l) && !/rmcp|models cache/.test(l));
477
1101
  let message = run.error ?? (detail ? detail.trim() : `codex exited with code ${exitCode}`);
478
1102
  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 };
1103
+ return { kind: classify3(message), message };
480
1104
  }
481
1105
  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"));
1106
+ const root = path7.join(codexHome(), "sessions");
1107
+ const day = (d) => path7.join(root, String(d.getFullYear()), String(d.getMonth() + 1).padStart(2, "0"), String(d.getDate()).padStart(2, "0"));
484
1108
  const now = /* @__PURE__ */ new Date();
485
1109
  for (const dir of [day(now), day(new Date(now.getTime() - 864e5))]) {
486
1110
  let files = [];
487
1111
  try {
488
- files = fs3.readdirSync(dir).filter((f) => f.includes(sessionId));
1112
+ files = fs8.readdirSync(dir).filter((f) => f.includes(sessionId));
489
1113
  } catch {
490
1114
  continue;
491
1115
  }
492
1116
  for (const f of files) {
493
- const m = /"model":"([^"]+)"/.exec(fs3.readFileSync(path3.join(dir, f), "utf8"));
1117
+ const m = /"model":"([^"]+)"/.exec(fs8.readFileSync(path7.join(dir, f), "utf8"));
494
1118
  if (m) return m[1];
495
1119
  }
496
1120
  }
497
1121
  return void 0;
498
1122
  }
499
- function doctor2({ models, hasFallback }) {
1123
+ function doctor3({ models, hasFallback }) {
500
1124
  const version = cx(["--version"]);
501
1125
  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` }];
1126
+ return [{ level: "fail", message: `codex not runnable (${binary3()}). Install: npm i -g @openai/codex, or set PITROOM_CODEX_BIN` }];
503
1127
  }
504
- const checks = [{ level: "ok", message: `${version.out.trim()} at ${binary2()}` }];
1128
+ const checks = [{ level: "ok", message: `${version.out.trim()} at ${binary3()}` }];
505
1129
  const login = cx(["login", "status"]);
506
1130
  checks.push(
507
1131
  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 +1141,9 @@ function doctor2({ models, hasFallback }) {
517
1141
  return checks;
518
1142
  }
519
1143
  function catalog2() {
520
- const file = path3.join(codexHome(), "models_cache.json");
1144
+ const file = path7.join(codexHome(), "models_cache.json");
521
1145
  try {
522
- const raw = JSON.parse(fs3.readFileSync(file, "utf8"));
1146
+ const raw = JSON.parse(fs8.readFileSync(file, "utf8"));
523
1147
  const models = (raw.models ?? []).filter((m) => m.visibility !== "hide" && typeof m.slug === "string").map((m) => ({
524
1148
  id: m.slug,
525
1149
  efforts: (m.supported_reasoning_levels ?? []).map(
@@ -537,143 +1161,25 @@ var codex = {
537
1161
  name: "Codex",
538
1162
  catalog: catalog2,
539
1163
  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
- }
1164
+ binary: binary3,
1165
+ invocation: invocation3,
1166
+ parse: parseEvents3,
1167
+ failure: failure3,
1168
+ resolveModel,
1169
+ doctor: doctor3
1170
+ };
1171
+
1172
+ // src/backends/gemini/index.ts
1173
+ import { spawnSync as spawnSync4 } from "node:child_process";
1174
+ import fs9 from "node:fs";
1175
+ import os6 from "node:os";
1176
+ import path8 from "node:path";
1177
+ import { fileURLToPath } from "node:url";
672
1178
 
673
1179
  // 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) {
1180
+ var EDIT_TOOLS3 = /* @__PURE__ */ new Set(["replace", "write_file", "edit"]);
1181
+ var DENIED3 = /policy|permission|not allowed|denied|blocked|disallowed|refus/i;
1182
+ function parseEvents4(jsonl) {
677
1183
  const usage = {
678
1184
  input: 0,
679
1185
  output: 0,
@@ -703,8 +1209,8 @@ function parseEvents3(jsonl) {
703
1209
  text = "";
704
1210
  if (!said) return;
705
1211
  lastText = said;
706
- lastActivity = `says: ${oneLine3(said)}`;
707
- if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip3(said, 600) });
1212
+ lastActivity = `says: ${oneLine(said)}`;
1213
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(said, 600) });
708
1214
  };
709
1215
  const startTurn = () => {
710
1216
  if (!inTurn) turns++;
@@ -736,11 +1242,11 @@ function parseEvents3(jsonl) {
736
1242
  usage.toolCalls++;
737
1243
  tools[name] = (tools[name] ?? 0) + 1;
738
1244
  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();
1245
+ lastActivity = `${name} ${target ? oneLine(String(target), 60) : ""}`.trim();
740
1246
  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));
1247
+ if (EDIT_TOOLS3.has(name) && file) pendingEdits.set(String(e.tool_id), String(file));
742
1248
  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) };
1249
+ const step = { kind: EDIT_TOOLS3.has(name) ? "edit" : name === "run_shell_command" ? "shell" : "tool", name, text: clip(String(target ?? ""), 240) };
744
1250
  timeline.push(step);
745
1251
  stepOf.set(String(e.tool_id), step);
746
1252
  }
@@ -749,18 +1255,18 @@ function parseEvents3(jsonl) {
749
1255
  const ok = e.status === "success";
750
1256
  const step = stepOf.get(String(e.tool_id));
751
1257
  if (step) step.ok = ok;
752
- if (!ok && DENIED2.test(`${e.error?.type ?? ""} ${e.error?.message ?? ""}`)) usage.denied++;
1258
+ if (!ok && DENIED3.test(`${e.error?.type ?? ""} ${e.error?.message ?? ""}`)) usage.denied++;
753
1259
  if (ok && pendingEdits.has(String(e.tool_id))) edits.push(pendingEdits.get(String(e.tool_id)));
754
1260
  } else if (e.type === "error") {
755
1261
  if (e.severity === "error") error ??= String(e.message ?? "Gemini CLI error");
756
1262
  } else if (e.type === "result") {
757
1263
  const s = e.stats ?? {};
758
- const cached3 = num3(s.cached);
1264
+ const cached3 = num(s.cached);
759
1265
  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));
1266
+ usage.input += s.input !== void 0 ? num(s.input) : Math.max(0, num(s.input_tokens) - cached3);
1267
+ usage.output += num(s.output_tokens);
1268
+ usage.total += num(s.total_tokens) || num(s.input_tokens) + num(s.output_tokens);
1269
+ usage.toolCalls = Math.max(usage.toolCalls, num(s.tool_calls));
764
1270
  model ??= Object.keys(s.models ?? {})[0];
765
1271
  if (e.status === "error") error ??= String(e.error?.message ?? e.error?.type ?? "Gemini CLI error");
766
1272
  }
@@ -769,20 +1275,12 @@ function parseEvents3(jsonl) {
769
1275
  usage.steps = turns;
770
1276
  return { sessionId, model, finalText: error ? "" : lastText, usage, tools, edits, lastActivity, timeline, error };
771
1277
  }
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
1278
 
781
1279
  // src/backends/gemini/index.ts
782
- var binary3 = () => findBinary("gemini", "PITROOM_GEMINI_BIN", ["~/.local/bin/gemini"]);
1280
+ var binary4 = () => findBinary("gemini", "PITROOM_GEMINI_BIN", ["~/.local/bin/gemini"]);
783
1281
  function gem(args, timeout = 6e4) {
784
- const { command, prefix } = resolveCommand(binary3());
785
- const r = spawnSync3(command, [...prefix, ...args], {
1282
+ const { command, prefix } = resolveCommand(binary4());
1283
+ const r = spawnSync4(command, [...prefix, ...args], {
786
1284
  encoding: "utf8",
787
1285
  timeout,
788
1286
  stdio: ["ignore", "pipe", "pipe"],
@@ -790,32 +1288,19 @@ function gem(args, timeout = 6e4) {
790
1288
  });
791
1289
  return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
792
1290
  }
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"));
1291
+ var policyDir = () => path8.resolve(path8.dirname(fileURLToPath(import.meta.url)), "..", "policies", "gemini");
1292
+ function workerHome2() {
1293
+ const root = path8.join(home(), "gemini-home");
1294
+ const dir = path8.join(root, ".gemini");
1295
+ const base = JSON.parse(fs9.readFileSync(path8.join(policyDir(), "worker-settings.json"), "utf8"));
798
1296
  const auth = settings().security?.auth;
799
1297
  const body = `${JSON.stringify(auth ? { ...base, security: { ...base.security, auth } } : base, null, 2)}
800
1298
  `;
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
- }
1299
+ writeIfChanged(path8.join(dir, "settings.json"), body);
1300
+ for (const name of ["oauth_creds.json", "google_accounts.json"]) linkInto(dir, path8.join(geminiHome(), name));
816
1301
  return root;
817
1302
  }
818
- function invocation3(req) {
1303
+ function invocation4(req) {
819
1304
  const dir = policyDir();
820
1305
  const policies = ["base.toml", ...req.web ? [] : ["no-web.toml"], ...req.mode === "read" ? [] : ["shell.toml"]];
821
1306
  const args = [
@@ -829,18 +1314,18 @@ function invocation3(req) {
829
1314
  "-e",
830
1315
  "none"
831
1316
  ];
832
- for (const p of policies) args.push("--policy", path5.join(dir, p));
1317
+ for (const p of policies) args.push("--policy", path8.join(dir, p));
833
1318
  if (req.sessionId) args.push("--resume", req.sessionId);
834
1319
  if (process.env.PITROOM_GEMINI_TRUST === "1") args.push("--skip-trust");
835
1320
  const [model] = (req.model ?? "").split("#");
836
1321
  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;
1322
+ const { command, prefix } = resolveCommand(binary4());
1323
+ const env = { GEMINI_CLI_HOME: workerHome2() };
1324
+ const trusted = path8.join(geminiHome(), "trustedFolders.json");
1325
+ if (fs9.existsSync(trusted)) env.GEMINI_CLI_TRUSTED_FOLDERS_PATH = trusted;
841
1326
  return { command, args: [...prefix, ...args], env };
842
1327
  }
843
- function classify3(message) {
1328
+ function classify4(message) {
844
1329
  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
1330
  return "auth";
846
1331
  }
@@ -849,20 +1334,20 @@ function classify3(message) {
849
1334
  return "other";
850
1335
  }
851
1336
  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) {
1337
+ function failure4(run, stderr, exitCode) {
853
1338
  if (exitCode === 0 && !run.error) return void 0;
854
1339
  const lines = stderr.trim().split("\n").map((l) => l.trim()).filter(Boolean);
855
1340
  const detail = lines.find((l) => /error|failed|exceeded|quota|exhausted|unsupported/i.test(l) && !/^at /.test(l)) ?? lines.at(-1);
856
1341
  const raw = run.error ?? detail ?? `gemini exited with code ${exitCode}`;
857
1342
  const message = /not running in a trusted directory/i.test(`${stderr} ${raw}`) ? UNTRUSTED : raw;
858
- return { kind: classify3(message), message };
1343
+ return { kind: classify4(message), message };
859
1344
  }
860
1345
  function geminiHome() {
861
- return path5.join(process.env.GEMINI_CLI_HOME ?? os5.homedir(), ".gemini");
1346
+ return path8.join(process.env.GEMINI_CLI_HOME ?? os6.homedir(), ".gemini");
862
1347
  }
863
1348
  function settings() {
864
1349
  try {
865
- return JSON.parse(fs6.readFileSync(path5.join(geminiHome(), "settings.json"), "utf8"));
1350
+ return JSON.parse(fs9.readFileSync(path8.join(geminiHome(), "settings.json"), "utf8"));
866
1351
  } catch {
867
1352
  return {};
868
1353
  }
@@ -873,15 +1358,15 @@ function defaultModel2() {
873
1358
  const name = typeof m === "string" ? m : m?.name;
874
1359
  return typeof name === "string" ? name : void 0;
875
1360
  }
876
- function doctor3({ models, hasFallback }) {
1361
+ function doctor4({ models, hasFallback }) {
877
1362
  const version = gem(["--version"]);
878
1363
  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` }];
1364
+ return [{ level: "fail", message: `gemini not runnable (${binary4()}). Install Gemini CLI (npm i -g @google/gemini-cli), or set PITROOM_GEMINI_BIN` }];
880
1365
  }
881
- const checks = [{ level: "ok", message: `Gemini CLI ${version.out.trim()} at ${binary3()}` }];
1366
+ const checks = [{ level: "ok", message: `Gemini CLI ${version.out.trim()} at ${binary4()}` }];
882
1367
  const key = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
883
1368
  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"));
1369
+ const signedIn = fs9.existsSync(path8.join(geminiHome(), "oauth_creds.json"));
885
1370
  const type = settings().security?.auth?.selectedType;
886
1371
  if (key || vertex) {
887
1372
  checks.push({ level: "ok", message: `Gemini CLI: ${key ? "an API key is set" : "Vertex AI is set up"}` });
@@ -910,7 +1395,7 @@ function doctor3({ models, hasFallback }) {
910
1395
  }
911
1396
  function catalog3() {
912
1397
  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 }));
1398
+ 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
1399
  if (def && !models.some((m) => m.id === def)) models.push({ id: def });
915
1400
  return { models, source: "Gemini CLI's built-in model names (it cannot list models); the effort suffix is not used" };
916
1401
  }
@@ -918,26 +1403,26 @@ var gemini = {
918
1403
  id: "gemini",
919
1404
  name: "Gemini CLI",
920
1405
  capabilities: { readOnly: "approval-mode", resume: "by-id", reportsCost: false, attachFiles: false },
921
- binary: binary3,
1406
+ binary: binary4,
922
1407
  catalog: catalog3,
923
- invocation: invocation3,
924
- parse: parseEvents3,
925
- failure: failure3,
1408
+ invocation: invocation4,
1409
+ parse: parseEvents4,
1410
+ failure: failure4,
926
1411
  defaultModel: defaultModel2,
927
- doctor: doctor3
1412
+ doctor: doctor4
928
1413
  };
929
1414
 
930
1415
  // src/backends/kilo/index.ts
931
- import { spawnSync as spawnSync5 } from "node:child_process";
932
- import path7 from "node:path";
1416
+ import { spawnSync as spawnSync6 } from "node:child_process";
1417
+ import path10 from "node:path";
933
1418
 
934
1419
  // src/backends/opencode/index.ts
935
- import { spawnSync as spawnSync4 } from "node:child_process";
1420
+ import { spawnSync as spawnSync5 } from "node:child_process";
936
1421
 
937
1422
  // 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) {
1423
+ var EDIT_TOOLS4 = /* @__PURE__ */ new Set(["edit", "write", "patch", "multiedit", "apply_patch"]);
1424
+ var DENIED4 = /rule which prevents you|permission denied|permission\.rejected/i;
1425
+ function parseEvents5(ndjson) {
941
1426
  const usage = {
942
1427
  input: 0,
943
1428
  output: 0,
@@ -974,8 +1459,8 @@ function parseEvents4(ndjson) {
974
1459
  const key = String(p.messageID ?? "");
975
1460
  if (!textByMessage.has(key)) textByMessage.set(key, []);
976
1461
  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) });
1462
+ lastActivity = `says: ${oneLine(text)}`;
1463
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(text.trim(), 600), at: stamp(e.timestamp) });
979
1464
  break;
980
1465
  }
981
1466
  case "tool_use": {
@@ -983,28 +1468,28 @@ function parseEvents4(ndjson) {
983
1468
  const st = p.state ?? {};
984
1469
  usage.toolCalls++;
985
1470
  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)) {
1471
+ if (st.status === "error" && DENIED4.test(String(st.error ?? ""))) usage.denied++;
1472
+ if (st.status === "completed" && EDIT_TOOLS4.has(name)) {
988
1473
  edits.push(String(st.input?.filePath ?? st.input?.path ?? name));
989
1474
  }
990
1475
  lastActivity = `${name} ${describe2(st.input)}`.trim();
991
1476
  if (timeline.length < MAX_STEPS) {
992
- const kind2 = EDIT_TOOLS3.has(name) ? "edit" : /^(bash|shell)$/.test(name) ? "shell" : "tool";
1477
+ const kind2 = EDIT_TOOLS4.has(name) ? "edit" : /^(bash|shell)$/.test(name) ? "shell" : "tool";
993
1478
  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) });
1479
+ 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
1480
  }
996
1481
  break;
997
1482
  }
998
1483
  case "step_finish": {
999
1484
  const t = p.tokens ?? {};
1000
1485
  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);
1486
+ usage.input += num(t.input);
1487
+ usage.output += num(t.output);
1488
+ usage.reasoning += num(t.reasoning);
1489
+ usage.cacheRead += num(t.cache?.read);
1490
+ usage.cacheWrite += num(t.cache?.write);
1491
+ usage.total += num(t.total) || num(t.input) + num(t.output) + num(t.reasoning) + num(t.cache?.read);
1492
+ usage.cost = (usage.cost ?? 0) + num(p.cost);
1008
1493
  break;
1009
1494
  }
1010
1495
  case "error": {
@@ -1028,25 +1513,17 @@ function describeError(err) {
1028
1513
  const tags = [err.type, inner, err.status && `HTTP ${err.status}`].filter(Boolean);
1029
1514
  return tags.length ? `${message} [${tags.join(", ")}]` : message;
1030
1515
  }
1031
- var clip4 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
1032
1516
  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
1517
  function describe2(input) {
1041
1518
  if (!input || typeof input !== "object") return "";
1042
1519
  const v = input.filePath ?? input.path ?? input.pattern ?? input.command ?? input.url ?? input.query;
1043
- return v ? oneLine4(String(v), 60) : "";
1520
+ return v ? oneLine(String(v), 60) : "";
1044
1521
  }
1045
1522
 
1046
1523
  // src/backends/opencode/profiles.ts
1047
- import fs7 from "node:fs";
1048
- import os6 from "node:os";
1049
- import path6 from "node:path";
1524
+ import fs10 from "node:fs";
1525
+ import os7 from "node:os";
1526
+ import path9 from "node:path";
1050
1527
  var AGENT = { read: "pitroom-read", write: "pitroom-write" };
1051
1528
  var READ_FILES = {
1052
1529
  "*": "allow",
@@ -1142,22 +1619,23 @@ var FORBIDDEN_BASH = [
1142
1619
  "opencode*",
1143
1620
  "kilo *",
1144
1621
  "kilocode*",
1622
+ "agy *",
1145
1623
  "pitroom *",
1146
1624
  "*pitroom*"
1147
1625
  ];
1148
1626
  function scratchDirs(app) {
1149
- const data = process.env.XDG_DATA_HOME ?? path6.join(os6.homedir(), ".local", "share");
1627
+ const data = process.env.XDG_DATA_HOME ?? path9.join(os7.homedir(), ".local", "share");
1150
1628
  const rules = {
1151
1629
  "*": "deny",
1152
- [path6.join(data, app, "tool-output", "*")]: "allow",
1153
- [path6.join(data, app, "shell", "*", "*")]: "allow"
1630
+ [path9.join(data, app, "tool-output", "*")]: "allow",
1631
+ [path9.join(data, app, "shell", "*", "*")]: "allow"
1154
1632
  };
1155
- for (const tmp of /* @__PURE__ */ new Set([os6.tmpdir(), realpath(os6.tmpdir())])) rules[path6.join(tmp, app, "*")] = "allow";
1633
+ for (const tmp of /* @__PURE__ */ new Set([os7.tmpdir(), realpath(os7.tmpdir())])) rules[path9.join(tmp, app, "*")] = "allow";
1156
1634
  return rules;
1157
1635
  }
1158
1636
  function realpath(p) {
1159
1637
  try {
1160
- return fs7.realpathSync(p);
1638
+ return fs10.realpathSync(p);
1161
1639
  } catch {
1162
1640
  return p;
1163
1641
  }
@@ -1246,10 +1724,10 @@ function isObject(v) {
1246
1724
  }
1247
1725
 
1248
1726
  // src/backends/opencode/index.ts
1249
- var binary4 = () => findBinary("opencode", "PITROOM_OPENCODE_BIN", ["~/.opencode/bin/opencode"]);
1727
+ var binary5 = () => findBinary("opencode", "PITROOM_OPENCODE_BIN", ["~/.opencode/bin/opencode"]);
1250
1728
  function oc(args, opts = {}) {
1251
- const { command, prefix } = resolveCommand(binary4());
1252
- const r = spawnSync4(command, [...prefix, ...args], {
1729
+ const { command, prefix } = resolveCommand(binary5());
1730
+ const r = spawnSync5(command, [...prefix, ...args], {
1253
1731
  encoding: "utf8",
1254
1732
  timeout: opts.timeout ?? 6e4,
1255
1733
  env: opts.env ?? process.env,
@@ -1259,7 +1737,7 @@ function oc(args, opts = {}) {
1259
1737
  return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
1260
1738
  }
1261
1739
  var jsonIn = (s) => JSON.parse(s.slice(s.indexOf("{")));
1262
- function invocation4(req) {
1740
+ function invocation5(req) {
1263
1741
  const args = [
1264
1742
  "run",
1265
1743
  "--standalone",
@@ -1278,7 +1756,7 @@ function invocation4(req) {
1278
1756
  if (req.sessionId) args.push("--session", req.sessionId);
1279
1757
  for (const f of req.files) args.push("--file", f);
1280
1758
  args.push(req.prompt);
1281
- const { command, prefix } = resolveCommand(binary4());
1759
+ const { command, prefix } = resolveCommand(binary5());
1282
1760
  return {
1283
1761
  command,
1284
1762
  args: [...prefix, ...args],
@@ -1293,7 +1771,7 @@ function majorVersion(output) {
1293
1771
  const m = /(\d+)\.\d+\.\d+/.exec(output);
1294
1772
  return m ? Number(m[1]) : void 0;
1295
1773
  }
1296
- function classify4(message) {
1774
+ function classify5(message) {
1297
1775
  if (/model not found|ModelNotFound|unknown model|no such model|deprecated/i.test(message)) return "model-unavailable";
1298
1776
  if (/rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|unavailable|capacity/i.test(message)) return "rate-limited";
1299
1777
  if (/provider\.auth|FreeTierError|free tier|unauthori[sz]ed|\b40[13]\b|api.?key|insufficient|credits?\b|billing|payment/i.test(message)) {
@@ -1301,17 +1779,17 @@ function classify4(message) {
1301
1779
  }
1302
1780
  return "other";
1303
1781
  }
1304
- function failure4(run, stderr, exitCode) {
1782
+ function failure5(run, stderr, exitCode) {
1305
1783
  if (exitCode === 0 && !run.error) return void 0;
1306
1784
  const detail = stderr.match(/error="([^"]+)"/g)?.pop()?.slice(7, -1);
1307
1785
  const raw = run.error ?? `opencode exited with code ${exitCode}`;
1308
1786
  const message = detail && !raw.includes(detail) ? `${raw}: ${detail}` : raw;
1309
- return { kind: classify4(message), message };
1787
+ return { kind: classify5(message), message };
1310
1788
  }
1311
1789
  function modelId(m) {
1312
1790
  if (typeof m === "string") return m;
1313
- const id = m?.model ?? m?.id;
1314
- return m?.providerID && id ? `${m.providerID}/${id}` : void 0;
1791
+ const id2 = m?.model ?? m?.id;
1792
+ return m?.providerID && id2 ? `${m.providerID}/${id2}` : void 0;
1315
1793
  }
1316
1794
  function defaultModel3() {
1317
1795
  try {
@@ -1341,25 +1819,25 @@ function resolveModel2(sessionId) {
1341
1819
  function catalog4() {
1342
1820
  const ids = listModels();
1343
1821
  return {
1344
- models: (ids.length ? ids : listModels()).map((id) => ({ id })),
1822
+ models: (ids.length ? ids : listModels()).map((id2) => ({ id: id2 })),
1345
1823
  source: "`opencode models` (reasoning variants are provider-specific: provider/model#variant)"
1346
1824
  };
1347
1825
  }
1348
1826
  function listModels() {
1349
1827
  return oc(["models"]).out.split("\n").map((s) => s.trim()).filter(Boolean);
1350
1828
  }
1351
- function doctor4({ models, hasFallback }) {
1829
+ function doctor5({ models, hasFallback }) {
1352
1830
  const checks = [];
1353
1831
  const version = oc(["--version"]);
1354
1832
  if (version.missing || !version.ok) {
1355
- return [{ level: "fail", message: `opencode not runnable (${binary4()}). Install: https://opencode.ai or set PITROOM_OPENCODE_BIN` }];
1833
+ return [{ level: "fail", message: `opencode not runnable (${binary5()}). Install: https://opencode.ai or set PITROOM_OPENCODE_BIN` }];
1356
1834
  }
1357
1835
  const v = version.out.trim().replace(/^opencode\s+/i, "");
1358
1836
  const major = majorVersion(v);
1359
1837
  if (major !== void 0 && major < 2) {
1360
1838
  return [{ level: "fail", message: `OpenCode ${v} is too old: Pitroom needs OpenCode v2 or newer (run \`opencode upgrade\`)` }];
1361
1839
  }
1362
- checks.push({ level: "ok", message: `opencode ${v} at ${binary4()} (private --standalone server per run)` });
1840
+ checks.push({ level: "ok", message: `opencode ${v} at ${binary5()} (private --standalone server per run)` });
1363
1841
  const known = new Set(listModels());
1364
1842
  const def = defaultModel3();
1365
1843
  for (const m of models) {
@@ -1387,25 +1865,25 @@ var opencode = {
1387
1865
  id: "opencode",
1388
1866
  name: "OpenCode",
1389
1867
  capabilities: { readOnly: "permission-rules", resume: "by-id", reportsCost: true, attachFiles: true },
1390
- binary: binary4,
1391
- invocation: invocation4,
1392
- parse: parseEvents4,
1393
- failure: failure4,
1868
+ binary: binary5,
1869
+ invocation: invocation5,
1870
+ parse: parseEvents5,
1871
+ failure: failure5,
1394
1872
  defaultModel: defaultModel3,
1395
1873
  resolveModel: resolveModel2,
1396
1874
  listModels,
1397
1875
  catalog: catalog4,
1398
- doctor: doctor4
1876
+ doctor: doctor5
1399
1877
  };
1400
1878
 
1401
1879
  // src/backends/kilo/index.ts
1402
- var binary5 = () => findBinary("kilo", "PITROOM_KILO_BIN", ["~/.kilo/bin/kilo"]);
1880
+ var binary6 = () => findBinary("kilo", "PITROOM_KILO_BIN", ["~/.kilo/bin/kilo"]);
1403
1881
  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 });
1882
+ const { command, prefix } = resolveCommand(binary6());
1883
+ const r = spawnSync6(command, [...prefix, ...args], { encoding: "utf8", timeout, stdio: ["ignore", "pipe", "pipe"], maxBuffer: 64 * 1024 * 1024 });
1406
1884
  return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
1407
1885
  }
1408
- function invocation5(req) {
1886
+ function invocation6(req) {
1409
1887
  const args = [
1410
1888
  "run",
1411
1889
  "--agent",
@@ -1423,7 +1901,7 @@ function invocation5(req) {
1423
1901
  if (req.sessionId) args.push("--session", req.sessionId);
1424
1902
  for (const f of req.files) args.push("--file", f);
1425
1903
  args.push(req.prompt);
1426
- const { command, prefix } = resolveCommand(binary5());
1904
+ const { command, prefix } = resolveCommand(binary6());
1427
1905
  return {
1428
1906
  command,
1429
1907
  args: [...prefix, ...args],
@@ -1431,16 +1909,16 @@ function invocation5(req) {
1431
1909
  // A worker run must not upgrade Kilo underneath the user.
1432
1910
  KILO_CONFIG_CONTENT: configContent(process.env.KILO_CONFIG_CONTENT, req.web, "kilo", { autoupdate: false }),
1433
1911
  // No daemon to attach to: a private server per run (see above).
1434
- XDG_STATE_HOME: path7.join(home(), "kilo-state")
1912
+ XDG_STATE_HOME: path10.join(home(), "kilo-state")
1435
1913
  }
1436
1914
  };
1437
1915
  }
1438
- function failure5(run, stderr, exitCode) {
1916
+ function failure6(run, stderr, exitCode) {
1439
1917
  if (exitCode === 0 && !run.error) return void 0;
1440
1918
  const detail = stderr.match(/error="([^"]+)"/g)?.pop()?.slice(7, -1);
1441
1919
  const raw = run.error ?? `kilo exited with code ${exitCode}`;
1442
1920
  const message = detail && !raw.includes(detail) ? `${raw}: ${detail}` : raw;
1443
- return { kind: classify4(message), message };
1921
+ return { kind: classify5(message), message };
1444
1922
  }
1445
1923
  function listModels2() {
1446
1924
  return kilo(["models"]).out.split("\n").map((s) => s.trim()).filter((s) => /^[\w.-]+\/\S+$/.test(s));
@@ -1455,14 +1933,14 @@ function defaultModel4() {
1455
1933
  }
1456
1934
  }
1457
1935
  function catalog5() {
1458
- return { models: listModels2().map((id) => ({ id })), source: "`kilo models` (reasoning variants are provider-specific: provider/model#variant)" };
1936
+ return { models: listModels2().map((id2) => ({ id: id2 })), source: "`kilo models` (reasoning variants are provider-specific: provider/model#variant)" };
1459
1937
  }
1460
- function doctor5({ models, hasFallback }) {
1938
+ function doctor6({ models, hasFallback }) {
1461
1939
  const version = kilo(["--version"]);
1462
1940
  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` }];
1941
+ return [{ level: "fail", message: `kilo not runnable (${binary6()}). Install Kilo Code's CLI (npm i -g @kilocode/cli), or set PITROOM_KILO_BIN` }];
1464
1942
  }
1465
- const checks = [{ level: "ok", message: `Kilo Code ${version.out.trim().split("\n").pop()} at ${binary5()} (beta worker: checked against 7.8)` }];
1943
+ const checks = [{ level: "ok", message: `Kilo Code ${version.out.trim().split("\n").pop()} at ${binary6()} (beta worker: checked against 7.8)` }];
1466
1944
  const def = defaultModel4();
1467
1945
  for (const m of models) {
1468
1946
  const model = m ?? def;
@@ -1476,29 +1954,29 @@ var kiloBackend = {
1476
1954
  id: "kilo",
1477
1955
  name: "Kilo Code",
1478
1956
  capabilities: { readOnly: "permission-rules", resume: "by-id", reportsCost: true, attachFiles: true },
1479
- binary: binary5,
1480
- invocation: invocation5,
1481
- parse: parseEvents4,
1482
- failure: failure5,
1957
+ binary: binary6,
1958
+ invocation: invocation6,
1959
+ parse: parseEvents5,
1960
+ failure: failure6,
1483
1961
  defaultModel: defaultModel4,
1484
1962
  listModels: listModels2,
1485
1963
  catalog: catalog5,
1486
- doctor: doctor5
1964
+ doctor: doctor6
1487
1965
  };
1488
1966
 
1489
1967
  // 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";
1968
+ import { spawnSync as spawnSync7 } from "node:child_process";
1969
+ import fs11 from "node:fs";
1970
+ import os8 from "node:os";
1971
+ import path11 from "node:path";
1494
1972
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1495
1973
 
1496
1974
  // 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;
1975
+ var EDIT_TOOLS5 = /* @__PURE__ */ new Set(["write_file", "edit", "notebook_edit"]);
1976
+ var SHELL2 = "run_shell_command";
1977
+ var DENIED5 = /permission was declined|denied by permission rules|requires permission|not allowed/i;
1500
1978
  var API_ERROR = /^\[API Error: /;
1501
- function parseEvents5(jsonl) {
1979
+ function parseEvents6(jsonl) {
1502
1980
  const usage = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0, steps: 0, toolCalls: 0, denied: 0 };
1503
1981
  const tools = {};
1504
1982
  const edits = [];
@@ -1535,16 +2013,16 @@ function parseEvents5(jsonl) {
1535
2013
  for (const c of content) {
1536
2014
  if (c.type === "text" && String(c.text ?? "").trim()) {
1537
2015
  lastText = String(c.text).trim();
1538
- lastActivity = `says: ${oneLine5(lastText)}`;
1539
- if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip5(lastText, 600) });
2016
+ lastActivity = `says: ${oneLine(lastText)}`;
2017
+ if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(lastText, 600) });
1540
2018
  } else if (c.type === "tool_use") {
1541
2019
  usage.toolCalls++;
1542
2020
  tools[c.name] = (tools[c.name] ?? 0) + 1;
1543
2021
  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));
2022
+ lastActivity = `${c.name} ${target ? oneLine(String(target), 60) : ""}`.trim();
2023
+ if (EDIT_TOOLS5.has(c.name) && c.input?.file_path) pendingEdits.set(String(c.id), String(c.input.file_path));
1546
2024
  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) };
2025
+ const step = { kind: EDIT_TOOLS5.has(c.name) ? "edit" : c.name === SHELL2 ? "shell" : "tool", name: c.name, text: clip(String(target ?? ""), 240) };
1548
2026
  timeline.push(step);
1549
2027
  stepOf.set(String(c.id), step);
1550
2028
  }
@@ -1557,17 +2035,17 @@ function parseEvents5(jsonl) {
1557
2035
  const step = stepOf.get(String(c.tool_use_id));
1558
2036
  if (step) step.ok = !c.is_error;
1559
2037
  if (c.is_error) {
1560
- if (DENIED4.test(text)) usage.denied++;
2038
+ if (DENIED5.test(text)) usage.denied++;
1561
2039
  } else if (pendingEdits.has(String(c.tool_use_id))) {
1562
2040
  edits.push(pendingEdits.get(String(c.tool_use_id)));
1563
2041
  }
1564
2042
  }
1565
2043
  } else if (e.type === "result") {
1566
2044
  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);
2045
+ usage.input += Math.max(0, num(u.input_tokens) - num(u.cache_read_input_tokens));
2046
+ usage.cacheRead += num(u.cache_read_input_tokens);
2047
+ usage.output += num(u.output_tokens);
2048
+ usage.total += num(u.total_tokens) || num(u.input_tokens) + num(u.output_tokens);
1571
2049
  usage.denied = Math.max(usage.denied, Array.isArray(e.permission_denials) ? e.permission_denials.length : 0);
1572
2050
  if (e.is_error) {
1573
2051
  error ??= apiError(String(e.error?.message ?? e.result ?? e.subtype ?? "Qwen Code error"));
@@ -1579,16 +2057,10 @@ function parseEvents5(jsonl) {
1579
2057
  return { sessionId, model, finalText: finalText ?? (error ? "" : lastText), usage, tools, edits, lastActivity, timeline, error };
1580
2058
  }
1581
2059
  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
2060
 
1589
2061
  // 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");
2062
+ var binary7 = () => findBinary("qwen", "PITROOM_QWEN_BIN", ["~/.local/bin/qwen", "~/.npm-global/bin/qwen"]);
2063
+ var policyDir2 = () => path11.resolve(path11.dirname(fileURLToPath2(import.meta.url)), "..", "policies", "qwen");
1592
2064
  var EXCLUDED = [
1593
2065
  "agent",
1594
2066
  "send_message",
@@ -1613,48 +2085,8 @@ var EXCLUDED = [
1613
2085
  ];
1614
2086
  var WEB = ["web_fetch", "web_search"];
1615
2087
  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) {
2088
+ var DENY_SHELL = FORBIDDEN_COMMANDS.map((c) => `run_shell_command(${c})`);
2089
+ function invocation7(req) {
1658
2090
  const read = req.mode === "read";
1659
2091
  const exclude = [...EXCLUDED, ...req.web ? [] : WEB, ...read ? [] : DENY_SHELL];
1660
2092
  const args = [
@@ -1673,32 +2105,32 @@ function invocation6(req) {
1673
2105
  const model = (req.model ?? "").split("#")[0];
1674
2106
  if (model) args.push("--model", model);
1675
2107
  if (req.sessionId) args.push("--resume", req.sessionId);
1676
- const { command, prefix } = resolveCommand(binary6());
2108
+ const { command, prefix } = resolveCommand(binary7());
1677
2109
  return {
1678
2110
  command,
1679
2111
  args: [...prefix, ...args],
1680
2112
  env: {
1681
- QWEN_CODE_SYSTEM_DEFAULTS_PATH: path8.join(policyDir2(), "worker-defaults.json"),
2113
+ QWEN_CODE_SYSTEM_DEFAULTS_PATH: path11.join(policyDir2(), "worker-defaults.json"),
1682
2114
  QWEN_CODE_DISABLE_CRON: "1",
1683
2115
  QWEN_DISABLE_AUTO_TITLE: "1"
1684
2116
  }
1685
2117
  };
1686
2118
  }
1687
- function classify5(message) {
2119
+ function classify6(message) {
1688
2120
  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
2121
  if (/\b429\b|rate.?limit|too many requests|quota|capacity|overloaded|\b529\b/i.test(message)) return "rate-limited";
1690
2122
  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
2123
  return "other";
1692
2124
  }
1693
- function failure6(run, stderr, exitCode) {
2125
+ function failure7(run, stderr, exitCode) {
1694
2126
  if (exitCode === 0 && !run.error) return void 0;
1695
2127
  const detail = stderr.trim().split("\n").filter((l) => l.trim() && !/SAFE MODE/.test(l)).pop();
1696
2128
  const message = run.error ?? detail ?? `qwen exited with code ${exitCode}`;
1697
- return { kind: classify5(message), message };
2129
+ return { kind: classify6(message), message };
1698
2130
  }
1699
2131
  function userSettings() {
1700
2132
  try {
1701
- return JSON.parse(fs8.readFileSync(path8.join(process.env.QWEN_HOME ?? path8.join(os7.homedir(), ".qwen"), "settings.json"), "utf8"));
2133
+ return JSON.parse(fs11.readFileSync(path11.join(process.env.QWEN_HOME ?? path11.join(os8.homedir(), ".qwen"), "settings.json"), "utf8"));
1702
2134
  } catch {
1703
2135
  return {};
1704
2136
  }
@@ -1707,11 +2139,11 @@ function defaultModel5() {
1707
2139
  const name = userSettings().model?.name;
1708
2140
  return typeof name === "string" && name ? name : void 0;
1709
2141
  }
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)` }];
2142
+ function doctor7({ models, hasFallback }) {
2143
+ const { command, prefix } = resolveCommand(binary7());
2144
+ const r = spawnSync7(command, [...prefix, "--version"], { encoding: "utf8", timeout: 6e4, stdio: ["ignore", "pipe", "pipe"] });
2145
+ 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` }];
2146
+ const checks = [{ level: "ok", message: `Qwen Code ${r.stdout.trim()} at ${binary7()} (beta worker: checked against Qwen Code 0.25)` }];
1715
2147
  const auth = userSettings().security?.auth?.selectedType;
1716
2148
  const key = process.env.OPENAI_API_KEY || process.env.DASHSCOPE_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.GEMINI_API_KEY;
1717
2149
  checks.push(
@@ -1728,27 +2160,27 @@ var qwen = {
1728
2160
  id: "qwen",
1729
2161
  name: "Qwen Code",
1730
2162
  capabilities: { readOnly: "approval-mode", resume: "by-id", reportsCost: false, attachFiles: false },
1731
- binary: binary6,
1732
- invocation: invocation6,
1733
- parse: parseEvents5,
1734
- failure: failure6,
2163
+ binary: binary7,
2164
+ invocation: invocation7,
2165
+ parse: parseEvents6,
2166
+ failure: failure7,
1735
2167
  defaultModel: defaultModel5,
1736
- doctor: doctor6
2168
+ doctor: doctor7
1737
2169
  };
1738
2170
 
1739
2171
  // src/backends/index.ts
1740
- var REGISTRY = new Map([opencode, codex, claude, gemini, qwen, kiloBackend].map((b) => [b.id, b]));
2172
+ var REGISTRY = new Map([opencode, codex, claude, gemini, qwen, kiloBackend, antigravity].map((b) => [b.id, b]));
1741
2173
  var DEFAULT_BACKEND = opencode.id;
1742
2174
  var PLANNED = ["hermes"];
1743
2175
  var backendIds = () => [...REGISTRY.keys()];
1744
2176
  var allBackends = () => [...REGISTRY.values()];
1745
- var isBackendId = (id) => REGISTRY.has(id) || PLANNED.includes(id);
1746
- function getBackend(id) {
1747
- const backend = REGISTRY.get(id);
2177
+ var isBackendId = (id2) => REGISTRY.has(id2) || PLANNED.includes(id2);
2178
+ function getBackend(id2) {
2179
+ const backend = REGISTRY.get(id2);
1748
2180
  if (backend) return backend;
1749
2181
  const available = backendIds().join(", ");
1750
2182
  throw new UserError(
1751
- PLANNED.includes(id) ? `the "${id}" worker is not supported yet (available: ${available})` : `unknown worker "${id}" (available: ${available})`,
2183
+ PLANNED.includes(id2) ? `the "${id2}" worker is not supported yet (available: ${available})` : `unknown worker "${id2}" (available: ${available})`,
1752
2184
  3
1753
2185
  );
1754
2186
  }
@@ -1769,12 +2201,12 @@ var formatTarget = (t) => t.model ? `${t.backend}:${t.model}` : t.backend;
1769
2201
  var describeTarget = (t) => t.model ? formatTarget(t) : `${t.backend} (default model)`;
1770
2202
 
1771
2203
  // src/core/refs.ts
1772
- import fs9 from "node:fs";
1773
- import path9 from "node:path";
2204
+ import fs12 from "node:fs";
2205
+ import path12 from "node:path";
1774
2206
  var FILE_EXTENSIONS = new Set(
1775
2207
  "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
2208
  );
1777
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
2209
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
1778
2210
  var MAX_INDEXED = 6e4;
1779
2211
  var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
1780
2212
  var REF = new RegExp(
@@ -1821,13 +2253,13 @@ function extractRefs(answer) {
1821
2253
  return [...seen.values()];
1822
2254
  }
1823
2255
  function verifyRefs(refs, dirs) {
1824
- const roots = [...new Set(dirs.filter(Boolean).map((d) => path9.resolve(d)))];
2256
+ const roots = [...new Set(dirs.filter(Boolean).map((d) => path12.resolve(d)))];
1825
2257
  const invalid = [];
1826
2258
  const lineCache = /* @__PURE__ */ new Map();
1827
2259
  const load = (file) => {
1828
2260
  if (!lineCache.has(file)) {
1829
- const size = fs9.statSync(file).size;
1830
- const lines = size > MAX_BYTES ? null : fs9.readFileSync(file, "utf8").split("\n");
2261
+ const size = fs12.statSync(file).size;
2262
+ const lines = size > MAX_BYTES ? null : fs12.readFileSync(file, "utf8").split("\n");
1831
2263
  if (lines && lines[lines.length - 1] === "") lines.pop();
1832
2264
  lineCache.set(file, lines);
1833
2265
  }
@@ -1838,9 +2270,9 @@ function verifyRefs(refs, dirs) {
1838
2270
  const candidates = (ref) => {
1839
2271
  const direct = resolve(ref.file, roots);
1840
2272
  if (direct) return [direct];
1841
- if (path9.isAbsolute(ref.file)) return [];
2273
+ if (path12.isAbsolute(ref.file)) return [];
1842
2274
  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);
2275
+ return (byName().get(path12.basename(wanted)) ?? []).filter((f) => f.split(path12.sep).join("/").endsWith(`/${wanted}`) || path12.basename(f) === wanted).slice(0, 20);
1844
2276
  };
1845
2277
  const check = (file, ref) => {
1846
2278
  const lines = load(file);
@@ -1855,7 +2287,7 @@ function verifyRefs(refs, dirs) {
1855
2287
  if (!files.length) {
1856
2288
  if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
1857
2289
  total++;
1858
- invalid.push({ ref: ref.text, reason: path9.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
2290
+ invalid.push({ ref: ref.text, reason: path12.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
1859
2291
  continue;
1860
2292
  }
1861
2293
  total++;
@@ -1876,7 +2308,7 @@ function enclosedBy(lines, ref) {
1876
2308
  return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
1877
2309
  }
1878
2310
  var isFileName = (name) => {
1879
- const base = path9.basename(name);
2311
+ const base = path12.basename(name);
1880
2312
  return !base.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base) || FILE_EXTENSIONS.has(base.split(".").pop().toLowerCase());
1881
2313
  };
1882
2314
  function indexFiles(roots) {
@@ -1885,19 +2317,19 @@ function indexFiles(roots) {
1885
2317
  const walk = (dir) => {
1886
2318
  let entries;
1887
2319
  try {
1888
- entries = fs9.readdirSync(dir, { withFileTypes: true });
2320
+ entries = fs12.readdirSync(dir, { withFileTypes: true });
1889
2321
  } catch {
1890
2322
  return;
1891
2323
  }
1892
2324
  for (const e of entries) {
1893
2325
  if (count >= MAX_INDEXED) return;
1894
2326
  if (e.isDirectory()) {
1895
- if (!SKIP_DIRS.has(e.name)) walk(path9.join(dir, e.name));
2327
+ if (!SKIP_DIRS2.has(e.name)) walk(path12.join(dir, e.name));
1896
2328
  } else if (e.isFile()) {
1897
2329
  count++;
1898
2330
  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)]);
2331
+ if (list2) list2.push(path12.join(dir, e.name));
2332
+ else index.set(e.name, [path12.join(dir, e.name)]);
1901
2333
  }
1902
2334
  }
1903
2335
  };
@@ -1905,10 +2337,10 @@ function indexFiles(roots) {
1905
2337
  return index;
1906
2338
  }
1907
2339
  function resolve(ref, roots) {
1908
- const candidates = path9.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path9.join(r, ref));
2340
+ const candidates = path12.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path12.join(r, ref));
1909
2341
  return candidates.find((c) => {
1910
2342
  try {
1911
- return fs9.statSync(c).isFile() && inside(c, roots);
2343
+ return fs12.statSync(c).isFile() && inside(c, roots);
1912
2344
  } catch {
1913
2345
  return false;
1914
2346
  }
@@ -1916,21 +2348,21 @@ function resolve(ref, roots) {
1916
2348
  }
1917
2349
  function inside(file, roots) {
1918
2350
  return roots.some((r) => {
1919
- const rel = path9.relative(r, path9.resolve(file));
1920
- return rel !== "" && !rel.startsWith("..") && !path9.isAbsolute(rel);
2351
+ const rel = path12.relative(r, path12.resolve(file));
2352
+ return rel !== "" && !rel.startsWith("..") && !path12.isAbsolute(rel);
1921
2353
  });
1922
2354
  }
1923
2355
  var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1924
2356
 
1925
2357
  // src/core/templates.ts
1926
- import fs10 from "node:fs";
1927
- import path11 from "node:path";
2358
+ import fs13 from "node:fs";
2359
+ import path14 from "node:path";
1928
2360
 
1929
2361
  // src/core/install.ts
1930
- import path10 from "node:path";
2362
+ import path13 from "node:path";
1931
2363
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1932
2364
  function packageRoot() {
1933
- return path10.resolve(path10.dirname(fileURLToPath3(import.meta.url)), "..");
2365
+ return path13.resolve(path13.dirname(fileURLToPath3(import.meta.url)), "..");
1934
2366
  }
1935
2367
 
1936
2368
  // src/core/templates.ts
@@ -1941,9 +2373,9 @@ var FILES = {
1941
2373
  "code-reviewer": "pitroom-review/code-reviewer.md"
1942
2374
  };
1943
2375
  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*/, "");
2376
+ const file = path14.join(root, "skills", FILES[name]);
2377
+ if (!fs13.existsSync(file)) throw new UserError(`template missing: ${file} (broken install? run pitroom doctor)`, 3);
2378
+ return fs13.readFileSync(file, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
1947
2379
  }
1948
2380
  function fill(template, values) {
1949
2381
  const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
@@ -2000,9 +2432,9 @@ function parseAudit(text) {
2000
2432
  }
2001
2433
 
2002
2434
  // src/core/config.ts
2003
- import fs11 from "node:fs";
2004
- import os8 from "node:os";
2005
- import path12 from "node:path";
2435
+ import fs14 from "node:fs";
2436
+ import os9 from "node:os";
2437
+ import path15 from "node:path";
2006
2438
  var SCHEMA = {
2007
2439
  worker: "string",
2008
2440
  fallback: "string[]",
@@ -2033,37 +2465,37 @@ var SCHEMA = {
2033
2465
  rankWorkers: "boolean"
2034
2466
  };
2035
2467
  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");
2468
+ if (process.env.PITROOM_CONFIG) return path15.resolve(process.env.PITROOM_CONFIG);
2469
+ const base = process.platform === "win32" ? process.env.APPDATA ?? path15.join(os9.homedir(), "AppData", "Roaming") : process.env.XDG_CONFIG_HOME ?? path15.join(os9.homedir(), ".config");
2470
+ return path15.join(base, "pitroom", "config.json");
2039
2471
  }
2040
2472
  var cached;
2041
2473
  function loadConfig() {
2042
2474
  if (cached) return cached;
2043
2475
  const file = configPath();
2044
2476
  const config = {};
2045
- const warnings = [];
2046
- if (fs11.existsSync(file)) {
2477
+ const warnings2 = [];
2478
+ if (fs14.existsSync(file)) {
2047
2479
  let raw;
2048
2480
  try {
2049
- raw = JSON.parse(fs11.readFileSync(file, "utf8"));
2481
+ raw = JSON.parse(fs14.readFileSync(file, "utf8"));
2050
2482
  } catch (e) {
2051
- warnings.push(`${file}: invalid JSON (${e.message}); ignored`);
2483
+ warnings2.push(`${file}: invalid JSON (${e.message}); ignored`);
2052
2484
  }
2053
2485
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
2054
2486
  for (const [key, value] of Object.entries(raw)) {
2055
2487
  const type = SCHEMA[key];
2056
2488
  if (!type) {
2057
- warnings.push(`${file}: unknown key "${key}" ignored`);
2489
+ warnings2.push(`${file}: unknown key "${key}" ignored`);
2058
2490
  } else if (matches(value, type)) {
2059
2491
  config[key] = value;
2060
2492
  } else {
2061
- warnings.push(`${file}: "${key}" must be ${type}; ignored`);
2493
+ warnings2.push(`${file}: "${key}" must be ${type}; ignored`);
2062
2494
  }
2063
2495
  }
2064
2496
  }
2065
2497
  }
2066
- cached = { config, warnings };
2498
+ cached = { config, warnings: warnings2 };
2067
2499
  return cached;
2068
2500
  }
2069
2501
  function matches(v, type) {
@@ -2199,22 +2631,22 @@ function rankChain(chain, recordOf) {
2199
2631
  }
2200
2632
 
2201
2633
  // src/core/receipt.ts
2202
- import fs13 from "node:fs";
2634
+ import fs16 from "node:fs";
2203
2635
 
2204
2636
  // src/core/prices.ts
2205
- import fs12 from "node:fs";
2206
- import path13 from "node:path";
2637
+ import fs15 from "node:fs";
2638
+ import path16 from "node:path";
2207
2639
  var RETRY_AFTER_FAILURE_MS = 60 * 6e4;
2208
- var catalogFile = () => path13.join(home(), "prices.json");
2640
+ var catalogFile = () => path16.join(home(), "prices.json");
2209
2641
  function trim(raw) {
2210
2642
  const out = {};
2211
2643
  if (!raw || typeof raw !== "object") return out;
2212
2644
  for (const [provider, p] of Object.entries(raw)) {
2213
- for (const [id, m] of Object.entries(p?.models ?? {})) {
2645
+ for (const [id2, m] of Object.entries(p?.models ?? {})) {
2214
2646
  const c = m?.cost;
2215
2647
  const ok = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
2216
2648
  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];
2649
+ out[`${provider}/${id2}`] = [c.input, c.output, ok(c.cache_read) ? c.cache_read : c.input / 10];
2218
2650
  }
2219
2651
  }
2220
2652
  return out;
@@ -2223,9 +2655,9 @@ var cached2;
2223
2655
  function readCatalog() {
2224
2656
  const file = catalogFile();
2225
2657
  try {
2226
- const { mtimeMs } = fs12.statSync(file);
2658
+ const { mtimeMs } = fs15.statSync(file);
2227
2659
  if (cached2?.file === file && cached2.mtimeMs === mtimeMs) return cached2.catalog;
2228
- const parsed = JSON.parse(fs12.readFileSync(file, "utf8"));
2660
+ const parsed = JSON.parse(fs15.readFileSync(file, "utf8"));
2229
2661
  const catalog6 = parsed && typeof parsed.models === "object" && parsed.models && Number.isFinite(Date.parse(parsed.fetchedAt)) ? parsed : void 0;
2230
2662
  cached2 = { file, mtimeMs, catalog: catalog6 };
2231
2663
  return catalog6;
@@ -2238,10 +2670,10 @@ function catalogPrice(backend, ...models) {
2238
2670
  const catalog6 = readCatalog();
2239
2671
  if (!catalog6) return void 0;
2240
2672
  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];
2673
+ const id2 = raw?.split("#")[0];
2674
+ if (!id2) continue;
2675
+ const keys = [id2, ...[...OWN[backend] ?? [], "anthropic", "openai", "google"].map((p) => `${p}/${id2}`)];
2676
+ const any = keys.find((k) => catalog6.models[k]) ?? Object.keys(catalog6.models).filter((k) => k.endsWith(`/${id2}`)).sort()[0];
2245
2677
  const triple = any ? catalog6.models[any] : void 0;
2246
2678
  if (any && triple && triple.length === 3 && triple.every((n) => Number.isFinite(n) && n >= 0)) {
2247
2679
  const [input, output, cachedInput] = triple;
@@ -2283,8 +2715,8 @@ function savedUsd(usage, returnedTokens, price = primaryPrice()) {
2283
2715
  return Math.max(0, costAt(usage, price) - (workerCostOf(usage) ?? 0) - readingTheReport);
2284
2716
  }
2285
2717
  function readLedger(sinceMs) {
2286
- if (!fs13.existsSync(ledgerFile())) return [];
2287
- return fs13.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
2718
+ if (!fs16.existsSync(ledgerFile())) return [];
2719
+ return fs16.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
2288
2720
  try {
2289
2721
  return [JSON.parse(l)];
2290
2722
  } catch {
@@ -2362,8 +2794,8 @@ ${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.rou
2362
2794
  var WEEK_MS = 7 * 24 * 3600 * 1e3;
2363
2795
 
2364
2796
  // src/core/plan.ts
2365
- import fs14 from "node:fs";
2366
- import path14 from "node:path";
2797
+ import fs17 from "node:fs";
2798
+ import path17 from "node:path";
2367
2799
  var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
2368
2800
  var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
2369
2801
  function structure(lines) {
@@ -2410,9 +2842,9 @@ function parsePlan(text, file = "") {
2410
2842
  return { file, title: titleHeading?.text ?? "", header, constraints, tasks };
2411
2843
  }
2412
2844
  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));
2845
+ const abs = path17.resolve(file);
2846
+ if (!fs17.existsSync(abs) || !fs17.statSync(abs).isFile()) throw new UserError(`plan not found: ${file}`);
2847
+ const plan = parsePlan(fs17.readFileSync(abs, "utf8"), fs17.realpathSync(abs));
2416
2848
  if (!plan.tasks.length) throw new UserError(`${file} has no "Task N" headings (see pitroom-writing-plans)`);
2417
2849
  return plan;
2418
2850
  }
@@ -2431,7 +2863,7 @@ function brief(plan, task) {
2431
2863
  ].filter(Boolean).join("\n\n")}
2432
2864
  `;
2433
2865
  }
2434
- var planName = (file) => path14.basename(file).replace(/\.md$/i, "");
2866
+ var planName = (file) => path17.basename(file).replace(/\.md$/i, "");
2435
2867
 
2436
2868
  // src/core/style.ts
2437
2869
  var enabled = () => {
@@ -2463,13 +2895,13 @@ function retryCommands(metas, shell = retryShell()) {
2463
2895
  }
2464
2896
 
2465
2897
  // src/core/eval.ts
2466
- import { spawnSync as spawnSync7 } from "node:child_process";
2467
- import fs15 from "node:fs";
2898
+ import { spawnSync as spawnSync8 } from "node:child_process";
2899
+ import fs18 from "node:fs";
2468
2900
  var KINDS = ["definition", "count", "set"];
2469
2901
  function loadEval(file) {
2470
2902
  let raw;
2471
2903
  try {
2472
- raw = JSON.parse(fs15.readFileSync(file, "utf8"));
2904
+ raw = JSON.parse(fs18.readFileSync(file, "utf8"));
2473
2905
  } catch (e) {
2474
2906
  throw new UserError(`cannot read ${file}: ${e.message}`);
2475
2907
  }
@@ -2479,24 +2911,24 @@ function loadEval(file) {
2479
2911
  return list2.map((q, i) => {
2480
2912
  const where = `${file}: question ${i + 1}`;
2481
2913
  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(", ")}`);
2914
+ const id2 = typeof q.id === "string" && q.id.trim() ? q.id.trim() : `q${i + 1}`;
2915
+ if (ids.has(id2)) throw new UserError(`${where}: the id "${id2}" is used twice`);
2916
+ ids.add(id2);
2917
+ if (typeof q.task !== "string" || !q.task.trim()) throw new UserError(`${where} (${id2}) has no task`);
2918
+ if (!KINDS.includes(q.kind)) throw new UserError(`${where} (${id2}): kind must be one of ${KINDS.join(", ")}`);
2487
2919
  const t = q.truth;
2488
2920
  const grep = t && typeof t === "object" && !Array.isArray(t) && typeof t.grep === "string" && t.grep;
2489
2921
  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
2922
  if (!grep && !literal) {
2491
2923
  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}`);
2924
+ throw new UserError(`${where} (${id2}): truth must be ${want} or {"grep": "pattern", \u2026}`);
2493
2925
  }
2494
- return { id, task: q.task.trim(), kind: q.kind, truth: t };
2926
+ return { id: id2, task: q.task.trim(), kind: q.kind, truth: t };
2495
2927
  });
2496
2928
  }
2497
2929
  var clean = (p) => p.replace(/\\/g, "/").replace(/^\.?\//, "");
2498
2930
  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 ?? "."], {
2931
+ const r = spawnSync8("git", ["-C", root, "grep", ...flags, t.regex ? "-E" : "-F", ...t.word ? ["-w"] : [], "-e", t.grep, "--", t.dir ?? "."], {
2500
2932
  encoding: "utf8",
2501
2933
  maxBuffer: 64 * 1024 * 1024
2502
2934
  });
@@ -2580,8 +3012,8 @@ function summarize(rows) {
2580
3012
  }
2581
3013
 
2582
3014
  // src/vcs/guard.ts
2583
- import fs16 from "node:fs";
2584
- import path15 from "node:path";
3015
+ import fs19 from "node:fs";
3016
+ import path18 from "node:path";
2585
3017
  var VERSION = 2;
2586
3018
  var WINDOWS = process.platform === "win32";
2587
3019
  var fwd = (p) => WINDOWS ? p.replace(/\\/g, "/") : p;
@@ -2658,29 +3090,29 @@ esac
2658
3090
  exec "$real" "$@"
2659
3091
  `;
2660
3092
  function shimDir() {
2661
- return path15.join(home(), "shim", `v${VERSION}`);
3093
+ return path18.join(home(), "shim", `v${VERSION}`);
2662
3094
  }
2663
3095
  function findRealGit(skip) {
2664
3096
  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");
3097
+ for (const dir of (process.env.PATH ?? "").split(path18.delimiter).filter(Boolean)) {
3098
+ if (same(path18.resolve(dir), skip)) continue;
3099
+ const p = path18.join(dir, WINDOWS ? "git.exe" : "git");
2668
3100
  try {
2669
- fs16.accessSync(p, fs16.constants.X_OK);
2670
- if (fs16.statSync(p).isFile()) return p;
3101
+ fs19.accessSync(p, fs19.constants.X_OK);
3102
+ if (fs19.statSync(p).isFile()) return p;
2671
3103
  } catch {
2672
3104
  }
2673
3105
  }
2674
3106
  return void 0;
2675
3107
  }
2676
3108
  function findGitSh(realGit) {
2677
- let dir = path15.dirname(realGit);
3109
+ let dir = path18.dirname(realGit);
2678
3110
  for (let i = 0; i < 4; i++) {
2679
3111
  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;
3112
+ const p = path18.join(dir, rel);
3113
+ if (fs19.existsSync(p)) return p;
2682
3114
  }
2683
- const up = path15.dirname(dir);
3115
+ const up = path18.dirname(dir);
2684
3116
  if (up === dir) break;
2685
3117
  dir = up;
2686
3118
  }
@@ -2698,12 +3130,12 @@ var REF_HOOK = `#!/bin/sh
2698
3130
  echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
2699
3131
  exit 1
2700
3132
  `;
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 });
3133
+ var hooksDir = () => path18.join(home(), "git-hooks", `v${HOOK_VERSION}`);
3134
+ function writeIfChanged2(file, content) {
3135
+ if (fs19.existsSync(file) && fs19.readFileSync(file, "utf8") === content) return;
3136
+ fs19.mkdirSync(path18.dirname(file), { recursive: true });
2705
3137
  const tmp = `${file}.${process.pid}.tmp`;
2706
- fs16.writeFileSync(tmp, content, { mode: 493 });
3138
+ fs19.writeFileSync(tmp, content, { mode: 493 });
2707
3139
  renameOver(tmp, file);
2708
3140
  }
2709
3141
  function pathKey(env) {
@@ -2735,7 +3167,7 @@ function guardEnv(env) {
2735
3167
  GIT_PAGER: "cat",
2736
3168
  PAGER: "cat"
2737
3169
  };
2738
- writeIfChanged(path15.join(hooksDir(), "reference-transaction"), REF_HOOK);
3170
+ writeIfChanged2(path18.join(hooksDir(), "reference-transaction"), REF_HOOK);
2739
3171
  env = withGitConfig(onePath(env), [
2740
3172
  ["core.hooksPath", fwd(hooksDir())],
2741
3173
  ["url.pitroom-push-blocked://.pushInsteadOf", ""]
@@ -2747,11 +3179,11 @@ function guardEnv(env) {
2747
3179
  if (WINDOWS) {
2748
3180
  sh = findGitSh(real);
2749
3181
  if (!sh) return { ...env, ...quiet };
2750
- writeIfChanged(path15.join(dir, "git.cmd"), cmdShim(sh));
3182
+ writeIfChanged2(path18.join(dir, "git.cmd"), cmdShim(sh));
2751
3183
  }
2752
- writeIfChanged(path15.join(dir, "git"), SHIM);
3184
+ writeIfChanged2(path18.join(dir, "git"), SHIM);
2753
3185
  const key = pathKey(env);
2754
- return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${path15.delimiter}${env[key] ?? ""}` };
3186
+ return { ...env, ...quiet, PITROOM_REAL_GIT: fwd(real), [key]: `${dir}${path18.delimiter}${env[key] ?? ""}` };
2755
3187
  }
2756
3188
  function shimReady() {
2757
3189
  const env = guardEnv({ ...process.env });
@@ -2759,11 +3191,11 @@ function shimReady() {
2759
3191
  }
2760
3192
 
2761
3193
  // 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");
3194
+ import crypto4 from "node:crypto";
3195
+ import fs20 from "node:fs";
3196
+ import path19 from "node:path";
3197
+ var slotsDir = () => path19.join(home(), "slots");
3198
+ var locksDir = () => path19.join(home(), "locks");
2767
3199
  var STARTUP_GRACE_MS = 3e4;
2768
3200
  function holderActive(runId, self) {
2769
3201
  if (!runId || runId === self) return false;
@@ -2777,23 +3209,23 @@ function holderActive(runId, self) {
2777
3209
  }
2778
3210
  }
2779
3211
  function tryClaim(file, runId) {
2780
- fs17.mkdirSync(path16.dirname(file), { recursive: true });
3212
+ fs20.mkdirSync(path19.dirname(file), { recursive: true });
2781
3213
  try {
2782
- fs17.writeFileSync(file, runId, { flag: "wx" });
3214
+ fs20.writeFileSync(file, runId, { flag: "wx" });
2783
3215
  return true;
2784
3216
  } catch (e) {
2785
3217
  if (e.code !== "EEXIST") throw e;
2786
3218
  }
2787
3219
  let owner = "";
2788
3220
  try {
2789
- owner = fs17.readFileSync(file, "utf8").trim();
3221
+ owner = fs20.readFileSync(file, "utf8").trim();
2790
3222
  } catch {
2791
3223
  }
2792
3224
  if (owner === runId) return true;
2793
3225
  if (holderActive(owner, runId)) return false;
2794
- fs17.rmSync(file, { force: true });
3226
+ fs20.rmSync(file, { force: true });
2795
3227
  try {
2796
- fs17.writeFileSync(file, runId, { flag: "wx" });
3228
+ fs20.writeFileSync(file, runId, { flag: "wx" });
2797
3229
  return true;
2798
3230
  } catch {
2799
3231
  return false;
@@ -2802,101 +3234,47 @@ function tryClaim(file, runId) {
2802
3234
  function releaseIfOwner(file, runId) {
2803
3235
  if (!file) return;
2804
3236
  try {
2805
- if (fs17.readFileSync(file, "utf8").trim() === runId) fs17.rmSync(file, { force: true });
3237
+ if (fs20.readFileSync(file, "utf8").trim() === runId) fs20.rmSync(file, { force: true });
2806
3238
  } catch {
2807
3239
  }
2808
3240
  }
2809
3241
  function tryAcquireSlot(runId, maxParallel) {
2810
3242
  for (let n = 0; n < Math.max(1, maxParallel); n++) {
2811
- const file = path16.join(slotsDir(), `slot-${n}`);
3243
+ const file = path19.join(slotsDir(), `slot-${n}`);
2812
3244
  if (tryClaim(file, runId)) return file;
2813
3245
  }
2814
3246
  return void 0;
2815
3247
  }
2816
3248
  var releaseSlot = (file, runId) => releaseIfOwner(file, runId);
2817
3249
  function slotHolders() {
2818
- if (!fs17.existsSync(slotsDir())) return [];
2819
- return fs17.readdirSync(slotsDir()).map((f) => {
3250
+ if (!fs20.existsSync(slotsDir())) return [];
3251
+ return fs20.readdirSync(slotsDir()).map((f) => {
2820
3252
  try {
2821
- return fs17.readFileSync(path16.join(slotsDir(), f), "utf8").trim();
3253
+ return fs20.readFileSync(path19.join(slotsDir(), f), "utf8").trim();
2822
3254
  } catch {
2823
3255
  return "";
2824
3256
  }
2825
- }).filter((id) => holderActive(id, ""));
3257
+ }).filter((id2) => holderActive(id2, ""));
2826
3258
  }
2827
- var lockFile = (repoRoot) => path16.join(locksDir(), `write-${crypto2.createHash("sha1").update(path16.resolve(repoRoot)).digest("hex").slice(0, 16)}`);
3259
+ var lockFile = (repoRoot) => path19.join(locksDir(), `write-${crypto4.createHash("sha1").update(path19.resolve(repoRoot)).digest("hex").slice(0, 16)}`);
2828
3260
  function acquireWriteLock(repoRoot, runId) {
2829
3261
  const file = lockFile(repoRoot);
2830
3262
  if (tryClaim(file, runId)) return;
2831
3263
  let owner = "";
2832
3264
  try {
2833
- owner = fs17.readFileSync(file, "utf8").trim();
3265
+ owner = fs20.readFileSync(file, "utf8").trim();
2834
3266
  } catch {
2835
3267
  }
2836
3268
  throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
2837
3269
  }
2838
3270
  var releaseWriteLock = (repoRoot, runId) => releaseIfOwner(lockFile(repoRoot), runId);
2839
3271
 
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
3272
  // src/core/process.ts
2895
3273
  import { spawn } from "node:child_process";
2896
- import fs19 from "node:fs";
3274
+ import fs21 from "node:fs";
2897
3275
  async function spawnWorker(inv, opts) {
2898
- const out = fs19.openSync(opts.stdoutFile, "w");
2899
- const err = fs19.openSync(opts.stderrFile, "w");
3276
+ const out = fs21.openSync(opts.stdoutFile, "w");
3277
+ const err = fs21.openSync(opts.stderrFile, "w");
2900
3278
  const res = { code: null, timedOut: false, stopped: false };
2901
3279
  const child = spawn(inv.command, inv.args, {
2902
3280
  cwd: opts.cwd,
@@ -2919,7 +3297,7 @@ async function spawnWorker(inv, opts) {
2919
3297
  }, opts.timeoutSec * 1e3);
2920
3298
  res.code = await new Promise((resolve2) => {
2921
3299
  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;
3300
+ res.spawnError = e.code === "ENOENT" ? fs21.existsSync(opts.cwd) ? `${inv.command} not found` : `the working directory ${opts.cwd} does not exist` : e.message;
2923
3301
  resolve2(127);
2924
3302
  });
2925
3303
  child.on("close", (c) => resolve2(c));
@@ -2928,13 +3306,14 @@ async function spawnWorker(inv, opts) {
2928
3306
  if (killer) clearTimeout(killer);
2929
3307
  process.off("SIGINT", stop);
2930
3308
  process.off("SIGTERM", stop);
2931
- fs19.closeSync(out);
2932
- fs19.closeSync(err);
3309
+ fs21.closeSync(out);
3310
+ fs21.closeSync(err);
2933
3311
  return res;
2934
3312
  }
2935
3313
  export {
2936
3314
  ASKS_FOR_LIST,
2937
3315
  DEFAULT_BACKEND,
3316
+ FORBIDDEN_COMMANDS,
2938
3317
  STOP_FILE,
2939
3318
  TERMINAL,
2940
3319
  acquireWriteLock,
@@ -2963,6 +3342,7 @@ export {
2963
3342
  home,
2964
3343
  isActive,
2965
3344
  isAlive,
3345
+ linkInto,
2966
3346
  listRunIds,
2967
3347
  loadEval,
2968
3348
  loadPlan,
@@ -3008,5 +3388,6 @@ export {
3008
3388
  usd,
3009
3389
  verifyRefs,
3010
3390
  workerCostOf,
3391
+ writeIfChanged,
3011
3392
  writeMeta
3012
3393
  };