pitroom 0.8.1 → 0.10.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/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +26 -0
- package/README.md +31 -10
- package/THIRD_PARTY_NOTICES.md +3 -3
- package/dist/lib.mjs +367 -86
- package/dist/pitroom.mjs +981 -505
- package/dist/ui/app.css +1 -1
- package/dist/ui/app.js +30 -30
- package/docs/backends.md +28 -1
- package/package.json +3 -2
- package/policies/gemini/base.toml +27 -0
- package/policies/gemini/no-web.toml +6 -0
- package/policies/gemini/shell.toml +50 -0
- package/policies/gemini/worker-settings.json +9 -0
- package/skills/using-pitroom/SKILL.md +2 -2
package/dist/lib.mjs
CHANGED
|
@@ -208,7 +208,8 @@ var DENY_BASH = [
|
|
|
208
208
|
"pitroom",
|
|
209
209
|
"opencode",
|
|
210
210
|
"claude",
|
|
211
|
-
"codex"
|
|
211
|
+
"codex",
|
|
212
|
+
"gemini"
|
|
212
213
|
].map((c) => `Bash(${c}:*)`);
|
|
213
214
|
var SECRETS = ["**/.env", "**/.env.*", "**/*.pem", "**/id_rsa*", "**/id_ed25519*"].map((p) => `Read(${p})`);
|
|
214
215
|
function toolsFor(mode, web2) {
|
|
@@ -261,8 +262,8 @@ function failure(run, stderr, exitCode) {
|
|
|
261
262
|
}
|
|
262
263
|
function defaultModel() {
|
|
263
264
|
try {
|
|
264
|
-
const
|
|
265
|
-
return typeof
|
|
265
|
+
const settings2 = JSON.parse(fs2.readFileSync(path2.join(os2.homedir(), ".claude", "settings.json"), "utf8"));
|
|
266
|
+
return typeof settings2.model === "string" ? settings2.model : void 0;
|
|
266
267
|
} catch {
|
|
267
268
|
return void 0;
|
|
268
269
|
}
|
|
@@ -541,13 +542,285 @@ var codex = {
|
|
|
541
542
|
doctor: doctor2
|
|
542
543
|
};
|
|
543
544
|
|
|
544
|
-
// src/backends/
|
|
545
|
+
// src/backends/gemini/index.ts
|
|
545
546
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
547
|
+
import fs4 from "node:fs";
|
|
548
|
+
import os5 from "node:os";
|
|
549
|
+
import path5 from "node:path";
|
|
550
|
+
import { fileURLToPath } from "node:url";
|
|
551
|
+
|
|
552
|
+
// src/core/store.ts
|
|
553
|
+
import os4 from "node:os";
|
|
554
|
+
import path4 from "node:path";
|
|
555
|
+
function home() {
|
|
556
|
+
if (process.env.PITROOM_HOME) return path4.resolve(process.env.PITROOM_HOME);
|
|
557
|
+
if (process.platform === "win32") {
|
|
558
|
+
return path4.join(process.env.LOCALAPPDATA ?? path4.join(os4.homedir(), "AppData", "Local"), "pitroom");
|
|
559
|
+
}
|
|
560
|
+
return path4.join(process.env.XDG_STATE_HOME ?? path4.join(os4.homedir(), ".local", "state"), "pitroom");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/backends/gemini/events.ts
|
|
564
|
+
var EDIT_TOOLS2 = /* @__PURE__ */ new Set(["replace", "write_file", "edit"]);
|
|
565
|
+
var DENIED2 = /policy|permission|not allowed|denied|blocked|disallowed|refus/i;
|
|
566
|
+
function parseEvents3(jsonl) {
|
|
567
|
+
const usage = {
|
|
568
|
+
input: 0,
|
|
569
|
+
output: 0,
|
|
570
|
+
reasoning: 0,
|
|
571
|
+
cacheRead: 0,
|
|
572
|
+
cacheWrite: 0,
|
|
573
|
+
total: 0,
|
|
574
|
+
steps: 0,
|
|
575
|
+
toolCalls: 0,
|
|
576
|
+
denied: 0
|
|
577
|
+
};
|
|
578
|
+
const tools = {};
|
|
579
|
+
const edits = [];
|
|
580
|
+
const pendingEdits = /* @__PURE__ */ new Map();
|
|
581
|
+
const timeline = [];
|
|
582
|
+
const stepOf = /* @__PURE__ */ new Map();
|
|
583
|
+
let sessionId;
|
|
584
|
+
let model;
|
|
585
|
+
let text = "";
|
|
586
|
+
let lastText = "";
|
|
587
|
+
let lastActivity;
|
|
588
|
+
let error;
|
|
589
|
+
let turns = 0;
|
|
590
|
+
let inTurn = false;
|
|
591
|
+
const flush = () => {
|
|
592
|
+
const said = text.trim();
|
|
593
|
+
text = "";
|
|
594
|
+
if (!said) return;
|
|
595
|
+
lastText = said;
|
|
596
|
+
lastActivity = `says: ${oneLine3(said)}`;
|
|
597
|
+
if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip3(said, 600) });
|
|
598
|
+
};
|
|
599
|
+
const startTurn = () => {
|
|
600
|
+
if (!inTurn) turns++;
|
|
601
|
+
inTurn = true;
|
|
602
|
+
};
|
|
603
|
+
for (const line of jsonl.split("\n")) {
|
|
604
|
+
if (!line.trim()) continue;
|
|
605
|
+
let e;
|
|
606
|
+
try {
|
|
607
|
+
e = JSON.parse(line);
|
|
608
|
+
} catch {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (!e || typeof e !== "object") continue;
|
|
612
|
+
if (e.type === "init") {
|
|
613
|
+
sessionId ??= e.session_id;
|
|
614
|
+
model ??= e.model;
|
|
615
|
+
} else if (e.type === "message") {
|
|
616
|
+
if (e.role !== "assistant") continue;
|
|
617
|
+
startTurn();
|
|
618
|
+
if (!e.delta && text) flush();
|
|
619
|
+
text += String(e.content ?? "");
|
|
620
|
+
} else if (e.type === "tool_use") {
|
|
621
|
+
flush();
|
|
622
|
+
lastText = "";
|
|
623
|
+
startTurn();
|
|
624
|
+
const name = String(e.tool_name ?? "tool");
|
|
625
|
+
const p = e.parameters ?? {};
|
|
626
|
+
usage.toolCalls++;
|
|
627
|
+
tools[name] = (tools[name] ?? 0) + 1;
|
|
628
|
+
const target = p.file_path ?? p.absolute_path ?? p.path ?? p.dir_path ?? p.pattern ?? p.command ?? p.query ?? p.url ?? p.prompt;
|
|
629
|
+
lastActivity = `${name} ${target ? oneLine3(String(target), 60) : ""}`.trim();
|
|
630
|
+
const file = p.file_path ?? p.absolute_path ?? p.path;
|
|
631
|
+
if (EDIT_TOOLS2.has(name) && file) pendingEdits.set(String(e.tool_id), String(file));
|
|
632
|
+
if (timeline.length < MAX_STEPS) {
|
|
633
|
+
const step = { kind: EDIT_TOOLS2.has(name) ? "edit" : name === "run_shell_command" ? "shell" : "tool", name, text: clip3(String(target ?? ""), 240) };
|
|
634
|
+
timeline.push(step);
|
|
635
|
+
stepOf.set(String(e.tool_id), step);
|
|
636
|
+
}
|
|
637
|
+
} else if (e.type === "tool_result") {
|
|
638
|
+
inTurn = false;
|
|
639
|
+
const ok = e.status === "success";
|
|
640
|
+
const step = stepOf.get(String(e.tool_id));
|
|
641
|
+
if (step) step.ok = ok;
|
|
642
|
+
if (!ok && DENIED2.test(`${e.error?.type ?? ""} ${e.error?.message ?? ""}`)) usage.denied++;
|
|
643
|
+
if (ok && pendingEdits.has(String(e.tool_id))) edits.push(pendingEdits.get(String(e.tool_id)));
|
|
644
|
+
} else if (e.type === "error") {
|
|
645
|
+
if (e.severity === "error") error ??= String(e.message ?? "Gemini CLI error");
|
|
646
|
+
} else if (e.type === "result") {
|
|
647
|
+
const s = e.stats ?? {};
|
|
648
|
+
const cached = num3(s.cached);
|
|
649
|
+
usage.cacheRead += cached;
|
|
650
|
+
usage.input += s.input !== void 0 ? num3(s.input) : Math.max(0, num3(s.input_tokens) - cached);
|
|
651
|
+
usage.output += num3(s.output_tokens);
|
|
652
|
+
usage.total += num3(s.total_tokens) || num3(s.input_tokens) + num3(s.output_tokens);
|
|
653
|
+
usage.toolCalls = Math.max(usage.toolCalls, num3(s.tool_calls));
|
|
654
|
+
model ??= Object.keys(s.models ?? {})[0];
|
|
655
|
+
if (e.status === "error") error ??= String(e.error?.message ?? e.error?.type ?? "Gemini CLI error");
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
flush();
|
|
659
|
+
usage.steps = turns;
|
|
660
|
+
return { sessionId, model, finalText: error ? "" : lastText, usage, tools, edits, lastActivity, timeline, error };
|
|
661
|
+
}
|
|
662
|
+
var clip3 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
663
|
+
function num3(v) {
|
|
664
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
665
|
+
}
|
|
666
|
+
function oneLine3(s, max = 80) {
|
|
667
|
+
const t = s.replace(/\s+/g, " ").trim();
|
|
668
|
+
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// src/backends/gemini/index.ts
|
|
672
|
+
var binary3 = () => findBinary("gemini", "PITROOM_GEMINI_BIN", ["~/.local/bin/gemini"]);
|
|
673
|
+
function gem(args, timeout = 6e4) {
|
|
674
|
+
const { command, prefix } = resolveCommand(binary3());
|
|
675
|
+
const r = spawnSync3(command, [...prefix, ...args], {
|
|
676
|
+
encoding: "utf8",
|
|
677
|
+
timeout,
|
|
678
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
679
|
+
maxBuffer: 16 * 1024 * 1024
|
|
680
|
+
});
|
|
681
|
+
return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
|
|
682
|
+
}
|
|
683
|
+
var policyDir = () => path5.resolve(path5.dirname(fileURLToPath(import.meta.url)), "..", "policies", "gemini");
|
|
684
|
+
function workerHome() {
|
|
685
|
+
const root = path5.join(home(), "gemini-home");
|
|
686
|
+
const dir = path5.join(root, ".gemini");
|
|
687
|
+
const base = JSON.parse(fs4.readFileSync(path5.join(policyDir(), "worker-settings.json"), "utf8"));
|
|
688
|
+
const auth = settings().security?.auth;
|
|
689
|
+
const body = `${JSON.stringify(auth ? { ...base, security: { ...base.security, auth } } : base, null, 2)}
|
|
690
|
+
`;
|
|
691
|
+
const file = path5.join(dir, "settings.json");
|
|
692
|
+
try {
|
|
693
|
+
if (fs4.readFileSync(file, "utf8") === body) return root;
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
696
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
697
|
+
fs4.writeFileSync(file, body);
|
|
698
|
+
for (const name of ["oauth_creds.json", "google_accounts.json"]) {
|
|
699
|
+
const from = path5.join(geminiHome(), name);
|
|
700
|
+
const to = path5.join(dir, name);
|
|
701
|
+
try {
|
|
702
|
+
if (fs4.existsSync(from) && !fs4.existsSync(to)) fs4.symlinkSync(from, to);
|
|
703
|
+
} catch {
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return root;
|
|
707
|
+
}
|
|
708
|
+
function invocation3(req) {
|
|
709
|
+
const dir = policyDir();
|
|
710
|
+
const policies = ["base.toml", ...req.web ? [] : ["no-web.toml"], ...req.mode === "read" ? [] : ["shell.toml"]];
|
|
711
|
+
const args = [
|
|
712
|
+
// A prompt that starts with "-" would be read as an option: a leading space keeps it a value.
|
|
713
|
+
"--prompt",
|
|
714
|
+
req.prompt.startsWith("-") ? ` ${req.prompt}` : req.prompt,
|
|
715
|
+
"--output-format",
|
|
716
|
+
"stream-json",
|
|
717
|
+
"--approval-mode",
|
|
718
|
+
req.mode === "read" ? "plan" : "auto_edit",
|
|
719
|
+
"-e",
|
|
720
|
+
"none"
|
|
721
|
+
];
|
|
722
|
+
for (const p of policies) args.push("--policy", path5.join(dir, p));
|
|
723
|
+
if (process.env.PITROOM_GEMINI_TRUST === "1") args.push("--skip-trust");
|
|
724
|
+
const [model] = (req.model ?? "").split("#");
|
|
725
|
+
if (model) args.push("--model", model);
|
|
726
|
+
const { command, prefix } = resolveCommand(binary3());
|
|
727
|
+
const env = { GEMINI_CLI_HOME: workerHome() };
|
|
728
|
+
const trusted = path5.join(geminiHome(), "trustedFolders.json");
|
|
729
|
+
if (fs4.existsSync(trusted)) env.GEMINI_CLI_TRUSTED_FOLDERS_PATH = trusted;
|
|
730
|
+
return { command, args: [...prefix, ...args], env };
|
|
731
|
+
}
|
|
732
|
+
function classify3(message) {
|
|
733
|
+
if (/IneligibleTier|no longer supported for Gemini|error authenticating|not logged in|please (log ?in|sign in)|credentials|api key|UNAUTHENTICATED|PERMISSION_DENIED|\b40[13]\b|unauthori[sz]ed/i.test(message)) {
|
|
734
|
+
return "auth";
|
|
735
|
+
}
|
|
736
|
+
if (/RESOURCE_EXHAUSTED|\b429\b|quota|rate.?limit|too many requests|overloaded|\b503\b|exhausted your capacity|capacity/i.test(message)) return "rate-limited";
|
|
737
|
+
if (/model.{0,80}(not found|not available|no longer available|does not exist|unsupported|invalid)|\b404\b|NOT_FOUND|invalid model|is not supported/i.test(message)) return "model-unavailable";
|
|
738
|
+
return "other";
|
|
739
|
+
}
|
|
740
|
+
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)";
|
|
741
|
+
function failure3(run, stderr, exitCode) {
|
|
742
|
+
if (exitCode === 0 && !run.error) return void 0;
|
|
743
|
+
const lines = stderr.trim().split("\n").map((l) => l.trim()).filter(Boolean);
|
|
744
|
+
const detail = lines.find((l) => /error|failed|exceeded|quota|exhausted|unsupported/i.test(l) && !/^at /.test(l)) ?? lines.at(-1);
|
|
745
|
+
const raw = run.error ?? detail ?? `gemini exited with code ${exitCode}`;
|
|
746
|
+
const message = /not running in a trusted directory/i.test(`${stderr} ${raw}`) ? UNTRUSTED : raw;
|
|
747
|
+
return { kind: classify3(message), message };
|
|
748
|
+
}
|
|
749
|
+
function geminiHome() {
|
|
750
|
+
return path5.join(process.env.GEMINI_CLI_HOME ?? os5.homedir(), ".gemini");
|
|
751
|
+
}
|
|
752
|
+
function settings() {
|
|
753
|
+
try {
|
|
754
|
+
return JSON.parse(fs4.readFileSync(path5.join(geminiHome(), "settings.json"), "utf8"));
|
|
755
|
+
} catch {
|
|
756
|
+
return {};
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
function defaultModel2() {
|
|
760
|
+
if (process.env.GEMINI_MODEL) return process.env.GEMINI_MODEL;
|
|
761
|
+
const m = settings().model;
|
|
762
|
+
const name = typeof m === "string" ? m : m?.name;
|
|
763
|
+
return typeof name === "string" ? name : void 0;
|
|
764
|
+
}
|
|
765
|
+
function doctor3({ models, hasFallback }) {
|
|
766
|
+
const version = gem(["--version"]);
|
|
767
|
+
if (version.missing || !version.ok) {
|
|
768
|
+
return [{ level: "fail", message: `gemini not runnable (${binary3()}). Install Gemini CLI (npm i -g @google/gemini-cli), or set PITROOM_GEMINI_BIN` }];
|
|
769
|
+
}
|
|
770
|
+
const checks = [{ level: "ok", message: `Gemini CLI ${version.out.trim()} at ${binary3()}` }];
|
|
771
|
+
const key = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
772
|
+
const vertex = process.env.GOOGLE_GENAI_USE_VERTEXAI === "true" || process.env.GOOGLE_GENAI_USE_VERTEXAI === "1";
|
|
773
|
+
const signedIn = fs4.existsSync(path5.join(geminiHome(), "oauth_creds.json"));
|
|
774
|
+
const type = settings().security?.auth?.selectedType;
|
|
775
|
+
if (key || vertex) {
|
|
776
|
+
checks.push({ level: "ok", message: `Gemini CLI: ${key ? "an API key is set" : "Vertex AI is set up"}` });
|
|
777
|
+
} else if (signedIn && type === "oauth-personal") {
|
|
778
|
+
checks.push({
|
|
779
|
+
level: "warn",
|
|
780
|
+
message: "Gemini CLI is signed in with a Google account (oauth-personal). If a run fails with IneligibleTierError, Google no longer serves that sign-in to this CLI: set GEMINI_API_KEY (Google AI Studio) instead"
|
|
781
|
+
});
|
|
782
|
+
} else if (signedIn) {
|
|
783
|
+
checks.push({ level: "ok", message: "Gemini CLI: signed in" });
|
|
784
|
+
} else {
|
|
785
|
+
checks.push({ level: "fail", message: "Gemini CLI is not signed in: run `gemini` once to sign in, or set GEMINI_API_KEY (Google AI Studio)" });
|
|
786
|
+
}
|
|
787
|
+
for (const m of models) {
|
|
788
|
+
const model = m ?? defaultModel2();
|
|
789
|
+
const pricey = model && /pro/i.test(model);
|
|
790
|
+
checks.push({
|
|
791
|
+
level: pricey ? "warn" : "ok",
|
|
792
|
+
message: pricey ? `Gemini model: ${model} is the largest tier for a worker; consider -W gemini:gemini-3.8-flash` : model ? `Gemini model: ${model}` : "Gemini model: Gemini CLI's own choice (it may pick a Pro model; a cheaper worker is -W gemini:gemini-3.8-flash)"
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
if (!hasFallback) checks.push({ level: "warn", message: "no fallback workers configured for Gemini runs" });
|
|
796
|
+
return checks;
|
|
797
|
+
}
|
|
798
|
+
function catalog3() {
|
|
799
|
+
const def = defaultModel2();
|
|
800
|
+
const models = ["gemini-3.8-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview", "gemini-3-flash-preview"].map((id) => ({ id }));
|
|
801
|
+
if (def && !models.some((m) => m.id === def)) models.push({ id: def });
|
|
802
|
+
return { models, source: "Gemini CLI's built-in model names (it cannot list models); the effort suffix is not used" };
|
|
803
|
+
}
|
|
804
|
+
var gemini = {
|
|
805
|
+
id: "gemini",
|
|
806
|
+
name: "Gemini CLI",
|
|
807
|
+
capabilities: { readOnly: "approval-mode", resume: "none", reportsCost: false, attachFiles: false },
|
|
808
|
+
binary: binary3,
|
|
809
|
+
catalog: catalog3,
|
|
810
|
+
invocation: invocation3,
|
|
811
|
+
parse: parseEvents3,
|
|
812
|
+
failure: failure3,
|
|
813
|
+
defaultModel: defaultModel2,
|
|
814
|
+
doctor: doctor3
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
// src/backends/opencode/index.ts
|
|
818
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
546
819
|
|
|
547
820
|
// src/backends/opencode/events.ts
|
|
548
|
-
var
|
|
549
|
-
var
|
|
550
|
-
function
|
|
821
|
+
var EDIT_TOOLS3 = /* @__PURE__ */ new Set(["edit", "write", "patch", "multiedit", "apply_patch"]);
|
|
822
|
+
var DENIED3 = /rule which prevents you|permission denied|permission\.rejected/i;
|
|
823
|
+
function parseEvents4(ndjson) {
|
|
551
824
|
const usage = {
|
|
552
825
|
input: 0,
|
|
553
826
|
output: 0,
|
|
@@ -584,8 +857,8 @@ function parseEvents3(ndjson) {
|
|
|
584
857
|
const key = String(p.messageID ?? "");
|
|
585
858
|
if (!textByMessage.has(key)) textByMessage.set(key, []);
|
|
586
859
|
textByMessage.get(key).push(text);
|
|
587
|
-
lastActivity = `says: ${
|
|
588
|
-
if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text:
|
|
860
|
+
lastActivity = `says: ${oneLine4(text)}`;
|
|
861
|
+
if (timeline.length < MAX_STEPS) timeline.push({ kind: "say", text: clip4(text.trim(), 600), at: stamp(e.timestamp) });
|
|
589
862
|
break;
|
|
590
863
|
}
|
|
591
864
|
case "tool_use": {
|
|
@@ -593,28 +866,28 @@ function parseEvents3(ndjson) {
|
|
|
593
866
|
const st = p.state ?? {};
|
|
594
867
|
usage.toolCalls++;
|
|
595
868
|
tools[name] = (tools[name] ?? 0) + 1;
|
|
596
|
-
if (st.status === "error" &&
|
|
597
|
-
if (st.status === "completed" &&
|
|
869
|
+
if (st.status === "error" && DENIED3.test(String(st.error ?? ""))) usage.denied++;
|
|
870
|
+
if (st.status === "completed" && EDIT_TOOLS3.has(name)) {
|
|
598
871
|
edits.push(String(st.input?.filePath ?? st.input?.path ?? name));
|
|
599
872
|
}
|
|
600
873
|
lastActivity = `${name} ${describe2(st.input)}`.trim();
|
|
601
874
|
if (timeline.length < MAX_STEPS) {
|
|
602
|
-
const kind =
|
|
875
|
+
const kind = EDIT_TOOLS3.has(name) ? "edit" : /^(bash|shell)$/.test(name) ? "shell" : "tool";
|
|
603
876
|
const text = kind === "shell" ? String(st.input?.command ?? "") : String(st.input?.filePath ?? st.input?.path ?? st.input?.pattern ?? st.input?.url ?? "") || describe2(st.input);
|
|
604
|
-
timeline.push({ kind, name, text:
|
|
877
|
+
timeline.push({ kind, name, text: clip4(text, 240), ok: st.status === "completed" ? true : st.status === "error" ? false : void 0, at: stamp(e.timestamp) });
|
|
605
878
|
}
|
|
606
879
|
break;
|
|
607
880
|
}
|
|
608
881
|
case "step_finish": {
|
|
609
882
|
const t = p.tokens ?? {};
|
|
610
883
|
usage.steps++;
|
|
611
|
-
usage.input +=
|
|
612
|
-
usage.output +=
|
|
613
|
-
usage.reasoning +=
|
|
614
|
-
usage.cacheRead +=
|
|
615
|
-
usage.cacheWrite +=
|
|
616
|
-
usage.total +=
|
|
617
|
-
usage.cost = (usage.cost ?? 0) +
|
|
884
|
+
usage.input += num4(t.input);
|
|
885
|
+
usage.output += num4(t.output);
|
|
886
|
+
usage.reasoning += num4(t.reasoning);
|
|
887
|
+
usage.cacheRead += num4(t.cache?.read);
|
|
888
|
+
usage.cacheWrite += num4(t.cache?.write);
|
|
889
|
+
usage.total += num4(t.total) || num4(t.input) + num4(t.output) + num4(t.reasoning) + num4(t.cache?.read);
|
|
890
|
+
usage.cost = (usage.cost ?? 0) + num4(p.cost);
|
|
618
891
|
break;
|
|
619
892
|
}
|
|
620
893
|
case "error": {
|
|
@@ -638,25 +911,25 @@ function describeError(err) {
|
|
|
638
911
|
const tags = [err.type, inner, err.status && `HTTP ${err.status}`].filter(Boolean);
|
|
639
912
|
return tags.length ? `${message} [${tags.join(", ")}]` : message;
|
|
640
913
|
}
|
|
641
|
-
var
|
|
914
|
+
var clip4 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
642
915
|
var stamp = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
643
|
-
function
|
|
916
|
+
function num4(v) {
|
|
644
917
|
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
645
918
|
}
|
|
646
|
-
function
|
|
919
|
+
function oneLine4(s, max = 80) {
|
|
647
920
|
const t = s.replace(/\s+/g, " ").trim();
|
|
648
921
|
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
649
922
|
}
|
|
650
923
|
function describe2(input) {
|
|
651
924
|
if (!input || typeof input !== "object") return "";
|
|
652
925
|
const v = input.filePath ?? input.path ?? input.pattern ?? input.command ?? input.url ?? input.query;
|
|
653
|
-
return v ?
|
|
926
|
+
return v ? oneLine4(String(v), 60) : "";
|
|
654
927
|
}
|
|
655
928
|
|
|
656
929
|
// src/backends/opencode/profiles.ts
|
|
657
|
-
import
|
|
658
|
-
import
|
|
659
|
-
import
|
|
930
|
+
import fs5 from "node:fs";
|
|
931
|
+
import os6 from "node:os";
|
|
932
|
+
import path6 from "node:path";
|
|
660
933
|
var AGENT = { read: "pitroom-read", write: "pitroom-write" };
|
|
661
934
|
var READ_FILES = {
|
|
662
935
|
"*": "allow",
|
|
@@ -754,18 +1027,18 @@ var FORBIDDEN_BASH = [
|
|
|
754
1027
|
"*pitroom*"
|
|
755
1028
|
];
|
|
756
1029
|
function scratchDirs() {
|
|
757
|
-
const data = process.env.XDG_DATA_HOME ??
|
|
1030
|
+
const data = process.env.XDG_DATA_HOME ?? path6.join(os6.homedir(), ".local", "share");
|
|
758
1031
|
const rules = {
|
|
759
1032
|
"*": "deny",
|
|
760
|
-
[
|
|
761
|
-
[
|
|
1033
|
+
[path6.join(data, "opencode", "tool-output", "*")]: "allow",
|
|
1034
|
+
[path6.join(data, "opencode", "shell", "*", "*")]: "allow"
|
|
762
1035
|
};
|
|
763
|
-
for (const tmp of /* @__PURE__ */ new Set([
|
|
1036
|
+
for (const tmp of /* @__PURE__ */ new Set([os6.tmpdir(), realpath(os6.tmpdir())])) rules[path6.join(tmp, "opencode", "*")] = "allow";
|
|
764
1037
|
return rules;
|
|
765
1038
|
}
|
|
766
1039
|
function realpath(p) {
|
|
767
1040
|
try {
|
|
768
|
-
return
|
|
1041
|
+
return fs5.realpathSync(p);
|
|
769
1042
|
} catch {
|
|
770
1043
|
return p;
|
|
771
1044
|
}
|
|
@@ -853,10 +1126,10 @@ function isObject(v) {
|
|
|
853
1126
|
}
|
|
854
1127
|
|
|
855
1128
|
// src/backends/opencode/index.ts
|
|
856
|
-
var
|
|
1129
|
+
var binary4 = () => findBinary("opencode", "PITROOM_OPENCODE_BIN", ["~/.opencode/bin/opencode"]);
|
|
857
1130
|
function oc(args, opts = {}) {
|
|
858
|
-
const { command, prefix } = resolveCommand(
|
|
859
|
-
const r =
|
|
1131
|
+
const { command, prefix } = resolveCommand(binary4());
|
|
1132
|
+
const r = spawnSync4(command, [...prefix, ...args], {
|
|
860
1133
|
encoding: "utf8",
|
|
861
1134
|
timeout: opts.timeout ?? 6e4,
|
|
862
1135
|
env: opts.env ?? process.env,
|
|
@@ -866,7 +1139,7 @@ function oc(args, opts = {}) {
|
|
|
866
1139
|
return { ok: r.status === 0, out: r.stdout ?? "", err: r.stderr ?? "", missing: !!r.error };
|
|
867
1140
|
}
|
|
868
1141
|
var jsonIn = (s) => JSON.parse(s.slice(s.indexOf("{")));
|
|
869
|
-
function
|
|
1142
|
+
function invocation4(req) {
|
|
870
1143
|
const args = [
|
|
871
1144
|
"run",
|
|
872
1145
|
"--standalone",
|
|
@@ -885,7 +1158,7 @@ function invocation3(req) {
|
|
|
885
1158
|
if (req.sessionId) args.push("--session", req.sessionId);
|
|
886
1159
|
for (const f of req.files) args.push("--file", f);
|
|
887
1160
|
args.push(req.prompt);
|
|
888
|
-
const { command, prefix } = resolveCommand(
|
|
1161
|
+
const { command, prefix } = resolveCommand(binary4());
|
|
889
1162
|
return {
|
|
890
1163
|
command,
|
|
891
1164
|
args: [...prefix, ...args],
|
|
@@ -900,7 +1173,7 @@ function majorVersion(output) {
|
|
|
900
1173
|
const m = /(\d+)\.\d+\.\d+/.exec(output);
|
|
901
1174
|
return m ? Number(m[1]) : void 0;
|
|
902
1175
|
}
|
|
903
|
-
function
|
|
1176
|
+
function classify4(message) {
|
|
904
1177
|
if (/model not found|ModelNotFound|unknown model|no such model|deprecated/i.test(message)) return "model-unavailable";
|
|
905
1178
|
if (/rate.?limit|too many requests|\b429\b|quota|overloaded|\b50[23]\b|unavailable|capacity/i.test(message)) return "rate-limited";
|
|
906
1179
|
if (/provider\.auth|FreeTierError|free tier|unauthori[sz]ed|\b40[13]\b|api.?key|insufficient|credits?\b|billing|payment/i.test(message)) {
|
|
@@ -908,19 +1181,19 @@ function classify3(message) {
|
|
|
908
1181
|
}
|
|
909
1182
|
return "other";
|
|
910
1183
|
}
|
|
911
|
-
function
|
|
1184
|
+
function failure4(run, stderr, exitCode) {
|
|
912
1185
|
if (exitCode === 0 && !run.error) return void 0;
|
|
913
1186
|
const detail = stderr.match(/error="([^"]+)"/g)?.pop()?.slice(7, -1);
|
|
914
1187
|
const raw = run.error ?? `opencode exited with code ${exitCode}`;
|
|
915
1188
|
const message = detail && !raw.includes(detail) ? `${raw}: ${detail}` : raw;
|
|
916
|
-
return { kind:
|
|
1189
|
+
return { kind: classify4(message), message };
|
|
917
1190
|
}
|
|
918
1191
|
function modelId(m) {
|
|
919
1192
|
if (typeof m === "string") return m;
|
|
920
1193
|
const id = m?.model ?? m?.id;
|
|
921
1194
|
return m?.providerID && id ? `${m.providerID}/${id}` : void 0;
|
|
922
1195
|
}
|
|
923
|
-
function
|
|
1196
|
+
function defaultModel3() {
|
|
924
1197
|
try {
|
|
925
1198
|
const out = oc(["debug", "config"], { timeout: 3e4 }).out;
|
|
926
1199
|
const json = JSON.parse(out.slice(out.search(/[[{]/)));
|
|
@@ -945,7 +1218,7 @@ function resolveModel2(sessionId) {
|
|
|
945
1218
|
return void 0;
|
|
946
1219
|
}
|
|
947
1220
|
}
|
|
948
|
-
function
|
|
1221
|
+
function catalog4() {
|
|
949
1222
|
const ids = listModels();
|
|
950
1223
|
return {
|
|
951
1224
|
models: (ids.length ? ids : listModels()).map((id) => ({ id })),
|
|
@@ -955,20 +1228,20 @@ function catalog3() {
|
|
|
955
1228
|
function listModels() {
|
|
956
1229
|
return oc(["models"]).out.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
957
1230
|
}
|
|
958
|
-
function
|
|
1231
|
+
function doctor4({ models, hasFallback }) {
|
|
959
1232
|
const checks = [];
|
|
960
1233
|
const version = oc(["--version"]);
|
|
961
1234
|
if (version.missing || !version.ok) {
|
|
962
|
-
return [{ level: "fail", message: `opencode not runnable (${
|
|
1235
|
+
return [{ level: "fail", message: `opencode not runnable (${binary4()}). Install: https://opencode.ai or set PITROOM_OPENCODE_BIN` }];
|
|
963
1236
|
}
|
|
964
1237
|
const v = version.out.trim().replace(/^opencode\s+/i, "");
|
|
965
1238
|
const major = majorVersion(v);
|
|
966
1239
|
if (major !== void 0 && major < 2) {
|
|
967
1240
|
return [{ level: "fail", message: `OpenCode ${v} is too old: Pitroom needs OpenCode v2 or newer (run \`opencode upgrade\`)` }];
|
|
968
1241
|
}
|
|
969
|
-
checks.push({ level: "ok", message: `opencode ${v} at ${
|
|
1242
|
+
checks.push({ level: "ok", message: `opencode ${v} at ${binary4()} (private --standalone server per run)` });
|
|
970
1243
|
const known = new Set(listModels());
|
|
971
|
-
const def =
|
|
1244
|
+
const def = defaultModel3();
|
|
972
1245
|
for (const m of models) {
|
|
973
1246
|
const model = m ?? def;
|
|
974
1247
|
const label = m ? "model" : "default model";
|
|
@@ -994,21 +1267,21 @@ var opencode = {
|
|
|
994
1267
|
id: "opencode",
|
|
995
1268
|
name: "OpenCode",
|
|
996
1269
|
capabilities: { readOnly: "permission-rules", resume: "by-id", reportsCost: true, attachFiles: true },
|
|
997
|
-
binary:
|
|
998
|
-
invocation:
|
|
999
|
-
parse:
|
|
1000
|
-
failure:
|
|
1001
|
-
defaultModel:
|
|
1270
|
+
binary: binary4,
|
|
1271
|
+
invocation: invocation4,
|
|
1272
|
+
parse: parseEvents4,
|
|
1273
|
+
failure: failure4,
|
|
1274
|
+
defaultModel: defaultModel3,
|
|
1002
1275
|
resolveModel: resolveModel2,
|
|
1003
1276
|
listModels,
|
|
1004
|
-
catalog:
|
|
1005
|
-
doctor:
|
|
1277
|
+
catalog: catalog4,
|
|
1278
|
+
doctor: doctor4
|
|
1006
1279
|
};
|
|
1007
1280
|
|
|
1008
1281
|
// src/backends/index.ts
|
|
1009
|
-
var REGISTRY = new Map([opencode, codex, claude].map((b) => [b.id, b]));
|
|
1282
|
+
var REGISTRY = new Map([opencode, codex, claude, gemini].map((b) => [b.id, b]));
|
|
1010
1283
|
var DEFAULT_BACKEND = opencode.id;
|
|
1011
|
-
var PLANNED = [
|
|
1284
|
+
var PLANNED = [];
|
|
1012
1285
|
var backendIds = () => [...REGISTRY.keys()];
|
|
1013
1286
|
var allBackends = () => [...REGISTRY.values()];
|
|
1014
1287
|
var isBackendId = (id) => REGISTRY.has(id) || PLANNED.includes(id);
|
|
@@ -1038,8 +1311,8 @@ var formatTarget = (t) => t.model ? `${t.backend}:${t.model}` : t.backend;
|
|
|
1038
1311
|
var describeTarget = (t) => t.model ? formatTarget(t) : `${t.backend} (default model)`;
|
|
1039
1312
|
|
|
1040
1313
|
// src/core/refs.ts
|
|
1041
|
-
import
|
|
1042
|
-
import
|
|
1314
|
+
import fs6 from "node:fs";
|
|
1315
|
+
import path7 from "node:path";
|
|
1043
1316
|
var FILE_EXTENSIONS = new Set(
|
|
1044
1317
|
"ts tsx mts cts js jsx mjs cjs json jsonc json5 md mdx txt rst py pyi rb go rs java kt kts swift c h cc cpp cxx hpp hh cs fs php lua r jl sh bash zsh fish ps1 bat yml yaml toml ini cfg conf env html htm css scss sass less vue svelte astro sql graphql gql proto lock xml svg csv tsv gradle tf hcl dart ex exs erl hs ml scala clj vim el mk cmake dockerfile gitignore gitattributes editorconfig npmrc nvmrc".split(" ")
|
|
1045
1318
|
);
|
|
@@ -1090,13 +1363,13 @@ function extractRefs(answer) {
|
|
|
1090
1363
|
return [...seen.values()];
|
|
1091
1364
|
}
|
|
1092
1365
|
function verifyRefs(refs, dirs) {
|
|
1093
|
-
const roots = [...new Set(dirs.filter(Boolean).map((d) =>
|
|
1366
|
+
const roots = [...new Set(dirs.filter(Boolean).map((d) => path7.resolve(d)))];
|
|
1094
1367
|
const invalid = [];
|
|
1095
1368
|
const lineCache = /* @__PURE__ */ new Map();
|
|
1096
1369
|
const load = (file) => {
|
|
1097
1370
|
if (!lineCache.has(file)) {
|
|
1098
|
-
const size =
|
|
1099
|
-
const lines = size > MAX_BYTES ? null :
|
|
1371
|
+
const size = fs6.statSync(file).size;
|
|
1372
|
+
const lines = size > MAX_BYTES ? null : fs6.readFileSync(file, "utf8").split("\n");
|
|
1100
1373
|
if (lines && lines[lines.length - 1] === "") lines.pop();
|
|
1101
1374
|
lineCache.set(file, lines);
|
|
1102
1375
|
}
|
|
@@ -1107,9 +1380,9 @@ function verifyRefs(refs, dirs) {
|
|
|
1107
1380
|
const candidates = (ref) => {
|
|
1108
1381
|
const direct = resolve(ref.file, roots);
|
|
1109
1382
|
if (direct) return [direct];
|
|
1110
|
-
if (
|
|
1383
|
+
if (path7.isAbsolute(ref.file)) return [];
|
|
1111
1384
|
const wanted = ref.file.replace(/^(\.{1,2}\/)+/, "");
|
|
1112
|
-
return (byName().get(
|
|
1385
|
+
return (byName().get(path7.basename(wanted)) ?? []).filter((f) => f.endsWith(`/${wanted}`) || path7.basename(f) === wanted).slice(0, 20);
|
|
1113
1386
|
};
|
|
1114
1387
|
const check = (file, ref) => {
|
|
1115
1388
|
const lines = load(file);
|
|
@@ -1124,7 +1397,7 @@ function verifyRefs(refs, dirs) {
|
|
|
1124
1397
|
if (!files.length) {
|
|
1125
1398
|
if (!ref.file.includes("/") && !isFileName(ref.file)) continue;
|
|
1126
1399
|
total++;
|
|
1127
|
-
invalid.push({ ref: ref.text, reason:
|
|
1400
|
+
invalid.push({ ref: ref.text, reason: path7.isAbsolute(ref.file) && !inside(ref.file, roots) ? "outside the project" : "file not found" });
|
|
1128
1401
|
continue;
|
|
1129
1402
|
}
|
|
1130
1403
|
total++;
|
|
@@ -1145,7 +1418,7 @@ function enclosedBy(lines, ref) {
|
|
|
1145
1418
|
return lines.slice(Math.max(0, ref.start - 1 - DEFINITION_LOOKBACK), ref.start).some((l) => def.test(l));
|
|
1146
1419
|
}
|
|
1147
1420
|
var isFileName = (name) => {
|
|
1148
|
-
const base =
|
|
1421
|
+
const base = path7.basename(name);
|
|
1149
1422
|
return !base.includes(".") || new RegExp(`^(${EXTENSIONLESS})$`).test(base) || FILE_EXTENSIONS.has(base.split(".").pop().toLowerCase());
|
|
1150
1423
|
};
|
|
1151
1424
|
function indexFiles(roots) {
|
|
@@ -1154,19 +1427,19 @@ function indexFiles(roots) {
|
|
|
1154
1427
|
const walk = (dir) => {
|
|
1155
1428
|
let entries;
|
|
1156
1429
|
try {
|
|
1157
|
-
entries =
|
|
1430
|
+
entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
1158
1431
|
} catch {
|
|
1159
1432
|
return;
|
|
1160
1433
|
}
|
|
1161
1434
|
for (const e of entries) {
|
|
1162
1435
|
if (count >= MAX_INDEXED) return;
|
|
1163
1436
|
if (e.isDirectory()) {
|
|
1164
|
-
if (!SKIP_DIRS.has(e.name)) walk(
|
|
1437
|
+
if (!SKIP_DIRS.has(e.name)) walk(path7.join(dir, e.name));
|
|
1165
1438
|
} else if (e.isFile()) {
|
|
1166
1439
|
count++;
|
|
1167
1440
|
const list = index.get(e.name);
|
|
1168
|
-
if (list) list.push(
|
|
1169
|
-
else index.set(e.name, [
|
|
1441
|
+
if (list) list.push(path7.join(dir, e.name));
|
|
1442
|
+
else index.set(e.name, [path7.join(dir, e.name)]);
|
|
1170
1443
|
}
|
|
1171
1444
|
}
|
|
1172
1445
|
};
|
|
@@ -1174,10 +1447,10 @@ function indexFiles(roots) {
|
|
|
1174
1447
|
return index;
|
|
1175
1448
|
}
|
|
1176
1449
|
function resolve(ref, roots) {
|
|
1177
|
-
const candidates =
|
|
1450
|
+
const candidates = path7.isAbsolute(ref) ? inside(ref, roots) ? [ref] : [] : roots.map((r) => path7.join(r, ref));
|
|
1178
1451
|
return candidates.find((c) => {
|
|
1179
1452
|
try {
|
|
1180
|
-
return
|
|
1453
|
+
return fs6.statSync(c).isFile() && inside(c, roots);
|
|
1181
1454
|
} catch {
|
|
1182
1455
|
return false;
|
|
1183
1456
|
}
|
|
@@ -1185,21 +1458,21 @@ function resolve(ref, roots) {
|
|
|
1185
1458
|
}
|
|
1186
1459
|
function inside(file, roots) {
|
|
1187
1460
|
return roots.some((r) => {
|
|
1188
|
-
const rel =
|
|
1189
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
1461
|
+
const rel = path7.relative(r, path7.resolve(file));
|
|
1462
|
+
return rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
|
|
1190
1463
|
});
|
|
1191
1464
|
}
|
|
1192
1465
|
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1193
1466
|
|
|
1194
1467
|
// src/core/templates.ts
|
|
1195
|
-
import
|
|
1196
|
-
import
|
|
1468
|
+
import fs7 from "node:fs";
|
|
1469
|
+
import path9 from "node:path";
|
|
1197
1470
|
|
|
1198
1471
|
// src/core/install.ts
|
|
1199
|
-
import
|
|
1200
|
-
import { fileURLToPath } from "node:url";
|
|
1472
|
+
import path8 from "node:path";
|
|
1473
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1201
1474
|
function packageRoot() {
|
|
1202
|
-
return
|
|
1475
|
+
return path8.resolve(path8.dirname(fileURLToPath2(import.meta.url)), "..");
|
|
1203
1476
|
}
|
|
1204
1477
|
|
|
1205
1478
|
// src/core/templates.ts
|
|
@@ -1210,9 +1483,9 @@ var FILES = {
|
|
|
1210
1483
|
"code-reviewer": "pitroom-review/code-reviewer.md"
|
|
1211
1484
|
};
|
|
1212
1485
|
function loadTemplate(name, root = packageRoot()) {
|
|
1213
|
-
const file =
|
|
1214
|
-
if (!
|
|
1215
|
-
return
|
|
1486
|
+
const file = path9.join(root, "skills", FILES[name]);
|
|
1487
|
+
if (!fs7.existsSync(file)) throw new UserError(`template missing: ${file} (broken install? run pitroom doctor)`, 3);
|
|
1488
|
+
return fs7.readFileSync(file, "utf8").replace(/^\s*<!--[\s\S]*?-->\s*/, "");
|
|
1216
1489
|
}
|
|
1217
1490
|
function fill(template, values) {
|
|
1218
1491
|
const missing = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map((m) => m[1]).filter((k) => !(k in values));
|
|
@@ -1239,10 +1512,17 @@ function parseVerdict(text) {
|
|
|
1239
1512
|
minor: count("minor")
|
|
1240
1513
|
};
|
|
1241
1514
|
}
|
|
1515
|
+
function parseAudit(text) {
|
|
1516
|
+
const word = /^\s*AUDIT:\s*(AGREE|PARTIAL|DISAGREE)\b/im.exec(text)?.[1]?.toLowerCase();
|
|
1517
|
+
const verdict = word === "agree" || word === "partial" || word === "disagree" ? word : "unclear";
|
|
1518
|
+
const after = /^\s*DISPUTED:\s*$/im.exec(text);
|
|
1519
|
+
const disputed = after ? text.slice(after.index + after[0].length).split("\n").map((l) => l.trim()).filter((l) => /^[-*•]\s+\S/.test(l)).map((l) => l.replace(/^[-*•]\s+/, "")).filter((l) => !/^\(?none\)?\.?$/i.test(l)).slice(0, 8) : [];
|
|
1520
|
+
return { verdict, disputed };
|
|
1521
|
+
}
|
|
1242
1522
|
|
|
1243
1523
|
// src/core/plan.ts
|
|
1244
|
-
import
|
|
1245
|
-
import
|
|
1524
|
+
import fs8 from "node:fs";
|
|
1525
|
+
import path10 from "node:path";
|
|
1246
1526
|
var FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
1247
1527
|
var RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
|
|
1248
1528
|
function structure(lines) {
|
|
@@ -1289,9 +1569,9 @@ function parsePlan(text, file = "") {
|
|
|
1289
1569
|
return { file, title: titleHeading?.text ?? "", header, constraints, tasks };
|
|
1290
1570
|
}
|
|
1291
1571
|
function loadPlan(file) {
|
|
1292
|
-
const abs =
|
|
1293
|
-
if (!
|
|
1294
|
-
const plan = parsePlan(
|
|
1572
|
+
const abs = path10.resolve(file);
|
|
1573
|
+
if (!fs8.existsSync(abs) || !fs8.statSync(abs).isFile()) throw new UserError(`plan not found: ${file}`);
|
|
1574
|
+
const plan = parsePlan(fs8.readFileSync(abs, "utf8"), fs8.realpathSync(abs));
|
|
1295
1575
|
if (!plan.tasks.length) throw new UserError(`${file} has no "Task N" headings (see pitroom-writing-plans)`);
|
|
1296
1576
|
return plan;
|
|
1297
1577
|
}
|
|
@@ -1310,7 +1590,7 @@ function brief(plan, task) {
|
|
|
1310
1590
|
].filter(Boolean).join("\n\n")}
|
|
1311
1591
|
`;
|
|
1312
1592
|
}
|
|
1313
|
-
var planName = (file) =>
|
|
1593
|
+
var planName = (file) => path10.basename(file).replace(/\.md$/i, "");
|
|
1314
1594
|
export {
|
|
1315
1595
|
DEFAULT_BACKEND,
|
|
1316
1596
|
allBackends,
|
|
@@ -1323,6 +1603,7 @@ export {
|
|
|
1323
1603
|
getBackend,
|
|
1324
1604
|
loadPlan,
|
|
1325
1605
|
loadTemplate,
|
|
1606
|
+
parseAudit,
|
|
1326
1607
|
parsePlan,
|
|
1327
1608
|
parseStatus,
|
|
1328
1609
|
parseTarget,
|