@sechroom/cli 2026.6.238-rc.10da7380 → 2026.7.1

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 +513 -341
  2. package/package.json +4 -3
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 readFileSync8 } from "fs";
4
+ import { readFileSync as readFileSync10 } from "fs";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/auth.ts
@@ -433,6 +433,7 @@ async function requireToken(cfg) {
433
433
  import createClient from "openapi-fetch";
434
434
 
435
435
  // src/ui.ts
436
+ import * as clack from "@clack/prompts";
436
437
  var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
437
438
  var quiet = false;
438
439
  function setQuiet(q) {
@@ -458,7 +459,7 @@ var err = (s) => style.red(s);
458
459
  function active() {
459
460
  return !quiet && Boolean(process.stderr.isTTY);
460
461
  }
461
- function spinner(text) {
462
+ function spinner(text2) {
462
463
  if (!active()) {
463
464
  return { succeed() {
464
465
  }, fail() {
@@ -468,9 +469,9 @@ function spinner(text) {
468
469
  let i = 0;
469
470
  const render = () => {
470
471
  i = (i + 1) % FRAMES.length;
471
- process.stderr.write(`\r${FRAMES[i]} ${text}`);
472
+ process.stderr.write(`\r${FRAMES[i]} ${text2}`);
472
473
  };
473
- process.stderr.write(`\r${FRAMES[0]} ${text}`);
474
+ process.stderr.write(`\r${FRAMES[0]} ${text2}`);
474
475
  const timer = setInterval(render, 80);
475
476
  timer.unref?.();
476
477
  const clear = () => {
@@ -480,12 +481,12 @@ function spinner(text) {
480
481
  return {
481
482
  succeed(t) {
482
483
  clear();
483
- process.stderr.write(`${ok("\u2713")} ${t ?? text}
484
+ process.stderr.write(`${ok("\u2713")} ${t ?? text2}
484
485
  `);
485
486
  },
486
487
  fail(t) {
487
488
  clear();
488
- process.stderr.write(`${err("\u2717")} ${t ?? text}
489
+ process.stderr.write(`${err("\u2717")} ${t ?? text2}
489
490
  `);
490
491
  },
491
492
  stop: clear
@@ -496,31 +497,24 @@ function canPrompt() {
496
497
  }
497
498
  async function promptYesNo(question) {
498
499
  if (!canPrompt()) return false;
499
- const { createInterface } = await import("readline");
500
- const rl = createInterface({ input: process.stdin, output: process.stderr });
501
- try {
502
- const answer = await new Promise((resolve3) => {
503
- rl.question(`${question} [y/N] `, resolve3);
504
- });
505
- return /^y(es)?$/i.test(answer.trim());
506
- } finally {
507
- rl.close();
508
- }
500
+ const r = await clack.confirm({
501
+ message: question,
502
+ initialValue: false,
503
+ output: process.stderr
504
+ });
505
+ return clack.isCancel(r) ? false : r;
509
506
  }
510
507
  async function promptText(question, def) {
511
508
  if (!canPrompt()) return def ?? "";
512
- const { createInterface } = await import("readline");
513
- const rl = createInterface({ input: process.stdin, output: process.stderr });
514
- try {
515
- const suffix = def ? ` [${def}]` : "";
516
- const answer = await new Promise((resolve3) => {
517
- rl.question(`${question}${suffix} `, resolve3);
518
- });
519
- const trimmed = answer.trim();
520
- return trimmed.length > 0 ? trimmed : def ?? "";
521
- } finally {
522
- rl.close();
523
- }
509
+ const r = await clack.text({
510
+ message: question,
511
+ defaultValue: def,
512
+ placeholder: def,
513
+ output: process.stderr
514
+ });
515
+ if (clack.isCancel(r)) return def ?? "";
516
+ const trimmed = (r ?? "").trim();
517
+ return trimmed.length > 0 ? trimmed : def ?? "";
524
518
  }
525
519
  async function promptSelect(question, choices, def) {
526
520
  if (choices.length === 0) throw new Error("promptSelect: no choices");
@@ -529,74 +523,50 @@ async function promptSelect(question, choices, def) {
529
523
  def !== void 0 ? choices.findIndex((c) => c.value === def) : 0
530
524
  );
531
525
  if (!canPrompt()) return choices[defIdx].value;
532
- const { createInterface } = await import("readline");
533
- const rl = createInterface({ input: process.stdin, output: process.stderr });
534
- try {
535
- process.stderr.write(`${style.bold(question)}
536
- `);
537
- choices.forEach((c, i) => {
538
- const marker = i === defIdx ? style.cyan("\u203A") : " ";
539
- const hint = c.hint ? ` ${style.dim(`\u2014 ${c.hint}`)}` : "";
540
- process.stderr.write(` ${marker} ${style.bold(String(i + 1))}. ${c.label}${hint}
541
- `);
542
- });
543
- const answer = await new Promise((resolve3) => {
544
- rl.question(`Choose ${style.dim(`[${defIdx + 1}]`)} `, resolve3);
545
- });
546
- const trimmed = answer.trim();
547
- if (!trimmed) return choices[defIdx].value;
548
- const n = Number(trimmed);
549
- if (Number.isInteger(n) && n >= 1 && n <= choices.length) return choices[n - 1].value;
550
- const byLabel = choices.find(
551
- (c) => c.label.toLowerCase().startsWith(trimmed.toLowerCase())
552
- );
553
- return byLabel ? byLabel.value : choices[defIdx].value;
554
- } finally {
555
- rl.close();
556
- }
526
+ const r = await clack.select({
527
+ message: question,
528
+ // Cast: clack's Option<T> is a conditional type (primitive vs not) that TS
529
+ // can't verify against a generic T; the runtime shape is correct.
530
+ options: choices.map((c) => ({ value: c.value, label: c.label, hint: c.hint })),
531
+ initialValue: choices[defIdx].value,
532
+ output: process.stderr
533
+ });
534
+ return clack.isCancel(r) ? choices[defIdx].value : r;
557
535
  }
558
536
  async function promptMultiSelect(question, choices, preselected = []) {
559
537
  if (choices.length === 0) return [];
560
- const pre = (v) => preselected.includes(v);
561
- const preValues = () => choices.filter((c) => pre(c.value)).map((c) => c.value);
562
- if (!canPrompt()) return preValues();
563
- const { createInterface } = await import("readline");
564
- const rl = createInterface({ input: process.stdin, output: process.stderr });
565
- try {
566
- process.stderr.write(
567
- `${style.bold(question)} ${style.dim("(numbers or 'all', comma-separated; Enter keeps \u25C9)")}
568
- `
569
- );
570
- choices.forEach((c, i) => {
571
- const box = pre(c.value) ? style.cyan("\u25C9") : "\u25CB";
572
- const hint = c.hint ? ` ${style.dim(`\u2014 ${c.hint}`)}` : "";
573
- process.stderr.write(` ${box} ${style.bold(String(i + 1))}. ${c.label}${hint}
574
- `);
575
- });
576
- const answer = await new Promise((resolve3) => {
577
- rl.question(`Select ${style.dim("[Enter = \u25C9]")} `, resolve3);
578
- });
579
- const trimmed = answer.trim().toLowerCase();
580
- if (!trimmed) return preValues();
581
- if (trimmed === "all") return choices.map((c) => c.value);
582
- const picks = [];
583
- for (const tok of trimmed.split(",").map((t) => t.trim()).filter(Boolean)) {
584
- const n = Number(tok);
585
- if (Number.isInteger(n) && n >= 1 && n <= choices.length) {
586
- picks.push(choices[n - 1].value);
587
- continue;
588
- }
589
- const byLabel = choices.find((c) => c.label.toLowerCase().startsWith(tok));
590
- if (byLabel) picks.push(byLabel.value);
591
- }
592
- const uniq = [...new Set(picks)];
593
- return uniq.length > 0 ? uniq : preValues();
594
- } finally {
595
- rl.close();
596
- }
538
+ const preValues = choices.filter((c) => preselected.includes(c.value)).map((c) => c.value);
539
+ if (!canPrompt()) return preValues;
540
+ const r = await clack.multiselect({
541
+ message: question,
542
+ // Cast: clack's Option<T> is a conditional type (primitive vs not) that TS
543
+ // can't verify against a generic T; the runtime shape is correct.
544
+ options: choices.map((c) => ({ value: c.value, label: c.label, hint: c.hint })),
545
+ initialValues: preValues,
546
+ required: false,
547
+ output: process.stderr
548
+ });
549
+ return clack.isCancel(r) ? preValues : r;
597
550
  }
598
- async function withSpinner(text, fn) {
599
- const s = spinner(text);
551
+ async function promptAutocomplete(question, choices, def) {
552
+ if (choices.length === 0) throw new Error("promptAutocomplete: no choices");
553
+ const defIdx = Math.max(
554
+ 0,
555
+ def !== void 0 ? choices.findIndex((c) => c.value === def) : 0
556
+ );
557
+ if (!canPrompt()) return choices[defIdx].value;
558
+ const r = await clack.autocomplete({
559
+ message: question,
560
+ // Cast: clack's Option<T> is a conditional type (primitive vs not) that TS
561
+ // can't verify against a generic T; the runtime shape is correct.
562
+ options: choices.map((c) => ({ value: c.value, label: c.label, hint: c.hint })),
563
+ placeholder: "Type to filter\u2026",
564
+ output: process.stderr
565
+ });
566
+ return clack.isCancel(r) ? choices[defIdx].value : r;
567
+ }
568
+ async function withSpinner(text2, fn) {
569
+ const s = spinner(text2);
600
570
  try {
601
571
  const result = await fn();
602
572
  s.succeed();
@@ -1521,7 +1491,7 @@ Examples:
1521
1491
  $ sechroom chat replies 1718049600.123456 --surface slack
1522
1492
  $ sechroom chat stop-tracking 1718049600.123456 --surface slack`
1523
1493
  );
1524
- chat.command("send <channelId> <text>").description("Send a message to a channel (POST /chat/channel-messages/{surface})").option("--guild <guildId>", "Discord guild snowflake \u2014 required for --surface discord").option("--memory <memoryId>", "Attach a sechroom memory id").option("--no-track", "Don't capture replies to this message").option("--parent <parentMessage>", "Thread under a parent (Slack thread_ts / Discord message id)").option("--source <source>", "Source / lane stamp (renders an attribution footer)", "cli").option("--as <as>", "Slack only: 'bot' (default) or 'user' (your linked Slack identity)", "bot").action(async (channelId, text, opts, cmd) => {
1494
+ chat.command("send <channelId> <text>").description("Send a message to a channel (POST /chat/channel-messages/{surface})").option("--guild <guildId>", "Discord guild snowflake \u2014 required for --surface discord").option("--memory <memoryId>", "Attach a sechroom memory id").option("--no-track", "Don't capture replies to this message").option("--parent <parentMessage>", "Thread under a parent (Slack thread_ts / Discord message id)").option("--source <source>", "Source / lane stamp (renders an attribution footer)", "cli").option("--as <as>", "Slack only: 'bot' (default) or 'user' (your linked Slack identity)", "bot").action(async (channelId, text2, opts, cmd) => {
1525
1495
  const { surface, ...globals } = cmd.optsWithGlobals();
1526
1496
  const json = Boolean(cmd.optsWithGlobals().json);
1527
1497
  const cfg = resolveConfig(globals);
@@ -1531,7 +1501,7 @@ Examples:
1531
1501
  params: { path: { surface: String(surface) } },
1532
1502
  body: {
1533
1503
  channelId,
1534
- text,
1504
+ text: text2,
1535
1505
  guildId: opts.guild ?? null,
1536
1506
  attachedMemoryId: opts.memory ?? null,
1537
1507
  trackReplies: opts.track,
@@ -1590,13 +1560,13 @@ Examples:
1590
1560
  }
1591
1561
 
1592
1562
  // src/commands/checkpoint.ts
1593
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
1594
- import { dirname as dirname5, join as join8 } from "path";
1563
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
1564
+ import { dirname as dirname6, join as join9 } from "path";
1595
1565
 
1596
1566
  // src/commands/hook.ts
1597
1567
  import { createHash as createHash2 } from "crypto";
1598
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync5 } from "fs";
1599
- import { delimiter, dirname as dirname4, join as join7 } from "path";
1568
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
1569
+ import { dirname as dirname5, join as join8 } from "path";
1600
1570
 
1601
1571
  // src/sem.ts
1602
1572
  import { dirname as dirname2, join as join5 } from "path";
@@ -1666,9 +1636,9 @@ function readLocalSemValues(cwd = process.cwd()) {
1666
1636
  if (existsSync4(next)) return readSem(next)?.values ?? {};
1667
1637
  return {};
1668
1638
  }
1669
- function parseLaneJson(text) {
1639
+ function parseLaneJson(text2) {
1670
1640
  try {
1671
- const parsed = JSON.parse(text);
1641
+ const parsed = JSON.parse(text2);
1672
1642
  const out = {};
1673
1643
  for (const [k, v] of Object.entries(parsed)) {
1674
1644
  if (typeof v === "string") out[k] = v;
@@ -1686,6 +1656,9 @@ function writeSem(values, path = localSemPath()) {
1686
1656
  ensureContinuityScaffold(path);
1687
1657
  return path;
1688
1658
  }
1659
+ function ensureStateDirIgnored(cwd = process.cwd()) {
1660
+ ensureSemIgnored(localSemPath(cwd));
1661
+ }
1689
1662
  var CONTINUITY_FILE_NAME = "continuity.json";
1690
1663
  var CONTINUITY_SCAFFOLD = JSON.stringify(
1691
1664
  {
@@ -1757,6 +1730,10 @@ function ensureSemIgnored(semPath) {
1757
1730
  }
1758
1731
  }
1759
1732
 
1733
+ // src/commands/hook-install.ts
1734
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
1735
+ import { delimiter, dirname as dirname4, join as join7 } from "path";
1736
+
1760
1737
  // src/setup/clients.ts
1761
1738
  import { existsSync as existsSync5 } from "fs";
1762
1739
  import { homedir as homedir3 } from "os";
@@ -1827,6 +1804,144 @@ function detectInstalledClients(cwd) {
1827
1804
  return detected;
1828
1805
  }
1829
1806
 
1807
+ // src/commands/hook-install.ts
1808
+ var CLAUDE_HOOK_COMMANDS = {
1809
+ SessionStart: "sechroom hook session-start",
1810
+ PreCompact: "sechroom hook pre-compact",
1811
+ SessionEnd: "sechroom hook session-end",
1812
+ // WLP telemetry tap (D-WLP-10) — per-turn executor self-report. No-op (exit 0) unless this
1813
+ // checkout is bound to a decomposition+task via `sechroom telemetry bind`, so it's safe to wire
1814
+ // for every Claude install. Claude-only (it parses a Claude Code transcript).
1815
+ Stop: "sechroom telemetry hook"
1816
+ };
1817
+ var CODEX_HOOK_COMMANDS = {
1818
+ SessionStart: "sechroom hook session-start",
1819
+ Stop: "sechroom hook session-end --debounce-minutes 10"
1820
+ };
1821
+ function hasHookCommand(config2, event, command) {
1822
+ const groups = config2.hooks?.[event] ?? [];
1823
+ return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
1824
+ }
1825
+ function mergeHooks(config2, commands) {
1826
+ config2.hooks ??= {};
1827
+ let added = 0;
1828
+ for (const [event, command] of Object.entries(commands)) {
1829
+ if (hasHookCommand(config2, event, command)) continue;
1830
+ const groups = config2.hooks[event] ??= [];
1831
+ groups.push({ hooks: [{ type: "command", command }] });
1832
+ added += 1;
1833
+ }
1834
+ return added;
1835
+ }
1836
+ function readJsonConfig2(path) {
1837
+ if (!existsSync6(path)) return {};
1838
+ const raw = readFileSync4(path, "utf8");
1839
+ if (!raw.trim()) return {};
1840
+ return JSON.parse(raw);
1841
+ }
1842
+ function installHooksJson(path, commands, dryRun) {
1843
+ const existed = existsSync6(path) && readFileSync4(path, "utf8").trim().length > 0;
1844
+ const config2 = readJsonConfig2(path);
1845
+ const added = mergeHooks(config2, commands);
1846
+ if (added === 0 && existed) return { path, status: "current" };
1847
+ if (!dryRun) {
1848
+ mkdirSync5(dirname4(path), { recursive: true });
1849
+ writeFileSync5(path, JSON.stringify(config2, null, 2) + "\n");
1850
+ }
1851
+ return { path, status: existed ? "merged" : "created" };
1852
+ }
1853
+ function installClaudeCommands(claudeDir, commands, dryRun) {
1854
+ return installHooksJson(join7(claudeDir, "settings.json"), commands, dryRun);
1855
+ }
1856
+ function ensureCodexFeaturesHooks(content) {
1857
+ const lines = content.split("\n");
1858
+ const headerIdx = lines.findIndex((l) => l.trim() === "[features]");
1859
+ if (headerIdx === -1) {
1860
+ const base = content.length === 0 || content.endsWith("\n") ? content : content + "\n";
1861
+ return { next: base + "\n[features]\nhooks = true\n", changed: true };
1862
+ }
1863
+ for (let i = headerIdx + 1; i < lines.length; i += 1) {
1864
+ const trimmed = lines[i].trim();
1865
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) break;
1866
+ const m = lines[i].match(/^(\s*)hooks(\s*)=(\s*)(.*)$/);
1867
+ if (!m) continue;
1868
+ const value = m[4].replace(/\s*#.*$/, "").trim();
1869
+ if (value === "true") return { next: content, changed: false };
1870
+ lines[i] = `${m[1]}hooks${m[2]}=${m[3]}true`;
1871
+ return { next: lines.join("\n"), changed: true };
1872
+ }
1873
+ lines.splice(headerIdx + 1, 0, "hooks = true");
1874
+ return { next: lines.join("\n"), changed: true };
1875
+ }
1876
+ function installCodexFeatureFlag(path, dryRun) {
1877
+ const existed = existsSync6(path);
1878
+ const content = existed ? readFileSync4(path, "utf8") : "";
1879
+ const { next, changed } = ensureCodexFeaturesHooks(content);
1880
+ if (!changed) return { path, status: "current" };
1881
+ if (!dryRun) {
1882
+ mkdirSync5(dirname4(path), { recursive: true });
1883
+ writeFileSync5(path, next);
1884
+ }
1885
+ return { path, status: existed ? "merged" : "created" };
1886
+ }
1887
+ function resolveSurfaces(surface, cwd) {
1888
+ if (surface === "claude") return ["claude"];
1889
+ if (surface === "codex") return ["codex"];
1890
+ if (surface === "both") return ["claude", "codex"];
1891
+ if (surface) throw new Error(`--surface must be one of claude | codex | both (got '${surface}')`);
1892
+ const surfaces = detectHookSurfaces(cwd);
1893
+ return surfaces.length > 0 ? surfaces : ["claude", "codex"];
1894
+ }
1895
+ function describe(result, dryRun) {
1896
+ if (result.status === "current") return ` \u2713 ${result.path} (already configured)`;
1897
+ const verb = dryRun ? "would" : result.status === "created" ? "created" : "updated";
1898
+ return ` \u2713 ${result.path} (${dryRun ? `${verb} ${result.status === "created" ? "create" : "update"}` : verb})`;
1899
+ }
1900
+ var HOOK_SURFACE_LABEL = {
1901
+ claude: "Claude Code",
1902
+ codex: "Codex"
1903
+ };
1904
+ function installHookSurfaces(surfaces, opts) {
1905
+ const out = [];
1906
+ for (const surface of surfaces) {
1907
+ if (surface === "claude") {
1908
+ const path = join7(opts.claudeDir, "settings.json");
1909
+ out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
1910
+ } else {
1911
+ const hooksJson = installHooksJson(join7(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
1912
+ const featureFlag = installCodexFeatureFlag(join7(opts.codexHome, "config.toml"), opts.dryRun);
1913
+ out.push({ surface, results: [hooksJson, featureFlag] });
1914
+ }
1915
+ }
1916
+ return out;
1917
+ }
1918
+ function detectHookSurfaces(cwd) {
1919
+ const detected = detectInstalledClients(cwd);
1920
+ const surfaces = [];
1921
+ if (detected.includes("claude-code")) surfaces.push("claude");
1922
+ if (detected.includes("codex")) surfaces.push("codex");
1923
+ return surfaces;
1924
+ }
1925
+ function isSechroomOnPath() {
1926
+ const pathEnv = process.env.PATH ?? "";
1927
+ if (!pathEnv) return false;
1928
+ const names = process.platform === "win32" ? ["sechroom.cmd", "sechroom.exe", "sechroom.bat", "sechroom"] : ["sechroom"];
1929
+ for (const dir of pathEnv.split(delimiter)) {
1930
+ if (!dir) continue;
1931
+ for (const name of names) {
1932
+ if (existsSync6(join7(dir, name))) return true;
1933
+ }
1934
+ }
1935
+ return false;
1936
+ }
1937
+ function warnIfSechroomNotOnPath(write = (s) => void process.stderr.write(s)) {
1938
+ if (isSechroomOnPath()) return false;
1939
+ write(
1940
+ "\n\u26A0 `sechroom` isn't on your PATH. The hooks run a bare `sechroom hook \u2026` command\n when your agent fires them, so a non-global install (npx / local) will fail at\n that point. Install globally so the command resolves:\n npm i -g @sechroom/cli\n"
1941
+ );
1942
+ return true;
1943
+ }
1944
+
1830
1945
  // src/commands/hook.ts
1831
1946
  async function readStdin() {
1832
1947
  if (process.stdin.isTTY) return "";
@@ -1851,13 +1966,13 @@ function resolveLane(flagLane, cwd) {
1851
1966
  if (!base) return void 0;
1852
1967
  return applyWorktreeLaneSuffix(base, start);
1853
1968
  }
1854
- var INTENT_FILE = join7(".sechroom", "continuity.json");
1969
+ var INTENT_FILE = join8(".sechroom", "continuity.json");
1855
1970
  function resolveIntentPath(start) {
1856
1971
  let dir = start;
1857
1972
  for (; ; ) {
1858
- const candidate = join7(dir, INTENT_FILE);
1859
- if (existsSync6(candidate)) return candidate;
1860
- const parent = dirname4(dir);
1973
+ const candidate = join8(dir, INTENT_FILE);
1974
+ if (existsSync7(candidate)) return candidate;
1975
+ const parent = dirname5(dir);
1861
1976
  if (parent === dir) return void 0;
1862
1977
  dir = parent;
1863
1978
  }
@@ -1866,7 +1981,7 @@ function readIntent(start) {
1866
1981
  const path = resolveIntentPath(start);
1867
1982
  if (!path) return void 0;
1868
1983
  try {
1869
- return JSON.parse(readFileSync4(path, "utf8"));
1984
+ return JSON.parse(readFileSync5(path, "utf8"));
1870
1985
  } catch {
1871
1986
  return void 0;
1872
1987
  }
@@ -1908,14 +2023,14 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
1908
2023
  }
1909
2024
  function ledgerPath(start) {
1910
2025
  const intent = resolveIntentPath(start);
1911
- const dir = intent ? dirname4(intent) : join7(start, ".sechroom");
1912
- return join7(dir, ".checkpoint-state.json");
2026
+ const dir = intent ? dirname5(intent) : join8(start, ".sechroom");
2027
+ return join8(dir, ".checkpoint-state.json");
1913
2028
  }
1914
2029
  function readLedger(start) {
1915
2030
  try {
1916
2031
  const p = ledgerPath(start);
1917
- if (!existsSync6(p)) return {};
1918
- return JSON.parse(readFileSync4(p, "utf8"));
2032
+ if (!existsSync7(p)) return {};
2033
+ return JSON.parse(readFileSync5(p, "utf8"));
1919
2034
  } catch {
1920
2035
  return {};
1921
2036
  }
@@ -1962,13 +2077,13 @@ function recordPush(start, intent) {
1962
2077
  } catch {
1963
2078
  mtimeMs = void 0;
1964
2079
  }
1965
- mkdirSync5(dirname4(p), { recursive: true });
2080
+ mkdirSync6(dirname5(p), { recursive: true });
1966
2081
  const ledger = {
1967
2082
  lastEpochMs: Date.now(),
1968
2083
  lastMtimeMs: mtimeMs,
1969
2084
  lastHash: intentHash(intent)
1970
2085
  };
1971
- writeFileSync5(p, JSON.stringify(ledger) + "\n");
2086
+ writeFileSync6(p, JSON.stringify(ledger) + "\n");
1972
2087
  } catch {
1973
2088
  }
1974
2089
  }
@@ -2008,135 +2123,6 @@ function emitSessionStart(additionalContext) {
2008
2123
  }) + "\n"
2009
2124
  );
2010
2125
  }
2011
- var CLAUDE_HOOK_COMMANDS = {
2012
- SessionStart: "sechroom hook session-start",
2013
- PreCompact: "sechroom hook pre-compact",
2014
- SessionEnd: "sechroom hook session-end"
2015
- };
2016
- var CODEX_HOOK_COMMANDS = {
2017
- SessionStart: "sechroom hook session-start",
2018
- Stop: "sechroom hook session-end --debounce-minutes 10"
2019
- };
2020
- function hasHookCommand(config2, event, command) {
2021
- const groups = config2.hooks?.[event] ?? [];
2022
- return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
2023
- }
2024
- function mergeHooks(config2, commands) {
2025
- config2.hooks ??= {};
2026
- let added = 0;
2027
- for (const [event, command] of Object.entries(commands)) {
2028
- if (hasHookCommand(config2, event, command)) continue;
2029
- const groups = config2.hooks[event] ??= [];
2030
- groups.push({ hooks: [{ type: "command", command }] });
2031
- added += 1;
2032
- }
2033
- return added;
2034
- }
2035
- function readJsonConfig2(path) {
2036
- if (!existsSync6(path)) return {};
2037
- const raw = readFileSync4(path, "utf8");
2038
- if (!raw.trim()) return {};
2039
- return JSON.parse(raw);
2040
- }
2041
- function installHooksJson(path, commands, dryRun) {
2042
- const existed = existsSync6(path) && readFileSync4(path, "utf8").trim().length > 0;
2043
- const config2 = readJsonConfig2(path);
2044
- const added = mergeHooks(config2, commands);
2045
- if (added === 0 && existed) return { path, status: "current" };
2046
- if (!dryRun) {
2047
- mkdirSync5(dirname4(path), { recursive: true });
2048
- writeFileSync5(path, JSON.stringify(config2, null, 2) + "\n");
2049
- }
2050
- return { path, status: existed ? "merged" : "created" };
2051
- }
2052
- function ensureCodexFeaturesHooks(content) {
2053
- const lines = content.split("\n");
2054
- const headerIdx = lines.findIndex((l) => l.trim() === "[features]");
2055
- if (headerIdx === -1) {
2056
- const base = content.length === 0 || content.endsWith("\n") ? content : content + "\n";
2057
- return { next: base + "\n[features]\nhooks = true\n", changed: true };
2058
- }
2059
- for (let i = headerIdx + 1; i < lines.length; i += 1) {
2060
- const trimmed = lines[i].trim();
2061
- if (trimmed.startsWith("[") && trimmed.endsWith("]")) break;
2062
- const m = lines[i].match(/^(\s*)hooks(\s*)=(\s*)(.*)$/);
2063
- if (!m) continue;
2064
- const value = m[4].replace(/\s*#.*$/, "").trim();
2065
- if (value === "true") return { next: content, changed: false };
2066
- lines[i] = `${m[1]}hooks${m[2]}=${m[3]}true`;
2067
- return { next: lines.join("\n"), changed: true };
2068
- }
2069
- lines.splice(headerIdx + 1, 0, "hooks = true");
2070
- return { next: lines.join("\n"), changed: true };
2071
- }
2072
- function installCodexFeatureFlag(path, dryRun) {
2073
- const existed = existsSync6(path);
2074
- const content = existed ? readFileSync4(path, "utf8") : "";
2075
- const { next, changed } = ensureCodexFeaturesHooks(content);
2076
- if (!changed) return { path, status: "current" };
2077
- if (!dryRun) {
2078
- mkdirSync5(dirname4(path), { recursive: true });
2079
- writeFileSync5(path, next);
2080
- }
2081
- return { path, status: existed ? "merged" : "created" };
2082
- }
2083
- function resolveSurfaces(surface, cwd) {
2084
- if (surface === "claude") return ["claude"];
2085
- if (surface === "codex") return ["codex"];
2086
- if (surface === "both") return ["claude", "codex"];
2087
- if (surface) throw new Error(`--surface must be one of claude | codex | both (got '${surface}')`);
2088
- const surfaces = detectHookSurfaces(cwd);
2089
- return surfaces.length > 0 ? surfaces : ["claude", "codex"];
2090
- }
2091
- function describe(result, dryRun) {
2092
- if (result.status === "current") return ` \u2713 ${result.path} (already configured)`;
2093
- const verb = dryRun ? "would" : result.status === "created" ? "created" : "updated";
2094
- return ` \u2713 ${result.path} (${dryRun ? `${verb} ${result.status === "created" ? "create" : "update"}` : verb})`;
2095
- }
2096
- var HOOK_SURFACE_LABEL = {
2097
- claude: "Claude Code",
2098
- codex: "Codex"
2099
- };
2100
- function installHookSurfaces(surfaces, opts) {
2101
- const out = [];
2102
- for (const surface of surfaces) {
2103
- if (surface === "claude") {
2104
- const path = join7(opts.claudeDir, "settings.json");
2105
- out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
2106
- } else {
2107
- const hooksJson = installHooksJson(join7(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
2108
- const featureFlag = installCodexFeatureFlag(join7(opts.codexHome, "config.toml"), opts.dryRun);
2109
- out.push({ surface, results: [hooksJson, featureFlag] });
2110
- }
2111
- }
2112
- return out;
2113
- }
2114
- function detectHookSurfaces(cwd) {
2115
- const detected = detectInstalledClients(cwd);
2116
- const surfaces = [];
2117
- if (detected.includes("claude-code")) surfaces.push("claude");
2118
- if (detected.includes("codex")) surfaces.push("codex");
2119
- return surfaces;
2120
- }
2121
- function isSechroomOnPath() {
2122
- const pathEnv = process.env.PATH ?? "";
2123
- if (!pathEnv) return false;
2124
- const names = process.platform === "win32" ? ["sechroom.cmd", "sechroom.exe", "sechroom.bat", "sechroom"] : ["sechroom"];
2125
- for (const dir of pathEnv.split(delimiter)) {
2126
- if (!dir) continue;
2127
- for (const name of names) {
2128
- if (existsSync6(join7(dir, name))) return true;
2129
- }
2130
- }
2131
- return false;
2132
- }
2133
- function warnIfSechroomNotOnPath(write = (s) => void process.stderr.write(s)) {
2134
- if (isSechroomOnPath()) return false;
2135
- write(
2136
- "\n\u26A0 `sechroom` isn't on your PATH. The hooks run a bare `sechroom hook \u2026` command\n when your agent fires them, so a non-global install (npx / local) will fail at\n that point. Install globally so the command resolves:\n npm i -g @sechroom/cli\n"
2137
- );
2138
- return true;
2139
- }
2140
2126
  function registerHook(program2) {
2141
2127
  const hook = program2.command("hook").description("Agent-lifecycle hook adapter (Claude Code / Codex) \u2014 bridges hooks to continuity");
2142
2128
  hook.addHelpText(
@@ -2344,10 +2330,10 @@ Examples:
2344
2330
  const client = await makeClient(cfg);
2345
2331
  return client.POST("/continuity/snapshots", { body });
2346
2332
  });
2347
- const path = resolveIntentPath(cwd) ?? join8(cwd, INTENT_FILE);
2333
+ const path = resolveIntentPath(cwd) ?? join9(cwd, INTENT_FILE);
2348
2334
  const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
2349
- mkdirSync6(dirname5(path), { recursive: true });
2350
- writeFileSync6(path, JSON.stringify(fileBody, null, 2) + "\n");
2335
+ mkdirSync7(dirname6(path), { recursive: true });
2336
+ writeFileSync7(path, JSON.stringify(fileBody, null, 2) + "\n");
2351
2337
  recordPush(cwd, merged);
2352
2338
  if (json) {
2353
2339
  emit({ snapshotId: data.snapshotId, lane, scope, file: path }, true);
@@ -3031,8 +3017,8 @@ Examples:
3031
3017
 
3032
3018
  // src/setup/apply.ts
3033
3019
  import { createHash as createHash3 } from "crypto";
3034
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync7, existsSync as existsSync7 } from "fs";
3035
- import { dirname as dirname6 } from "path";
3020
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync8, existsSync as existsSync8 } from "fs";
3021
+ import { dirname as dirname7 } from "path";
3036
3022
  var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
3037
3023
  var MARKER_END = "<!-- @sechroom/cli:end";
3038
3024
  function normalizeBody(s) {
@@ -3085,22 +3071,22 @@ function parseManagedBlock(content, block) {
3085
3071
  return null;
3086
3072
  }
3087
3073
  function ensureDir2(path) {
3088
- mkdirSync7(dirname6(path), { recursive: true });
3074
+ mkdirSync8(dirname7(path), { recursive: true });
3089
3075
  }
3090
3076
  function readOr(path, fallback) {
3091
3077
  try {
3092
- return readFileSync5(path, "utf8");
3078
+ return readFileSync6(path, "utf8");
3093
3079
  } catch {
3094
3080
  return fallback;
3095
3081
  }
3096
3082
  }
3097
3083
  function mergeMcpJson(path, snippet, dryRun) {
3098
3084
  const incoming = JSON.parse(snippet);
3099
- const existed = existsSync7(path);
3085
+ const existed = existsSync8(path);
3100
3086
  let current = {};
3101
3087
  if (existed) {
3102
3088
  try {
3103
- current = JSON.parse(readFileSync5(path, "utf8"));
3089
+ current = JSON.parse(readFileSync6(path, "utf8"));
3104
3090
  } catch {
3105
3091
  return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
3106
3092
  }
@@ -3108,26 +3094,26 @@ function mergeMcpJson(path, snippet, dryRun) {
3108
3094
  current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
3109
3095
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
3110
3096
  ensureDir2(path);
3111
- writeFileSync7(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
3097
+ writeFileSync8(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
3112
3098
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
3113
3099
  }
3114
3100
  function mergeCodexToml(path, snippet, dryRun) {
3115
- const existed = existsSync7(path);
3101
+ const existed = existsSync8(path);
3116
3102
  let body = readOr(path, "");
3117
3103
  body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
3118
3104
  const trimmed = body.trim();
3119
3105
  const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
3120
3106
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
3121
3107
  ensureDir2(path);
3122
- writeFileSync7(path, next, { mode: 384 });
3108
+ writeFileSync8(path, next, { mode: 384 });
3123
3109
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
3124
3110
  }
3125
3111
  function writeInstructionBlock(path, write, dryRun) {
3126
- const existed = existsSync7(path);
3112
+ const existed = existsSync8(path);
3127
3113
  const next = computeBlockFile(readOr(path, ""), write);
3128
3114
  if (dryRun) return { kind: "instruction", path, status: "dry-run" };
3129
3115
  ensureDir2(path);
3130
- writeFileSync7(path, next);
3116
+ writeFileSync8(path, next);
3131
3117
  return { kind: "instruction", path, status: existed ? "merged" : "created" };
3132
3118
  }
3133
3119
  function computeBlockFile(current, write) {
@@ -3168,7 +3154,7 @@ function applyBlock(path, write, mode, dryRun) {
3168
3154
  const next = computeBlockFile(current, write);
3169
3155
  if (!dryRun) {
3170
3156
  ensureDir2(proposedPath);
3171
- writeFileSync7(proposedPath, next);
3157
+ writeFileSync8(proposedPath, next);
3172
3158
  }
3173
3159
  return {
3174
3160
  kind: "instruction",
@@ -3292,8 +3278,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
3292
3278
  }
3293
3279
 
3294
3280
  // src/setup/skills-offer.ts
3295
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
3296
- import { join as join9 } from "path";
3281
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
3282
+ import { join as join10 } from "path";
3297
3283
 
3298
3284
  // src/setup/lane-pin.ts
3299
3285
  var CODE_LANE_PREFIX_BY_CLIENT = {
@@ -3409,8 +3395,8 @@ Found ${summary} available to you for ${surface}.
3409
3395
  if (skills.length > 0) {
3410
3396
  const written = [];
3411
3397
  for (const s of skills) {
3412
- mkdirSync8(join9(sDir, s.name), { recursive: true });
3413
- writeFileSync8(join9(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
3398
+ mkdirSync9(join10(sDir, s.name), { recursive: true });
3399
+ writeFileSync9(join10(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
3414
3400
  written.push(s.name);
3415
3401
  }
3416
3402
  recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
@@ -3418,11 +3404,11 @@ Found ${summary} available to you for ${surface}.
3418
3404
  `);
3419
3405
  }
3420
3406
  if (agents.length > 0) {
3421
- mkdirSync8(aDir, { recursive: true });
3407
+ mkdirSync9(aDir, { recursive: true });
3422
3408
  const written = [];
3423
3409
  for (const a of agents) {
3424
3410
  const file = `${a.name}.md`;
3425
- writeFileSync8(join9(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
3411
+ writeFileSync9(join10(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
3426
3412
  written.push(file);
3427
3413
  }
3428
3414
  recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
@@ -3651,20 +3637,20 @@ Examples:
3651
3637
  fail("no workspace \u2014 pass --workspace <id> or bind one (`sechroom config set --local workspaceId <id>`).");
3652
3638
  const kind = String(opts.kind).toLowerCase() === "standard" ? "standard" : "reference";
3653
3639
  const body = typeof opts.body === "string" && opts.body.trim().length > 0 ? opts.body.trim() : "_TODO: write this section, then edit the memo and re-run the regen._";
3654
- const text = `# ${title}
3640
+ const text2 = `# ${title}
3655
3641
 
3656
3642
  ${body}
3657
3643
  `;
3658
3644
  const tags = ["agent-setup-bundle", "scope:sechroom", `kind:${kind}`, "archetype:document"];
3659
3645
  if (opts.dryRun) {
3660
- emit({ dryRun: true, workspaceId, title, kind, tags, text }, json);
3646
+ emit({ dryRun: true, workspaceId, title, kind, tags, text: text2 }, json);
3661
3647
  return;
3662
3648
  }
3663
3649
  const data = await runApi("Authoring convention memo", async () => {
3664
3650
  const client = await makeClient(cfg);
3665
3651
  return client.POST("/memories", {
3666
3652
  body: {
3667
- text,
3653
+ text: text2,
3668
3654
  type: kind,
3669
3655
  content: "{}",
3670
3656
  confidence: 1,
@@ -3805,13 +3791,13 @@ Wired to namespace '${slug}'. Restart your AI client (or reload MCP) to pick it
3805
3791
  }
3806
3792
 
3807
3793
  // src/commands/onboard.ts
3808
- import { existsSync as existsSync9 } from "fs";
3809
- import { basename as basename2, join as join11 } from "path";
3794
+ import { existsSync as existsSync10 } from "fs";
3795
+ import { basename as basename2, join as join12 } from "path";
3810
3796
 
3811
3797
  // src/commands/fanout.ts
3812
3798
  import { spawnSync } from "child_process";
3813
- import { existsSync as existsSync8, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
3814
- import { isAbsolute, join as join10, resolve } from "path";
3799
+ import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
3800
+ import { isAbsolute, join as join11, resolve } from "path";
3815
3801
  var ICON = {
3816
3802
  refresh: "\u21BB",
3817
3803
  bind: "+",
@@ -3831,21 +3817,21 @@ function discoverChildren(root) {
3831
3817
  const out = [];
3832
3818
  for (const name of names.sort()) {
3833
3819
  if (name.startsWith(".") || name === "node_modules") continue;
3834
- const dir = join10(root, name);
3820
+ const dir = join11(root, name);
3835
3821
  try {
3836
3822
  if (!statSync3(dir).isDirectory()) continue;
3837
3823
  } catch {
3838
3824
  continue;
3839
3825
  }
3840
- if (existsSync8(join10(dir, ".git")) || committedBindingPath(dir)) out.push(name);
3826
+ if (existsSync9(join11(dir, ".git")) || committedBindingPath(dir)) out.push(name);
3841
3827
  }
3842
3828
  return out;
3843
3829
  }
3844
3830
  function readManifest(path) {
3845
- if (!existsSync8(path)) return null;
3831
+ if (!existsSync9(path)) return null;
3846
3832
  let parsed;
3847
3833
  try {
3848
- parsed = JSON.parse(readFileSync6(path, "utf8"));
3834
+ parsed = JSON.parse(readFileSync7(path, "utf8"));
3849
3835
  } catch (err2) {
3850
3836
  throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
3851
3837
  }
@@ -4029,28 +4015,17 @@ async function pickWorkspace(client, opts = {}) {
4029
4015
  if (candidates.length === 0) candidates = all;
4030
4016
  const dirToks = new Set(nameTokens(dirName));
4031
4017
  const isMatch = (w) => nameTokens(w.name).some((t) => dirToks.has(t));
4032
- const suggestions = candidates.filter(isMatch);
4033
- let pool = candidates;
4034
- if (candidates.length > 12 && suggestions.length === 0) {
4035
- const q = (await promptText(`Filter ${candidates.length} workspaces (substring, Enter to list all)?`, "")).trim().toLowerCase();
4036
- if (q) {
4037
- const hits = candidates.filter((w) => `${w.name} ${workspacePath(w, byId)}`.toLowerCase().includes(q));
4038
- if (hits.length > 0) pool = hits;
4039
- else process.stderr.write(`no match for "${q}" \u2014 listing all
4040
- `);
4041
- }
4042
- }
4043
4018
  const SKIP = "__skip__";
4044
4019
  const byPath = (a, b) => workspacePath(a, byId).localeCompare(workspacePath(b, byId));
4045
- const matched = pool.filter(isMatch).sort(byPath);
4046
- const rest = pool.filter((w) => !isMatch(w)).sort(byPath);
4020
+ const matched = candidates.filter(isMatch).sort(byPath);
4021
+ const rest = candidates.filter((w) => !isMatch(w)).sort(byPath);
4047
4022
  const choices = [
4048
4023
  ...matched.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: style.dim(`matches "${dirName}"`) })),
4049
4024
  ...rest.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: w.id })),
4050
4025
  { label: style.dim("skip \u2014 don't bind a workspace"), value: SKIP, hint: void 0 }
4051
4026
  ];
4052
4027
  const defaultValue = matched.length === 1 ? matched[0].id : SKIP;
4053
- const chosen = await promptSelect(promptLabel, choices, defaultValue);
4028
+ const chosen = candidates.length > 12 ? await promptAutocomplete(promptLabel, choices, defaultValue) : await promptSelect(promptLabel, choices, defaultValue);
4054
4029
  if (chosen === SKIP) return void 0;
4055
4030
  const picked = byId.get(chosen);
4056
4031
  const collisions = all.filter((w) => w.id !== picked.id && namesCollide(w.name, picked.name));
@@ -4222,10 +4197,10 @@ async function chooseScope(scopeFlag, yes) {
4222
4197
  }
4223
4198
  async function planRecurseChild(entry, root, client, opts) {
4224
4199
  const dir = resolveChildDir(entry.path, root);
4225
- if (!existsSync9(dir)) {
4200
+ if (!existsSync10(dir)) {
4226
4201
  return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
4227
4202
  }
4228
- if (existsSync9(join11(dir, ".sechroom.json"))) {
4203
+ if (existsSync10(join12(dir, ".sechroom.json"))) {
4229
4204
  return {
4230
4205
  label: entry.path,
4231
4206
  dir,
@@ -4298,7 +4273,7 @@ This fan-out will pin the same lane in every repo:
4298
4273
  async function runRecurse(cfg, g, opts) {
4299
4274
  const { yes, dryRun, json } = opts;
4300
4275
  const root = process.cwd();
4301
- const manifestPath = join11(root, ".sechroom", "repos.json");
4276
+ const manifestPath = join12(root, ".sechroom", "repos.json");
4302
4277
  const fromManifest = readManifest(manifestPath);
4303
4278
  const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
4304
4279
  const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
@@ -4810,23 +4785,23 @@ Examples:
4810
4785
 
4811
4786
  // src/commands/reset.ts
4812
4787
  import { homedir as homedir4 } from "os";
4813
- import { join as join12 } from "path";
4814
- import { existsSync as existsSync10, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
4788
+ import { join as join13 } from "path";
4789
+ import { existsSync as existsSync11, readFileSync as readFileSync8, rmSync as rmSync3 } from "fs";
4815
4790
  var SKILLS_LOCK2 = ".sechroom-skills.json";
4816
- var localSkillsDir = () => join12(process.cwd(), ".claude", "skills");
4817
- var globalSkillsDir = () => join12(homedir4(), ".claude", "skills");
4818
- var localAgentsDir = () => join12(process.cwd(), ".claude", "agents");
4819
- var globalAgentsDir = () => join12(homedir4(), ".claude", "agents");
4791
+ var localSkillsDir = () => join13(process.cwd(), ".claude", "skills");
4792
+ var globalSkillsDir = () => join13(homedir4(), ".claude", "skills");
4793
+ var localAgentsDir = () => join13(process.cwd(), ".claude", "agents");
4794
+ var globalAgentsDir = () => join13(homedir4(), ".claude", "agents");
4820
4795
  function removeMaterialisedSkills(dir) {
4821
4796
  const removed = [];
4822
- const lockPath = join12(dir, SKILLS_LOCK2);
4823
- if (!existsSync10(lockPath)) return removed;
4797
+ const lockPath = join13(dir, SKILLS_LOCK2);
4798
+ if (!existsSync11(lockPath)) return removed;
4824
4799
  try {
4825
- const lock = JSON.parse(readFileSync7(lockPath, "utf8"));
4800
+ const lock = JSON.parse(readFileSync8(lockPath, "utf8"));
4826
4801
  for (const entry of Object.values(lock)) {
4827
4802
  for (const name of entry.skills ?? []) {
4828
- const p = join12(dir, name);
4829
- if (existsSync10(p)) {
4803
+ const p = join13(dir, name);
4804
+ if (existsSync11(p)) {
4830
4805
  rmSync3(p, { recursive: true, force: true });
4831
4806
  removed.push(p);
4832
4807
  }
@@ -4871,18 +4846,18 @@ function registerReset(program2) {
4871
4846
  }
4872
4847
  }
4873
4848
  const removed = [];
4874
- const stateDir = join12(process.cwd(), ".sechroom");
4875
- if (existsSync10(stateDir)) {
4849
+ const stateDir = join13(process.cwd(), ".sechroom");
4850
+ if (existsSync11(stateDir)) {
4876
4851
  rmSync3(stateDir, { recursive: true, force: true });
4877
4852
  removed.push(stateDir);
4878
4853
  }
4879
- const legacyCfg = join12(process.cwd(), ".sechroom.json");
4880
- if (existsSync10(legacyCfg)) {
4854
+ const legacyCfg = join13(process.cwd(), ".sechroom.json");
4855
+ if (existsSync11(legacyCfg)) {
4881
4856
  rmSync3(legacyCfg, { force: true });
4882
4857
  removed.push(legacyCfg);
4883
4858
  }
4884
- const legacySem = join12(process.cwd(), ".sem");
4885
- if (existsSync10(legacySem)) {
4859
+ const legacySem = join13(process.cwd(), ".sem");
4860
+ if (existsSync11(legacySem)) {
4886
4861
  rmSync3(legacySem, { force: true });
4887
4862
  removed.push(legacySem);
4888
4863
  }
@@ -4908,8 +4883,8 @@ function registerReset(program2) {
4908
4883
  }
4909
4884
 
4910
4885
  // src/commands/skills.ts
4911
- import { existsSync as existsSync11, mkdirSync as mkdirSync9, statSync as statSync4, writeFileSync as writeFileSync9 } from "fs";
4912
- import { join as join13 } from "path";
4886
+ import { existsSync as existsSync12, mkdirSync as mkdirSync10, statSync as statSync4, writeFileSync as writeFileSync10 } from "fs";
4887
+ import { join as join14 } from "path";
4913
4888
  function filenameFromDisposition(header) {
4914
4889
  if (!header) return void 0;
4915
4890
  const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
@@ -4917,11 +4892,11 @@ function filenameFromDisposition(header) {
4917
4892
  }
4918
4893
  function resolveOutputPath(output, serverFilename) {
4919
4894
  const filename = serverFilename || "skills.zip";
4920
- if (!output) return join13(process.cwd(), filename);
4921
- const looksLikeDir = output.endsWith("/") || existsSync11(output) && statSync4(output).isDirectory();
4895
+ if (!output) return join14(process.cwd(), filename);
4896
+ const looksLikeDir = output.endsWith("/") || existsSync12(output) && statSync4(output).isDirectory();
4922
4897
  if (looksLikeDir) {
4923
- mkdirSync9(output, { recursive: true });
4924
- return join13(output, filename);
4898
+ mkdirSync10(output, { recursive: true });
4899
+ return join14(output, filename);
4925
4900
  }
4926
4901
  return output;
4927
4902
  }
@@ -4952,7 +4927,7 @@ async function downloadZip(label, call, output) {
4952
4927
  const buf = Buffer.from(res.data);
4953
4928
  const filename = filenameFromDisposition(res.response.headers.get("content-disposition")) ?? "skills.zip";
4954
4929
  const path = resolveOutputPath(output, filename);
4955
- writeFileSync9(path, buf);
4930
+ writeFileSync10(path, buf);
4956
4931
  return { path, bytes: buf.length, filename };
4957
4932
  }
4958
4933
  function registerSkills(program2) {
@@ -5120,12 +5095,12 @@ Examples:
5120
5095
  }
5121
5096
 
5122
5097
  // src/commands/sweep.ts
5123
- import { existsSync as existsSync12 } from "fs";
5124
- import { dirname as dirname7, join as join14, resolve as resolve2 } from "path";
5125
- var DEFAULT_MANIFEST = join14(".sechroom", "repos.json");
5098
+ import { existsSync as existsSync13 } from "fs";
5099
+ import { dirname as dirname8, join as join15, resolve as resolve2 } from "path";
5100
+ var DEFAULT_MANIFEST = join15(".sechroom", "repos.json");
5126
5101
  function planEntry(entry, root) {
5127
5102
  const dir = resolveChildDir(entry.path, root);
5128
- if (!existsSync12(dir)) {
5103
+ if (!existsSync13(dir)) {
5129
5104
  return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
5130
5105
  }
5131
5106
  if (committedBindingPath(dir)) {
@@ -5201,7 +5176,7 @@ Examples:
5201
5176
  `);
5202
5177
  return;
5203
5178
  }
5204
- const root = dirname7(dirname7(manifestPath));
5179
+ const root = dirname8(dirname8(manifestPath));
5205
5180
  const plans = repos.map((entry) => planEntry(entry, root));
5206
5181
  if (!json) {
5207
5182
  process.stderr.write(
@@ -5219,6 +5194,14 @@ Examples:
5219
5194
  }
5220
5195
 
5221
5196
  // src/commands/telemetry.ts
5197
+ import {
5198
+ existsSync as existsSync14,
5199
+ mkdirSync as mkdirSync11,
5200
+ readFileSync as readFileSync9,
5201
+ rmSync as rmSync4,
5202
+ writeFileSync as writeFileSync11
5203
+ } from "fs";
5204
+ import { dirname as dirname9, join as join16 } from "path";
5222
5205
  function registerTelemetry(program2) {
5223
5206
  const telemetry = program2.command("telemetry").description(
5224
5207
  "Emit WLP run telemetry (an executor leg's progress events) into a decomposition run"
@@ -5253,10 +5236,9 @@ function registerTelemetry(program2) {
5253
5236
  ).action(async (opts, cmd) => {
5254
5237
  const json = Boolean(cmd.optsWithGlobals().json);
5255
5238
  const cfg = resolveConfig(cmd.optsWithGlobals());
5256
- const kind = normalizeKind(opts.kind);
5257
5239
  const event = {
5258
5240
  taskId: opts.task,
5259
- kind,
5241
+ kind: normalizeKind(opts.kind),
5260
5242
  tokensIn: opts.tokensIn ?? null,
5261
5243
  tokensOut: opts.tokensOut ?? null,
5262
5244
  contextUsed: opts.contextUsed ?? null,
@@ -5265,36 +5247,226 @@ function registerTelemetry(program2) {
5265
5247
  approvalState: opts.approval ?? null,
5266
5248
  verdict: opts.verdict ?? null
5267
5249
  };
5268
- const token = await requireToken(cfg);
5269
- const resp = await fetch(
5270
- `${cfg.baseUrl}/decompositions/${encodeURIComponent(opts.decomposition)}/run/telemetry`,
5271
- {
5272
- method: "POST",
5273
- headers: {
5274
- authorization: `Bearer ${token}`,
5275
- tenant: cfg.tenant,
5276
- "content-type": "application/json",
5277
- "x-sechroom-surface": "cli"
5278
- },
5279
- body: JSON.stringify({ events: [event] })
5280
- }
5281
- );
5282
- if (!resp.ok)
5283
- fail(
5284
- `Telemetry emit failed (HTTP ${resp.status}): ${await resp.text()}`
5285
- );
5286
- const body = await resp.json();
5250
+ let body;
5251
+ try {
5252
+ body = await postTelemetry(cfg, opts.decomposition, [event]);
5253
+ } catch (e) {
5254
+ return fail(`Telemetry emit failed: ${e.message}`);
5255
+ }
5287
5256
  if (json) {
5288
5257
  emit(body, true);
5289
5258
  } else {
5290
5259
  process.stderr.write(
5291
5260
  style.green("telemetry emitted") + style.dim(
5292
- ` \u2014 ${kind} for task ${opts.task}; run now carries ${body.eventCount} event${body.eventCount === 1 ? "" : "s"}
5261
+ ` \u2014 ${event.kind} for task ${opts.task}; run now carries ${body.eventCount} event${body.eventCount === 1 ? "" : "s"}
5293
5262
  `
5294
5263
  )
5295
5264
  );
5296
5265
  }
5297
5266
  });
5267
+ telemetry.command("bind").description(
5268
+ "Bind this checkout to a decomposition+task so the Stop hook auto-emits per-turn telemetry"
5269
+ ).requiredOption(
5270
+ "--decomposition <id>",
5271
+ "Decomposition id this session executes"
5272
+ ).requiredOption("--task <id>", "Task id this session executes").action((opts, cmd) => {
5273
+ const json = Boolean(cmd.optsWithGlobals().json);
5274
+ const dir = join16(process.cwd(), ".sechroom");
5275
+ mkdirSync11(dir, { recursive: true });
5276
+ const path = join16(dir, BINDING_FILE);
5277
+ const binding = {
5278
+ decompositionId: opts.decomposition,
5279
+ taskId: opts.task
5280
+ };
5281
+ writeFileSync11(path, JSON.stringify(binding, null, 2) + "\n");
5282
+ ensureStateDirIgnored(process.cwd());
5283
+ if (json) {
5284
+ emit({ bound: true, ...binding, path }, true);
5285
+ } else {
5286
+ process.stdout.write(
5287
+ style.green("telemetry bound") + style.dim(
5288
+ ` \u2014 decomposition ${binding.decompositionId}, task ${binding.taskId} (${path})
5289
+ `
5290
+ )
5291
+ );
5292
+ }
5293
+ });
5294
+ telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
5295
+ const json = Boolean(cmd.optsWithGlobals().json);
5296
+ const path = join16(process.cwd(), ".sechroom", BINDING_FILE);
5297
+ const existed = existsSync14(path);
5298
+ if (existed) rmSync4(path);
5299
+ if (json) emit({ unbound: existed, path }, true);
5300
+ else
5301
+ process.stdout.write(
5302
+ existed ? "telemetry binding cleared\n" : "no telemetry binding to clear\n"
5303
+ );
5304
+ });
5305
+ telemetry.command("hook").description(
5306
+ "Per-turn telemetry self-report for a Claude Code Stop hook (reads stdin; no-op unless bound). Fail-soft."
5307
+ ).action(async (_opts, cmd) => {
5308
+ try {
5309
+ const raw = await readStdin2();
5310
+ const input = parseHookInput2(raw);
5311
+ const cwd = input.cwd ?? process.cwd();
5312
+ const binding = findBinding(cwd);
5313
+ if (!binding) return process.exit(0);
5314
+ const usage = input.transcript_path ? parseTranscript(input.transcript_path) : null;
5315
+ if (!usage) return process.exit(0);
5316
+ const cfg = resolveConfig(cmd.optsWithGlobals());
5317
+ await postTelemetry(cfg, binding.decompositionId, [
5318
+ {
5319
+ taskId: binding.taskId,
5320
+ kind: "Parsed",
5321
+ tokensIn: usage.tokensIn,
5322
+ tokensOut: usage.tokensOut,
5323
+ contextUsed: usage.contextUsed,
5324
+ contextWindow: usage.contextWindow,
5325
+ text: null,
5326
+ approvalState: null,
5327
+ verdict: null
5328
+ }
5329
+ ]);
5330
+ return process.exit(0);
5331
+ } catch {
5332
+ return process.exit(0);
5333
+ }
5334
+ });
5335
+ telemetry.command("install").description(
5336
+ "Wire the per-turn telemetry Stop hook into Claude Code settings (also folded into `sechroom hook install`)"
5337
+ ).option(
5338
+ "--scope <scope>",
5339
+ "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global"
5340
+ ).option("--local", "alias for --scope project").option("--dry-run", "Print what would change; write nothing").action((opts, cmd) => {
5341
+ const g = cmd.optsWithGlobals();
5342
+ const dryRun = Boolean(opts.dryRun);
5343
+ const cwd = process.cwd();
5344
+ let scope;
5345
+ try {
5346
+ scope = opts.local ? "project" : resolveScope(opts.scope);
5347
+ } catch (err2) {
5348
+ process.stderr.write(`${err2.message}
5349
+ `);
5350
+ return process.exit(2);
5351
+ }
5352
+ const targets = resolveClaudeTargets({
5353
+ override: g.claudeConfigDir,
5354
+ scope,
5355
+ cwd
5356
+ });
5357
+ const commands = { Stop: "sechroom telemetry hook" };
5358
+ try {
5359
+ const multi = targets.length > 1;
5360
+ const results = targets.map((t) => {
5361
+ const r = installClaudeCommands(t.dir, commands, dryRun);
5362
+ process.stdout.write(
5363
+ `${HOOK_SURFACE_LABEL.claude}${multi ? ` (${t.label})` : ""}:
5364
+ `
5365
+ );
5366
+ process.stdout.write(describe(r, dryRun) + "\n");
5367
+ return r;
5368
+ });
5369
+ if (dryRun) {
5370
+ process.stdout.write("\n(dry run \u2014 no files were written.)\n");
5371
+ } else if (results.every((r) => r.status === "current")) {
5372
+ process.stdout.write("\nAlready up to date \u2014 nothing to change.\n");
5373
+ } else {
5374
+ process.stdout.write(
5375
+ "\nRestart your agent for the hook to take effect, then bind a task with `sechroom telemetry bind`.\n"
5376
+ );
5377
+ }
5378
+ } catch (err2) {
5379
+ process.stderr.write(
5380
+ `telemetry install failed: ${err2.message}
5381
+ `
5382
+ );
5383
+ return process.exit(1);
5384
+ }
5385
+ warnIfSechroomNotOnPath();
5386
+ return process.exit(0);
5387
+ });
5388
+ }
5389
+ var BINDING_FILE = "telemetry.json";
5390
+ async function postTelemetry(cfg, decompositionId, events) {
5391
+ const token = await requireToken(cfg);
5392
+ const resp = await fetch(
5393
+ `${cfg.baseUrl}/decompositions/${encodeURIComponent(decompositionId)}/run/telemetry`,
5394
+ {
5395
+ method: "POST",
5396
+ headers: {
5397
+ authorization: `Bearer ${token}`,
5398
+ tenant: cfg.tenant,
5399
+ "content-type": "application/json",
5400
+ "x-sechroom-surface": "cli"
5401
+ },
5402
+ body: JSON.stringify({ events })
5403
+ }
5404
+ );
5405
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);
5406
+ return await resp.json();
5407
+ }
5408
+ function findBinding(start) {
5409
+ let dir = start;
5410
+ for (; ; ) {
5411
+ const path = join16(dir, ".sechroom", BINDING_FILE);
5412
+ if (existsSync14(path)) {
5413
+ try {
5414
+ const b = JSON.parse(
5415
+ readFileSync9(path, "utf8")
5416
+ );
5417
+ if (b.decompositionId && b.taskId)
5418
+ return { decompositionId: b.decompositionId, taskId: b.taskId };
5419
+ } catch {
5420
+ }
5421
+ return null;
5422
+ }
5423
+ const parent = dirname9(dir);
5424
+ if (parent === dir) return null;
5425
+ dir = parent;
5426
+ }
5427
+ }
5428
+ function parseTranscript(path) {
5429
+ if (!existsSync14(path)) return null;
5430
+ let tokensIn = 0;
5431
+ let tokensOut = 0;
5432
+ let contextUsed = 0;
5433
+ let model = "";
5434
+ for (const line of readFileSync9(path, "utf8").split("\n")) {
5435
+ if (!line.trim()) continue;
5436
+ let obj;
5437
+ try {
5438
+ obj = JSON.parse(line);
5439
+ } catch {
5440
+ continue;
5441
+ }
5442
+ const usage = obj.type === "assistant" ? obj.message?.usage : void 0;
5443
+ if (!usage) continue;
5444
+ const input = (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
5445
+ tokensIn += input;
5446
+ tokensOut += usage.output_tokens ?? 0;
5447
+ contextUsed = input;
5448
+ if (obj.message?.model) model = obj.message.model;
5449
+ }
5450
+ if (tokensIn === 0 && tokensOut === 0) return null;
5451
+ return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model) };
5452
+ }
5453
+ function windowFor(model) {
5454
+ const m = model.toLowerCase();
5455
+ return m.includes("[1m]") || m.includes("-1m") ? 1e6 : 2e5;
5456
+ }
5457
+ async function readStdin2() {
5458
+ if (process.stdin.isTTY) return "";
5459
+ const chunks = [];
5460
+ for await (const chunk of process.stdin) chunks.push(chunk);
5461
+ return Buffer.concat(chunks).toString("utf8");
5462
+ }
5463
+ function parseHookInput2(raw) {
5464
+ if (!raw.trim()) return {};
5465
+ try {
5466
+ return JSON.parse(raw);
5467
+ } catch {
5468
+ return {};
5469
+ }
5298
5470
  }
5299
5471
  var KINDS = {
5300
5472
  raw: "Raw",
@@ -5516,7 +5688,7 @@ Examples:
5516
5688
  function resolveVersion() {
5517
5689
  try {
5518
5690
  const pkg = JSON.parse(
5519
- readFileSync8(new URL("../package.json", import.meta.url), "utf8")
5691
+ readFileSync10(new URL("../package.json", import.meta.url), "utf8")
5520
5692
  );
5521
5693
  return pkg.version ?? "0.0.0";
5522
5694
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sechroom/cli",
3
- "version": "2026.6.238-rc.10da7380",
3
+ "version": "2026.7.1",
4
4
  "description": "Sechroom CLI — a thin, generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -24,11 +24,12 @@
24
24
  "registry": "https://registry.npmjs.org/"
25
25
  },
26
26
  "dependencies": {
27
+ "@clack/prompts": "^1.6.0",
27
28
  "@microsoft/signalr": "^10.0.0",
29
+ "@modelcontextprotocol/sdk": "^1.12.0",
28
30
  "commander": "^12.1.0",
29
31
  "open": "^10.1.0",
30
- "openapi-fetch": "^0.13.0",
31
- "@modelcontextprotocol/sdk": "^1.12.0"
32
+ "openapi-fetch": "^0.13.0"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@types/node": "^20.14.0",