@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.
Files changed (2) hide show
  1. package/dist/index.js +1527 -145
  2. 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 readFileSync15 } from "fs";
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 = process.cwd().startsWith(local.home) ? process.cwd().slice(local.home.length).replace(/^[/\\]/, "") : "";
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((resolve6) => setTimeout(resolve6, ms)));
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((resolve6) => {
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
- resolve6("completed");
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((resolve6, reject) => {
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
- resolve6(value);
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((resolve6) => {
2739
+ return new Promise((resolve8) => {
2740
2740
  this.child?.once(
2741
2741
  "exit",
2742
- (code) => resolve6(`app-server exited (${code ?? "signal"})`)
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
- (resolve6) => setTimeout(() => resolve6("timeout"), ms).unref?.()
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((resolve6) => {
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) => resolve6({
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((resolve6) => {
5235
- signalCapacityTerminal = resolve6;
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((resolve6) => {
5256
- wake = resolve6;
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((resolve6) => {
5376
- setTimeout(resolve6, ms).unref?.();
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((resolve6, reject) => {
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
- resolve6();
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((resolve6) => setTimeout(resolve6, milliseconds)));
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((resolve6) => {
6519
+ return new Promise((resolve8) => {
6520
6520
  const stop = () => {
6521
- void conn.stop().finally(resolve6);
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 mkdirSync12, writeFileSync as writeFileSync11 } from "fs";
6648
- import { dirname as dirname11, join as join14 } from "path";
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 existsSync10, mkdirSync as mkdirSync11, readFileSync as readFileSync9, statSync as statSync2, writeFileSync as writeFileSync10 } from "fs";
6653
- import { dirname as dirname10, join as join13 } from "path";
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 = join13(".sechroom", "continuity.json");
6888
+ var INTENT_FILE = join14(".sechroom", "continuity.json");
6678
6889
  function resolveIntentPath(start) {
6679
6890
  let dir = start;
6680
6891
  for (; ; ) {
6681
- const candidate = join13(dir, INTENT_FILE);
6682
- if (existsSync10(candidate)) return candidate;
6683
- const parent = dirname10(dir);
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(readFileSync9(path, "utf8"));
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 ? dirname10(intent) : join13(start, ".sechroom");
6735
- return join13(dir, ".checkpoint-state.json");
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 (!existsSync10(p)) return {};
6741
- return JSON.parse(readFileSync9(p, "utf8"));
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
- mkdirSync11(dirname10(p), { recursive: true });
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
- writeFileSync10(p, JSON.stringify(ledger) + "\n");
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: no lane / no auth / no-or-partial intent file / API error -> exit 0, never blocks.`
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("Resume the checkout's lane and emit continuity context for a SessionStart hook").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) => {
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 lane = resolveLane(opts.lane, input.cwd);
7067
+ const cwd = input.cwd ?? process.cwd();
7068
+ const lane = resolveLane(opts.lane, cwd);
6857
7069
  if (!lane) return process.exit(0);
6858
- const semPath = resolveSemPathForRead(input.cwd ?? process.cwd());
7070
+ installLaneCommitHook(cwd);
7071
+ const semPath = resolveSemPathForRead(cwd);
6859
7072
  if (semPath) ensureContinuityScaffold(semPath);
6860
- const cfg = resolveConfig(cmd.optsWithGlobals());
6861
- const client = await makeClient(cfg);
6862
- const { data } = await client.POST("/continuity/resume/lane", {
6863
- body: {
6864
- laneId: lane,
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
- const context = formatContext(data, lane);
6872
- if (context) emitSessionStart(context);
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) ?? join14(cwd, INTENT_FILE);
7288
+ const path = resolveIntentPath(cwd) ?? join15(cwd, INTENT_FILE);
7048
7289
  const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
7049
- mkdirSync12(dirname11(path), { recursive: true });
7050
- writeFileSync11(path, JSON.stringify(fileBody, null, 2) + "\n");
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 readFileSync10 } from "fs";
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 ? readFileSync10(opts.file, "utf8") : readFileSync10(0, "utf8");
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 readFileSync11 } from "fs";
7878
- import { basename as basename2 } from "path";
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 ? readFileSync11(0, "utf8") : readFileSync11(String(fileOpt), "utf8");
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 heading = text2.match(/^#\s+(.+?)\s*$/m)?.[1];
7890
- const defaultTitle = heading ?? (fromStdin ? null : basename2(String(fileOpt)).replace(/\.(md|markdown|txt)$/i, ""));
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 mkdirSync13, readFileSync as readFileSync12, writeFileSync as writeFileSync12, existsSync as existsSync11 } from "fs";
8313
- import { dirname as dirname12 } from "path";
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
- mkdirSync13(dirname12(path), { recursive: true });
9747
+ mkdirSync15(dirname14(path), { recursive: true });
8367
9748
  }
8368
9749
  function readOr(path, fallback) {
8369
9750
  try {
8370
- return readFileSync12(path, "utf8");
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 = existsSync11(path);
9758
+ const existed = existsSync12(path);
8378
9759
  let current = {};
8379
9760
  if (existed) {
8380
9761
  try {
8381
- current = JSON.parse(readFileSync12(path, "utf8"));
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
- writeFileSync12(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
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 = existsSync11(path);
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
- writeFileSync12(path, next, { mode: 384 });
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 = existsSync11(path);
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
- writeFileSync12(path, next);
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
- writeFileSync12(proposedPath, next);
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 mkdirSync14, writeFileSync as writeFileSync13 } from "fs";
8580
- import { join as join15 } from "path";
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
- mkdirSync14(join15(sDir, s.name), { recursive: true });
8697
- writeFileSync13(join15(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
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
- mkdirSync14(aDir, { recursive: true });
10086
+ mkdirSync16(aDir, { recursive: true });
8706
10087
  const written = [];
8707
10088
  for (const a of agents) {
8708
10089
  const file = `${a.name}.md`;
8709
- writeFileSync13(join15(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
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 existsSync13 } from "fs";
9093
- import { basename as basename3, join as join17 } from "path";
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 existsSync12, readFileSync as readFileSync13, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
9098
- import { isAbsolute, join as join16, resolve as resolve4 } from "path";
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 isAbsolute(path) ? path : resolve4(root, path);
10487
+ return isAbsolute2(path) ? path : resolve6(root, path);
9107
10488
  }
9108
10489
  function discoverChildren(root) {
9109
10490
  let names;
9110
10491
  try {
9111
- names = readdirSync2(root);
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 = join16(root, name);
10499
+ const dir = join19(root, name);
9119
10500
  try {
9120
- if (!statSync3(dir).isDirectory()) continue;
10501
+ if (!statSync4(dir).isDirectory()) continue;
9121
10502
  } catch {
9122
10503
  continue;
9123
10504
  }
9124
- if (existsSync12(join16(dir, ".git")) || committedBindingPath(dir)) out.push(name);
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 (!existsSync12(path)) return null;
10510
+ if (!existsSync13(path)) return null;
9130
10511
  let parsed;
9131
10512
  try {
9132
- parsed = JSON.parse(readFileSync13(path, "utf8"));
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 ?? basename3(process.cwd());
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: basename3(process.cwd()) });
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 (!existsSync13(dir)) {
10879
+ if (!existsSync14(dir)) {
9499
10880
  return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
9500
10881
  }
9501
- if (existsSync13(join17(dir, ".sechroom.json"))) {
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: basename3(entry.path)
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 = join17(root, ".sechroom", "repos.json");
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 homedir5 } from "os";
10107
- import { join as join18 } from "path";
10108
- import { existsSync as existsSync14, readFileSync as readFileSync14, rmSync as rmSync5 } from "fs";
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 = () => join18(process.cwd(), ".claude", "skills");
10111
- var globalSkillsDir = () => join18(homedir5(), ".claude", "skills");
10112
- var localAgentsDir = () => join18(process.cwd(), ".claude", "agents");
10113
- var globalAgentsDir = () => join18(homedir5(), ".claude", "agents");
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 = join18(dir, SKILLS_LOCK2);
10117
- if (!existsSync14(lockPath)) return removed;
11497
+ const lockPath = join21(dir, SKILLS_LOCK2);
11498
+ if (!existsSync15(lockPath)) return removed;
10118
11499
  try {
10119
- const lock = JSON.parse(readFileSync14(lockPath, "utf8"));
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 = join18(dir, name);
10123
- if (existsSync14(p)) {
10124
- rmSync5(p, { recursive: true, force: true });
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
- rmSync5(lockPath, { force: true });
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 = join18(process.cwd(), ".sechroom");
10169
- if (existsSync14(stateDir)) {
10170
- rmSync5(stateDir, { recursive: true, force: true });
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 = join18(process.cwd(), ".sechroom.json");
10174
- if (existsSync14(legacyCfg)) {
10175
- rmSync5(legacyCfg, { force: true });
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 = join18(process.cwd(), ".sem");
10179
- if (existsSync14(legacySem)) {
10180
- rmSync5(legacySem, { force: true });
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 existsSync15, mkdirSync as mkdirSync15, statSync as statSync4, writeFileSync as writeFileSync14 } from "fs";
10206
- import { join as join19 } from "path";
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 join19(process.cwd(), filename);
10215
- const looksLikeDir = output.endsWith("/") || existsSync15(output) && statSync4(output).isDirectory();
11595
+ if (!output) return join22(process.cwd(), filename);
11596
+ const looksLikeDir = output.endsWith("/") || existsSync16(output) && statSync5(output).isDirectory();
10216
11597
  if (looksLikeDir) {
10217
- mkdirSync15(output, { recursive: true });
10218
- return join19(output, filename);
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
- writeFileSync14(path, buf);
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 existsSync16 } from "fs";
10424
- import { dirname as dirname13, join as join20, resolve as resolve5 } from "path";
10425
- var DEFAULT_MANIFEST = join20(".sechroom", "repos.json");
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 (!existsSync16(dir)) {
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 = resolve5(opts.manifest);
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 = dirname13(dirname13(manifestPath));
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
- readFileSync15(new URL("../package.json", import.meta.url), "utf8")
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(