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