pitroom 0.14.1 → 0.15.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 +8 -0
- package/README.md +10 -2
- package/dist/pitroom.mjs +375 -135
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pitroom",
|
|
3
3
|
"description": "A free pit crew for your expensive coding agent: the superpowers development workflow, run by cheap workers. Delegate research, fixes, tests, reviews and whole plans from Claude Code, Codex or any agent to OpenCode, Codex or Claude Code workers, with receipts that prove the savings.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.15.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TahaAtas"
|
|
7
7
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pitroom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "A free pit crew for your expensive coding agent: delegate research, fixes, tests, reviews and parallel investigations to cheap worker agents running your own models, with verified answers, exact diffs and cost receipts.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TahaAtas"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.0
|
|
4
|
+
|
|
5
|
+
### `pitroom install --mcp`
|
|
6
|
+
- Registers `pitroom mcp` in the MCP clients found on the machine, so setting it up is one command: Claude Code, Codex and Gemini CLI are changed with their own `mcp add` command (user scope where they have one), Cursor and Claude Desktop have their JSON config merged (other servers and settings kept, a `.bak-pitroom` backup the first time, a file that is not valid JSON left alone). `--dry-run` only says what it would do, `--client cursor,claude-desktop` limits it, `--no-skills` skips linking the skills, and `pitroom uninstall` removes the registrations again (a client whose command is gone cannot be asked to, which is reported and exits 1).
|
|
7
|
+
- The command registered is the launcher by its full path (apps such as Claude Desktop start without your `PATH`), and only when that file really is a Pitroom launcher. Whether a client already has the server is read from its config file, never by starting the server; a client that runs a different command (a Codex entry included) is updated, and `--force` registers again.
|
|
8
|
+
- `pitroom doctor` has an MCP section: where `pitroom mcp` is registered, and a warning for a client that runs another command.
|
|
9
|
+
- Checked with the real `claude`, `codex` and `gemini` commands in a throwaway home: right entries written, a second run "already registered", `uninstall` removed exactly those. The change was reviewed by a free Pitroom worker before merging; its findings (a foreign file at the launcher path, unguarded writes, the Codex command not compared, uninstall failures, a few messages) are fixed.
|
|
10
|
+
|
|
3
11
|
## 0.14.1
|
|
4
12
|
|
|
5
13
|
### Docs
|
package/README.md
CHANGED
|
@@ -273,7 +273,15 @@ npm i -g pitroom
|
|
|
273
273
|
|
|
274
274
|
**Any agent that speaks MCP (Cursor, Claude Desktop, and the three above too)**
|
|
275
275
|
|
|
276
|
-
Pitroom is also an [MCP](https://modelcontextprotocol.io) server on stdio: nine tools (`pitroom_run`, `pitroom_wait`, `pitroom_status`, `pitroom_show`, `pitroom_review`, `pitroom_audit`, `pitroom_apply`, `pitroom_discard`, `pitroom_stop`) instead of shell commands and skills. After `npm i -g pitroom
|
|
276
|
+
Pitroom is also an [MCP](https://modelcontextprotocol.io) server on stdio: nine tools (`pitroom_run`, `pitroom_wait`, `pitroom_status`, `pitroom_show`, `pitroom_review`, `pitroom_audit`, `pitroom_apply`, `pitroom_discard`, `pitroom_stop`) instead of shell commands and skills. After `npm i -g pitroom`, let Pitroom register itself in the clients it finds (Claude Code, Codex, Gemini CLI, Cursor, Claude Desktop):
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
pitroom install --mcp # also links the skills; add --no-skills to skip that
|
|
280
|
+
pitroom install --mcp --dry-run # only say what it would do
|
|
281
|
+
pitroom install --mcp --client cursor,claude-desktop
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
It registers the launcher by its full path (apps like Claude Desktop start without your `PATH`), changes Claude Code, Codex and Gemini CLI with their own `mcp add` command (user scope where it has one), merges the JSON of Cursor and Claude Desktop without touching their other servers (keeping a `.bak-pitroom` backup, and leaving a file that is not valid JSON alone), skips what is already registered, and `pitroom uninstall` removes it again. `pitroom doctor` shows where it is registered. Restart the client afterwards. Or do it by hand:
|
|
277
285
|
|
|
278
286
|
```bash
|
|
279
287
|
claude mcp add pitroom -- pitroom mcp
|
|
@@ -495,7 +503,7 @@ pitroom ls [--running] [-g NAME] · pitroom clean [--days 14] [--yes]
|
|
|
495
503
|
pitroom savings [--since 7d|30d|all] [--models] [--card file.svg] [--badge]
|
|
496
504
|
pitroom models [worker] [--all] [--json] models, effort levels, your costs, your usage
|
|
497
505
|
pitroom statusline [--then CMD] · pitroom hook-card
|
|
498
|
-
pitroom doctor [--probe] · pitroom config · pitroom init [--model ID] [--fallback A,B] [--yes] [--force] · pitroom install [--copy] [--force] · pitroom uninstall
|
|
506
|
+
pitroom doctor [--probe] · pitroom config · pitroom init [--model ID] [--fallback A,B] [--yes] [--force] · pitroom install [--copy] [--force] [--mcp [--client A,B] [--no-skills] [--dry-run]] · pitroom uninstall
|
|
499
507
|
```
|
|
500
508
|
|
|
501
509
|
Exit codes: `0` ok · `1` worker failed · `2` usage · `3` refused/setup · `4` timeout · `5` read-only violation · `6` verify failed · `75` still running.
|
package/dist/pitroom.mjs
CHANGED
|
@@ -262,10 +262,10 @@ function classify(message) {
|
|
|
262
262
|
if (/model[^.]*(not found|does not exist|invalid|not available)|invalid model|not_found_error/i.test(message)) return "model-unavailable";
|
|
263
263
|
return "other";
|
|
264
264
|
}
|
|
265
|
-
function failure(
|
|
266
|
-
if (exitCode === 0 && !
|
|
265
|
+
function failure(run2, stderr, exitCode) {
|
|
266
|
+
if (exitCode === 0 && !run2.error) return void 0;
|
|
267
267
|
const detail = stderr.trim().split("\n").filter(Boolean).pop();
|
|
268
|
-
const message =
|
|
268
|
+
const message = run2.error ?? detail ?? `claude exited with code ${exitCode}`;
|
|
269
269
|
return { kind: classify(message), message };
|
|
270
270
|
}
|
|
271
271
|
function defaultModel() {
|
|
@@ -476,10 +476,10 @@ function classify2(message) {
|
|
|
476
476
|
if (/unauthori[sz]ed|\b40[13]\b|not logged in|log ?in|api.?key|credits?\b|billing|subscription/i.test(message)) return "auth";
|
|
477
477
|
return "other";
|
|
478
478
|
}
|
|
479
|
-
function failure2(
|
|
480
|
-
if (exitCode === 0 && !
|
|
479
|
+
function failure2(run2, stderr, exitCode) {
|
|
480
|
+
if (exitCode === 0 && !run2.error) return void 0;
|
|
481
481
|
const detail = stderr.split("\n").find((l) => /^Error[: ]|ERROR/.test(l) && !/rmcp|models cache/.test(l));
|
|
482
|
-
let message =
|
|
482
|
+
let message = run2.error ?? (detail ? detail.trim() : `codex exited with code ${exitCode}`);
|
|
483
483
|
if (/not supported when using Codex/i.test(message)) message += " (an outdated Codex CLI says this too: npm i -g @openai/codex@latest)";
|
|
484
484
|
return { kind: classify2(message), message };
|
|
485
485
|
}
|
|
@@ -838,11 +838,11 @@ function classify3(message) {
|
|
|
838
838
|
return "other";
|
|
839
839
|
}
|
|
840
840
|
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)";
|
|
841
|
-
function failure3(
|
|
842
|
-
if (exitCode === 0 && !
|
|
841
|
+
function failure3(run2, stderr, exitCode) {
|
|
842
|
+
if (exitCode === 0 && !run2.error) return void 0;
|
|
843
843
|
const lines = stderr.trim().split("\n").map((l) => l.trim()).filter(Boolean);
|
|
844
844
|
const detail = lines.find((l) => /error|failed|exceeded|quota|exhausted|unsupported/i.test(l) && !/^at /.test(l)) ?? lines.at(-1);
|
|
845
|
-
const raw =
|
|
845
|
+
const raw = run2.error ?? detail ?? `gemini exited with code ${exitCode}`;
|
|
846
846
|
const message = /not running in a trusted directory/i.test(`${stderr} ${raw}`) ? UNTRUSTED : raw;
|
|
847
847
|
return { kind: classify3(message), message };
|
|
848
848
|
}
|
|
@@ -1283,10 +1283,10 @@ function classify4(message) {
|
|
|
1283
1283
|
}
|
|
1284
1284
|
return "other";
|
|
1285
1285
|
}
|
|
1286
|
-
function failure4(
|
|
1287
|
-
if (exitCode === 0 && !
|
|
1286
|
+
function failure4(run2, stderr, exitCode) {
|
|
1287
|
+
if (exitCode === 0 && !run2.error) return void 0;
|
|
1288
1288
|
const detail = stderr.match(/error="([^"]+)"/g)?.pop()?.slice(7, -1);
|
|
1289
|
-
const raw =
|
|
1289
|
+
const raw = run2.error ?? `opencode exited with code ${exitCode}`;
|
|
1290
1290
|
const message = detail && !raw.includes(detail) ? `${raw}: ${detail}` : raw;
|
|
1291
1291
|
return { kind: classify4(message), message };
|
|
1292
1292
|
}
|
|
@@ -1538,7 +1538,8 @@ var VALUE_FLAGS = {
|
|
|
1538
1538
|
"--limit": "limit",
|
|
1539
1539
|
"--plan": "plan",
|
|
1540
1540
|
"--step": "step",
|
|
1541
|
-
"--fallback": "fallback"
|
|
1541
|
+
"--fallback": "fallback",
|
|
1542
|
+
"--client": "client"
|
|
1542
1543
|
};
|
|
1543
1544
|
var BOOL_FLAGS = {
|
|
1544
1545
|
"-r": "read",
|
|
@@ -1548,6 +1549,9 @@ var BOOL_FLAGS = {
|
|
|
1548
1549
|
"-i": "isolate",
|
|
1549
1550
|
"--isolate": "isolate",
|
|
1550
1551
|
"--bg": "bg",
|
|
1552
|
+
"--mcp": "mcp",
|
|
1553
|
+
"--no-skills": "no-skills",
|
|
1554
|
+
"--dry-run": "dry-run",
|
|
1551
1555
|
"--in-place": "in-place",
|
|
1552
1556
|
"--audit": "audit",
|
|
1553
1557
|
"--no-audit": "no-audit",
|
|
@@ -2499,9 +2503,9 @@ function skillNames(root = packageRoot()) {
|
|
|
2499
2503
|
return fs16.readdirSync(dir).filter((d) => fs16.existsSync(path14.join(dir, d, "SKILL.md"))).sort();
|
|
2500
2504
|
}
|
|
2501
2505
|
function skillTargets() {
|
|
2502
|
-
const
|
|
2503
|
-
const targets = [path14.join(
|
|
2504
|
-
if (fs16.existsSync(path14.join(
|
|
2506
|
+
const home3 = os9.homedir();
|
|
2507
|
+
const targets = [path14.join(home3, ".agents", "skills")];
|
|
2508
|
+
if (fs16.existsSync(path14.join(home3, ".claude"))) targets.push(path14.join(home3, ".claude", "skills"));
|
|
2505
2509
|
return targets;
|
|
2506
2510
|
}
|
|
2507
2511
|
var launcherPath = () => path14.join(os9.homedir(), ".local", "bin", "pitroom");
|
|
@@ -2528,6 +2532,13 @@ function isOurLauncher(file2, root) {
|
|
|
2528
2532
|
return false;
|
|
2529
2533
|
}
|
|
2530
2534
|
}
|
|
2535
|
+
function isPitroomLauncher(file2) {
|
|
2536
|
+
try {
|
|
2537
|
+
return fs16.readFileSync(file2, "utf8").includes(LAUNCHER_MARK);
|
|
2538
|
+
} catch {
|
|
2539
|
+
return false;
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2531
2542
|
function placeLauncher(bundle, root, force) {
|
|
2532
2543
|
const dest = launcherPath();
|
|
2533
2544
|
fs16.mkdirSync(path14.dirname(dest), { recursive: true });
|
|
@@ -2563,10 +2574,10 @@ function place(src, dest, opts) {
|
|
|
2563
2574
|
}
|
|
2564
2575
|
function install(opts) {
|
|
2565
2576
|
const root = packageRoot();
|
|
2566
|
-
const skills = skillNames(root);
|
|
2567
|
-
if (!skills.length) throw new Error(`no skills found under ${path14.join(root, "skills")}`);
|
|
2577
|
+
const skills = opts.skills === false ? [] : skillNames(root);
|
|
2578
|
+
if (opts.skills !== false && !skills.length) throw new Error(`no skills found under ${path14.join(root, "skills")}`);
|
|
2568
2579
|
const out = [];
|
|
2569
|
-
for (const base2 of skillTargets()) {
|
|
2580
|
+
for (const base2 of opts.skills === false ? [] : skillTargets()) {
|
|
2570
2581
|
for (const legacy of LEGACY) {
|
|
2571
2582
|
const l = path14.join(base2, legacy);
|
|
2572
2583
|
if (!skills.includes(legacy) && linksInto(l, root)) {
|
|
@@ -3620,7 +3631,7 @@ function progress(meta) {
|
|
|
3620
3631
|
}
|
|
3621
3632
|
|
|
3622
3633
|
// src/core/run.ts
|
|
3623
|
-
var VERSION2 = true ? "0.
|
|
3634
|
+
var VERSION2 = true ? "0.15.0" : "0.0.0-dev";
|
|
3624
3635
|
function planWork(o) {
|
|
3625
3636
|
if (!o.plan) return { task: o.task.trim(), tier: o.tier };
|
|
3626
3637
|
if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");
|
|
@@ -3916,9 +3927,9 @@ function attempt(meta, backend, target) {
|
|
|
3916
3927
|
}
|
|
3917
3928
|
var read2 = (f) => fs24.existsSync(f) ? fs24.readFileSync(f, "utf8") : "";
|
|
3918
3929
|
function retryableFailure(meta, backend, res) {
|
|
3919
|
-
const
|
|
3920
|
-
if (
|
|
3921
|
-
const f = backend.failure(
|
|
3930
|
+
const run2 = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
3931
|
+
if (run2.usage.steps > 0) return void 0;
|
|
3932
|
+
const f = backend.failure(run2, read2(runFile(meta.id, "stderr.log")), res.code);
|
|
3922
3933
|
return f && f.kind !== "other" ? f : void 0;
|
|
3923
3934
|
}
|
|
3924
3935
|
var HINTS = {
|
|
@@ -3930,19 +3941,19 @@ var HINTS = {
|
|
|
3930
3941
|
function finalize(meta, res) {
|
|
3931
3942
|
const ran = meta.ran ?? meta.worker;
|
|
3932
3943
|
const backend = getBackend(ran.backend);
|
|
3933
|
-
const
|
|
3934
|
-
if (meta.snapshot?.dir && meta.repoRoot)
|
|
3935
|
-
meta.sessionId =
|
|
3936
|
-
meta.usage =
|
|
3937
|
-
fs24.writeFileSync(runFile(meta.id, "summary.md"), `${
|
|
3944
|
+
const run2 = backend.parse(read2(runFile(meta.id, "events.jsonl")));
|
|
3945
|
+
if (meta.snapshot?.dir && meta.repoRoot) run2.finalText = backToProject(run2.finalText, meta.snapshot.dir, meta.repoRoot);
|
|
3946
|
+
meta.sessionId = run2.sessionId ?? meta.sessionId;
|
|
3947
|
+
meta.usage = run2.usage;
|
|
3948
|
+
fs24.writeFileSync(runFile(meta.id, "summary.md"), `${run2.finalText}
|
|
3938
3949
|
`);
|
|
3939
|
-
if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(
|
|
3950
|
+
if (meta.plan && !meta.reviewOf) meta.taskStatus = parseStatus(run2.finalText);
|
|
3940
3951
|
if (meta.reviewOf) {
|
|
3941
|
-
meta.verdict = parseVerdict(
|
|
3952
|
+
meta.verdict = parseVerdict(run2.finalText);
|
|
3942
3953
|
if (meta.packageFile) fs24.rmSync(meta.packageFile, { force: true });
|
|
3943
3954
|
}
|
|
3944
3955
|
if (!res.timedOut && !res.stopped && !meta.error) {
|
|
3945
|
-
const f = backend.failure(
|
|
3956
|
+
const f = backend.failure(run2, read2(runFile(meta.id, "stderr.log")), res.code);
|
|
3946
3957
|
if (f) meta.error = HINTS[f.kind] ? `${f.message} (${HINTS[f.kind]})` : f.message;
|
|
3947
3958
|
}
|
|
3948
3959
|
try {
|
|
@@ -3950,12 +3961,12 @@ function finalize(meta, res) {
|
|
|
3950
3961
|
} catch (e) {
|
|
3951
3962
|
meta.warnings.push(`could not compute changes: ${e.message}`);
|
|
3952
3963
|
}
|
|
3953
|
-
if (meta.mode === "read" &&
|
|
3954
|
-
meta.warnings.push(`READ-ONLY VIOLATION: worker modified ${[...new Set(
|
|
3964
|
+
if (meta.mode === "read" && run2.edits.length) {
|
|
3965
|
+
meta.warnings.push(`READ-ONLY VIOLATION: worker modified ${[...new Set(run2.edits)].join(", ")}`);
|
|
3955
3966
|
}
|
|
3956
3967
|
if (meta.sessionId && backend.resolveModel) meta.resolvedModel = backend.resolveModel(meta.sessionId);
|
|
3957
|
-
meta.resolvedModel ??=
|
|
3958
|
-
const refs = extractRefs(
|
|
3968
|
+
meta.resolvedModel ??= run2.model ?? ran.model;
|
|
3969
|
+
const refs = extractRefs(run2.finalText);
|
|
3959
3970
|
if (refs.length) {
|
|
3960
3971
|
meta.refs = verifyRefs(refs, [meta.cwd, meta.repoRoot ?? "", meta.dir]);
|
|
3961
3972
|
if (meta.refs.invalid.length) {
|
|
@@ -3964,14 +3975,14 @@ function finalize(meta, res) {
|
|
|
3964
3975
|
}
|
|
3965
3976
|
meta.state = res.timedOut ? "timeout" : res.stopped ? "stopped" : meta.error ? "failed" : "done";
|
|
3966
3977
|
if (meta.auditOf && meta.state === "done") {
|
|
3967
|
-
const found = parseAudit(
|
|
3978
|
+
const found = parseAudit(run2.finalText);
|
|
3968
3979
|
meta.auditVerdict = found.verdict;
|
|
3969
3980
|
meta.auditDisputed = found.disputed;
|
|
3970
3981
|
}
|
|
3971
|
-
if (meta.state === "done" && !
|
|
3982
|
+
if (meta.state === "done" && !run2.finalText) meta.warnings.push("worker finished without a written answer");
|
|
3972
3983
|
if (meta.state === "done" && meta.verify) meta.verifyResult = runVerify(meta);
|
|
3973
3984
|
meta.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3974
|
-
meta.returnedTokens = estimateTokens(formatReport(meta,
|
|
3985
|
+
meta.returnedTokens = estimateTokens(formatReport(meta, run2.finalText));
|
|
3975
3986
|
meta.savedUsd = meta.auditOf ? 0 : savedUsd(meta.usage, meta.returnedTokens);
|
|
3976
3987
|
writeMeta(meta);
|
|
3977
3988
|
if (meta.mode === "write" && meta.repoRoot) releaseWriteLock(meta.repoRoot, meta.id);
|
|
@@ -4219,8 +4230,8 @@ var TOOLS2 = [
|
|
|
4219
4230
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
4220
4231
|
async call(a) {
|
|
4221
4232
|
const group = str(a, "group");
|
|
4222
|
-
const
|
|
4223
|
-
return asResult(await pit(["status", ...group ? ["-g", group] :
|
|
4233
|
+
const run2 = str(a, "run");
|
|
4234
|
+
return asResult(await pit(["status", ...group ? ["-g", group] : run2 ? [run2] : []], 6e4));
|
|
4224
4235
|
}
|
|
4225
4236
|
},
|
|
4226
4237
|
{
|
|
@@ -4230,8 +4241,8 @@ var TOOLS2 = [
|
|
|
4230
4241
|
inputSchema: { type: "object", properties: { run: RUN_ID2, patch: { type: "boolean" }, full: { type: "boolean" } }, required: ["run"] },
|
|
4231
4242
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
4232
4243
|
async call(a) {
|
|
4233
|
-
const
|
|
4234
|
-
return asResult(await pit(["show",
|
|
4244
|
+
const run2 = str(a, "run", true);
|
|
4245
|
+
return asResult(await pit(["show", run2, ...bool(a, "patch") ? ["--patch"] : bool(a, "full") ? ["--full"] : []], 6e4));
|
|
4235
4246
|
}
|
|
4236
4247
|
},
|
|
4237
4248
|
{
|
|
@@ -4253,12 +4264,12 @@ var TOOLS2 = [
|
|
|
4253
4264
|
},
|
|
4254
4265
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
4255
4266
|
async call(a) {
|
|
4256
|
-
const
|
|
4267
|
+
const run2 = str(a, "run");
|
|
4257
4268
|
const range = str(a, "range");
|
|
4258
|
-
if (!
|
|
4269
|
+
if (!run2 === !range) throw new ToolError('give exactly one of "run" or "range"');
|
|
4259
4270
|
const plan = str(a, "plan");
|
|
4260
4271
|
const flags = workerFlags(a, ["worker", "tier", "dir", "group"]);
|
|
4261
|
-
return startAndWait(["review", ...range ? ["--range", range, ...plan ? ["--plan", plan] : []] : [
|
|
4272
|
+
return startAndWait(["review", ...range ? ["--range", range, ...plan ? ["--plan", plan] : []] : [run2], ...flags], waitSeconds(a));
|
|
4262
4273
|
}
|
|
4263
4274
|
},
|
|
4264
4275
|
{
|
|
@@ -4268,9 +4279,9 @@ var TOOLS2 = [
|
|
|
4268
4279
|
inputSchema: { type: "object", properties: { run: RUN_ID2, worker: WORKER_PROPS.worker, ...WAIT_PROP }, required: ["run"] },
|
|
4269
4280
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
4270
4281
|
async call(a) {
|
|
4271
|
-
const
|
|
4282
|
+
const run2 = str(a, "run", true);
|
|
4272
4283
|
const worker = str(a, "worker");
|
|
4273
|
-
return startAndWait(["audit",
|
|
4284
|
+
return startAndWait(["audit", run2, ...worker ? ["-W", worker] : []], waitSeconds(a));
|
|
4274
4285
|
}
|
|
4275
4286
|
},
|
|
4276
4287
|
{
|
|
@@ -4281,9 +4292,9 @@ var TOOLS2 = [
|
|
|
4281
4292
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
4282
4293
|
async call(a) {
|
|
4283
4294
|
const group = str(a, "group");
|
|
4284
|
-
const
|
|
4285
|
-
if (!
|
|
4286
|
-
return asResult(await pit(["apply", ...group && !
|
|
4295
|
+
const run2 = str(a, "run");
|
|
4296
|
+
if (!run2 && !group) throw new ToolError('give "run" or "group"');
|
|
4297
|
+
return asResult(await pit(["apply", ...group && !run2 ? ["-g", group] : [run2], ...bool(a, "allowDelete") ? ["--allow-delete"] : []], 12e4));
|
|
4287
4298
|
}
|
|
4288
4299
|
},
|
|
4289
4300
|
{
|
|
@@ -4303,10 +4314,10 @@ var TOOLS2 = [
|
|
|
4303
4314
|
inputSchema: { type: "object", properties: { run: RUN_ID2, group: { type: "string" } } },
|
|
4304
4315
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
4305
4316
|
async call(a) {
|
|
4306
|
-
const
|
|
4317
|
+
const run2 = str(a, "run");
|
|
4307
4318
|
const group = str(a, "group");
|
|
4308
|
-
if (!
|
|
4309
|
-
return asResult(await pit(["stop", ...
|
|
4319
|
+
if (!run2 && !group) throw new ToolError('give "run" or "group"');
|
|
4320
|
+
return asResult(await pit(["stop", ...run2 ? [run2] : ["-g", group]], 6e4));
|
|
4310
4321
|
}
|
|
4311
4322
|
}
|
|
4312
4323
|
];
|
|
@@ -4397,9 +4408,9 @@ async function serveMcp() {
|
|
|
4397
4408
|
}
|
|
4398
4409
|
|
|
4399
4410
|
// src/cli/commands.ts
|
|
4400
|
-
import { spawnSync as
|
|
4401
|
-
import
|
|
4402
|
-
import
|
|
4411
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
4412
|
+
import fs30 from "node:fs";
|
|
4413
|
+
import path26 from "node:path";
|
|
4403
4414
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
4404
4415
|
|
|
4405
4416
|
// src/core/init.ts
|
|
@@ -4589,18 +4600,217 @@ ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \xB7 Ctrl-C to stop watchin
|
|
|
4589
4600
|
}
|
|
4590
4601
|
}
|
|
4591
4602
|
|
|
4592
|
-
// src/core/
|
|
4593
|
-
import
|
|
4603
|
+
// src/core/mcp-install.ts
|
|
4604
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
4594
4605
|
import fs26 from "node:fs";
|
|
4606
|
+
import os10 from "node:os";
|
|
4595
4607
|
import path22 from "node:path";
|
|
4608
|
+
var CLIENT_IDS = ["claude-code", "codex", "gemini", "cursor", "claude-desktop"];
|
|
4609
|
+
var NAME = "pitroom";
|
|
4610
|
+
function mcpCommand() {
|
|
4611
|
+
const launcher = launcherPath();
|
|
4612
|
+
if (isPitroomLauncher(launcher)) return { command: launcher, args: ["mcp"] };
|
|
4613
|
+
for (const dir of (process.env.PATH ?? "").split(path22.delimiter).filter(Boolean)) {
|
|
4614
|
+
const p = path22.join(dir, process.platform === "win32" ? "pitroom.cmd" : "pitroom");
|
|
4615
|
+
if (fs26.existsSync(p)) return { command: path22.resolve(p), args: ["mcp"] };
|
|
4616
|
+
}
|
|
4617
|
+
return { command: process.execPath, args: [process.argv[1] ?? "pitroom", "mcp"] };
|
|
4618
|
+
}
|
|
4619
|
+
var same = (a, b) => {
|
|
4620
|
+
const o = b;
|
|
4621
|
+
return !!o && o.command === a.command && Array.isArray(o.args) && o.args.join("\0") === a.args.join("\0");
|
|
4622
|
+
};
|
|
4623
|
+
function run(backendId, args) {
|
|
4624
|
+
const { command, prefix } = resolveCommand(getBackend(backendId).binary());
|
|
4625
|
+
const r = spawnSync7(command, [...prefix, ...args], { encoding: "utf8", timeout: 6e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
4626
|
+
return { ok: r.status === 0 && !r.error, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
|
|
4627
|
+
}
|
|
4628
|
+
var runnable = (backendId) => run(backendId, ["--version"]).ok;
|
|
4629
|
+
var readJson = (file2) => {
|
|
4630
|
+
try {
|
|
4631
|
+
return JSON.parse(fs26.readFileSync(file2, "utf8"));
|
|
4632
|
+
} catch {
|
|
4633
|
+
return void 0;
|
|
4634
|
+
}
|
|
4635
|
+
};
|
|
4636
|
+
var jsonState = (file2, c) => {
|
|
4637
|
+
const entry = readJson(file2)?.mcpServers?.[NAME];
|
|
4638
|
+
return entry === void 0 ? "absent" : same(c, entry) ? "same" : "different";
|
|
4639
|
+
};
|
|
4640
|
+
var home2 = () => os10.homedir();
|
|
4641
|
+
var geminiDir = () => path22.join(process.env.GEMINI_CLI_HOME ?? home2(), ".gemini");
|
|
4642
|
+
function cliClient(id, backend, name, where, o) {
|
|
4643
|
+
return {
|
|
4644
|
+
id,
|
|
4645
|
+
name,
|
|
4646
|
+
where,
|
|
4647
|
+
found: () => runnable(backend),
|
|
4648
|
+
state: o.state,
|
|
4649
|
+
add: (c) => {
|
|
4650
|
+
const r = run(backend, o.add(c));
|
|
4651
|
+
return { ok: r.ok, message: r.ok ? where : r.out.split("\n")[0]?.trim() || `${name}'s command failed` };
|
|
4652
|
+
},
|
|
4653
|
+
remove: () => {
|
|
4654
|
+
const r = run(backend, o.remove());
|
|
4655
|
+
return { ok: r.ok, message: r.ok ? where : r.out.split("\n")[0]?.trim() || `${name}'s command failed` };
|
|
4656
|
+
}
|
|
4657
|
+
};
|
|
4658
|
+
}
|
|
4659
|
+
function jsonClient(id, name, file2, dir) {
|
|
4660
|
+
const rel = () => file2().replace(home2(), "~");
|
|
4661
|
+
return {
|
|
4662
|
+
id,
|
|
4663
|
+
name,
|
|
4664
|
+
found: () => fs26.existsSync(dir()),
|
|
4665
|
+
state: (c) => jsonState(file2(), c),
|
|
4666
|
+
add: (c) => {
|
|
4667
|
+
let config = {};
|
|
4668
|
+
if (fs26.existsSync(file2())) {
|
|
4669
|
+
config = readJson(file2());
|
|
4670
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) return { ok: false, message: `${rel()} is not valid JSON: left alone` };
|
|
4671
|
+
const backup = `${file2()}.bak-pitroom`;
|
|
4672
|
+
if (!fs26.existsSync(backup)) fs26.copyFileSync(file2(), backup);
|
|
4673
|
+
}
|
|
4674
|
+
config.mcpServers = { ...config.mcpServers ?? {}, [NAME]: { command: c.command, args: c.args } };
|
|
4675
|
+
fs26.mkdirSync(path22.dirname(file2()), { recursive: true });
|
|
4676
|
+
const tmp = `${file2()}.${process.pid}.tmp`;
|
|
4677
|
+
fs26.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
|
|
4678
|
+
`);
|
|
4679
|
+
fs26.renameSync(tmp, file2());
|
|
4680
|
+
return { ok: true, message: rel() };
|
|
4681
|
+
},
|
|
4682
|
+
remove: () => {
|
|
4683
|
+
const config = readJson(file2());
|
|
4684
|
+
if (!config?.mcpServers?.[NAME]) return { ok: true, message: rel() };
|
|
4685
|
+
delete config.mcpServers[NAME];
|
|
4686
|
+
fs26.writeFileSync(file2(), `${JSON.stringify(config, null, 2)}
|
|
4687
|
+
`);
|
|
4688
|
+
return { ok: true, message: rel() };
|
|
4689
|
+
}
|
|
4690
|
+
};
|
|
4691
|
+
}
|
|
4692
|
+
function desktopFile() {
|
|
4693
|
+
if (process.platform === "darwin") return path22.join(home2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
4694
|
+
if (process.platform === "win32") return path22.join(process.env.APPDATA ?? path22.join(home2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
4695
|
+
return path22.join(process.env.XDG_CONFIG_HOME ?? path22.join(home2(), ".config"), "Claude", "claude_desktop_config.json");
|
|
4696
|
+
}
|
|
4697
|
+
function codexState(c) {
|
|
4698
|
+
let text;
|
|
4699
|
+
try {
|
|
4700
|
+
text = fs26.readFileSync(path22.join(process.env.CODEX_HOME ?? path22.join(home2(), ".codex"), "config.toml"), "utf8");
|
|
4701
|
+
} catch {
|
|
4702
|
+
return "absent";
|
|
4703
|
+
}
|
|
4704
|
+
const at = text.search(/^\[mcp_servers\.pitroom\]\s*$/m);
|
|
4705
|
+
if (at < 0) return "absent";
|
|
4706
|
+
const table2 = text.slice(at).split(/\n(?=\[)/)[0] ?? "";
|
|
4707
|
+
try {
|
|
4708
|
+
const command = /^command\s*=\s*("(?:[^"\\]|\\.)*")/m.exec(table2)?.[1];
|
|
4709
|
+
const args = /^args\s*=\s*(\[[^\n]*\])/m.exec(table2)?.[1];
|
|
4710
|
+
return command && args && same(c, { command: JSON.parse(command), args: JSON.parse(args) }) ? "same" : "different";
|
|
4711
|
+
} catch {
|
|
4712
|
+
return "different";
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4715
|
+
function clients() {
|
|
4716
|
+
return [
|
|
4717
|
+
cliClient("claude-code", "claude", "Claude Code", "user settings (~/.claude.json)", {
|
|
4718
|
+
state: (c) => jsonState(path22.join(home2(), ".claude.json"), c),
|
|
4719
|
+
add: (c) => ["mcp", "add", "--scope", "user", NAME, "--", c.command, ...c.args],
|
|
4720
|
+
remove: () => ["mcp", "remove", NAME, "--scope", "user"]
|
|
4721
|
+
}),
|
|
4722
|
+
cliClient("codex", "codex", "Codex", "~/.codex/config.toml", {
|
|
4723
|
+
state: (c) => codexState(c),
|
|
4724
|
+
add: (c) => ["mcp", "add", NAME, "--", c.command, ...c.args],
|
|
4725
|
+
remove: () => ["mcp", "remove", NAME]
|
|
4726
|
+
}),
|
|
4727
|
+
cliClient("gemini", "gemini", "Gemini CLI", "user settings (~/.gemini/settings.json)", {
|
|
4728
|
+
state: (c) => jsonState(path22.join(geminiDir(), "settings.json"), c),
|
|
4729
|
+
add: (c) => ["mcp", "add", "--scope", "user", NAME, c.command, ...c.args],
|
|
4730
|
+
remove: () => ["mcp", "remove", "--scope", "user", NAME]
|
|
4731
|
+
}),
|
|
4732
|
+
jsonClient("cursor", "Cursor", () => path22.join(home2(), ".cursor", "mcp.json"), () => path22.join(home2(), ".cursor")),
|
|
4733
|
+
jsonClient("claude-desktop", "Claude Desktop", desktopFile, () => path22.dirname(desktopFile()))
|
|
4734
|
+
];
|
|
4735
|
+
}
|
|
4736
|
+
function attempt2(f) {
|
|
4737
|
+
try {
|
|
4738
|
+
return f();
|
|
4739
|
+
} catch (e) {
|
|
4740
|
+
return { ok: false, message: e.message.split("\n")[0] ?? "failed" };
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
function installMcp(opts = {}) {
|
|
4744
|
+
const c = mcpCommand();
|
|
4745
|
+
const out = [];
|
|
4746
|
+
for (const client of clients()) {
|
|
4747
|
+
if (opts.only && !opts.only.includes(client.id)) continue;
|
|
4748
|
+
const r = (state, message) => out.push({ id: client.id, name: client.name, state, message });
|
|
4749
|
+
if (!client.found()) {
|
|
4750
|
+
r("not-found", client.id === "cursor" || client.id === "claude-desktop" ? "not found on this machine" : "not installed (its command does not run)");
|
|
4751
|
+
continue;
|
|
4752
|
+
}
|
|
4753
|
+
const now = client.state(c);
|
|
4754
|
+
if (now === "same" && !opts.force) {
|
|
4755
|
+
r("already", "already registered");
|
|
4756
|
+
continue;
|
|
4757
|
+
}
|
|
4758
|
+
if (opts.dryRun) {
|
|
4759
|
+
r("would-add", `would register ${[c.command, ...c.args].join(" ")}`);
|
|
4760
|
+
continue;
|
|
4761
|
+
}
|
|
4762
|
+
if (now !== "absent") attempt2(() => client.remove());
|
|
4763
|
+
const done = attempt2(() => client.add(c));
|
|
4764
|
+
r(done.ok ? now === "absent" ? "added" : "updated" : "failed", done.ok ? `${now === "absent" ? "registered" : "updated"} in ${done.message}` : done.message);
|
|
4765
|
+
}
|
|
4766
|
+
return out;
|
|
4767
|
+
}
|
|
4768
|
+
function uninstallMcp() {
|
|
4769
|
+
const c = mcpCommand();
|
|
4770
|
+
const out = [];
|
|
4771
|
+
for (const client of clients()) {
|
|
4772
|
+
let has2 = false;
|
|
4773
|
+
try {
|
|
4774
|
+
has2 = client.state(c) !== "absent";
|
|
4775
|
+
} catch {
|
|
4776
|
+
has2 = false;
|
|
4777
|
+
}
|
|
4778
|
+
if (!has2) continue;
|
|
4779
|
+
const done = client.found() ? attempt2(() => client.remove()) : { ok: false, message: `its command does not run: remove the pitroom entry from its config by hand` };
|
|
4780
|
+
out.push({ id: client.id, name: client.name, state: done.ok ? "removed" : "failed", message: done.ok ? `removed from ${done.message}` : done.message });
|
|
4781
|
+
}
|
|
4782
|
+
return out;
|
|
4783
|
+
}
|
|
4784
|
+
function mcpStatus() {
|
|
4785
|
+
const c = mcpCommand();
|
|
4786
|
+
return clients().map((client) => {
|
|
4787
|
+
try {
|
|
4788
|
+
return { id: client.id, name: client.name, state: client.state(c) };
|
|
4789
|
+
} catch {
|
|
4790
|
+
return { id: client.id, name: client.name, state: "absent" };
|
|
4791
|
+
}
|
|
4792
|
+
});
|
|
4793
|
+
}
|
|
4794
|
+
var parseClients = (value) => {
|
|
4795
|
+
if (!value) return void 0;
|
|
4796
|
+
const wanted = value.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
4797
|
+
const bad = wanted.filter((w) => !CLIENT_IDS.includes(w));
|
|
4798
|
+
if (bad.length) throw new Error(`unknown client ${bad.join(", ")} (one of: ${CLIENT_IDS.join(", ")})`);
|
|
4799
|
+
return wanted;
|
|
4800
|
+
};
|
|
4801
|
+
|
|
4802
|
+
// src/core/plan-status.ts
|
|
4803
|
+
import crypto6 from "node:crypto";
|
|
4804
|
+
import fs27 from "node:fs";
|
|
4805
|
+
import path23 from "node:path";
|
|
4596
4806
|
function notesFile(plan) {
|
|
4597
4807
|
const id = crypto6.createHash("sha1").update(plan.file).digest("hex").slice(0, 12);
|
|
4598
|
-
return
|
|
4808
|
+
return path23.join(home(), "plans", id, "notes.md");
|
|
4599
4809
|
}
|
|
4600
4810
|
function readNotes(plan) {
|
|
4601
4811
|
const f = notesFile(plan);
|
|
4602
|
-
if (!
|
|
4603
|
-
return
|
|
4812
|
+
if (!fs27.existsSync(f)) return [];
|
|
4813
|
+
return fs27.readFileSync(f, "utf8").split("\n").slice(1).filter(Boolean).map((l) => {
|
|
4604
4814
|
const m = /^(\d{4}-\d\d-\d\d \d\d:\d\d) (.*)$/.exec(l);
|
|
4605
4815
|
return m ? { at: m[1], text: m[2] } : { at: "", text: l };
|
|
4606
4816
|
});
|
|
@@ -4608,11 +4818,11 @@ function readNotes(plan) {
|
|
|
4608
4818
|
function addNote(planFile, text) {
|
|
4609
4819
|
const plan = loadPlan(planFile);
|
|
4610
4820
|
const f = notesFile(plan);
|
|
4611
|
-
|
|
4612
|
-
if (!
|
|
4821
|
+
fs27.mkdirSync(path23.dirname(f), { recursive: true });
|
|
4822
|
+
if (!fs27.existsSync(f)) fs27.writeFileSync(f, `# plan: ${plan.file}
|
|
4613
4823
|
`);
|
|
4614
4824
|
const at = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
|
|
4615
|
-
|
|
4825
|
+
fs27.appendFileSync(f, `${at} ${text.replace(/\s+/g, " ").trim()}
|
|
4616
4826
|
`);
|
|
4617
4827
|
return f;
|
|
4618
4828
|
}
|
|
@@ -4680,8 +4890,8 @@ ${rulings}`,
|
|
|
4680
4890
|
|
|
4681
4891
|
// src/core/review.ts
|
|
4682
4892
|
import crypto7 from "node:crypto";
|
|
4683
|
-
import
|
|
4684
|
-
import
|
|
4893
|
+
import fs28 from "node:fs";
|
|
4894
|
+
import path24 from "node:path";
|
|
4685
4895
|
var TEMPLATE2 = { task: "task-reviewer", fix: "re-review", range: "code-reviewer" };
|
|
4686
4896
|
var section = (title, body) => `## ${title}
|
|
4687
4897
|
|
|
@@ -4699,7 +4909,7 @@ function ancestors(m) {
|
|
|
4699
4909
|
function briefOf(m) {
|
|
4700
4910
|
const root = [m, ...ancestors(m)].at(-1);
|
|
4701
4911
|
const f = runFile(root.id, "brief.md");
|
|
4702
|
-
return
|
|
4912
|
+
return fs28.existsSync(f) ? fs28.readFileSync(f, "utf8") : root.task;
|
|
4703
4913
|
}
|
|
4704
4914
|
function latestReview(ids) {
|
|
4705
4915
|
for (const id of listRunIds().reverse()) {
|
|
@@ -4713,7 +4923,7 @@ function latestReview(ids) {
|
|
|
4713
4923
|
}
|
|
4714
4924
|
function diffOf(m, from = m.baseTree) {
|
|
4715
4925
|
const where = m.mode === "isolate" ? m.worktree : m.repoRoot;
|
|
4716
|
-
if (where &&
|
|
4926
|
+
if (where && fs28.existsSync(where) && from && m.afterTree) return reviewDiff(where, from, m.afterTree);
|
|
4717
4927
|
if (from !== m.baseTree) {
|
|
4718
4928
|
throw new UserError(`the isolated copy of ${m.id} is gone, so its fix round cannot be shown on its own; review a run that is not applied yet`, 3);
|
|
4719
4929
|
}
|
|
@@ -4727,7 +4937,7 @@ function runReview(id) {
|
|
|
4727
4937
|
if (!m.changes?.length) throw new UserError(`run ${m.id} made no changes; nothing to review`);
|
|
4728
4938
|
const chain = ancestors(m);
|
|
4729
4939
|
const previous = latestReview(chain.map((a) => a.id));
|
|
4730
|
-
const dir = m.mode === "isolate" && m.worktree &&
|
|
4940
|
+
const dir = m.mode === "isolate" && m.worktree && fs28.existsSync(m.worktree) ? m.cwd : m.dir;
|
|
4731
4941
|
const job = { of: m.id, dir, implementer: m.ran ?? m.worker, group: m.group, plan: m.plan };
|
|
4732
4942
|
const title = m.plan ? `Task ${m.plan.step}: ${m.plan.title}` : `run ${m.id}`;
|
|
4733
4943
|
if (!previous) {
|
|
@@ -4801,9 +5011,9 @@ function pickReviewer(job) {
|
|
|
4801
5011
|
function writePackage(job) {
|
|
4802
5012
|
const g = gitDir(job.dir);
|
|
4803
5013
|
if (!g) throw new UserError(`not a git repository: ${job.dir}`);
|
|
4804
|
-
const file2 =
|
|
4805
|
-
|
|
4806
|
-
|
|
5014
|
+
const file2 = path24.join(g, "pitroom", `review-${crypto7.randomBytes(4).toString("hex")}.md`);
|
|
5015
|
+
fs28.mkdirSync(path24.dirname(file2), { recursive: true });
|
|
5016
|
+
fs28.writeFileSync(file2, job.package);
|
|
4807
5017
|
return file2;
|
|
4808
5018
|
}
|
|
4809
5019
|
|
|
@@ -4906,15 +5116,15 @@ function formatModels(t) {
|
|
|
4906
5116
|
// src/core/dash.ts
|
|
4907
5117
|
import { spawn as spawn4 } from "node:child_process";
|
|
4908
5118
|
import crypto8 from "node:crypto";
|
|
4909
|
-
import
|
|
5119
|
+
import fs29 from "node:fs";
|
|
4910
5120
|
import http from "node:http";
|
|
4911
|
-
import
|
|
5121
|
+
import path25 from "node:path";
|
|
4912
5122
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
4913
5123
|
|
|
4914
5124
|
// src/core/file-diff.ts
|
|
4915
5125
|
var PREVIEW_LINES = 300;
|
|
4916
5126
|
function gitPath(value, prefix = true) {
|
|
4917
|
-
let
|
|
5127
|
+
let path28 = value;
|
|
4918
5128
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
4919
5129
|
const bytes = [];
|
|
4920
5130
|
const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
|
|
@@ -4922,9 +5132,9 @@ function gitPath(value, prefix = true) {
|
|
|
4922
5132
|
if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
|
|
4923
5133
|
else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
|
|
4924
5134
|
}
|
|
4925
|
-
|
|
5135
|
+
path28 = new TextDecoder().decode(new Uint8Array(bytes));
|
|
4926
5136
|
}
|
|
4927
|
-
return prefix ?
|
|
5137
|
+
return prefix ? path28.replace(/^[ab]\//, "") : path28;
|
|
4928
5138
|
}
|
|
4929
5139
|
function headerPath(header) {
|
|
4930
5140
|
const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
|
|
@@ -5172,11 +5382,11 @@ var assetsDir = () => fileURLToPath3(new URL("./ui/", import.meta.url));
|
|
|
5172
5382
|
var assetCache = /* @__PURE__ */ new Map();
|
|
5173
5383
|
function readAsset(route) {
|
|
5174
5384
|
try {
|
|
5175
|
-
const file2 =
|
|
5176
|
-
const { mtimeMs } =
|
|
5385
|
+
const file2 = path25.join(assetsDir(), path25.basename(route));
|
|
5386
|
+
const { mtimeMs } = fs29.statSync(file2);
|
|
5177
5387
|
const hit = assetCache.get(route);
|
|
5178
5388
|
if (hit && hit.mtimeMs === mtimeMs) return hit.data;
|
|
5179
|
-
const data =
|
|
5389
|
+
const data = fs29.readFileSync(file2);
|
|
5180
5390
|
assetCache.set(route, { mtimeMs, data });
|
|
5181
5391
|
return data;
|
|
5182
5392
|
} catch {
|
|
@@ -5256,7 +5466,7 @@ function startDash(opts) {
|
|
|
5256
5466
|
});
|
|
5257
5467
|
});
|
|
5258
5468
|
}
|
|
5259
|
-
var registryFile = () =>
|
|
5469
|
+
var registryFile = () => path25.join(home(), "dash.json");
|
|
5260
5470
|
async function ping(port) {
|
|
5261
5471
|
try {
|
|
5262
5472
|
const r = await fetch(`http://127.0.0.1:${port}/api/state?limit=1`, { signal: AbortSignal.timeout(1500) });
|
|
@@ -5267,11 +5477,11 @@ async function ping(port) {
|
|
|
5267
5477
|
}
|
|
5268
5478
|
async function runningDash() {
|
|
5269
5479
|
try {
|
|
5270
|
-
const r = JSON.parse(
|
|
5480
|
+
const r = JSON.parse(fs29.readFileSync(registryFile(), "utf8"));
|
|
5271
5481
|
if (isAlive(r.pid) && await ping(r.port)) return { ...r, url: `http://127.0.0.1:${r.port}/` };
|
|
5272
5482
|
} catch {
|
|
5273
5483
|
}
|
|
5274
|
-
|
|
5484
|
+
fs29.rmSync(registryFile(), { force: true });
|
|
5275
5485
|
return void 0;
|
|
5276
5486
|
}
|
|
5277
5487
|
function openBrowser(url) {
|
|
@@ -5286,7 +5496,7 @@ async function dashCommand(o) {
|
|
|
5286
5496
|
if (o.stop) {
|
|
5287
5497
|
if (!existing) throw new UserError("pitroom dash is not running");
|
|
5288
5498
|
process.kill(existing.pid, "SIGTERM");
|
|
5289
|
-
|
|
5499
|
+
fs29.rmSync(registryFile(), { force: true });
|
|
5290
5500
|
o.log(`stopped pitroom dash (${existing.url})`);
|
|
5291
5501
|
return 0;
|
|
5292
5502
|
}
|
|
@@ -5323,8 +5533,8 @@ async function dashCommand(o) {
|
|
|
5323
5533
|
if (explicit || e.code !== "EADDRINUSE") throw new UserError(`pitroom dash: ${e.message}`);
|
|
5324
5534
|
dash = await startDash({ port: 0, idleMs: o.idleMs });
|
|
5325
5535
|
}
|
|
5326
|
-
|
|
5327
|
-
|
|
5536
|
+
fs29.mkdirSync(home(), { recursive: true });
|
|
5537
|
+
fs29.writeFileSync(registryFile(), JSON.stringify({ pid: process.pid, port: dash.port }));
|
|
5328
5538
|
const stop = () => void dash.close();
|
|
5329
5539
|
process.once("SIGINT", stop);
|
|
5330
5540
|
process.once("SIGTERM", stop);
|
|
@@ -5333,7 +5543,7 @@ async function dashCommand(o) {
|
|
|
5333
5543
|
if (o.open) openBrowser(dash.url);
|
|
5334
5544
|
await dash.closed;
|
|
5335
5545
|
try {
|
|
5336
|
-
if (JSON.parse(
|
|
5546
|
+
if (JSON.parse(fs29.readFileSync(registryFile(), "utf8")).pid === process.pid) fs29.rmSync(registryFile(), { force: true });
|
|
5337
5547
|
} catch {
|
|
5338
5548
|
}
|
|
5339
5549
|
return 0;
|
|
@@ -5386,10 +5596,10 @@ async function cmdReview(p) {
|
|
|
5386
5596
|
writeMeta(meta);
|
|
5387
5597
|
}
|
|
5388
5598
|
} catch (e) {
|
|
5389
|
-
|
|
5599
|
+
fs30.rmSync(packageFile, { force: true });
|
|
5390
5600
|
throw e;
|
|
5391
5601
|
}
|
|
5392
|
-
|
|
5602
|
+
fs30.writeFileSync(runFile(meta.id, "package.md"), job.package);
|
|
5393
5603
|
return launch(p, meta);
|
|
5394
5604
|
}
|
|
5395
5605
|
async function cmdAudit(p) {
|
|
@@ -5457,7 +5667,7 @@ function cmdPlan(p) {
|
|
|
5457
5667
|
}
|
|
5458
5668
|
function cmdCrew(p) {
|
|
5459
5669
|
const file2 = flag(p, "task-file");
|
|
5460
|
-
const tasks = (file2 ?
|
|
5670
|
+
const tasks = (file2 ? fs30.readFileSync(file2, "utf8").split(/^\s*---\s*$/m) : p.positional).map((t) => t.trim()).filter(Boolean);
|
|
5461
5671
|
if (!tasks.length) throw new UserError('crew needs tasks: pitroom crew -g NAME "task 1" "task 2" (or --task-file with --- separators)');
|
|
5462
5672
|
if (has(p, "write") && tasks.length > 1) {
|
|
5463
5673
|
throw new UserError("parallel --write runs would edit the same tree; use --isolate (each worker gets its own isolated copy)");
|
|
@@ -5660,13 +5870,13 @@ function sinceMs(s) {
|
|
|
5660
5870
|
if (!m) throw new UserError("--since takes 7d, 30d, \u2026 or all");
|
|
5661
5871
|
return Date.now() - Number(m[1]) * 864e5;
|
|
5662
5872
|
}
|
|
5663
|
-
var readStdin = () => process.stdin.isTTY ? "" :
|
|
5873
|
+
var readStdin = () => process.stdin.isTTY ? "" : fs30.readFileSync(0, "utf8");
|
|
5664
5874
|
function cmdStatusline(p) {
|
|
5665
5875
|
const input = readStdin();
|
|
5666
5876
|
const lines = [];
|
|
5667
5877
|
const then = flag(p, "then");
|
|
5668
5878
|
if (then) {
|
|
5669
|
-
const r =
|
|
5879
|
+
const r = spawnSync8("/bin/sh", ["-c", then], { input, encoding: "utf8", timeout: 5e3 });
|
|
5670
5880
|
if (r.stdout?.trimEnd()) lines.push(r.stdout.trimEnd());
|
|
5671
5881
|
}
|
|
5672
5882
|
try {
|
|
@@ -5700,8 +5910,8 @@ function cmdModels(p) {
|
|
|
5700
5910
|
return 0;
|
|
5701
5911
|
}
|
|
5702
5912
|
function cmdHookStart() {
|
|
5703
|
-
const script =
|
|
5704
|
-
if (
|
|
5913
|
+
const script = path26.join(path26.dirname(path26.dirname(fileURLToPath4(import.meta.url))), "hooks", "session-start.mjs");
|
|
5914
|
+
if (fs30.existsSync(script)) spawnSync8(process.execPath, [script], { stdio: ["ignore", "inherit", "ignore"], env: process.env });
|
|
5705
5915
|
return 0;
|
|
5706
5916
|
}
|
|
5707
5917
|
function cmdInit(p) {
|
|
@@ -5741,19 +5951,41 @@ function cmdSavings(p) {
|
|
|
5741
5951
|
est. saved ${usd(t.saved)} (vs ${primaryPrice().name} list prices)`);
|
|
5742
5952
|
const out = flag(p, "card");
|
|
5743
5953
|
if (out) {
|
|
5744
|
-
|
|
5745
|
-
console.log(`card written to ${
|
|
5954
|
+
fs30.writeFileSync(out, card(t, period));
|
|
5955
|
+
console.log(`card written to ${path26.resolve(out)}`);
|
|
5746
5956
|
}
|
|
5747
5957
|
if (has(p, "badge")) console.log(`})`);
|
|
5748
5958
|
return 0;
|
|
5749
5959
|
}
|
|
5960
|
+
var MCP_ICON = { added: "\u2714", updated: "\u2714", already: "\xB7", removed: "\u2714", "not-found": "\xB7", failed: "\u2718", "would-add": "\u2192" };
|
|
5750
5961
|
function cmdInstall(p) {
|
|
5751
|
-
|
|
5752
|
-
|
|
5962
|
+
const mcp = has(p, "mcp") || has(p, "client");
|
|
5963
|
+
if (has(p, "dry-run") && !mcp) throw new UserError("--dry-run goes with --mcp");
|
|
5964
|
+
if (has(p, "no-skills") && !mcp) throw new UserError("--no-skills goes with --mcp: without it install links the skills");
|
|
5965
|
+
let only;
|
|
5966
|
+
try {
|
|
5967
|
+
only = parseClients(flag(p, "client"));
|
|
5968
|
+
} catch (e) {
|
|
5969
|
+
throw new UserError(e.message);
|
|
5970
|
+
}
|
|
5971
|
+
if (!has(p, "dry-run")) for (const line of install({ copy: has(p, "copy"), force: has(p, "force"), skills: !has(p, "no-skills") })) console.log(line);
|
|
5972
|
+
if (!mcp) return 0;
|
|
5973
|
+
const results = installMcp({ only, dryRun: has(p, "dry-run"), force: has(p, "force") });
|
|
5974
|
+
console.log(`
|
|
5975
|
+
MCP clients (pitroom mcp):`);
|
|
5976
|
+
for (const r of results) console.log(`${MCP_ICON[r.state] ?? "\u2022"} ${r.name}: ${r.message}`);
|
|
5977
|
+
if (results.every((r) => r.state === "not-found")) {
|
|
5978
|
+
console.log("no MCP client found: add the server by hand (see the README), or install a client first");
|
|
5979
|
+
} else if (results.some((r) => r.state === "added" || r.state === "updated")) {
|
|
5980
|
+
console.log("restart those clients (or start a new session) so they pick the server up");
|
|
5981
|
+
}
|
|
5982
|
+
return results.some((r) => r.state === "failed") ? 1 : 0;
|
|
5753
5983
|
}
|
|
5754
5984
|
function cmdUninstall() {
|
|
5755
5985
|
for (const line of uninstall()) console.log(line);
|
|
5756
|
-
|
|
5986
|
+
const mcp = uninstallMcp();
|
|
5987
|
+
for (const r of mcp) console.log(`${MCP_ICON[r.state] ?? "\u2022"} ${r.name}: ${r.message}`);
|
|
5988
|
+
return mcp.some((r) => r.state === "failed") ? 1 : 0;
|
|
5757
5989
|
}
|
|
5758
5990
|
function cmdConfig(p) {
|
|
5759
5991
|
const { warnings } = loadConfig();
|
|
@@ -5762,7 +5994,7 @@ function cmdConfig(p) {
|
|
|
5762
5994
|
console.log(JSON.stringify({ path: configPath(), settings: eff, warnings }, null, 2));
|
|
5763
5995
|
return 0;
|
|
5764
5996
|
}
|
|
5765
|
-
console.log(`config file: ${configPath()}${
|
|
5997
|
+
console.log(`config file: ${configPath()}${fs30.existsSync(configPath()) ? "" : " (not present)"}`);
|
|
5766
5998
|
for (const [key, s] of Object.entries(eff)) {
|
|
5767
5999
|
const v = Array.isArray(s.value) ? s.value.join(", ") || "\u2014" : s.value && typeof s.value === "object" ? Object.entries(s.value).map(([k, m]) => `${k}=${m}`).join(", ") || "\u2014" : s.value === void 0 ? key === "model" ? "the worker's default" : "\u2014" : String(s.value);
|
|
5768
6000
|
console.log(` ${key.padEnd(15)} ${v} (${s.source})`);
|
|
@@ -5786,7 +6018,7 @@ function cmdClean(p) {
|
|
|
5786
6018
|
const m = readMeta(id);
|
|
5787
6019
|
if (m.mode === "isolate" && !m.discarded && !m.applied) discardRun(m);
|
|
5788
6020
|
recordRun(m);
|
|
5789
|
-
|
|
6021
|
+
fs30.rmSync(runDir(id), { recursive: true, force: true });
|
|
5790
6022
|
}
|
|
5791
6023
|
const snapshots = prune(Date.now(), 0);
|
|
5792
6024
|
console.log(`removed ${old.length} run(s)${snapshots ? ` and ${snapshots} read snapshot${snapshots === 1 ? "" : "s"}` : ""}; the history and the savings ledger are kept`);
|
|
@@ -5796,10 +6028,10 @@ var cmdRevert = (p) => (console.log(revertRun(freshMeta(resolveRun(p.positional[
|
|
|
5796
6028
|
var cmdDiscard = (p) => (console.log(discardRun(freshMeta(resolveRun(p.positional[0])))), 0);
|
|
5797
6029
|
|
|
5798
6030
|
// src/core/doctor.ts
|
|
5799
|
-
import { spawnSync as
|
|
5800
|
-
import
|
|
5801
|
-
import
|
|
5802
|
-
import
|
|
6031
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
6032
|
+
import fs31 from "node:fs";
|
|
6033
|
+
import os11 from "node:os";
|
|
6034
|
+
import path27 from "node:path";
|
|
5803
6035
|
var MARK = { ok: () => green("\u2714"), warn: () => yellow("!"), fail: () => red("\u2718") };
|
|
5804
6036
|
var NEXT = [
|
|
5805
6037
|
{ when: /no default model|no fallback workers/, command: "pitroom init", why: "propose a starter config (fallback models from your catalogue)" },
|
|
@@ -5809,14 +6041,14 @@ var NEXT = [
|
|
|
5809
6041
|
{ when: /Gemini CLI is not signed in|IneligibleTierError/, command: "export GEMINI_API_KEY=\u2026", why: "a Google AI Studio key for the Gemini worker" }
|
|
5810
6042
|
];
|
|
5811
6043
|
function firstNodeOnPath() {
|
|
5812
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
5813
|
-
const file2 =
|
|
6044
|
+
for (const dir of (process.env.PATH ?? "").split(path27.delimiter).filter(Boolean)) {
|
|
6045
|
+
const file2 = path27.join(dir, process.platform === "win32" ? "node.exe" : "node");
|
|
5814
6046
|
try {
|
|
5815
|
-
if (!
|
|
6047
|
+
if (!fs31.statSync(file2).isFile()) continue;
|
|
5816
6048
|
} catch {
|
|
5817
6049
|
continue;
|
|
5818
6050
|
}
|
|
5819
|
-
const r =
|
|
6051
|
+
const r = spawnSync9(file2, ["-p", "process.versions.node"], { encoding: "utf8", timeout: 5e3 });
|
|
5820
6052
|
return r.status === 0 ? { path: file2, version: r.stdout.trim() } : { path: file2, version: "" };
|
|
5821
6053
|
}
|
|
5822
6054
|
return void 0;
|
|
@@ -5839,12 +6071,12 @@ function doctor5(probe) {
|
|
|
5839
6071
|
const addAll = (list2) => list2.forEach((c) => add(c.level, c.message));
|
|
5840
6072
|
add("ok", `pitroom ${VERSION2} \xB7 node ${process.versions.node} \xB7 state in ${home()}`);
|
|
5841
6073
|
const onPath = firstNodeOnPath();
|
|
5842
|
-
if (onPath && onPath.version && tooOld(onPath.version) &&
|
|
6074
|
+
if (onPath && onPath.version && tooOld(onPath.version) && path27.resolve(onPath.path) !== path27.resolve(process.execPath)) {
|
|
5843
6075
|
add("warn", `the first \`node\` on PATH is v${onPath.version} (${onPath.path}), older than the 22.13 Pitroom needs: shells that agent apps start may use it (the pitroom command finds a newer Node itself; other tools may not)`);
|
|
5844
6076
|
}
|
|
5845
6077
|
add(gitAvailable() ? "ok" : "warn", gitAvailable() ? "git available" : "git not found: --write/--isolate tracking disabled");
|
|
5846
6078
|
const cfg = loadConfig();
|
|
5847
|
-
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${
|
|
6079
|
+
add(cfg.warnings.length ? "warn" : "ok", `config: ${configPath()}${fs31.existsSync(configPath()) ? "" : " (not present, defaults in use)"}`);
|
|
5848
6080
|
for (const w of cfg.warnings) add("warn", w);
|
|
5849
6081
|
if (process.platform !== "win32") {
|
|
5850
6082
|
const guarded = guardEnv(process.env).PATH?.startsWith(shimDir());
|
|
@@ -5916,6 +6148,11 @@ function doctor5(probe) {
|
|
|
5916
6148
|
for (const c of found) add("ok", c.message);
|
|
5917
6149
|
add("ok", `use it with -W ${backend.id}[:model], or name it in "fallback" or "tiers" in the config`);
|
|
5918
6150
|
}
|
|
6151
|
+
section2("MCP");
|
|
6152
|
+
const have = mcpStatus().filter((m) => m.state !== "absent");
|
|
6153
|
+
const stale = have.filter((m) => m.state === "different");
|
|
6154
|
+
add("ok", have.length ? `pitroom mcp is registered in: ${have.map((m) => m.name).join(", ")}` : "pitroom mcp is not registered in any client: `pitroom install --mcp` does it for the ones found (Cursor, Claude Desktop, Claude Code, Codex, Gemini CLI)");
|
|
6155
|
+
for (const m of stale) add("warn", `${m.name} runs a different command for pitroom mcp than this install's: \`pitroom install --mcp --no-skills\` updates it`);
|
|
5919
6156
|
section2("Skills and agents");
|
|
5920
6157
|
addAll(skillChecks());
|
|
5921
6158
|
if (probe && chain[0]) {
|
|
@@ -5962,7 +6199,7 @@ function liveProbe(target) {
|
|
|
5962
6199
|
title: "pitroom doctor probe"
|
|
5963
6200
|
});
|
|
5964
6201
|
const t0 = Date.now();
|
|
5965
|
-
const r =
|
|
6202
|
+
const r = spawnSync9(inv.command, inv.args, {
|
|
5966
6203
|
encoding: "utf8",
|
|
5967
6204
|
timeout: 3e5,
|
|
5968
6205
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -5970,9 +6207,9 @@ function liveProbe(target) {
|
|
|
5970
6207
|
env: guardEnv({ ...process.env, ...inv.env, PWD: process.cwd(), PITROOM_ACTIVE: "1" })
|
|
5971
6208
|
});
|
|
5972
6209
|
const secs2 = ((Date.now() - t0) / 1e3).toFixed(1);
|
|
5973
|
-
const
|
|
5974
|
-
if (
|
|
5975
|
-
const why = backend.failure(
|
|
6210
|
+
const run2 = backend.parse(r.stdout ?? "");
|
|
6211
|
+
if (run2.finalText.includes("PONG")) return { level: "ok", message: `live probe (${describeTarget(target)}) answered in ${secs2}s` };
|
|
6212
|
+
const why = backend.failure(run2, r.stderr ?? "", r.status)?.message ?? run2.finalText.slice(0, 200);
|
|
5976
6213
|
return { level: "fail", message: `live probe (${describeTarget(target)}) failed after ${secs2}s: ${why}` };
|
|
5977
6214
|
}
|
|
5978
6215
|
function skillChecks() {
|
|
@@ -5992,20 +6229,20 @@ function skillChecks() {
|
|
|
5992
6229
|
else if (!viaPlugin) checks.push({ level: "warn", message: `no Pitroom skills in ${base2}; run \`pitroom install\`` });
|
|
5993
6230
|
}
|
|
5994
6231
|
if (geminiExtensionInstalled()) {
|
|
5995
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
6232
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path27.sep}.agents${path27.sep}`) && i.names.length);
|
|
5996
6233
|
checks.push(
|
|
5997
6234
|
linked ? { level: "warn", message: "Pitroom is installed as a Gemini CLI extension and linked into ~/.agents/skills: Gemini loads the skills twice; run `pitroom uninstall` or `gemini extensions uninstall pitroom`" } : { level: "ok", message: "Gemini CLI extension installed (skills + context)" }
|
|
5998
6235
|
);
|
|
5999
6236
|
}
|
|
6000
6237
|
if (viaPlugin) {
|
|
6001
|
-
const linked = installedSkills().some((i) => i.base.includes(`${
|
|
6238
|
+
const linked = installedSkills().some((i) => i.base.includes(`${path27.sep}.claude${path27.sep}`) && i.names.length);
|
|
6002
6239
|
checks.push(
|
|
6003
6240
|
linked ? { level: "warn", message: "Pitroom is installed as a Claude Code plugin and linked into ~/.claude/skills: skills load twice; run `pitroom uninstall` or remove the plugin" } : { level: "ok", message: "Claude Code plugin installed (skills + session-start hook)" }
|
|
6004
6241
|
);
|
|
6005
6242
|
}
|
|
6006
6243
|
const launcher = launcherPath();
|
|
6007
|
-
if (
|
|
6008
|
-
const r =
|
|
6244
|
+
if (fs31.existsSync(launcher)) {
|
|
6245
|
+
const r = spawnSync9(launcher, ["--version"], { encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
6009
6246
|
checks.push(
|
|
6010
6247
|
r.status === 0 ? { level: "ok", message: `launcher ${launcher} \u2192 pitroom ${r.stdout.trim()}` } : { level: "fail", message: `launcher ${launcher} does not start: ${(r.stderr || r.stdout).trim().slice(0, 200)}` }
|
|
6011
6248
|
);
|
|
@@ -6015,13 +6252,13 @@ function skillChecks() {
|
|
|
6015
6252
|
return checks;
|
|
6016
6253
|
}
|
|
6017
6254
|
function geminiExtensionInstalled() {
|
|
6018
|
-
const
|
|
6019
|
-
return
|
|
6255
|
+
const home3 = process.env.GEMINI_CLI_HOME ?? os11.homedir();
|
|
6256
|
+
return fs31.existsSync(path27.join(home3, ".gemini", "extensions", "pitroom"));
|
|
6020
6257
|
}
|
|
6021
6258
|
function pluginInstalled() {
|
|
6022
6259
|
try {
|
|
6023
|
-
const f =
|
|
6024
|
-
const plugins = JSON.parse(
|
|
6260
|
+
const f = path27.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
6261
|
+
const plugins = JSON.parse(fs31.readFileSync(f, "utf8")).plugins ?? {};
|
|
6025
6262
|
return Object.keys(plugins).some((k) => k.startsWith("pitroom@"));
|
|
6026
6263
|
} catch {
|
|
6027
6264
|
return false;
|
|
@@ -6038,24 +6275,24 @@ function superpowersActive() {
|
|
|
6038
6275
|
for (const p of openCodePlugins()) {
|
|
6039
6276
|
if (/superpowers/i.test(p)) found.push(`OpenCode plugin ${p}`);
|
|
6040
6277
|
}
|
|
6041
|
-
for (const base2 of [
|
|
6042
|
-
const dir =
|
|
6043
|
-
if (
|
|
6278
|
+
for (const base2 of [path27.join(os11.homedir(), ".agents", "skills"), path27.join(os11.homedir(), ".claude", "skills")]) {
|
|
6279
|
+
const dir = path27.join(base2, "using-superpowers");
|
|
6280
|
+
if (fs31.existsSync(path27.join(dir, "SKILL.md"))) found.push(dir);
|
|
6044
6281
|
}
|
|
6045
6282
|
return found;
|
|
6046
6283
|
}
|
|
6047
6284
|
function claudePlugins() {
|
|
6048
6285
|
try {
|
|
6049
|
-
const f =
|
|
6050
|
-
return Object.keys(JSON.parse(
|
|
6286
|
+
const f = path27.join(os11.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
6287
|
+
return Object.keys(JSON.parse(fs31.readFileSync(f, "utf8")).plugins ?? {});
|
|
6051
6288
|
} catch {
|
|
6052
6289
|
return [];
|
|
6053
6290
|
}
|
|
6054
6291
|
}
|
|
6055
6292
|
function claudePluginEnabled(key) {
|
|
6056
6293
|
try {
|
|
6057
|
-
const f =
|
|
6058
|
-
return JSON.parse(
|
|
6294
|
+
const f = path27.join(os11.homedir(), ".claude", "settings.json");
|
|
6295
|
+
return JSON.parse(fs31.readFileSync(f, "utf8")).enabledPlugins?.[key] !== false;
|
|
6059
6296
|
} catch {
|
|
6060
6297
|
return true;
|
|
6061
6298
|
}
|
|
@@ -6064,7 +6301,7 @@ function codexPlugins() {
|
|
|
6064
6301
|
const plugins = /* @__PURE__ */ new Map();
|
|
6065
6302
|
let text;
|
|
6066
6303
|
try {
|
|
6067
|
-
text =
|
|
6304
|
+
text = fs31.readFileSync(path27.join(process.env.CODEX_HOME ?? path27.join(os11.homedir(), ".codex"), "config.toml"), "utf8");
|
|
6068
6305
|
} catch {
|
|
6069
6306
|
return plugins;
|
|
6070
6307
|
}
|
|
@@ -6082,11 +6319,11 @@ function codexPlugins() {
|
|
|
6082
6319
|
return plugins;
|
|
6083
6320
|
}
|
|
6084
6321
|
function openCodePlugins() {
|
|
6085
|
-
const dir =
|
|
6322
|
+
const dir = path27.join(process.env.XDG_CONFIG_HOME ?? path27.join(os11.homedir(), ".config"), "opencode");
|
|
6086
6323
|
const plugins = [];
|
|
6087
6324
|
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
6088
6325
|
try {
|
|
6089
|
-
const text =
|
|
6326
|
+
const text = fs31.readFileSync(path27.join(dir, name), "utf8").replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, "");
|
|
6090
6327
|
const list2 = JSON.parse(text).plugin;
|
|
6091
6328
|
if (Array.isArray(list2)) plugins.push(...list2.filter((p) => typeof p === "string"));
|
|
6092
6329
|
} catch {
|
|
@@ -6094,7 +6331,7 @@ function openCodePlugins() {
|
|
|
6094
6331
|
}
|
|
6095
6332
|
for (const folder of ["plugin", "plugins"]) {
|
|
6096
6333
|
try {
|
|
6097
|
-
for (const f of
|
|
6334
|
+
for (const f of fs31.readdirSync(path27.join(dir, folder))) plugins.push(path27.join(dir, folder, f));
|
|
6098
6335
|
} catch {
|
|
6099
6336
|
}
|
|
6100
6337
|
}
|
|
@@ -6154,7 +6391,10 @@ Usage
|
|
|
6154
6391
|
writes it only with --yes (models are suggested, never chosen for you)
|
|
6155
6392
|
pitroom install [--copy] [--force] link the skills into ~/.agents/skills + ~/.claude/skills,
|
|
6156
6393
|
and the CLI into ~/.local/bin
|
|
6157
|
-
pitroom
|
|
6394
|
+
pitroom install --mcp [--client A,B] [--no-skills] [--dry-run]
|
|
6395
|
+
also register "pitroom mcp" in the MCP clients found (Claude Code, Codex,
|
|
6396
|
+
Gemini CLI, Cursor, Claude Desktop); --dry-run only says what it would do
|
|
6397
|
+
pitroom uninstall remove what install linked (and the MCP registrations)
|
|
6158
6398
|
pitroom clean [--days 14] [--yes]
|
|
6159
6399
|
|
|
6160
6400
|
Run options
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pitroom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "A free pit crew for your expensive coding agent: hand reading, fixes, tests and reviews to cheap worker agents and get back the answer, the exact diff and a receipt. Fourteen skills adapt a full development workflow. Needs the pitroom command (npm i -g pitroom).",
|
|
5
5
|
"contextFileName": "gemini-context.md"
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pitroom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "A free pit crew for your expensive coding agent: the superpowers development workflow, run by cheap workers. Delegate research, fixes, tests, reviews and whole plans from Claude Code, Codex or any agent to OpenCode, Codex, Claude Code or Gemini CLI workers, with receipts that prove the savings.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "TahaAtas",
|