pitroom 0.6.4 → 0.6.5
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 +25 -0
- package/PRIVACY.md +28 -12
- package/README.md +157 -79
- package/THIRD_PARTY_NOTICES.md +19 -0
- package/dist/lib.mjs +39 -3
- package/dist/pitroom.mjs +300 -78
- package/package.json +4 -2
- package/skills/pitroom-crew/SKILL.md +2 -2
- package/skills/pitroom-writing-plans/SKILL.md +1 -1
- package/skills/using-pitroom/SKILL.md +3 -0
package/dist/pitroom.mjs
CHANGED
|
@@ -61,7 +61,7 @@ function resolveCommand(bin) {
|
|
|
61
61
|
var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
62
62
|
var DENIED = /permission|not allowed|denied|blocked|disallowed/i;
|
|
63
63
|
function parseEvents(jsonl) {
|
|
64
|
-
const
|
|
64
|
+
const usage2 = {
|
|
65
65
|
input: 0,
|
|
66
66
|
output: 0,
|
|
67
67
|
reasoning: 0,
|
|
@@ -106,7 +106,7 @@ function parseEvents(jsonl) {
|
|
|
106
106
|
lastText = String(c.text).trim();
|
|
107
107
|
lastActivity = `says: ${oneLine(lastText)}`;
|
|
108
108
|
} else if (c.type === "tool_use") {
|
|
109
|
-
|
|
109
|
+
usage2.toolCalls++;
|
|
110
110
|
tools[c.name] = (tools[c.name] ?? 0) + 1;
|
|
111
111
|
const target = c.input?.file_path ?? c.input?.path ?? c.input?.pattern ?? c.input?.command ?? c.input?.url;
|
|
112
112
|
lastActivity = `${c.name} ${target ? oneLine(String(target), 60) : ""}`.trim();
|
|
@@ -118,21 +118,21 @@ function parseEvents(jsonl) {
|
|
|
118
118
|
if (c?.type !== "tool_result") continue;
|
|
119
119
|
const text = typeof c.content === "string" ? c.content : JSON.stringify(c.content ?? "");
|
|
120
120
|
if (c.is_error) {
|
|
121
|
-
if (DENIED.test(text))
|
|
121
|
+
if (DENIED.test(text)) usage2.denied++;
|
|
122
122
|
} else if (pendingEdits.has(c.tool_use_id)) {
|
|
123
123
|
edits.push(pendingEdits.get(c.tool_use_id));
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
} else if (e.type === "result") {
|
|
127
127
|
const u = e.usage ?? {};
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
128
|
+
usage2.input += num(u.input_tokens);
|
|
129
|
+
usage2.cacheRead += num(u.cache_read_input_tokens);
|
|
130
|
+
usage2.cacheWrite += num(u.cache_creation_input_tokens);
|
|
131
|
+
usage2.reasoning += num(u.output_tokens_details?.thinking_tokens);
|
|
132
|
+
usage2.output += Math.max(0, num(u.output_tokens) - num(u.output_tokens_details?.thinking_tokens));
|
|
133
|
+
usage2.total += num(u.input_tokens) + num(u.cache_read_input_tokens) + num(u.cache_creation_input_tokens) + num(u.output_tokens);
|
|
134
|
+
usage2.cost = (usage2.cost ?? 0) + num(e.total_cost_usd);
|
|
135
|
+
usage2.denied = Math.max(usage2.denied, Array.isArray(e.permission_denials) ? e.permission_denials.length : 0);
|
|
136
136
|
if (e.is_error) {
|
|
137
137
|
const tags = [e.terminal_reason, e.api_error_status && `HTTP ${e.api_error_status}`].filter(Boolean);
|
|
138
138
|
error ??= `${String(e.result ?? e.subtype ?? "Claude Code error")}${tags.length ? ` [${tags.join(", ")}]` : ""}`;
|
|
@@ -141,8 +141,8 @@ function parseEvents(jsonl) {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
-
|
|
145
|
-
return { sessionId, model, finalText: finalText ?? (error ? "" : lastText), usage, tools, edits, lastActivity, error };
|
|
144
|
+
usage2.steps = steps.size;
|
|
145
|
+
return { sessionId, model, finalText: finalText ?? (error ? "" : lastText), usage: usage2, tools, edits, lastActivity, error };
|
|
146
146
|
}
|
|
147
147
|
function num(v) {
|
|
148
148
|
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
@@ -231,7 +231,9 @@ function invocation(req) {
|
|
|
231
231
|
"stream-json",
|
|
232
232
|
"--verbose"
|
|
233
233
|
);
|
|
234
|
-
|
|
234
|
+
const [model, effort] = (req.model ?? "").split("#");
|
|
235
|
+
if (model) args.push("--model", model);
|
|
236
|
+
if (effort) args.push("--effort", effort);
|
|
235
237
|
if (req.sessionId) args.push("--resume", req.sessionId);
|
|
236
238
|
args.push("--", req.prompt);
|
|
237
239
|
const { command, prefix } = resolveCommand(binary());
|
|
@@ -284,11 +286,19 @@ function doctor({ models, hasFallback }) {
|
|
|
284
286
|
if (!hasFallback) checks.push({ level: "warn", message: "no fallback workers configured for Claude Code runs" });
|
|
285
287
|
return checks;
|
|
286
288
|
}
|
|
289
|
+
var CLAUDE_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
290
|
+
function catalog() {
|
|
291
|
+
const def = defaultModel();
|
|
292
|
+
const models = ["haiku", "sonnet", "opus"].map((id) => ({ id, efforts: CLAUDE_EFFORTS }));
|
|
293
|
+
if (def && !models.some((m) => m.id === def)) models.push({ id: def, efforts: CLAUDE_EFFORTS });
|
|
294
|
+
return { models, source: "Claude Code aliases (latest of each size) and --effort levels from `claude --help`" };
|
|
295
|
+
}
|
|
287
296
|
var claude = {
|
|
288
297
|
id: "claude",
|
|
289
298
|
name: "Claude Code",
|
|
290
299
|
capabilities: { readOnly: "tool-allowlist", resume: "by-id", reportsCost: true, attachFiles: false },
|
|
291
300
|
binary,
|
|
301
|
+
catalog,
|
|
292
302
|
invocation,
|
|
293
303
|
parse: parseEvents,
|
|
294
304
|
failure,
|
|
@@ -306,7 +316,7 @@ import path3 from "node:path";
|
|
|
306
316
|
var WORK = /* @__PURE__ */ new Set(["agent_message", "reasoning", "command_execution", "file_change", "mcp_tool_call", "web_search", "todo_list"]);
|
|
307
317
|
var TOOLS = /* @__PURE__ */ new Set(["command_execution", "file_change", "mcp_tool_call", "web_search"]);
|
|
308
318
|
function parseEvents2(jsonl) {
|
|
309
|
-
const
|
|
319
|
+
const usage2 = {
|
|
310
320
|
input: 0,
|
|
311
321
|
output: 0,
|
|
312
322
|
reasoning: 0,
|
|
@@ -338,21 +348,21 @@ function parseEvents2(jsonl) {
|
|
|
338
348
|
case "item.completed": {
|
|
339
349
|
const it = e.item ?? {};
|
|
340
350
|
if (!WORK.has(it.type)) break;
|
|
341
|
-
|
|
351
|
+
usage2.steps++;
|
|
342
352
|
if (TOOLS.has(it.type)) {
|
|
343
|
-
|
|
353
|
+
usage2.toolCalls++;
|
|
344
354
|
tools[it.type] = (tools[it.type] ?? 0) + 1;
|
|
345
355
|
}
|
|
346
356
|
if (it.type === "agent_message" && String(it.text ?? "").trim()) {
|
|
347
357
|
finalText = String(it.text).trim();
|
|
348
358
|
lastActivity = `says: ${oneLine2(finalText)}`;
|
|
349
359
|
} else if (it.type === "command_execution") {
|
|
350
|
-
if (it.status === "declined" || /operation not permitted|sandbox/i.test(String(it.aggregated_output ?? "")))
|
|
360
|
+
if (it.status === "declined" || /operation not permitted|sandbox/i.test(String(it.aggregated_output ?? ""))) usage2.denied++;
|
|
351
361
|
lastActivity = `shell ${oneLine2(String(it.command ?? ""), 60)}`;
|
|
352
362
|
} else if (it.type === "file_change") {
|
|
353
363
|
const paths = (it.changes ?? []).map((c) => String(c.path));
|
|
354
364
|
if (it.status !== "failed" && it.status !== "declined") edits.push(...paths);
|
|
355
|
-
else
|
|
365
|
+
else usage2.denied++;
|
|
356
366
|
lastActivity = `edit ${oneLine2(paths.join(", "), 60)}`;
|
|
357
367
|
}
|
|
358
368
|
break;
|
|
@@ -361,12 +371,12 @@ function parseEvents2(jsonl) {
|
|
|
361
371
|
const u = e.usage ?? {};
|
|
362
372
|
const cached2 = num2(u.cached_input_tokens);
|
|
363
373
|
const reasoning = num2(u.reasoning_output_tokens);
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
374
|
+
usage2.input += Math.max(0, num2(u.input_tokens) - cached2);
|
|
375
|
+
usage2.cacheRead += cached2;
|
|
376
|
+
usage2.cacheWrite += num2(u.cache_write_input_tokens);
|
|
377
|
+
usage2.output += Math.max(0, num2(u.output_tokens) - reasoning);
|
|
378
|
+
usage2.reasoning += reasoning;
|
|
379
|
+
usage2.total += num2(u.input_tokens) + num2(u.output_tokens);
|
|
370
380
|
break;
|
|
371
381
|
}
|
|
372
382
|
case "error":
|
|
@@ -377,7 +387,7 @@ function parseEvents2(jsonl) {
|
|
|
377
387
|
break;
|
|
378
388
|
}
|
|
379
389
|
}
|
|
380
|
-
return { sessionId, finalText, usage, tools, edits, lastActivity, error };
|
|
390
|
+
return { sessionId, finalText, usage: usage2, tools, edits, lastActivity, error };
|
|
381
391
|
}
|
|
382
392
|
function describe(raw) {
|
|
383
393
|
const s = String(raw ?? "unknown Codex error");
|
|
@@ -482,17 +492,35 @@ function doctor2({ models, hasFallback }) {
|
|
|
482
492
|
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`" }
|
|
483
493
|
);
|
|
484
494
|
for (const m of models) {
|
|
495
|
+
const pinned = m?.replace(/#.*$/, "");
|
|
485
496
|
checks.push({
|
|
486
|
-
level: "ok",
|
|
487
|
-
message:
|
|
497
|
+
level: pinned ? "ok" : "warn",
|
|
498
|
+
message: pinned ? `Codex model: ${pinned} (checked on first use; unsupported models fail over)` : 'Codex model: none pinned, so Codex runs its own default (it can change and cost more); set "models": {"codex": "<model>"} or use -W codex:<model>'
|
|
488
499
|
});
|
|
489
500
|
}
|
|
490
501
|
if (!hasFallback) checks.push({ level: "warn", message: "no fallback workers configured for Codex runs" });
|
|
491
502
|
return checks;
|
|
492
503
|
}
|
|
504
|
+
function catalog2() {
|
|
505
|
+
const file = path3.join(codexHome(), "models_cache.json");
|
|
506
|
+
try {
|
|
507
|
+
const raw = JSON.parse(fs3.readFileSync(file, "utf8"));
|
|
508
|
+
const models = (raw.models ?? []).filter((m) => m.visibility !== "hide" && typeof m.slug === "string").map((m) => ({
|
|
509
|
+
id: m.slug,
|
|
510
|
+
efforts: (m.supported_reasoning_levels ?? []).map(
|
|
511
|
+
(l) => typeof l === "string" ? l : l.effort ?? ""
|
|
512
|
+
).filter(Boolean),
|
|
513
|
+
defaultEffort: typeof m.default_reasoning_level === "string" ? m.default_reasoning_level : void 0
|
|
514
|
+
}));
|
|
515
|
+
return { models, source: `Codex model cache${raw.fetched_at ? `, fetched ${raw.fetched_at.slice(0, 10)}` : ""}` };
|
|
516
|
+
} catch {
|
|
517
|
+
return { models: [], source: "no Codex model cache found (run codex once)" };
|
|
518
|
+
}
|
|
519
|
+
}
|
|
493
520
|
var codex = {
|
|
494
521
|
id: "codex",
|
|
495
522
|
name: "Codex",
|
|
523
|
+
catalog: catalog2,
|
|
496
524
|
capabilities: { readOnly: "os-sandbox", resume: "by-id", reportsCost: false, attachFiles: false },
|
|
497
525
|
binary: binary2,
|
|
498
526
|
invocation: invocation2,
|
|
@@ -509,7 +537,7 @@ import { spawnSync as spawnSync3 } from "node:child_process";
|
|
|
509
537
|
var EDIT_TOOLS2 = /* @__PURE__ */ new Set(["edit", "write", "patch", "multiedit", "apply_patch"]);
|
|
510
538
|
var DENIED2 = /rule which prevents you|permission denied|permission\.rejected/i;
|
|
511
539
|
function parseEvents3(ndjson) {
|
|
512
|
-
const
|
|
540
|
+
const usage2 = {
|
|
513
541
|
input: 0,
|
|
514
542
|
output: 0,
|
|
515
543
|
reasoning: 0,
|
|
@@ -550,9 +578,9 @@ function parseEvents3(ndjson) {
|
|
|
550
578
|
case "tool_use": {
|
|
551
579
|
const name = String(p.tool ?? "tool");
|
|
552
580
|
const st = p.state ?? {};
|
|
553
|
-
|
|
581
|
+
usage2.toolCalls++;
|
|
554
582
|
tools[name] = (tools[name] ?? 0) + 1;
|
|
555
|
-
if (st.status === "error" && DENIED2.test(String(st.error ?? "")))
|
|
583
|
+
if (st.status === "error" && DENIED2.test(String(st.error ?? ""))) usage2.denied++;
|
|
556
584
|
if (st.status === "completed" && EDIT_TOOLS2.has(name)) {
|
|
557
585
|
edits.push(String(st.input?.filePath ?? st.input?.path ?? name));
|
|
558
586
|
}
|
|
@@ -561,14 +589,14 @@ function parseEvents3(ndjson) {
|
|
|
561
589
|
}
|
|
562
590
|
case "step_finish": {
|
|
563
591
|
const t = p.tokens ?? {};
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
592
|
+
usage2.steps++;
|
|
593
|
+
usage2.input += num3(t.input);
|
|
594
|
+
usage2.output += num3(t.output);
|
|
595
|
+
usage2.reasoning += num3(t.reasoning);
|
|
596
|
+
usage2.cacheRead += num3(t.cache?.read);
|
|
597
|
+
usage2.cacheWrite += num3(t.cache?.write);
|
|
598
|
+
usage2.total += num3(t.total) || num3(t.input) + num3(t.output) + num3(t.reasoning) + num3(t.cache?.read);
|
|
599
|
+
usage2.cost = (usage2.cost ?? 0) + num3(p.cost);
|
|
572
600
|
break;
|
|
573
601
|
}
|
|
574
602
|
case "error": {
|
|
@@ -579,7 +607,7 @@ function parseEvents3(ndjson) {
|
|
|
579
607
|
}
|
|
580
608
|
const groups = [...textByMessage.values()];
|
|
581
609
|
const finalText = (groups[groups.length - 1] ?? []).join("\n").trim();
|
|
582
|
-
return { sessionId, finalText, usage, tools, edits, lastActivity, error };
|
|
610
|
+
return { sessionId, finalText, usage: usage2, tools, edits, lastActivity, error };
|
|
583
611
|
}
|
|
584
612
|
function describeError(err) {
|
|
585
613
|
if (!err || typeof err !== "object") return "unknown OpenCode error";
|
|
@@ -784,15 +812,15 @@ function configContent(existing, allowWeb = false) {
|
|
|
784
812
|
}
|
|
785
813
|
}
|
|
786
814
|
};
|
|
787
|
-
let
|
|
815
|
+
let base2 = {};
|
|
788
816
|
if (existing?.trim()) {
|
|
789
817
|
try {
|
|
790
|
-
|
|
818
|
+
base2 = JSON.parse(existing);
|
|
791
819
|
} catch {
|
|
792
|
-
|
|
820
|
+
base2 = {};
|
|
793
821
|
}
|
|
794
822
|
}
|
|
795
|
-
return JSON.stringify(deepMerge(
|
|
823
|
+
return JSON.stringify(deepMerge(base2, ours));
|
|
796
824
|
}
|
|
797
825
|
function deepMerge(a, b) {
|
|
798
826
|
if (!isObject(a) || !isObject(b)) return b;
|
|
@@ -897,6 +925,13 @@ function resolveModel2(sessionId) {
|
|
|
897
925
|
return void 0;
|
|
898
926
|
}
|
|
899
927
|
}
|
|
928
|
+
function catalog3() {
|
|
929
|
+
const ids = listModels();
|
|
930
|
+
return {
|
|
931
|
+
models: (ids.length ? ids : listModels()).map((id) => ({ id })),
|
|
932
|
+
source: "`opencode models` (reasoning variants are provider-specific: provider/model#variant)"
|
|
933
|
+
};
|
|
934
|
+
}
|
|
900
935
|
function listModels() {
|
|
901
936
|
return oc(["models"]).out.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
902
937
|
}
|
|
@@ -946,6 +981,7 @@ var opencode = {
|
|
|
946
981
|
defaultModel: defaultModel2,
|
|
947
982
|
resolveModel: resolveModel2,
|
|
948
983
|
listModels,
|
|
984
|
+
catalog: catalog3,
|
|
949
985
|
doctor: doctor3
|
|
950
986
|
};
|
|
951
987
|
|
|
@@ -954,6 +990,7 @@ var REGISTRY = new Map([opencode, codex, claude].map((b) => [b.id, b]));
|
|
|
954
990
|
var DEFAULT_BACKEND = opencode.id;
|
|
955
991
|
var PLANNED = ["gemini"];
|
|
956
992
|
var backendIds = () => [...REGISTRY.keys()];
|
|
993
|
+
var allBackends = () => [...REGISTRY.values()];
|
|
957
994
|
var isBackendId = (id) => REGISTRY.has(id) || PLANNED.includes(id);
|
|
958
995
|
function getBackend(id) {
|
|
959
996
|
const backend = REGISTRY.get(id);
|
|
@@ -982,12 +1019,13 @@ var SCHEMA = {
|
|
|
982
1019
|
web: "boolean",
|
|
983
1020
|
maxParallel: "number",
|
|
984
1021
|
models: "record",
|
|
985
|
-
tiers: "record"
|
|
1022
|
+
tiers: "record",
|
|
1023
|
+
costs: "numbers"
|
|
986
1024
|
};
|
|
987
1025
|
function configPath() {
|
|
988
1026
|
if (process.env.PITROOM_CONFIG) return path5.resolve(process.env.PITROOM_CONFIG);
|
|
989
|
-
const
|
|
990
|
-
return path5.join(
|
|
1027
|
+
const base2 = process.platform === "win32" ? process.env.APPDATA ?? path5.join(os5.homedir(), "AppData", "Roaming") : process.env.XDG_CONFIG_HOME ?? path5.join(os5.homedir(), ".config");
|
|
1028
|
+
return path5.join(base2, "pitroom", "config.json");
|
|
991
1029
|
}
|
|
992
1030
|
var cached;
|
|
993
1031
|
function loadConfig() {
|
|
@@ -1023,8 +1061,14 @@ function matches(v, type) {
|
|
|
1023
1061
|
if (type === "record") {
|
|
1024
1062
|
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((s) => typeof s === "string");
|
|
1025
1063
|
}
|
|
1064
|
+
if (type === "numbers") {
|
|
1065
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0);
|
|
1066
|
+
}
|
|
1026
1067
|
return typeof v === type;
|
|
1027
1068
|
}
|
|
1069
|
+
var DEFAULT_PARALLEL = 20;
|
|
1070
|
+
var MAX_PARALLEL_LIMIT = 30;
|
|
1071
|
+
var clampParallel = (s) => ({ ...s, value: Math.min(s.value, MAX_PARALLEL_LIMIT) });
|
|
1028
1072
|
var positiveInt = (v) => {
|
|
1029
1073
|
const n = Number(v);
|
|
1030
1074
|
return v !== void 0 && v !== "" && Number.isInteger(n) && n > 0 ? n : void 0;
|
|
@@ -1049,9 +1093,10 @@ function effective(flags = {}) {
|
|
|
1049
1093
|
price: setting(void 0, e.PITROOM_PRICE, c.price, void 0),
|
|
1050
1094
|
link: setting(void 0, void 0, c.link, []),
|
|
1051
1095
|
web: setting(void 0, void 0, c.web, false),
|
|
1052
|
-
maxParallel: setting(void 0, positiveInt(e.PITROOM_MAX_PARALLEL), positiveInt(c.maxParallel),
|
|
1096
|
+
maxParallel: clampParallel(setting(void 0, positiveInt(e.PITROOM_MAX_PARALLEL), positiveInt(c.maxParallel), DEFAULT_PARALLEL)),
|
|
1053
1097
|
models: setting(void 0, void 0, c.models, {}),
|
|
1054
|
-
tiers: setting(void 0, void 0, c.tiers, {})
|
|
1098
|
+
tiers: setting(void 0, void 0, c.tiers, {}),
|
|
1099
|
+
costs: setting(void 0, void 0, c.costs, {})
|
|
1055
1100
|
};
|
|
1056
1101
|
}
|
|
1057
1102
|
|
|
@@ -1143,6 +1188,7 @@ var VALUE_FLAGS = {
|
|
|
1143
1188
|
"-W": "worker",
|
|
1144
1189
|
"--worker": "worker",
|
|
1145
1190
|
"--tier": "tier",
|
|
1191
|
+
"--effort": "effort",
|
|
1146
1192
|
"-g": "group",
|
|
1147
1193
|
"--group": "group",
|
|
1148
1194
|
"-t": "timeout",
|
|
@@ -1179,6 +1225,8 @@ var BOOL_FLAGS = {
|
|
|
1179
1225
|
"--badge": "badge",
|
|
1180
1226
|
"--probe": "probe",
|
|
1181
1227
|
"--copy": "copy",
|
|
1228
|
+
"--all": "all",
|
|
1229
|
+
"--models": "models",
|
|
1182
1230
|
"--force": "force",
|
|
1183
1231
|
"--allow-delete": "allow-delete",
|
|
1184
1232
|
"--yes": "yes",
|
|
@@ -1258,6 +1306,7 @@ function runOptions(p, task) {
|
|
|
1258
1306
|
worker: flag(p, "worker"),
|
|
1259
1307
|
model: flag(p, "model"),
|
|
1260
1308
|
tier: flag(p, "tier"),
|
|
1309
|
+
effort: effortFlag(p),
|
|
1261
1310
|
timeoutSec: parseDuration(effective({ timeout: flag(p, "timeout") }).timeout.value),
|
|
1262
1311
|
verify: flag(p, "verify"),
|
|
1263
1312
|
continueFrom: cont ? resolveRun(cont) : void 0,
|
|
@@ -1275,6 +1324,11 @@ function planStep(p) {
|
|
|
1275
1324
|
if (!/^\d+$/.test(step)) throw new UserError(`--step takes a task number, not "${step}"`);
|
|
1276
1325
|
return { file, step: Number(step) };
|
|
1277
1326
|
}
|
|
1327
|
+
function effortFlag(p) {
|
|
1328
|
+
const v = flag(p, "effort");
|
|
1329
|
+
if (v !== void 0 && !/^[a-z][a-z0-9-]*$/i.test(v)) throw new UserError(`--effort takes a level such as low, medium, high or xhigh, not "${v}"`);
|
|
1330
|
+
return v?.toLowerCase();
|
|
1331
|
+
}
|
|
1278
1332
|
|
|
1279
1333
|
// src/cli/commands.ts
|
|
1280
1334
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
@@ -1374,10 +1428,10 @@ function primaryPrice() {
|
|
|
1374
1428
|
return PRESETS[eff.primary.value.toLowerCase()] ?? PRESETS.sonnet;
|
|
1375
1429
|
}
|
|
1376
1430
|
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
1377
|
-
function savedUsd(
|
|
1378
|
-
const wouldCost = (
|
|
1431
|
+
function savedUsd(usage2, returnedTokens, price = primaryPrice()) {
|
|
1432
|
+
const wouldCost = (usage2.input * price.input + usage2.cacheRead * price.cachedInput + (usage2.output + usage2.reasoning) * price.output) / 1e6;
|
|
1379
1433
|
const readingTheReport = returnedTokens * price.input / 1e6;
|
|
1380
|
-
return Math.max(0, wouldCost - (
|
|
1434
|
+
return Math.max(0, wouldCost - (usage2.cost ?? 0) - readingTheReport);
|
|
1381
1435
|
}
|
|
1382
1436
|
function record(meta) {
|
|
1383
1437
|
if (!meta.usage?.steps) return;
|
|
@@ -1632,9 +1686,9 @@ async function watch(select, opts) {
|
|
|
1632
1686
|
const prev = seen.get(m.id);
|
|
1633
1687
|
const l = m.state === "running" ? live(m) : { steps: m.usage?.steps ?? 0, toolCalls: m.usage?.toolCalls ?? 0 };
|
|
1634
1688
|
const attempts = m.attempts?.length ?? 0;
|
|
1635
|
-
const
|
|
1636
|
-
if (!prev) emit({ event: m.state === "queued" ? "queued" : "started", ...
|
|
1637
|
-
else if (prev.state === "queued" && m.state === "running") emit({ event: "started", ...
|
|
1689
|
+
const base2 = { run: m.id, mode: m.mode, task: oneLine4(m.task, 60) };
|
|
1690
|
+
if (!prev) emit({ event: m.state === "queued" ? "queued" : "started", ...base2 });
|
|
1691
|
+
else if (prev.state === "queued" && m.state === "running") emit({ event: "started", ...base2 });
|
|
1638
1692
|
if (attempts > (prev?.attempts ?? 0)) {
|
|
1639
1693
|
const a = m.attempts[attempts - 1];
|
|
1640
1694
|
emit({ event: "fallback", run: m.id, failed: a.target, reason: oneLine4(a.error, 120) });
|
|
@@ -1757,15 +1811,15 @@ function install(opts) {
|
|
|
1757
1811
|
const skills = skillNames(root);
|
|
1758
1812
|
if (!skills.length) throw new Error(`no skills found under ${path9.join(root, "skills")}`);
|
|
1759
1813
|
const out = [];
|
|
1760
|
-
for (const
|
|
1814
|
+
for (const base2 of skillTargets()) {
|
|
1761
1815
|
for (const legacy of LEGACY) {
|
|
1762
|
-
const l = path9.join(
|
|
1816
|
+
const l = path9.join(base2, legacy);
|
|
1763
1817
|
if (!skills.includes(legacy) && linksInto(l, root)) {
|
|
1764
1818
|
fs11.unlinkSync(l);
|
|
1765
1819
|
out.push(`\u2714 removed old link ${l}`);
|
|
1766
1820
|
}
|
|
1767
1821
|
}
|
|
1768
|
-
for (const name of skills) out.push(place(path9.join(root, "skills", name), path9.join(
|
|
1822
|
+
for (const name of skills) out.push(place(path9.join(root, "skills", name), path9.join(base2, name), opts));
|
|
1769
1823
|
}
|
|
1770
1824
|
const bundle = path9.join(root, "dist", "pitroom.mjs");
|
|
1771
1825
|
if (fs11.existsSync(bundle)) {
|
|
@@ -1778,10 +1832,10 @@ function install(opts) {
|
|
|
1778
1832
|
function uninstall() {
|
|
1779
1833
|
const root = packageRoot();
|
|
1780
1834
|
const out = [];
|
|
1781
|
-
for (const
|
|
1782
|
-
if (!fs11.existsSync(
|
|
1783
|
-
for (const name of fs11.readdirSync(
|
|
1784
|
-
const l = path9.join(
|
|
1835
|
+
for (const base2 of skillTargets()) {
|
|
1836
|
+
if (!fs11.existsSync(base2)) continue;
|
|
1837
|
+
for (const name of fs11.readdirSync(base2)) {
|
|
1838
|
+
const l = path9.join(base2, name);
|
|
1785
1839
|
if (linksInto(l, root)) {
|
|
1786
1840
|
fs11.unlinkSync(l);
|
|
1787
1841
|
out.push(`\u2714 removed ${l}`);
|
|
@@ -1796,7 +1850,7 @@ function uninstall() {
|
|
|
1796
1850
|
}
|
|
1797
1851
|
function installedSkills() {
|
|
1798
1852
|
const names = skillNames();
|
|
1799
|
-
return skillTargets().map((
|
|
1853
|
+
return skillTargets().map((base2) => ({ base: base2, names: names.filter((n) => fs11.existsSync(path9.join(base2, n, "SKILL.md"))) }));
|
|
1800
1854
|
}
|
|
1801
1855
|
|
|
1802
1856
|
// src/core/plan-status.ts
|
|
@@ -2134,6 +2188,7 @@ function pickReviewer(job) {
|
|
|
2134
2188
|
const eff = effective();
|
|
2135
2189
|
const tiers = eff.tiers.value;
|
|
2136
2190
|
if (job.kind === "range") return tiers.capable;
|
|
2191
|
+
if (tiers.review) return tiers.review;
|
|
2137
2192
|
const implementer = job.implementer;
|
|
2138
2193
|
if (!implementer) return void 0;
|
|
2139
2194
|
const def = parseTarget(eff.worker.value, DEFAULT_BACKEND).backend;
|
|
@@ -2169,10 +2224,108 @@ function fill(template, values) {
|
|
|
2169
2224
|
return template.replace(/\{\{([A-Z_]+)\}\}/g, (_, key) => values[key]);
|
|
2170
2225
|
}
|
|
2171
2226
|
|
|
2227
|
+
// src/core/models.ts
|
|
2228
|
+
var base = (model) => (model ?? "").split("#")[0];
|
|
2229
|
+
function usage() {
|
|
2230
|
+
const eff = effective();
|
|
2231
|
+
const out = /* @__PURE__ */ new Map();
|
|
2232
|
+
const add = (spec, label) => {
|
|
2233
|
+
let t;
|
|
2234
|
+
try {
|
|
2235
|
+
t = parseTarget(spec, "opencode");
|
|
2236
|
+
} catch {
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
const model = base(t.model) || base(eff.models.value[t.backend]);
|
|
2240
|
+
if (!model) return;
|
|
2241
|
+
const key = `${t.backend}:${model}`;
|
|
2242
|
+
out.set(key, [...out.get(key) ?? [], label]);
|
|
2243
|
+
};
|
|
2244
|
+
add(eff.worker.value, "default worker");
|
|
2245
|
+
for (const [name, spec] of Object.entries(eff.tiers.value)) add(spec, `tier ${name}`);
|
|
2246
|
+
eff.fallback.value.forEach((spec) => add(spec, "fallback"));
|
|
2247
|
+
for (const [backend, model] of Object.entries(eff.models.value)) add(`${backend}:${model}`, "models");
|
|
2248
|
+
return out;
|
|
2249
|
+
}
|
|
2250
|
+
function modelTable(opts = {}) {
|
|
2251
|
+
const costs = effective().costs.value;
|
|
2252
|
+
const used = usage();
|
|
2253
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2254
|
+
for (const e of readLedger()) {
|
|
2255
|
+
if (!e.backend || !e.model) continue;
|
|
2256
|
+
const key = `${e.backend}:${base(e.model)}`;
|
|
2257
|
+
const a = seen.get(key) ?? { runs: 0, tokens: 0, usd: 0 };
|
|
2258
|
+
seen.set(key, { runs: a.runs + 1, tokens: a.tokens + e.tokens, usd: a.usd + e.workerCost });
|
|
2259
|
+
}
|
|
2260
|
+
const rows = [];
|
|
2261
|
+
const sources = [];
|
|
2262
|
+
let hiddenOpenCode = 0;
|
|
2263
|
+
const known = /* @__PURE__ */ new Set();
|
|
2264
|
+
for (const b of allBackends()) {
|
|
2265
|
+
if (opts.backend && b.id !== opts.backend) continue;
|
|
2266
|
+
const catalog4 = b.catalog?.() ?? { models: [], source: "no model list for this worker" };
|
|
2267
|
+
sources.push(`${b.id}: ${catalog4.source}`);
|
|
2268
|
+
for (const m of catalog4.models) {
|
|
2269
|
+
const key = `${b.id}:${m.id}`;
|
|
2270
|
+
known.add(key);
|
|
2271
|
+
const s = seen.get(key);
|
|
2272
|
+
const row = {
|
|
2273
|
+
worker: b.id,
|
|
2274
|
+
model: m.id,
|
|
2275
|
+
efforts: m.efforts?.length ? m.efforts : void 0,
|
|
2276
|
+
defaultEffort: m.defaultEffort,
|
|
2277
|
+
cost: costs[key],
|
|
2278
|
+
runs: s?.runs ?? 0,
|
|
2279
|
+
avgTokens: s ? Math.round(s.tokens / s.runs) : void 0,
|
|
2280
|
+
reportedUsdPerRun: s && s.usd > 0 ? s.usd / s.runs : void 0,
|
|
2281
|
+
inUse: used.get(key) ?? []
|
|
2282
|
+
};
|
|
2283
|
+
const relevant = row.inUse.length || row.runs || row.cost !== void 0;
|
|
2284
|
+
if (b.id === "opencode" && !opts.all && !relevant) hiddenOpenCode++;
|
|
2285
|
+
else rows.push(row);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
for (const [key, labels] of used) {
|
|
2289
|
+
const [worker, ...rest] = key.split(":");
|
|
2290
|
+
if (known.has(key) || opts.backend && worker !== opts.backend) continue;
|
|
2291
|
+
const s = seen.get(key);
|
|
2292
|
+
rows.push({ worker, model: rest.join(":"), cost: costs[key], runs: s?.runs ?? 0, avgTokens: s ? Math.round(s.tokens / s.runs) : void 0, inUse: labels });
|
|
2293
|
+
}
|
|
2294
|
+
rows.sort(
|
|
2295
|
+
(a, b) => a.worker.localeCompare(b.worker) || Number(b.inUse.length > 0) - Number(a.inUse.length > 0) || (a.cost ?? Infinity) - (b.cost ?? Infinity) || b.runs - a.runs || a.model.localeCompare(b.model)
|
|
2296
|
+
);
|
|
2297
|
+
return { rows, sources, hiddenOpenCode };
|
|
2298
|
+
}
|
|
2299
|
+
var compact2 = (n) => n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n);
|
|
2300
|
+
function formatModels(t) {
|
|
2301
|
+
const efforts = (r) => r.efforts ? r.efforts.map((e) => e === r.defaultEffort ? `${e}*` : e).join("/") : "-";
|
|
2302
|
+
const cells = t.rows.map((r) => [
|
|
2303
|
+
r.worker,
|
|
2304
|
+
r.model,
|
|
2305
|
+
efforts(r),
|
|
2306
|
+
r.cost !== void 0 ? String(r.cost) : "?",
|
|
2307
|
+
r.runs ? String(r.runs) : "-",
|
|
2308
|
+
r.avgTokens !== void 0 ? compact2(r.avgTokens) : "-",
|
|
2309
|
+
r.reportedUsdPerRun !== void 0 ? `$${r.reportedUsdPerRun.toFixed(3)}` : "-",
|
|
2310
|
+
r.inUse.join(", ") || "-"
|
|
2311
|
+
]);
|
|
2312
|
+
const head = ["worker", "model", "effort (* default)", "cost", "runs", "avg tokens", "$/run", "in use"];
|
|
2313
|
+
const widths = head.map((h, i) => Math.max(h.length, ...cells.map((c) => c[i].length)));
|
|
2314
|
+
const line = (c) => c.map((x, i) => x.padEnd(widths[i])).join(" ").trimEnd();
|
|
2315
|
+
const out = [line(head), ...cells.map(line), ""];
|
|
2316
|
+
out.push('cost: your relative cost from the config ("costs": {"codex:gpt-6-sol": 1, \u2026}); ? = not set. Pitroom cannot know vendor prices.');
|
|
2317
|
+
out.push("runs, avg tokens, $/run: from your own runs (only Claude Code and OpenCode report dollars). Choose one with -W worker:model --effort LEVEL.");
|
|
2318
|
+
if (t.hiddenOpenCode) out.push(`${t.hiddenOpenCode} more OpenCode models not shown (use --all).`);
|
|
2319
|
+
out.push(...t.sources.map((s) => `source ${s}`));
|
|
2320
|
+
return out.join("\n");
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2172
2323
|
// src/core/ui.ts
|
|
2173
2324
|
import fs16 from "node:fs";
|
|
2174
2325
|
var WEEK_MS = 7 * 24 * 3600 * 1e3;
|
|
2175
2326
|
var RECENT = 40;
|
|
2327
|
+
var RECENT_HOURS = 1;
|
|
2328
|
+
var MAX_CARDS = 5;
|
|
2176
2329
|
function activeRuns() {
|
|
2177
2330
|
const out = [];
|
|
2178
2331
|
for (const id of listRunIds().slice(-RECENT)) {
|
|
@@ -2271,8 +2424,17 @@ function hookCards(input) {
|
|
|
2271
2424
|
const output = typeof response === "string" ? response : [response?.stdout, response?.stderr].filter(Boolean).join("\n");
|
|
2272
2425
|
const ids = [...new Set(`${command}
|
|
2273
2426
|
${output}`.match(RUN_ID) ?? [])].filter((id) => fs16.existsSync(`${runsDir()}/${id}`));
|
|
2427
|
+
const since = Date.now() - RECENT_HOURS * 36e5;
|
|
2428
|
+
for (const id of listRunIds().slice(-RECENT)) {
|
|
2429
|
+
if (ids.includes(id) || fs16.existsSync(runFile(id, "card-ended"))) continue;
|
|
2430
|
+
try {
|
|
2431
|
+
const m = readMeta(id);
|
|
2432
|
+
if (!isActive(m.state) && Date.parse(m.endedAt ?? m.startedAt) > since) ids.push(id);
|
|
2433
|
+
} catch {
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2274
2436
|
const cards = [];
|
|
2275
|
-
for (const id of ids) {
|
|
2437
|
+
for (const id of ids.slice(0, MAX_CARDS)) {
|
|
2276
2438
|
try {
|
|
2277
2439
|
cards.push(...cardsFor(readMeta(id)));
|
|
2278
2440
|
} catch {
|
|
@@ -2297,10 +2459,28 @@ function resolveChain(flags = {}) {
|
|
|
2297
2459
|
}
|
|
2298
2460
|
const eff = effective({ worker: spec, model: flags.model });
|
|
2299
2461
|
const models = eff.models.value;
|
|
2300
|
-
const withDefault = (t) =>
|
|
2462
|
+
const withDefault = (t) => {
|
|
2463
|
+
const pinned = models[t.backend];
|
|
2464
|
+
if (!pinned) return t;
|
|
2465
|
+
if (!t.model) return { ...t, model: pinned };
|
|
2466
|
+
return t.model.startsWith("#") ? { ...t, model: `${pinned}${t.model}` } : t;
|
|
2467
|
+
};
|
|
2301
2468
|
let worker = parseTarget(eff.worker.value, DEFAULT_BACKEND);
|
|
2302
2469
|
if (eff.model.value) worker = { ...worker, model: eff.model.value };
|
|
2303
2470
|
worker = withDefault(worker);
|
|
2471
|
+
if (flags.effort) {
|
|
2472
|
+
const base2 = (worker.model ?? "").split("#")[0];
|
|
2473
|
+
if (worker.backend === "opencode" && !base2) {
|
|
2474
|
+
warnings.push("--effort needs a model for OpenCode (provider/model#variant): set one in the config or pass -m; ignored");
|
|
2475
|
+
} else {
|
|
2476
|
+
worker = { ...worker, model: `${base2}#${flags.effort}` };
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
if ((worker.backend === "codex" || worker.backend === "claude") && !(worker.model ?? "").replace(/#.*$/, "")) {
|
|
2480
|
+
warnings.push(
|
|
2481
|
+
`${worker.backend} has no pinned model, so it runs its own default (which can change and cost more): set "models": {"${worker.backend}": "<model>"} in the pitroom config, or pass -W ${worker.backend}:<model>`
|
|
2482
|
+
);
|
|
2483
|
+
}
|
|
2304
2484
|
const fallback = [];
|
|
2305
2485
|
if (!flags.noFallback) {
|
|
2306
2486
|
for (const s of eff.fallback.value) {
|
|
@@ -2734,7 +2914,7 @@ function inside(file, roots) {
|
|
|
2734
2914
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2735
2915
|
|
|
2736
2916
|
// src/core/run.ts
|
|
2737
|
-
var VERSION2 = true ? "0.6.
|
|
2917
|
+
var VERSION2 = true ? "0.6.5" : "0.0.0-dev";
|
|
2738
2918
|
function planWork(o) {
|
|
2739
2919
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
2740
2920
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -2764,7 +2944,7 @@ function prepareRun(o) {
|
|
|
2764
2944
|
let fallback;
|
|
2765
2945
|
if (o.continueFrom) {
|
|
2766
2946
|
parent = readMeta(o.continueFrom);
|
|
2767
|
-
if (o.worker || o.tier) throw new UserError("a follow-up runs on the same worker as its parent; drop --worker/--tier");
|
|
2947
|
+
if (o.worker || o.tier || o.effort) throw new UserError("a follow-up runs on the same worker as its parent; drop --worker/--tier/--effort");
|
|
2768
2948
|
if (!parent.sessionId) throw new UserError(`run ${parent.id} has no worker session to continue`, 3);
|
|
2769
2949
|
if (parent.mode === "isolate" && (!parent.worktree || !fs21.existsSync(parent.worktree))) {
|
|
2770
2950
|
throw new UserError(`the isolated copy of run ${parent.id} is gone (applied or discarded)`, 3);
|
|
@@ -2773,7 +2953,7 @@ function prepareRun(o) {
|
|
|
2773
2953
|
worker = o.model ? { ...ran, model: o.model } : ran;
|
|
2774
2954
|
fallback = o.noFallback ? [] : parent.fallback.filter((t) => t.backend === ran.backend);
|
|
2775
2955
|
} else {
|
|
2776
|
-
const chain = resolveChain({ worker: o.worker, model: o.model, tier: work.tier, noFallback: o.noFallback });
|
|
2956
|
+
const chain = resolveChain({ worker: o.worker, model: o.model, tier: work.tier, effort: o.effort, noFallback: o.noFallback });
|
|
2777
2957
|
({ worker, fallback } = chain);
|
|
2778
2958
|
warnings.push(...chain.warnings);
|
|
2779
2959
|
}
|
|
@@ -3117,7 +3297,7 @@ async function cmdReview(p) {
|
|
|
3117
3297
|
group: flag(p, "group") ?? job.group,
|
|
3118
3298
|
review: { of: job.of, kind: job.kind, packageFile, plan: job.plan }
|
|
3119
3299
|
});
|
|
3120
|
-
const automatic = !range && !flag(p, "worker") && !flag(p, "tier");
|
|
3300
|
+
const automatic = !range && !flag(p, "worker") && !flag(p, "tier") && !effective().tiers.value.review;
|
|
3121
3301
|
if (automatic && job.implementer && meta.worker.backend === job.implementer.backend) {
|
|
3122
3302
|
meta.warnings.push(
|
|
3123
3303
|
`reviewer runs on the same backend as the implementer (${job.implementer.backend}); configure tiers "standard" or "capable" for a second model`
|
|
@@ -3313,14 +3493,42 @@ function cmdStatusline(p) {
|
|
|
3313
3493
|
function cmdHookCard() {
|
|
3314
3494
|
try {
|
|
3315
3495
|
const cards = hookCards(readStdin());
|
|
3316
|
-
if (cards)
|
|
3496
|
+
if (cards) {
|
|
3497
|
+
console.log(
|
|
3498
|
+
JSON.stringify({
|
|
3499
|
+
systemMessage: cards,
|
|
3500
|
+
hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: `Pitroom, for the user:
|
|
3501
|
+
${cards}` }
|
|
3502
|
+
})
|
|
3503
|
+
);
|
|
3504
|
+
}
|
|
3317
3505
|
} catch {
|
|
3318
3506
|
}
|
|
3319
3507
|
return 0;
|
|
3320
3508
|
}
|
|
3509
|
+
function cmdModels(p) {
|
|
3510
|
+
const backend = p.positional[0];
|
|
3511
|
+
const table2 = modelTable({ backend, all: has(p, "all") });
|
|
3512
|
+
console.log(has(p, "json") ? JSON.stringify(table2, null, 2) : formatModels(table2));
|
|
3513
|
+
return 0;
|
|
3514
|
+
}
|
|
3321
3515
|
function cmdSavings(p) {
|
|
3322
3516
|
const since = flag(p, "since") ?? "all";
|
|
3323
3517
|
const t = totals(readLedger(sinceMs(since)));
|
|
3518
|
+
if (has(p, "models")) {
|
|
3519
|
+
const by = /* @__PURE__ */ new Map();
|
|
3520
|
+
for (const e of readLedger(sinceMs(since))) {
|
|
3521
|
+
const key = `${e.backend ?? "?"} ${e.model ?? "(default model)"}`;
|
|
3522
|
+
const a = by.get(key) ?? { runs: 0, tokens: 0, returned: 0, cost: 0, saved: 0 };
|
|
3523
|
+
by.set(key, { runs: a.runs + 1, tokens: a.tokens + e.tokens, returned: a.returned + e.returned, cost: a.cost + e.workerCost, saved: a.saved + e.saved });
|
|
3524
|
+
}
|
|
3525
|
+
const width = Math.max(12, ...[...by.keys()].map((k) => k.length));
|
|
3526
|
+
console.log(`${"worker model".padEnd(width)} ${"runs".padStart(5)} ${"processed".padStart(10)} ${"returned".padStart(9)} ${"cost".padStart(8)} ${"saved".padStart(8)}`);
|
|
3527
|
+
for (const [key, a] of [...by].sort((x, y) => y[1].runs - x[1].runs)) {
|
|
3528
|
+
console.log(`${key.padEnd(width)} ${String(a.runs).padStart(5)} ${compact(a.tokens).padStart(10)} ${compact(a.returned).padStart(9)} ${usd(a.cost).padStart(8)} ${usd(a.saved).padStart(8)}`);
|
|
3529
|
+
}
|
|
3530
|
+
return 0;
|
|
3531
|
+
}
|
|
3324
3532
|
const period = since === "all" ? "all time" : `last ${since.replace("d", " days")}`;
|
|
3325
3533
|
if (has(p, "json")) {
|
|
3326
3534
|
console.log(JSON.stringify({ period, ...t, price: primaryPrice() }, null, 2));
|
|
@@ -3430,6 +3638,16 @@ function doctor4(probe) {
|
|
|
3430
3638
|
}
|
|
3431
3639
|
}
|
|
3432
3640
|
if (tierNames.length) add("ok", `tiers: ${tierNames.map((name, i) => `${name}=${describeTarget(tierTargets[i])}`).join(", ")}`);
|
|
3641
|
+
const costs = effective().costs.value;
|
|
3642
|
+
if (Object.keys(costs).length) {
|
|
3643
|
+
for (const t of [...chain, ...tierTargets]) {
|
|
3644
|
+
const key = `${t.backend}:${(t.model ?? "").split("#")[0]}`;
|
|
3645
|
+
const mine = costs[key];
|
|
3646
|
+
if (mine === void 0) continue;
|
|
3647
|
+
const cheaper = Object.entries(costs).filter(([k, v]) => k.startsWith(`${t.backend}:`) && v < mine).sort((a, b) => a[1] - b[1])[0];
|
|
3648
|
+
add("ok", `cost: ${key} = ${mine}${cheaper ? `; you priced ${cheaper[0]} cheaper (${cheaper[1]}): is the dearer one needed?` : ""}`);
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3433
3651
|
const byBackend = /* @__PURE__ */ new Map();
|
|
3434
3652
|
for (const t of [...chain, ...tierTargets]) byBackend.set(t.backend, [...byBackend.get(t.backend) ?? [], t.model]);
|
|
3435
3653
|
for (const [id, models] of byBackend) {
|
|
@@ -3489,10 +3707,10 @@ function skillChecks() {
|
|
|
3489
3707
|
message: `superpowers is installed too (${superpowers.join(", ")}): two bootstraps compete for the same work; keep one (Pitroom includes the superpowers workflow)`
|
|
3490
3708
|
});
|
|
3491
3709
|
}
|
|
3492
|
-
for (const { base, names } of installedSkills()) {
|
|
3493
|
-
if (names.length === all.length) checks.push({ level: "ok", message: `skills in ${
|
|
3494
|
-
else if (names.length) checks.push({ level: "warn", message: `skills in ${
|
|
3495
|
-
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${
|
|
3710
|
+
for (const { base: base2, names } of installedSkills()) {
|
|
3711
|
+
if (names.length === all.length) checks.push({ level: "ok", message: `skills in ${base2}: ${names.join(", ")}` });
|
|
3712
|
+
else if (names.length) checks.push({ level: "warn", message: `skills in ${base2}: only ${names.join(", ")} of ${all.length}; run \`pitroom install\`` });
|
|
3713
|
+
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
|
|
3496
3714
|
}
|
|
3497
3715
|
if (viaPlugin) {
|
|
3498
3716
|
const linked = installedSkills().some((i) => i.base.includes(`${path19.sep}.claude${path19.sep}`) && i.names.length);
|
|
@@ -3531,8 +3749,8 @@ function superpowersActive() {
|
|
|
3531
3749
|
for (const p of openCodePlugins()) {
|
|
3532
3750
|
if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
|
|
3533
3751
|
}
|
|
3534
|
-
for (const
|
|
3535
|
-
const dir = path19.join(
|
|
3752
|
+
for (const base2 of [path19.join(os9.homedir(), ".agents", "skills"), path19.join(os9.homedir(), ".claude", "skills")]) {
|
|
3753
|
+
const dir = path19.join(base2, "using-superpowers");
|
|
3536
3754
|
if (fs23.existsSync(path19.join(dir, "SKILL.md"))) found.push(dir);
|
|
3537
3755
|
}
|
|
3538
3756
|
return found;
|
|
@@ -3621,7 +3839,9 @@ Usage
|
|
|
3621
3839
|
pitroom discard [run] drop an --isolate run's copy (the patch is kept)
|
|
3622
3840
|
pitroom revert [run] undo the changes of a --write run (checked first)
|
|
3623
3841
|
pitroom stop [run | -g NAME] stop running or queued workers
|
|
3624
|
-
pitroom savings [--since 7d|30d|all] [--card file.svg] [--badge]
|
|
3842
|
+
pitroom savings [--since 7d|30d|all] [--models] [--card file.svg] [--badge]
|
|
3843
|
+
pitroom models [worker] [--all] [--json]
|
|
3844
|
+
models each worker offers, with effort levels, your costs and usage
|
|
3625
3845
|
pitroom statusline [--then CMD] status-bar line: running workers, savings this week (after CMD's)
|
|
3626
3846
|
pitroom hook-card PostToolUse hook: a card after each Bash \`pitroom\` command
|
|
3627
3847
|
pitroom doctor [--probe] check workers, models, permissions, skills
|
|
@@ -3640,6 +3860,7 @@ Run options
|
|
|
3640
3860
|
-W, --worker T worker target "backend[:model]" (default: config "worker", else opencode)
|
|
3641
3861
|
-m, --model M model for that worker (default: the worker CLI's own default)
|
|
3642
3862
|
--tier NAME a worker from the config's "tiers" (e.g. cheap, standard, capable); -W wins
|
|
3863
|
+
--effort LEVEL reasoning effort for the worker: low, medium, high, xhigh, \u2026 (model#level)
|
|
3643
3864
|
--plan PLAN with --step N: implement Task N of a plan (-i or -w); the task text is your notes
|
|
3644
3865
|
--step N the plan task for --plan
|
|
3645
3866
|
-t, --timeout DUR e.g. 900, 20m, 1h (default 30m, or PITROOM_TIMEOUT)
|
|
@@ -3654,7 +3875,7 @@ Run options
|
|
|
3654
3875
|
--task-file PATH read the task from a file (crew: tasks separated by --- lines)
|
|
3655
3876
|
--allow-non-git allow --write outside a git repository (no tracking/revert)
|
|
3656
3877
|
|
|
3657
|
-
Parallel: at most maxParallel workers (default
|
|
3878
|
+
Parallel: at most maxParallel workers (default 20, up to 30; PITROOM_MAX_PARALLEL) run at once; others queue.
|
|
3658
3879
|
One --write run per repository; use --isolate for parallel changes.
|
|
3659
3880
|
Exit codes: 0 ok \xB7 1 worker failed \xB7 2 usage \xB7 3 refused/setup \xB7 4 timeout
|
|
3660
3881
|
5 read-only violation \xB7 6 verify failed \xB7 75 still running (wait again)
|
|
@@ -3679,6 +3900,7 @@ var COMMANDS = {
|
|
|
3679
3900
|
discard: cmdDiscard,
|
|
3680
3901
|
stop: cmdStop,
|
|
3681
3902
|
savings: cmdSavings,
|
|
3903
|
+
models: cmdModels,
|
|
3682
3904
|
statusline: cmdStatusline,
|
|
3683
3905
|
"hook-card": cmdHookCard,
|
|
3684
3906
|
doctor: (p) => doctor4(has(p, "probe")),
|