@sechroom/cli 2026.6.237-rc.9357cead → 2026.6.239-rc.6418da97

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 +519 -207
  2. package/package.json +2 -2
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
@@ -1590,13 +1590,13 @@ Examples:
1590
1590
  }
1591
1591
 
1592
1592
  // src/commands/checkpoint.ts
1593
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
1594
- import { dirname as dirname5, join as join8 } from "path";
1593
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
1594
+ import { dirname as dirname6, join as join9 } from "path";
1595
1595
 
1596
1596
  // src/commands/hook.ts
1597
1597
  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";
1598
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
1599
+ import { dirname as dirname5, join as join8 } from "path";
1600
1600
 
1601
1601
  // src/sem.ts
1602
1602
  import { dirname as dirname2, join as join5 } from "path";
@@ -1686,6 +1686,9 @@ function writeSem(values, path = localSemPath()) {
1686
1686
  ensureContinuityScaffold(path);
1687
1687
  return path;
1688
1688
  }
1689
+ function ensureStateDirIgnored(cwd = process.cwd()) {
1690
+ ensureSemIgnored(localSemPath(cwd));
1691
+ }
1689
1692
  var CONTINUITY_FILE_NAME = "continuity.json";
1690
1693
  var CONTINUITY_SCAFFOLD = JSON.stringify(
1691
1694
  {
@@ -1757,6 +1760,10 @@ function ensureSemIgnored(semPath) {
1757
1760
  }
1758
1761
  }
1759
1762
 
1763
+ // src/commands/hook-install.ts
1764
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
1765
+ import { delimiter, dirname as dirname4, join as join7 } from "path";
1766
+
1760
1767
  // src/setup/clients.ts
1761
1768
  import { existsSync as existsSync5 } from "fs";
1762
1769
  import { homedir as homedir3 } from "os";
@@ -1827,6 +1834,144 @@ function detectInstalledClients(cwd) {
1827
1834
  return detected;
1828
1835
  }
1829
1836
 
1837
+ // src/commands/hook-install.ts
1838
+ var CLAUDE_HOOK_COMMANDS = {
1839
+ SessionStart: "sechroom hook session-start",
1840
+ PreCompact: "sechroom hook pre-compact",
1841
+ SessionEnd: "sechroom hook session-end",
1842
+ // WLP telemetry tap (D-WLP-10) — per-turn executor self-report. No-op (exit 0) unless this
1843
+ // checkout is bound to a decomposition+task via `sechroom telemetry bind`, so it's safe to wire
1844
+ // for every Claude install. Claude-only (it parses a Claude Code transcript).
1845
+ Stop: "sechroom telemetry hook"
1846
+ };
1847
+ var CODEX_HOOK_COMMANDS = {
1848
+ SessionStart: "sechroom hook session-start",
1849
+ Stop: "sechroom hook session-end --debounce-minutes 10"
1850
+ };
1851
+ function hasHookCommand(config2, event, command) {
1852
+ const groups = config2.hooks?.[event] ?? [];
1853
+ return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
1854
+ }
1855
+ function mergeHooks(config2, commands) {
1856
+ config2.hooks ??= {};
1857
+ let added = 0;
1858
+ for (const [event, command] of Object.entries(commands)) {
1859
+ if (hasHookCommand(config2, event, command)) continue;
1860
+ const groups = config2.hooks[event] ??= [];
1861
+ groups.push({ hooks: [{ type: "command", command }] });
1862
+ added += 1;
1863
+ }
1864
+ return added;
1865
+ }
1866
+ function readJsonConfig2(path) {
1867
+ if (!existsSync6(path)) return {};
1868
+ const raw = readFileSync4(path, "utf8");
1869
+ if (!raw.trim()) return {};
1870
+ return JSON.parse(raw);
1871
+ }
1872
+ function installHooksJson(path, commands, dryRun) {
1873
+ const existed = existsSync6(path) && readFileSync4(path, "utf8").trim().length > 0;
1874
+ const config2 = readJsonConfig2(path);
1875
+ const added = mergeHooks(config2, commands);
1876
+ if (added === 0 && existed) return { path, status: "current" };
1877
+ if (!dryRun) {
1878
+ mkdirSync5(dirname4(path), { recursive: true });
1879
+ writeFileSync5(path, JSON.stringify(config2, null, 2) + "\n");
1880
+ }
1881
+ return { path, status: existed ? "merged" : "created" };
1882
+ }
1883
+ function installClaudeCommands(claudeDir, commands, dryRun) {
1884
+ return installHooksJson(join7(claudeDir, "settings.json"), commands, dryRun);
1885
+ }
1886
+ function ensureCodexFeaturesHooks(content) {
1887
+ const lines = content.split("\n");
1888
+ const headerIdx = lines.findIndex((l) => l.trim() === "[features]");
1889
+ if (headerIdx === -1) {
1890
+ const base = content.length === 0 || content.endsWith("\n") ? content : content + "\n";
1891
+ return { next: base + "\n[features]\nhooks = true\n", changed: true };
1892
+ }
1893
+ for (let i = headerIdx + 1; i < lines.length; i += 1) {
1894
+ const trimmed = lines[i].trim();
1895
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) break;
1896
+ const m = lines[i].match(/^(\s*)hooks(\s*)=(\s*)(.*)$/);
1897
+ if (!m) continue;
1898
+ const value = m[4].replace(/\s*#.*$/, "").trim();
1899
+ if (value === "true") return { next: content, changed: false };
1900
+ lines[i] = `${m[1]}hooks${m[2]}=${m[3]}true`;
1901
+ return { next: lines.join("\n"), changed: true };
1902
+ }
1903
+ lines.splice(headerIdx + 1, 0, "hooks = true");
1904
+ return { next: lines.join("\n"), changed: true };
1905
+ }
1906
+ function installCodexFeatureFlag(path, dryRun) {
1907
+ const existed = existsSync6(path);
1908
+ const content = existed ? readFileSync4(path, "utf8") : "";
1909
+ const { next, changed } = ensureCodexFeaturesHooks(content);
1910
+ if (!changed) return { path, status: "current" };
1911
+ if (!dryRun) {
1912
+ mkdirSync5(dirname4(path), { recursive: true });
1913
+ writeFileSync5(path, next);
1914
+ }
1915
+ return { path, status: existed ? "merged" : "created" };
1916
+ }
1917
+ function resolveSurfaces(surface, cwd) {
1918
+ if (surface === "claude") return ["claude"];
1919
+ if (surface === "codex") return ["codex"];
1920
+ if (surface === "both") return ["claude", "codex"];
1921
+ if (surface) throw new Error(`--surface must be one of claude | codex | both (got '${surface}')`);
1922
+ const surfaces = detectHookSurfaces(cwd);
1923
+ return surfaces.length > 0 ? surfaces : ["claude", "codex"];
1924
+ }
1925
+ function describe(result, dryRun) {
1926
+ if (result.status === "current") return ` \u2713 ${result.path} (already configured)`;
1927
+ const verb = dryRun ? "would" : result.status === "created" ? "created" : "updated";
1928
+ return ` \u2713 ${result.path} (${dryRun ? `${verb} ${result.status === "created" ? "create" : "update"}` : verb})`;
1929
+ }
1930
+ var HOOK_SURFACE_LABEL = {
1931
+ claude: "Claude Code",
1932
+ codex: "Codex"
1933
+ };
1934
+ function installHookSurfaces(surfaces, opts) {
1935
+ const out = [];
1936
+ for (const surface of surfaces) {
1937
+ if (surface === "claude") {
1938
+ const path = join7(opts.claudeDir, "settings.json");
1939
+ out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
1940
+ } else {
1941
+ const hooksJson = installHooksJson(join7(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
1942
+ const featureFlag = installCodexFeatureFlag(join7(opts.codexHome, "config.toml"), opts.dryRun);
1943
+ out.push({ surface, results: [hooksJson, featureFlag] });
1944
+ }
1945
+ }
1946
+ return out;
1947
+ }
1948
+ function detectHookSurfaces(cwd) {
1949
+ const detected = detectInstalledClients(cwd);
1950
+ const surfaces = [];
1951
+ if (detected.includes("claude-code")) surfaces.push("claude");
1952
+ if (detected.includes("codex")) surfaces.push("codex");
1953
+ return surfaces;
1954
+ }
1955
+ function isSechroomOnPath() {
1956
+ const pathEnv = process.env.PATH ?? "";
1957
+ if (!pathEnv) return false;
1958
+ const names = process.platform === "win32" ? ["sechroom.cmd", "sechroom.exe", "sechroom.bat", "sechroom"] : ["sechroom"];
1959
+ for (const dir of pathEnv.split(delimiter)) {
1960
+ if (!dir) continue;
1961
+ for (const name of names) {
1962
+ if (existsSync6(join7(dir, name))) return true;
1963
+ }
1964
+ }
1965
+ return false;
1966
+ }
1967
+ function warnIfSechroomNotOnPath(write = (s) => void process.stderr.write(s)) {
1968
+ if (isSechroomOnPath()) return false;
1969
+ write(
1970
+ "\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"
1971
+ );
1972
+ return true;
1973
+ }
1974
+
1830
1975
  // src/commands/hook.ts
1831
1976
  async function readStdin() {
1832
1977
  if (process.stdin.isTTY) return "";
@@ -1851,13 +1996,13 @@ function resolveLane(flagLane, cwd) {
1851
1996
  if (!base) return void 0;
1852
1997
  return applyWorktreeLaneSuffix(base, start);
1853
1998
  }
1854
- var INTENT_FILE = join7(".sechroom", "continuity.json");
1999
+ var INTENT_FILE = join8(".sechroom", "continuity.json");
1855
2000
  function resolveIntentPath(start) {
1856
2001
  let dir = start;
1857
2002
  for (; ; ) {
1858
- const candidate = join7(dir, INTENT_FILE);
1859
- if (existsSync6(candidate)) return candidate;
1860
- const parent = dirname4(dir);
2003
+ const candidate = join8(dir, INTENT_FILE);
2004
+ if (existsSync7(candidate)) return candidate;
2005
+ const parent = dirname5(dir);
1861
2006
  if (parent === dir) return void 0;
1862
2007
  dir = parent;
1863
2008
  }
@@ -1866,7 +2011,7 @@ function readIntent(start) {
1866
2011
  const path = resolveIntentPath(start);
1867
2012
  if (!path) return void 0;
1868
2013
  try {
1869
- return JSON.parse(readFileSync4(path, "utf8"));
2014
+ return JSON.parse(readFileSync5(path, "utf8"));
1870
2015
  } catch {
1871
2016
  return void 0;
1872
2017
  }
@@ -1908,14 +2053,14 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
1908
2053
  }
1909
2054
  function ledgerPath(start) {
1910
2055
  const intent = resolveIntentPath(start);
1911
- const dir = intent ? dirname4(intent) : join7(start, ".sechroom");
1912
- return join7(dir, ".checkpoint-state.json");
2056
+ const dir = intent ? dirname5(intent) : join8(start, ".sechroom");
2057
+ return join8(dir, ".checkpoint-state.json");
1913
2058
  }
1914
2059
  function readLedger(start) {
1915
2060
  try {
1916
2061
  const p = ledgerPath(start);
1917
- if (!existsSync6(p)) return {};
1918
- return JSON.parse(readFileSync4(p, "utf8"));
2062
+ if (!existsSync7(p)) return {};
2063
+ return JSON.parse(readFileSync5(p, "utf8"));
1919
2064
  } catch {
1920
2065
  return {};
1921
2066
  }
@@ -1962,13 +2107,13 @@ function recordPush(start, intent) {
1962
2107
  } catch {
1963
2108
  mtimeMs = void 0;
1964
2109
  }
1965
- mkdirSync5(dirname4(p), { recursive: true });
2110
+ mkdirSync6(dirname5(p), { recursive: true });
1966
2111
  const ledger = {
1967
2112
  lastEpochMs: Date.now(),
1968
2113
  lastMtimeMs: mtimeMs,
1969
2114
  lastHash: intentHash(intent)
1970
2115
  };
1971
- writeFileSync5(p, JSON.stringify(ledger) + "\n");
2116
+ writeFileSync6(p, JSON.stringify(ledger) + "\n");
1972
2117
  } catch {
1973
2118
  }
1974
2119
  }
@@ -2008,135 +2153,6 @@ function emitSessionStart(additionalContext) {
2008
2153
  }) + "\n"
2009
2154
  );
2010
2155
  }
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
2156
  function registerHook(program2) {
2141
2157
  const hook = program2.command("hook").description("Agent-lifecycle hook adapter (Claude Code / Codex) \u2014 bridges hooks to continuity");
2142
2158
  hook.addHelpText(
@@ -2344,10 +2360,10 @@ Examples:
2344
2360
  const client = await makeClient(cfg);
2345
2361
  return client.POST("/continuity/snapshots", { body });
2346
2362
  });
2347
- const path = resolveIntentPath(cwd) ?? join8(cwd, INTENT_FILE);
2363
+ const path = resolveIntentPath(cwd) ?? join9(cwd, INTENT_FILE);
2348
2364
  const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
2349
- mkdirSync6(dirname5(path), { recursive: true });
2350
- writeFileSync6(path, JSON.stringify(fileBody, null, 2) + "\n");
2365
+ mkdirSync7(dirname6(path), { recursive: true });
2366
+ writeFileSync7(path, JSON.stringify(fileBody, null, 2) + "\n");
2351
2367
  recordPush(cwd, merged);
2352
2368
  if (json) {
2353
2369
  emit({ snapshotId: data.snapshotId, lane, scope, file: path }, true);
@@ -3031,8 +3047,8 @@ Examples:
3031
3047
 
3032
3048
  // src/setup/apply.ts
3033
3049
  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";
3050
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync8, existsSync as existsSync8 } from "fs";
3051
+ import { dirname as dirname7 } from "path";
3036
3052
  var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
3037
3053
  var MARKER_END = "<!-- @sechroom/cli:end";
3038
3054
  function normalizeBody(s) {
@@ -3085,22 +3101,22 @@ function parseManagedBlock(content, block) {
3085
3101
  return null;
3086
3102
  }
3087
3103
  function ensureDir2(path) {
3088
- mkdirSync7(dirname6(path), { recursive: true });
3104
+ mkdirSync8(dirname7(path), { recursive: true });
3089
3105
  }
3090
3106
  function readOr(path, fallback) {
3091
3107
  try {
3092
- return readFileSync5(path, "utf8");
3108
+ return readFileSync6(path, "utf8");
3093
3109
  } catch {
3094
3110
  return fallback;
3095
3111
  }
3096
3112
  }
3097
3113
  function mergeMcpJson(path, snippet, dryRun) {
3098
3114
  const incoming = JSON.parse(snippet);
3099
- const existed = existsSync7(path);
3115
+ const existed = existsSync8(path);
3100
3116
  let current = {};
3101
3117
  if (existed) {
3102
3118
  try {
3103
- current = JSON.parse(readFileSync5(path, "utf8"));
3119
+ current = JSON.parse(readFileSync6(path, "utf8"));
3104
3120
  } catch {
3105
3121
  return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
3106
3122
  }
@@ -3108,26 +3124,26 @@ function mergeMcpJson(path, snippet, dryRun) {
3108
3124
  current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
3109
3125
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
3110
3126
  ensureDir2(path);
3111
- writeFileSync7(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
3127
+ writeFileSync8(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
3112
3128
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
3113
3129
  }
3114
3130
  function mergeCodexToml(path, snippet, dryRun) {
3115
- const existed = existsSync7(path);
3131
+ const existed = existsSync8(path);
3116
3132
  let body = readOr(path, "");
3117
3133
  body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
3118
3134
  const trimmed = body.trim();
3119
3135
  const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
3120
3136
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
3121
3137
  ensureDir2(path);
3122
- writeFileSync7(path, next, { mode: 384 });
3138
+ writeFileSync8(path, next, { mode: 384 });
3123
3139
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
3124
3140
  }
3125
3141
  function writeInstructionBlock(path, write, dryRun) {
3126
- const existed = existsSync7(path);
3142
+ const existed = existsSync8(path);
3127
3143
  const next = computeBlockFile(readOr(path, ""), write);
3128
3144
  if (dryRun) return { kind: "instruction", path, status: "dry-run" };
3129
3145
  ensureDir2(path);
3130
- writeFileSync7(path, next);
3146
+ writeFileSync8(path, next);
3131
3147
  return { kind: "instruction", path, status: existed ? "merged" : "created" };
3132
3148
  }
3133
3149
  function computeBlockFile(current, write) {
@@ -3168,7 +3184,7 @@ function applyBlock(path, write, mode, dryRun) {
3168
3184
  const next = computeBlockFile(current, write);
3169
3185
  if (!dryRun) {
3170
3186
  ensureDir2(proposedPath);
3171
- writeFileSync7(proposedPath, next);
3187
+ writeFileSync8(proposedPath, next);
3172
3188
  }
3173
3189
  return {
3174
3190
  kind: "instruction",
@@ -3292,8 +3308,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
3292
3308
  }
3293
3309
 
3294
3310
  // src/setup/skills-offer.ts
3295
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
3296
- import { join as join9 } from "path";
3311
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
3312
+ import { join as join10 } from "path";
3297
3313
 
3298
3314
  // src/setup/lane-pin.ts
3299
3315
  var CODE_LANE_PREFIX_BY_CLIENT = {
@@ -3409,8 +3425,8 @@ Found ${summary} available to you for ${surface}.
3409
3425
  if (skills.length > 0) {
3410
3426
  const written = [];
3411
3427
  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");
3428
+ mkdirSync9(join10(sDir, s.name), { recursive: true });
3429
+ writeFileSync9(join10(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
3414
3430
  written.push(s.name);
3415
3431
  }
3416
3432
  recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
@@ -3418,11 +3434,11 @@ Found ${summary} available to you for ${surface}.
3418
3434
  `);
3419
3435
  }
3420
3436
  if (agents.length > 0) {
3421
- mkdirSync8(aDir, { recursive: true });
3437
+ mkdirSync9(aDir, { recursive: true });
3422
3438
  const written = [];
3423
3439
  for (const a of agents) {
3424
3440
  const file = `${a.name}.md`;
3425
- writeFileSync8(join9(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
3441
+ writeFileSync9(join10(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
3426
3442
  written.push(file);
3427
3443
  }
3428
3444
  recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
@@ -3805,13 +3821,13 @@ Wired to namespace '${slug}'. Restart your AI client (or reload MCP) to pick it
3805
3821
  }
3806
3822
 
3807
3823
  // src/commands/onboard.ts
3808
- import { existsSync as existsSync9 } from "fs";
3809
- import { basename as basename2, join as join11 } from "path";
3824
+ import { existsSync as existsSync10 } from "fs";
3825
+ import { basename as basename2, join as join12 } from "path";
3810
3826
 
3811
3827
  // src/commands/fanout.ts
3812
3828
  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";
3829
+ import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
3830
+ import { isAbsolute, join as join11, resolve } from "path";
3815
3831
  var ICON = {
3816
3832
  refresh: "\u21BB",
3817
3833
  bind: "+",
@@ -3831,21 +3847,21 @@ function discoverChildren(root) {
3831
3847
  const out = [];
3832
3848
  for (const name of names.sort()) {
3833
3849
  if (name.startsWith(".") || name === "node_modules") continue;
3834
- const dir = join10(root, name);
3850
+ const dir = join11(root, name);
3835
3851
  try {
3836
3852
  if (!statSync3(dir).isDirectory()) continue;
3837
3853
  } catch {
3838
3854
  continue;
3839
3855
  }
3840
- if (existsSync8(join10(dir, ".git")) || committedBindingPath(dir)) out.push(name);
3856
+ if (existsSync9(join11(dir, ".git")) || committedBindingPath(dir)) out.push(name);
3841
3857
  }
3842
3858
  return out;
3843
3859
  }
3844
3860
  function readManifest(path) {
3845
- if (!existsSync8(path)) return null;
3861
+ if (!existsSync9(path)) return null;
3846
3862
  let parsed;
3847
3863
  try {
3848
- parsed = JSON.parse(readFileSync6(path, "utf8"));
3864
+ parsed = JSON.parse(readFileSync7(path, "utf8"));
3849
3865
  } catch (err2) {
3850
3866
  throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
3851
3867
  }
@@ -4222,10 +4238,10 @@ async function chooseScope(scopeFlag, yes) {
4222
4238
  }
4223
4239
  async function planRecurseChild(entry, root, client, opts) {
4224
4240
  const dir = resolveChildDir(entry.path, root);
4225
- if (!existsSync9(dir)) {
4241
+ if (!existsSync10(dir)) {
4226
4242
  return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
4227
4243
  }
4228
- if (existsSync9(join11(dir, ".sechroom.json"))) {
4244
+ if (existsSync10(join12(dir, ".sechroom.json"))) {
4229
4245
  return {
4230
4246
  label: entry.path,
4231
4247
  dir,
@@ -4298,7 +4314,7 @@ This fan-out will pin the same lane in every repo:
4298
4314
  async function runRecurse(cfg, g, opts) {
4299
4315
  const { yes, dryRun, json } = opts;
4300
4316
  const root = process.cwd();
4301
- const manifestPath = join11(root, ".sechroom", "repos.json");
4317
+ const manifestPath = join12(root, ".sechroom", "repos.json");
4302
4318
  const fromManifest = readManifest(manifestPath);
4303
4319
  const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
4304
4320
  const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
@@ -4810,23 +4826,23 @@ Examples:
4810
4826
 
4811
4827
  // src/commands/reset.ts
4812
4828
  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";
4829
+ import { join as join13 } from "path";
4830
+ import { existsSync as existsSync11, readFileSync as readFileSync8, rmSync as rmSync3 } from "fs";
4815
4831
  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");
4832
+ var localSkillsDir = () => join13(process.cwd(), ".claude", "skills");
4833
+ var globalSkillsDir = () => join13(homedir4(), ".claude", "skills");
4834
+ var localAgentsDir = () => join13(process.cwd(), ".claude", "agents");
4835
+ var globalAgentsDir = () => join13(homedir4(), ".claude", "agents");
4820
4836
  function removeMaterialisedSkills(dir) {
4821
4837
  const removed = [];
4822
- const lockPath = join12(dir, SKILLS_LOCK2);
4823
- if (!existsSync10(lockPath)) return removed;
4838
+ const lockPath = join13(dir, SKILLS_LOCK2);
4839
+ if (!existsSync11(lockPath)) return removed;
4824
4840
  try {
4825
- const lock = JSON.parse(readFileSync7(lockPath, "utf8"));
4841
+ const lock = JSON.parse(readFileSync8(lockPath, "utf8"));
4826
4842
  for (const entry of Object.values(lock)) {
4827
4843
  for (const name of entry.skills ?? []) {
4828
- const p = join12(dir, name);
4829
- if (existsSync10(p)) {
4844
+ const p = join13(dir, name);
4845
+ if (existsSync11(p)) {
4830
4846
  rmSync3(p, { recursive: true, force: true });
4831
4847
  removed.push(p);
4832
4848
  }
@@ -4871,18 +4887,18 @@ function registerReset(program2) {
4871
4887
  }
4872
4888
  }
4873
4889
  const removed = [];
4874
- const stateDir = join12(process.cwd(), ".sechroom");
4875
- if (existsSync10(stateDir)) {
4890
+ const stateDir = join13(process.cwd(), ".sechroom");
4891
+ if (existsSync11(stateDir)) {
4876
4892
  rmSync3(stateDir, { recursive: true, force: true });
4877
4893
  removed.push(stateDir);
4878
4894
  }
4879
- const legacyCfg = join12(process.cwd(), ".sechroom.json");
4880
- if (existsSync10(legacyCfg)) {
4895
+ const legacyCfg = join13(process.cwd(), ".sechroom.json");
4896
+ if (existsSync11(legacyCfg)) {
4881
4897
  rmSync3(legacyCfg, { force: true });
4882
4898
  removed.push(legacyCfg);
4883
4899
  }
4884
- const legacySem = join12(process.cwd(), ".sem");
4885
- if (existsSync10(legacySem)) {
4900
+ const legacySem = join13(process.cwd(), ".sem");
4901
+ if (existsSync11(legacySem)) {
4886
4902
  rmSync3(legacySem, { force: true });
4887
4903
  removed.push(legacySem);
4888
4904
  }
@@ -4908,8 +4924,8 @@ function registerReset(program2) {
4908
4924
  }
4909
4925
 
4910
4926
  // 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";
4927
+ import { existsSync as existsSync12, mkdirSync as mkdirSync10, statSync as statSync4, writeFileSync as writeFileSync10 } from "fs";
4928
+ import { join as join14 } from "path";
4913
4929
  function filenameFromDisposition(header) {
4914
4930
  if (!header) return void 0;
4915
4931
  const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
@@ -4917,11 +4933,11 @@ function filenameFromDisposition(header) {
4917
4933
  }
4918
4934
  function resolveOutputPath(output, serverFilename) {
4919
4935
  const filename = serverFilename || "skills.zip";
4920
- if (!output) return join13(process.cwd(), filename);
4921
- const looksLikeDir = output.endsWith("/") || existsSync11(output) && statSync4(output).isDirectory();
4936
+ if (!output) return join14(process.cwd(), filename);
4937
+ const looksLikeDir = output.endsWith("/") || existsSync12(output) && statSync4(output).isDirectory();
4922
4938
  if (looksLikeDir) {
4923
- mkdirSync9(output, { recursive: true });
4924
- return join13(output, filename);
4939
+ mkdirSync10(output, { recursive: true });
4940
+ return join14(output, filename);
4925
4941
  }
4926
4942
  return output;
4927
4943
  }
@@ -4952,7 +4968,7 @@ async function downloadZip(label, call, output) {
4952
4968
  const buf = Buffer.from(res.data);
4953
4969
  const filename = filenameFromDisposition(res.response.headers.get("content-disposition")) ?? "skills.zip";
4954
4970
  const path = resolveOutputPath(output, filename);
4955
- writeFileSync9(path, buf);
4971
+ writeFileSync10(path, buf);
4956
4972
  return { path, bytes: buf.length, filename };
4957
4973
  }
4958
4974
  function registerSkills(program2) {
@@ -5120,12 +5136,12 @@ Examples:
5120
5136
  }
5121
5137
 
5122
5138
  // 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");
5139
+ import { existsSync as existsSync13 } from "fs";
5140
+ import { dirname as dirname8, join as join15, resolve as resolve2 } from "path";
5141
+ var DEFAULT_MANIFEST = join15(".sechroom", "repos.json");
5126
5142
  function planEntry(entry, root) {
5127
5143
  const dir = resolveChildDir(entry.path, root);
5128
- if (!existsSync12(dir)) {
5144
+ if (!existsSync13(dir)) {
5129
5145
  return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
5130
5146
  }
5131
5147
  if (committedBindingPath(dir)) {
@@ -5201,7 +5217,7 @@ Examples:
5201
5217
  `);
5202
5218
  return;
5203
5219
  }
5204
- const root = dirname7(dirname7(manifestPath));
5220
+ const root = dirname8(dirname8(manifestPath));
5205
5221
  const plans = repos.map((entry) => planEntry(entry, root));
5206
5222
  if (!json) {
5207
5223
  process.stderr.write(
@@ -5218,6 +5234,301 @@ Examples:
5218
5234
  });
5219
5235
  }
5220
5236
 
5237
+ // src/commands/telemetry.ts
5238
+ import {
5239
+ existsSync as existsSync14,
5240
+ mkdirSync as mkdirSync11,
5241
+ readFileSync as readFileSync9,
5242
+ rmSync as rmSync4,
5243
+ writeFileSync as writeFileSync11
5244
+ } from "fs";
5245
+ import { dirname as dirname9, join as join16 } from "path";
5246
+ function registerTelemetry(program2) {
5247
+ const telemetry = program2.command("telemetry").description(
5248
+ "Emit WLP run telemetry (an executor leg's progress events) into a decomposition run"
5249
+ );
5250
+ telemetry.command("emit").description(
5251
+ "POST one progress event to /decompositions/{id}/run/telemetry (the 5a ingest)"
5252
+ ).requiredOption(
5253
+ "--decomposition <id>",
5254
+ "Decomposition id whose run this event belongs to"
5255
+ ).requiredOption("--task <id>", "Task id this event belongs to").requiredOption(
5256
+ "--kind <kind>",
5257
+ "Event kind: raw | parsed | approval | terminal"
5258
+ ).option(
5259
+ "--tokens-in <n>",
5260
+ "Cumulative input tokens (spend meter)",
5261
+ parseIntOpt
5262
+ ).option(
5263
+ "--tokens-out <n>",
5264
+ "Cumulative output tokens (spend meter)",
5265
+ parseIntOpt
5266
+ ).option(
5267
+ "--context-used <n>",
5268
+ "Context tokens currently used (occupancy meter)",
5269
+ parseIntOpt
5270
+ ).option(
5271
+ "--context-window <n>",
5272
+ "Context window size (occupancy meter)",
5273
+ parseIntOpt
5274
+ ).option("--text <s>", "Raw/parsed payload text").option("--approval <state>", "Approval gate state (approval events)").option(
5275
+ "--verdict <v>",
5276
+ "Typed verdict (terminal events): pass | soft-fail | plan-invalid | blocked"
5277
+ ).action(async (opts, cmd) => {
5278
+ const json = Boolean(cmd.optsWithGlobals().json);
5279
+ const cfg = resolveConfig(cmd.optsWithGlobals());
5280
+ const event = {
5281
+ taskId: opts.task,
5282
+ kind: normalizeKind(opts.kind),
5283
+ tokensIn: opts.tokensIn ?? null,
5284
+ tokensOut: opts.tokensOut ?? null,
5285
+ contextUsed: opts.contextUsed ?? null,
5286
+ contextWindow: opts.contextWindow ?? null,
5287
+ text: opts.text ?? null,
5288
+ approvalState: opts.approval ?? null,
5289
+ verdict: opts.verdict ?? null
5290
+ };
5291
+ let body;
5292
+ try {
5293
+ body = await postTelemetry(cfg, opts.decomposition, [event]);
5294
+ } catch (e) {
5295
+ return fail(`Telemetry emit failed: ${e.message}`);
5296
+ }
5297
+ if (json) {
5298
+ emit(body, true);
5299
+ } else {
5300
+ process.stderr.write(
5301
+ style.green("telemetry emitted") + style.dim(
5302
+ ` \u2014 ${event.kind} for task ${opts.task}; run now carries ${body.eventCount} event${body.eventCount === 1 ? "" : "s"}
5303
+ `
5304
+ )
5305
+ );
5306
+ }
5307
+ });
5308
+ telemetry.command("bind").description(
5309
+ "Bind this checkout to a decomposition+task so the Stop hook auto-emits per-turn telemetry"
5310
+ ).requiredOption(
5311
+ "--decomposition <id>",
5312
+ "Decomposition id this session executes"
5313
+ ).requiredOption("--task <id>", "Task id this session executes").action((opts, cmd) => {
5314
+ const json = Boolean(cmd.optsWithGlobals().json);
5315
+ const dir = join16(process.cwd(), ".sechroom");
5316
+ mkdirSync11(dir, { recursive: true });
5317
+ const path = join16(dir, BINDING_FILE);
5318
+ const binding = {
5319
+ decompositionId: opts.decomposition,
5320
+ taskId: opts.task
5321
+ };
5322
+ writeFileSync11(path, JSON.stringify(binding, null, 2) + "\n");
5323
+ ensureStateDirIgnored(process.cwd());
5324
+ if (json) {
5325
+ emit({ bound: true, ...binding, path }, true);
5326
+ } else {
5327
+ process.stdout.write(
5328
+ style.green("telemetry bound") + style.dim(
5329
+ ` \u2014 decomposition ${binding.decompositionId}, task ${binding.taskId} (${path})
5330
+ `
5331
+ )
5332
+ );
5333
+ }
5334
+ });
5335
+ telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
5336
+ const json = Boolean(cmd.optsWithGlobals().json);
5337
+ const path = join16(process.cwd(), ".sechroom", BINDING_FILE);
5338
+ const existed = existsSync14(path);
5339
+ if (existed) rmSync4(path);
5340
+ if (json) emit({ unbound: existed, path }, true);
5341
+ else
5342
+ process.stdout.write(
5343
+ existed ? "telemetry binding cleared\n" : "no telemetry binding to clear\n"
5344
+ );
5345
+ });
5346
+ telemetry.command("hook").description(
5347
+ "Per-turn telemetry self-report for a Claude Code Stop hook (reads stdin; no-op unless bound). Fail-soft."
5348
+ ).action(async (_opts, cmd) => {
5349
+ try {
5350
+ const raw = await readStdin2();
5351
+ const input = parseHookInput2(raw);
5352
+ const cwd = input.cwd ?? process.cwd();
5353
+ const binding = findBinding(cwd);
5354
+ if (!binding) return process.exit(0);
5355
+ const usage = input.transcript_path ? parseTranscript(input.transcript_path) : null;
5356
+ if (!usage) return process.exit(0);
5357
+ const cfg = resolveConfig(cmd.optsWithGlobals());
5358
+ await postTelemetry(cfg, binding.decompositionId, [
5359
+ {
5360
+ taskId: binding.taskId,
5361
+ kind: "Parsed",
5362
+ tokensIn: usage.tokensIn,
5363
+ tokensOut: usage.tokensOut,
5364
+ contextUsed: usage.contextUsed,
5365
+ contextWindow: usage.contextWindow,
5366
+ text: null,
5367
+ approvalState: null,
5368
+ verdict: null
5369
+ }
5370
+ ]);
5371
+ return process.exit(0);
5372
+ } catch {
5373
+ return process.exit(0);
5374
+ }
5375
+ });
5376
+ telemetry.command("install").description(
5377
+ "Wire the per-turn telemetry Stop hook into Claude Code settings (also folded into `sechroom hook install`)"
5378
+ ).option(
5379
+ "--scope <scope>",
5380
+ "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global"
5381
+ ).option("--local", "alias for --scope project").option("--dry-run", "Print what would change; write nothing").action((opts, cmd) => {
5382
+ const g = cmd.optsWithGlobals();
5383
+ const dryRun = Boolean(opts.dryRun);
5384
+ const cwd = process.cwd();
5385
+ let scope;
5386
+ try {
5387
+ scope = opts.local ? "project" : resolveScope(opts.scope);
5388
+ } catch (err2) {
5389
+ process.stderr.write(`${err2.message}
5390
+ `);
5391
+ return process.exit(2);
5392
+ }
5393
+ const targets = resolveClaudeTargets({
5394
+ override: g.claudeConfigDir,
5395
+ scope,
5396
+ cwd
5397
+ });
5398
+ const commands = { Stop: "sechroom telemetry hook" };
5399
+ try {
5400
+ const multi = targets.length > 1;
5401
+ const results = targets.map((t) => {
5402
+ const r = installClaudeCommands(t.dir, commands, dryRun);
5403
+ process.stdout.write(
5404
+ `${HOOK_SURFACE_LABEL.claude}${multi ? ` (${t.label})` : ""}:
5405
+ `
5406
+ );
5407
+ process.stdout.write(describe(r, dryRun) + "\n");
5408
+ return r;
5409
+ });
5410
+ if (dryRun) {
5411
+ process.stdout.write("\n(dry run \u2014 no files were written.)\n");
5412
+ } else if (results.every((r) => r.status === "current")) {
5413
+ process.stdout.write("\nAlready up to date \u2014 nothing to change.\n");
5414
+ } else {
5415
+ process.stdout.write(
5416
+ "\nRestart your agent for the hook to take effect, then bind a task with `sechroom telemetry bind`.\n"
5417
+ );
5418
+ }
5419
+ } catch (err2) {
5420
+ process.stderr.write(
5421
+ `telemetry install failed: ${err2.message}
5422
+ `
5423
+ );
5424
+ return process.exit(1);
5425
+ }
5426
+ warnIfSechroomNotOnPath();
5427
+ return process.exit(0);
5428
+ });
5429
+ }
5430
+ var BINDING_FILE = "telemetry.json";
5431
+ async function postTelemetry(cfg, decompositionId, events) {
5432
+ const token = await requireToken(cfg);
5433
+ const resp = await fetch(
5434
+ `${cfg.baseUrl}/decompositions/${encodeURIComponent(decompositionId)}/run/telemetry`,
5435
+ {
5436
+ method: "POST",
5437
+ headers: {
5438
+ authorization: `Bearer ${token}`,
5439
+ tenant: cfg.tenant,
5440
+ "content-type": "application/json",
5441
+ "x-sechroom-surface": "cli"
5442
+ },
5443
+ body: JSON.stringify({ events })
5444
+ }
5445
+ );
5446
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);
5447
+ return await resp.json();
5448
+ }
5449
+ function findBinding(start) {
5450
+ let dir = start;
5451
+ for (; ; ) {
5452
+ const path = join16(dir, ".sechroom", BINDING_FILE);
5453
+ if (existsSync14(path)) {
5454
+ try {
5455
+ const b = JSON.parse(
5456
+ readFileSync9(path, "utf8")
5457
+ );
5458
+ if (b.decompositionId && b.taskId)
5459
+ return { decompositionId: b.decompositionId, taskId: b.taskId };
5460
+ } catch {
5461
+ }
5462
+ return null;
5463
+ }
5464
+ const parent = dirname9(dir);
5465
+ if (parent === dir) return null;
5466
+ dir = parent;
5467
+ }
5468
+ }
5469
+ function parseTranscript(path) {
5470
+ if (!existsSync14(path)) return null;
5471
+ let tokensIn = 0;
5472
+ let tokensOut = 0;
5473
+ let contextUsed = 0;
5474
+ let model = "";
5475
+ for (const line of readFileSync9(path, "utf8").split("\n")) {
5476
+ if (!line.trim()) continue;
5477
+ let obj;
5478
+ try {
5479
+ obj = JSON.parse(line);
5480
+ } catch {
5481
+ continue;
5482
+ }
5483
+ const usage = obj.type === "assistant" ? obj.message?.usage : void 0;
5484
+ if (!usage) continue;
5485
+ const input = (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
5486
+ tokensIn += input;
5487
+ tokensOut += usage.output_tokens ?? 0;
5488
+ contextUsed = input;
5489
+ if (obj.message?.model) model = obj.message.model;
5490
+ }
5491
+ if (tokensIn === 0 && tokensOut === 0) return null;
5492
+ return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model) };
5493
+ }
5494
+ function windowFor(model) {
5495
+ const m = model.toLowerCase();
5496
+ return m.includes("[1m]") || m.includes("-1m") ? 1e6 : 2e5;
5497
+ }
5498
+ async function readStdin2() {
5499
+ if (process.stdin.isTTY) return "";
5500
+ const chunks = [];
5501
+ for await (const chunk of process.stdin) chunks.push(chunk);
5502
+ return Buffer.concat(chunks).toString("utf8");
5503
+ }
5504
+ function parseHookInput2(raw) {
5505
+ if (!raw.trim()) return {};
5506
+ try {
5507
+ return JSON.parse(raw);
5508
+ } catch {
5509
+ return {};
5510
+ }
5511
+ }
5512
+ var KINDS = {
5513
+ raw: "Raw",
5514
+ parsed: "Parsed",
5515
+ approval: "Approval",
5516
+ terminal: "Terminal"
5517
+ };
5518
+ function normalizeKind(k) {
5519
+ const v = KINDS[k.toLowerCase()];
5520
+ if (!v)
5521
+ fail(
5522
+ `Unknown --kind '${k}'. Expected one of: raw, parsed, approval, terminal.`
5523
+ );
5524
+ return v;
5525
+ }
5526
+ function parseIntOpt(v) {
5527
+ const n = Number.parseInt(v, 10);
5528
+ if (Number.isNaN(n)) fail(`Expected an integer, got '${v}'.`);
5529
+ return n;
5530
+ }
5531
+
5221
5532
  // src/commands/worklog.ts
5222
5533
  function registerWorklog(program2) {
5223
5534
  const worklog = program2.command("worklog").description("Append to the daily work log");
@@ -5418,7 +5729,7 @@ Examples:
5418
5729
  function resolveVersion() {
5419
5730
  try {
5420
5731
  const pkg = JSON.parse(
5421
- readFileSync8(new URL("../package.json", import.meta.url), "utf8")
5732
+ readFileSync10(new URL("../package.json", import.meta.url), "utf8")
5422
5733
  );
5423
5734
  return pkg.version ?? "0.0.0";
5424
5735
  } catch {
@@ -5592,6 +5903,7 @@ registerSkills(program);
5592
5903
  registerAgents(program);
5593
5904
  registerLane(program);
5594
5905
  registerChannel(program);
5906
+ registerTelemetry(program);
5595
5907
  registerReset(program);
5596
5908
  program.parseAsync().catch((err2) => {
5597
5909
  process.stderr.write(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sechroom/cli",
3
- "version": "2026.6.237-rc.9357cead",
3
+ "version": "2026.6.239-rc.6418da97",
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,7 +24,7 @@
24
24
  "registry": "https://registry.npmjs.org/"
25
25
  },
26
26
  "dependencies": {
27
- "@microsoft/signalr": "^8.0.7",
27
+ "@microsoft/signalr": "^10.0.0",
28
28
  "commander": "^12.1.0",
29
29
  "open": "^10.1.0",
30
30
  "openapi-fetch": "^0.13.0",