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