@sechroom/cli 2026.7.31 → 2026.7.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1527 -145
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/auth.ts
|
|
@@ -104,8 +104,8 @@ function setDefaultAccount(alias) {
|
|
|
104
104
|
file.default = alias;
|
|
105
105
|
writeAccountsFile(file);
|
|
106
106
|
}
|
|
107
|
-
function resolveAccountAlias(flagAccount) {
|
|
108
|
-
return flagAccount ?? process.env.SECHROOM_ACCOUNT ?? readLocalConfig().account ?? readAccountsFile().default ?? DEFAULT_ACCOUNT;
|
|
107
|
+
function resolveAccountAlias(flagAccount, start = process.cwd()) {
|
|
108
|
+
return flagAccount ?? process.env.SECHROOM_ACCOUNT ?? readLocalConfig(start).account ?? readAccountsFile().default ?? DEFAULT_ACCOUNT;
|
|
109
109
|
}
|
|
110
110
|
function writeLocalAccount(alias) {
|
|
111
111
|
const home = findConfigHome() ?? process.cwd();
|
|
@@ -149,8 +149,8 @@ function findConfigHome(start = process.cwd()) {
|
|
|
149
149
|
dir = parent;
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
-
function readLocalConfig() {
|
|
153
|
-
const home = findConfigHome();
|
|
152
|
+
function readLocalConfig(start = process.cwd()) {
|
|
153
|
+
const home = findConfigHome(start);
|
|
154
154
|
if (!home) return {};
|
|
155
155
|
const baselinePath = join(home, BASELINE_CONFIG_NAME);
|
|
156
156
|
const overridePath = join(home, OVERRIDE_CONFIG_NAME);
|
|
@@ -186,7 +186,7 @@ function committedBindingPath(dir) {
|
|
|
186
186
|
const p = join(dir, BASELINE_CONFIG_NAME);
|
|
187
187
|
return existsSync(p) ? p : void 0;
|
|
188
188
|
}
|
|
189
|
-
function selectWorkspaceBinding(local, explicitName) {
|
|
189
|
+
function selectWorkspaceBinding(local, explicitName, start = process.cwd()) {
|
|
190
190
|
const bindings = local.workspaces ?? [];
|
|
191
191
|
if (explicitName) {
|
|
192
192
|
const hit = bindings.find((b) => b.name === explicitName);
|
|
@@ -197,7 +197,7 @@ function selectWorkspaceBinding(local, explicitName) {
|
|
|
197
197
|
return hit;
|
|
198
198
|
}
|
|
199
199
|
if (!local.home || bindings.length === 0) return void 0;
|
|
200
|
-
const rel =
|
|
200
|
+
const rel = start.startsWith(local.home) ? start.slice(local.home.length).replace(/^[/\\]/, "") : "";
|
|
201
201
|
const norm = (p) => p.replace(/\/\*\*$/, "").replace(/[/\\]+$/, "");
|
|
202
202
|
let best;
|
|
203
203
|
for (const b of bindings) {
|
|
@@ -210,8 +210,8 @@ function selectWorkspaceBinding(local, explicitName) {
|
|
|
210
210
|
}
|
|
211
211
|
return best?.binding;
|
|
212
212
|
}
|
|
213
|
-
function resolveConfig(flags) {
|
|
214
|
-
const local = readLocalConfig();
|
|
213
|
+
function resolveConfig(flags, start = process.cwd()) {
|
|
214
|
+
const local = readLocalConfig(start);
|
|
215
215
|
const persisted = readPersisted();
|
|
216
216
|
const baseUrl = flags.baseUrl ?? process.env.SECHROOM_BASE_URL ?? local.baseUrl ?? persisted.baseUrl ?? DEFAULT_BASE_URL;
|
|
217
217
|
const tenant = flags.tenant ?? process.env.SECHROOM_TENANT ?? local.tenant ?? persisted.tenant ?? "";
|
|
@@ -220,10 +220,10 @@ function resolveConfig(flags) {
|
|
|
220
220
|
"No tenant set. The Sechroom API rejects untenanted requests (HTTP 400). Pass --tenant <id>, set SECHROOM_TENANT, run `sechroom config set tenant <id>`, or `sechroom config set --local tenant <id>` for this directory."
|
|
221
221
|
);
|
|
222
222
|
}
|
|
223
|
-
const binding = selectWorkspaceBinding(local, flags.binding ?? process.env.SECHROOM_BINDING);
|
|
223
|
+
const binding = selectWorkspaceBinding(local, flags.binding ?? process.env.SECHROOM_BINDING, start);
|
|
224
224
|
const workspaceId = process.env.SECHROOM_WORKSPACE ?? binding?.workspaceId ?? local.workspaceId ?? persisted.workspaceId ?? void 0;
|
|
225
225
|
const defaultProjectId = (binding ? binding.defaultProjectId : void 0) ?? local.defaultProjectId ?? persisted.defaultProjectId ?? void 0;
|
|
226
|
-
const account = resolveAccountAlias(flags.account);
|
|
226
|
+
const account = resolveAccountAlias(flags.account, start);
|
|
227
227
|
return { baseUrl: baseUrl.replace(/\/$/, ""), tenant, account, workspaceId, defaultProjectId, clientId: persisted.clientId };
|
|
228
228
|
}
|
|
229
229
|
function describeConfig(flags) {
|
|
@@ -2141,7 +2141,7 @@ var SUCCESS = /* @__PURE__ */ new Set(["Claimed", "AlreadyHeld"]);
|
|
|
2141
2141
|
var CONTENTION_STATUSES = /* @__PURE__ */ new Set([409, 410]);
|
|
2142
2142
|
async function claimNextTask(deps) {
|
|
2143
2143
|
const { request, executorInstanceId } = deps;
|
|
2144
|
-
const sleep = deps.sleep ?? ((ms) => new Promise((
|
|
2144
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
|
|
2145
2145
|
const idempotencyKey = deps.idempotencyKey ?? ((offer) => `executor-run:${offer.generationId}`);
|
|
2146
2146
|
const tokenVersion = deps.tokenVersion ?? 1;
|
|
2147
2147
|
const log = deps.log ?? (() => {
|
|
@@ -2471,12 +2471,12 @@ var CodexAppServer = class {
|
|
|
2471
2471
|
))
|
|
2472
2472
|
fileChangeCount += 1;
|
|
2473
2473
|
};
|
|
2474
|
-
const terminal = new Promise((
|
|
2474
|
+
const terminal = new Promise((resolve8) => {
|
|
2475
2475
|
this.onServerRequest = (msg) => {
|
|
2476
2476
|
const method = msg.method ?? "";
|
|
2477
2477
|
if (terminalMethod(method)) {
|
|
2478
2478
|
this.respond(msg.id, {});
|
|
2479
|
-
|
|
2479
|
+
resolve8("completed");
|
|
2480
2480
|
return;
|
|
2481
2481
|
}
|
|
2482
2482
|
if (method === "item/tool/call") {
|
|
@@ -2663,14 +2663,14 @@ var CodexAppServer = class {
|
|
|
2663
2663
|
if (!child || this.exited)
|
|
2664
2664
|
return Promise.reject(new Error("codex app-server is not running"));
|
|
2665
2665
|
const id = this.nextId++;
|
|
2666
|
-
return new Promise((
|
|
2666
|
+
return new Promise((resolve8, reject) => {
|
|
2667
2667
|
let timer;
|
|
2668
2668
|
const clearTimer = () => {
|
|
2669
2669
|
if (timer) clearTimeout(timer);
|
|
2670
2670
|
};
|
|
2671
2671
|
const resolvePending = (value) => {
|
|
2672
2672
|
clearTimer();
|
|
2673
|
-
|
|
2673
|
+
resolve8(value);
|
|
2674
2674
|
};
|
|
2675
2675
|
const rejectPending = (error) => {
|
|
2676
2676
|
clearTimer();
|
|
@@ -2736,10 +2736,10 @@ var CodexAppServer = class {
|
|
|
2736
2736
|
if (parsed) this.options.onRateLimits?.(parsed);
|
|
2737
2737
|
}
|
|
2738
2738
|
exitAsResult() {
|
|
2739
|
-
return new Promise((
|
|
2739
|
+
return new Promise((resolve8) => {
|
|
2740
2740
|
this.child?.once(
|
|
2741
2741
|
"exit",
|
|
2742
|
-
(code) =>
|
|
2742
|
+
(code) => resolve8(`app-server exited (${code ?? "signal"})`)
|
|
2743
2743
|
);
|
|
2744
2744
|
});
|
|
2745
2745
|
}
|
|
@@ -2765,7 +2765,7 @@ function terminalMethod(method) {
|
|
|
2765
2765
|
}
|
|
2766
2766
|
function timeout(ms) {
|
|
2767
2767
|
return new Promise(
|
|
2768
|
-
(
|
|
2768
|
+
(resolve8) => setTimeout(() => resolve8("timeout"), ms).unref?.()
|
|
2769
2769
|
);
|
|
2770
2770
|
}
|
|
2771
2771
|
function dynamicToolDefinitions() {
|
|
@@ -2805,12 +2805,12 @@ function dynamicToolDefinitions() {
|
|
|
2805
2805
|
// src/executor-run/delivery.ts
|
|
2806
2806
|
import { execFile as execFile2 } from "child_process";
|
|
2807
2807
|
function createGitRunner(rootDir) {
|
|
2808
|
-
return (bin, args) => new Promise((
|
|
2808
|
+
return (bin, args) => new Promise((resolve8) => {
|
|
2809
2809
|
execFile2(
|
|
2810
2810
|
bin,
|
|
2811
2811
|
bin === "git" ? ["-C", rootDir, ...args] : args,
|
|
2812
2812
|
{ cwd: rootDir, maxBuffer: 10 * 1024 * 1024 },
|
|
2813
|
-
(error, stdout, stderr) =>
|
|
2813
|
+
(error, stdout, stderr) => resolve8({
|
|
2814
2814
|
ok: !error,
|
|
2815
2815
|
stdout: String(stdout),
|
|
2816
2816
|
stderr: String(stderr)
|
|
@@ -5231,8 +5231,8 @@ function registerExecutorRunCommand(executor) {
|
|
|
5231
5231
|
await appServer.start();
|
|
5232
5232
|
log(`codex app-server up (${String(opts.codexBin)})`);
|
|
5233
5233
|
let signalCapacityTerminal;
|
|
5234
|
-
const capacityTerminal = new Promise((
|
|
5235
|
-
signalCapacityTerminal =
|
|
5234
|
+
const capacityTerminal = new Promise((resolve8) => {
|
|
5235
|
+
signalCapacityTerminal = resolve8;
|
|
5236
5236
|
});
|
|
5237
5237
|
let stopCapacityCapture = () => {
|
|
5238
5238
|
};
|
|
@@ -5252,8 +5252,8 @@ function registerExecutorRunCommand(executor) {
|
|
|
5252
5252
|
process.once("SIGTERM", requestStop);
|
|
5253
5253
|
let wake = () => {
|
|
5254
5254
|
};
|
|
5255
|
-
const wakeSignal = () => new Promise((
|
|
5256
|
-
wake =
|
|
5255
|
+
const wakeSignal = () => new Promise((resolve8) => {
|
|
5256
|
+
wake = resolve8;
|
|
5257
5257
|
});
|
|
5258
5258
|
let connStop = async () => {
|
|
5259
5259
|
};
|
|
@@ -5372,8 +5372,8 @@ function registerExecutorRunCommand(executor) {
|
|
|
5372
5372
|
),
|
|
5373
5373
|
log,
|
|
5374
5374
|
waitForWake: (ms) => Promise.race([
|
|
5375
|
-
new Promise((
|
|
5376
|
-
setTimeout(
|
|
5375
|
+
new Promise((resolve8) => {
|
|
5376
|
+
setTimeout(resolve8, ms).unref?.();
|
|
5377
5377
|
}),
|
|
5378
5378
|
wakeSignal()
|
|
5379
5379
|
])
|
|
@@ -6185,11 +6185,11 @@ function parseInteger(value) {
|
|
|
6185
6185
|
return parsed;
|
|
6186
6186
|
}
|
|
6187
6187
|
function holdHeartbeat(tick, intervalMs) {
|
|
6188
|
-
return new Promise((
|
|
6188
|
+
return new Promise((resolve8, reject) => {
|
|
6189
6189
|
const timer = setInterval(() => void tick().catch(reject), intervalMs);
|
|
6190
6190
|
const stop = () => {
|
|
6191
6191
|
clearInterval(timer);
|
|
6192
|
-
|
|
6192
|
+
resolve8();
|
|
6193
6193
|
};
|
|
6194
6194
|
process.once("SIGINT", stop);
|
|
6195
6195
|
process.once("SIGTERM", stop);
|
|
@@ -6442,7 +6442,7 @@ function startExecutorHeartbeat(refresh, intervalMilliseconds, dependencies = {}
|
|
|
6442
6442
|
}
|
|
6443
6443
|
async function drainClaims(cfg, executorInstanceId, deliver, dependencies = {}) {
|
|
6444
6444
|
const request = dependencies.request ?? api;
|
|
6445
|
-
const sleep = dependencies.sleep ?? ((milliseconds) => new Promise((
|
|
6445
|
+
const sleep = dependencies.sleep ?? ((milliseconds) => new Promise((resolve8) => setTimeout(resolve8, milliseconds)));
|
|
6446
6446
|
const idempotencyKey = dependencies.idempotencyKey ?? ((offer) => `channel:${offer.generationId}`);
|
|
6447
6447
|
const state = dependencies.state ?? {};
|
|
6448
6448
|
for (; ; ) {
|
|
@@ -6516,9 +6516,9 @@ async function openConnection(cfg, onEvent, executorInstanceId) {
|
|
|
6516
6516
|
return conn;
|
|
6517
6517
|
}
|
|
6518
6518
|
function holdOpen(conn) {
|
|
6519
|
-
return new Promise((
|
|
6519
|
+
return new Promise((resolve8) => {
|
|
6520
6520
|
const stop = () => {
|
|
6521
|
-
void conn.stop().finally(
|
|
6521
|
+
void conn.stop().finally(resolve8);
|
|
6522
6522
|
};
|
|
6523
6523
|
process.on("SIGINT", stop);
|
|
6524
6524
|
process.on("SIGTERM", stop);
|
|
@@ -6644,13 +6644,224 @@ Examples:
|
|
|
6644
6644
|
}
|
|
6645
6645
|
|
|
6646
6646
|
// src/commands/checkpoint.ts
|
|
6647
|
-
import { mkdirSync as
|
|
6648
|
-
import { dirname as
|
|
6647
|
+
import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync13 } from "fs";
|
|
6648
|
+
import { dirname as dirname13, join as join15 } from "path";
|
|
6649
6649
|
|
|
6650
6650
|
// src/commands/hook.ts
|
|
6651
6651
|
import { createHash as createHash3 } from "crypto";
|
|
6652
|
-
import { existsSync as
|
|
6653
|
-
import { dirname as
|
|
6652
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync13, readFileSync as readFileSync10, statSync as statSync2, writeFileSync as writeFileSync12 } from "fs";
|
|
6653
|
+
import { dirname as dirname12, join as join14 } from "path";
|
|
6654
|
+
|
|
6655
|
+
// src/commands/lane-commit-hook.ts
|
|
6656
|
+
import { execFileSync } from "child_process";
|
|
6657
|
+
import {
|
|
6658
|
+
chmodSync,
|
|
6659
|
+
existsSync as existsSync10,
|
|
6660
|
+
mkdirSync as mkdirSync11,
|
|
6661
|
+
readFileSync as readFileSync9,
|
|
6662
|
+
renameSync,
|
|
6663
|
+
unlinkSync,
|
|
6664
|
+
writeFileSync as writeFileSync10
|
|
6665
|
+
} from "fs";
|
|
6666
|
+
import { basename as basename2, dirname as dirname10, isAbsolute, resolve as resolve4 } from "path";
|
|
6667
|
+
var BEGIN = "# sechroom:begin lane-trailer";
|
|
6668
|
+
var END = "# sechroom:end lane-trailer";
|
|
6669
|
+
var INCUMBENT_SUFFIX = ".sechroom-incumbent";
|
|
6670
|
+
var HOOK_NAME = "commit-msg";
|
|
6671
|
+
var LEGACY_HOOK_NAME = "prepare-commit-msg";
|
|
6672
|
+
var MANAGED_BLOCK = `${BEGIN}
|
|
6673
|
+
if command -v sechroom >/dev/null 2>&1; then
|
|
6674
|
+
sechroom hook commit-msg "$1"
|
|
6675
|
+
fi
|
|
6676
|
+
${END}`;
|
|
6677
|
+
function resolveCheckoutLane(start) {
|
|
6678
|
+
const pin = readSem(resolveSemPathForRead(start))?.values["code-lane"];
|
|
6679
|
+
return pin ? applyWorktreeLaneSuffix(pin, start) : void 0;
|
|
6680
|
+
}
|
|
6681
|
+
function appendLaneTrailer(messagePath, lane) {
|
|
6682
|
+
const original = readFileSync9(messagePath, "utf8").replace(/\r\n/g, "\n");
|
|
6683
|
+
const lines = original.split("\n");
|
|
6684
|
+
const scissorsIndex = lines.findIndex(
|
|
6685
|
+
(line) => /^#\s*-+\s*>8\s*-+\s*$/.test(line)
|
|
6686
|
+
);
|
|
6687
|
+
const editableLines = scissorsIndex < 0 ? lines : lines.slice(0, scissorsIndex);
|
|
6688
|
+
const scissorsLines = scissorsIndex < 0 ? [] : lines.slice(scissorsIndex);
|
|
6689
|
+
const editable = editableLines.join("\n").trimEnd();
|
|
6690
|
+
const finalParagraphStart = editable.lastIndexOf("\n\n") + 2;
|
|
6691
|
+
const body = editable.slice(0, finalParagraphStart);
|
|
6692
|
+
const finalParagraph = editable.slice(finalParagraphStart).split("\n").filter((line) => !/^Lane:\s.*$/.test(line)).join("\n");
|
|
6693
|
+
const withoutPriorLane = `${body}${finalParagraph}`.trimEnd();
|
|
6694
|
+
const hasMessage = withoutPriorLane.split("\n").some((line) => line.trim() !== "" && !line.trimStart().startsWith("#"));
|
|
6695
|
+
if (!hasMessage) return;
|
|
6696
|
+
const withTrailer = `${withoutPriorLane}
|
|
6697
|
+
|
|
6698
|
+
Lane: ${lane}
|
|
6699
|
+
`;
|
|
6700
|
+
const next = scissorsLines.length > 0 ? `${withTrailer}
|
|
6701
|
+
${scissorsLines.join("\n").replace(/^\n+/, "")}` : withTrailer;
|
|
6702
|
+
writeFileSync10(messagePath, next.endsWith("\n") ? next : `${next}
|
|
6703
|
+
`, "utf8");
|
|
6704
|
+
}
|
|
6705
|
+
function isShellHook(current) {
|
|
6706
|
+
const shebang = current.match(/^#!([^\n\r]*)/)?.[1];
|
|
6707
|
+
if (!shebang) return true;
|
|
6708
|
+
return /(?:^|[/\s])(?:sh|bash|dash|ksh|zsh)(?:\s|$)/.test(shebang);
|
|
6709
|
+
}
|
|
6710
|
+
function insertManagedBlock(current) {
|
|
6711
|
+
const blockPattern = managedBlockPattern();
|
|
6712
|
+
if (blockPattern.test(current)) return current.replace(blockPattern, MANAGED_BLOCK);
|
|
6713
|
+
const shebang = current.match(/^#![^\n]*(?:\n|$)/)?.[0];
|
|
6714
|
+
if (shebang) return `${shebang}${MANAGED_BLOCK}
|
|
6715
|
+
${current.slice(shebang.length)}`;
|
|
6716
|
+
return `#!/bin/sh
|
|
6717
|
+
${MANAGED_BLOCK}
|
|
6718
|
+
${current}`;
|
|
6719
|
+
}
|
|
6720
|
+
function managedBlockPattern() {
|
|
6721
|
+
return new RegExp(
|
|
6722
|
+
`${BEGIN.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`
|
|
6723
|
+
);
|
|
6724
|
+
}
|
|
6725
|
+
function chainWrapper(incumbentName) {
|
|
6726
|
+
return `#!/bin/sh
|
|
6727
|
+
${MANAGED_BLOCK}
|
|
6728
|
+
hook_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
6729
|
+
exec "$hook_dir/${incumbentName}" "$@"
|
|
6730
|
+
`;
|
|
6731
|
+
}
|
|
6732
|
+
function nextIncumbentPath(path) {
|
|
6733
|
+
const base = `${path}${INCUMBENT_SUFFIX}`;
|
|
6734
|
+
if (!existsSync10(base)) return base;
|
|
6735
|
+
for (let index = 2; ; index++) {
|
|
6736
|
+
const candidate = `${base}.${index}`;
|
|
6737
|
+
if (!existsSync10(candidate)) return candidate;
|
|
6738
|
+
}
|
|
6739
|
+
}
|
|
6740
|
+
function resolveHookPath(root, hookName) {
|
|
6741
|
+
const gitPath = execFileSync(
|
|
6742
|
+
"git",
|
|
6743
|
+
["-C", root, "rev-parse", "--git-path", `hooks/${hookName}`],
|
|
6744
|
+
{
|
|
6745
|
+
encoding: "utf8",
|
|
6746
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
6747
|
+
}
|
|
6748
|
+
).trim();
|
|
6749
|
+
return isAbsolute(gitPath) ? gitPath : resolve4(root, gitPath);
|
|
6750
|
+
}
|
|
6751
|
+
function removeLegacyPrepareCommitMsgLeg(root) {
|
|
6752
|
+
const path = resolveHookPath(root, LEGACY_HOOK_NAME);
|
|
6753
|
+
if (!existsSync10(path)) return;
|
|
6754
|
+
const current = readFileSync9(path, "utf8");
|
|
6755
|
+
const pattern = managedBlockPattern();
|
|
6756
|
+
if (!pattern.test(current)) return;
|
|
6757
|
+
const next = current.replace(pattern, "").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
6758
|
+
if (!next || next === "#!/bin/sh") {
|
|
6759
|
+
unlinkSync(path);
|
|
6760
|
+
return;
|
|
6761
|
+
}
|
|
6762
|
+
writeFileSync10(path, `${next}
|
|
6763
|
+
`, "utf8");
|
|
6764
|
+
chmodSync(path, 493);
|
|
6765
|
+
}
|
|
6766
|
+
function installLaneCommitHook(start) {
|
|
6767
|
+
try {
|
|
6768
|
+
const root = execFileSync("git", ["-C", start, "rev-parse", "--show-toplevel"], {
|
|
6769
|
+
encoding: "utf8",
|
|
6770
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
6771
|
+
}).trim();
|
|
6772
|
+
removeLegacyPrepareCommitMsgLeg(root);
|
|
6773
|
+
const path = resolveHookPath(root, HOOK_NAME);
|
|
6774
|
+
const current = existsSync10(path) ? readFileSync9(path, "utf8") : "";
|
|
6775
|
+
let next;
|
|
6776
|
+
if (current && !isShellHook(current)) {
|
|
6777
|
+
const incumbentPath = nextIncumbentPath(path);
|
|
6778
|
+
renameSync(path, incumbentPath);
|
|
6779
|
+
chmodSync(incumbentPath, 493);
|
|
6780
|
+
next = chainWrapper(basename2(incumbentPath));
|
|
6781
|
+
} else {
|
|
6782
|
+
next = insertManagedBlock(current);
|
|
6783
|
+
}
|
|
6784
|
+
if (next !== current) {
|
|
6785
|
+
mkdirSync11(dirname10(path), { recursive: true });
|
|
6786
|
+
writeFileSync10(path, next.endsWith("\n") ? next : `${next}
|
|
6787
|
+
`, "utf8");
|
|
6788
|
+
}
|
|
6789
|
+
chmodSync(path, 493);
|
|
6790
|
+
return path;
|
|
6791
|
+
} catch {
|
|
6792
|
+
return void 0;
|
|
6793
|
+
}
|
|
6794
|
+
}
|
|
6795
|
+
|
|
6796
|
+
// src/commands/session-context.ts
|
|
6797
|
+
import { randomUUID } from "crypto";
|
|
6798
|
+
import { mkdirSync as mkdirSync12, renameSync as renameSync2, rmSync as rmSync5, writeFileSync as writeFileSync11 } from "fs";
|
|
6799
|
+
import { dirname as dirname11, join as join13 } from "path";
|
|
6800
|
+
var DYNAMIC_AGENT_CONTEXT_FILE = join13(".sechroom", "CLAUDE.md");
|
|
6801
|
+
function checkoutRoot(start) {
|
|
6802
|
+
const semPath = resolveSemPathForRead(start);
|
|
6803
|
+
return semPath ? dirname11(dirname11(semPath)) : start;
|
|
6804
|
+
}
|
|
6805
|
+
function renderSessionContext(result, lane) {
|
|
6806
|
+
if (result.status === "hold") {
|
|
6807
|
+
return [
|
|
6808
|
+
"# Sechroom dynamic agent context \u2014 HOLD",
|
|
6809
|
+
"",
|
|
6810
|
+
`Lane: ${lane}`,
|
|
6811
|
+
result.workspaceId ? `Workspace: ${result.workspaceId}` : null,
|
|
6812
|
+
"",
|
|
6813
|
+
result.reason?.trim() || "Session context resolution held without a reason.",
|
|
6814
|
+
"",
|
|
6815
|
+
"Do not use a previously materialized context file for this session.",
|
|
6816
|
+
""
|
|
6817
|
+
].filter((line) => line !== null).join("\n");
|
|
6818
|
+
}
|
|
6819
|
+
const lines = [
|
|
6820
|
+
"# Sechroom dynamic agent context",
|
|
6821
|
+
"",
|
|
6822
|
+
`<!-- sechroom-context lane=${JSON.stringify(lane)} workspace=${JSON.stringify(result.workspaceId ?? "")} -->`,
|
|
6823
|
+
""
|
|
6824
|
+
];
|
|
6825
|
+
for (const member of result.members) {
|
|
6826
|
+
lines.push(
|
|
6827
|
+
`<!-- sechroom-context-member key=${JSON.stringify(member.key)} layer=${JSON.stringify(member.layer)} bundle=${JSON.stringify(`${member.bundleSlug}@${member.bundleVersion}`)} component=${JSON.stringify(member.componentSlug)} id=${JSON.stringify(member.sourceId)} sourceVersion=${member.sourceVersion} -->`
|
|
6828
|
+
);
|
|
6829
|
+
if (member.heading?.trim()) lines.push(member.heading.trim(), "");
|
|
6830
|
+
else if (member.title?.trim()) lines.push(`## ${member.title.trim()}`, "");
|
|
6831
|
+
lines.push(member.body.trimEnd(), "");
|
|
6832
|
+
}
|
|
6833
|
+
return lines.join("\n");
|
|
6834
|
+
}
|
|
6835
|
+
function writeSessionContext(start, lane, result) {
|
|
6836
|
+
const path = join13(checkoutRoot(start), DYNAMIC_AGENT_CONTEXT_FILE);
|
|
6837
|
+
const context = renderSessionContext(result, lane);
|
|
6838
|
+
mkdirSync12(dirname11(path), { recursive: true });
|
|
6839
|
+
const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
6840
|
+
try {
|
|
6841
|
+
writeFileSync11(temporaryPath, context.endsWith("\n") ? context : `${context}
|
|
6842
|
+
`, "utf8");
|
|
6843
|
+
renameSync2(temporaryPath, path);
|
|
6844
|
+
} catch (error) {
|
|
6845
|
+
rmSync5(temporaryPath, { force: true });
|
|
6846
|
+
throw error;
|
|
6847
|
+
}
|
|
6848
|
+
return { status: result.status, path, context };
|
|
6849
|
+
}
|
|
6850
|
+
async function materializeSessionContext(client, cfg, lane, start) {
|
|
6851
|
+
const { data, error, response } = await client.POST("/session-context/resolve", {
|
|
6852
|
+
body: {
|
|
6853
|
+
laneId: lane,
|
|
6854
|
+
workspaceId: cfg.workspaceId ?? null
|
|
6855
|
+
}
|
|
6856
|
+
});
|
|
6857
|
+
if (!data) {
|
|
6858
|
+
const detail = error ? JSON.stringify(error) : `HTTP ${response?.status ?? "unknown"}`;
|
|
6859
|
+
throw new Error(`session context resolution failed: ${detail}`);
|
|
6860
|
+
}
|
|
6861
|
+
return writeSessionContext(start, lane, data);
|
|
6862
|
+
}
|
|
6863
|
+
|
|
6864
|
+
// src/commands/hook.ts
|
|
6654
6865
|
async function readStdin2() {
|
|
6655
6866
|
if (process.stdin.isTTY) return "";
|
|
6656
6867
|
const chunks = [];
|
|
@@ -6674,13 +6885,13 @@ function resolveLane(flagLane, cwd) {
|
|
|
6674
6885
|
if (!base) return void 0;
|
|
6675
6886
|
return applyWorktreeLaneSuffix(base, start);
|
|
6676
6887
|
}
|
|
6677
|
-
var INTENT_FILE =
|
|
6888
|
+
var INTENT_FILE = join14(".sechroom", "continuity.json");
|
|
6678
6889
|
function resolveIntentPath(start) {
|
|
6679
6890
|
let dir = start;
|
|
6680
6891
|
for (; ; ) {
|
|
6681
|
-
const candidate =
|
|
6682
|
-
if (
|
|
6683
|
-
const parent =
|
|
6892
|
+
const candidate = join14(dir, INTENT_FILE);
|
|
6893
|
+
if (existsSync11(candidate)) return candidate;
|
|
6894
|
+
const parent = dirname12(dir);
|
|
6684
6895
|
if (parent === dir) return void 0;
|
|
6685
6896
|
dir = parent;
|
|
6686
6897
|
}
|
|
@@ -6689,7 +6900,7 @@ function readIntent(start) {
|
|
|
6689
6900
|
const path = resolveIntentPath(start);
|
|
6690
6901
|
if (!path) return void 0;
|
|
6691
6902
|
try {
|
|
6692
|
-
return JSON.parse(
|
|
6903
|
+
return JSON.parse(readFileSync10(path, "utf8"));
|
|
6693
6904
|
} catch {
|
|
6694
6905
|
return void 0;
|
|
6695
6906
|
}
|
|
@@ -6731,14 +6942,14 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
|
|
|
6731
6942
|
}
|
|
6732
6943
|
function ledgerPath(start) {
|
|
6733
6944
|
const intent = resolveIntentPath(start);
|
|
6734
|
-
const dir = intent ?
|
|
6735
|
-
return
|
|
6945
|
+
const dir = intent ? dirname12(intent) : join14(start, ".sechroom");
|
|
6946
|
+
return join14(dir, ".checkpoint-state.json");
|
|
6736
6947
|
}
|
|
6737
6948
|
function readLedger(start) {
|
|
6738
6949
|
try {
|
|
6739
6950
|
const p = ledgerPath(start);
|
|
6740
|
-
if (!
|
|
6741
|
-
return JSON.parse(
|
|
6951
|
+
if (!existsSync11(p)) return {};
|
|
6952
|
+
return JSON.parse(readFileSync10(p, "utf8"));
|
|
6742
6953
|
} catch {
|
|
6743
6954
|
return {};
|
|
6744
6955
|
}
|
|
@@ -6785,13 +6996,13 @@ function recordPush(start, intent) {
|
|
|
6785
6996
|
} catch {
|
|
6786
6997
|
mtimeMs = void 0;
|
|
6787
6998
|
}
|
|
6788
|
-
|
|
6999
|
+
mkdirSync13(dirname12(p), { recursive: true });
|
|
6789
7000
|
const ledger = {
|
|
6790
7001
|
lastEpochMs: Date.now(),
|
|
6791
7002
|
lastMtimeMs: mtimeMs,
|
|
6792
7003
|
lastHash: intentHash(intent)
|
|
6793
7004
|
};
|
|
6794
|
-
|
|
7005
|
+
writeFileSync12(p, JSON.stringify(ledger) + "\n");
|
|
6795
7006
|
} catch {
|
|
6796
7007
|
}
|
|
6797
7008
|
}
|
|
@@ -6847,29 +7058,59 @@ Examples:
|
|
|
6847
7058
|
$ sechroom hook install --local --dry-run preview the project .claude/settings.json
|
|
6848
7059
|
|
|
6849
7060
|
Lane source (high -> low): --lane > SECHROOM_LANE > ./.sechroom/lane.json code-lane (D-binding-5).
|
|
6850
|
-
Fail-soft:
|
|
7061
|
+
Fail-soft: failures exit 0 and never block; session-context refresh failures render an explicit HOLD.`
|
|
6851
7062
|
);
|
|
6852
|
-
hook.command("session-start").description("
|
|
7063
|
+
hook.command("session-start").description("Materialize dynamic agent context, install commit attribution, and resume continuity").option("--lane <laneId>", "Override the resolved lane (else SECHROOM_LANE, else ./.sechroom/lane.json code-lane)").option("--surface <surface>", "Target surface: claude | codex (output is identical for session-start)", "claude").option("--max-artifacts <n>", "Cap artifacts in the resume bundle").action(async (opts, cmd) => {
|
|
6853
7064
|
try {
|
|
6854
7065
|
const raw = await readStdin2();
|
|
6855
7066
|
const input = parseHookInput2(raw);
|
|
6856
|
-
const
|
|
7067
|
+
const cwd = input.cwd ?? process.cwd();
|
|
7068
|
+
const lane = resolveLane(opts.lane, cwd);
|
|
6857
7069
|
if (!lane) return process.exit(0);
|
|
6858
|
-
|
|
7070
|
+
installLaneCommitHook(cwd);
|
|
7071
|
+
const semPath = resolveSemPathForRead(cwd);
|
|
6859
7072
|
if (semPath) ensureContinuityScaffold(semPath);
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
workspaceId: null,
|
|
6866
|
-
maxArtifacts: opts.maxArtifacts != null ? Number(opts.maxArtifacts) : null,
|
|
6867
|
-
includeLookingAtMyself: null,
|
|
6868
|
-
changedSince: null
|
|
6869
|
-
}
|
|
7073
|
+
let dynamic = writeSessionContext(cwd, lane, {
|
|
7074
|
+
status: "hold",
|
|
7075
|
+
workspaceId: null,
|
|
7076
|
+
reason: "SESSION_CONTEXT_UNAVAILABLE: the session-start hook could not refresh the pinned context (transport or authentication failure).",
|
|
7077
|
+
members: []
|
|
6870
7078
|
});
|
|
6871
|
-
|
|
6872
|
-
|
|
7079
|
+
let continuity = null;
|
|
7080
|
+
try {
|
|
7081
|
+
const cfg = resolveConfig(cmd.optsWithGlobals(), cwd);
|
|
7082
|
+
const client = await makeClient(cfg);
|
|
7083
|
+
try {
|
|
7084
|
+
dynamic = await materializeSessionContext(client, cfg, lane, cwd);
|
|
7085
|
+
} catch {
|
|
7086
|
+
}
|
|
7087
|
+
try {
|
|
7088
|
+
const { data } = await client.POST("/continuity/resume/lane", {
|
|
7089
|
+
body: {
|
|
7090
|
+
laneId: lane,
|
|
7091
|
+
workspaceId: null,
|
|
7092
|
+
maxArtifacts: opts.maxArtifacts != null ? Number(opts.maxArtifacts) : null,
|
|
7093
|
+
includeLookingAtMyself: null,
|
|
7094
|
+
changedSince: null
|
|
7095
|
+
}
|
|
7096
|
+
});
|
|
7097
|
+
continuity = formatContext(data, lane);
|
|
7098
|
+
} catch {
|
|
7099
|
+
}
|
|
7100
|
+
} catch {
|
|
7101
|
+
}
|
|
7102
|
+
const contexts = [dynamic.context];
|
|
7103
|
+
if (continuity) contexts.push(continuity);
|
|
7104
|
+
if (contexts.length) emitSessionStart(contexts.join("\n\n"));
|
|
7105
|
+
return process.exit(0);
|
|
7106
|
+
} catch {
|
|
7107
|
+
return process.exit(0);
|
|
7108
|
+
}
|
|
7109
|
+
});
|
|
7110
|
+
hook.command("commit-msg <messagePath>").description("Append the checkout lane to a final Git commit message (installed helper)").action((messagePath) => {
|
|
7111
|
+
try {
|
|
7112
|
+
const lane = resolveCheckoutLane(process.cwd());
|
|
7113
|
+
if (lane) appendLaneTrailer(messagePath, lane);
|
|
6873
7114
|
return process.exit(0);
|
|
6874
7115
|
} catch {
|
|
6875
7116
|
return process.exit(0);
|
|
@@ -7044,10 +7285,10 @@ Examples:
|
|
|
7044
7285
|
const client = await makeClient(cfg);
|
|
7045
7286
|
return client.POST("/continuity/snapshots", { body });
|
|
7046
7287
|
});
|
|
7047
|
-
const path = resolveIntentPath(cwd) ??
|
|
7288
|
+
const path = resolveIntentPath(cwd) ?? join15(cwd, INTENT_FILE);
|
|
7048
7289
|
const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
|
|
7049
|
-
|
|
7050
|
-
|
|
7290
|
+
mkdirSync14(dirname13(path), { recursive: true });
|
|
7291
|
+
writeFileSync13(path, JSON.stringify(fileBody, null, 2) + "\n");
|
|
7051
7292
|
recordPush(cwd, merged);
|
|
7052
7293
|
if (json) {
|
|
7053
7294
|
emit({ snapshotId: data.snapshotId, lane, scope, file: path }, true);
|
|
@@ -7061,7 +7302,7 @@ Examples:
|
|
|
7061
7302
|
}
|
|
7062
7303
|
|
|
7063
7304
|
// src/commands/close.ts
|
|
7064
|
-
import { readFileSync as
|
|
7305
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
7065
7306
|
var VERDICTS = ["pass", "soft-fail", "plan-invalid", "blocked"];
|
|
7066
7307
|
function registerClose(program2) {
|
|
7067
7308
|
program2.command("close").description(
|
|
@@ -7102,7 +7343,7 @@ Examples:
|
|
|
7102
7343
|
);
|
|
7103
7344
|
let bodyText;
|
|
7104
7345
|
try {
|
|
7105
|
-
bodyText = opts.file ?
|
|
7346
|
+
bodyText = opts.file ? readFileSync11(opts.file, "utf8") : readFileSync11(0, "utf8");
|
|
7106
7347
|
} catch {
|
|
7107
7348
|
fail(
|
|
7108
7349
|
opts.file ? `could not read --file ${opts.file}` : "no closeout body \u2014 pass --file <path> or pipe it on stdin"
|
|
@@ -7791,6 +8032,764 @@ Examples:
|
|
|
7791
8032
|
});
|
|
7792
8033
|
}
|
|
7793
8034
|
|
|
8035
|
+
// src/commands/herdr.ts
|
|
8036
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
8037
|
+
import { basename as basename3 } from "path";
|
|
8038
|
+
|
|
8039
|
+
// src/herdr/client.ts
|
|
8040
|
+
import { createConnection as createConnection2 } from "net";
|
|
8041
|
+
import { homedir as homedir5 } from "os";
|
|
8042
|
+
import { join as join16 } from "path";
|
|
8043
|
+
var DEFAULT_HERDR_SOCKET_RELATIVE = ".config/herdr/herdr.sock";
|
|
8044
|
+
var HerdrUnreachableError = class extends Error {
|
|
8045
|
+
constructor(socketPath, reason) {
|
|
8046
|
+
super(`no Herdr socket at ${socketPath} (${reason}) \u2014 is Herdr running?`);
|
|
8047
|
+
this.socketPath = socketPath;
|
|
8048
|
+
this.reason = reason;
|
|
8049
|
+
this.name = "HerdrUnreachableError";
|
|
8050
|
+
}
|
|
8051
|
+
socketPath;
|
|
8052
|
+
reason;
|
|
8053
|
+
};
|
|
8054
|
+
var HerdrRpcError = class extends Error {
|
|
8055
|
+
constructor(message, code) {
|
|
8056
|
+
super(message);
|
|
8057
|
+
this.code = code;
|
|
8058
|
+
this.name = "HerdrRpcError";
|
|
8059
|
+
}
|
|
8060
|
+
code;
|
|
8061
|
+
};
|
|
8062
|
+
function resolveSocketPath(flag, env = process.env, home = homedir5()) {
|
|
8063
|
+
const fromFlag = flag?.trim();
|
|
8064
|
+
if (fromFlag) return fromFlag;
|
|
8065
|
+
const fromEnv = env.HERDR_SOCKET?.trim();
|
|
8066
|
+
if (fromEnv) return fromEnv;
|
|
8067
|
+
return join16(home, DEFAULT_HERDR_SOCKET_RELATIVE);
|
|
8068
|
+
}
|
|
8069
|
+
function expandTarget(target) {
|
|
8070
|
+
const trimmed = target.trim();
|
|
8071
|
+
if (trimmed.length === 0) {
|
|
8072
|
+
throw new Error(
|
|
8073
|
+
"target is required (e.g. w6:p1, or w6 for its first pane)"
|
|
8074
|
+
);
|
|
8075
|
+
}
|
|
8076
|
+
return trimmed.includes(":") ? trimmed : `${trimmed}:p1`;
|
|
8077
|
+
}
|
|
8078
|
+
var requestCounter = 0;
|
|
8079
|
+
function nextRequestId() {
|
|
8080
|
+
requestCounter += 1;
|
|
8081
|
+
return `sechroom-${process.pid}-${requestCounter}`;
|
|
8082
|
+
}
|
|
8083
|
+
function encodeRequest(id, method, params) {
|
|
8084
|
+
return `${JSON.stringify({ id, method, params })}
|
|
8085
|
+
`;
|
|
8086
|
+
}
|
|
8087
|
+
function createLineReader() {
|
|
8088
|
+
let buffer = "";
|
|
8089
|
+
return (chunk) => {
|
|
8090
|
+
buffer += chunk;
|
|
8091
|
+
const lines = [];
|
|
8092
|
+
for (; ; ) {
|
|
8093
|
+
const nl = buffer.indexOf("\n");
|
|
8094
|
+
if (nl === -1) break;
|
|
8095
|
+
const line = buffer.slice(0, nl).trim();
|
|
8096
|
+
buffer = buffer.slice(nl + 1);
|
|
8097
|
+
if (line.length > 0) lines.push(line);
|
|
8098
|
+
}
|
|
8099
|
+
return lines;
|
|
8100
|
+
};
|
|
8101
|
+
}
|
|
8102
|
+
function truncateForError(line) {
|
|
8103
|
+
return line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
|
|
8104
|
+
}
|
|
8105
|
+
function parseResponse(line) {
|
|
8106
|
+
let parsed;
|
|
8107
|
+
try {
|
|
8108
|
+
parsed = JSON.parse(line);
|
|
8109
|
+
} catch {
|
|
8110
|
+
throw new Error(`malformed Herdr response: ${truncateForError(line)}`);
|
|
8111
|
+
}
|
|
8112
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
8113
|
+
throw new Error(`malformed Herdr response: ${truncateForError(line)}`);
|
|
8114
|
+
}
|
|
8115
|
+
const obj = parsed;
|
|
8116
|
+
if (obj.error) {
|
|
8117
|
+
throw new HerdrRpcError(
|
|
8118
|
+
obj.error.message ?? "Herdr returned an error with no message",
|
|
8119
|
+
obj.error.code
|
|
8120
|
+
);
|
|
8121
|
+
}
|
|
8122
|
+
if (!("result" in obj)) {
|
|
8123
|
+
throw new Error(
|
|
8124
|
+
`Herdr response carried neither result nor error: ${truncateForError(line)}`
|
|
8125
|
+
);
|
|
8126
|
+
}
|
|
8127
|
+
return obj.result;
|
|
8128
|
+
}
|
|
8129
|
+
function classifyStreamLine(line, requestId) {
|
|
8130
|
+
let parsed;
|
|
8131
|
+
try {
|
|
8132
|
+
parsed = JSON.parse(line);
|
|
8133
|
+
} catch {
|
|
8134
|
+
return { kind: "malformed", line };
|
|
8135
|
+
}
|
|
8136
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
8137
|
+
return { kind: "malformed", line };
|
|
8138
|
+
}
|
|
8139
|
+
const obj = parsed;
|
|
8140
|
+
if (obj.error) {
|
|
8141
|
+
return {
|
|
8142
|
+
kind: "error",
|
|
8143
|
+
message: obj.error.message ?? "Herdr returned an error with no message",
|
|
8144
|
+
code: obj.error.code
|
|
8145
|
+
};
|
|
8146
|
+
}
|
|
8147
|
+
if (obj.id === requestId && "result" in obj) return { kind: "ack" };
|
|
8148
|
+
return { kind: "event", payload: parsed };
|
|
8149
|
+
}
|
|
8150
|
+
function toTransportError(socketPath, error) {
|
|
8151
|
+
const code = error.code;
|
|
8152
|
+
if (code === "ENOENT" || code === "ECONNREFUSED" || code === "EACCES") {
|
|
8153
|
+
return new HerdrUnreachableError(socketPath, code);
|
|
8154
|
+
}
|
|
8155
|
+
return error;
|
|
8156
|
+
}
|
|
8157
|
+
function herdrRequest(socketPath, method, params, timeoutMs = 1e4) {
|
|
8158
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
8159
|
+
const id = nextRequestId();
|
|
8160
|
+
const readLines = createLineReader();
|
|
8161
|
+
let settled = false;
|
|
8162
|
+
let timer;
|
|
8163
|
+
const socket = createConnection2(socketPath);
|
|
8164
|
+
const finish = (fn) => {
|
|
8165
|
+
if (settled) return;
|
|
8166
|
+
settled = true;
|
|
8167
|
+
clearTimeout(timer);
|
|
8168
|
+
socket.destroy();
|
|
8169
|
+
fn();
|
|
8170
|
+
};
|
|
8171
|
+
timer = setTimeout(
|
|
8172
|
+
() => finish(
|
|
8173
|
+
() => rejectPromise(
|
|
8174
|
+
new Error(
|
|
8175
|
+
`Herdr request '${method}' timed out after ${timeoutMs}ms (socket ${socketPath})`
|
|
8176
|
+
)
|
|
8177
|
+
)
|
|
8178
|
+
),
|
|
8179
|
+
timeoutMs
|
|
8180
|
+
);
|
|
8181
|
+
timer.unref?.();
|
|
8182
|
+
socket.setEncoding("utf8");
|
|
8183
|
+
socket.on("connect", () => {
|
|
8184
|
+
socket.write(encodeRequest(id, method, params));
|
|
8185
|
+
});
|
|
8186
|
+
socket.on("data", (chunk) => {
|
|
8187
|
+
const [line] = readLines(chunk);
|
|
8188
|
+
if (line === void 0) return;
|
|
8189
|
+
try {
|
|
8190
|
+
const result = parseResponse(line);
|
|
8191
|
+
finish(() => resolvePromise(result));
|
|
8192
|
+
} catch (error) {
|
|
8193
|
+
finish(() => rejectPromise(error));
|
|
8194
|
+
}
|
|
8195
|
+
});
|
|
8196
|
+
socket.on("error", (error) => {
|
|
8197
|
+
finish(() => rejectPromise(toTransportError(socketPath, error)));
|
|
8198
|
+
});
|
|
8199
|
+
socket.on("close", () => {
|
|
8200
|
+
finish(
|
|
8201
|
+
() => rejectPromise(
|
|
8202
|
+
new HerdrUnreachableError(
|
|
8203
|
+
socketPath,
|
|
8204
|
+
"connection closed before a response"
|
|
8205
|
+
)
|
|
8206
|
+
)
|
|
8207
|
+
);
|
|
8208
|
+
});
|
|
8209
|
+
});
|
|
8210
|
+
}
|
|
8211
|
+
function herdrStream(socketPath, subscriptions, onEvent, options = {}) {
|
|
8212
|
+
const { signal, onWarning, subscribeTimeoutMs = 1e4 } = options;
|
|
8213
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
8214
|
+
if (signal?.aborted) {
|
|
8215
|
+
resolvePromise();
|
|
8216
|
+
return;
|
|
8217
|
+
}
|
|
8218
|
+
const id = nextRequestId();
|
|
8219
|
+
const readLines = createLineReader();
|
|
8220
|
+
let settled = false;
|
|
8221
|
+
let acked = false;
|
|
8222
|
+
const socket = createConnection2(socketPath);
|
|
8223
|
+
const ackTimer = setTimeout(() => {
|
|
8224
|
+
if (!acked) {
|
|
8225
|
+
finish(
|
|
8226
|
+
() => rejectPromise(
|
|
8227
|
+
new Error(
|
|
8228
|
+
`Herdr did not acknowledge events.subscribe within ${subscribeTimeoutMs}ms (socket ${socketPath})`
|
|
8229
|
+
)
|
|
8230
|
+
)
|
|
8231
|
+
);
|
|
8232
|
+
}
|
|
8233
|
+
}, subscribeTimeoutMs);
|
|
8234
|
+
ackTimer.unref?.();
|
|
8235
|
+
const onAbort = () => finish(() => resolvePromise());
|
|
8236
|
+
const finish = (fn) => {
|
|
8237
|
+
if (settled) return;
|
|
8238
|
+
settled = true;
|
|
8239
|
+
clearTimeout(ackTimer);
|
|
8240
|
+
signal?.removeEventListener("abort", onAbort);
|
|
8241
|
+
socket.destroy();
|
|
8242
|
+
fn();
|
|
8243
|
+
};
|
|
8244
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
8245
|
+
socket.setEncoding("utf8");
|
|
8246
|
+
socket.on("connect", () => {
|
|
8247
|
+
socket.write(encodeRequest(id, "events.subscribe", { subscriptions }));
|
|
8248
|
+
});
|
|
8249
|
+
socket.on("data", (chunk) => {
|
|
8250
|
+
for (const line of readLines(chunk)) {
|
|
8251
|
+
if (!acked) {
|
|
8252
|
+
acked = true;
|
|
8253
|
+
clearTimeout(ackTimer);
|
|
8254
|
+
}
|
|
8255
|
+
const classified = classifyStreamLine(line, id);
|
|
8256
|
+
if (classified.kind === "ack") continue;
|
|
8257
|
+
if (classified.kind === "error") {
|
|
8258
|
+
finish(
|
|
8259
|
+
() => rejectPromise(
|
|
8260
|
+
new HerdrRpcError(classified.message, classified.code)
|
|
8261
|
+
)
|
|
8262
|
+
);
|
|
8263
|
+
return;
|
|
8264
|
+
}
|
|
8265
|
+
if (classified.kind === "malformed") {
|
|
8266
|
+
onWarning?.(
|
|
8267
|
+
`ignoring malformed Herdr event line: ${truncateForError(classified.line)}`
|
|
8268
|
+
);
|
|
8269
|
+
continue;
|
|
8270
|
+
}
|
|
8271
|
+
try {
|
|
8272
|
+
onEvent(classified.payload);
|
|
8273
|
+
} catch (error) {
|
|
8274
|
+
finish(
|
|
8275
|
+
() => rejectPromise(
|
|
8276
|
+
error instanceof Error ? error : new Error(String(error))
|
|
8277
|
+
)
|
|
8278
|
+
);
|
|
8279
|
+
return;
|
|
8280
|
+
}
|
|
8281
|
+
}
|
|
8282
|
+
});
|
|
8283
|
+
socket.on("error", (error) => {
|
|
8284
|
+
finish(() => rejectPromise(toTransportError(socketPath, error)));
|
|
8285
|
+
});
|
|
8286
|
+
socket.on("close", () => finish(() => resolvePromise()));
|
|
8287
|
+
});
|
|
8288
|
+
}
|
|
8289
|
+
function abortableSleep(milliseconds, signal) {
|
|
8290
|
+
return new Promise((resolve8) => {
|
|
8291
|
+
if (signal?.aborted) {
|
|
8292
|
+
resolve8();
|
|
8293
|
+
return;
|
|
8294
|
+
}
|
|
8295
|
+
let timer;
|
|
8296
|
+
const onAbort = () => {
|
|
8297
|
+
clearTimeout(timer);
|
|
8298
|
+
resolve8();
|
|
8299
|
+
};
|
|
8300
|
+
timer = setTimeout(() => {
|
|
8301
|
+
signal?.removeEventListener("abort", onAbort);
|
|
8302
|
+
resolve8();
|
|
8303
|
+
}, milliseconds);
|
|
8304
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
8305
|
+
});
|
|
8306
|
+
}
|
|
8307
|
+
function createHerdrPorts(socketPath) {
|
|
8308
|
+
return {
|
|
8309
|
+
socketPath,
|
|
8310
|
+
request: (method, params) => herdrRequest(socketPath, method, params),
|
|
8311
|
+
stream: (subscriptions, onEvent, options) => herdrStream(socketPath, subscriptions, onEvent, options),
|
|
8312
|
+
sleep: (milliseconds, signal) => abortableSleep(milliseconds, signal)
|
|
8313
|
+
};
|
|
8314
|
+
}
|
|
8315
|
+
|
|
8316
|
+
// src/commands/herdr.ts
|
|
8317
|
+
var HERDR_READ_SOURCES = [
|
|
8318
|
+
"visible",
|
|
8319
|
+
"recent",
|
|
8320
|
+
"recent_unwrapped",
|
|
8321
|
+
"detection"
|
|
8322
|
+
];
|
|
8323
|
+
var DEFAULT_READ_LINES = 40;
|
|
8324
|
+
var DEFAULT_READ_SOURCE = "recent";
|
|
8325
|
+
var TITLE_WIDTH = 48;
|
|
8326
|
+
var STREAM_HEALTHY_MS = 3e4;
|
|
8327
|
+
var WAIT_FOR_PANES_MAX_MS = 5e3;
|
|
8328
|
+
var ATTENTION_STATUSES = {
|
|
8329
|
+
blocked: "BLOCKED",
|
|
8330
|
+
waiting: "WAITING",
|
|
8331
|
+
needs_input: "NEEDS-INPUT"
|
|
8332
|
+
};
|
|
8333
|
+
function displayStatus(status) {
|
|
8334
|
+
return ATTENTION_STATUSES[status.trim().toLowerCase()] ?? status;
|
|
8335
|
+
}
|
|
8336
|
+
function isAttentionStatus(status) {
|
|
8337
|
+
return status !== void 0 && status.trim().toLowerCase() in ATTENTION_STATUSES;
|
|
8338
|
+
}
|
|
8339
|
+
function agentLabel(agent, paneId) {
|
|
8340
|
+
return agent?.name?.trim() || agent?.agent?.trim() || agent?.pane_id || paneId || "?";
|
|
8341
|
+
}
|
|
8342
|
+
function cwdBasename(cwd) {
|
|
8343
|
+
const trimmed = cwd?.trim();
|
|
8344
|
+
if (!trimmed) return "?";
|
|
8345
|
+
return basename3(trimmed.replace(/\/+$/, "")) || trimmed;
|
|
8346
|
+
}
|
|
8347
|
+
function agentTitle(agent) {
|
|
8348
|
+
return agent?.terminal_title_stripped?.trim() || agent?.terminal_title?.trim() || "";
|
|
8349
|
+
}
|
|
8350
|
+
function truncate(value, width) {
|
|
8351
|
+
return value.length > width ? `${value.slice(0, width - 1)}\u2026` : value;
|
|
8352
|
+
}
|
|
8353
|
+
function describeAgent(agent, paneId) {
|
|
8354
|
+
const title = agentTitle(agent);
|
|
8355
|
+
const head = `${agentLabel(agent, paneId)}@${cwdBasename(agent?.cwd)}`;
|
|
8356
|
+
return title ? `${head} [${truncate(title, TITLE_WIDTH)}]` : head;
|
|
8357
|
+
}
|
|
8358
|
+
function renderAgentTable(agents) {
|
|
8359
|
+
if (agents.length === 0) return ["(no agents)"];
|
|
8360
|
+
const rows = agents.map((agent) => ({
|
|
8361
|
+
name: agentLabel(agent),
|
|
8362
|
+
status: displayStatus(agent.agent_status ?? "?"),
|
|
8363
|
+
cwd: cwdBasename(agent.cwd),
|
|
8364
|
+
// Guarded like every other cell: a row missing its pane must not take the
|
|
8365
|
+
// whole table down with `undefined.length` in the column-width pass.
|
|
8366
|
+
pane: agent.pane_id ?? "?",
|
|
8367
|
+
title: truncate(agentTitle(agent), TITLE_WIDTH)
|
|
8368
|
+
}));
|
|
8369
|
+
const header = {
|
|
8370
|
+
name: "agent",
|
|
8371
|
+
status: "status",
|
|
8372
|
+
cwd: "cwd",
|
|
8373
|
+
pane: "pane",
|
|
8374
|
+
title: "title"
|
|
8375
|
+
};
|
|
8376
|
+
const width = (key) => Math.max(header[key].length, ...rows.map((row) => row[key].length));
|
|
8377
|
+
const widths = {
|
|
8378
|
+
name: width("name"),
|
|
8379
|
+
status: width("status"),
|
|
8380
|
+
cwd: width("cwd"),
|
|
8381
|
+
pane: width("pane")
|
|
8382
|
+
};
|
|
8383
|
+
const line = (row) => `${row.name.padEnd(widths.name)} ${row.status.padEnd(widths.status)} ${row.cwd.padEnd(widths.cwd)} ${row.pane.padEnd(widths.pane)} ${row.title}`.trimEnd();
|
|
8384
|
+
return [line(header), ...rows.map(line)];
|
|
8385
|
+
}
|
|
8386
|
+
function parseLines(value, fallback = DEFAULT_READ_LINES) {
|
|
8387
|
+
if (value === void 0) return fallback;
|
|
8388
|
+
const parsed = Number(value);
|
|
8389
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
8390
|
+
throw new Error(`--lines expects a positive integer (got '${value}')`);
|
|
8391
|
+
}
|
|
8392
|
+
return parsed;
|
|
8393
|
+
}
|
|
8394
|
+
function parseSource(value, fallback = DEFAULT_READ_SOURCE) {
|
|
8395
|
+
if (value === void 0) return fallback;
|
|
8396
|
+
const trimmed = value.trim();
|
|
8397
|
+
const match = HERDR_READ_SOURCES.find((source) => source === trimmed);
|
|
8398
|
+
if (match === void 0) {
|
|
8399
|
+
throw new Error(
|
|
8400
|
+
`--source expects one of ${HERDR_READ_SOURCES.join(" | ")} (got '${value}')`
|
|
8401
|
+
);
|
|
8402
|
+
}
|
|
8403
|
+
return match;
|
|
8404
|
+
}
|
|
8405
|
+
function resolveSendText(textArgs, useStdin, readStdin5 = () => readFileSync12(0, "utf8")) {
|
|
8406
|
+
if (useStdin) {
|
|
8407
|
+
if (textArgs.length > 0) {
|
|
8408
|
+
throw new Error(
|
|
8409
|
+
"--stdin reads the prompt from stdin \u2014 don't also pass text arguments"
|
|
8410
|
+
);
|
|
8411
|
+
}
|
|
8412
|
+
const text2 = readStdin5().replace(/\n+$/, "");
|
|
8413
|
+
if (text2.trim().length === 0)
|
|
8414
|
+
throw new Error("stdin was empty \u2014 nothing to send");
|
|
8415
|
+
return text2;
|
|
8416
|
+
}
|
|
8417
|
+
const joined = textArgs.join(" ").trim();
|
|
8418
|
+
if (joined.length === 0) {
|
|
8419
|
+
throw new Error("prompt text is required (or pass --stdin)");
|
|
8420
|
+
}
|
|
8421
|
+
return joined;
|
|
8422
|
+
}
|
|
8423
|
+
function indexAgents(agents) {
|
|
8424
|
+
return new Map(agents.map((agent) => [agent.pane_id, agent]));
|
|
8425
|
+
}
|
|
8426
|
+
function buildWatchSubscriptions(agents) {
|
|
8427
|
+
return agents.flatMap((agent) => [
|
|
8428
|
+
{ type: "pane.agent_status_changed", pane_id: agent.pane_id },
|
|
8429
|
+
{ type: "pane.exited", pane_id: agent.pane_id }
|
|
8430
|
+
]);
|
|
8431
|
+
}
|
|
8432
|
+
function normalizeWatchEvent(raw) {
|
|
8433
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
8434
|
+
const outer = raw;
|
|
8435
|
+
const nested = ["event", "params", "data", "payload"].map((key) => outer[key]).find((value) => typeof value === "object" && value !== null);
|
|
8436
|
+
const readFrom = (source, keys) => {
|
|
8437
|
+
for (const key of keys) {
|
|
8438
|
+
const value = source?.[key];
|
|
8439
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
8440
|
+
return value.trim();
|
|
8441
|
+
}
|
|
8442
|
+
}
|
|
8443
|
+
return void 0;
|
|
8444
|
+
};
|
|
8445
|
+
const pick = (...keys) => {
|
|
8446
|
+
for (const key of keys) {
|
|
8447
|
+
for (const source of [nested, outer]) {
|
|
8448
|
+
const value = readFrom(source, [key]);
|
|
8449
|
+
if (value !== void 0) return value;
|
|
8450
|
+
}
|
|
8451
|
+
}
|
|
8452
|
+
return void 0;
|
|
8453
|
+
};
|
|
8454
|
+
const type = readFrom(outer, ["type", "event_type", "event"]) ?? readFrom(nested, ["type", "event_type"]);
|
|
8455
|
+
const paneId = pick("pane_id", "paneId", "target");
|
|
8456
|
+
if (!type || !paneId) return null;
|
|
8457
|
+
return {
|
|
8458
|
+
type,
|
|
8459
|
+
paneId,
|
|
8460
|
+
from: pick("old_status", "from_status", "previous_status", "from", "old"),
|
|
8461
|
+
to: pick("new_status", "to_status", "agent_status", "status", "to", "new")
|
|
8462
|
+
};
|
|
8463
|
+
}
|
|
8464
|
+
function needsRelistFallback(event) {
|
|
8465
|
+
if (event === null) return true;
|
|
8466
|
+
if (event.type === "pane.exited") return false;
|
|
8467
|
+
return event.to === void 0;
|
|
8468
|
+
}
|
|
8469
|
+
function diffAgentStatuses(previous, current) {
|
|
8470
|
+
const events = [];
|
|
8471
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8472
|
+
for (const agent of current) {
|
|
8473
|
+
const paneId = agent.pane_id;
|
|
8474
|
+
if (!paneId) continue;
|
|
8475
|
+
seen.add(paneId);
|
|
8476
|
+
const to = agent.agent_status ?? void 0;
|
|
8477
|
+
const before = previous.get(paneId);
|
|
8478
|
+
if (before === void 0) {
|
|
8479
|
+
events.push({ type: "pane.appeared", paneId, to });
|
|
8480
|
+
continue;
|
|
8481
|
+
}
|
|
8482
|
+
const from = before.agent_status ?? void 0;
|
|
8483
|
+
if (from !== to) {
|
|
8484
|
+
events.push({ type: "pane.agent_status_changed", paneId, from, to });
|
|
8485
|
+
}
|
|
8486
|
+
}
|
|
8487
|
+
for (const paneId of previous.keys()) {
|
|
8488
|
+
if (!seen.has(paneId)) events.push({ type: "pane.exited", paneId });
|
|
8489
|
+
}
|
|
8490
|
+
return events;
|
|
8491
|
+
}
|
|
8492
|
+
function formatWatchEvent(event, agents) {
|
|
8493
|
+
const head = describeAgent(agents.get(event.paneId), event.paneId);
|
|
8494
|
+
if (event.type === "pane.exited") return `${head}: exited`;
|
|
8495
|
+
if (event.type === "pane.appeared") {
|
|
8496
|
+
return `${head}: appeared \u2014 ${displayStatus(event.to ?? "?")}`;
|
|
8497
|
+
}
|
|
8498
|
+
return `${head}: ${displayStatus(event.from ?? "?")} -> ${displayStatus(event.to ?? "?")}`;
|
|
8499
|
+
}
|
|
8500
|
+
function watchEventJson(event, agents) {
|
|
8501
|
+
const agent = agents.get(event.paneId);
|
|
8502
|
+
return {
|
|
8503
|
+
type: event.type,
|
|
8504
|
+
paneId: event.paneId,
|
|
8505
|
+
name: agentLabel(agent, event.paneId),
|
|
8506
|
+
cwd: agent?.cwd ?? null,
|
|
8507
|
+
title: agentTitle(agent) || null,
|
|
8508
|
+
from: event.from ?? null,
|
|
8509
|
+
to: event.to ?? null
|
|
8510
|
+
};
|
|
8511
|
+
}
|
|
8512
|
+
function watchBackoffMs(attempt, maxMs = 15e3, baseMs = 500) {
|
|
8513
|
+
return Math.min(baseMs * 2 ** Math.max(0, attempt), maxMs);
|
|
8514
|
+
}
|
|
8515
|
+
async function runList2(ports, json) {
|
|
8516
|
+
const { agents } = await ports.request("agent.list", {});
|
|
8517
|
+
if (json) {
|
|
8518
|
+
emit({ agents: agents ?? [] }, true);
|
|
8519
|
+
return;
|
|
8520
|
+
}
|
|
8521
|
+
for (const line of renderAgentTable(agents ?? [])) {
|
|
8522
|
+
process.stdout.write(`${line}
|
|
8523
|
+
`);
|
|
8524
|
+
}
|
|
8525
|
+
}
|
|
8526
|
+
async function runRead(ports, target, options, json) {
|
|
8527
|
+
const result = await ports.request("agent.read", {
|
|
8528
|
+
target: expandTarget(target),
|
|
8529
|
+
source: parseSource(options.source),
|
|
8530
|
+
lines: parseLines(options.lines)
|
|
8531
|
+
});
|
|
8532
|
+
if (json) {
|
|
8533
|
+
emit(result, true);
|
|
8534
|
+
return;
|
|
8535
|
+
}
|
|
8536
|
+
const text2 = result.read?.text ?? "";
|
|
8537
|
+
process.stdout.write(text2.endsWith("\n") || text2 === "" ? text2 : `${text2}
|
|
8538
|
+
`);
|
|
8539
|
+
}
|
|
8540
|
+
async function runSend(ports, target, text2, json) {
|
|
8541
|
+
const paneId = expandTarget(target);
|
|
8542
|
+
const result = await ports.request("agent.prompt", {
|
|
8543
|
+
target: paneId,
|
|
8544
|
+
text: text2
|
|
8545
|
+
});
|
|
8546
|
+
const agent = result.agent;
|
|
8547
|
+
emitAction(
|
|
8548
|
+
`sent to ${describeAgent(agent, paneId)} \u2014 ${displayStatus(agent?.agent_status ?? "?")}`,
|
|
8549
|
+
result,
|
|
8550
|
+
json
|
|
8551
|
+
);
|
|
8552
|
+
}
|
|
8553
|
+
async function runWatch(ports, options) {
|
|
8554
|
+
const write = options.write ?? ((line) => process.stdout.write(`${line}
|
|
8555
|
+
`));
|
|
8556
|
+
const warn2 = options.warn ?? ((line) => process.stderr.write(`${line}
|
|
8557
|
+
`));
|
|
8558
|
+
const now = options.now ?? Date.now;
|
|
8559
|
+
const { json, signal } = options;
|
|
8560
|
+
let attempt = 0;
|
|
8561
|
+
let waitingForPanes = false;
|
|
8562
|
+
let seed = /* @__PURE__ */ new Map();
|
|
8563
|
+
const render = (event, agents) => {
|
|
8564
|
+
if (json) {
|
|
8565
|
+
write(JSON.stringify(watchEventJson(event, agents)));
|
|
8566
|
+
return;
|
|
8567
|
+
}
|
|
8568
|
+
const line = formatWatchEvent(event, agents);
|
|
8569
|
+
write(isAttentionStatus(event.to) ? warn(line) : line);
|
|
8570
|
+
};
|
|
8571
|
+
let relisting = false;
|
|
8572
|
+
let relistQueued = false;
|
|
8573
|
+
const relistAndDiff = async () => {
|
|
8574
|
+
if (relisting) {
|
|
8575
|
+
relistQueued = true;
|
|
8576
|
+
return;
|
|
8577
|
+
}
|
|
8578
|
+
relisting = true;
|
|
8579
|
+
try {
|
|
8580
|
+
do {
|
|
8581
|
+
relistQueued = false;
|
|
8582
|
+
const { agents } = await ports.request(
|
|
8583
|
+
"agent.list",
|
|
8584
|
+
{}
|
|
8585
|
+
);
|
|
8586
|
+
const current = agents ?? [];
|
|
8587
|
+
const transitions = diffAgentStatuses(seed, current);
|
|
8588
|
+
const fresh = indexAgents(current);
|
|
8589
|
+
const labels = new Map([...seed, ...fresh]);
|
|
8590
|
+
seed = fresh;
|
|
8591
|
+
for (const transition of transitions) render(transition, labels);
|
|
8592
|
+
} while (relistQueued && !signal?.aborted);
|
|
8593
|
+
} finally {
|
|
8594
|
+
relisting = false;
|
|
8595
|
+
}
|
|
8596
|
+
};
|
|
8597
|
+
while (!signal?.aborted) {
|
|
8598
|
+
try {
|
|
8599
|
+
const { agents } = await ports.request("agent.list", {});
|
|
8600
|
+
const current = agents ?? [];
|
|
8601
|
+
seed = indexAgents(current);
|
|
8602
|
+
const subscriptions = buildWatchSubscriptions(current);
|
|
8603
|
+
if (subscriptions.length === 0) {
|
|
8604
|
+
if (!waitingForPanes) {
|
|
8605
|
+
waitingForPanes = true;
|
|
8606
|
+
warn2(
|
|
8607
|
+
style.dim(
|
|
8608
|
+
`no panes on ${ports.socketPath} \u2014 waiting for one to appear`
|
|
8609
|
+
)
|
|
8610
|
+
);
|
|
8611
|
+
}
|
|
8612
|
+
} else {
|
|
8613
|
+
if (waitingForPanes) {
|
|
8614
|
+
waitingForPanes = false;
|
|
8615
|
+
attempt = 0;
|
|
8616
|
+
}
|
|
8617
|
+
if (!json) {
|
|
8618
|
+
warn2(
|
|
8619
|
+
style.dim(
|
|
8620
|
+
`watching ${seed.size} pane(s) on ${ports.socketPath} \u2014 Ctrl-C to stop`
|
|
8621
|
+
)
|
|
8622
|
+
);
|
|
8623
|
+
}
|
|
8624
|
+
let carriedTraffic = false;
|
|
8625
|
+
let warnedDegraded = false;
|
|
8626
|
+
const startedAt = now();
|
|
8627
|
+
await ports.stream(
|
|
8628
|
+
subscriptions,
|
|
8629
|
+
(raw) => {
|
|
8630
|
+
carriedTraffic = true;
|
|
8631
|
+
const event = normalizeWatchEvent(raw);
|
|
8632
|
+
if (event === null || needsRelistFallback(event)) {
|
|
8633
|
+
if (!warnedDegraded) {
|
|
8634
|
+
warnedDegraded = true;
|
|
8635
|
+
warn2(
|
|
8636
|
+
`herdr watch: unreadable event payload \u2014 deriving transitions from agent.list instead`
|
|
8637
|
+
);
|
|
8638
|
+
}
|
|
8639
|
+
void relistAndDiff().catch(
|
|
8640
|
+
(error) => warn2(
|
|
8641
|
+
`herdr watch: agent.list fallback failed: ${formatFailureMessage(error)}`
|
|
8642
|
+
)
|
|
8643
|
+
);
|
|
8644
|
+
return;
|
|
8645
|
+
}
|
|
8646
|
+
render(event, seed);
|
|
8647
|
+
const known = seed.get(event.paneId);
|
|
8648
|
+
if (known && event.to) known.agent_status = event.to;
|
|
8649
|
+
},
|
|
8650
|
+
{
|
|
8651
|
+
signal,
|
|
8652
|
+
onWarning: (message) => {
|
|
8653
|
+
if (!json) warn2(style.dim(message));
|
|
8654
|
+
}
|
|
8655
|
+
}
|
|
8656
|
+
);
|
|
8657
|
+
if (carriedTraffic || now() - startedAt >= STREAM_HEALTHY_MS) {
|
|
8658
|
+
attempt = 0;
|
|
8659
|
+
}
|
|
8660
|
+
}
|
|
8661
|
+
} catch (error) {
|
|
8662
|
+
if (signal?.aborted) return;
|
|
8663
|
+
waitingForPanes = false;
|
|
8664
|
+
warn2(`herdr watch: ${formatFailureMessage(error)}`);
|
|
8665
|
+
}
|
|
8666
|
+
if (signal?.aborted) return;
|
|
8667
|
+
const delay = watchBackoffMs(
|
|
8668
|
+
attempt,
|
|
8669
|
+
waitingForPanes ? WAIT_FOR_PANES_MAX_MS : void 0
|
|
8670
|
+
);
|
|
8671
|
+
attempt += 1;
|
|
8672
|
+
if (!json && !waitingForPanes) {
|
|
8673
|
+
warn2(style.dim(`reconnecting in ${delay / 1e3}s\u2026`));
|
|
8674
|
+
}
|
|
8675
|
+
await ports.sleep(delay, signal);
|
|
8676
|
+
}
|
|
8677
|
+
}
|
|
8678
|
+
function exitCleanlyOnBrokenPipe(stream, exit = (code) => process.exit(code), report = (line) => process.stderr.write(line)) {
|
|
8679
|
+
stream.on("error", (error) => {
|
|
8680
|
+
if (error.code === "EPIPE" || error.code === "ERR_STREAM_DESTROYED") {
|
|
8681
|
+
exit(0);
|
|
8682
|
+
return;
|
|
8683
|
+
}
|
|
8684
|
+
report(`error: ${formatFailureMessage(error)}
|
|
8685
|
+
`);
|
|
8686
|
+
exit(1);
|
|
8687
|
+
});
|
|
8688
|
+
}
|
|
8689
|
+
function registerHerdr(program2) {
|
|
8690
|
+
const herdr = program2.command("herdr").description(
|
|
8691
|
+
"Drive local Herdr agents (herdr.dev) over its Unix-socket API \u2014 list, read, send, watch"
|
|
8692
|
+
);
|
|
8693
|
+
herdr.addHelpText(
|
|
8694
|
+
"after",
|
|
8695
|
+
`
|
|
8696
|
+
Examples:
|
|
8697
|
+
$ sechroom herdr list every pane Herdr is tracking, with status
|
|
8698
|
+
$ sechroom herdr list --json machine output
|
|
8699
|
+
$ sechroom herdr read w6 last 40 lines of w6:p1 (bare workspace -> :p1)
|
|
8700
|
+
$ sechroom herdr read w6:p1 --lines 200 more scrollback
|
|
8701
|
+
$ sechroom herdr read w6:p1 --source visible only what's on screen
|
|
8702
|
+
$ sechroom herdr send w6 "run the tests and report back"
|
|
8703
|
+
$ cat brief.md | sechroom herdr send w6:p1 --stdin long dispatch from stdin
|
|
8704
|
+
$ sechroom herdr watch live status transitions; Ctrl-C to stop
|
|
8705
|
+
$ sechroom herdr watch --json | jq . one JSON object per event
|
|
8706
|
+
|
|
8707
|
+
Socket: --socket <path>, else $HERDR_SOCKET, else ~/.config/herdr/herdr.sock.
|
|
8708
|
+
Read sources: ${HERDR_READ_SOURCES.join(" | ")}.
|
|
8709
|
+
|
|
8710
|
+
watch event types: pane.agent_status_changed, pane.exited, and pane.appeared \u2014
|
|
8711
|
+
the last is SYNTHESIZED by this CLI (Herdr does not push it) when a transition
|
|
8712
|
+
is re-derived from agent.list and a pane is present that wasn't before.
|
|
8713
|
+
|
|
8714
|
+
Tool use only: this talks to a running Herdr over its local socket. Herdr is
|
|
8715
|
+
AGPL \u2014 no Herdr code is vendored here.`
|
|
8716
|
+
);
|
|
8717
|
+
const withSocket = (command) => command.option(
|
|
8718
|
+
"--socket <path>",
|
|
8719
|
+
"Herdr socket path (default: $HERDR_SOCKET, else ~/.config/herdr/herdr.sock)"
|
|
8720
|
+
);
|
|
8721
|
+
const portsFor = (opts) => createHerdrPorts(resolveSocketPath(opts.socket));
|
|
8722
|
+
withSocket(
|
|
8723
|
+
herdr.command("list").description(
|
|
8724
|
+
"List the agents Herdr is tracking (name, status, cwd, pane)"
|
|
8725
|
+
)
|
|
8726
|
+
).action(async (opts, cmd) => {
|
|
8727
|
+
exitCleanlyOnBrokenPipe(process.stdout);
|
|
8728
|
+
try {
|
|
8729
|
+
await runList2(portsFor(opts), Boolean(cmd.optsWithGlobals().json));
|
|
8730
|
+
} catch (error) {
|
|
8731
|
+
fail(error);
|
|
8732
|
+
}
|
|
8733
|
+
});
|
|
8734
|
+
withSocket(
|
|
8735
|
+
herdr.command("read <target>").description(
|
|
8736
|
+
`Print a pane's text (default ${DEFAULT_READ_LINES} lines, source ${DEFAULT_READ_SOURCE})`
|
|
8737
|
+
).option("--lines <n>", `Lines to read (default ${DEFAULT_READ_LINES})`).option(
|
|
8738
|
+
"--source <source>",
|
|
8739
|
+
`One of ${HERDR_READ_SOURCES.join(" | ")} (default ${DEFAULT_READ_SOURCE})`
|
|
8740
|
+
)
|
|
8741
|
+
).action(async (target, opts, cmd) => {
|
|
8742
|
+
exitCleanlyOnBrokenPipe(process.stdout);
|
|
8743
|
+
try {
|
|
8744
|
+
await runRead(
|
|
8745
|
+
portsFor(opts),
|
|
8746
|
+
target,
|
|
8747
|
+
opts,
|
|
8748
|
+
Boolean(cmd.optsWithGlobals().json)
|
|
8749
|
+
);
|
|
8750
|
+
} catch (error) {
|
|
8751
|
+
fail(error);
|
|
8752
|
+
}
|
|
8753
|
+
});
|
|
8754
|
+
withSocket(
|
|
8755
|
+
herdr.command("send <target> [text...]").description("Type a prompt into a pane and press enter").option("--stdin", "Read the prompt body from stdin instead of arguments")
|
|
8756
|
+
).action(async (target, textArgs, opts, cmd) => {
|
|
8757
|
+
try {
|
|
8758
|
+
const text2 = resolveSendText(textArgs ?? [], Boolean(opts.stdin));
|
|
8759
|
+
await runSend(
|
|
8760
|
+
portsFor(opts),
|
|
8761
|
+
target,
|
|
8762
|
+
text2,
|
|
8763
|
+
Boolean(cmd.optsWithGlobals().json)
|
|
8764
|
+
);
|
|
8765
|
+
} catch (error) {
|
|
8766
|
+
fail(error);
|
|
8767
|
+
}
|
|
8768
|
+
});
|
|
8769
|
+
withSocket(
|
|
8770
|
+
herdr.command("watch").description(
|
|
8771
|
+
"Stream agent status transitions live (reconnects on drop; Ctrl-C to stop)"
|
|
8772
|
+
)
|
|
8773
|
+
).action(async (opts, cmd) => {
|
|
8774
|
+
exitCleanlyOnBrokenPipe(process.stdout);
|
|
8775
|
+
const controller = new AbortController();
|
|
8776
|
+
const onSignal = () => controller.abort();
|
|
8777
|
+
process.once("SIGINT", onSignal);
|
|
8778
|
+
process.once("SIGTERM", onSignal);
|
|
8779
|
+
try {
|
|
8780
|
+
await runWatch(portsFor(opts), {
|
|
8781
|
+
json: Boolean(cmd.optsWithGlobals().json),
|
|
8782
|
+
signal: controller.signal
|
|
8783
|
+
});
|
|
8784
|
+
} catch (error) {
|
|
8785
|
+
fail(error);
|
|
8786
|
+
} finally {
|
|
8787
|
+
process.off("SIGINT", onSignal);
|
|
8788
|
+
process.off("SIGTERM", onSignal);
|
|
8789
|
+
}
|
|
8790
|
+
});
|
|
8791
|
+
}
|
|
8792
|
+
|
|
7794
8793
|
// src/commands/lane.ts
|
|
7795
8794
|
var LANE_KEYS = ["code-lane", "design-lane"];
|
|
7796
8795
|
function showLane(json) {
|
|
@@ -7874,20 +8873,211 @@ Examples:
|
|
|
7874
8873
|
}
|
|
7875
8874
|
|
|
7876
8875
|
// src/commands/memory.ts
|
|
7877
|
-
import { readFileSync as
|
|
7878
|
-
|
|
8876
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
8877
|
+
|
|
8878
|
+
// src/commands/memory-import.ts
|
|
8879
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync13, realpathSync, statSync as statSync3 } from "fs";
|
|
8880
|
+
import { basename as basename4, join as join17, resolve as resolve5 } from "path";
|
|
8881
|
+
var MARKDOWN_RE = /\.(md|markdown)$/i;
|
|
8882
|
+
function isMarkdownPath(path) {
|
|
8883
|
+
return MARKDOWN_RE.test(path);
|
|
8884
|
+
}
|
|
8885
|
+
function defaultTitleForFile(text2, path) {
|
|
8886
|
+
const heading = text2.match(/^#\s+(.+?)\s*$/m)?.[1];
|
|
8887
|
+
if (heading) return heading;
|
|
8888
|
+
return path == null ? null : basename4(path).replace(/\.(md|markdown|txt)$/i, "");
|
|
8889
|
+
}
|
|
8890
|
+
function collectImportFiles(inputs, opts = {}) {
|
|
8891
|
+
const files = [];
|
|
8892
|
+
const skipped = [];
|
|
8893
|
+
const missing = [];
|
|
8894
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8895
|
+
const addFile = (path) => {
|
|
8896
|
+
let key;
|
|
8897
|
+
try {
|
|
8898
|
+
key = realpathSync(path);
|
|
8899
|
+
} catch {
|
|
8900
|
+
key = resolve5(path);
|
|
8901
|
+
}
|
|
8902
|
+
if (seen.has(key)) return;
|
|
8903
|
+
seen.add(key);
|
|
8904
|
+
files.push(path);
|
|
8905
|
+
};
|
|
8906
|
+
const walk = (dir) => {
|
|
8907
|
+
const entries = readdirSync2(dir, { withFileTypes: true }).sort(
|
|
8908
|
+
(a, b) => a.name.localeCompare(b.name)
|
|
8909
|
+
);
|
|
8910
|
+
for (const entry of entries) {
|
|
8911
|
+
if (entry.name.startsWith(".")) continue;
|
|
8912
|
+
const child = join17(dir, entry.name);
|
|
8913
|
+
let isDirectory = entry.isDirectory();
|
|
8914
|
+
let isFile = entry.isFile();
|
|
8915
|
+
if (entry.isSymbolicLink()) {
|
|
8916
|
+
let target;
|
|
8917
|
+
try {
|
|
8918
|
+
target = statSync3(child);
|
|
8919
|
+
} catch {
|
|
8920
|
+
skipped.push({ path: child, reason: "broken symlink" });
|
|
8921
|
+
continue;
|
|
8922
|
+
}
|
|
8923
|
+
if (target.isDirectory()) {
|
|
8924
|
+
skipped.push({
|
|
8925
|
+
path: child,
|
|
8926
|
+
reason: "symlinked directory \u2014 not followed"
|
|
8927
|
+
});
|
|
8928
|
+
continue;
|
|
8929
|
+
}
|
|
8930
|
+
isDirectory = false;
|
|
8931
|
+
isFile = target.isFile();
|
|
8932
|
+
}
|
|
8933
|
+
if (isDirectory) {
|
|
8934
|
+
if (opts.recursive) walk(child);
|
|
8935
|
+
else
|
|
8936
|
+
skipped.push({
|
|
8937
|
+
path: child,
|
|
8938
|
+
reason: "subdirectory \u2014 pass --recursive"
|
|
8939
|
+
});
|
|
8940
|
+
continue;
|
|
8941
|
+
}
|
|
8942
|
+
if (!isFile) {
|
|
8943
|
+
skipped.push({ path: child, reason: "not a regular file" });
|
|
8944
|
+
continue;
|
|
8945
|
+
}
|
|
8946
|
+
if (!isMarkdownPath(entry.name)) {
|
|
8947
|
+
skipped.push({ path: child, reason: "not markdown" });
|
|
8948
|
+
continue;
|
|
8949
|
+
}
|
|
8950
|
+
addFile(child);
|
|
8951
|
+
}
|
|
8952
|
+
};
|
|
8953
|
+
for (const input of inputs) {
|
|
8954
|
+
let isDirectory;
|
|
8955
|
+
try {
|
|
8956
|
+
isDirectory = statSync3(input).isDirectory();
|
|
8957
|
+
} catch {
|
|
8958
|
+
missing.push(input);
|
|
8959
|
+
continue;
|
|
8960
|
+
}
|
|
8961
|
+
if (isDirectory) walk(input);
|
|
8962
|
+
else addFile(input);
|
|
8963
|
+
}
|
|
8964
|
+
return { files, skipped, missing };
|
|
8965
|
+
}
|
|
8966
|
+
function buildImportPlan(collected) {
|
|
8967
|
+
const rows = [];
|
|
8968
|
+
const skipped = [...collected.skipped];
|
|
8969
|
+
for (const path of collected.files) {
|
|
8970
|
+
let text2;
|
|
8971
|
+
try {
|
|
8972
|
+
text2 = readFileSync13(path, "utf8");
|
|
8973
|
+
} catch (error) {
|
|
8974
|
+
throw new Error(
|
|
8975
|
+
`couldn't read ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
8976
|
+
);
|
|
8977
|
+
}
|
|
8978
|
+
if (text2.trim().length === 0) {
|
|
8979
|
+
skipped.push({ path, reason: "empty file" });
|
|
8980
|
+
continue;
|
|
8981
|
+
}
|
|
8982
|
+
rows.push({ path, title: defaultTitleForFile(text2, path), text: text2 });
|
|
8983
|
+
}
|
|
8984
|
+
return { rows, skipped };
|
|
8985
|
+
}
|
|
8986
|
+
function classifyWorkspaceArg(value) {
|
|
8987
|
+
const trimmed = value.trim();
|
|
8988
|
+
return trimmed.startsWith("wsp_") ? { kind: "id", id: trimmed } : { kind: "name", name: trimmed };
|
|
8989
|
+
}
|
|
8990
|
+
function matchWorkspacesByName(rows, name) {
|
|
8991
|
+
const target = name.trim().toLowerCase();
|
|
8992
|
+
return rows.filter((row) => row.name.trim().toLowerCase() === target);
|
|
8993
|
+
}
|
|
8994
|
+
async function resolveImportWorkspace(arg, opts, ports) {
|
|
8995
|
+
const unresolved = { created: false, wouldCreate: false };
|
|
8996
|
+
if (!arg) return { id: null, name: null, ...unresolved };
|
|
8997
|
+
const classified = classifyWorkspaceArg(arg);
|
|
8998
|
+
if (classified.kind === "id") {
|
|
8999
|
+
return { id: classified.id, name: null, ...unresolved };
|
|
9000
|
+
}
|
|
9001
|
+
const name = classified.name;
|
|
9002
|
+
const matches = matchWorkspacesByName(await ports.listWorkspaces(), name);
|
|
9003
|
+
if (matches.length > 1) {
|
|
9004
|
+
const candidates = matches.map((match) => `${match.id} ("${match.name}")`).join(", ");
|
|
9005
|
+
throw new Error(
|
|
9006
|
+
`Workspace name "${name}" is ambiguous \u2014 ${matches.length} matches: ${candidates}. Pass --workspace <wsp_id>.`
|
|
9007
|
+
);
|
|
9008
|
+
}
|
|
9009
|
+
if (matches.length === 1) {
|
|
9010
|
+
const match = matches[0];
|
|
9011
|
+
return { id: match.id, name: match.name, ...unresolved };
|
|
9012
|
+
}
|
|
9013
|
+
if (!opts.createWorkspace) {
|
|
9014
|
+
throw new Error(
|
|
9015
|
+
`No workspace named "${name}". Pass --create-workspace to create it, or --workspace <wsp_id>.`
|
|
9016
|
+
);
|
|
9017
|
+
}
|
|
9018
|
+
if (opts.dryRun) {
|
|
9019
|
+
return { id: null, name, created: false, wouldCreate: true };
|
|
9020
|
+
}
|
|
9021
|
+
const created = await ports.createWorkspace(name);
|
|
9022
|
+
return { id: created.id, name, created: true, wouldCreate: false };
|
|
9023
|
+
}
|
|
9024
|
+
async function executeImport(rows, options, ports, onCreated) {
|
|
9025
|
+
const created = [];
|
|
9026
|
+
for (const row of rows) {
|
|
9027
|
+
const ref = await ports.createMemory({
|
|
9028
|
+
text: row.text,
|
|
9029
|
+
title: row.title,
|
|
9030
|
+
type: options.type,
|
|
9031
|
+
tags: options.tags,
|
|
9032
|
+
source: options.source,
|
|
9033
|
+
confidence: options.confidence,
|
|
9034
|
+
workspaceId: options.workspaceId
|
|
9035
|
+
});
|
|
9036
|
+
const entry = {
|
|
9037
|
+
path: row.path,
|
|
9038
|
+
id: ref.id,
|
|
9039
|
+
title: row.title,
|
|
9040
|
+
url: ref.url ?? null
|
|
9041
|
+
};
|
|
9042
|
+
created.push(entry);
|
|
9043
|
+
onCreated?.(entry);
|
|
9044
|
+
}
|
|
9045
|
+
return created;
|
|
9046
|
+
}
|
|
9047
|
+
async function unwrapApi(call) {
|
|
9048
|
+
const res = await call;
|
|
9049
|
+
const queued = res.data ?? res.error;
|
|
9050
|
+
if (res.response?.status === 202 && isGovernanceQueued(queued)) {
|
|
9051
|
+
throw new Error(
|
|
9052
|
+
"the write is queued for operator approval \u2014 import stopped."
|
|
9053
|
+
);
|
|
9054
|
+
}
|
|
9055
|
+
const httpFailed = res.response !== void 0 && !res.response.ok;
|
|
9056
|
+
if (res.error !== void 0 && res.error !== null || httpFailed) {
|
|
9057
|
+
throw new Error(
|
|
9058
|
+
formatFailureMessage(
|
|
9059
|
+
res.error ?? (res.response ? `HTTP ${res.response.status} ${res.response.statusText}`.trim() : "request failed")
|
|
9060
|
+
)
|
|
9061
|
+
);
|
|
9062
|
+
}
|
|
9063
|
+
return res.data;
|
|
9064
|
+
}
|
|
9065
|
+
|
|
9066
|
+
// src/commands/memory.ts
|
|
7879
9067
|
function resolveCreateBody(textOpt, fileOpt) {
|
|
7880
9068
|
if (textOpt == null === (fileOpt == null)) {
|
|
7881
9069
|
fail("Provide exactly one of --text or --file.");
|
|
7882
9070
|
}
|
|
7883
9071
|
if (textOpt != null) return { text: textOpt, defaultTitle: null };
|
|
7884
9072
|
const fromStdin = fileOpt === "-";
|
|
7885
|
-
const text2 = fromStdin ?
|
|
9073
|
+
const text2 = fromStdin ? readFileSync14(0, "utf8") : readFileSync14(String(fileOpt), "utf8");
|
|
7886
9074
|
if (text2.trim().length === 0) {
|
|
7887
9075
|
fail(fromStdin ? "Stdin was empty." : `File is empty: ${fileOpt}`);
|
|
7888
9076
|
}
|
|
7889
|
-
const
|
|
7890
|
-
|
|
9077
|
+
const defaultTitle = defaultTitleForFile(
|
|
9078
|
+
text2,
|
|
9079
|
+
fromStdin ? null : String(fileOpt)
|
|
9080
|
+
);
|
|
7891
9081
|
return { text: text2, defaultTitle };
|
|
7892
9082
|
}
|
|
7893
9083
|
function registerMemory(program2) {
|
|
@@ -7900,6 +9090,8 @@ Examples:
|
|
|
7900
9090
|
$ sechroom memory create --text "filed note" --owner-type Workspace --owner-id wsp_XXXX
|
|
7901
9091
|
$ sechroom memory create --file docs/conventions.md --owner-type Workspace --owner-id wsp_XXXX
|
|
7902
9092
|
$ cat NOTES.md | sechroom memory create --file - --tag kind:reference
|
|
9093
|
+
$ sechroom memory import docs/ --workspace "Team Notes" --create-workspace --tag kind:reference
|
|
9094
|
+
$ sechroom memory import notes/*.md --workspace wsp_XXXX --dry-run
|
|
7903
9095
|
$ sechroom memory search "rate limiting" --limit 5 --tag kind:decision
|
|
7904
9096
|
$ sechroom memory search "auth flow" --workspace wsp_XXXX --json
|
|
7905
9097
|
$ sechroom memory get mem_XXXX --json
|
|
@@ -7953,6 +9145,195 @@ Examples:
|
|
|
7953
9145
|
cmd.optsWithGlobals().json
|
|
7954
9146
|
);
|
|
7955
9147
|
});
|
|
9148
|
+
memory.command("import <paths...>").description(
|
|
9149
|
+
"Bulk-import files as memories (POST /memories per file); a directory contributes its *.md"
|
|
9150
|
+
).option(
|
|
9151
|
+
"--workspace <nameOrId>",
|
|
9152
|
+
"Target workspace: a wsp_ id, or a name (case-insensitive exact match). Omit to import Unfiled."
|
|
9153
|
+
).option(
|
|
9154
|
+
"--create-workspace",
|
|
9155
|
+
"Create the --workspace when its name matches nothing",
|
|
9156
|
+
false
|
|
9157
|
+
).option("--recursive", "Walk subdirectories of a given directory", false).option("--type <type>", "Memory type", "reference").option("--tag <tag...>", "Tags applied to every memory (repeatable)").option("--source <source>", "Source / lane stamp", "cli").option("--confidence <n>", "Confidence 0..1", "1.0").option("--dry-run", "Resolve and print the plan; write nothing", false).action(async (paths, opts, cmd) => {
|
|
9158
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
9159
|
+
const json = Boolean(cmd.optsWithGlobals().json);
|
|
9160
|
+
const dryRun = Boolean(opts.dryRun);
|
|
9161
|
+
if (opts.createWorkspace && !opts.workspace) {
|
|
9162
|
+
fail("--create-workspace needs --workspace <name>.");
|
|
9163
|
+
}
|
|
9164
|
+
const collected = collectImportFiles(paths, {
|
|
9165
|
+
recursive: Boolean(opts.recursive)
|
|
9166
|
+
});
|
|
9167
|
+
if (collected.missing.length > 0) {
|
|
9168
|
+
fail(`no such file or directory: ${collected.missing.join(", ")}`);
|
|
9169
|
+
}
|
|
9170
|
+
const plan = (() => {
|
|
9171
|
+
try {
|
|
9172
|
+
return buildImportPlan(collected);
|
|
9173
|
+
} catch (error) {
|
|
9174
|
+
return fail(error);
|
|
9175
|
+
}
|
|
9176
|
+
})();
|
|
9177
|
+
if (plan.rows.length === 0) {
|
|
9178
|
+
fail(
|
|
9179
|
+
`nothing to import from ${paths.join(", ")} \u2014 a directory contributes *.md / *.markdown (add --recursive for subdirectories), and empty files are skipped.`
|
|
9180
|
+
);
|
|
9181
|
+
}
|
|
9182
|
+
let cached;
|
|
9183
|
+
const client = async () => cached ??= await makeClient(cfg);
|
|
9184
|
+
const ports = {
|
|
9185
|
+
listWorkspaces: async () => {
|
|
9186
|
+
const data = await unwrapApi(
|
|
9187
|
+
(await client()).GET("/workspaces", {
|
|
9188
|
+
params: { query: { includeArchived: false } }
|
|
9189
|
+
})
|
|
9190
|
+
);
|
|
9191
|
+
const items = data ?? [];
|
|
9192
|
+
return items.map((row) => row.item ?? row).filter((w) => Boolean(w?.id && w?.name));
|
|
9193
|
+
},
|
|
9194
|
+
createWorkspace: async (name) => unwrapApi(
|
|
9195
|
+
(await client()).POST("/workspaces", {
|
|
9196
|
+
body: { name, description: null, parentId: null }
|
|
9197
|
+
})
|
|
9198
|
+
),
|
|
9199
|
+
createMemory: async (input) => unwrapApi(
|
|
9200
|
+
(await client()).POST("/memories", {
|
|
9201
|
+
body: {
|
|
9202
|
+
text: input.text,
|
|
9203
|
+
type: input.type,
|
|
9204
|
+
content: "{}",
|
|
9205
|
+
confidence: input.confidence,
|
|
9206
|
+
source: input.source,
|
|
9207
|
+
archetype: "Document",
|
|
9208
|
+
title: input.title,
|
|
9209
|
+
tags: input.tags,
|
|
9210
|
+
owner: input.workspaceId ? { type: "Workspace", id: input.workspaceId } : null
|
|
9211
|
+
}
|
|
9212
|
+
})
|
|
9213
|
+
)
|
|
9214
|
+
};
|
|
9215
|
+
const workspace = await resolveImportWorkspace(
|
|
9216
|
+
opts.workspace,
|
|
9217
|
+
{ createWorkspace: Boolean(opts.createWorkspace), dryRun },
|
|
9218
|
+
ports
|
|
9219
|
+
).catch((error) => fail(error));
|
|
9220
|
+
const destination = workspace.id ? `${style.bold(workspace.id)}${workspace.name ? ` ${style.dim(`"${workspace.name}"`)}` : ""}${workspace.created ? " (created)" : ""}` : workspace.wouldCreate ? `${style.dim(`"${workspace.name}"`)} (would create)` : "Unfiled";
|
|
9221
|
+
const skippedLines = plan.skipped.map(
|
|
9222
|
+
(s) => ` ${style.dim("skipped")} ${s.path} ${style.dim(`(${s.reason})`)}`
|
|
9223
|
+
);
|
|
9224
|
+
if (dryRun) {
|
|
9225
|
+
if (json) {
|
|
9226
|
+
return emit(
|
|
9227
|
+
{
|
|
9228
|
+
dryRun: true,
|
|
9229
|
+
workspace: {
|
|
9230
|
+
id: workspace.id,
|
|
9231
|
+
name: workspace.name,
|
|
9232
|
+
created: false,
|
|
9233
|
+
wouldCreate: workspace.wouldCreate
|
|
9234
|
+
},
|
|
9235
|
+
plan: plan.rows.map((row) => ({
|
|
9236
|
+
path: row.path,
|
|
9237
|
+
title: row.title,
|
|
9238
|
+
chars: row.text.length
|
|
9239
|
+
})),
|
|
9240
|
+
skipped: plan.skipped
|
|
9241
|
+
},
|
|
9242
|
+
true
|
|
9243
|
+
);
|
|
9244
|
+
}
|
|
9245
|
+
process.stdout.write(
|
|
9246
|
+
`${style.dim("dry run \u2014 nothing written")}
|
|
9247
|
+
${plan.rows.map(
|
|
9248
|
+
(row) => ` ${row.path} ${style.dim("\u2192")} "${row.title}" ${style.dim("\u2192")} ${destination}`
|
|
9249
|
+
).join("\n")}
|
|
9250
|
+
`
|
|
9251
|
+
);
|
|
9252
|
+
if (skippedLines.length > 0) {
|
|
9253
|
+
process.stdout.write(`${skippedLines.join("\n")}
|
|
9254
|
+
`);
|
|
9255
|
+
}
|
|
9256
|
+
process.stdout.write(
|
|
9257
|
+
`${plan.rows.length} memor${plan.rows.length === 1 ? "y" : "ies"} would be created.
|
|
9258
|
+
`
|
|
9259
|
+
);
|
|
9260
|
+
return;
|
|
9261
|
+
}
|
|
9262
|
+
const created = [];
|
|
9263
|
+
try {
|
|
9264
|
+
await executeImport(
|
|
9265
|
+
plan.rows,
|
|
9266
|
+
{
|
|
9267
|
+
workspaceId: workspace.id,
|
|
9268
|
+
type: opts.type,
|
|
9269
|
+
tags: opts.tag ?? null,
|
|
9270
|
+
source: opts.source,
|
|
9271
|
+
confidence: Number(opts.confidence)
|
|
9272
|
+
},
|
|
9273
|
+
ports,
|
|
9274
|
+
(entry) => {
|
|
9275
|
+
created.push(entry);
|
|
9276
|
+
if (json) return;
|
|
9277
|
+
const view = resolveViewUrl(cfg.baseUrl, entry.url);
|
|
9278
|
+
process.stdout.write(
|
|
9279
|
+
`${ok("\u2713")} ${style.bold(entry.id)} ${style.dim("\u2190")} ${entry.path}${view ? ` ${style.dim("\u2192")} ${view}` : ""}
|
|
9280
|
+
`
|
|
9281
|
+
);
|
|
9282
|
+
}
|
|
9283
|
+
);
|
|
9284
|
+
} catch (error) {
|
|
9285
|
+
if (json) {
|
|
9286
|
+
emit(
|
|
9287
|
+
{
|
|
9288
|
+
dryRun: false,
|
|
9289
|
+
ok: false,
|
|
9290
|
+
workspace: {
|
|
9291
|
+
id: workspace.id,
|
|
9292
|
+
name: workspace.name,
|
|
9293
|
+
created: workspace.created
|
|
9294
|
+
},
|
|
9295
|
+
created,
|
|
9296
|
+
createdCount: created.length,
|
|
9297
|
+
skipped: plan.skipped,
|
|
9298
|
+
remaining: plan.rows.length - created.length
|
|
9299
|
+
},
|
|
9300
|
+
true
|
|
9301
|
+
);
|
|
9302
|
+
} else {
|
|
9303
|
+
process.stderr.write(
|
|
9304
|
+
`${created.length} of ${plan.rows.length} memor${plan.rows.length === 1 ? "y" : "ies"} created before the failure.
|
|
9305
|
+
`
|
|
9306
|
+
);
|
|
9307
|
+
}
|
|
9308
|
+
fail(error);
|
|
9309
|
+
}
|
|
9310
|
+
if (json) {
|
|
9311
|
+
return emit(
|
|
9312
|
+
{
|
|
9313
|
+
dryRun: false,
|
|
9314
|
+
ok: true,
|
|
9315
|
+
workspace: {
|
|
9316
|
+
id: workspace.id,
|
|
9317
|
+
name: workspace.name,
|
|
9318
|
+
created: workspace.created
|
|
9319
|
+
},
|
|
9320
|
+
created,
|
|
9321
|
+
createdCount: created.length,
|
|
9322
|
+
skipped: plan.skipped
|
|
9323
|
+
},
|
|
9324
|
+
true
|
|
9325
|
+
);
|
|
9326
|
+
}
|
|
9327
|
+
if (skippedLines.length > 0) {
|
|
9328
|
+
process.stdout.write(`${skippedLines.join("\n")}
|
|
9329
|
+
`);
|
|
9330
|
+
}
|
|
9331
|
+
emitAction(
|
|
9332
|
+
`imported ${style.bold(String(created.length))} memor${created.length === 1 ? "y" : "ies"} into ${destination}`,
|
|
9333
|
+
created,
|
|
9334
|
+
false
|
|
9335
|
+
);
|
|
9336
|
+
});
|
|
7956
9337
|
memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").action(async (memoryId, _opts, cmd) => {
|
|
7957
9338
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
7958
9339
|
const data = await runApi("Fetching memory", async () => {
|
|
@@ -8309,8 +9690,8 @@ Examples:
|
|
|
8309
9690
|
|
|
8310
9691
|
// src/setup/apply.ts
|
|
8311
9692
|
import { createHash as createHash4 } from "crypto";
|
|
8312
|
-
import { mkdirSync as
|
|
8313
|
-
import { dirname as
|
|
9693
|
+
import { mkdirSync as mkdirSync15, readFileSync as readFileSync15, writeFileSync as writeFileSync14, existsSync as existsSync12 } from "fs";
|
|
9694
|
+
import { dirname as dirname14 } from "path";
|
|
8314
9695
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
8315
9696
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
8316
9697
|
function normalizeBody(s) {
|
|
@@ -8363,22 +9744,22 @@ function parseManagedBlock(content, block) {
|
|
|
8363
9744
|
return null;
|
|
8364
9745
|
}
|
|
8365
9746
|
function ensureDir2(path) {
|
|
8366
|
-
|
|
9747
|
+
mkdirSync15(dirname14(path), { recursive: true });
|
|
8367
9748
|
}
|
|
8368
9749
|
function readOr(path, fallback) {
|
|
8369
9750
|
try {
|
|
8370
|
-
return
|
|
9751
|
+
return readFileSync15(path, "utf8");
|
|
8371
9752
|
} catch {
|
|
8372
9753
|
return fallback;
|
|
8373
9754
|
}
|
|
8374
9755
|
}
|
|
8375
9756
|
function mergeMcpJson(path, snippet, dryRun) {
|
|
8376
9757
|
const incoming = JSON.parse(snippet);
|
|
8377
|
-
const existed =
|
|
9758
|
+
const existed = existsSync12(path);
|
|
8378
9759
|
let current = {};
|
|
8379
9760
|
if (existed) {
|
|
8380
9761
|
try {
|
|
8381
|
-
current = JSON.parse(
|
|
9762
|
+
current = JSON.parse(readFileSync15(path, "utf8"));
|
|
8382
9763
|
} catch {
|
|
8383
9764
|
return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
|
|
8384
9765
|
}
|
|
@@ -8386,26 +9767,26 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
8386
9767
|
current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
|
|
8387
9768
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
8388
9769
|
ensureDir2(path);
|
|
8389
|
-
|
|
9770
|
+
writeFileSync14(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
8390
9771
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
8391
9772
|
}
|
|
8392
9773
|
function mergeCodexToml(path, snippet, dryRun) {
|
|
8393
|
-
const existed =
|
|
9774
|
+
const existed = existsSync12(path);
|
|
8394
9775
|
let body = readOr(path, "");
|
|
8395
9776
|
body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
|
|
8396
9777
|
const trimmed = body.trim();
|
|
8397
9778
|
const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
|
|
8398
9779
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
8399
9780
|
ensureDir2(path);
|
|
8400
|
-
|
|
9781
|
+
writeFileSync14(path, next, { mode: 384 });
|
|
8401
9782
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
8402
9783
|
}
|
|
8403
9784
|
function writeInstructionBlock(path, write, dryRun) {
|
|
8404
|
-
const existed =
|
|
9785
|
+
const existed = existsSync12(path);
|
|
8405
9786
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
8406
9787
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
8407
9788
|
ensureDir2(path);
|
|
8408
|
-
|
|
9789
|
+
writeFileSync14(path, next);
|
|
8409
9790
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
8410
9791
|
}
|
|
8411
9792
|
function computeBlockFile(current, write) {
|
|
@@ -8446,7 +9827,7 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
8446
9827
|
const next = computeBlockFile(current, write);
|
|
8447
9828
|
if (!dryRun) {
|
|
8448
9829
|
ensureDir2(proposedPath);
|
|
8449
|
-
|
|
9830
|
+
writeFileSync14(proposedPath, next);
|
|
8450
9831
|
}
|
|
8451
9832
|
return {
|
|
8452
9833
|
kind: "instruction",
|
|
@@ -8576,8 +9957,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
8576
9957
|
}
|
|
8577
9958
|
|
|
8578
9959
|
// src/setup/skills-offer.ts
|
|
8579
|
-
import { mkdirSync as
|
|
8580
|
-
import { join as
|
|
9960
|
+
import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync15 } from "fs";
|
|
9961
|
+
import { join as join18 } from "path";
|
|
8581
9962
|
|
|
8582
9963
|
// src/setup/lane-pin.ts
|
|
8583
9964
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -8693,8 +10074,8 @@ Found ${summary} available to you for ${surface}.
|
|
|
8693
10074
|
if (skills.length > 0) {
|
|
8694
10075
|
const written = [];
|
|
8695
10076
|
for (const s of skills) {
|
|
8696
|
-
|
|
8697
|
-
|
|
10077
|
+
mkdirSync16(join18(sDir, s.name), { recursive: true });
|
|
10078
|
+
writeFileSync15(join18(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
8698
10079
|
written.push(s.name);
|
|
8699
10080
|
}
|
|
8700
10081
|
recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -8702,11 +10083,11 @@ Found ${summary} available to you for ${surface}.
|
|
|
8702
10083
|
`);
|
|
8703
10084
|
}
|
|
8704
10085
|
if (agents.length > 0) {
|
|
8705
|
-
|
|
10086
|
+
mkdirSync16(aDir, { recursive: true });
|
|
8706
10087
|
const written = [];
|
|
8707
10088
|
for (const a of agents) {
|
|
8708
10089
|
const file = `${a.name}.md`;
|
|
8709
|
-
|
|
10090
|
+
writeFileSync15(join18(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
8710
10091
|
written.push(file);
|
|
8711
10092
|
}
|
|
8712
10093
|
recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -9089,13 +10470,13 @@ Wired to namespace '${slug2}'. Restart your AI client (or reload MCP) to pick it
|
|
|
9089
10470
|
}
|
|
9090
10471
|
|
|
9091
10472
|
// src/commands/onboard.ts
|
|
9092
|
-
import { existsSync as
|
|
9093
|
-
import { basename as
|
|
10473
|
+
import { existsSync as existsSync14 } from "fs";
|
|
10474
|
+
import { basename as basename5, join as join20 } from "path";
|
|
9094
10475
|
|
|
9095
10476
|
// src/commands/fanout.ts
|
|
9096
10477
|
import { spawnSync } from "child_process";
|
|
9097
|
-
import { existsSync as
|
|
9098
|
-
import { isAbsolute, join as
|
|
10478
|
+
import { existsSync as existsSync13, readFileSync as readFileSync16, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
10479
|
+
import { isAbsolute as isAbsolute2, join as join19, resolve as resolve6 } from "path";
|
|
9099
10480
|
var ICON = {
|
|
9100
10481
|
refresh: "\u21BB",
|
|
9101
10482
|
bind: "+",
|
|
@@ -9103,33 +10484,33 @@ var ICON = {
|
|
|
9103
10484
|
"skip-unbound": "\u26A0"
|
|
9104
10485
|
};
|
|
9105
10486
|
function resolveChildDir(path, root) {
|
|
9106
|
-
return
|
|
10487
|
+
return isAbsolute2(path) ? path : resolve6(root, path);
|
|
9107
10488
|
}
|
|
9108
10489
|
function discoverChildren(root) {
|
|
9109
10490
|
let names;
|
|
9110
10491
|
try {
|
|
9111
|
-
names =
|
|
10492
|
+
names = readdirSync3(root);
|
|
9112
10493
|
} catch {
|
|
9113
10494
|
return [];
|
|
9114
10495
|
}
|
|
9115
10496
|
const out = [];
|
|
9116
10497
|
for (const name of names.sort()) {
|
|
9117
10498
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
9118
|
-
const dir =
|
|
10499
|
+
const dir = join19(root, name);
|
|
9119
10500
|
try {
|
|
9120
|
-
if (!
|
|
10501
|
+
if (!statSync4(dir).isDirectory()) continue;
|
|
9121
10502
|
} catch {
|
|
9122
10503
|
continue;
|
|
9123
10504
|
}
|
|
9124
|
-
if (
|
|
10505
|
+
if (existsSync13(join19(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
9125
10506
|
}
|
|
9126
10507
|
return out;
|
|
9127
10508
|
}
|
|
9128
10509
|
function readManifest(path) {
|
|
9129
|
-
if (!
|
|
10510
|
+
if (!existsSync13(path)) return null;
|
|
9130
10511
|
let parsed;
|
|
9131
10512
|
try {
|
|
9132
|
-
parsed = JSON.parse(
|
|
10513
|
+
parsed = JSON.parse(readFileSync16(path, "utf8"));
|
|
9133
10514
|
} catch (err2) {
|
|
9134
10515
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
9135
10516
|
}
|
|
@@ -9299,7 +10680,7 @@ function personalSubtreeIds(personalId, all) {
|
|
|
9299
10680
|
}
|
|
9300
10681
|
async function pickWorkspace(client, opts = {}) {
|
|
9301
10682
|
const promptLabel = opts.promptLabel ?? "Bind this directory to a workspace:";
|
|
9302
|
-
const dirName = opts.dirName ??
|
|
10683
|
+
const dirName = opts.dirName ?? basename5(process.cwd());
|
|
9303
10684
|
const all = await withSpinner("Listing your workspaces", () => fetchWorkspaces(client));
|
|
9304
10685
|
if (all.length === 0) {
|
|
9305
10686
|
process.stderr.write(`no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
|
|
@@ -9350,7 +10731,7 @@ async function resolveWorkspaceBinding(client, existing, opts) {
|
|
|
9350
10731
|
}
|
|
9351
10732
|
if (existing) return existing;
|
|
9352
10733
|
if (!canPrompt() || opts.yes) return void 0;
|
|
9353
|
-
return pickWorkspace(client, { dirName:
|
|
10734
|
+
return pickWorkspace(client, { dirName: basename5(process.cwd()) });
|
|
9354
10735
|
}
|
|
9355
10736
|
async function ensureTenant(baseUrl, g, opts) {
|
|
9356
10737
|
const persisted = readPersisted();
|
|
@@ -9495,10 +10876,10 @@ async function chooseScope(scopeFlag, yes) {
|
|
|
9495
10876
|
}
|
|
9496
10877
|
async function planRecurseChild(entry, root, client, opts) {
|
|
9497
10878
|
const dir = resolveChildDir(entry.path, root);
|
|
9498
|
-
if (!
|
|
10879
|
+
if (!existsSync14(dir)) {
|
|
9499
10880
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
9500
10881
|
}
|
|
9501
|
-
if (
|
|
10882
|
+
if (existsSync14(join20(dir, ".sechroom.json"))) {
|
|
9502
10883
|
return {
|
|
9503
10884
|
label: entry.path,
|
|
9504
10885
|
dir,
|
|
@@ -9527,7 +10908,7 @@ ${style.bold(entry.path)} ${style.dim("is not bound yet.")}
|
|
|
9527
10908
|
`);
|
|
9528
10909
|
const ws = await pickWorkspace(client, {
|
|
9529
10910
|
promptLabel: `Bind ${style.cyan(entry.path)} to a workspace:`,
|
|
9530
|
-
dirName:
|
|
10911
|
+
dirName: basename5(entry.path)
|
|
9531
10912
|
});
|
|
9532
10913
|
if (!ws) {
|
|
9533
10914
|
return { label: entry.path, dir, disposition: "skip-unbound", argv: [], reason: "unbound \u2014 no workspace chosen (skipped)" };
|
|
@@ -9571,7 +10952,7 @@ This fan-out will pin the same lane in every repo:
|
|
|
9571
10952
|
async function runRecurse(cfg, g, opts) {
|
|
9572
10953
|
const { yes, dryRun, json } = opts;
|
|
9573
10954
|
const root = process.cwd();
|
|
9574
|
-
const manifestPath =
|
|
10955
|
+
const manifestPath = join20(root, ".sechroom", "repos.json");
|
|
9575
10956
|
const fromManifest = readManifest(manifestPath);
|
|
9576
10957
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
9577
10958
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
@@ -10103,32 +11484,32 @@ Examples:
|
|
|
10103
11484
|
}
|
|
10104
11485
|
|
|
10105
11486
|
// src/commands/reset.ts
|
|
10106
|
-
import { homedir as
|
|
10107
|
-
import { join as
|
|
10108
|
-
import { existsSync as
|
|
11487
|
+
import { homedir as homedir6 } from "os";
|
|
11488
|
+
import { join as join21 } from "path";
|
|
11489
|
+
import { existsSync as existsSync15, readFileSync as readFileSync17, rmSync as rmSync6 } from "fs";
|
|
10109
11490
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
10110
|
-
var localSkillsDir = () =>
|
|
10111
|
-
var globalSkillsDir = () =>
|
|
10112
|
-
var localAgentsDir = () =>
|
|
10113
|
-
var globalAgentsDir = () =>
|
|
11491
|
+
var localSkillsDir = () => join21(process.cwd(), ".claude", "skills");
|
|
11492
|
+
var globalSkillsDir = () => join21(homedir6(), ".claude", "skills");
|
|
11493
|
+
var localAgentsDir = () => join21(process.cwd(), ".claude", "agents");
|
|
11494
|
+
var globalAgentsDir = () => join21(homedir6(), ".claude", "agents");
|
|
10114
11495
|
function removeMaterialisedSkills(dir) {
|
|
10115
11496
|
const removed = [];
|
|
10116
|
-
const lockPath =
|
|
10117
|
-
if (!
|
|
11497
|
+
const lockPath = join21(dir, SKILLS_LOCK2);
|
|
11498
|
+
if (!existsSync15(lockPath)) return removed;
|
|
10118
11499
|
try {
|
|
10119
|
-
const lock = JSON.parse(
|
|
11500
|
+
const lock = JSON.parse(readFileSync17(lockPath, "utf8"));
|
|
10120
11501
|
for (const entry of Object.values(lock)) {
|
|
10121
11502
|
for (const name of entry.skills ?? []) {
|
|
10122
|
-
const p =
|
|
10123
|
-
if (
|
|
10124
|
-
|
|
11503
|
+
const p = join21(dir, name);
|
|
11504
|
+
if (existsSync15(p)) {
|
|
11505
|
+
rmSync6(p, { recursive: true, force: true });
|
|
10125
11506
|
removed.push(p);
|
|
10126
11507
|
}
|
|
10127
11508
|
}
|
|
10128
11509
|
}
|
|
10129
11510
|
} catch {
|
|
10130
11511
|
}
|
|
10131
|
-
|
|
11512
|
+
rmSync6(lockPath, { force: true });
|
|
10132
11513
|
removed.push(lockPath);
|
|
10133
11514
|
return removed;
|
|
10134
11515
|
}
|
|
@@ -10165,19 +11546,19 @@ function registerReset(program2) {
|
|
|
10165
11546
|
}
|
|
10166
11547
|
}
|
|
10167
11548
|
const removed = [];
|
|
10168
|
-
const stateDir =
|
|
10169
|
-
if (
|
|
10170
|
-
|
|
11549
|
+
const stateDir = join21(process.cwd(), ".sechroom");
|
|
11550
|
+
if (existsSync15(stateDir)) {
|
|
11551
|
+
rmSync6(stateDir, { recursive: true, force: true });
|
|
10171
11552
|
removed.push(stateDir);
|
|
10172
11553
|
}
|
|
10173
|
-
const legacyCfg =
|
|
10174
|
-
if (
|
|
10175
|
-
|
|
11554
|
+
const legacyCfg = join21(process.cwd(), ".sechroom.json");
|
|
11555
|
+
if (existsSync15(legacyCfg)) {
|
|
11556
|
+
rmSync6(legacyCfg, { force: true });
|
|
10176
11557
|
removed.push(legacyCfg);
|
|
10177
11558
|
}
|
|
10178
|
-
const legacySem =
|
|
10179
|
-
if (
|
|
10180
|
-
|
|
11559
|
+
const legacySem = join21(process.cwd(), ".sem");
|
|
11560
|
+
if (existsSync15(legacySem)) {
|
|
11561
|
+
rmSync6(legacySem, { force: true });
|
|
10181
11562
|
removed.push(legacySem);
|
|
10182
11563
|
}
|
|
10183
11564
|
removed.push(...removeMaterialisedSkills(localSkillsDir()));
|
|
@@ -10202,8 +11583,8 @@ function registerReset(program2) {
|
|
|
10202
11583
|
}
|
|
10203
11584
|
|
|
10204
11585
|
// src/commands/skills.ts
|
|
10205
|
-
import { existsSync as
|
|
10206
|
-
import { join as
|
|
11586
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync17, statSync as statSync5, writeFileSync as writeFileSync16 } from "fs";
|
|
11587
|
+
import { join as join22 } from "path";
|
|
10207
11588
|
function filenameFromDisposition(header) {
|
|
10208
11589
|
if (!header) return void 0;
|
|
10209
11590
|
const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
|
|
@@ -10211,11 +11592,11 @@ function filenameFromDisposition(header) {
|
|
|
10211
11592
|
}
|
|
10212
11593
|
function resolveOutputPath(output, serverFilename) {
|
|
10213
11594
|
const filename = serverFilename || "skills.zip";
|
|
10214
|
-
if (!output) return
|
|
10215
|
-
const looksLikeDir = output.endsWith("/") ||
|
|
11595
|
+
if (!output) return join22(process.cwd(), filename);
|
|
11596
|
+
const looksLikeDir = output.endsWith("/") || existsSync16(output) && statSync5(output).isDirectory();
|
|
10216
11597
|
if (looksLikeDir) {
|
|
10217
|
-
|
|
10218
|
-
return
|
|
11598
|
+
mkdirSync17(output, { recursive: true });
|
|
11599
|
+
return join22(output, filename);
|
|
10219
11600
|
}
|
|
10220
11601
|
return output;
|
|
10221
11602
|
}
|
|
@@ -10246,7 +11627,7 @@ async function downloadZip(label, call, output) {
|
|
|
10246
11627
|
const buf = Buffer.from(res.data);
|
|
10247
11628
|
const filename = filenameFromDisposition(res.response.headers.get("content-disposition")) ?? "skills.zip";
|
|
10248
11629
|
const path = resolveOutputPath(output, filename);
|
|
10249
|
-
|
|
11630
|
+
writeFileSync16(path, buf);
|
|
10250
11631
|
return { path, bytes: buf.length, filename };
|
|
10251
11632
|
}
|
|
10252
11633
|
function registerSkills(program2) {
|
|
@@ -10420,12 +11801,12 @@ clients select all; an unconfigured machine preserves the legacy Claude default.
|
|
|
10420
11801
|
}
|
|
10421
11802
|
|
|
10422
11803
|
// src/commands/sweep.ts
|
|
10423
|
-
import { existsSync as
|
|
10424
|
-
import { dirname as
|
|
10425
|
-
var DEFAULT_MANIFEST =
|
|
11804
|
+
import { existsSync as existsSync17 } from "fs";
|
|
11805
|
+
import { dirname as dirname15, join as join23, resolve as resolve7 } from "path";
|
|
11806
|
+
var DEFAULT_MANIFEST = join23(".sechroom", "repos.json");
|
|
10426
11807
|
function planEntry(entry, root) {
|
|
10427
11808
|
const dir = resolveChildDir(entry.path, root);
|
|
10428
|
-
if (!
|
|
11809
|
+
if (!existsSync17(dir)) {
|
|
10429
11810
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
10430
11811
|
}
|
|
10431
11812
|
if (committedBindingPath(dir)) {
|
|
@@ -10485,7 +11866,7 @@ Examples:
|
|
|
10485
11866
|
const g = cmd.optsWithGlobals();
|
|
10486
11867
|
const json = Boolean(g.json);
|
|
10487
11868
|
const dryRun = Boolean(opts.dryRun);
|
|
10488
|
-
const manifestPath =
|
|
11869
|
+
const manifestPath = resolve7(opts.manifest);
|
|
10489
11870
|
let repos;
|
|
10490
11871
|
try {
|
|
10491
11872
|
repos = readManifest(manifestPath);
|
|
@@ -10501,7 +11882,7 @@ Examples:
|
|
|
10501
11882
|
`);
|
|
10502
11883
|
return;
|
|
10503
11884
|
}
|
|
10504
|
-
const root =
|
|
11885
|
+
const root = dirname15(dirname15(manifestPath));
|
|
10505
11886
|
const plans = repos.map((entry) => planEntry(entry, root));
|
|
10506
11887
|
if (!json) {
|
|
10507
11888
|
process.stderr.write(
|
|
@@ -10921,7 +12302,7 @@ async function readStdin4() {
|
|
|
10921
12302
|
function resolveVersion() {
|
|
10922
12303
|
try {
|
|
10923
12304
|
const pkg = JSON.parse(
|
|
10924
|
-
|
|
12305
|
+
readFileSync18(new URL("../package.json", import.meta.url), "utf8")
|
|
10925
12306
|
);
|
|
10926
12307
|
return pkg.version ?? "0.0.0";
|
|
10927
12308
|
} catch {
|
|
@@ -11101,6 +12482,7 @@ registerAgents(program);
|
|
|
11101
12482
|
registerLane(program);
|
|
11102
12483
|
registerChannel(program);
|
|
11103
12484
|
registerTelemetry(program);
|
|
12485
|
+
registerHerdr(program);
|
|
11104
12486
|
registerReset(program);
|
|
11105
12487
|
program.parseAsync().catch((err2) => {
|
|
11106
12488
|
process.stderr.write(
|