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/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 usage = {
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
- usage.toolCalls++;
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)) usage.denied++;
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
- usage.input += num(u.input_tokens);
129
- usage.cacheRead += num(u.cache_read_input_tokens);
130
- usage.cacheWrite += num(u.cache_creation_input_tokens);
131
- usage.reasoning += num(u.output_tokens_details?.thinking_tokens);
132
- usage.output += Math.max(0, num(u.output_tokens) - num(u.output_tokens_details?.thinking_tokens));
133
- usage.total += num(u.input_tokens) + num(u.cache_read_input_tokens) + num(u.cache_creation_input_tokens) + num(u.output_tokens);
134
- usage.cost = (usage.cost ?? 0) + num(e.total_cost_usd);
135
- usage.denied = Math.max(usage.denied, Array.isArray(e.permission_denials) ? e.permission_denials.length : 0);
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
- usage.steps = steps.size;
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
- if (req.model) args.push("--model", req.model);
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 usage = {
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
- usage.steps++;
351
+ usage2.steps++;
342
352
  if (TOOLS.has(it.type)) {
343
- usage.toolCalls++;
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 ?? ""))) usage.denied++;
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 usage.denied++;
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
- usage.input += Math.max(0, num2(u.input_tokens) - cached2);
365
- usage.cacheRead += cached2;
366
- usage.cacheWrite += num2(u.cache_write_input_tokens);
367
- usage.output += Math.max(0, num2(u.output_tokens) - reasoning);
368
- usage.reasoning += reasoning;
369
- usage.total += num2(u.input_tokens) + num2(u.output_tokens);
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: m ? `Codex model: ${m} (checked on first use; unsupported models fail over)` : "Codex model: Codex's built-in default (your ~/.codex/config.toml is ignored for workers; pick one with -W codex:<model>)"
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 usage = {
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
- usage.toolCalls++;
581
+ usage2.toolCalls++;
554
582
  tools[name] = (tools[name] ?? 0) + 1;
555
- if (st.status === "error" && DENIED2.test(String(st.error ?? ""))) usage.denied++;
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
- usage.steps++;
565
- usage.input += num3(t.input);
566
- usage.output += num3(t.output);
567
- usage.reasoning += num3(t.reasoning);
568
- usage.cacheRead += num3(t.cache?.read);
569
- usage.cacheWrite += num3(t.cache?.write);
570
- usage.total += num3(t.total) || num3(t.input) + num3(t.output) + num3(t.reasoning) + num3(t.cache?.read);
571
- usage.cost = (usage.cost ?? 0) + num3(p.cost);
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 base = {};
815
+ let base2 = {};
788
816
  if (existing?.trim()) {
789
817
  try {
790
- base = JSON.parse(existing);
818
+ base2 = JSON.parse(existing);
791
819
  } catch {
792
- base = {};
820
+ base2 = {};
793
821
  }
794
822
  }
795
- return JSON.stringify(deepMerge(base, ours));
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 base = process.platform === "win32" ? process.env.APPDATA ?? path5.join(os5.homedir(), "AppData", "Roaming") : process.env.XDG_CONFIG_HOME ?? path5.join(os5.homedir(), ".config");
990
- return path5.join(base, "pitroom", "config.json");
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), 4),
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(usage, returnedTokens, price = primaryPrice()) {
1378
- const wouldCost = (usage.input * price.input + usage.cacheRead * price.cachedInput + (usage.output + usage.reasoning) * price.output) / 1e6;
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 - (usage.cost ?? 0) - readingTheReport);
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 base = { run: m.id, mode: m.mode, task: oneLine4(m.task, 60) };
1636
- if (!prev) emit({ event: m.state === "queued" ? "queued" : "started", ...base });
1637
- else if (prev.state === "queued" && m.state === "running") emit({ event: "started", ...base });
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 base of skillTargets()) {
1814
+ for (const base2 of skillTargets()) {
1761
1815
  for (const legacy of LEGACY) {
1762
- const l = path9.join(base, legacy);
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(base, name), opts));
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 base of skillTargets()) {
1782
- if (!fs11.existsSync(base)) continue;
1783
- for (const name of fs11.readdirSync(base)) {
1784
- const l = path9.join(base, name);
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((base) => ({ base, names: names.filter((n) => fs11.existsSync(path9.join(base, n, "SKILL.md"))) }));
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) => t.model || !models[t.backend] ? t : { ...t, model: models[t.backend] };
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.4" : "0.0.0-dev";
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) console.log(JSON.stringify({ systemMessage: 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 ${base}: ${names.join(", ")}` });
3494
- else if (names.length) checks.push({ level: "warn", message: `skills in ${base}: only ${names.join(", ")} of ${all.length}; run \`pitroom install\`` });
3495
- else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base}; run \`pitroom install\`` });
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 base of [path19.join(os9.homedir(), ".agents", "skills"), path19.join(os9.homedir(), ".claude", "skills")]) {
3535
- const dir = path19.join(base, "using-superpowers");
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 4, PITROOM_MAX_PARALLEL) run at once; others queue.
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")),