pitroom 0.22.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 +41 -0
- package/README.md +39 -4
- package/dist/lib.mjs +2010 -1054
- package/dist/pitroom.mjs +2462 -1181
- package/dist/ui/app.css +1 -1
- package/dist/ui/app.js +32 -30
- package/docs/backends.md +83 -1
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/policies/gemini/shell.toml +8 -1
- package/policies/qwen/worker-defaults.json +9 -0
package/dist/lib.mjs
CHANGED
|
@@ -6,174 +6,187 @@ 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";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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;
|
|
27
40
|
}
|
|
28
|
-
|
|
29
|
-
|
|
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();
|
|
30
52
|
}
|
|
31
|
-
function
|
|
53
|
+
function findSecretFilesInTree(root, dir) {
|
|
32
54
|
try {
|
|
33
|
-
|
|
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();
|
|
34
57
|
} catch {
|
|
35
|
-
return
|
|
58
|
+
return [];
|
|
36
59
|
}
|
|
37
60
|
}
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
fs.closeSync(fd);
|
|
45
|
-
const firstLine = head.subarray(0, n).toString("utf8").split("\n")[0] ?? "";
|
|
46
|
-
if (/^#!.*\bnode\b/.test(firstLine)) return { command: process.execPath, prefix: [real] };
|
|
47
|
-
} catch {
|
|
48
|
-
}
|
|
49
|
-
return { command: bin, prefix: [] };
|
|
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`;
|
|
50
67
|
}
|
|
68
|
+
var slash = (p) => p.split(path.sep).join("/");
|
|
51
69
|
|
|
52
|
-
// src/
|
|
53
|
-
|
|
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";
|
|
54
75
|
|
|
55
|
-
// src/
|
|
56
|
-
|
|
57
|
-
var
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
input: 0,
|
|
61
|
-
output: 0,
|
|
62
|
-
reasoning: 0,
|
|
63
|
-
cacheRead: 0,
|
|
64
|
-
cacheWrite: 0,
|
|
65
|
-
total: 0,
|
|
66
|
-
steps: 0,
|
|
67
|
-
toolCalls: 0,
|
|
68
|
-
denied: 0
|
|
69
|
-
};
|
|
70
|
-
const tools = {};
|
|
71
|
-
const edits = [];
|
|
72
|
-
const pendingEdits = /* @__PURE__ */ new Map();
|
|
73
|
-
const timeline = [];
|
|
74
|
-
const stepOf = /* @__PURE__ */ new Map();
|
|
75
|
-
const steps = /* @__PURE__ */ new Set();
|
|
76
|
-
let sessionId;
|
|
77
|
-
let model;
|
|
78
|
-
let lastText = "";
|
|
79
|
-
let finalText;
|
|
80
|
-
let lastActivity;
|
|
81
|
-
let error;
|
|
82
|
-
for (const line of jsonl.split("\n")) {
|
|
83
|
-
if (!line.trim()) continue;
|
|
84
|
-
let e;
|
|
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++) {
|
|
85
81
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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);
|
|
89
87
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (step) step.ok = !c.is_error;
|
|
125
|
-
if (c.is_error) {
|
|
126
|
-
if (DENIED.test(text)) usage.denied++;
|
|
127
|
-
} else if (pendingEdits.has(c.tool_use_id)) {
|
|
128
|
-
edits.push(pendingEdits.get(c.tool_use_id));
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
} else if (e.type === "result") {
|
|
132
|
-
const u = e.usage ?? {};
|
|
133
|
-
usage.input += num(u.input_tokens);
|
|
134
|
-
usage.cacheRead += num(u.cache_read_input_tokens);
|
|
135
|
-
usage.cacheWrite += num(u.cache_creation_input_tokens);
|
|
136
|
-
usage.reasoning += num(u.output_tokens_details?.thinking_tokens);
|
|
137
|
-
usage.output += Math.max(0, num(u.output_tokens) - num(u.output_tokens_details?.thinking_tokens));
|
|
138
|
-
usage.total += num(u.input_tokens) + num(u.cache_read_input_tokens) + num(u.cache_creation_input_tokens) + num(u.output_tokens);
|
|
139
|
-
usage.cost = (usage.cost ?? 0) + num(e.total_cost_usd);
|
|
140
|
-
usage.denied = Math.max(usage.denied, Array.isArray(e.permission_denials) ? e.permission_denials.length : 0);
|
|
141
|
-
if (e.is_error) {
|
|
142
|
-
const tags = [e.terminal_reason, e.api_error_status && `HTTP ${e.api_error_status}`].filter(Boolean);
|
|
143
|
-
error ??= `${String(e.result ?? e.subtype ?? "Claude Code error")}${tags.length ? ` [${tags.join(", ")}]` : ""}`;
|
|
144
|
-
} else if (typeof e.result === "string") {
|
|
145
|
-
finalText = e.result.trim();
|
|
146
|
-
}
|
|
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 {
|
|
147
122
|
}
|
|
148
123
|
}
|
|
149
|
-
usage.steps = steps.size;
|
|
150
|
-
return { sessionId, model, finalText: finalText ?? (error ? "" : lastText), usage, tools, edits, lastActivity, timeline, error };
|
|
151
124
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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")));
|
|
155
132
|
}
|
|
156
|
-
function
|
|
157
|
-
|
|
158
|
-
|
|
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;
|
|
159
142
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
function cl(args, timeout = 6e4) {
|
|
164
|
-
const { command, prefix } = resolveCommand(binary());
|
|
165
|
-
const r = spawnSync(command, [...prefix, ...args], {
|
|
166
|
-
encoding: "utf8",
|
|
167
|
-
timeout,
|
|
168
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
169
|
-
maxBuffer: 16 * 1024 * 1024
|
|
170
|
-
});
|
|
171
|
-
return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
|
|
143
|
+
function listRunIds() {
|
|
144
|
+
if (!fs3.existsSync(runsDir())) return [];
|
|
145
|
+
return fs3.readdirSync(runsDir()).filter((d) => fs3.existsSync(runFile(d, "meta.json"))).sort();
|
|
172
146
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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 = [
|
|
177
190
|
"git commit",
|
|
178
191
|
"git push",
|
|
179
192
|
"git pull",
|
|
@@ -198,6 +211,9 @@ var DENY_BASH = [
|
|
|
198
211
|
"git add",
|
|
199
212
|
"rm -rf",
|
|
200
213
|
"rm -fr",
|
|
214
|
+
"rm -r",
|
|
215
|
+
"rm -R",
|
|
216
|
+
"rm --recursive",
|
|
201
217
|
"sudo",
|
|
202
218
|
"chown",
|
|
203
219
|
"kill",
|
|
@@ -209,119 +225,521 @@ var DENY_BASH = [
|
|
|
209
225
|
"opencode",
|
|
210
226
|
"claude",
|
|
211
227
|
"codex",
|
|
212
|
-
"gemini"
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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";
|
|
239
|
+
function findBinary(name, envVar, known = []) {
|
|
240
|
+
const override = process.env[envVar];
|
|
241
|
+
if (override) return override;
|
|
242
|
+
const exts = process.platform === "win32" ? [".exe", ".cmd", ""] : [""];
|
|
243
|
+
for (const dir of (process.env.PATH ?? "").split(path3.delimiter).filter(Boolean)) {
|
|
244
|
+
for (const ext of exts) {
|
|
245
|
+
const p = path3.join(dir, `${name}${ext}`);
|
|
246
|
+
if (isFile(p)) return p;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
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
|
+
}
|
|
217
629
|
}
|
|
218
630
|
function invocation(req) {
|
|
219
|
-
const tools = toolsFor(req.mode, req.web).join(",");
|
|
220
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.
|
|
221
633
|
"-p",
|
|
222
|
-
"
|
|
223
|
-
"--restricted",
|
|
224
|
-
"--strict-mcp-config",
|
|
225
|
-
"--tools",
|
|
226
|
-
tools,
|
|
227
|
-
"--allowedTools",
|
|
228
|
-
tools
|
|
229
|
-
];
|
|
230
|
-
if (req.mode !== "read") args.push("--disallowedTools", ...DENY_BASH);
|
|
231
|
-
args.push(
|
|
232
|
-
"--settings",
|
|
233
|
-
JSON.stringify({ permissions: { deny: SECRETS } }),
|
|
234
|
-
// Non-variadic options last, so no list above can swallow what follows.
|
|
235
|
-
"--permission-mode",
|
|
236
|
-
"dontAsk",
|
|
634
|
+
req.prompt.startsWith("-") ? ` ${req.prompt}` : req.prompt,
|
|
237
635
|
"--output-format",
|
|
238
636
|
"stream-json",
|
|
239
|
-
"--
|
|
240
|
-
|
|
241
|
-
|
|
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);
|
|
242
646
|
if (model) args.push("--model", model);
|
|
243
647
|
if (effort) args.push("--effort", effort);
|
|
244
|
-
if (req.sessionId) args.push("--resume", req.sessionId);
|
|
245
|
-
args.push("--", req.prompt);
|
|
246
648
|
const { command, prefix } = resolveCommand(binary());
|
|
247
|
-
|
|
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 };
|
|
248
653
|
}
|
|
249
654
|
function classify(message) {
|
|
250
|
-
if (/
|
|
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)) {
|
|
251
657
|
return "auth";
|
|
252
658
|
}
|
|
253
|
-
if (/
|
|
254
|
-
|
|
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
|
+
}
|
|
255
662
|
return "other";
|
|
256
663
|
}
|
|
664
|
+
var TIMED_OUT = /print timeout after/i;
|
|
665
|
+
var BLOCKED = /^This request was blocked by \S+ filters\b/;
|
|
257
666
|
function failure(run, stderr, exitCode) {
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
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) {
|
|
264
687
|
try {
|
|
265
|
-
const
|
|
266
|
-
|
|
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;
|
|
267
692
|
} catch {
|
|
268
|
-
return void 0;
|
|
693
|
+
return line.replace(/^AGY_ERROR:?\s*/, "") || void 0;
|
|
269
694
|
}
|
|
270
695
|
}
|
|
271
696
|
function doctor({ models, hasFallback }) {
|
|
272
|
-
const version =
|
|
697
|
+
const version = agy(["--version"]);
|
|
273
698
|
if (version.missing || !version.ok) {
|
|
274
|
-
return [{
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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)" });
|
|
281
713
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
);
|
|
285
|
-
for (const m of models) {
|
|
286
|
-
const model = m ?? defaultModel();
|
|
287
|
-
const pricey = model && /opus|fable/i.test(model);
|
|
288
|
-
checks.push({
|
|
289
|
-
level: pricey ? "warn" : "ok",
|
|
290
|
-
message: pricey ? `Claude Code model: ${model} is the most expensive tier for a worker; consider -W claude:haiku or claude:sonnet` : model ? `Claude Code model: ${model}` : "Claude Code model: Claude Code's default (often the largest model; a cheaper worker is -W claude:haiku)"
|
|
291
|
-
});
|
|
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" });
|
|
292
716
|
}
|
|
293
|
-
|
|
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)" });
|
|
294
720
|
return checks;
|
|
295
721
|
}
|
|
296
|
-
var
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (def && !models.some((m) => m.id === def)) models.push({ id: def, efforts: CLAUDE_EFFORTS });
|
|
301
|
-
return { models, source: "Claude Code aliases (latest of each size) and --effort levels from `claude --help`" };
|
|
302
|
-
}
|
|
303
|
-
var claude = {
|
|
304
|
-
id: "claude",
|
|
305
|
-
name: "Claude Code",
|
|
306
|
-
capabilities: { readOnly: "tool-allowlist", resume: "by-id", reportsCost: true, attachFiles: false },
|
|
722
|
+
var antigravity = {
|
|
723
|
+
id: "antigravity",
|
|
724
|
+
name: "Antigravity CLI",
|
|
725
|
+
capabilities: { readOnly: "snapshot", resume: "by-id", reportsCost: false, attachFiles: false },
|
|
307
726
|
binary,
|
|
308
|
-
catalog,
|
|
309
727
|
invocation,
|
|
310
728
|
parse: parseEvents,
|
|
311
729
|
failure,
|
|
312
|
-
|
|
730
|
+
warnings,
|
|
313
731
|
doctor
|
|
314
732
|
};
|
|
315
733
|
|
|
316
|
-
// src/backends/
|
|
734
|
+
// src/backends/claude/index.ts
|
|
317
735
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
318
|
-
import
|
|
319
|
-
import
|
|
320
|
-
import
|
|
736
|
+
import fs7 from "node:fs";
|
|
737
|
+
import os4 from "node:os";
|
|
738
|
+
import path6 from "node:path";
|
|
321
739
|
|
|
322
|
-
// src/backends/
|
|
323
|
-
var
|
|
324
|
-
var
|
|
740
|
+
// src/backends/claude/events.ts
|
|
741
|
+
var EDIT_TOOLS2 = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
742
|
+
var DENIED2 = /permission|not allowed|denied|blocked|disallowed/i;
|
|
325
743
|
function parseEvents2(jsonl) {
|
|
326
744
|
const usage = {
|
|
327
745
|
input: 0,
|
|
@@ -336,9 +754,14 @@ function parseEvents2(jsonl) {
|
|
|
336
754
|
};
|
|
337
755
|
const tools = {};
|
|
338
756
|
const edits = [];
|
|
757
|
+
const pendingEdits = /* @__PURE__ */ new Map();
|
|
339
758
|
const timeline = [];
|
|
759
|
+
const stepOf = /* @__PURE__ */ new Map();
|
|
760
|
+
const steps = /* @__PURE__ */ new Set();
|
|
340
761
|
let sessionId;
|
|
341
|
-
let
|
|
762
|
+
let model;
|
|
763
|
+
let lastText = "";
|
|
764
|
+
let finalText;
|
|
342
765
|
let lastActivity;
|
|
343
766
|
let error;
|
|
344
767
|
for (const line of jsonl.split("\n")) {
|
|
@@ -349,45 +772,262 @@ function parseEvents2(jsonl) {
|
|
|
349
772
|
} catch {
|
|
350
773
|
continue;
|
|
351
774
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (
|
|
365
|
-
|
|
366
|
-
lastActivity = `says: ${
|
|
367
|
-
if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text:
|
|
368
|
-
} else if (
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
if (
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
775
|
+
sessionId ??= e.session_id;
|
|
776
|
+
if (e.type === "system" && e.subtype === "init") {
|
|
777
|
+
model = e.model;
|
|
778
|
+
} else if (e.type === "assistant") {
|
|
779
|
+
const content = e.message?.content ?? [];
|
|
780
|
+
if (e.is_api_error_message || e.error) {
|
|
781
|
+
error = content.map((c) => c.text).filter(Boolean).join(" ") || String(e.error);
|
|
782
|
+
if (e.error) error = `${error} [${e.error}]`;
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
steps.add(String(e.message?.id ?? e.uuid ?? steps.size));
|
|
786
|
+
for (const c of content) {
|
|
787
|
+
if (c.type === "text" && String(c.text ?? "").trim()) {
|
|
788
|
+
lastText = String(c.text).trim();
|
|
789
|
+
lastActivity = `says: ${oneLine(lastText)}`;
|
|
790
|
+
if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(lastText, 600) });
|
|
791
|
+
} else if (c.type === "tool_use") {
|
|
792
|
+
usage.toolCalls++;
|
|
793
|
+
tools[c.name] = (tools[c.name] ?? 0) + 1;
|
|
794
|
+
const target = c.input?.file_path ?? c.input?.path ?? c.input?.pattern ?? c.input?.command ?? c.input?.url;
|
|
795
|
+
lastActivity = `${c.name} ${target ? oneLine(String(target), 60) : ""}`.trim();
|
|
796
|
+
if (EDIT_TOOLS2.has(c.name) && c.input?.file_path) pendingEdits.set(c.id, String(c.input.file_path));
|
|
797
|
+
if (timeline.length < MAX_STEPS) {
|
|
798
|
+
const step = { kind: EDIT_TOOLS2.has(c.name) ? "edit" : c.name === "Bash" ? "shell" : "tool", name: c.name, text: clip(String(target ?? ""), 240) };
|
|
799
|
+
timeline.push(step);
|
|
800
|
+
stepOf.set(String(c.id), step);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
} else if (e.type === "user") {
|
|
805
|
+
for (const c of e.message?.content ?? []) {
|
|
806
|
+
if (c?.type !== "tool_result") continue;
|
|
807
|
+
const text = typeof c.content === "string" ? c.content : JSON.stringify(c.content ?? "");
|
|
808
|
+
const step = stepOf.get(String(c.tool_use_id));
|
|
809
|
+
if (step) step.ok = !c.is_error;
|
|
810
|
+
if (c.is_error) {
|
|
811
|
+
if (DENIED2.test(text)) usage.denied++;
|
|
812
|
+
} else if (pendingEdits.has(c.tool_use_id)) {
|
|
813
|
+
edits.push(pendingEdits.get(c.tool_use_id));
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
} else if (e.type === "result") {
|
|
817
|
+
const u = e.usage ?? {};
|
|
818
|
+
usage.input += num(u.input_tokens);
|
|
819
|
+
usage.cacheRead += num(u.cache_read_input_tokens);
|
|
820
|
+
usage.cacheWrite += num(u.cache_creation_input_tokens);
|
|
821
|
+
usage.reasoning += num(u.output_tokens_details?.thinking_tokens);
|
|
822
|
+
usage.output += Math.max(0, num(u.output_tokens) - num(u.output_tokens_details?.thinking_tokens));
|
|
823
|
+
usage.total += num(u.input_tokens) + num(u.cache_read_input_tokens) + num(u.cache_creation_input_tokens) + num(u.output_tokens);
|
|
824
|
+
usage.cost = (usage.cost ?? 0) + num(e.total_cost_usd);
|
|
825
|
+
usage.denied = Math.max(usage.denied, Array.isArray(e.permission_denials) ? e.permission_denials.length : 0);
|
|
826
|
+
if (e.is_error) {
|
|
827
|
+
const tags = [e.terminal_reason, e.api_error_status && `HTTP ${e.api_error_status}`].filter(Boolean);
|
|
828
|
+
error ??= `${String(e.result ?? e.subtype ?? "Claude Code error")}${tags.length ? ` [${tags.join(", ")}]` : ""}`;
|
|
829
|
+
} else if (typeof e.result === "string") {
|
|
830
|
+
finalText = e.result.trim();
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
usage.steps = steps.size;
|
|
835
|
+
return { sessionId, model, finalText: finalText ?? (error ? "" : lastText), usage, tools, edits, lastActivity, timeline, error };
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// src/backends/claude/index.ts
|
|
839
|
+
var binary2 = () => findBinary("claude", "PITROOM_CLAUDE_BIN", ["~/.local/bin/claude", "~/.claude/local/claude"]);
|
|
840
|
+
function cl(args, timeout = 6e4) {
|
|
841
|
+
const { command, prefix } = resolveCommand(binary2());
|
|
842
|
+
const r = spawnSync2(command, [...prefix, ...args], {
|
|
843
|
+
encoding: "utf8",
|
|
844
|
+
timeout,
|
|
845
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
846
|
+
maxBuffer: 16 * 1024 * 1024
|
|
847
|
+
});
|
|
848
|
+
return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
|
|
849
|
+
}
|
|
850
|
+
var READ_TOOLS = ["Read", "Grep", "Glob"];
|
|
851
|
+
var WRITE_TOOLS = [...READ_TOOLS, "Edit", "Write", "Bash"];
|
|
852
|
+
var WEB_TOOLS = ["WebFetch", "WebSearch"];
|
|
853
|
+
var DENY_BASH = FORBIDDEN_COMMANDS.map((c) => `Bash(${c}:*)`);
|
|
854
|
+
var SECRETS = ["**/.env", "**/.env.*", "**/*.pem", "**/id_rsa*", "**/id_ed25519*"].map((p) => `Read(${p})`);
|
|
855
|
+
function toolsFor(mode, web2) {
|
|
856
|
+
return [...mode === "read" ? READ_TOOLS : WRITE_TOOLS, ...web2 ? WEB_TOOLS : []];
|
|
857
|
+
}
|
|
858
|
+
function invocation2(req) {
|
|
859
|
+
const tools = toolsFor(req.mode, req.web).join(",");
|
|
860
|
+
const args = [
|
|
861
|
+
"-p",
|
|
862
|
+
"--safe-mode",
|
|
863
|
+
"--restricted",
|
|
864
|
+
"--strict-mcp-config",
|
|
865
|
+
"--tools",
|
|
866
|
+
tools,
|
|
867
|
+
"--allowedTools",
|
|
868
|
+
tools
|
|
869
|
+
];
|
|
870
|
+
if (req.mode !== "read") args.push("--disallowedTools", ...DENY_BASH);
|
|
871
|
+
args.push(
|
|
872
|
+
"--settings",
|
|
873
|
+
JSON.stringify({ permissions: { deny: SECRETS } }),
|
|
874
|
+
// Non-variadic options last, so no list above can swallow what follows.
|
|
875
|
+
"--permission-mode",
|
|
876
|
+
"dontAsk",
|
|
877
|
+
"--output-format",
|
|
878
|
+
"stream-json",
|
|
879
|
+
"--verbose"
|
|
880
|
+
);
|
|
881
|
+
const [model, effort] = (req.model ?? "").split("#");
|
|
882
|
+
if (model) args.push("--model", model);
|
|
883
|
+
if (effort) args.push("--effort", effort);
|
|
884
|
+
if (req.sessionId) args.push("--resume", req.sessionId);
|
|
885
|
+
args.push("--", req.prompt);
|
|
886
|
+
const { command, prefix } = resolveCommand(binary2());
|
|
887
|
+
return { command, args: [...prefix, ...args], env: { DISABLE_AUTOUPDATER: "1" } };
|
|
888
|
+
}
|
|
889
|
+
function classify2(message) {
|
|
890
|
+
if (/authentication_failed|failed to authenticate|oauth|not logged in|\/login|invalid api key|\b40[13]\b|unauthori[sz]ed/i.test(message)) {
|
|
891
|
+
return "auth";
|
|
892
|
+
}
|
|
893
|
+
if (/rate.?limit|too many requests|\b429\b|\b529\b|overloaded|usage limit|limit reached|quota|capacity/i.test(message)) return "rate-limited";
|
|
894
|
+
if (/model[^.]*(not found|does not exist|invalid|not available)|invalid model|not_found_error/i.test(message)) return "model-unavailable";
|
|
895
|
+
return "other";
|
|
896
|
+
}
|
|
897
|
+
function failure2(run, stderr, exitCode) {
|
|
898
|
+
if (exitCode === 0 && !run.error) return void 0;
|
|
899
|
+
const detail = stderr.trim().split("\n").filter(Boolean).pop();
|
|
900
|
+
const message = run.error ?? detail ?? `claude exited with code ${exitCode}`;
|
|
901
|
+
return { kind: classify2(message), message };
|
|
902
|
+
}
|
|
903
|
+
function defaultModel() {
|
|
904
|
+
try {
|
|
905
|
+
const settings2 = JSON.parse(fs7.readFileSync(path6.join(os4.homedir(), ".claude", "settings.json"), "utf8"));
|
|
906
|
+
return typeof settings2.model === "string" ? settings2.model : void 0;
|
|
907
|
+
} catch {
|
|
908
|
+
return void 0;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
function doctor2({ models, hasFallback }) {
|
|
912
|
+
const version = cl(["--version"]);
|
|
913
|
+
if (version.missing || !version.ok) {
|
|
914
|
+
return [{ level: "fail", message: `claude not runnable (${binary2()}). Install Claude Code, or set PITROOM_CLAUDE_BIN` }];
|
|
915
|
+
}
|
|
916
|
+
const checks = [{ level: "ok", message: `Claude Code ${version.out.trim()} at ${binary2()}` }];
|
|
917
|
+
let loggedIn = false;
|
|
918
|
+
try {
|
|
919
|
+
loggedIn = JSON.parse(cl(["auth", "status"]).out).loggedIn === true;
|
|
920
|
+
} catch {
|
|
921
|
+
}
|
|
922
|
+
checks.push(
|
|
923
|
+
loggedIn || process.env.ANTHROPIC_API_KEY ? { level: "ok", message: `Claude Code: ${loggedIn ? "logged in" : "ANTHROPIC_API_KEY set"}` } : { level: "fail", message: "Claude Code is not logged in (or the session expired): run `claude auth login`" }
|
|
924
|
+
);
|
|
925
|
+
for (const m of models) {
|
|
926
|
+
const model = m ?? defaultModel();
|
|
927
|
+
const pricey = model && /opus|fable/i.test(model);
|
|
928
|
+
checks.push({
|
|
929
|
+
level: pricey ? "warn" : "ok",
|
|
930
|
+
message: pricey ? `Claude Code model: ${model} is the most expensive tier for a worker; consider -W claude:haiku or claude:sonnet` : model ? `Claude Code model: ${model}` : "Claude Code model: Claude Code's default (often the largest model; a cheaper worker is -W claude:haiku)"
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
if (!hasFallback) checks.push({ level: "warn", message: "no fallback workers configured for Claude Code runs" });
|
|
934
|
+
return checks;
|
|
935
|
+
}
|
|
936
|
+
var CLAUDE_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
937
|
+
function catalog() {
|
|
938
|
+
const def = defaultModel();
|
|
939
|
+
const models = ["haiku", "sonnet", "opus"].map((id2) => ({ id: id2, efforts: CLAUDE_EFFORTS }));
|
|
940
|
+
if (def && !models.some((m) => m.id === def)) models.push({ id: def, efforts: CLAUDE_EFFORTS });
|
|
941
|
+
return { models, source: "Claude Code aliases (latest of each size) and --effort levels from `claude --help`" };
|
|
942
|
+
}
|
|
943
|
+
var claude = {
|
|
944
|
+
id: "claude",
|
|
945
|
+
name: "Claude Code",
|
|
946
|
+
capabilities: { readOnly: "tool-allowlist", resume: "by-id", reportsCost: true, attachFiles: false },
|
|
947
|
+
binary: binary2,
|
|
948
|
+
catalog,
|
|
949
|
+
invocation: invocation2,
|
|
950
|
+
parse: parseEvents2,
|
|
951
|
+
failure: failure2,
|
|
952
|
+
defaultModel,
|
|
953
|
+
doctor: doctor2
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
// src/backends/codex/index.ts
|
|
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";
|
|
961
|
+
|
|
962
|
+
// src/backends/codex/events.ts
|
|
963
|
+
var WORK = /* @__PURE__ */ new Set(["agent_message", "reasoning", "command_execution", "file_change", "mcp_tool_call", "web_search", "todo_list"]);
|
|
964
|
+
var TOOLS = /* @__PURE__ */ new Set(["command_execution", "file_change", "mcp_tool_call", "web_search"]);
|
|
965
|
+
function parseEvents3(jsonl) {
|
|
966
|
+
const usage = {
|
|
967
|
+
input: 0,
|
|
968
|
+
output: 0,
|
|
969
|
+
reasoning: 0,
|
|
970
|
+
cacheRead: 0,
|
|
971
|
+
cacheWrite: 0,
|
|
972
|
+
total: 0,
|
|
973
|
+
steps: 0,
|
|
974
|
+
toolCalls: 0,
|
|
975
|
+
denied: 0
|
|
976
|
+
};
|
|
977
|
+
const tools = {};
|
|
978
|
+
const edits = [];
|
|
979
|
+
const timeline = [];
|
|
980
|
+
let sessionId;
|
|
981
|
+
let finalText = "";
|
|
982
|
+
let lastActivity;
|
|
983
|
+
let error;
|
|
984
|
+
for (const line of jsonl.split("\n")) {
|
|
985
|
+
if (!line.trim()) continue;
|
|
986
|
+
let e;
|
|
987
|
+
try {
|
|
988
|
+
e = JSON.parse(line);
|
|
989
|
+
} catch {
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
switch (e.type) {
|
|
993
|
+
case "thread.started":
|
|
994
|
+
sessionId ??= e.thread_id;
|
|
995
|
+
break;
|
|
996
|
+
case "item.completed": {
|
|
997
|
+
const it = e.item ?? {};
|
|
998
|
+
if (!WORK.has(it.type)) break;
|
|
999
|
+
usage.steps++;
|
|
1000
|
+
if (TOOLS.has(it.type)) {
|
|
1001
|
+
usage.toolCalls++;
|
|
1002
|
+
tools[it.type] = (tools[it.type] ?? 0) + 1;
|
|
1003
|
+
}
|
|
1004
|
+
if (it.type === "agent_message" && String(it.text ?? "").trim()) {
|
|
1005
|
+
finalText = String(it.text).trim();
|
|
1006
|
+
lastActivity = `says: ${oneLine(finalText)}`;
|
|
1007
|
+
if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(finalText, 600) });
|
|
1008
|
+
} else if (it.type === "command_execution") {
|
|
1009
|
+
if (it.status === "declined" || /operation not permitted|sandbox/i.test(String(it.aggregated_output ?? ""))) usage.denied++;
|
|
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 });
|
|
1012
|
+
} else if (it.type === "file_change") {
|
|
1013
|
+
const paths = (it.changes ?? []).map((c) => String(c.path));
|
|
1014
|
+
if (it.status !== "failed" && it.status !== "declined") edits.push(...paths);
|
|
1015
|
+
else usage.denied++;
|
|
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" });
|
|
1018
|
+
}
|
|
1019
|
+
break;
|
|
1020
|
+
}
|
|
381
1021
|
case "turn.completed": {
|
|
382
1022
|
const u = e.usage ?? {};
|
|
383
|
-
const cached3 =
|
|
384
|
-
const reasoning =
|
|
385
|
-
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);
|
|
386
1026
|
usage.cacheRead += cached3;
|
|
387
|
-
usage.cacheWrite +=
|
|
388
|
-
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);
|
|
389
1029
|
usage.reasoning += reasoning;
|
|
390
|
-
usage.total +=
|
|
1030
|
+
usage.total += num(u.input_tokens) + num(u.output_tokens);
|
|
391
1031
|
break;
|
|
392
1032
|
}
|
|
393
1033
|
case "error":
|
|
@@ -411,21 +1051,13 @@ function describe(raw) {
|
|
|
411
1051
|
return s;
|
|
412
1052
|
}
|
|
413
1053
|
}
|
|
414
|
-
function num2(v) {
|
|
415
|
-
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
416
|
-
}
|
|
417
|
-
function oneLine2(s, max = 80) {
|
|
418
|
-
const t = s.replace(/\s+/g, " ").trim();
|
|
419
|
-
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
420
|
-
}
|
|
421
|
-
var clip2 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
422
1054
|
|
|
423
1055
|
// src/backends/codex/index.ts
|
|
424
|
-
var
|
|
425
|
-
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");
|
|
426
1058
|
function cx(args, timeout = 6e4) {
|
|
427
|
-
const { command, prefix } = resolveCommand(
|
|
428
|
-
const r =
|
|
1059
|
+
const { command, prefix } = resolveCommand(binary3());
|
|
1060
|
+
const r = spawnSync3(command, [...prefix, ...args], {
|
|
429
1061
|
encoding: "utf8",
|
|
430
1062
|
timeout,
|
|
431
1063
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -435,12 +1067,7 @@ function cx(args, timeout = 6e4) {
|
|
|
435
1067
|
}
|
|
436
1068
|
var SANDBOX = { read: "read-only", write: "workspace-write", isolate: "workspace-write" };
|
|
437
1069
|
var toml = (s) => JSON.stringify(s);
|
|
438
|
-
function
|
|
439
|
-
if (!model) return {};
|
|
440
|
-
const [m, effort] = model.split("#");
|
|
441
|
-
return { model: m || void 0, effort: effort || void 0 };
|
|
442
|
-
}
|
|
443
|
-
function invocation2(req) {
|
|
1070
|
+
function invocation3(req) {
|
|
444
1071
|
const { model, effort } = splitModel(req.model);
|
|
445
1072
|
const args = ["exec"];
|
|
446
1073
|
if (req.sessionId) args.push("resume", req.sessionId);
|
|
@@ -457,10 +1084,10 @@ function invocation2(req) {
|
|
|
457
1084
|
if (model) args.push("--model", model);
|
|
458
1085
|
if (effort) args.push("-c", `model_reasoning_effort=${toml(effort)}`);
|
|
459
1086
|
args.push(req.prompt);
|
|
460
|
-
const { command, prefix } = resolveCommand(
|
|
1087
|
+
const { command, prefix } = resolveCommand(binary3());
|
|
461
1088
|
return { command, args: [...prefix, ...args], env: {} };
|
|
462
1089
|
}
|
|
463
|
-
function
|
|
1090
|
+
function classify3(message) {
|
|
464
1091
|
if (/not supported|model[^.]*(not found|does not exist|unknown|unavailable)|unsupported model|no such model/i.test(message)) {
|
|
465
1092
|
return "model-unavailable";
|
|
466
1093
|
}
|
|
@@ -468,37 +1095,37 @@ function classify2(message) {
|
|
|
468
1095
|
if (/unauthori[sz]ed|\b40[13]\b|not logged in|log ?in|api.?key|credits?\b|billing|subscription/i.test(message)) return "auth";
|
|
469
1096
|
return "other";
|
|
470
1097
|
}
|
|
471
|
-
function
|
|
1098
|
+
function failure3(run, stderr, exitCode) {
|
|
472
1099
|
if (exitCode === 0 && !run.error) return void 0;
|
|
473
1100
|
const detail = stderr.split("\n").find((l) => /^Error[: ]|ERROR/.test(l) && !/rmcp|models cache/.test(l));
|
|
474
1101
|
let message = run.error ?? (detail ? detail.trim() : `codex exited with code ${exitCode}`);
|
|
475
1102
|
if (/not supported when using Codex/i.test(message)) message += " (an outdated Codex CLI says this too: npm i -g @openai/codex@latest)";
|
|
476
|
-
return { kind:
|
|
1103
|
+
return { kind: classify3(message), message };
|
|
477
1104
|
}
|
|
478
1105
|
function resolveModel(sessionId) {
|
|
479
|
-
const root =
|
|
480
|
-
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"));
|
|
481
1108
|
const now = /* @__PURE__ */ new Date();
|
|
482
1109
|
for (const dir of [day(now), day(new Date(now.getTime() - 864e5))]) {
|
|
483
1110
|
let files = [];
|
|
484
1111
|
try {
|
|
485
|
-
files =
|
|
1112
|
+
files = fs8.readdirSync(dir).filter((f) => f.includes(sessionId));
|
|
486
1113
|
} catch {
|
|
487
1114
|
continue;
|
|
488
1115
|
}
|
|
489
1116
|
for (const f of files) {
|
|
490
|
-
const m = /"model":"([^"]+)"/.exec(
|
|
1117
|
+
const m = /"model":"([^"]+)"/.exec(fs8.readFileSync(path7.join(dir, f), "utf8"));
|
|
491
1118
|
if (m) return m[1];
|
|
492
1119
|
}
|
|
493
1120
|
}
|
|
494
1121
|
return void 0;
|
|
495
1122
|
}
|
|
496
|
-
function
|
|
1123
|
+
function doctor3({ models, hasFallback }) {
|
|
497
1124
|
const version = cx(["--version"]);
|
|
498
1125
|
if (version.missing || !version.ok) {
|
|
499
|
-
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` }];
|
|
500
1127
|
}
|
|
501
|
-
const checks = [{ level: "ok", message: `${version.out.trim()} at ${
|
|
1128
|
+
const checks = [{ level: "ok", message: `${version.out.trim()} at ${binary3()}` }];
|
|
502
1129
|
const login = cx(["login", "status"]);
|
|
503
1130
|
checks.push(
|
|
504
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`" }
|
|
@@ -514,9 +1141,9 @@ function doctor2({ models, hasFallback }) {
|
|
|
514
1141
|
return checks;
|
|
515
1142
|
}
|
|
516
1143
|
function catalog2() {
|
|
517
|
-
const file =
|
|
1144
|
+
const file = path7.join(codexHome(), "models_cache.json");
|
|
518
1145
|
try {
|
|
519
|
-
const raw = JSON.parse(
|
|
1146
|
+
const raw = JSON.parse(fs8.readFileSync(file, "utf8"));
|
|
520
1147
|
const models = (raw.models ?? []).filter((m) => m.visibility !== "hide" && typeof m.slug === "string").map((m) => ({
|
|
521
1148
|
id: m.slug,
|
|
522
1149
|
efforts: (m.supported_reasoning_levels ?? []).map(
|
|
@@ -534,143 +1161,25 @@ var codex = {
|
|
|
534
1161
|
name: "Codex",
|
|
535
1162
|
catalog: catalog2,
|
|
536
1163
|
capabilities: { readOnly: "os-sandbox", resume: "by-id", reportsCost: false, attachFiles: false },
|
|
537
|
-
binary:
|
|
538
|
-
invocation:
|
|
539
|
-
parse:
|
|
540
|
-
failure:
|
|
1164
|
+
binary: binary3,
|
|
1165
|
+
invocation: invocation3,
|
|
1166
|
+
parse: parseEvents3,
|
|
1167
|
+
failure: failure3,
|
|
541
1168
|
resolveModel,
|
|
542
|
-
doctor:
|
|
1169
|
+
doctor: doctor3
|
|
543
1170
|
};
|
|
544
1171
|
|
|
545
1172
|
// src/backends/gemini/index.ts
|
|
546
|
-
import { spawnSync as
|
|
547
|
-
import
|
|
548
|
-
import
|
|
549
|
-
import
|
|
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";
|
|
550
1177
|
import { fileURLToPath } from "node:url";
|
|
551
1178
|
|
|
552
|
-
// src/core/store.ts
|
|
553
|
-
import fs5 from "node:fs";
|
|
554
|
-
import os4 from "node:os";
|
|
555
|
-
import path4 from "node:path";
|
|
556
|
-
import crypto from "node:crypto";
|
|
557
|
-
|
|
558
|
-
// src/core/fs-atomic.ts
|
|
559
|
-
import fs4 from "node:fs";
|
|
560
|
-
var BUSY = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
561
|
-
function renameOver(tmp, file) {
|
|
562
|
-
for (let attempt = 0; ; attempt++) {
|
|
563
|
-
try {
|
|
564
|
-
fs4.renameSync(tmp, file);
|
|
565
|
-
return;
|
|
566
|
-
} catch (e) {
|
|
567
|
-
if (process.platform !== "win32" || !BUSY.has(e.code ?? "") || attempt >= 40) throw e;
|
|
568
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// src/core/store.ts
|
|
574
|
-
var TERMINAL = ["done", "failed", "timeout", "stopped"];
|
|
575
|
-
var isActive = (s) => !TERMINAL.includes(s);
|
|
576
|
-
function home() {
|
|
577
|
-
if (process.env.PITROOM_HOME) return path4.resolve(process.env.PITROOM_HOME);
|
|
578
|
-
if (process.platform === "win32") {
|
|
579
|
-
return path4.join(process.env.LOCALAPPDATA ?? path4.join(os4.homedir(), "AppData", "Local"), "pitroom");
|
|
580
|
-
}
|
|
581
|
-
return path4.join(process.env.XDG_STATE_HOME ?? path4.join(os4.homedir(), ".local", "state"), "pitroom");
|
|
582
|
-
}
|
|
583
|
-
var runsDir = () => path4.join(home(), "runs");
|
|
584
|
-
var ledgerFile = () => path4.join(home(), "ledger.jsonl");
|
|
585
|
-
var runDir = (id) => path4.join(runsDir(), id);
|
|
586
|
-
var runFile = (id, name) => path4.join(runDir(id), name);
|
|
587
|
-
function newRunId(now = /* @__PURE__ */ new Date()) {
|
|
588
|
-
const p = (n) => String(n).padStart(2, "0");
|
|
589
|
-
const stamp2 = `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
|
|
590
|
-
return `${stamp2}-${crypto.randomBytes(2).toString("hex")}`;
|
|
591
|
-
}
|
|
592
|
-
var onFinished;
|
|
593
|
-
var archive;
|
|
594
|
-
function writeMeta(meta) {
|
|
595
|
-
fs5.mkdirSync(runDir(meta.id), { recursive: true });
|
|
596
|
-
const file = runFile(meta.id, "meta.json");
|
|
597
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
598
|
-
fs5.writeFileSync(tmp, JSON.stringify(meta, null, 2));
|
|
599
|
-
renameOver(tmp, file);
|
|
600
|
-
if (TERMINAL.includes(meta.state)) {
|
|
601
|
-
try {
|
|
602
|
-
onFinished?.(meta);
|
|
603
|
-
} catch {
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
function readMeta(id) {
|
|
608
|
-
const file = runFile(id, "meta.json");
|
|
609
|
-
if (!fs5.existsSync(file)) {
|
|
610
|
-
const kept = archive?.meta(id);
|
|
611
|
-
if (kept) return upgrade(kept);
|
|
612
|
-
}
|
|
613
|
-
return upgrade(JSON.parse(fs5.readFileSync(file, "utf8")));
|
|
614
|
-
}
|
|
615
|
-
function upgrade(raw) {
|
|
616
|
-
if (!raw.worker) raw.worker = raw.model ? { backend: "opencode", model: raw.model } : { backend: "opencode" };
|
|
617
|
-
if (!Array.isArray(raw.fallback)) {
|
|
618
|
-
raw.fallback = (raw.fallbackModels ?? []).map((m) => ({ backend: "opencode", model: m }));
|
|
619
|
-
}
|
|
620
|
-
raw.warnings ??= [];
|
|
621
|
-
raw.files ??= [];
|
|
622
|
-
raw.link ??= [];
|
|
623
|
-
return raw;
|
|
624
|
-
}
|
|
625
|
-
function listRunIds() {
|
|
626
|
-
if (!fs5.existsSync(runsDir())) return [];
|
|
627
|
-
return fs5.readdirSync(runsDir()).filter((d) => fs5.existsSync(runFile(d, "meta.json"))).sort();
|
|
628
|
-
}
|
|
629
|
-
function resolveRun(ref) {
|
|
630
|
-
const ids = listRunIds();
|
|
631
|
-
if (!ref || ref === "latest" || ref === "last") {
|
|
632
|
-
if (!ids.length) throw new UserError("no runs yet");
|
|
633
|
-
return ids[ids.length - 1];
|
|
634
|
-
}
|
|
635
|
-
if (ids.includes(ref)) return ref;
|
|
636
|
-
const hits = ids.filter((id) => id.startsWith(ref) || id.endsWith(ref));
|
|
637
|
-
if (hits.length === 1) return hits[0];
|
|
638
|
-
if (!hits.length) {
|
|
639
|
-
const kept = archive?.id(ref);
|
|
640
|
-
if (kept) return kept;
|
|
641
|
-
}
|
|
642
|
-
throw new UserError(hits.length ? `ambiguous run "${ref}": ${hits.join(", ")}` : ids.length ? `unknown run "${ref}"` : "no runs yet");
|
|
643
|
-
}
|
|
644
|
-
function isAlive(pid) {
|
|
645
|
-
if (!pid) return false;
|
|
646
|
-
try {
|
|
647
|
-
process.kill(pid, 0);
|
|
648
|
-
return true;
|
|
649
|
-
} catch (e) {
|
|
650
|
-
return e.code === "EPERM";
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
var STOP_FILE = "stop-requested";
|
|
654
|
-
var requestStop = (id) => fs5.writeFileSync(runFile(id, STOP_FILE), "");
|
|
655
|
-
function freshMeta(id) {
|
|
656
|
-
const meta = readMeta(id);
|
|
657
|
-
if (!TERMINAL.includes(meta.state) && meta.pid && !isAlive(meta.pid)) {
|
|
658
|
-
const latest = readMeta(id);
|
|
659
|
-
if (TERMINAL.includes(latest.state)) return latest;
|
|
660
|
-
const stopped = latest.stopRequested || fs5.existsSync(runFile(id, STOP_FILE));
|
|
661
|
-
latest.state = stopped ? "stopped" : "failed";
|
|
662
|
-
if (!stopped) latest.error ??= "worker process exited unexpectedly";
|
|
663
|
-
latest.endedAt ??= (/* @__PURE__ */ new Date()).toISOString();
|
|
664
|
-
writeMeta(latest);
|
|
665
|
-
return latest;
|
|
666
|
-
}
|
|
667
|
-
return meta;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
1179
|
// src/backends/gemini/events.ts
|
|
671
|
-
var
|
|
672
|
-
var
|
|
673
|
-
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) {
|
|
674
1183
|
const usage = {
|
|
675
1184
|
input: 0,
|
|
676
1185
|
output: 0,
|
|
@@ -700,8 +1209,8 @@ function parseEvents3(jsonl) {
|
|
|
700
1209
|
text = "";
|
|
701
1210
|
if (!said) return;
|
|
702
1211
|
lastText = said;
|
|
703
|
-
lastActivity = `says: ${
|
|
704
|
-
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) });
|
|
705
1214
|
};
|
|
706
1215
|
const startTurn = () => {
|
|
707
1216
|
if (!inTurn) turns++;
|
|
@@ -733,11 +1242,11 @@ function parseEvents3(jsonl) {
|
|
|
733
1242
|
usage.toolCalls++;
|
|
734
1243
|
tools[name] = (tools[name] ?? 0) + 1;
|
|
735
1244
|
const target = p.file_path ?? p.absolute_path ?? p.path ?? p.dir_path ?? p.pattern ?? p.command ?? p.query ?? p.url ?? p.prompt;
|
|
736
|
-
lastActivity = `${name} ${target ?
|
|
1245
|
+
lastActivity = `${name} ${target ? oneLine(String(target), 60) : ""}`.trim();
|
|
737
1246
|
const file = p.file_path ?? p.absolute_path ?? p.path;
|
|
738
|
-
if (
|
|
1247
|
+
if (EDIT_TOOLS3.has(name) && file) pendingEdits.set(String(e.tool_id), String(file));
|
|
739
1248
|
if (timeline.length < MAX_STEPS) {
|
|
740
|
-
const step = { kind:
|
|
1249
|
+
const step = { kind: EDIT_TOOLS3.has(name) ? "edit" : name === "run_shell_command" ? "shell" : "tool", name, text: clip(String(target ?? ""), 240) };
|
|
741
1250
|
timeline.push(step);
|
|
742
1251
|
stepOf.set(String(e.tool_id), step);
|
|
743
1252
|
}
|
|
@@ -746,18 +1255,18 @@ function parseEvents3(jsonl) {
|
|
|
746
1255
|
const ok = e.status === "success";
|
|
747
1256
|
const step = stepOf.get(String(e.tool_id));
|
|
748
1257
|
if (step) step.ok = ok;
|
|
749
|
-
if (!ok &&
|
|
1258
|
+
if (!ok && DENIED3.test(`${e.error?.type ?? ""} ${e.error?.message ?? ""}`)) usage.denied++;
|
|
750
1259
|
if (ok && pendingEdits.has(String(e.tool_id))) edits.push(pendingEdits.get(String(e.tool_id)));
|
|
751
1260
|
} else if (e.type === "error") {
|
|
752
1261
|
if (e.severity === "error") error ??= String(e.message ?? "Gemini CLI error");
|
|
753
1262
|
} else if (e.type === "result") {
|
|
754
1263
|
const s = e.stats ?? {};
|
|
755
|
-
const cached3 =
|
|
1264
|
+
const cached3 = num(s.cached);
|
|
756
1265
|
usage.cacheRead += cached3;
|
|
757
|
-
usage.input += s.input !== void 0 ?
|
|
758
|
-
usage.output +=
|
|
759
|
-
usage.total +=
|
|
760
|
-
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));
|
|
761
1270
|
model ??= Object.keys(s.models ?? {})[0];
|
|
762
1271
|
if (e.status === "error") error ??= String(e.error?.message ?? e.error?.type ?? "Gemini CLI error");
|
|
763
1272
|
}
|
|
@@ -766,20 +1275,12 @@ function parseEvents3(jsonl) {
|
|
|
766
1275
|
usage.steps = turns;
|
|
767
1276
|
return { sessionId, model, finalText: error ? "" : lastText, usage, tools, edits, lastActivity, timeline, error };
|
|
768
1277
|
}
|
|
769
|
-
var clip3 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
770
|
-
function num3(v) {
|
|
771
|
-
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
772
|
-
}
|
|
773
|
-
function oneLine3(s, max = 80) {
|
|
774
|
-
const t = s.replace(/\s+/g, " ").trim();
|
|
775
|
-
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
776
|
-
}
|
|
777
1278
|
|
|
778
1279
|
// src/backends/gemini/index.ts
|
|
779
|
-
var
|
|
1280
|
+
var binary4 = () => findBinary("gemini", "PITROOM_GEMINI_BIN", ["~/.local/bin/gemini"]);
|
|
780
1281
|
function gem(args, timeout = 6e4) {
|
|
781
|
-
const { command, prefix } = resolveCommand(
|
|
782
|
-
const r =
|
|
1282
|
+
const { command, prefix } = resolveCommand(binary4());
|
|
1283
|
+
const r = spawnSync4(command, [...prefix, ...args], {
|
|
783
1284
|
encoding: "utf8",
|
|
784
1285
|
timeout,
|
|
785
1286
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -787,32 +1288,19 @@ function gem(args, timeout = 6e4) {
|
|
|
787
1288
|
});
|
|
788
1289
|
return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
|
|
789
1290
|
}
|
|
790
|
-
var policyDir = () =>
|
|
791
|
-
function
|
|
792
|
-
const root =
|
|
793
|
-
const dir =
|
|
794
|
-
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"));
|
|
795
1296
|
const auth = settings().security?.auth;
|
|
796
1297
|
const body = `${JSON.stringify(auth ? { ...base, security: { ...base.security, auth } } : base, null, 2)}
|
|
797
1298
|
`;
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
if (fs6.readFileSync(file, "utf8") === body) return root;
|
|
801
|
-
} catch {
|
|
802
|
-
}
|
|
803
|
-
fs6.mkdirSync(dir, { recursive: true });
|
|
804
|
-
fs6.writeFileSync(file, body);
|
|
805
|
-
for (const name of ["oauth_creds.json", "google_accounts.json"]) {
|
|
806
|
-
const from = path5.join(geminiHome(), name);
|
|
807
|
-
const to = path5.join(dir, name);
|
|
808
|
-
try {
|
|
809
|
-
if (fs6.existsSync(from) && !fs6.existsSync(to)) fs6.symlinkSync(from, to);
|
|
810
|
-
} catch {
|
|
811
|
-
}
|
|
812
|
-
}
|
|
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));
|
|
813
1301
|
return root;
|
|
814
1302
|
}
|
|
815
|
-
function
|
|
1303
|
+
function invocation4(req) {
|
|
816
1304
|
const dir = policyDir();
|
|
817
1305
|
const policies = ["base.toml", ...req.web ? [] : ["no-web.toml"], ...req.mode === "read" ? [] : ["shell.toml"]];
|
|
818
1306
|
const args = [
|
|
@@ -826,18 +1314,18 @@ function invocation3(req) {
|
|
|
826
1314
|
"-e",
|
|
827
1315
|
"none"
|
|
828
1316
|
];
|
|
829
|
-
for (const p of policies) args.push("--policy",
|
|
1317
|
+
for (const p of policies) args.push("--policy", path8.join(dir, p));
|
|
830
1318
|
if (req.sessionId) args.push("--resume", req.sessionId);
|
|
831
1319
|
if (process.env.PITROOM_GEMINI_TRUST === "1") args.push("--skip-trust");
|
|
832
1320
|
const [model] = (req.model ?? "").split("#");
|
|
833
1321
|
if (model) args.push("--model", model);
|
|
834
|
-
const { command, prefix } = resolveCommand(
|
|
835
|
-
const env = { GEMINI_CLI_HOME:
|
|
836
|
-
const trusted =
|
|
837
|
-
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;
|
|
838
1326
|
return { command, args: [...prefix, ...args], env };
|
|
839
1327
|
}
|
|
840
|
-
function
|
|
1328
|
+
function classify4(message) {
|
|
841
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)) {
|
|
842
1330
|
return "auth";
|
|
843
1331
|
}
|
|
@@ -846,20 +1334,20 @@ function classify3(message) {
|
|
|
846
1334
|
return "other";
|
|
847
1335
|
}
|
|
848
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)";
|
|
849
|
-
function
|
|
1337
|
+
function failure4(run, stderr, exitCode) {
|
|
850
1338
|
if (exitCode === 0 && !run.error) return void 0;
|
|
851
1339
|
const lines = stderr.trim().split("\n").map((l) => l.trim()).filter(Boolean);
|
|
852
1340
|
const detail = lines.find((l) => /error|failed|exceeded|quota|exhausted|unsupported/i.test(l) && !/^at /.test(l)) ?? lines.at(-1);
|
|
853
1341
|
const raw = run.error ?? detail ?? `gemini exited with code ${exitCode}`;
|
|
854
1342
|
const message = /not running in a trusted directory/i.test(`${stderr} ${raw}`) ? UNTRUSTED : raw;
|
|
855
|
-
return { kind:
|
|
1343
|
+
return { kind: classify4(message), message };
|
|
856
1344
|
}
|
|
857
1345
|
function geminiHome() {
|
|
858
|
-
return
|
|
1346
|
+
return path8.join(process.env.GEMINI_CLI_HOME ?? os6.homedir(), ".gemini");
|
|
859
1347
|
}
|
|
860
1348
|
function settings() {
|
|
861
1349
|
try {
|
|
862
|
-
return JSON.parse(
|
|
1350
|
+
return JSON.parse(fs9.readFileSync(path8.join(geminiHome(), "settings.json"), "utf8"));
|
|
863
1351
|
} catch {
|
|
864
1352
|
return {};
|
|
865
1353
|
}
|
|
@@ -870,15 +1358,15 @@ function defaultModel2() {
|
|
|
870
1358
|
const name = typeof m === "string" ? m : m?.name;
|
|
871
1359
|
return typeof name === "string" ? name : void 0;
|
|
872
1360
|
}
|
|
873
|
-
function
|
|
1361
|
+
function doctor4({ models, hasFallback }) {
|
|
874
1362
|
const version = gem(["--version"]);
|
|
875
1363
|
if (version.missing || !version.ok) {
|
|
876
|
-
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` }];
|
|
877
1365
|
}
|
|
878
|
-
const checks = [{ level: "ok", message: `Gemini CLI ${version.out.trim()} at ${
|
|
1366
|
+
const checks = [{ level: "ok", message: `Gemini CLI ${version.out.trim()} at ${binary4()}` }];
|
|
879
1367
|
const key = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
880
1368
|
const vertex = process.env.GOOGLE_GENAI_USE_VERTEXAI === "true" || process.env.GOOGLE_GENAI_USE_VERTEXAI === "1";
|
|
881
|
-
const signedIn =
|
|
1369
|
+
const signedIn = fs9.existsSync(path8.join(geminiHome(), "oauth_creds.json"));
|
|
882
1370
|
const type = settings().security?.auth?.selectedType;
|
|
883
1371
|
if (key || vertex) {
|
|
884
1372
|
checks.push({ level: "ok", message: `Gemini CLI: ${key ? "an API key is set" : "Vertex AI is set up"}` });
|
|
@@ -907,7 +1395,7 @@ function doctor3({ models, hasFallback }) {
|
|
|
907
1395
|
}
|
|
908
1396
|
function catalog3() {
|
|
909
1397
|
const def = defaultModel2();
|
|
910
|
-
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 }));
|
|
911
1399
|
if (def && !models.some((m) => m.id === def)) models.push({ id: def });
|
|
912
1400
|
return { models, source: "Gemini CLI's built-in model names (it cannot list models); the effort suffix is not used" };
|
|
913
1401
|
}
|
|
@@ -915,22 +1403,26 @@ var gemini = {
|
|
|
915
1403
|
id: "gemini",
|
|
916
1404
|
name: "Gemini CLI",
|
|
917
1405
|
capabilities: { readOnly: "approval-mode", resume: "by-id", reportsCost: false, attachFiles: false },
|
|
918
|
-
binary:
|
|
1406
|
+
binary: binary4,
|
|
919
1407
|
catalog: catalog3,
|
|
920
|
-
invocation:
|
|
921
|
-
parse:
|
|
922
|
-
failure:
|
|
1408
|
+
invocation: invocation4,
|
|
1409
|
+
parse: parseEvents4,
|
|
1410
|
+
failure: failure4,
|
|
923
1411
|
defaultModel: defaultModel2,
|
|
924
|
-
doctor:
|
|
1412
|
+
doctor: doctor4
|
|
925
1413
|
};
|
|
926
1414
|
|
|
1415
|
+
// src/backends/kilo/index.ts
|
|
1416
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
1417
|
+
import path10 from "node:path";
|
|
1418
|
+
|
|
927
1419
|
// src/backends/opencode/index.ts
|
|
928
|
-
import { spawnSync as
|
|
1420
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
929
1421
|
|
|
930
1422
|
// src/backends/opencode/events.ts
|
|
931
|
-
var
|
|
932
|
-
var
|
|
933
|
-
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) {
|
|
934
1426
|
const usage = {
|
|
935
1427
|
input: 0,
|
|
936
1428
|
output: 0,
|
|
@@ -967,8 +1459,8 @@ function parseEvents4(ndjson) {
|
|
|
967
1459
|
const key = String(p.messageID ?? "");
|
|
968
1460
|
if (!textByMessage.has(key)) textByMessage.set(key, []);
|
|
969
1461
|
textByMessage.get(key).push(text);
|
|
970
|
-
lastActivity = `says: ${
|
|
971
|
-
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) });
|
|
972
1464
|
break;
|
|
973
1465
|
}
|
|
974
1466
|
case "tool_use": {
|
|
@@ -976,28 +1468,28 @@ function parseEvents4(ndjson) {
|
|
|
976
1468
|
const st = p.state ?? {};
|
|
977
1469
|
usage.toolCalls++;
|
|
978
1470
|
tools[name] = (tools[name] ?? 0) + 1;
|
|
979
|
-
if (st.status === "error" &&
|
|
980
|
-
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)) {
|
|
981
1473
|
edits.push(String(st.input?.filePath ?? st.input?.path ?? name));
|
|
982
1474
|
}
|
|
983
1475
|
lastActivity = `${name} ${describe2(st.input)}`.trim();
|
|
984
1476
|
if (timeline.length < MAX_STEPS) {
|
|
985
|
-
const
|
|
986
|
-
const text =
|
|
987
|
-
timeline.push({ kind, name, text:
|
|
1477
|
+
const kind2 = EDIT_TOOLS4.has(name) ? "edit" : /^(bash|shell)$/.test(name) ? "shell" : "tool";
|
|
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);
|
|
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) });
|
|
988
1480
|
}
|
|
989
1481
|
break;
|
|
990
1482
|
}
|
|
991
1483
|
case "step_finish": {
|
|
992
1484
|
const t = p.tokens ?? {};
|
|
993
1485
|
usage.steps++;
|
|
994
|
-
usage.input +=
|
|
995
|
-
usage.output +=
|
|
996
|
-
usage.reasoning +=
|
|
997
|
-
usage.cacheRead +=
|
|
998
|
-
usage.cacheWrite +=
|
|
999
|
-
usage.total +=
|
|
1000
|
-
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);
|
|
1001
1493
|
break;
|
|
1002
1494
|
}
|
|
1003
1495
|
case "error": {
|
|
@@ -1021,25 +1513,17 @@ function describeError(err) {
|
|
|
1021
1513
|
const tags = [err.type, inner, err.status && `HTTP ${err.status}`].filter(Boolean);
|
|
1022
1514
|
return tags.length ? `${message} [${tags.join(", ")}]` : message;
|
|
1023
1515
|
}
|
|
1024
|
-
var clip4 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
1025
1516
|
var stamp = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
1026
|
-
function num4(v) {
|
|
1027
|
-
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
1028
|
-
}
|
|
1029
|
-
function oneLine4(s, max = 80) {
|
|
1030
|
-
const t = s.replace(/\s+/g, " ").trim();
|
|
1031
|
-
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
1032
|
-
}
|
|
1033
1517
|
function describe2(input) {
|
|
1034
1518
|
if (!input || typeof input !== "object") return "";
|
|
1035
1519
|
const v = input.filePath ?? input.path ?? input.pattern ?? input.command ?? input.url ?? input.query;
|
|
1036
|
-
return v ?
|
|
1520
|
+
return v ? oneLine(String(v), 60) : "";
|
|
1037
1521
|
}
|
|
1038
1522
|
|
|
1039
1523
|
// src/backends/opencode/profiles.ts
|
|
1040
|
-
import
|
|
1041
|
-
import
|
|
1042
|
-
import
|
|
1524
|
+
import fs10 from "node:fs";
|
|
1525
|
+
import os7 from "node:os";
|
|
1526
|
+
import path9 from "node:path";
|
|
1043
1527
|
var AGENT = { read: "pitroom-read", write: "pitroom-write" };
|
|
1044
1528
|
var READ_FILES = {
|
|
1045
1529
|
"*": "allow",
|
|
@@ -1133,22 +1617,25 @@ var FORBIDDEN_BASH = [
|
|
|
1133
1617
|
"twine upload*",
|
|
1134
1618
|
// no recursive delegation
|
|
1135
1619
|
"opencode*",
|
|
1620
|
+
"kilo *",
|
|
1621
|
+
"kilocode*",
|
|
1622
|
+
"agy *",
|
|
1136
1623
|
"pitroom *",
|
|
1137
1624
|
"*pitroom*"
|
|
1138
1625
|
];
|
|
1139
|
-
function scratchDirs() {
|
|
1140
|
-
const data = process.env.XDG_DATA_HOME ??
|
|
1626
|
+
function scratchDirs(app) {
|
|
1627
|
+
const data = process.env.XDG_DATA_HOME ?? path9.join(os7.homedir(), ".local", "share");
|
|
1141
1628
|
const rules = {
|
|
1142
1629
|
"*": "deny",
|
|
1143
|
-
[
|
|
1144
|
-
[
|
|
1630
|
+
[path9.join(data, app, "tool-output", "*")]: "allow",
|
|
1631
|
+
[path9.join(data, app, "shell", "*", "*")]: "allow"
|
|
1145
1632
|
};
|
|
1146
|
-
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";
|
|
1147
1634
|
return rules;
|
|
1148
1635
|
}
|
|
1149
1636
|
function realpath(p) {
|
|
1150
1637
|
try {
|
|
1151
|
-
return
|
|
1638
|
+
return fs10.realpathSync(p);
|
|
1152
1639
|
} catch {
|
|
1153
1640
|
return p;
|
|
1154
1641
|
}
|
|
@@ -1164,7 +1651,7 @@ var COMMON = {
|
|
|
1164
1651
|
doom_loop: "deny"
|
|
1165
1652
|
};
|
|
1166
1653
|
var web = (on) => ({ webfetch: on ? "allow" : "deny", websearch: on ? "allow" : "deny" });
|
|
1167
|
-
function readProfile(allowWeb = false) {
|
|
1654
|
+
function readProfile(allowWeb = false, app = "opencode") {
|
|
1168
1655
|
return {
|
|
1169
1656
|
"*": "deny",
|
|
1170
1657
|
read: READ_FILES,
|
|
@@ -1180,11 +1667,11 @@ function readProfile(allowWeb = false) {
|
|
|
1180
1667
|
bash: READ_ONLY_BASH,
|
|
1181
1668
|
shell: READ_ONLY_BASH,
|
|
1182
1669
|
edit: "deny",
|
|
1183
|
-
external_directory: scratchDirs(),
|
|
1670
|
+
external_directory: scratchDirs(app),
|
|
1184
1671
|
...COMMON
|
|
1185
1672
|
};
|
|
1186
1673
|
}
|
|
1187
|
-
function writeProfile(allowWeb = false) {
|
|
1674
|
+
function writeProfile(allowWeb = false, app = "opencode") {
|
|
1188
1675
|
const bash = { "*": "allow" };
|
|
1189
1676
|
for (const pattern of FORBIDDEN_BASH) bash[pattern] = "deny";
|
|
1190
1677
|
return {
|
|
@@ -1193,25 +1680,26 @@ function writeProfile(allowWeb = false) {
|
|
|
1193
1680
|
...web(allowWeb),
|
|
1194
1681
|
bash,
|
|
1195
1682
|
shell: bash,
|
|
1196
|
-
external_directory: scratchDirs(),
|
|
1683
|
+
external_directory: scratchDirs(app),
|
|
1197
1684
|
...COMMON
|
|
1198
1685
|
};
|
|
1199
1686
|
}
|
|
1200
1687
|
function agentFor(mode) {
|
|
1201
1688
|
return mode === "read" ? AGENT.read : AGENT.write;
|
|
1202
1689
|
}
|
|
1203
|
-
function configContent(existing, allowWeb = false) {
|
|
1690
|
+
function configContent(existing, allowWeb = false, app = "opencode", extra = {}) {
|
|
1204
1691
|
const ours = {
|
|
1692
|
+
...extra,
|
|
1205
1693
|
agent: {
|
|
1206
1694
|
[AGENT.read]: {
|
|
1207
1695
|
mode: "primary",
|
|
1208
1696
|
description: "pitroom: read-only worker (research, search, review)",
|
|
1209
|
-
permission: readProfile(allowWeb)
|
|
1697
|
+
permission: readProfile(allowWeb, app)
|
|
1210
1698
|
},
|
|
1211
1699
|
[AGENT.write]: {
|
|
1212
1700
|
mode: "primary",
|
|
1213
1701
|
description: "pitroom: editing worker (no git history changes, no bulk deletes)",
|
|
1214
|
-
permission: writeProfile(allowWeb)
|
|
1702
|
+
permission: writeProfile(allowWeb, app)
|
|
1215
1703
|
}
|
|
1216
1704
|
}
|
|
1217
1705
|
};
|
|
@@ -1236,10 +1724,10 @@ function isObject(v) {
|
|
|
1236
1724
|
}
|
|
1237
1725
|
|
|
1238
1726
|
// src/backends/opencode/index.ts
|
|
1239
|
-
var
|
|
1727
|
+
var binary5 = () => findBinary("opencode", "PITROOM_OPENCODE_BIN", ["~/.opencode/bin/opencode"]);
|
|
1240
1728
|
function oc(args, opts = {}) {
|
|
1241
|
-
const { command, prefix } = resolveCommand(
|
|
1242
|
-
const r =
|
|
1729
|
+
const { command, prefix } = resolveCommand(binary5());
|
|
1730
|
+
const r = spawnSync5(command, [...prefix, ...args], {
|
|
1243
1731
|
encoding: "utf8",
|
|
1244
1732
|
timeout: opts.timeout ?? 6e4,
|
|
1245
1733
|
env: opts.env ?? process.env,
|
|
@@ -1249,7 +1737,7 @@ function oc(args, opts = {}) {
|
|
|
1249
1737
|
return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
|
|
1250
1738
|
}
|
|
1251
1739
|
var jsonIn = (s) => JSON.parse(s.slice(s.indexOf("{")));
|
|
1252
|
-
function
|
|
1740
|
+
function invocation5(req) {
|
|
1253
1741
|
const args = [
|
|
1254
1742
|
"run",
|
|
1255
1743
|
"--standalone",
|
|
@@ -1268,7 +1756,7 @@ function invocation4(req) {
|
|
|
1268
1756
|
if (req.sessionId) args.push("--session", req.sessionId);
|
|
1269
1757
|
for (const f of req.files) args.push("--file", f);
|
|
1270
1758
|
args.push(req.prompt);
|
|
1271
|
-
const { command, prefix } = resolveCommand(
|
|
1759
|
+
const { command, prefix } = resolveCommand(binary5());
|
|
1272
1760
|
return {
|
|
1273
1761
|
command,
|
|
1274
1762
|
args: [...prefix, ...args],
|
|
@@ -1283,7 +1771,7 @@ function majorVersion(output) {
|
|
|
1283
1771
|
const m = /(\d+)\.\d+\.\d+/.exec(output);
|
|
1284
1772
|
return m ? Number(m[1]) : void 0;
|
|
1285
1773
|
}
|
|
1286
|
-
function
|
|
1774
|
+
function classify5(message) {
|
|
1287
1775
|
if (/model not found|ModelNotFound|unknown model|no such model|deprecated/i.test(message)) return "model-unavailable";
|
|
1288
1776
|
if (/rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|unavailable|capacity/i.test(message)) return "rate-limited";
|
|
1289
1777
|
if (/provider\.auth|FreeTierError|free tier|unauthori[sz]ed|\b40[13]\b|api.?key|insufficient|credits?\b|billing|payment/i.test(message)) {
|
|
@@ -1291,17 +1779,17 @@ function classify4(message) {
|
|
|
1291
1779
|
}
|
|
1292
1780
|
return "other";
|
|
1293
1781
|
}
|
|
1294
|
-
function
|
|
1782
|
+
function failure5(run, stderr, exitCode) {
|
|
1295
1783
|
if (exitCode === 0 && !run.error) return void 0;
|
|
1296
1784
|
const detail = stderr.match(/error="([^"]+)"/g)?.pop()?.slice(7, -1);
|
|
1297
1785
|
const raw = run.error ?? `opencode exited with code ${exitCode}`;
|
|
1298
1786
|
const message = detail && !raw.includes(detail) ? `${raw}: ${detail}` : raw;
|
|
1299
|
-
return { kind:
|
|
1787
|
+
return { kind: classify5(message), message };
|
|
1300
1788
|
}
|
|
1301
1789
|
function modelId(m) {
|
|
1302
1790
|
if (typeof m === "string") return m;
|
|
1303
|
-
const
|
|
1304
|
-
return m?.providerID &&
|
|
1791
|
+
const id2 = m?.model ?? m?.id;
|
|
1792
|
+
return m?.providerID && id2 ? `${m.providerID}/${id2}` : void 0;
|
|
1305
1793
|
}
|
|
1306
1794
|
function defaultModel3() {
|
|
1307
1795
|
try {
|
|
@@ -1331,25 +1819,25 @@ function resolveModel2(sessionId) {
|
|
|
1331
1819
|
function catalog4() {
|
|
1332
1820
|
const ids = listModels();
|
|
1333
1821
|
return {
|
|
1334
|
-
models: (ids.length ? ids : listModels()).map((
|
|
1822
|
+
models: (ids.length ? ids : listModels()).map((id2) => ({ id: id2 })),
|
|
1335
1823
|
source: "`opencode models` (reasoning variants are provider-specific: provider/model#variant)"
|
|
1336
1824
|
};
|
|
1337
1825
|
}
|
|
1338
1826
|
function listModels() {
|
|
1339
1827
|
return oc(["models"]).out.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
1340
1828
|
}
|
|
1341
|
-
function
|
|
1829
|
+
function doctor5({ models, hasFallback }) {
|
|
1342
1830
|
const checks = [];
|
|
1343
1831
|
const version = oc(["--version"]);
|
|
1344
1832
|
if (version.missing || !version.ok) {
|
|
1345
|
-
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` }];
|
|
1346
1834
|
}
|
|
1347
1835
|
const v = version.out.trim().replace(/^opencode\s+/i, "");
|
|
1348
1836
|
const major = majorVersion(v);
|
|
1349
1837
|
if (major !== void 0 && major < 2) {
|
|
1350
1838
|
return [{ level: "fail", message: `OpenCode ${v} is too old: Pitroom needs OpenCode v2 or newer (run \`opencode upgrade\`)` }];
|
|
1351
1839
|
}
|
|
1352
|
-
checks.push({ level: "ok", message: `opencode ${v} at ${
|
|
1840
|
+
checks.push({ level: "ok", message: `opencode ${v} at ${binary5()} (private --standalone server per run)` });
|
|
1353
1841
|
const known = new Set(listModels());
|
|
1354
1842
|
const def = defaultModel3();
|
|
1355
1843
|
for (const m of models) {
|
|
@@ -1377,79 +1865,371 @@ var opencode = {
|
|
|
1377
1865
|
id: "opencode",
|
|
1378
1866
|
name: "OpenCode",
|
|
1379
1867
|
capabilities: { readOnly: "permission-rules", resume: "by-id", reportsCost: true, attachFiles: true },
|
|
1380
|
-
binary:
|
|
1381
|
-
invocation:
|
|
1382
|
-
parse:
|
|
1383
|
-
failure:
|
|
1868
|
+
binary: binary5,
|
|
1869
|
+
invocation: invocation5,
|
|
1870
|
+
parse: parseEvents5,
|
|
1871
|
+
failure: failure5,
|
|
1384
1872
|
defaultModel: defaultModel3,
|
|
1385
1873
|
resolveModel: resolveModel2,
|
|
1386
1874
|
listModels,
|
|
1387
1875
|
catalog: catalog4,
|
|
1388
|
-
doctor:
|
|
1876
|
+
doctor: doctor5
|
|
1389
1877
|
};
|
|
1390
1878
|
|
|
1391
|
-
// src/backends/index.ts
|
|
1392
|
-
var
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
var isBackendId = (id) => REGISTRY.has(id) || PLANNED.includes(id);
|
|
1398
|
-
function getBackend(id) {
|
|
1399
|
-
const backend = REGISTRY.get(id);
|
|
1400
|
-
if (backend) return backend;
|
|
1401
|
-
const available = backendIds().join(", ");
|
|
1402
|
-
throw new UserError(
|
|
1403
|
-
PLANNED.includes(id) ? `the "${id}" worker is not supported yet (available: ${available})` : `unknown worker "${id}" (available: ${available})`,
|
|
1404
|
-
3
|
|
1405
|
-
);
|
|
1879
|
+
// src/backends/kilo/index.ts
|
|
1880
|
+
var binary6 = () => findBinary("kilo", "PITROOM_KILO_BIN", ["~/.kilo/bin/kilo"]);
|
|
1881
|
+
function kilo(args, timeout = 6e4) {
|
|
1882
|
+
const { command, prefix } = resolveCommand(binary6());
|
|
1883
|
+
const r = spawnSync6(command, [...prefix, ...args], { encoding: "utf8", timeout, stdio: ["ignore", "pipe", "pipe"], maxBuffer: 64 * 1024 * 1024 });
|
|
1884
|
+
return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
|
|
1406
1885
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1886
|
+
function invocation6(req) {
|
|
1887
|
+
const args = [
|
|
1888
|
+
"run",
|
|
1889
|
+
"--agent",
|
|
1890
|
+
agentFor(req.mode),
|
|
1891
|
+
"--format",
|
|
1892
|
+
"json",
|
|
1893
|
+
"--title",
|
|
1894
|
+
req.title,
|
|
1895
|
+
// Some failure causes only appear in the server logs (Kilo takes the level in capitals only).
|
|
1896
|
+
"--print-logs",
|
|
1897
|
+
"--log-level",
|
|
1898
|
+
"ERROR"
|
|
1899
|
+
];
|
|
1900
|
+
if (req.model) args.push("--model", req.model);
|
|
1901
|
+
if (req.sessionId) args.push("--session", req.sessionId);
|
|
1902
|
+
for (const f of req.files) args.push("--file", f);
|
|
1903
|
+
args.push(req.prompt);
|
|
1904
|
+
const { command, prefix } = resolveCommand(binary6());
|
|
1905
|
+
return {
|
|
1906
|
+
command,
|
|
1907
|
+
args: [...prefix, ...args],
|
|
1908
|
+
env: {
|
|
1909
|
+
// A worker run must not upgrade Kilo underneath the user.
|
|
1910
|
+
KILO_CONFIG_CONTENT: configContent(process.env.KILO_CONFIG_CONTENT, req.web, "kilo", { autoupdate: false }),
|
|
1911
|
+
// No daemon to attach to: a private server per run (see above).
|
|
1912
|
+
XDG_STATE_HOME: path10.join(home(), "kilo-state")
|
|
1913
|
+
}
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
function failure6(run, stderr, exitCode) {
|
|
1917
|
+
if (exitCode === 0 && !run.error) return void 0;
|
|
1918
|
+
const detail = stderr.match(/error="([^"]+)"/g)?.pop()?.slice(7, -1);
|
|
1919
|
+
const raw = run.error ?? `kilo exited with code ${exitCode}`;
|
|
1920
|
+
const message = detail && !raw.includes(detail) ? `${raw}: ${detail}` : raw;
|
|
1921
|
+
return { kind: classify5(message), message };
|
|
1922
|
+
}
|
|
1923
|
+
function listModels2() {
|
|
1924
|
+
return kilo(["models"]).out.split("\n").map((s) => s.trim()).filter((s) => /^[\w.-]+\/\S+$/.test(s));
|
|
1925
|
+
}
|
|
1926
|
+
function defaultModel4() {
|
|
1927
|
+
try {
|
|
1928
|
+
const out = kilo(["debug", "config"], 3e4).out;
|
|
1929
|
+
const model = JSON.parse(out.slice(out.indexOf("{"))).model;
|
|
1930
|
+
return typeof model === "string" ? model : void 0;
|
|
1931
|
+
} catch {
|
|
1932
|
+
return void 0;
|
|
1416
1933
|
}
|
|
1417
|
-
if (isBackendId(s)) return { backend: s };
|
|
1418
|
-
return { backend: defaultBackend, model: s };
|
|
1419
1934
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
// src/core/refs.ts
|
|
1424
|
-
import fs8 from "node:fs";
|
|
1425
|
-
import path7 from "node:path";
|
|
1426
|
-
var FILE_EXTENSIONS = new Set(
|
|
1427
|
-
"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(" ")
|
|
1428
|
-
);
|
|
1429
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
1430
|
-
var MAX_INDEXED = 6e4;
|
|
1431
|
-
var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
|
|
1432
|
-
var REF = new RegExp(
|
|
1433
|
-
String.raw`(?<![\w/:.-])(\.{0,2}/?(?:[\w@.+-]+/)*(?:[\w@+-][\w@.+-]*\.[A-Za-z][A-Za-z0-9]{0,7}|${EXTENSIONLESS})):(\d+)(?:[-–](\d+))?`,
|
|
1434
|
-
"g"
|
|
1435
|
-
);
|
|
1436
|
-
var IDENT = /`([A-Za-z_$][\w$]*)(?:\([^`]*\))?`/g;
|
|
1437
|
-
var CLAUSE = /[;|—]|\.\s|:\s/g;
|
|
1438
|
-
var MAX_BYTES = 5 * 1024 * 1024;
|
|
1439
|
-
var WINDOW = 5;
|
|
1440
|
-
var DEFINITION_LOOKBACK = 80;
|
|
1441
|
-
function looksLikeSymbol(m) {
|
|
1442
|
-
const name = m[1];
|
|
1443
|
-
return m[0].includes("(") || /[A-Z_$0-9]/.test(name) || name.length >= 5;
|
|
1935
|
+
function catalog5() {
|
|
1936
|
+
return { models: listModels2().map((id2) => ({ id: id2 })), source: "`kilo models` (reasoning variants are provider-specific: provider/model#variant)" };
|
|
1444
1937
|
}
|
|
1445
|
-
function
|
|
1446
|
-
const
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1938
|
+
function doctor6({ models, hasFallback }) {
|
|
1939
|
+
const version = kilo(["--version"]);
|
|
1940
|
+
if (version.missing || !version.ok) {
|
|
1941
|
+
return [{ level: "fail", message: `kilo not runnable (${binary6()}). Install Kilo Code's CLI (npm i -g @kilocode/cli), or set PITROOM_KILO_BIN` }];
|
|
1942
|
+
}
|
|
1943
|
+
const checks = [{ level: "ok", message: `Kilo Code ${version.out.trim().split("\n").pop()} at ${binary6()} (beta worker: checked against 7.8)` }];
|
|
1944
|
+
const def = defaultModel4();
|
|
1945
|
+
for (const m of models) {
|
|
1946
|
+
const model = m ?? def;
|
|
1947
|
+
checks.push(model ? { level: "ok", message: `Kilo Code ${m ? "model" : "default model"}: ${model}` } : { level: "warn", message: 'Kilo Code has no default model: pass -W kilo:<provider/model> or set "model" in its config' });
|
|
1948
|
+
}
|
|
1949
|
+
if (!hasFallback) checks.push({ level: "warn", message: "no fallback workers configured for Kilo Code runs" });
|
|
1950
|
+
checks.push({ level: "ok", message: `permission profiles ${AGENT.read}/${AGENT.write} injected per run through KILO_CONFIG_CONTENT` });
|
|
1951
|
+
return checks;
|
|
1952
|
+
}
|
|
1953
|
+
var kiloBackend = {
|
|
1954
|
+
id: "kilo",
|
|
1955
|
+
name: "Kilo Code",
|
|
1956
|
+
capabilities: { readOnly: "permission-rules", resume: "by-id", reportsCost: true, attachFiles: true },
|
|
1957
|
+
binary: binary6,
|
|
1958
|
+
invocation: invocation6,
|
|
1959
|
+
parse: parseEvents5,
|
|
1960
|
+
failure: failure6,
|
|
1961
|
+
defaultModel: defaultModel4,
|
|
1962
|
+
listModels: listModels2,
|
|
1963
|
+
catalog: catalog5,
|
|
1964
|
+
doctor: doctor6
|
|
1965
|
+
};
|
|
1966
|
+
|
|
1967
|
+
// src/backends/qwen/index.ts
|
|
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";
|
|
1972
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1973
|
+
|
|
1974
|
+
// src/backends/qwen/events.ts
|
|
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;
|
|
1978
|
+
var API_ERROR = /^\[API Error: /;
|
|
1979
|
+
function parseEvents6(jsonl) {
|
|
1980
|
+
const usage = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0, steps: 0, toolCalls: 0, denied: 0 };
|
|
1981
|
+
const tools = {};
|
|
1982
|
+
const edits = [];
|
|
1983
|
+
const pendingEdits = /* @__PURE__ */ new Map();
|
|
1984
|
+
const timeline = [];
|
|
1985
|
+
const stepOf = /* @__PURE__ */ new Map();
|
|
1986
|
+
let sessionId;
|
|
1987
|
+
let model;
|
|
1988
|
+
let lastText = "";
|
|
1989
|
+
let finalText;
|
|
1990
|
+
let lastActivity;
|
|
1991
|
+
let error;
|
|
1992
|
+
for (const line of jsonl.split("\n")) {
|
|
1993
|
+
if (!line.trim()) continue;
|
|
1994
|
+
let e;
|
|
1995
|
+
try {
|
|
1996
|
+
e = JSON.parse(line);
|
|
1997
|
+
} catch {
|
|
1998
|
+
continue;
|
|
1999
|
+
}
|
|
2000
|
+
sessionId ??= e.session_id;
|
|
2001
|
+
if (e.type === "system" && e.subtype === "init") {
|
|
2002
|
+
model = e.model;
|
|
2003
|
+
} else if (e.type === "system" && e.subtype === "retry") {
|
|
2004
|
+
lastActivity = `waiting to retry (attempt ${e.data?.attempt ?? "?"} of ${e.data?.maxRetries ?? "?"})`;
|
|
2005
|
+
} else if (e.type === "assistant") {
|
|
2006
|
+
const content = e.message?.content ?? [];
|
|
2007
|
+
const text = content.filter((c) => c.type === "text").map((c) => String(c.text ?? "")).join(" ").trim();
|
|
2008
|
+
if (API_ERROR.test(text)) {
|
|
2009
|
+
error = apiError(text);
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
2012
|
+
usage.steps++;
|
|
2013
|
+
for (const c of content) {
|
|
2014
|
+
if (c.type === "text" && String(c.text ?? "").trim()) {
|
|
2015
|
+
lastText = String(c.text).trim();
|
|
2016
|
+
lastActivity = `says: ${oneLine(lastText)}`;
|
|
2017
|
+
if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip(lastText, 600) });
|
|
2018
|
+
} else if (c.type === "tool_use") {
|
|
2019
|
+
usage.toolCalls++;
|
|
2020
|
+
tools[c.name] = (tools[c.name] ?? 0) + 1;
|
|
2021
|
+
const target = c.input?.file_path ?? c.input?.path ?? c.input?.pattern ?? c.input?.command ?? c.input?.url;
|
|
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));
|
|
2024
|
+
if (timeline.length < MAX_STEPS) {
|
|
2025
|
+
const step = { kind: EDIT_TOOLS5.has(c.name) ? "edit" : c.name === SHELL2 ? "shell" : "tool", name: c.name, text: clip(String(target ?? ""), 240) };
|
|
2026
|
+
timeline.push(step);
|
|
2027
|
+
stepOf.set(String(c.id), step);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
} else if (e.type === "user") {
|
|
2032
|
+
for (const c of e.message?.content ?? []) {
|
|
2033
|
+
if (c?.type !== "tool_result") continue;
|
|
2034
|
+
const text = typeof c.content === "string" ? c.content : JSON.stringify(c.content ?? "");
|
|
2035
|
+
const step = stepOf.get(String(c.tool_use_id));
|
|
2036
|
+
if (step) step.ok = !c.is_error;
|
|
2037
|
+
if (c.is_error) {
|
|
2038
|
+
if (DENIED5.test(text)) usage.denied++;
|
|
2039
|
+
} else if (pendingEdits.has(String(c.tool_use_id))) {
|
|
2040
|
+
edits.push(pendingEdits.get(String(c.tool_use_id)));
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
} else if (e.type === "result") {
|
|
2044
|
+
const u = e.usage ?? {};
|
|
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);
|
|
2049
|
+
usage.denied = Math.max(usage.denied, Array.isArray(e.permission_denials) ? e.permission_denials.length : 0);
|
|
2050
|
+
if (e.is_error) {
|
|
2051
|
+
error ??= apiError(String(e.error?.message ?? e.result ?? e.subtype ?? "Qwen Code error"));
|
|
2052
|
+
} else if (typeof e.result === "string") {
|
|
2053
|
+
finalText = e.result.trim();
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
return { sessionId, model, finalText: finalText ?? (error ? "" : lastText), usage, tools, edits, lastActivity, timeline, error };
|
|
2058
|
+
}
|
|
2059
|
+
var apiError = (text) => text.replace(API_ERROR, "").replace(/\](?=\s*(\n|$))/, "").replace(/\s+/g, " ").trim() || text;
|
|
2060
|
+
|
|
2061
|
+
// src/backends/qwen/index.ts
|
|
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");
|
|
2064
|
+
var EXCLUDED = [
|
|
2065
|
+
"agent",
|
|
2066
|
+
"send_message",
|
|
2067
|
+
"skill",
|
|
2068
|
+
"manage_memory",
|
|
2069
|
+
"search_memory",
|
|
2070
|
+
"enter_worktree",
|
|
2071
|
+
"exit_worktree",
|
|
2072
|
+
"record_artifact",
|
|
2073
|
+
"cron_create",
|
|
2074
|
+
"cron_list",
|
|
2075
|
+
"cron_delete",
|
|
2076
|
+
"loop_wakeup",
|
|
2077
|
+
"task_stop",
|
|
2078
|
+
"update_goal",
|
|
2079
|
+
"get_goal",
|
|
2080
|
+
"list_agents",
|
|
2081
|
+
"report_findings",
|
|
2082
|
+
"tool_call",
|
|
2083
|
+
"tool_search",
|
|
2084
|
+
"read_mcp_resource"
|
|
2085
|
+
];
|
|
2086
|
+
var WEB = ["web_fetch", "web_search"];
|
|
2087
|
+
var EDITS_HERE = ["Edit(./**)", "write_file(./**)", "notebook_edit(./**)"];
|
|
2088
|
+
var DENY_SHELL = FORBIDDEN_COMMANDS.map((c) => `run_shell_command(${c})`);
|
|
2089
|
+
function invocation7(req) {
|
|
2090
|
+
const read = req.mode === "read";
|
|
2091
|
+
const exclude = [...EXCLUDED, ...req.web ? [] : WEB, ...read ? [] : DENY_SHELL];
|
|
2092
|
+
const args = [
|
|
2093
|
+
// The prompt is the first word: the list options below take every word that follows them, and Qwen Code reads
|
|
2094
|
+
// no prompt after "--". One that starts with "-" would be read as an option: a leading space keeps it the prompt.
|
|
2095
|
+
req.prompt.startsWith("-") ? ` ${req.prompt}` : req.prompt,
|
|
2096
|
+
"--safe-mode",
|
|
2097
|
+
"--output-format",
|
|
2098
|
+
"stream-json",
|
|
2099
|
+
"--approval-mode",
|
|
2100
|
+
read ? "plan" : "default",
|
|
2101
|
+
"--exclude-tools",
|
|
2102
|
+
...exclude
|
|
2103
|
+
];
|
|
2104
|
+
if (!read) args.push("--allowed-tools", ...EDITS_HERE, "run_shell_command");
|
|
2105
|
+
const model = (req.model ?? "").split("#")[0];
|
|
2106
|
+
if (model) args.push("--model", model);
|
|
2107
|
+
if (req.sessionId) args.push("--resume", req.sessionId);
|
|
2108
|
+
const { command, prefix } = resolveCommand(binary7());
|
|
2109
|
+
return {
|
|
2110
|
+
command,
|
|
2111
|
+
args: [...prefix, ...args],
|
|
2112
|
+
env: {
|
|
2113
|
+
QWEN_CODE_SYSTEM_DEFAULTS_PATH: path11.join(policyDir2(), "worker-defaults.json"),
|
|
2114
|
+
QWEN_CODE_DISABLE_CRON: "1",
|
|
2115
|
+
QWEN_DISABLE_AUTO_TITLE: "1"
|
|
2116
|
+
}
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
function classify6(message) {
|
|
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";
|
|
2121
|
+
if (/\b429\b|rate.?limit|too many requests|quota|capacity|overloaded|\b529\b/i.test(message)) return "rate-limited";
|
|
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";
|
|
2123
|
+
return "other";
|
|
2124
|
+
}
|
|
2125
|
+
function failure7(run, stderr, exitCode) {
|
|
2126
|
+
if (exitCode === 0 && !run.error) return void 0;
|
|
2127
|
+
const detail = stderr.trim().split("\n").filter((l) => l.trim() && !/SAFE MODE/.test(l)).pop();
|
|
2128
|
+
const message = run.error ?? detail ?? `qwen exited with code ${exitCode}`;
|
|
2129
|
+
return { kind: classify6(message), message };
|
|
2130
|
+
}
|
|
2131
|
+
function userSettings() {
|
|
2132
|
+
try {
|
|
2133
|
+
return JSON.parse(fs11.readFileSync(path11.join(process.env.QWEN_HOME ?? path11.join(os8.homedir(), ".qwen"), "settings.json"), "utf8"));
|
|
2134
|
+
} catch {
|
|
2135
|
+
return {};
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
function defaultModel5() {
|
|
2139
|
+
const name = userSettings().model?.name;
|
|
2140
|
+
return typeof name === "string" && name ? name : void 0;
|
|
2141
|
+
}
|
|
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)` }];
|
|
2147
|
+
const auth = userSettings().security?.auth?.selectedType;
|
|
2148
|
+
const key = process.env.OPENAI_API_KEY || process.env.DASHSCOPE_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.GEMINI_API_KEY;
|
|
2149
|
+
checks.push(
|
|
2150
|
+
auth || key ? { level: "ok", message: `Qwen Code: sign-in ${auth ?? "from an API key in the environment"}` } : { level: "warn", message: "Qwen Code has no sign-in method set: run `qwen` once to choose one, or set OPENAI_API_KEY (and OPENAI_BASE_URL) for an OpenAI-compatible provider" }
|
|
2151
|
+
);
|
|
2152
|
+
for (const m of models) {
|
|
2153
|
+
const model = m ?? defaultModel5();
|
|
2154
|
+
checks.push({ level: model ? "ok" : "warn", message: model ? `Qwen Code model: ${model}` : "Qwen Code model: none pinned (pass -W qwen:<model> or set model.name in ~/.qwen/settings.json)" });
|
|
2155
|
+
}
|
|
2156
|
+
if (!hasFallback) checks.push({ level: "warn", message: "no fallback workers configured for Qwen Code runs" });
|
|
2157
|
+
return checks;
|
|
2158
|
+
}
|
|
2159
|
+
var qwen = {
|
|
2160
|
+
id: "qwen",
|
|
2161
|
+
name: "Qwen Code",
|
|
2162
|
+
capabilities: { readOnly: "approval-mode", resume: "by-id", reportsCost: false, attachFiles: false },
|
|
2163
|
+
binary: binary7,
|
|
2164
|
+
invocation: invocation7,
|
|
2165
|
+
parse: parseEvents6,
|
|
2166
|
+
failure: failure7,
|
|
2167
|
+
defaultModel: defaultModel5,
|
|
2168
|
+
doctor: doctor7
|
|
2169
|
+
};
|
|
2170
|
+
|
|
2171
|
+
// src/backends/index.ts
|
|
2172
|
+
var REGISTRY = new Map([opencode, codex, claude, gemini, qwen, kiloBackend, antigravity].map((b) => [b.id, b]));
|
|
2173
|
+
var DEFAULT_BACKEND = opencode.id;
|
|
2174
|
+
var PLANNED = ["hermes"];
|
|
2175
|
+
var backendIds = () => [...REGISTRY.keys()];
|
|
2176
|
+
var allBackends = () => [...REGISTRY.values()];
|
|
2177
|
+
var isBackendId = (id2) => REGISTRY.has(id2) || PLANNED.includes(id2);
|
|
2178
|
+
function getBackend(id2) {
|
|
2179
|
+
const backend = REGISTRY.get(id2);
|
|
2180
|
+
if (backend) return backend;
|
|
2181
|
+
const available = backendIds().join(", ");
|
|
2182
|
+
throw new UserError(
|
|
2183
|
+
PLANNED.includes(id2) ? `the "${id2}" worker is not supported yet (available: ${available})` : `unknown worker "${id2}" (available: ${available})`,
|
|
2184
|
+
3
|
|
2185
|
+
);
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
// src/core/target.ts
|
|
2189
|
+
function parseTarget(spec, defaultBackend) {
|
|
2190
|
+
const s = spec.trim();
|
|
2191
|
+
if (!s) throw new UserError("empty worker target");
|
|
2192
|
+
const i = s.indexOf(":");
|
|
2193
|
+
if (i > 0 && isBackendId(s.slice(0, i))) {
|
|
2194
|
+
const model = s.slice(i + 1).trim();
|
|
2195
|
+
return model ? { backend: s.slice(0, i), model } : { backend: s.slice(0, i) };
|
|
2196
|
+
}
|
|
2197
|
+
if (isBackendId(s)) return { backend: s };
|
|
2198
|
+
return { backend: defaultBackend, model: s };
|
|
2199
|
+
}
|
|
2200
|
+
var formatTarget = (t) => t.model ? `${t.backend}:${t.model}` : t.backend;
|
|
2201
|
+
var describeTarget = (t) => t.model ? formatTarget(t) : `${t.backend} (default model)`;
|
|
2202
|
+
|
|
2203
|
+
// src/core/refs.ts
|
|
2204
|
+
import fs12 from "node:fs";
|
|
2205
|
+
import path12 from "node:path";
|
|
2206
|
+
var FILE_EXTENSIONS = new Set(
|
|
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(" ")
|
|
2208
|
+
);
|
|
2209
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
2210
|
+
var MAX_INDEXED = 6e4;
|
|
2211
|
+
var EXTENSIONLESS = "Makefile|Dockerfile|Containerfile|Gemfile|Rakefile|Procfile|Justfile|Vagrantfile|BUILD|WORKSPACE";
|
|
2212
|
+
var REF = new RegExp(
|
|
2213
|
+
String.raw`(?<![\w/:.-])(\.{0,2}/?(?:[\w@.+-]+/)*(?:[\w@+-][\w@.+-]*\.[A-Za-z][A-Za-z0-9]{0,7}|${EXTENSIONLESS})):(\d+)(?:[-–](\d+))?`,
|
|
2214
|
+
"g"
|
|
2215
|
+
);
|
|
2216
|
+
var IDENT = /`([A-Za-z_$][\w$]*)(?:\([^`]*\))?`/g;
|
|
2217
|
+
var CLAUSE = /[;|—]|\.\s|:\s/g;
|
|
2218
|
+
var MAX_BYTES = 5 * 1024 * 1024;
|
|
2219
|
+
var WINDOW = 5;
|
|
2220
|
+
var DEFINITION_LOOKBACK = 80;
|
|
2221
|
+
function looksLikeSymbol(m) {
|
|
2222
|
+
const name = m[1];
|
|
2223
|
+
return m[0].includes("(") || /[A-Z_$0-9]/.test(name) || name.length >= 5;
|
|
2224
|
+
}
|
|
2225
|
+
function extractRefs(answer) {
|
|
2226
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2227
|
+
for (const line of answer.split("\n")) {
|
|
2228
|
+
const matches2 = [...line.matchAll(REF)];
|
|
2229
|
+
const seps = [...line.matchAll(CLAUSE)].map((s) => s.index ?? 0);
|
|
2230
|
+
const sepBetween = (a, b) => seps.some((s) => s >= a && s < b);
|
|
2231
|
+
const idents = [...line.matchAll(IDENT)].filter((i) => {
|
|
2232
|
+
if (!looksLikeSymbol(i)) return false;
|
|
1453
2233
|
const pos = i.index ?? 0;
|
|
1454
2234
|
const before = matches2.filter((m) => (m.index ?? 0) + m[0].length <= pos).at(-1);
|
|
1455
2235
|
const after = matches2.find((m) => (m.index ?? 0) >= pos + i[0].length);
|
|
@@ -1473,13 +2253,13 @@ function extractRefs(answer) {
|
|
|
1473
2253
|
return [...seen.values()];
|
|
1474
2254
|
}
|
|
1475
2255
|
function verifyRefs(refs, dirs) {
|
|
1476
|
-
const roots = [...new Set(dirs.filter(Boolean).map((d) =>
|
|
2256
|
+
const roots = [...new Set(dirs.filter(Boolean).map((d) => path12.resolve(d)))];
|
|
1477
2257
|
const invalid = [];
|
|
1478
2258
|
const lineCache = /* @__PURE__ */ new Map();
|
|
1479
2259
|
const load = (file) => {
|
|
1480
2260
|
if (!lineCache.has(file)) {
|
|
1481
|
-
const size =
|
|
1482
|
-
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");
|
|
1483
2263
|
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
1484
2264
|
lineCache.set(file, lines);
|
|
1485
2265
|
}
|
|
@@ -1490,9 +2270,9 @@ function verifyRefs(refs, dirs) {
|
|
|
1490
2270
|
const candidates = (ref) => {
|
|
1491
2271
|
const direct = resolve(ref.file, roots);
|
|
1492
2272
|
if (direct) return [direct];
|
|
1493
|
-
if (
|
|
2273
|
+
if (path12.isAbsolute(ref.file)) return [];
|
|
1494
2274
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
1495
|
-
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);
|
|
1496
2276
|
};
|
|
1497
2277
|
const check = (file, ref) => {
|
|
1498
2278
|
const lines = load(file);
|
|
@@ -1507,7 +2287,7 @@ function verifyRefs(refs, dirs) {
|
|
|
1507
2287
|
if (!files.length) {
|
|
1508
2288
|
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
1509
2289
|
total++;
|
|
1510
|
-
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" });
|
|
1511
2291
|
continue;
|
|
1512
2292
|
}
|
|
1513
2293
|
total++;
|
|
@@ -1528,7 +2308,7 @@ function enclosedBy(lines, ref) {
|
|
|
1528
2308
|
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
1529
2309
|
}
|
|
1530
2310
|
var isFileName = (name) => {
|
|
1531
|
-
const base =
|
|
2311
|
+
const base = path12.basename(name);
|
|
1532
2312
|
return !base.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base) || FILE_EXTENSIONS.has(base.split(".").pop().toLowerCase());
|
|
1533
2313
|
};
|
|
1534
2314
|
function indexFiles(roots) {
|
|
@@ -1537,19 +2317,19 @@ function indexFiles(roots) {
|
|
|
1537
2317
|
const walk = (dir) => {
|
|
1538
2318
|
let entries;
|
|
1539
2319
|
try {
|
|
1540
|
-
entries =
|
|
2320
|
+
entries = fs12.readdirSync(dir, { withFileTypes: true });
|
|
1541
2321
|
} catch {
|
|
1542
2322
|
return;
|
|
1543
2323
|
}
|
|
1544
2324
|
for (const e of entries) {
|
|
1545
2325
|
if (count >= MAX_INDEXED) return;
|
|
1546
2326
|
if (e.isDirectory()) {
|
|
1547
|
-
if (!
|
|
2327
|
+
if (!SKIP_DIRS2.has(e.name)) walk(path12.join(dir, e.name));
|
|
1548
2328
|
} else if (e.isFile()) {
|
|
1549
2329
|
count++;
|
|
1550
2330
|
const list2 = index.get(e.name);
|
|
1551
|
-
if (list2) list2.push(
|
|
1552
|
-
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)]);
|
|
1553
2333
|
}
|
|
1554
2334
|
}
|
|
1555
2335
|
};
|
|
@@ -1557,10 +2337,10 @@ function indexFiles(roots) {
|
|
|
1557
2337
|
return index;
|
|
1558
2338
|
}
|
|
1559
2339
|
function resolve(ref, roots) {
|
|
1560
|
-
const candidates =
|
|
2340
|
+
const candidates = path12.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path12.join(r, ref));
|
|
1561
2341
|
return candidates.find((c) => {
|
|
1562
2342
|
try {
|
|
1563
|
-
return
|
|
2343
|
+
return fs12.statSync(c).isFile() && inside(c, roots);
|
|
1564
2344
|
} catch {
|
|
1565
2345
|
return false;
|
|
1566
2346
|
}
|
|
@@ -1568,21 +2348,21 @@ function resolve(ref, roots) {
|
|
|
1568
2348
|
}
|
|
1569
2349
|
function inside(file, roots) {
|
|
1570
2350
|
return roots.some((r) => {
|
|
1571
|
-
const rel =
|
|
1572
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
2351
|
+
const rel = path12.relative(r, path12.resolve(file));
|
|
2352
|
+
return rel !== "" && !rel.startsWith("..") && !path12.isAbsolute(rel);
|
|
1573
2353
|
});
|
|
1574
2354
|
}
|
|
1575
2355
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1576
2356
|
|
|
1577
2357
|
// src/core/templates.ts
|
|
1578
|
-
import
|
|
1579
|
-
import
|
|
2358
|
+
import fs13 from "node:fs";
|
|
2359
|
+
import path14 from "node:path";
|
|
1580
2360
|
|
|
1581
2361
|
// src/core/install.ts
|
|
1582
|
-
import
|
|
1583
|
-
import { fileURLToPath as
|
|
2362
|
+
import path13 from "node:path";
|
|
2363
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1584
2364
|
function packageRoot() {
|
|
1585
|
-
return
|
|
2365
|
+
return path13.resolve(path13.dirname(fileURLToPath3(import.meta.url)), "..");
|
|
1586
2366
|
}
|
|
1587
2367
|
|
|
1588
2368
|
// src/core/templates.ts
|
|
@@ -1593,9 +2373,9 @@ var FILES = {
|
|
|
1593
2373
|
"code-reviewer": "pitroom-review/code-reviewer.md"
|
|
1594
2374
|
};
|
|
1595
2375
|
function loadTemplate(name, root = packageRoot()) {
|
|
1596
|
-
const file =
|
|
1597
|
-
if (!
|
|
1598
|
-
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*/, "");
|
|
1599
2379
|
}
|
|
1600
2380
|
function fill(template, values) {
|
|
1601
2381
|
const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
|
|
@@ -1651,37 +2431,399 @@ function parseAudit(text) {
|
|
|
1651
2431
|
return { verdict, disputed };
|
|
1652
2432
|
}
|
|
1653
2433
|
|
|
1654
|
-
// src/core/
|
|
1655
|
-
import
|
|
1656
|
-
import
|
|
1657
|
-
|
|
1658
|
-
var
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
2434
|
+
// src/core/config.ts
|
|
2435
|
+
import fs14 from "node:fs";
|
|
2436
|
+
import os9 from "node:os";
|
|
2437
|
+
import path15 from "node:path";
|
|
2438
|
+
var SCHEMA = {
|
|
2439
|
+
worker: "string",
|
|
2440
|
+
fallback: "string[]",
|
|
2441
|
+
timeout: "string",
|
|
2442
|
+
primary: "string",
|
|
2443
|
+
price: "string",
|
|
2444
|
+
link: "string[]",
|
|
2445
|
+
web: "boolean",
|
|
2446
|
+
maxParallel: "number",
|
|
2447
|
+
models: "record",
|
|
2448
|
+
tiers: "record",
|
|
2449
|
+
costs: "numbers",
|
|
2450
|
+
audit: "number",
|
|
2451
|
+
auditFix: "boolean",
|
|
2452
|
+
auditFocus: "boolean",
|
|
2453
|
+
readIn: "string",
|
|
2454
|
+
cacheDays: "number",
|
|
2455
|
+
countRateLimits: "boolean",
|
|
2456
|
+
mcpDash: "boolean",
|
|
2457
|
+
notify: "boolean",
|
|
2458
|
+
notifyCommand: "string",
|
|
2459
|
+
notifyAfter: "number",
|
|
2460
|
+
workerPrices: "record",
|
|
2461
|
+
priceFeed: "boolean",
|
|
2462
|
+
priceFeedUrl: "string",
|
|
2463
|
+
priceFeedHours: "number",
|
|
2464
|
+
budgetDaily: "number",
|
|
2465
|
+
rankWorkers: "boolean"
|
|
2466
|
+
};
|
|
2467
|
+
function configPath() {
|
|
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");
|
|
2471
|
+
}
|
|
2472
|
+
var cached;
|
|
2473
|
+
function loadConfig() {
|
|
2474
|
+
if (cached) return cached;
|
|
2475
|
+
const file = configPath();
|
|
2476
|
+
const config = {};
|
|
2477
|
+
const warnings2 = [];
|
|
2478
|
+
if (fs14.existsSync(file)) {
|
|
2479
|
+
let raw;
|
|
2480
|
+
try {
|
|
2481
|
+
raw = JSON.parse(fs14.readFileSync(file, "utf8"));
|
|
2482
|
+
} catch (e) {
|
|
2483
|
+
warnings2.push(`${file}: invalid JSON (${e.message}); ignored`);
|
|
1672
2484
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
2485
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
2486
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
2487
|
+
const type = SCHEMA[key];
|
|
2488
|
+
if (!type) {
|
|
2489
|
+
warnings2.push(`${file}: unknown key "${key}" ignored`);
|
|
2490
|
+
} else if (matches(value, type)) {
|
|
2491
|
+
config[key] = value;
|
|
2492
|
+
} else {
|
|
2493
|
+
warnings2.push(`${file}: "${key}" must be ${type}; ignored`);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
cached = { config, warnings: warnings2 };
|
|
2499
|
+
return cached;
|
|
1680
2500
|
}
|
|
1681
|
-
function
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
2501
|
+
function matches(v, type) {
|
|
2502
|
+
if (type === "string[]") return Array.isArray(v) && v.every((s) => typeof s === "string");
|
|
2503
|
+
if (type === "record") {
|
|
2504
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((s) => typeof s === "string");
|
|
2505
|
+
}
|
|
2506
|
+
if (type === "numbers") {
|
|
2507
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0);
|
|
2508
|
+
}
|
|
2509
|
+
if (type === "number") return typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
2510
|
+
return typeof v === type;
|
|
2511
|
+
}
|
|
2512
|
+
var DEFAULT_PARALLEL = 20;
|
|
2513
|
+
var MAX_PARALLEL_LIMIT = 30;
|
|
2514
|
+
var clampParallel = (s) => ({ ...s, value: Math.min(s.value, MAX_PARALLEL_LIMIT) });
|
|
2515
|
+
var positiveInt = (v) => {
|
|
2516
|
+
const n = Number(v);
|
|
2517
|
+
return v !== void 0 && v !== "" && Number.isInteger(n) && n > 0 ? n : void 0;
|
|
2518
|
+
};
|
|
2519
|
+
var rate = (v) => {
|
|
2520
|
+
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
2521
|
+
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.min(n, 1) : void 0;
|
|
2522
|
+
};
|
|
2523
|
+
var amount = (v) => {
|
|
2524
|
+
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
2525
|
+
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
2526
|
+
};
|
|
2527
|
+
var days = (v) => {
|
|
2528
|
+
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
2529
|
+
return typeof n === "number" && Number.isInteger(n) && n >= 0 ? n : void 0;
|
|
2530
|
+
};
|
|
2531
|
+
var flag01 = (v) => {
|
|
2532
|
+
const t = v?.trim().toLowerCase();
|
|
2533
|
+
return t === void 0 || t === "" ? void 0 : ["1", "true", "on", "yes"].includes(t) ? true : ["0", "false", "off", "no"].includes(t) ? false : void 0;
|
|
2534
|
+
};
|
|
2535
|
+
var readIn = (v) => v === "auto" || v === "snapshot" || v === "project" ? v : void 0;
|
|
2536
|
+
var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
|
|
2537
|
+
function setting(flag, env, conf, fallback) {
|
|
2538
|
+
if (flag !== void 0) return { value: flag, source: "flag" };
|
|
2539
|
+
if (env !== void 0) return { value: env, source: "env" };
|
|
2540
|
+
if (conf !== void 0) return { value: conf, source: "config" };
|
|
2541
|
+
return { value: fallback, source: "default" };
|
|
2542
|
+
}
|
|
2543
|
+
function effective(flags = {}) {
|
|
2544
|
+
const c = loadConfig().config;
|
|
2545
|
+
const e = process.env;
|
|
2546
|
+
return {
|
|
2547
|
+
worker: setting(flags.worker, e.PITROOM_WORKER, c.worker, DEFAULT_BACKEND),
|
|
2548
|
+
/** Overrides the model of the preferred worker only. */
|
|
2549
|
+
model: setting(flags.model, e.PITROOM_MODEL, void 0, void 0),
|
|
2550
|
+
fallback: setting(void 0, list(e.PITROOM_FALLBACK), c.fallback, []),
|
|
2551
|
+
timeout: setting(flags.timeout, e.PITROOM_TIMEOUT, c.timeout, "30m"),
|
|
2552
|
+
primary: setting(void 0, e.PITROOM_PRIMARY, c.primary, "sonnet"),
|
|
2553
|
+
price: setting(void 0, e.PITROOM_PRICE, c.price, void 0),
|
|
2554
|
+
link: setting(void 0, void 0, c.link, []),
|
|
2555
|
+
web: setting(void 0, void 0, c.web, false),
|
|
2556
|
+
maxParallel: clampParallel(setting(void 0, positiveInt(e.PITROOM_MAX_PARALLEL), positiveInt(c.maxParallel), DEFAULT_PARALLEL)),
|
|
2557
|
+
models: setting(void 0, void 0, c.models, {}),
|
|
2558
|
+
tiers: setting(void 0, void 0, c.tiers, {}),
|
|
2559
|
+
costs: setting(void 0, void 0, c.costs, {}),
|
|
2560
|
+
audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0),
|
|
2561
|
+
auditFix: setting(void 0, flag01(e.PITROOM_AUDIT_FIX), c.auditFix, false),
|
|
2562
|
+
auditFocus: setting(void 0, flag01(e.PITROOM_AUDIT_FOCUS), c.auditFocus, false),
|
|
2563
|
+
readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto"),
|
|
2564
|
+
cacheDays: setting(void 0, days(e.PITROOM_CACHE_DAYS), days(c.cacheDays), 7),
|
|
2565
|
+
countRateLimits: setting(void 0, void 0, c.countRateLimits, false),
|
|
2566
|
+
mcpDash: setting(void 0, flag01(e.PITROOM_MCP_DASH), c.mcpDash, true),
|
|
2567
|
+
notify: setting(void 0, flag01(e.PITROOM_NOTIFY), c.notify, false),
|
|
2568
|
+
notifyCommand: setting(void 0, e.PITROOM_NOTIFY_COMMAND?.trim() || void 0, c.notifyCommand?.trim() || void 0, void 0),
|
|
2569
|
+
notifyAfter: setting(void 0, days(e.PITROOM_NOTIFY_AFTER), days(c.notifyAfter), 15),
|
|
2570
|
+
workerPrices: setting(void 0, void 0, c.workerPrices, {}),
|
|
2571
|
+
priceFeed: setting(void 0, flag01(e.PITROOM_PRICE_FEED), c.priceFeed, false),
|
|
2572
|
+
priceFeedUrl: setting(void 0, e.PITROOM_PRICE_FEED_URL?.trim() || void 0, c.priceFeedUrl?.trim() || void 0, "https://models.dev/api.json"),
|
|
2573
|
+
priceFeedHours: setting(void 0, positiveInt(e.PITROOM_PRICE_FEED_HOURS), positiveInt(c.priceFeedHours), 24),
|
|
2574
|
+
rankWorkers: setting(void 0, flag01(e.PITROOM_RANK_WORKERS), c.rankWorkers, false),
|
|
2575
|
+
budgetDaily: setting(void 0, amount(e.PITROOM_BUDGET_DAILY), amount(c.budgetDaily), void 0)
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
// src/core/audit.ts
|
|
2580
|
+
var ASKS_FOR_LIST = /\b(all|every|each|list|lists|enumerate|how many|count|which)\b/i;
|
|
2581
|
+
function focusedRate(base, task, record) {
|
|
2582
|
+
if (base <= 0 || base >= 1) return { rate: base, why: [] };
|
|
2583
|
+
let rate2 = base;
|
|
2584
|
+
const why = [];
|
|
2585
|
+
if (ASKS_FOR_LIST.test(task)) {
|
|
2586
|
+
rate2 *= 2;
|
|
2587
|
+
why.push("asks for a list or a count");
|
|
2588
|
+
}
|
|
2589
|
+
if (record && record.audited >= 3 && record.agreed / record.audited < 0.5) {
|
|
2590
|
+
rate2 *= 2;
|
|
2591
|
+
why.push(`the worker's audited answers were confirmed ${record.agreed} of ${record.audited} times`);
|
|
2592
|
+
} else if (record && record.audited >= 5 && record.agreed === record.audited) {
|
|
2593
|
+
rate2 /= 2;
|
|
2594
|
+
why.push(`the worker's ${record.audited} audited answers were all confirmed`);
|
|
2595
|
+
}
|
|
2596
|
+
return { rate: Math.min(rate2, 1), why };
|
|
2597
|
+
}
|
|
2598
|
+
function fixTask(disputed) {
|
|
2599
|
+
return [
|
|
2600
|
+
"Another worker audited your answer above and disputed these claims:",
|
|
2601
|
+
...disputed.map((d) => `- ${d}`),
|
|
2602
|
+
"",
|
|
2603
|
+
"Check each one yourself against the files (the auditor can be wrong too). Then give your whole answer again,",
|
|
2604
|
+
"corrected, in the same answer format: keep what holds, fix what does not, and add what was missing.",
|
|
2605
|
+
"Under OPEN ISSUES, say which disputed claims you accepted and which you kept, and why."
|
|
2606
|
+
].join("\n");
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
// src/core/rank.ts
|
|
2610
|
+
var MIN_RUNS = 5;
|
|
2611
|
+
var MIN_AUDITS = 3;
|
|
2612
|
+
function scoreOf(r) {
|
|
2613
|
+
if (!r || r.runs < MIN_RUNS) return void 0;
|
|
2614
|
+
const ok = Math.min(r.ok, r.runs) / r.runs;
|
|
2615
|
+
return r.audited >= MIN_AUDITS ? ok * (r.agreed / r.audited) : ok;
|
|
2616
|
+
}
|
|
2617
|
+
var pct = (x) => `${Math.round(x * 100)}%`;
|
|
2618
|
+
function rankChain(chain, recordOf) {
|
|
2619
|
+
const scored = chain.map((t, i) => ({ t, i, score: scoreOf(recordOf(t)) }));
|
|
2620
|
+
const slots = scored.filter((s) => s.score !== void 0);
|
|
2621
|
+
if (slots.length < 2) return { chain };
|
|
2622
|
+
const ranked = [...slots].sort((a, b) => Math.round(b.score * 10) - Math.round(a.score * 10) || a.i - b.i);
|
|
2623
|
+
const out = [...chain];
|
|
2624
|
+
slots.forEach((slot, k) => out[slot.i] = ranked[k].t);
|
|
2625
|
+
if (out[0] === chain[0]) return { chain: out };
|
|
2626
|
+
const by = (t) => scored.find((s) => s.t === t).score;
|
|
2627
|
+
return {
|
|
2628
|
+
chain: out,
|
|
2629
|
+
note: `ranked by record ("rankWorkers"): ${describeTarget(out[0])} (${pct(by(out[0]))}) goes before ${describeTarget(chain[0])} (${pct(by(chain[0]))})`
|
|
2630
|
+
};
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// src/core/receipt.ts
|
|
2634
|
+
import fs16 from "node:fs";
|
|
2635
|
+
|
|
2636
|
+
// src/core/prices.ts
|
|
2637
|
+
import fs15 from "node:fs";
|
|
2638
|
+
import path16 from "node:path";
|
|
2639
|
+
var RETRY_AFTER_FAILURE_MS = 60 * 6e4;
|
|
2640
|
+
var catalogFile = () => path16.join(home(), "prices.json");
|
|
2641
|
+
function trim(raw) {
|
|
2642
|
+
const out = {};
|
|
2643
|
+
if (!raw || typeof raw !== "object") return out;
|
|
2644
|
+
for (const [provider, p] of Object.entries(raw)) {
|
|
2645
|
+
for (const [id2, m] of Object.entries(p?.models ?? {})) {
|
|
2646
|
+
const c = m?.cost;
|
|
2647
|
+
const ok = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
2648
|
+
if (!c || !ok(c.input) || !ok(c.output)) continue;
|
|
2649
|
+
out[`${provider}/${id2}`] = [c.input, c.output, ok(c.cache_read) ? c.cache_read : c.input / 10];
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
return out;
|
|
2653
|
+
}
|
|
2654
|
+
var cached2;
|
|
2655
|
+
function readCatalog() {
|
|
2656
|
+
const file = catalogFile();
|
|
2657
|
+
try {
|
|
2658
|
+
const { mtimeMs } = fs15.statSync(file);
|
|
2659
|
+
if (cached2?.file === file && cached2.mtimeMs === mtimeMs) return cached2.catalog;
|
|
2660
|
+
const parsed = JSON.parse(fs15.readFileSync(file, "utf8"));
|
|
2661
|
+
const catalog6 = parsed && typeof parsed.models === "object" && parsed.models && Number.isFinite(Date.parse(parsed.fetchedAt)) ? parsed : void 0;
|
|
2662
|
+
cached2 = { file, mtimeMs, catalog: catalog6 };
|
|
2663
|
+
return catalog6;
|
|
2664
|
+
} catch {
|
|
2665
|
+
return void 0;
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
var OWN = { codex: ["openai"], claude: ["anthropic"], gemini: ["google"] };
|
|
2669
|
+
function catalogPrice(backend, ...models) {
|
|
2670
|
+
const catalog6 = readCatalog();
|
|
2671
|
+
if (!catalog6) return void 0;
|
|
2672
|
+
for (const raw of models) {
|
|
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];
|
|
2677
|
+
const triple = any ? catalog6.models[any] : void 0;
|
|
2678
|
+
if (any && triple && triple.length === 3 && triple.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2679
|
+
const [input, output, cachedInput] = triple;
|
|
2680
|
+
return { input, output, cachedInput, key: any };
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
return void 0;
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
// src/core/receipt.ts
|
|
2687
|
+
var PRESETS = {
|
|
2688
|
+
sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
|
|
2689
|
+
opus: { name: "Claude Opus", input: 5, output: 25, cachedInput: 0.5 },
|
|
2690
|
+
haiku: { name: "Claude Haiku", input: 1, output: 5, cachedInput: 0.1 },
|
|
2691
|
+
"gpt-5": { name: "GPT-5", input: 1.25, output: 10, cachedInput: 0.125 }
|
|
2692
|
+
};
|
|
2693
|
+
function primaryPrice() {
|
|
2694
|
+
const eff = effective();
|
|
2695
|
+
const custom = eff.price.value?.split(",").map(Number);
|
|
2696
|
+
if (custom && custom.length >= 2 && custom.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2697
|
+
return { name: "custom", input: custom[0], output: custom[1], cachedInput: custom[2] ?? custom[0] / 10 };
|
|
2698
|
+
}
|
|
2699
|
+
const named = eff.primary.value;
|
|
2700
|
+
const preset = PRESETS[named.toLowerCase()];
|
|
2701
|
+
if (preset) return preset;
|
|
2702
|
+
const c = eff.priceFeed.value ? catalogPrice("", named) : void 0;
|
|
2703
|
+
return c ? { name: named, source: "catalog", input: c.input, output: c.output, cachedInput: c.cachedInput } : PRESETS.sonnet;
|
|
2704
|
+
}
|
|
2705
|
+
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
2706
|
+
function parsePriceSpec(spec, name = "worker") {
|
|
2707
|
+
const parts = typeof spec === "string" ? spec.split(",").map((x) => Number(x.trim())) : [];
|
|
2708
|
+
if (parts.length < 2 || parts.length > 3 || !parts.every((n) => Number.isFinite(n) && n >= 0)) return void 0;
|
|
2709
|
+
return { name, source: "config", input: parts[0], output: parts[1], cachedInput: parts[2] ?? parts[0] / 10 };
|
|
2710
|
+
}
|
|
2711
|
+
var costAt = (usage, price) => (usage.input * price.input + usage.cacheRead * price.cachedInput + (usage.output + usage.reasoning) * price.output) / 1e6;
|
|
2712
|
+
var workerCostOf = (usage) => usage?.cost ?? usage?.costEstimate;
|
|
2713
|
+
function savedUsd(usage, returnedTokens, price = primaryPrice()) {
|
|
2714
|
+
const readingTheReport = returnedTokens * price.input / 1e6;
|
|
2715
|
+
return Math.max(0, costAt(usage, price) - (workerCostOf(usage) ?? 0) - readingTheReport);
|
|
2716
|
+
}
|
|
2717
|
+
function readLedger(sinceMs) {
|
|
2718
|
+
if (!fs16.existsSync(ledgerFile())) return [];
|
|
2719
|
+
return fs16.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
2720
|
+
try {
|
|
2721
|
+
return [JSON.parse(l)];
|
|
2722
|
+
} catch {
|
|
2723
|
+
return [];
|
|
2724
|
+
}
|
|
2725
|
+
}).filter((e) => !sinceMs || Date.parse(e.at) >= sinceMs);
|
|
2726
|
+
}
|
|
2727
|
+
function totals(entries) {
|
|
2728
|
+
const t = entries.reduce(
|
|
2729
|
+
(a, e) => ({
|
|
2730
|
+
runs: a.runs + 1,
|
|
2731
|
+
tokens: a.tokens + e.tokens,
|
|
2732
|
+
returned: a.returned + e.returned,
|
|
2733
|
+
workerCost: a.workerCost + e.workerCost,
|
|
2734
|
+
saved: a.saved + e.saved
|
|
2735
|
+
}),
|
|
2736
|
+
{ runs: 0, tokens: 0, returned: 0, workerCost: 0, saved: 0 }
|
|
2737
|
+
);
|
|
2738
|
+
return { ...t, ratio: t.returned ? t.tokens / t.returned : 0 };
|
|
2739
|
+
}
|
|
2740
|
+
var usd = (n) => n >= 100 ? `$${n.toFixed(0)}` : n >= 1 ? `$${n.toFixed(2)}` : `$${n.toFixed(3)}`;
|
|
2741
|
+
function compact(n) {
|
|
2742
|
+
const fmt = (v) => v.toFixed(v >= 100 ? 0 : 1).replace(/\.0$/, "");
|
|
2743
|
+
if (n >= 1e6) return `${fmt(n / 1e6)}M`;
|
|
2744
|
+
if (n >= 1e3) return `${fmt(n / 1e3)}k`;
|
|
2745
|
+
return String(Math.round(n));
|
|
2746
|
+
}
|
|
2747
|
+
function badgeUrl(t) {
|
|
2748
|
+
const msg = encodeURIComponent(`saved ${usd(t.saved)} \xB7 ${compact(t.tokens)} tokens offloaded`).replace(/-/g, "--");
|
|
2749
|
+
return `https://img.shields.io/badge/pitroom-${msg}-7c3aed`;
|
|
2750
|
+
}
|
|
2751
|
+
var esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
2752
|
+
function card(t, period) {
|
|
2753
|
+
const W = 720, H = 316, M = 32, GAP = 12;
|
|
2754
|
+
const tile = (i, value, label) => {
|
|
2755
|
+
const w = (W - 2 * M - 2 * GAP) / 3, x = M + i * (w + GAP);
|
|
2756
|
+
return `<rect x="${x}" y="196" width="${w}" height="72" rx="14" fill="#fff" fill-opacity=".035" stroke="#fff" stroke-opacity=".1"/>
|
|
2757
|
+
<text x="${x + 18}" y="224" class="lab">${esc(label.toUpperCase())}</text><text x="${x + 18}" y="254" class="val">${esc(value)}</text>`;
|
|
2758
|
+
};
|
|
2759
|
+
const when = period.toUpperCase();
|
|
2760
|
+
const pill = 22 + when.length * 7.4;
|
|
2761
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" role="img" aria-label="pitroom saved ${esc(usd(t.saved))}">
|
|
2762
|
+
<style>
|
|
2763
|
+
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Inter,Helvetica,Arial,sans-serif;fill:#f4f4f5}
|
|
2764
|
+
.name{font-size:20px;font-weight:650;letter-spacing:-.01em}
|
|
2765
|
+
.sub{font-size:12.5px;fill:#a1a1aa}
|
|
2766
|
+
.pill{font-size:11px;font-weight:600;letter-spacing:.1em;fill:#a1a1aa}
|
|
2767
|
+
.lab{font-size:11px;font-weight:500;letter-spacing:.09em;fill:#a1a1aa}
|
|
2768
|
+
.big{font-size:60px;font-weight:700;letter-spacing:-.03em;fill:#3ddc97}
|
|
2769
|
+
.val{font-size:26px;font-weight:650;letter-spacing:-.02em}
|
|
2770
|
+
.foot{font-size:11.5px;fill:#71717a}
|
|
2771
|
+
</style>
|
|
2772
|
+
<defs>
|
|
2773
|
+
<radialGradient id="o" cx="1" cy="0" r=".7"><stop offset="0" stop-color="#ff6a2b" stop-opacity=".24"/><stop offset="1" stop-color="#ff6a2b" stop-opacity="0"/></radialGradient>
|
|
2774
|
+
<radialGradient id="b" cx="0" cy="0" r=".7"><stop offset="0" stop-color="#38bdf8" stop-opacity=".16"/><stop offset="1" stop-color="#38bdf8" stop-opacity="0"/></radialGradient>
|
|
2775
|
+
<linearGradient id="m" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#ff6a2b"/><stop offset="1" stop-color="#ffa86b"/></linearGradient>
|
|
2776
|
+
<clipPath id="c"><rect width="${W}" height="${H}" rx="20"/></clipPath>
|
|
2777
|
+
</defs>
|
|
2778
|
+
<g clip-path="url(#c)"><rect width="${W}" height="${H}" fill="#0b0b0f"/><rect width="${W}" height="${H}" fill="url(#o)"/><rect width="${W}" height="${H}" fill="url(#b)"/></g>
|
|
2779
|
+
<rect x=".5" y=".5" width="${W - 1}" height="${H - 1}" rx="19.5" fill="none" stroke="#fff" stroke-opacity=".1"/>
|
|
2780
|
+
<rect x="${M}" y="28" width="38" height="38" rx="11" fill="url(#m)"/>
|
|
2781
|
+
<path d="M4 4h4v4H4zm8 0h4v4h-4zM8 8h4v4H8zm8 0h4v4h-4zM4 12h4v4H4zm8 0h4v4h-4zm-4 4h4v4H8zm8 0h4v4h-4z" fill="#fff" transform="translate(${M + 7} 35) scale(1)"/>
|
|
2782
|
+
<text x="${M + 52}" y="45" class="name">Pitroom</text><text x="${M + 52}" y="63" class="sub">Your agent's pit crew</text>
|
|
2783
|
+
<rect x="${W - M - pill}" y="34" width="${pill}" height="26" rx="13" fill="#fff" fill-opacity=".04" stroke="#fff" stroke-opacity=".14"/>
|
|
2784
|
+
<text x="${W - M - pill / 2}" y="51" class="pill" text-anchor="middle">${esc(when)}</text>
|
|
2785
|
+
<text x="${M}" y="112" class="lab">SAVED ON YOUR MAIN CODING AGENT</text>
|
|
2786
|
+
<text x="${M}" y="168" class="big">${esc(usd(t.saved))}</text>
|
|
2787
|
+
${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.round(t.ratio)}\xD7` : "\u2014", "Context compression")}${tile(2, String(t.runs), "Delegated tasks")}
|
|
2788
|
+
<text x="${M}" y="296" class="foot">Estimated against ${esc(primaryPrice().name)} pricing \xB7 npx pitroom</text>
|
|
2789
|
+
</svg>
|
|
2790
|
+
`;
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
// src/core/ui.ts
|
|
2794
|
+
var WEEK_MS = 7 * 24 * 3600 * 1e3;
|
|
2795
|
+
|
|
2796
|
+
// src/core/plan.ts
|
|
2797
|
+
import fs17 from "node:fs";
|
|
2798
|
+
import path17 from "node:path";
|
|
2799
|
+
var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
2800
|
+
var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
|
|
2801
|
+
function structure(lines) {
|
|
2802
|
+
const headings = [];
|
|
2803
|
+
const rules = [];
|
|
2804
|
+
const fenced = [];
|
|
2805
|
+
let open;
|
|
2806
|
+
lines.forEach((l, i) => {
|
|
2807
|
+
const f = FENCE.exec(l);
|
|
2808
|
+
if (f) {
|
|
2809
|
+
fenced[i] = true;
|
|
2810
|
+
const mark = f[1];
|
|
2811
|
+
if (!open) open = mark;
|
|
2812
|
+
else if (mark[0] === open[0] && mark.length >= open.length && !f[2].trim()) open = void 0;
|
|
2813
|
+
return;
|
|
2814
|
+
}
|
|
2815
|
+
fenced[i] = !!open;
|
|
2816
|
+
if (open) return;
|
|
2817
|
+
const h = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/.exec(l);
|
|
2818
|
+
if (h) headings.push({ line: i, level: h[1].length, text: h[2] });
|
|
2819
|
+
else if (RULE.test(l)) rules.push(i);
|
|
2820
|
+
});
|
|
2821
|
+
return { headings, rules, fenced };
|
|
2822
|
+
}
|
|
2823
|
+
function parsePlan(text, file = "") {
|
|
2824
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
2825
|
+
const { headings, rules, fenced } = structure(lines);
|
|
2826
|
+
const after = (line) => Math.min(headings.find((h) => h.line > line)?.line ?? lines.length, rules.find((r) => r > line) ?? lines.length);
|
|
1685
2827
|
const titleHeading = headings.find((h) => h.level === 1);
|
|
1686
2828
|
const start = titleHeading ? titleHeading.line + 1 : 0;
|
|
1687
2829
|
const header = lines.slice(start, after(start - 1)).filter((l) => !/^\s*>/.test(l)).join("\n").trim();
|
|
@@ -1700,9 +2842,9 @@ function parsePlan(text, file = "") {
|
|
|
1700
2842
|
return { file, title: titleHeading?.text ?? "", header, constraints, tasks };
|
|
1701
2843
|
}
|
|
1702
2844
|
function loadPlan(file) {
|
|
1703
|
-
const abs =
|
|
1704
|
-
if (!
|
|
1705
|
-
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));
|
|
1706
2848
|
if (!plan.tasks.length) throw new UserError(`${file} has no "Task N" headings (see pitroom-writing-plans)`);
|
|
1707
2849
|
return plan;
|
|
1708
2850
|
}
|
|
@@ -1721,11 +2863,157 @@ function brief(plan, task) {
|
|
|
1721
2863
|
].filter(Boolean).join("\n\n")}
|
|
1722
2864
|
`;
|
|
1723
2865
|
}
|
|
1724
|
-
var planName = (file) =>
|
|
2866
|
+
var planName = (file) => path17.basename(file).replace(/\.md$/i, "");
|
|
1725
2867
|
|
|
1726
|
-
// src/
|
|
1727
|
-
|
|
1728
|
-
|
|
2868
|
+
// src/core/style.ts
|
|
2869
|
+
var enabled = () => {
|
|
2870
|
+
if (process.env.NO_COLOR) return false;
|
|
2871
|
+
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
|
|
2872
|
+
return !!process.stdout.isTTY && process.env.TERM !== "dumb";
|
|
2873
|
+
};
|
|
2874
|
+
var wrap = (open, close) => (text) => enabled() ? `\x1B[${open}m${text}\x1B[${close}m` : text;
|
|
2875
|
+
var bold = wrap(1, 22);
|
|
2876
|
+
var dim = wrap(2, 22);
|
|
2877
|
+
var red = wrap(31, 39);
|
|
2878
|
+
var green = wrap(32, 39);
|
|
2879
|
+
var yellow = wrap(33, 39);
|
|
2880
|
+
var blue = wrap(34, 39);
|
|
2881
|
+
var cyan = wrap(36, 39);
|
|
2882
|
+
|
|
2883
|
+
// src/core/group.ts
|
|
2884
|
+
var retryShell = (platform = process.platform) => platform === "win32" ? "powershell" : "sh";
|
|
2885
|
+
var quoteFor = (shell) => (s) => /^[\w./:@=\\-]+$/.test(s) && !(shell === "sh" && s.includes("\\")) ? s : shell === "sh" ? `'${s.replace(/'/g, `'\\''`)}'` : `'${s.replace(/'/g, "''")}'`;
|
|
2886
|
+
function retryCommands(metas, shell = retryShell()) {
|
|
2887
|
+
const quote = quoteFor(shell);
|
|
2888
|
+
return metas.filter((m) => ["failed", "timeout", "stopped"].includes(m.state) && !m.auditOf && !m.reviewOf && !m.parent && !m.fixOf).map((m) => {
|
|
2889
|
+
const where = ["-d", quote(m.dir), ...m.group ? ["-g", quote(m.group)] : []];
|
|
2890
|
+
if (m.plan) return ["pitroom run", m.mode === "write" ? "-w" : "-i", "--plan", quote(m.plan.file), "--step", String(m.plan.step), ...where, "--bg"].join(" ");
|
|
2891
|
+
const mode = m.mode === "isolate" ? ["-i"] : m.mode === "write" ? ["-w"] : [];
|
|
2892
|
+
const end = shell === "sh" || m.task.startsWith("-") ? ["--"] : [];
|
|
2893
|
+
return ["pitroom run", ...mode, ...where, "--bg", ...end, quote(m.task)].join(" ");
|
|
2894
|
+
});
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
// src/core/eval.ts
|
|
2898
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
2899
|
+
import fs18 from "node:fs";
|
|
2900
|
+
var KINDS = ["definition", "count", "set"];
|
|
2901
|
+
function loadEval(file) {
|
|
2902
|
+
let raw;
|
|
2903
|
+
try {
|
|
2904
|
+
raw = JSON.parse(fs18.readFileSync(file, "utf8"));
|
|
2905
|
+
} catch (e) {
|
|
2906
|
+
throw new UserError(`cannot read ${file}: ${e.message}`);
|
|
2907
|
+
}
|
|
2908
|
+
const list2 = raw?.questions;
|
|
2909
|
+
if (!Array.isArray(list2) || !list2.length) throw new UserError(`${file}: expected {"questions": [{"id", "task", "kind", "truth"}, \u2026]}`);
|
|
2910
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2911
|
+
return list2.map((q, i) => {
|
|
2912
|
+
const where = `${file}: question ${i + 1}`;
|
|
2913
|
+
if (!q || typeof q !== "object") throw new UserError(`${where} is not an object`);
|
|
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(", ")}`);
|
|
2919
|
+
const t = q.truth;
|
|
2920
|
+
const grep = t && typeof t === "object" && !Array.isArray(t) && typeof t.grep === "string" && t.grep;
|
|
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");
|
|
2922
|
+
if (!grep && !literal) {
|
|
2923
|
+
const want = { definition: '"path:line"', count: "a whole number", set: "a list of paths" }[q.kind];
|
|
2924
|
+
throw new UserError(`${where} (${id2}): truth must be ${want} or {"grep": "pattern", \u2026}`);
|
|
2925
|
+
}
|
|
2926
|
+
return { id: id2, task: q.task.trim(), kind: q.kind, truth: t };
|
|
2927
|
+
});
|
|
2928
|
+
}
|
|
2929
|
+
var clean = (p) => p.replace(/\\/g, "/").replace(/^\.?\//, "");
|
|
2930
|
+
function gitGrep(root, flags, t) {
|
|
2931
|
+
const r = spawnSync8("git", ["-C", root, "grep", ...flags, t.regex ? "-E" : "-F", ...t.word ? ["-w"] : [], "-e", t.grep, "--", t.dir ?? "."], {
|
|
2932
|
+
encoding: "utf8",
|
|
2933
|
+
maxBuffer: 64 * 1024 * 1024
|
|
2934
|
+
});
|
|
2935
|
+
if (r.status === 1) return [];
|
|
2936
|
+
if (r.status !== 0) throw new UserError(`git grep "${t.grep}" failed: ${(r.stderr || r.error?.message || "").trim()}`);
|
|
2937
|
+
const exclude = t.exclude ? new RegExp(t.exclude) : void 0;
|
|
2938
|
+
return r.stdout.split("\n").filter(Boolean).filter((l) => !exclude?.test(l.split(":")[0]));
|
|
2939
|
+
}
|
|
2940
|
+
function truthOf(q, root) {
|
|
2941
|
+
const t = q.truth;
|
|
2942
|
+
if (t && typeof t === "object" && !Array.isArray(t)) {
|
|
2943
|
+
if (!root) throw new UserError(`question ${q.id}: a "grep" truth needs a git repository`);
|
|
2944
|
+
if (q.kind === "definition") {
|
|
2945
|
+
const hit = gitGrep(root, ["-n"], t).map((l) => /^(.*?):(\d+):/.exec(l)).find(Boolean);
|
|
2946
|
+
if (!hit) throw new UserError(`question ${q.id}: git grep found no definition for "${t.grep}"`);
|
|
2947
|
+
return { path: clean(hit[1]), line: Number(hit[2]) };
|
|
2948
|
+
}
|
|
2949
|
+
const files = [...new Set(gitGrep(root, ["-l"], t).map(clean))];
|
|
2950
|
+
return q.kind === "count" ? { count: files.length } : { files };
|
|
2951
|
+
}
|
|
2952
|
+
if (q.kind === "definition") {
|
|
2953
|
+
const m = /^(.*):(\d+)$/.exec(t);
|
|
2954
|
+
return { path: clean(m[1]), line: Number(m[2]) };
|
|
2955
|
+
}
|
|
2956
|
+
if (q.kind === "count") return { count: t };
|
|
2957
|
+
return { files: [...new Set(t.map(clean))] };
|
|
2958
|
+
}
|
|
2959
|
+
function answerParts(answer) {
|
|
2960
|
+
const text = answer.replace(/`/g, "");
|
|
2961
|
+
const s = /SUMMARY:\s*([\s\S]*?)(?=^\s*(?:DETAILS|FILES CHANGED|VERIFICATION|OPEN ISSUES):|$(?![\s\S]))/m.exec(text);
|
|
2962
|
+
const d = /^\s*DETAILS:\s*([\s\S]*?)(?=^\s*(?:FILES CHANGED|VERIFICATION|OPEN ISSUES):|$(?![\s\S]))/m.exec(text);
|
|
2963
|
+
const summary = s ? s[1] : text.split(/^\s*(?:DETAILS|FILES CHANGED|VERIFICATION|OPEN ISSUES):/m)[0];
|
|
2964
|
+
return { summary: summary.trim(), details: (d?.[1] ?? "").trim() };
|
|
2965
|
+
}
|
|
2966
|
+
function pathsIn(text) {
|
|
2967
|
+
const found = text.replace(/\\/g, "/").match(/[A-Za-z0-9_.@\-/]+\.[A-Za-z0-9]{1,8}(?::\d+)?/g) ?? [];
|
|
2968
|
+
return [...new Set(found.map((p) => clean(p.replace(/:\d+$/, ""))))];
|
|
2969
|
+
}
|
|
2970
|
+
function scoreAnswer(q, truth, answer, root) {
|
|
2971
|
+
const prefix = root ? `${clean(root).replace(/\/$/, "")}/` : "";
|
|
2972
|
+
const text = prefix ? answer.replace(/\\/g, "/").split(prefix).join("") : answer;
|
|
2973
|
+
const { summary, details } = answerParts(text);
|
|
2974
|
+
if (!summary && !details) return { score: 0, got: "(no answer)" };
|
|
2975
|
+
if ("line" in truth) {
|
|
2976
|
+
const flat = summary.replace(/\s+/g, " ");
|
|
2977
|
+
if (new RegExp(`(^|[^\\w./-])\\.?/?${escape2(truth.path)}:${truth.line}(?!\\d)`).test(flat)) return { score: 1, got: `${truth.path}:${truth.line}` };
|
|
2978
|
+
const named = pathsIn(flat).includes(truth.path);
|
|
2979
|
+
return { score: named ? 0.5 : 0, got: flat.slice(0, 120) };
|
|
2980
|
+
}
|
|
2981
|
+
if ("count" in truth) {
|
|
2982
|
+
const n = /\d[\d,]*/.exec(summary);
|
|
2983
|
+
const got2 = n ? Number(n[0].replace(/,/g, "")) : void 0;
|
|
2984
|
+
return { score: got2 === truth.count ? 1 : 0, got: got2 === void 0 ? "(no number)" : String(got2) };
|
|
2985
|
+
}
|
|
2986
|
+
let got = pathsIn(summary);
|
|
2987
|
+
if (!got.length) got = pathsIn(details);
|
|
2988
|
+
const hit = got.filter((p) => truth.files.includes(p)).length;
|
|
2989
|
+
const precision = got.length ? hit / got.length : 0;
|
|
2990
|
+
const recall = truth.files.length ? hit / truth.files.length : got.length ? 0 : 1;
|
|
2991
|
+
const f1 = precision + recall ? 2 * precision * recall / (precision + recall) : 0;
|
|
2992
|
+
return { score: !truth.files.length && !got.length ? 1 : f1, got: `${hit}/${truth.files.length} found, ${got.length - hit} extra` };
|
|
2993
|
+
}
|
|
2994
|
+
var escape2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2995
|
+
var mean = (a) => a.length ? a.reduce((x, y) => x + y, 0) / a.length : 0;
|
|
2996
|
+
var median = (a) => {
|
|
2997
|
+
const s = a.filter((x) => x !== void 0).sort((x, y) => x - y);
|
|
2998
|
+
return s.length ? s[Math.floor(s.length / 2)] : void 0;
|
|
2999
|
+
};
|
|
3000
|
+
function summarize(rows) {
|
|
3001
|
+
const by = /* @__PURE__ */ new Map();
|
|
3002
|
+
for (const r of rows) by.set(r.worker, [...by.get(r.worker) ?? [], r]);
|
|
3003
|
+
return [...by.entries()].map(([worker, rs]) => ({
|
|
3004
|
+
worker,
|
|
3005
|
+
questions: rs.length,
|
|
3006
|
+
failed: rs.filter((r) => r.state !== "done").length,
|
|
3007
|
+
score: mean(rs.map((r) => r.score)),
|
|
3008
|
+
byKind: Object.fromEntries(KINDS.filter((k) => rs.some((r) => r.kind === k)).map((k) => [k, mean(rs.filter((r) => r.kind === k).map((r) => r.score))])),
|
|
3009
|
+
medianSeconds: median(rs.map((r) => r.seconds)),
|
|
3010
|
+
medianTokens: median(rs.map((r) => r.tokens))
|
|
3011
|
+
})).sort((a, b) => b.score - a.score || (a.medianSeconds ?? 1e9) - (b.medianSeconds ?? 1e9));
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
// src/vcs/guard.ts
|
|
3015
|
+
import fs19 from "node:fs";
|
|
3016
|
+
import path18 from "node:path";
|
|
1729
3017
|
var VERSION = 2;
|
|
1730
3018
|
var WINDOWS = process.platform === "win32";
|
|
1731
3019
|
var fwd = (p) => WINDOWS ? p.replace(/\\/g, "/") : p;
|
|
@@ -1802,29 +3090,29 @@ esac
|
|
|
1802
3090
|
exec "$real" "$@"
|
|
1803
3091
|
`;
|
|
1804
3092
|
function shimDir() {
|
|
1805
|
-
return
|
|
3093
|
+
return path18.join(home(), "shim", `v${VERSION}`);
|
|
1806
3094
|
}
|
|
1807
3095
|
function findRealGit(skip) {
|
|
1808
3096
|
const same = (a, b) => WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
1809
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
1810
|
-
if (same(
|
|
1811
|
-
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");
|
|
1812
3100
|
try {
|
|
1813
|
-
|
|
1814
|
-
if (
|
|
3101
|
+
fs19.accessSync(p, fs19.constants.X_OK);
|
|
3102
|
+
if (fs19.statSync(p).isFile()) return p;
|
|
1815
3103
|
} catch {
|
|
1816
3104
|
}
|
|
1817
3105
|
}
|
|
1818
3106
|
return void 0;
|
|
1819
3107
|
}
|
|
1820
3108
|
function findGitSh(realGit) {
|
|
1821
|
-
let dir =
|
|
3109
|
+
let dir = path18.dirname(realGit);
|
|
1822
3110
|
for (let i = 0; i < 4; i++) {
|
|
1823
3111
|
for (const rel of ["bin/sh.exe", "usr/bin/sh.exe"]) {
|
|
1824
|
-
const p =
|
|
1825
|
-
if (
|
|
3112
|
+
const p = path18.join(dir, rel);
|
|
3113
|
+
if (fs19.existsSync(p)) return p;
|
|
1826
3114
|
}
|
|
1827
|
-
const up =
|
|
3115
|
+
const up = path18.dirname(dir);
|
|
1828
3116
|
if (up === dir) break;
|
|
1829
3117
|
dir = up;
|
|
1830
3118
|
}
|
|
@@ -1842,12 +3130,12 @@ var REF_HOOK = `#!/bin/sh
|
|
|
1842
3130
|
echo "pitroom: git ref updates (commit, reset, branch, tag, stash, rebase, merge) are blocked for workers" >&2
|
|
1843
3131
|
exit 1
|
|
1844
3132
|
`;
|
|
1845
|
-
var hooksDir = () =>
|
|
1846
|
-
function
|
|
1847
|
-
if (
|
|
1848
|
-
|
|
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 });
|
|
1849
3137
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
1850
|
-
|
|
3138
|
+
fs19.writeFileSync(tmp, content, { mode: 493 });
|
|
1851
3139
|
renameOver(tmp, file);
|
|
1852
3140
|
}
|
|
1853
3141
|
function pathKey(env) {
|
|
@@ -1879,7 +3167,7 @@ function guardEnv(env) {
|
|
|
1879
3167
|
GIT_PAGER: "cat",
|
|
1880
3168
|
PAGER: "cat"
|
|
1881
3169
|
};
|
|
1882
|
-
|
|
3170
|
+
writeIfChanged2(path18.join(hooksDir(), "reference-transaction"), REF_HOOK);
|
|
1883
3171
|
env = withGitConfig(onePath(env), [
|
|
1884
3172
|
["core.hooksPath", fwd(hooksDir())],
|
|
1885
3173
|
["url.pitroom-push-blocked://.pushInsteadOf", ""]
|
|
@@ -1891,11 +3179,11 @@ function guardEnv(env) {
|
|
|
1891
3179
|
if (WINDOWS) {
|
|
1892
3180
|
sh = findGitSh(real);
|
|
1893
3181
|
if (!sh) return { ...env, ...quiet };
|
|
1894
|
-
|
|
3182
|
+
writeIfChanged2(path18.join(dir, "git.cmd"), cmdShim(sh));
|
|
1895
3183
|
}
|
|
1896
|
-
|
|
3184
|
+
writeIfChanged2(path18.join(dir, "git"), SHIM);
|
|
1897
3185
|
const key = pathKey(env);
|
|
1898
|
-
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] ?? ""}` };
|
|
1899
3187
|
}
|
|
1900
3188
|
function shimReady() {
|
|
1901
3189
|
const env = guardEnv({ ...process.env });
|
|
@@ -1903,11 +3191,11 @@ function shimReady() {
|
|
|
1903
3191
|
}
|
|
1904
3192
|
|
|
1905
3193
|
// src/core/slots.ts
|
|
1906
|
-
import
|
|
1907
|
-
import
|
|
1908
|
-
import
|
|
1909
|
-
var slotsDir = () =>
|
|
1910
|
-
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");
|
|
1911
3199
|
var STARTUP_GRACE_MS = 3e4;
|
|
1912
3200
|
function holderActive(runId, self) {
|
|
1913
3201
|
if (!runId || runId === self) return false;
|
|
@@ -1921,23 +3209,23 @@ function holderActive(runId, self) {
|
|
|
1921
3209
|
}
|
|
1922
3210
|
}
|
|
1923
3211
|
function tryClaim(file, runId) {
|
|
1924
|
-
|
|
3212
|
+
fs20.mkdirSync(path19.dirname(file), { recursive: true });
|
|
1925
3213
|
try {
|
|
1926
|
-
|
|
3214
|
+
fs20.writeFileSync(file, runId, { flag: "wx" });
|
|
1927
3215
|
return true;
|
|
1928
3216
|
} catch (e) {
|
|
1929
3217
|
if (e.code !== "EEXIST") throw e;
|
|
1930
3218
|
}
|
|
1931
3219
|
let owner = "";
|
|
1932
3220
|
try {
|
|
1933
|
-
owner =
|
|
3221
|
+
owner = fs20.readFileSync(file, "utf8").trim();
|
|
1934
3222
|
} catch {
|
|
1935
3223
|
}
|
|
1936
3224
|
if (owner === runId) return true;
|
|
1937
3225
|
if (holderActive(owner, runId)) return false;
|
|
1938
|
-
|
|
3226
|
+
fs20.rmSync(file, { force: true });
|
|
1939
3227
|
try {
|
|
1940
|
-
|
|
3228
|
+
fs20.writeFileSync(file, runId, { flag: "wx" });
|
|
1941
3229
|
return true;
|
|
1942
3230
|
} catch {
|
|
1943
3231
|
return false;
|
|
@@ -1946,394 +3234,47 @@ function tryClaim(file, runId) {
|
|
|
1946
3234
|
function releaseIfOwner(file, runId) {
|
|
1947
3235
|
if (!file) return;
|
|
1948
3236
|
try {
|
|
1949
|
-
if (
|
|
3237
|
+
if (fs20.readFileSync(file, "utf8").trim() === runId) fs20.rmSync(file, { force: true });
|
|
1950
3238
|
} catch {
|
|
1951
3239
|
}
|
|
1952
3240
|
}
|
|
1953
3241
|
function tryAcquireSlot(runId, maxParallel) {
|
|
1954
3242
|
for (let n = 0; n < Math.max(1, maxParallel); n++) {
|
|
1955
|
-
const file =
|
|
3243
|
+
const file = path19.join(slotsDir(), `slot-${n}`);
|
|
1956
3244
|
if (tryClaim(file, runId)) return file;
|
|
1957
3245
|
}
|
|
1958
3246
|
return void 0;
|
|
1959
3247
|
}
|
|
1960
3248
|
var releaseSlot = (file, runId) => releaseIfOwner(file, runId);
|
|
1961
3249
|
function slotHolders() {
|
|
1962
|
-
if (!
|
|
1963
|
-
return
|
|
3250
|
+
if (!fs20.existsSync(slotsDir())) return [];
|
|
3251
|
+
return fs20.readdirSync(slotsDir()).map((f) => {
|
|
1964
3252
|
try {
|
|
1965
|
-
return
|
|
3253
|
+
return fs20.readFileSync(path19.join(slotsDir(), f), "utf8").trim();
|
|
1966
3254
|
} catch {
|
|
1967
3255
|
return "";
|
|
1968
3256
|
}
|
|
1969
|
-
}).filter((
|
|
3257
|
+
}).filter((id2) => holderActive(id2, ""));
|
|
1970
3258
|
}
|
|
1971
|
-
var lockFile = (repoRoot) =>
|
|
3259
|
+
var lockFile = (repoRoot) => path19.join(locksDir(), `write-${crypto4.createHash("sha1").update(path19.resolve(repoRoot)).digest("hex").slice(0, 16)}`);
|
|
1972
3260
|
function acquireWriteLock(repoRoot, runId) {
|
|
1973
3261
|
const file = lockFile(repoRoot);
|
|
1974
3262
|
if (tryClaim(file, runId)) return;
|
|
1975
3263
|
let owner = "";
|
|
1976
3264
|
try {
|
|
1977
|
-
owner =
|
|
3265
|
+
owner = fs20.readFileSync(file, "utf8").trim();
|
|
1978
3266
|
} catch {
|
|
1979
3267
|
}
|
|
1980
3268
|
throw new UserError(`another --write run (${owner}) is active in this repo; use --isolate for parallel changes`, 3);
|
|
1981
3269
|
}
|
|
1982
3270
|
var releaseWriteLock = (repoRoot, runId) => releaseIfOwner(lockFile(repoRoot), runId);
|
|
1983
3271
|
|
|
1984
|
-
// src/core/secrets.ts
|
|
1985
|
-
import { execFileSync } from "node:child_process";
|
|
1986
|
-
import fs13 from "node:fs";
|
|
1987
|
-
import path13 from "node:path";
|
|
1988
|
-
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "target", "vendor", ".venv", "venv", "__pycache__", "coverage", ".turbo", ".cache"]);
|
|
1989
|
-
var MAX_VISITED = 2e4;
|
|
1990
|
-
var MAX_DEPTH = 4;
|
|
1991
|
-
var TEMPLATE = /\.(example|sample|template|dist|defaults?|tpl)$/i;
|
|
1992
|
-
function looksSecret(file) {
|
|
1993
|
-
const name = file.toLowerCase();
|
|
1994
|
-
if (TEMPLATE.test(name)) return false;
|
|
1995
|
-
if (name === ".env" || name.startsWith(".env.") || name.endsWith(".env")) return true;
|
|
1996
|
-
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);
|
|
1997
|
-
}
|
|
1998
|
-
function findSecretFiles(dir) {
|
|
1999
|
-
const found = [];
|
|
2000
|
-
let visited = 0;
|
|
2001
|
-
const walk = (d, depth) => {
|
|
2002
|
-
if (depth > MAX_DEPTH || visited > MAX_VISITED) return;
|
|
2003
|
-
let entries;
|
|
2004
|
-
try {
|
|
2005
|
-
entries = fs13.readdirSync(d, { withFileTypes: true });
|
|
2006
|
-
} catch {
|
|
2007
|
-
return;
|
|
2008
|
-
}
|
|
2009
|
-
for (const e of entries) {
|
|
2010
|
-
visited++;
|
|
2011
|
-
if (e.isDirectory()) {
|
|
2012
|
-
if (!SKIP_DIRS2.has(e.name)) walk(path13.join(d, e.name), depth + 1);
|
|
2013
|
-
} else if (e.isFile() && looksSecret(e.name)) {
|
|
2014
|
-
found.push(slash(path13.relative(dir, path13.join(d, e.name))));
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
};
|
|
2018
|
-
walk(dir, 0);
|
|
2019
|
-
return found.sort();
|
|
2020
|
-
}
|
|
2021
|
-
function findSecretFilesInTree(root, dir) {
|
|
2022
|
-
try {
|
|
2023
|
-
const out = execFileSync("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
2024
|
-
return out.split("\0").filter((f) => f && looksSecret(path13.basename(f)) && !path13.relative(dir, path13.join(root, f)).startsWith("..")).map((f) => slash(path13.relative(dir, path13.join(root, f)))).sort();
|
|
2025
|
-
} catch {
|
|
2026
|
-
return [];
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
function secretWarning(files, mode) {
|
|
2030
|
-
if (!files.length) return void 0;
|
|
2031
|
-
const shown = files.slice(0, 3).join(", ") + (files.length > 3 ? `, +${files.length - 3} more` : "");
|
|
2032
|
-
const where = mode === "isolate" ? "would be copied into the isolated copy" : "sit in the directory the worker runs in";
|
|
2033
|
-
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";
|
|
2034
|
-
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`;
|
|
2035
|
-
}
|
|
2036
|
-
var slash = (p) => p.split(path13.sep).join("/");
|
|
2037
|
-
|
|
2038
|
-
// src/core/receipt.ts
|
|
2039
|
-
import fs16 from "node:fs";
|
|
2040
|
-
|
|
2041
|
-
// src/core/config.ts
|
|
2042
|
-
import fs14 from "node:fs";
|
|
2043
|
-
import os7 from "node:os";
|
|
2044
|
-
import path14 from "node:path";
|
|
2045
|
-
var SCHEMA = {
|
|
2046
|
-
worker: "string",
|
|
2047
|
-
fallback: "string[]",
|
|
2048
|
-
timeout: "string",
|
|
2049
|
-
primary: "string",
|
|
2050
|
-
price: "string",
|
|
2051
|
-
link: "string[]",
|
|
2052
|
-
web: "boolean",
|
|
2053
|
-
maxParallel: "number",
|
|
2054
|
-
models: "record",
|
|
2055
|
-
tiers: "record",
|
|
2056
|
-
costs: "numbers",
|
|
2057
|
-
audit: "number",
|
|
2058
|
-
readIn: "string",
|
|
2059
|
-
cacheDays: "number",
|
|
2060
|
-
countRateLimits: "boolean",
|
|
2061
|
-
mcpDash: "boolean",
|
|
2062
|
-
notify: "boolean",
|
|
2063
|
-
notifyCommand: "string",
|
|
2064
|
-
notifyAfter: "number",
|
|
2065
|
-
workerPrices: "record",
|
|
2066
|
-
priceFeed: "boolean",
|
|
2067
|
-
priceFeedUrl: "string",
|
|
2068
|
-
priceFeedHours: "number"
|
|
2069
|
-
};
|
|
2070
|
-
function configPath() {
|
|
2071
|
-
if (process.env.PITROOM_CONFIG) return path14.resolve(process.env.PITROOM_CONFIG);
|
|
2072
|
-
const base = process.platform === "win32" ? process.env.APPDATA ?? path14.join(os7.homedir(), "AppData", "Roaming") : process.env.XDG_CONFIG_HOME ?? path14.join(os7.homedir(), ".config");
|
|
2073
|
-
return path14.join(base, "pitroom", "config.json");
|
|
2074
|
-
}
|
|
2075
|
-
var cached;
|
|
2076
|
-
function loadConfig() {
|
|
2077
|
-
if (cached) return cached;
|
|
2078
|
-
const file = configPath();
|
|
2079
|
-
const config = {};
|
|
2080
|
-
const warnings = [];
|
|
2081
|
-
if (fs14.existsSync(file)) {
|
|
2082
|
-
let raw;
|
|
2083
|
-
try {
|
|
2084
|
-
raw = JSON.parse(fs14.readFileSync(file, "utf8"));
|
|
2085
|
-
} catch (e) {
|
|
2086
|
-
warnings.push(`${file}: invalid JSON (${e.message}); ignored`);
|
|
2087
|
-
}
|
|
2088
|
-
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
2089
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
2090
|
-
const type = SCHEMA[key];
|
|
2091
|
-
if (!type) {
|
|
2092
|
-
warnings.push(`${file}: unknown key "${key}" ignored`);
|
|
2093
|
-
} else if (matches(value, type)) {
|
|
2094
|
-
config[key] = value;
|
|
2095
|
-
} else {
|
|
2096
|
-
warnings.push(`${file}: "${key}" must be ${type}; ignored`);
|
|
2097
|
-
}
|
|
2098
|
-
}
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
cached = { config, warnings };
|
|
2102
|
-
return cached;
|
|
2103
|
-
}
|
|
2104
|
-
function matches(v, type) {
|
|
2105
|
-
if (type === "string[]") return Array.isArray(v) && v.every((s) => typeof s === "string");
|
|
2106
|
-
if (type === "record") {
|
|
2107
|
-
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((s) => typeof s === "string");
|
|
2108
|
-
}
|
|
2109
|
-
if (type === "numbers") {
|
|
2110
|
-
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0);
|
|
2111
|
-
}
|
|
2112
|
-
if (type === "number") return typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
2113
|
-
return typeof v === type;
|
|
2114
|
-
}
|
|
2115
|
-
var DEFAULT_PARALLEL = 20;
|
|
2116
|
-
var MAX_PARALLEL_LIMIT = 30;
|
|
2117
|
-
var clampParallel = (s) => ({ ...s, value: Math.min(s.value, MAX_PARALLEL_LIMIT) });
|
|
2118
|
-
var positiveInt = (v) => {
|
|
2119
|
-
const n = Number(v);
|
|
2120
|
-
return v !== void 0 && v !== "" && Number.isInteger(n) && n > 0 ? n : void 0;
|
|
2121
|
-
};
|
|
2122
|
-
var rate = (v) => {
|
|
2123
|
-
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
2124
|
-
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.min(n, 1) : void 0;
|
|
2125
|
-
};
|
|
2126
|
-
var days = (v) => {
|
|
2127
|
-
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
2128
|
-
return typeof n === "number" && Number.isInteger(n) && n >= 0 ? n : void 0;
|
|
2129
|
-
};
|
|
2130
|
-
var flag01 = (v) => {
|
|
2131
|
-
const t = v?.trim().toLowerCase();
|
|
2132
|
-
return t === void 0 || t === "" ? void 0 : ["1", "true", "on", "yes"].includes(t) ? true : ["0", "false", "off", "no"].includes(t) ? false : void 0;
|
|
2133
|
-
};
|
|
2134
|
-
var readIn = (v) => v === "auto" || v === "snapshot" || v === "project" ? v : void 0;
|
|
2135
|
-
var list = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
|
|
2136
|
-
function setting(flag, env, conf, fallback) {
|
|
2137
|
-
if (flag !== void 0) return { value: flag, source: "flag" };
|
|
2138
|
-
if (env !== void 0) return { value: env, source: "env" };
|
|
2139
|
-
if (conf !== void 0) return { value: conf, source: "config" };
|
|
2140
|
-
return { value: fallback, source: "default" };
|
|
2141
|
-
}
|
|
2142
|
-
function effective(flags = {}) {
|
|
2143
|
-
const c = loadConfig().config;
|
|
2144
|
-
const e = process.env;
|
|
2145
|
-
return {
|
|
2146
|
-
worker: setting(flags.worker, e.PITROOM_WORKER, c.worker, DEFAULT_BACKEND),
|
|
2147
|
-
/** Overrides the model of the preferred worker only. */
|
|
2148
|
-
model: setting(flags.model, e.PITROOM_MODEL, void 0, void 0),
|
|
2149
|
-
fallback: setting(void 0, list(e.PITROOM_FALLBACK), c.fallback, []),
|
|
2150
|
-
timeout: setting(flags.timeout, e.PITROOM_TIMEOUT, c.timeout, "30m"),
|
|
2151
|
-
primary: setting(void 0, e.PITROOM_PRIMARY, c.primary, "sonnet"),
|
|
2152
|
-
price: setting(void 0, e.PITROOM_PRICE, c.price, void 0),
|
|
2153
|
-
link: setting(void 0, void 0, c.link, []),
|
|
2154
|
-
web: setting(void 0, void 0, c.web, false),
|
|
2155
|
-
maxParallel: clampParallel(setting(void 0, positiveInt(e.PITROOM_MAX_PARALLEL), positiveInt(c.maxParallel), DEFAULT_PARALLEL)),
|
|
2156
|
-
models: setting(void 0, void 0, c.models, {}),
|
|
2157
|
-
tiers: setting(void 0, void 0, c.tiers, {}),
|
|
2158
|
-
costs: setting(void 0, void 0, c.costs, {}),
|
|
2159
|
-
audit: setting(void 0, rate(e.PITROOM_AUDIT), rate(c.audit), 0),
|
|
2160
|
-
readIn: setting(void 0, readIn(e.PITROOM_READ_IN), readIn(c.readIn), "auto"),
|
|
2161
|
-
cacheDays: setting(void 0, days(e.PITROOM_CACHE_DAYS), days(c.cacheDays), 7),
|
|
2162
|
-
countRateLimits: setting(void 0, void 0, c.countRateLimits, false),
|
|
2163
|
-
mcpDash: setting(void 0, flag01(e.PITROOM_MCP_DASH), c.mcpDash, true),
|
|
2164
|
-
notify: setting(void 0, flag01(e.PITROOM_NOTIFY), c.notify, false),
|
|
2165
|
-
notifyCommand: setting(void 0, e.PITROOM_NOTIFY_COMMAND?.trim() || void 0, c.notifyCommand?.trim() || void 0, void 0),
|
|
2166
|
-
notifyAfter: setting(void 0, days(e.PITROOM_NOTIFY_AFTER), days(c.notifyAfter), 15),
|
|
2167
|
-
workerPrices: setting(void 0, void 0, c.workerPrices, {}),
|
|
2168
|
-
priceFeed: setting(void 0, flag01(e.PITROOM_PRICE_FEED), c.priceFeed, false),
|
|
2169
|
-
priceFeedUrl: setting(void 0, e.PITROOM_PRICE_FEED_URL?.trim() || void 0, c.priceFeedUrl?.trim() || void 0, "https://models.dev/api.json"),
|
|
2170
|
-
priceFeedHours: setting(void 0, positiveInt(e.PITROOM_PRICE_FEED_HOURS), positiveInt(c.priceFeedHours), 24)
|
|
2171
|
-
};
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
// src/core/prices.ts
|
|
2175
|
-
import fs15 from "node:fs";
|
|
2176
|
-
import path15 from "node:path";
|
|
2177
|
-
var RETRY_AFTER_FAILURE_MS = 60 * 6e4;
|
|
2178
|
-
var catalogFile = () => path15.join(home(), "prices.json");
|
|
2179
|
-
function trim(raw) {
|
|
2180
|
-
const out = {};
|
|
2181
|
-
if (!raw || typeof raw !== "object") return out;
|
|
2182
|
-
for (const [provider, p] of Object.entries(raw)) {
|
|
2183
|
-
for (const [id, m] of Object.entries(p?.models ?? {})) {
|
|
2184
|
-
const c = m?.cost;
|
|
2185
|
-
const ok = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
2186
|
-
if (!c || !ok(c.input) || !ok(c.output)) continue;
|
|
2187
|
-
out[`${provider}/${id}`] = [c.input, c.output, ok(c.cache_read) ? c.cache_read : c.input / 10];
|
|
2188
|
-
}
|
|
2189
|
-
}
|
|
2190
|
-
return out;
|
|
2191
|
-
}
|
|
2192
|
-
var cached2;
|
|
2193
|
-
function readCatalog() {
|
|
2194
|
-
const file = catalogFile();
|
|
2195
|
-
try {
|
|
2196
|
-
const { mtimeMs } = fs15.statSync(file);
|
|
2197
|
-
if (cached2?.file === file && cached2.mtimeMs === mtimeMs) return cached2.catalog;
|
|
2198
|
-
const parsed = JSON.parse(fs15.readFileSync(file, "utf8"));
|
|
2199
|
-
const catalog5 = parsed && typeof parsed.models === "object" && parsed.models && Number.isFinite(Date.parse(parsed.fetchedAt)) ? parsed : void 0;
|
|
2200
|
-
cached2 = { file, mtimeMs, catalog: catalog5 };
|
|
2201
|
-
return catalog5;
|
|
2202
|
-
} catch {
|
|
2203
|
-
return void 0;
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
var OWN = { codex: ["openai"], claude: ["anthropic"], gemini: ["google"] };
|
|
2207
|
-
function catalogPrice(backend, ...models) {
|
|
2208
|
-
const catalog5 = readCatalog();
|
|
2209
|
-
if (!catalog5) return void 0;
|
|
2210
|
-
for (const raw of models) {
|
|
2211
|
-
const id = raw?.split("#")[0];
|
|
2212
|
-
if (!id) continue;
|
|
2213
|
-
const keys = [id, ...[...OWN[backend] ?? [], "anthropic", "openai", "google"].map((p) => `${p}/${id}`)];
|
|
2214
|
-
const any = keys.find((k) => catalog5.models[k]) ?? Object.keys(catalog5.models).filter((k) => k.endsWith(`/${id}`)).sort()[0];
|
|
2215
|
-
const triple = any ? catalog5.models[any] : void 0;
|
|
2216
|
-
if (any && triple && triple.length === 3 && triple.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2217
|
-
const [input, output, cachedInput] = triple;
|
|
2218
|
-
return { input, output, cachedInput, key: any };
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
return void 0;
|
|
2222
|
-
}
|
|
2223
|
-
|
|
2224
|
-
// src/core/receipt.ts
|
|
2225
|
-
var PRESETS = {
|
|
2226
|
-
sonnet: { name: "Claude Sonnet", input: 3, output: 15, cachedInput: 0.3 },
|
|
2227
|
-
opus: { name: "Claude Opus", input: 5, output: 25, cachedInput: 0.5 },
|
|
2228
|
-
haiku: { name: "Claude Haiku", input: 1, output: 5, cachedInput: 0.1 },
|
|
2229
|
-
"gpt-5": { name: "GPT-5", input: 1.25, output: 10, cachedInput: 0.125 }
|
|
2230
|
-
};
|
|
2231
|
-
function primaryPrice() {
|
|
2232
|
-
const eff = effective();
|
|
2233
|
-
const custom = eff.price.value?.split(",").map(Number);
|
|
2234
|
-
if (custom && custom.length >= 2 && custom.every((n) => Number.isFinite(n) && n >= 0)) {
|
|
2235
|
-
return { name: "custom", input: custom[0], output: custom[1], cachedInput: custom[2] ?? custom[0] / 10 };
|
|
2236
|
-
}
|
|
2237
|
-
const named = eff.primary.value;
|
|
2238
|
-
const preset = PRESETS[named.toLowerCase()];
|
|
2239
|
-
if (preset) return preset;
|
|
2240
|
-
const c = eff.priceFeed.value ? catalogPrice("", named) : void 0;
|
|
2241
|
-
return c ? { name: named, source: "catalog", input: c.input, output: c.output, cachedInput: c.cachedInput } : PRESETS.sonnet;
|
|
2242
|
-
}
|
|
2243
|
-
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
2244
|
-
function parsePriceSpec(spec, name = "worker") {
|
|
2245
|
-
const parts = typeof spec === "string" ? spec.split(",").map((x) => Number(x.trim())) : [];
|
|
2246
|
-
if (parts.length < 2 || parts.length > 3 || !parts.every((n) => Number.isFinite(n) && n >= 0)) return void 0;
|
|
2247
|
-
return { name, source: "config", input: parts[0], output: parts[1], cachedInput: parts[2] ?? parts[0] / 10 };
|
|
2248
|
-
}
|
|
2249
|
-
var costAt = (usage, price) => (usage.input * price.input + usage.cacheRead * price.cachedInput + (usage.output + usage.reasoning) * price.output) / 1e6;
|
|
2250
|
-
var workerCostOf = (usage) => usage?.cost ?? usage?.costEstimate;
|
|
2251
|
-
function savedUsd(usage, returnedTokens, price = primaryPrice()) {
|
|
2252
|
-
const readingTheReport = returnedTokens * price.input / 1e6;
|
|
2253
|
-
return Math.max(0, costAt(usage, price) - (workerCostOf(usage) ?? 0) - readingTheReport);
|
|
2254
|
-
}
|
|
2255
|
-
function readLedger(sinceMs) {
|
|
2256
|
-
if (!fs16.existsSync(ledgerFile())) return [];
|
|
2257
|
-
return fs16.readFileSync(ledgerFile(), "utf8").split("\n").filter(Boolean).flatMap((l) => {
|
|
2258
|
-
try {
|
|
2259
|
-
return [JSON.parse(l)];
|
|
2260
|
-
} catch {
|
|
2261
|
-
return [];
|
|
2262
|
-
}
|
|
2263
|
-
}).filter((e) => !sinceMs || Date.parse(e.at) >= sinceMs);
|
|
2264
|
-
}
|
|
2265
|
-
function totals(entries) {
|
|
2266
|
-
const t = entries.reduce(
|
|
2267
|
-
(a, e) => ({
|
|
2268
|
-
runs: a.runs + 1,
|
|
2269
|
-
tokens: a.tokens + e.tokens,
|
|
2270
|
-
returned: a.returned + e.returned,
|
|
2271
|
-
workerCost: a.workerCost + e.workerCost,
|
|
2272
|
-
saved: a.saved + e.saved
|
|
2273
|
-
}),
|
|
2274
|
-
{ runs: 0, tokens: 0, returned: 0, workerCost: 0, saved: 0 }
|
|
2275
|
-
);
|
|
2276
|
-
return { ...t, ratio: t.returned ? t.tokens / t.returned : 0 };
|
|
2277
|
-
}
|
|
2278
|
-
var usd = (n) => n >= 100 ? `$${n.toFixed(0)}` : n >= 1 ? `$${n.toFixed(2)}` : `$${n.toFixed(3)}`;
|
|
2279
|
-
function compact(n) {
|
|
2280
|
-
const fmt = (v) => v.toFixed(v >= 100 ? 0 : 1).replace(/\.0$/, "");
|
|
2281
|
-
if (n >= 1e6) return `${fmt(n / 1e6)}M`;
|
|
2282
|
-
if (n >= 1e3) return `${fmt(n / 1e3)}k`;
|
|
2283
|
-
return String(Math.round(n));
|
|
2284
|
-
}
|
|
2285
|
-
function badgeUrl(t) {
|
|
2286
|
-
const msg = encodeURIComponent(`saved ${usd(t.saved)} \xB7 ${compact(t.tokens)} tokens offloaded`).replace(/-/g, "--");
|
|
2287
|
-
return `https://img.shields.io/badge/pitroom-${msg}-7c3aed`;
|
|
2288
|
-
}
|
|
2289
|
-
var esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
2290
|
-
function card(t, period) {
|
|
2291
|
-
const W = 720, H = 316, M = 32, GAP = 12;
|
|
2292
|
-
const tile = (i, value, label) => {
|
|
2293
|
-
const w = (W - 2 * M - 2 * GAP) / 3, x = M + i * (w + GAP);
|
|
2294
|
-
return `<rect x="${x}" y="196" width="${w}" height="72" rx="14" fill="#fff" fill-opacity=".035" stroke="#fff" stroke-opacity=".1"/>
|
|
2295
|
-
<text x="${x + 18}" y="224" class="lab">${esc(label.toUpperCase())}</text><text x="${x + 18}" y="254" class="val">${esc(value)}</text>`;
|
|
2296
|
-
};
|
|
2297
|
-
const when = period.toUpperCase();
|
|
2298
|
-
const pill = 22 + when.length * 7.4;
|
|
2299
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" role="img" aria-label="pitroom saved ${esc(usd(t.saved))}">
|
|
2300
|
-
<style>
|
|
2301
|
-
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Inter,Helvetica,Arial,sans-serif;fill:#f4f4f5}
|
|
2302
|
-
.name{font-size:20px;font-weight:650;letter-spacing:-.01em}
|
|
2303
|
-
.sub{font-size:12.5px;fill:#a1a1aa}
|
|
2304
|
-
.pill{font-size:11px;font-weight:600;letter-spacing:.1em;fill:#a1a1aa}
|
|
2305
|
-
.lab{font-size:11px;font-weight:500;letter-spacing:.09em;fill:#a1a1aa}
|
|
2306
|
-
.big{font-size:60px;font-weight:700;letter-spacing:-.03em;fill:#3ddc97}
|
|
2307
|
-
.val{font-size:26px;font-weight:650;letter-spacing:-.02em}
|
|
2308
|
-
.foot{font-size:11.5px;fill:#71717a}
|
|
2309
|
-
</style>
|
|
2310
|
-
<defs>
|
|
2311
|
-
<radialGradient id="o" cx="1" cy="0" r=".7"><stop offset="0" stop-color="#ff6a2b" stop-opacity=".24"/><stop offset="1" stop-color="#ff6a2b" stop-opacity="0"/></radialGradient>
|
|
2312
|
-
<radialGradient id="b" cx="0" cy="0" r=".7"><stop offset="0" stop-color="#38bdf8" stop-opacity=".16"/><stop offset="1" stop-color="#38bdf8" stop-opacity="0"/></radialGradient>
|
|
2313
|
-
<linearGradient id="m" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#ff6a2b"/><stop offset="1" stop-color="#ffa86b"/></linearGradient>
|
|
2314
|
-
<clipPath id="c"><rect width="${W}" height="${H}" rx="20"/></clipPath>
|
|
2315
|
-
</defs>
|
|
2316
|
-
<g clip-path="url(#c)"><rect width="${W}" height="${H}" fill="#0b0b0f"/><rect width="${W}" height="${H}" fill="url(#o)"/><rect width="${W}" height="${H}" fill="url(#b)"/></g>
|
|
2317
|
-
<rect x=".5" y=".5" width="${W - 1}" height="${H - 1}" rx="19.5" fill="none" stroke="#fff" stroke-opacity=".1"/>
|
|
2318
|
-
<rect x="${M}" y="28" width="38" height="38" rx="11" fill="url(#m)"/>
|
|
2319
|
-
<path d="M4 4h4v4H4zm8 0h4v4h-4zM8 8h4v4H8zm8 0h4v4h-4zM4 12h4v4H4zm8 0h4v4h-4zm-4 4h4v4H8zm8 0h4v4h-4z" fill="#fff" transform="translate(${M + 7} 35) scale(1)"/>
|
|
2320
|
-
<text x="${M + 52}" y="45" class="name">Pitroom</text><text x="${M + 52}" y="63" class="sub">Your agent's pit crew</text>
|
|
2321
|
-
<rect x="${W - M - pill}" y="34" width="${pill}" height="26" rx="13" fill="#fff" fill-opacity=".04" stroke="#fff" stroke-opacity=".14"/>
|
|
2322
|
-
<text x="${W - M - pill / 2}" y="51" class="pill" text-anchor="middle">${esc(when)}</text>
|
|
2323
|
-
<text x="${M}" y="112" class="lab">SAVED ON YOUR MAIN CODING AGENT</text>
|
|
2324
|
-
<text x="${M}" y="168" class="big">${esc(usd(t.saved))}</text>
|
|
2325
|
-
${tile(0, compact(t.tokens), "Tokens offloaded")}${tile(1, t.ratio ? `${Math.round(t.ratio)}\xD7` : "\u2014", "Context compression")}${tile(2, String(t.runs), "Delegated tasks")}
|
|
2326
|
-
<text x="${M}" y="296" class="foot">Estimated against ${esc(primaryPrice().name)} pricing \xB7 npx pitroom</text>
|
|
2327
|
-
</svg>
|
|
2328
|
-
`;
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
3272
|
// src/core/process.ts
|
|
2332
3273
|
import { spawn } from "node:child_process";
|
|
2333
|
-
import
|
|
3274
|
+
import fs21 from "node:fs";
|
|
2334
3275
|
async function spawnWorker(inv, opts) {
|
|
2335
|
-
const out =
|
|
2336
|
-
const err =
|
|
3276
|
+
const out = fs21.openSync(opts.stdoutFile, "w");
|
|
3277
|
+
const err = fs21.openSync(opts.stderrFile, "w");
|
|
2337
3278
|
const res = { code: null, timedOut: false, stopped: false };
|
|
2338
3279
|
const child = spawn(inv.command, inv.args, {
|
|
2339
3280
|
cwd: opts.cwd,
|
|
@@ -2356,7 +3297,7 @@ async function spawnWorker(inv, opts) {
|
|
|
2356
3297
|
}, opts.timeoutSec * 1e3);
|
|
2357
3298
|
res.code = await new Promise((resolve2) => {
|
|
2358
3299
|
child.on("error", (e) => {
|
|
2359
|
-
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;
|
|
2360
3301
|
resolve2(127);
|
|
2361
3302
|
});
|
|
2362
3303
|
child.on("close", (c) => resolve2(c));
|
|
@@ -2365,16 +3306,19 @@ async function spawnWorker(inv, opts) {
|
|
|
2365
3306
|
if (killer) clearTimeout(killer);
|
|
2366
3307
|
process.off("SIGINT", stop);
|
|
2367
3308
|
process.off("SIGTERM", stop);
|
|
2368
|
-
|
|
2369
|
-
|
|
3309
|
+
fs21.closeSync(out);
|
|
3310
|
+
fs21.closeSync(err);
|
|
2370
3311
|
return res;
|
|
2371
3312
|
}
|
|
2372
3313
|
export {
|
|
3314
|
+
ASKS_FOR_LIST,
|
|
2373
3315
|
DEFAULT_BACKEND,
|
|
3316
|
+
FORBIDDEN_COMMANDS,
|
|
2374
3317
|
STOP_FILE,
|
|
2375
3318
|
TERMINAL,
|
|
2376
3319
|
acquireWriteLock,
|
|
2377
3320
|
allBackends,
|
|
3321
|
+
answerParts,
|
|
2378
3322
|
backendIds,
|
|
2379
3323
|
badgeUrl,
|
|
2380
3324
|
brief,
|
|
@@ -2389,6 +3333,8 @@ export {
|
|
|
2389
3333
|
fill,
|
|
2390
3334
|
findSecretFiles,
|
|
2391
3335
|
findSecretFilesInTree,
|
|
3336
|
+
fixTask,
|
|
3337
|
+
focusedRate,
|
|
2392
3338
|
formatTarget,
|
|
2393
3339
|
freshMeta,
|
|
2394
3340
|
getBackend,
|
|
@@ -2396,7 +3342,9 @@ export {
|
|
|
2396
3342
|
home,
|
|
2397
3343
|
isActive,
|
|
2398
3344
|
isAlive,
|
|
3345
|
+
linkInto,
|
|
2399
3346
|
listRunIds,
|
|
3347
|
+
loadEval,
|
|
2400
3348
|
loadPlan,
|
|
2401
3349
|
loadTemplate,
|
|
2402
3350
|
looksSecret,
|
|
@@ -2407,9 +3355,11 @@ export {
|
|
|
2407
3355
|
parseStatus,
|
|
2408
3356
|
parseTarget,
|
|
2409
3357
|
parseVerdict,
|
|
3358
|
+
pathsIn,
|
|
2410
3359
|
planName,
|
|
2411
3360
|
planTask,
|
|
2412
3361
|
primaryPrice,
|
|
3362
|
+
rankChain,
|
|
2413
3363
|
readCatalog,
|
|
2414
3364
|
readLedger,
|
|
2415
3365
|
readMeta,
|
|
@@ -2418,20 +3368,26 @@ export {
|
|
|
2418
3368
|
requestStop,
|
|
2419
3369
|
resolveRun,
|
|
2420
3370
|
retryAfterMs,
|
|
3371
|
+
retryCommands,
|
|
2421
3372
|
runDir,
|
|
2422
3373
|
runFile,
|
|
2423
3374
|
runsDir,
|
|
2424
3375
|
savedUsd,
|
|
3376
|
+
scoreAnswer,
|
|
3377
|
+
scoreOf,
|
|
2425
3378
|
secretWarning,
|
|
2426
3379
|
shimDir,
|
|
2427
3380
|
shimReady,
|
|
2428
3381
|
slotHolders,
|
|
2429
3382
|
spawnWorker,
|
|
3383
|
+
summarize,
|
|
2430
3384
|
totals,
|
|
2431
3385
|
trim,
|
|
3386
|
+
truthOf,
|
|
2432
3387
|
tryAcquireSlot,
|
|
2433
3388
|
usd,
|
|
2434
3389
|
verifyRefs,
|
|
2435
3390
|
workerCostOf,
|
|
3391
|
+
writeIfChanged,
|
|
2436
3392
|
writeMeta
|
|
2437
3393
|
};
|