@sechroom/cli 2026.7.2 → 2026.7.3

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 +404 -342
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync10 } from "fs";
4
+ import { readFileSync as readFileSync11 } from "fs";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/auth.ts
@@ -1300,12 +1300,228 @@ workers your loop skills call (e.g. find-prior-art \u2192 substrate-miner).`
1300
1300
  }
1301
1301
 
1302
1302
  // src/commands/channel.ts
1303
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
1304
+ import { dirname as dirname4, join as join7 } from "path";
1303
1305
  import {
1304
1306
  HttpTransportType,
1305
1307
  HubConnectionBuilder
1306
1308
  } from "@microsoft/signalr";
1307
1309
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1308
1310
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1311
+
1312
+ // src/commands/hook-install.ts
1313
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
1314
+ import { delimiter, dirname as dirname3, join as join6 } from "path";
1315
+
1316
+ // src/setup/clients.ts
1317
+ import { existsSync as existsSync4 } from "fs";
1318
+ import { homedir as homedir3 } from "os";
1319
+ import { dirname as dirname2, join as join5 } from "path";
1320
+ function claudeDesktopConfigPath(home) {
1321
+ switch (process.platform) {
1322
+ case "darwin":
1323
+ return join5(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
1324
+ case "win32":
1325
+ return join5(process.env.APPDATA ?? join5(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
1326
+ default:
1327
+ return join5(home, ".config", "Claude", "claude_desktop_config.json");
1328
+ }
1329
+ }
1330
+ function clientTargets(cwd, opts = {}) {
1331
+ const home = homedir3();
1332
+ const claudeDir = opts.claudeDir ?? join5(home, ".claude");
1333
+ const codexHome = opts.codexHome ?? join5(home, ".codex");
1334
+ return {
1335
+ "claude-code": {
1336
+ key: "claude-code",
1337
+ label: "Claude Code",
1338
+ mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join5(cwd, ".mcp.json"), format: "json" },
1339
+ instruction: { surfaceKey: "claude-code", path: join5(cwd, "CLAUDE.md") }
1340
+ },
1341
+ "claude-desktop": {
1342
+ key: "claude-desktop",
1343
+ label: "Claude Desktop",
1344
+ mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
1345
+ instruction: { surfaceKey: "claude-desktop", path: join5(claudeDir, "CLAUDE.md") }
1346
+ },
1347
+ codex: {
1348
+ key: "codex",
1349
+ label: "Codex CLI",
1350
+ mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join5(codexHome, "config.toml"), format: "toml" },
1351
+ instruction: { surfaceKey: "chatgpt", path: join5(cwd, "AGENTS.md") }
1352
+ },
1353
+ cursor: {
1354
+ key: "cursor",
1355
+ label: "Cursor",
1356
+ mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join5(cwd, ".cursor", "mcp.json"), format: "json" },
1357
+ instruction: { surfaceKey: "chatgpt", path: join5(cwd, "AGENTS.md") }
1358
+ },
1359
+ antigravity: {
1360
+ key: "antigravity",
1361
+ label: "Google Antigravity",
1362
+ // FR-sechroom-247 — Antigravity reads MCP from a GLOBAL, home-relative
1363
+ // `~/.gemini/config/mcp_config.json` (not cwd; not affected by
1364
+ // CLAUDE_CONFIG_DIR / CODEX_HOME). The snippet — `serverUrl`-shaped, no
1365
+ // `type` — comes from the `antigravity` server surface, so we don't
1366
+ // hardcode it here. Instructions go in the project `AGENTS.md`
1367
+ // (cross-tool, shared with Codex/Cursor).
1368
+ mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path: join5(home, ".gemini", "config", "mcp_config.json"), format: "json" },
1369
+ instruction: { surfaceKey: "antigravity", path: join5(cwd, "AGENTS.md") }
1370
+ }
1371
+ };
1372
+ }
1373
+ var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor", "antigravity"];
1374
+ var DEFAULT_CLIENT_KEY = "claude-code";
1375
+ function detectInstalledClients(cwd) {
1376
+ const home = homedir3();
1377
+ const detected = [];
1378
+ if (resolveClaudeTargets({}).some((t) => existsSync4(t.dir))) detected.push("claude-code");
1379
+ if (existsSync4(dirname2(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
1380
+ if (resolveCodexHomes({}).some((d) => existsSync4(d))) detected.push("codex");
1381
+ if (existsSync4(join5(home, ".cursor")) || existsSync4(join5(cwd, ".cursor"))) detected.push("cursor");
1382
+ if (existsSync4(join5(home, ".gemini"))) detected.push("antigravity");
1383
+ return detected;
1384
+ }
1385
+
1386
+ // src/commands/hook-install.ts
1387
+ var CLAUDE_HOOK_COMMANDS = {
1388
+ SessionStart: "sechroom hook session-start",
1389
+ PreCompact: "sechroom hook pre-compact",
1390
+ SessionEnd: "sechroom hook session-end",
1391
+ // WLP telemetry tap (D-WLP-10) — per-turn executor self-report. No-op (exit 0) unless this
1392
+ // checkout is bound to a decomposition+task via `sechroom telemetry bind`, so it's safe to wire
1393
+ // for every Claude install. Claude-only (it parses a Claude Code transcript).
1394
+ Stop: "sechroom telemetry hook"
1395
+ };
1396
+ var CODEX_HOOK_COMMANDS = {
1397
+ SessionStart: "sechroom hook session-start",
1398
+ Stop: "sechroom hook session-end --debounce-minutes 10"
1399
+ };
1400
+ function hasHookCommand(config2, event, command) {
1401
+ const groups = config2.hooks?.[event] ?? [];
1402
+ return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
1403
+ }
1404
+ function mergeHooks(config2, commands) {
1405
+ config2.hooks ??= {};
1406
+ let added = 0;
1407
+ for (const [event, command] of Object.entries(commands)) {
1408
+ if (hasHookCommand(config2, event, command)) continue;
1409
+ const groups = config2.hooks[event] ??= [];
1410
+ groups.push({ hooks: [{ type: "command", command }] });
1411
+ added += 1;
1412
+ }
1413
+ return added;
1414
+ }
1415
+ function readJsonConfig2(path) {
1416
+ if (!existsSync5(path)) return {};
1417
+ const raw = readFileSync3(path, "utf8");
1418
+ if (!raw.trim()) return {};
1419
+ return JSON.parse(raw);
1420
+ }
1421
+ function installHooksJson(path, commands, dryRun) {
1422
+ const existed = existsSync5(path) && readFileSync3(path, "utf8").trim().length > 0;
1423
+ const config2 = readJsonConfig2(path);
1424
+ const added = mergeHooks(config2, commands);
1425
+ if (added === 0 && existed) return { path, status: "current" };
1426
+ if (!dryRun) {
1427
+ mkdirSync4(dirname3(path), { recursive: true });
1428
+ writeFileSync4(path, JSON.stringify(config2, null, 2) + "\n");
1429
+ }
1430
+ return { path, status: existed ? "merged" : "created" };
1431
+ }
1432
+ function installClaudeCommands(claudeDir, commands, dryRun) {
1433
+ return installHooksJson(join6(claudeDir, "settings.json"), commands, dryRun);
1434
+ }
1435
+ function ensureCodexFeaturesHooks(content) {
1436
+ const lines = content.split("\n");
1437
+ const headerIdx = lines.findIndex((l) => l.trim() === "[features]");
1438
+ if (headerIdx === -1) {
1439
+ const base = content.length === 0 || content.endsWith("\n") ? content : content + "\n";
1440
+ return { next: base + "\n[features]\nhooks = true\n", changed: true };
1441
+ }
1442
+ for (let i = headerIdx + 1; i < lines.length; i += 1) {
1443
+ const trimmed = lines[i].trim();
1444
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) break;
1445
+ const m = lines[i].match(/^(\s*)hooks(\s*)=(\s*)(.*)$/);
1446
+ if (!m) continue;
1447
+ const value = m[4].replace(/\s*#.*$/, "").trim();
1448
+ if (value === "true") return { next: content, changed: false };
1449
+ lines[i] = `${m[1]}hooks${m[2]}=${m[3]}true`;
1450
+ return { next: lines.join("\n"), changed: true };
1451
+ }
1452
+ lines.splice(headerIdx + 1, 0, "hooks = true");
1453
+ return { next: lines.join("\n"), changed: true };
1454
+ }
1455
+ function installCodexFeatureFlag(path, dryRun) {
1456
+ const existed = existsSync5(path);
1457
+ const content = existed ? readFileSync3(path, "utf8") : "";
1458
+ const { next, changed } = ensureCodexFeaturesHooks(content);
1459
+ if (!changed) return { path, status: "current" };
1460
+ if (!dryRun) {
1461
+ mkdirSync4(dirname3(path), { recursive: true });
1462
+ writeFileSync4(path, next);
1463
+ }
1464
+ return { path, status: existed ? "merged" : "created" };
1465
+ }
1466
+ function resolveSurfaces(surface, cwd) {
1467
+ if (surface === "claude") return ["claude"];
1468
+ if (surface === "codex") return ["codex"];
1469
+ if (surface === "both") return ["claude", "codex"];
1470
+ if (surface) throw new Error(`--surface must be one of claude | codex | both (got '${surface}')`);
1471
+ const surfaces = detectHookSurfaces(cwd);
1472
+ return surfaces.length > 0 ? surfaces : ["claude", "codex"];
1473
+ }
1474
+ function describe(result, dryRun) {
1475
+ if (result.status === "current") return ` \u2713 ${result.path} (already configured)`;
1476
+ const verb = dryRun ? "would" : result.status === "created" ? "created" : "updated";
1477
+ return ` \u2713 ${result.path} (${dryRun ? `${verb} ${result.status === "created" ? "create" : "update"}` : verb})`;
1478
+ }
1479
+ var HOOK_SURFACE_LABEL = {
1480
+ claude: "Claude Code",
1481
+ codex: "Codex"
1482
+ };
1483
+ function installHookSurfaces(surfaces, opts) {
1484
+ const out = [];
1485
+ for (const surface of surfaces) {
1486
+ if (surface === "claude") {
1487
+ const path = join6(opts.claudeDir, "settings.json");
1488
+ out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
1489
+ } else {
1490
+ const hooksJson = installHooksJson(join6(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
1491
+ const featureFlag = installCodexFeatureFlag(join6(opts.codexHome, "config.toml"), opts.dryRun);
1492
+ out.push({ surface, results: [hooksJson, featureFlag] });
1493
+ }
1494
+ }
1495
+ return out;
1496
+ }
1497
+ function detectHookSurfaces(cwd) {
1498
+ const detected = detectInstalledClients(cwd);
1499
+ const surfaces = [];
1500
+ if (detected.includes("claude-code")) surfaces.push("claude");
1501
+ if (detected.includes("codex")) surfaces.push("codex");
1502
+ return surfaces;
1503
+ }
1504
+ function isSechroomOnPath() {
1505
+ const pathEnv = process.env.PATH ?? "";
1506
+ if (!pathEnv) return false;
1507
+ const names = process.platform === "win32" ? ["sechroom.cmd", "sechroom.exe", "sechroom.bat", "sechroom"] : ["sechroom"];
1508
+ for (const dir of pathEnv.split(delimiter)) {
1509
+ if (!dir) continue;
1510
+ for (const name of names) {
1511
+ if (existsSync5(join6(dir, name))) return true;
1512
+ }
1513
+ }
1514
+ return false;
1515
+ }
1516
+ function warnIfSechroomNotOnPath(write = (s) => void process.stderr.write(s)) {
1517
+ if (isSechroomOnPath()) return false;
1518
+ write(
1519
+ "\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"
1520
+ );
1521
+ return true;
1522
+ }
1523
+
1524
+ // src/commands/channel.ts
1309
1525
  function registerChannel(program2) {
1310
1526
  const channel = program2.command("channel").description(
1311
1527
  "Receive matched substrate events over the held SignalR push leg (D-WLP-9)"
@@ -1387,6 +1603,52 @@ function registerChannel(program2) {
1387
1603
  );
1388
1604
  await holdOpen(conn);
1389
1605
  });
1606
+ channel.command("install").description(
1607
+ "Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
1608
+ ).option("--workspace <wsp...>", "Restrict dispatches to these workspace id(s)").option(
1609
+ "--tag <tag...>",
1610
+ "Tag(s) a dispatch must carry to match (repeatable). Default targets WLP task dispatches.",
1611
+ ["kind:task"]
1612
+ ).option(
1613
+ "--name <name>",
1614
+ "MCP server + subscription name (idempotent per name)",
1615
+ "sechroom-channel"
1616
+ ).option("--dry-run", "Print what would change; write nothing").action((opts) => {
1617
+ const path = join7(process.cwd(), ".mcp.json");
1618
+ const dryRun = Boolean(opts.dryRun);
1619
+ const args = ["channel", "mcp", "--name", opts.name];
1620
+ for (const w of opts.workspace ?? [])
1621
+ args.push("--workspace", w);
1622
+ for (const t of opts.tag ?? []) args.push("--tag", t);
1623
+ const entry = { command: "sechroom", args };
1624
+ const config2 = readMcpConfig(path);
1625
+ config2.mcpServers ??= {};
1626
+ const existing = config2.mcpServers[opts.name];
1627
+ const status = JSON.stringify(existing) === JSON.stringify(entry) ? "current" : existing ? "updated" : "created";
1628
+ if (status !== "current" && !dryRun) {
1629
+ config2.mcpServers[opts.name] = entry;
1630
+ mkdirSync5(dirname4(path), { recursive: true });
1631
+ writeFileSync5(path, JSON.stringify(config2, null, 2) + "\n");
1632
+ }
1633
+ const verb = status === "current" ? "already configured" : dryRun ? `would ${status === "created" ? "create" : "update"}` : status;
1634
+ process.stdout.write(`${style.green("channel")} ${path} (${verb})
1635
+ `);
1636
+ process.stdout.write(
1637
+ style.dim(` server "${opts.name}": sechroom ${args.join(" ")}
1638
+ `)
1639
+ );
1640
+ if (status !== "current") {
1641
+ process.stdout.write(
1642
+ style.dim(
1643
+ `
1644
+ Load it (Channels research preview) by launching your agent with:
1645
+ claude --dangerously-load-development-channels server:${opts.name}
1646
+ `
1647
+ )
1648
+ );
1649
+ }
1650
+ warnIfSechroomNotOnPath();
1651
+ });
1390
1652
  channel.addHelpText(
1391
1653
  "after",
1392
1654
  `
@@ -1395,11 +1657,23 @@ Examples:
1395
1657
  $ sechroom channel connect --tag kind:task --tag status:in-progress
1396
1658
  $ sechroom channel connect --workspace wsp_X --json | jq .
1397
1659
 
1398
- # As a Claude Code channel (research preview, v2.1.80+):
1399
- # add to .mcp.json: { "mcpServers": { "sechroom": { "command": "npx", "args": ["@sechroom/cli", "channel", "mcp"] } } }
1400
- # then: claude --dangerously-load-development-channels server:sechroom`
1660
+ # Wire it as a Claude Code channel MCP server (research preview, v2.1.80+):
1661
+ $ sechroom channel install --workspace wsp_X --tag kind:task --tag status:in-progress
1662
+ # then: claude --dangerously-load-development-channels server:sechroom-channel`
1401
1663
  );
1402
1664
  }
1665
+ function readMcpConfig(path) {
1666
+ if (!existsSync6(path)) return {};
1667
+ const raw = readFileSync4(path, "utf8");
1668
+ if (!raw.trim()) return {};
1669
+ try {
1670
+ return JSON.parse(raw);
1671
+ } catch {
1672
+ return fail(
1673
+ `Could not parse ${path} as JSON \u2014 fix or remove it before installing the channel.`
1674
+ );
1675
+ }
1676
+ }
1403
1677
  function readFilter(opts) {
1404
1678
  const tags = opts.tag ?? [];
1405
1679
  const workspaceScope = opts.workspace ?? [];
@@ -1560,28 +1834,28 @@ Examples:
1560
1834
  }
1561
1835
 
1562
1836
  // src/commands/checkpoint.ts
1563
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
1564
- import { dirname as dirname6, join as join9 } from "path";
1837
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
1838
+ import { dirname as dirname7, join as join10 } from "path";
1565
1839
 
1566
1840
  // src/commands/hook.ts
1567
1841
  import { createHash as createHash2 } from "crypto";
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";
1842
+ import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync6, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
1843
+ import { dirname as dirname6, join as join9 } from "path";
1570
1844
 
1571
1845
  // src/sem.ts
1572
- import { dirname as dirname2, join as join5 } from "path";
1573
- import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync4 } from "fs";
1574
- var SEM_FILE = join5(".sechroom", "lane.json");
1846
+ import { dirname as dirname5, join as join8 } from "path";
1847
+ import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync6 } from "fs";
1848
+ var SEM_FILE = join8(".sechroom", "lane.json");
1575
1849
  var STATE_DIR_NAME2 = ".sechroom";
1576
1850
  function localSemPath(cwd = process.cwd()) {
1577
- return join5(cwd, SEM_FILE);
1851
+ return join8(cwd, SEM_FILE);
1578
1852
  }
1579
1853
  function resolveSemPathForRead(start = process.cwd()) {
1580
1854
  let dir = start;
1581
1855
  while (true) {
1582
- const candidate = join5(dir, SEM_FILE);
1583
- if (existsSync4(candidate)) return candidate;
1584
- const parent = dirname2(dir);
1856
+ const candidate = join8(dir, SEM_FILE);
1857
+ if (existsSync7(candidate)) return candidate;
1858
+ const parent = dirname5(dir);
1585
1859
  if (parent === dir) return void 0;
1586
1860
  dir = parent;
1587
1861
  }
@@ -1591,23 +1865,23 @@ function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
1591
1865
  let dir = start;
1592
1866
  let gitPath;
1593
1867
  for (; ; ) {
1594
- const candidate = join5(dir, ".git");
1595
- if (existsSync4(candidate)) {
1868
+ const candidate = join8(dir, ".git");
1869
+ if (existsSync7(candidate)) {
1596
1870
  gitPath = candidate;
1597
1871
  break;
1598
1872
  }
1599
- const parent = dirname2(dir);
1873
+ const parent = dirname5(dir);
1600
1874
  if (parent === dir) break;
1601
1875
  dir = parent;
1602
1876
  }
1603
1877
  if (!gitPath || statSync(gitPath).isDirectory()) return lane;
1604
- const gitFile = readFileSync3(gitPath, "utf8");
1878
+ const gitFile = readFileSync5(gitPath, "utf8");
1605
1879
  const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
1606
1880
  if (!common) return lane;
1607
- const worktreesDir = join5(common[1], "worktrees");
1881
+ const worktreesDir = join8(common[1], "worktrees");
1608
1882
  const siblings = readdirSync(worktreesDir).filter((n) => {
1609
1883
  try {
1610
- return statSync(join5(worktreesDir, n)).isDirectory();
1884
+ return statSync(join8(worktreesDir, n)).isDirectory();
1611
1885
  } catch {
1612
1886
  return false;
1613
1887
  }
@@ -1628,12 +1902,12 @@ function serializeSem(values) {
1628
1902
  }
1629
1903
  function readSem(path) {
1630
1904
  const p = path ?? resolveSemPathForRead();
1631
- if (!p || !existsSync4(p)) return void 0;
1632
- return { path: p, values: parseLaneJson(readFileSync3(p, "utf8")) };
1905
+ if (!p || !existsSync7(p)) return void 0;
1906
+ return { path: p, values: parseLaneJson(readFileSync5(p, "utf8")) };
1633
1907
  }
1634
1908
  function readLocalSemValues(cwd = process.cwd()) {
1635
- const next = join5(cwd, SEM_FILE);
1636
- if (existsSync4(next)) return readSem(next)?.values ?? {};
1909
+ const next = join8(cwd, SEM_FILE);
1910
+ if (existsSync7(next)) return readSem(next)?.values ?? {};
1637
1911
  return {};
1638
1912
  }
1639
1913
  function parseLaneJson(text2) {
@@ -1650,8 +1924,8 @@ function parseLaneJson(text2) {
1650
1924
  }
1651
1925
  var STATE_DIR_IGNORE = `${STATE_DIR_NAME2}/`;
1652
1926
  function writeSem(values, path = localSemPath()) {
1653
- mkdirSync4(dirname2(path), { recursive: true });
1654
- writeFileSync4(path, serializeSem(values));
1927
+ mkdirSync6(dirname5(path), { recursive: true });
1928
+ writeFileSync6(path, serializeSem(values));
1655
1929
  ensureSemIgnored(path);
1656
1930
  ensureContinuityScaffold(path);
1657
1931
  return path;
@@ -1678,9 +1952,9 @@ var CONTINUITY_SCAFFOLD = JSON.stringify(
1678
1952
  ) + "\n";
1679
1953
  function ensureContinuityScaffold(semPath) {
1680
1954
  try {
1681
- const target = join5(dirname2(semPath), CONTINUITY_FILE_NAME);
1682
- if (existsSync4(target)) return;
1683
- writeFileSync4(target, CONTINUITY_SCAFFOLD);
1955
+ const target = join8(dirname5(semPath), CONTINUITY_FILE_NAME);
1956
+ if (existsSync7(target)) return;
1957
+ writeFileSync6(target, CONTINUITY_SCAFFOLD);
1684
1958
  } catch {
1685
1959
  }
1686
1960
  }
@@ -1693,8 +1967,8 @@ function ignoresSem(content) {
1693
1967
  function inGitRepo(startDir) {
1694
1968
  let dir = startDir;
1695
1969
  for (; ; ) {
1696
- if (existsSync4(join5(dir, ".git"))) return true;
1697
- const parent = dirname2(dir);
1970
+ if (existsSync7(join8(dir, ".git"))) return true;
1971
+ const parent = dirname5(dir);
1698
1972
  if (parent === dir) return false;
1699
1973
  dir = parent;
1700
1974
  }
@@ -1702,246 +1976,34 @@ function inGitRepo(startDir) {
1702
1976
  function resolveGitignoreTarget(startDir) {
1703
1977
  let dir = startDir;
1704
1978
  for (; ; ) {
1705
- const gi = join5(dir, ".gitignore");
1706
- if (existsSync4(gi)) return { path: gi, exists: true };
1707
- const parent = dirname2(dir);
1708
- if (existsSync4(join5(dir, ".git")) || parent === dir) {
1709
- return { path: join5(startDir, ".gitignore"), exists: false };
1979
+ const gi = join8(dir, ".gitignore");
1980
+ if (existsSync7(gi)) return { path: gi, exists: true };
1981
+ const parent = dirname5(dir);
1982
+ if (existsSync7(join8(dir, ".git")) || parent === dir) {
1983
+ return { path: join8(startDir, ".gitignore"), exists: false };
1710
1984
  }
1711
1985
  dir = parent;
1712
1986
  }
1713
1987
  }
1714
1988
  function ensureSemIgnored(semPath) {
1715
1989
  try {
1716
- const checkoutDir = dirname2(dirname2(semPath));
1990
+ const checkoutDir = dirname5(dirname5(semPath));
1717
1991
  if (!inGitRepo(checkoutDir)) return;
1718
1992
  const target = resolveGitignoreTarget(checkoutDir);
1719
1993
  if (target.exists) {
1720
- const content = readFileSync3(target.path, "utf8");
1994
+ const content = readFileSync5(target.path, "utf8");
1721
1995
  if (ignoresSem(content)) return;
1722
1996
  const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
1723
1997
  appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
1724
1998
  `);
1725
1999
  } else {
1726
- writeFileSync4(target.path, `${STATE_DIR_IGNORE}
2000
+ writeFileSync6(target.path, `${STATE_DIR_IGNORE}
1727
2001
  `);
1728
2002
  }
1729
2003
  } catch {
1730
2004
  }
1731
2005
  }
1732
2006
 
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
-
1737
- // src/setup/clients.ts
1738
- import { existsSync as existsSync5 } from "fs";
1739
- import { homedir as homedir3 } from "os";
1740
- import { dirname as dirname3, join as join6 } from "path";
1741
- function claudeDesktopConfigPath(home) {
1742
- switch (process.platform) {
1743
- case "darwin":
1744
- return join6(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
1745
- case "win32":
1746
- return join6(process.env.APPDATA ?? join6(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
1747
- default:
1748
- return join6(home, ".config", "Claude", "claude_desktop_config.json");
1749
- }
1750
- }
1751
- function clientTargets(cwd, opts = {}) {
1752
- const home = homedir3();
1753
- const claudeDir = opts.claudeDir ?? join6(home, ".claude");
1754
- const codexHome = opts.codexHome ?? join6(home, ".codex");
1755
- return {
1756
- "claude-code": {
1757
- key: "claude-code",
1758
- label: "Claude Code",
1759
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join6(cwd, ".mcp.json"), format: "json" },
1760
- instruction: { surfaceKey: "claude-code", path: join6(cwd, "CLAUDE.md") }
1761
- },
1762
- "claude-desktop": {
1763
- key: "claude-desktop",
1764
- label: "Claude Desktop",
1765
- mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
1766
- instruction: { surfaceKey: "claude-desktop", path: join6(claudeDir, "CLAUDE.md") }
1767
- },
1768
- codex: {
1769
- key: "codex",
1770
- label: "Codex CLI",
1771
- mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join6(codexHome, "config.toml"), format: "toml" },
1772
- instruction: { surfaceKey: "chatgpt", path: join6(cwd, "AGENTS.md") }
1773
- },
1774
- cursor: {
1775
- key: "cursor",
1776
- label: "Cursor",
1777
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join6(cwd, ".cursor", "mcp.json"), format: "json" },
1778
- instruction: { surfaceKey: "chatgpt", path: join6(cwd, "AGENTS.md") }
1779
- },
1780
- antigravity: {
1781
- key: "antigravity",
1782
- label: "Google Antigravity",
1783
- // FR-sechroom-247 — Antigravity reads MCP from a GLOBAL, home-relative
1784
- // `~/.gemini/config/mcp_config.json` (not cwd; not affected by
1785
- // CLAUDE_CONFIG_DIR / CODEX_HOME). The snippet — `serverUrl`-shaped, no
1786
- // `type` — comes from the `antigravity` server surface, so we don't
1787
- // hardcode it here. Instructions go in the project `AGENTS.md`
1788
- // (cross-tool, shared with Codex/Cursor).
1789
- mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path: join6(home, ".gemini", "config", "mcp_config.json"), format: "json" },
1790
- instruction: { surfaceKey: "antigravity", path: join6(cwd, "AGENTS.md") }
1791
- }
1792
- };
1793
- }
1794
- var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor", "antigravity"];
1795
- var DEFAULT_CLIENT_KEY = "claude-code";
1796
- function detectInstalledClients(cwd) {
1797
- const home = homedir3();
1798
- const detected = [];
1799
- if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir))) detected.push("claude-code");
1800
- if (existsSync5(dirname3(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
1801
- if (resolveCodexHomes({}).some((d) => existsSync5(d))) detected.push("codex");
1802
- if (existsSync5(join6(home, ".cursor")) || existsSync5(join6(cwd, ".cursor"))) detected.push("cursor");
1803
- if (existsSync5(join6(home, ".gemini"))) detected.push("antigravity");
1804
- return detected;
1805
- }
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
-
1945
2007
  // src/commands/hook.ts
1946
2008
  async function readStdin() {
1947
2009
  if (process.stdin.isTTY) return "";
@@ -1966,13 +2028,13 @@ function resolveLane(flagLane, cwd) {
1966
2028
  if (!base) return void 0;
1967
2029
  return applyWorktreeLaneSuffix(base, start);
1968
2030
  }
1969
- var INTENT_FILE = join8(".sechroom", "continuity.json");
2031
+ var INTENT_FILE = join9(".sechroom", "continuity.json");
1970
2032
  function resolveIntentPath(start) {
1971
2033
  let dir = start;
1972
2034
  for (; ; ) {
1973
- const candidate = join8(dir, INTENT_FILE);
1974
- if (existsSync7(candidate)) return candidate;
1975
- const parent = dirname5(dir);
2035
+ const candidate = join9(dir, INTENT_FILE);
2036
+ if (existsSync8(candidate)) return candidate;
2037
+ const parent = dirname6(dir);
1976
2038
  if (parent === dir) return void 0;
1977
2039
  dir = parent;
1978
2040
  }
@@ -1981,7 +2043,7 @@ function readIntent(start) {
1981
2043
  const path = resolveIntentPath(start);
1982
2044
  if (!path) return void 0;
1983
2045
  try {
1984
- return JSON.parse(readFileSync5(path, "utf8"));
2046
+ return JSON.parse(readFileSync6(path, "utf8"));
1985
2047
  } catch {
1986
2048
  return void 0;
1987
2049
  }
@@ -2023,14 +2085,14 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
2023
2085
  }
2024
2086
  function ledgerPath(start) {
2025
2087
  const intent = resolveIntentPath(start);
2026
- const dir = intent ? dirname5(intent) : join8(start, ".sechroom");
2027
- return join8(dir, ".checkpoint-state.json");
2088
+ const dir = intent ? dirname6(intent) : join9(start, ".sechroom");
2089
+ return join9(dir, ".checkpoint-state.json");
2028
2090
  }
2029
2091
  function readLedger(start) {
2030
2092
  try {
2031
2093
  const p = ledgerPath(start);
2032
- if (!existsSync7(p)) return {};
2033
- return JSON.parse(readFileSync5(p, "utf8"));
2094
+ if (!existsSync8(p)) return {};
2095
+ return JSON.parse(readFileSync6(p, "utf8"));
2034
2096
  } catch {
2035
2097
  return {};
2036
2098
  }
@@ -2077,13 +2139,13 @@ function recordPush(start, intent) {
2077
2139
  } catch {
2078
2140
  mtimeMs = void 0;
2079
2141
  }
2080
- mkdirSync6(dirname5(p), { recursive: true });
2142
+ mkdirSync7(dirname6(p), { recursive: true });
2081
2143
  const ledger = {
2082
2144
  lastEpochMs: Date.now(),
2083
2145
  lastMtimeMs: mtimeMs,
2084
2146
  lastHash: intentHash(intent)
2085
2147
  };
2086
- writeFileSync6(p, JSON.stringify(ledger) + "\n");
2148
+ writeFileSync7(p, JSON.stringify(ledger) + "\n");
2087
2149
  } catch {
2088
2150
  }
2089
2151
  }
@@ -2330,10 +2392,10 @@ Examples:
2330
2392
  const client = await makeClient(cfg);
2331
2393
  return client.POST("/continuity/snapshots", { body });
2332
2394
  });
2333
- const path = resolveIntentPath(cwd) ?? join9(cwd, INTENT_FILE);
2395
+ const path = resolveIntentPath(cwd) ?? join10(cwd, INTENT_FILE);
2334
2396
  const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
2335
- mkdirSync7(dirname6(path), { recursive: true });
2336
- writeFileSync7(path, JSON.stringify(fileBody, null, 2) + "\n");
2397
+ mkdirSync8(dirname7(path), { recursive: true });
2398
+ writeFileSync8(path, JSON.stringify(fileBody, null, 2) + "\n");
2337
2399
  recordPush(cwd, merged);
2338
2400
  if (json) {
2339
2401
  emit({ snapshotId: data.snapshotId, lane, scope, file: path }, true);
@@ -3017,8 +3079,8 @@ Examples:
3017
3079
 
3018
3080
  // src/setup/apply.ts
3019
3081
  import { createHash as createHash3 } from "crypto";
3020
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync8, existsSync as existsSync8 } from "fs";
3021
- import { dirname as dirname7 } from "path";
3082
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync9, existsSync as existsSync9 } from "fs";
3083
+ import { dirname as dirname8 } from "path";
3022
3084
  var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
3023
3085
  var MARKER_END = "<!-- @sechroom/cli:end";
3024
3086
  function normalizeBody(s) {
@@ -3071,22 +3133,22 @@ function parseManagedBlock(content, block) {
3071
3133
  return null;
3072
3134
  }
3073
3135
  function ensureDir2(path) {
3074
- mkdirSync8(dirname7(path), { recursive: true });
3136
+ mkdirSync9(dirname8(path), { recursive: true });
3075
3137
  }
3076
3138
  function readOr(path, fallback) {
3077
3139
  try {
3078
- return readFileSync6(path, "utf8");
3140
+ return readFileSync7(path, "utf8");
3079
3141
  } catch {
3080
3142
  return fallback;
3081
3143
  }
3082
3144
  }
3083
3145
  function mergeMcpJson(path, snippet, dryRun) {
3084
3146
  const incoming = JSON.parse(snippet);
3085
- const existed = existsSync8(path);
3147
+ const existed = existsSync9(path);
3086
3148
  let current = {};
3087
3149
  if (existed) {
3088
3150
  try {
3089
- current = JSON.parse(readFileSync6(path, "utf8"));
3151
+ current = JSON.parse(readFileSync7(path, "utf8"));
3090
3152
  } catch {
3091
3153
  return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
3092
3154
  }
@@ -3094,26 +3156,26 @@ function mergeMcpJson(path, snippet, dryRun) {
3094
3156
  current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
3095
3157
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
3096
3158
  ensureDir2(path);
3097
- writeFileSync8(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
3159
+ writeFileSync9(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
3098
3160
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
3099
3161
  }
3100
3162
  function mergeCodexToml(path, snippet, dryRun) {
3101
- const existed = existsSync8(path);
3163
+ const existed = existsSync9(path);
3102
3164
  let body = readOr(path, "");
3103
3165
  body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
3104
3166
  const trimmed = body.trim();
3105
3167
  const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
3106
3168
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
3107
3169
  ensureDir2(path);
3108
- writeFileSync8(path, next, { mode: 384 });
3170
+ writeFileSync9(path, next, { mode: 384 });
3109
3171
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
3110
3172
  }
3111
3173
  function writeInstructionBlock(path, write, dryRun) {
3112
- const existed = existsSync8(path);
3174
+ const existed = existsSync9(path);
3113
3175
  const next = computeBlockFile(readOr(path, ""), write);
3114
3176
  if (dryRun) return { kind: "instruction", path, status: "dry-run" };
3115
3177
  ensureDir2(path);
3116
- writeFileSync8(path, next);
3178
+ writeFileSync9(path, next);
3117
3179
  return { kind: "instruction", path, status: existed ? "merged" : "created" };
3118
3180
  }
3119
3181
  function computeBlockFile(current, write) {
@@ -3154,7 +3216,7 @@ function applyBlock(path, write, mode, dryRun) {
3154
3216
  const next = computeBlockFile(current, write);
3155
3217
  if (!dryRun) {
3156
3218
  ensureDir2(proposedPath);
3157
- writeFileSync8(proposedPath, next);
3219
+ writeFileSync9(proposedPath, next);
3158
3220
  }
3159
3221
  return {
3160
3222
  kind: "instruction",
@@ -3284,8 +3346,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
3284
3346
  }
3285
3347
 
3286
3348
  // src/setup/skills-offer.ts
3287
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
3288
- import { join as join10 } from "path";
3349
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
3350
+ import { join as join11 } from "path";
3289
3351
 
3290
3352
  // src/setup/lane-pin.ts
3291
3353
  var CODE_LANE_PREFIX_BY_CLIENT = {
@@ -3401,8 +3463,8 @@ Found ${summary} available to you for ${surface}.
3401
3463
  if (skills.length > 0) {
3402
3464
  const written = [];
3403
3465
  for (const s of skills) {
3404
- mkdirSync9(join10(sDir, s.name), { recursive: true });
3405
- writeFileSync9(join10(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
3466
+ mkdirSync10(join11(sDir, s.name), { recursive: true });
3467
+ writeFileSync10(join11(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
3406
3468
  written.push(s.name);
3407
3469
  }
3408
3470
  recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
@@ -3410,11 +3472,11 @@ Found ${summary} available to you for ${surface}.
3410
3472
  `);
3411
3473
  }
3412
3474
  if (agents.length > 0) {
3413
- mkdirSync9(aDir, { recursive: true });
3475
+ mkdirSync10(aDir, { recursive: true });
3414
3476
  const written = [];
3415
3477
  for (const a of agents) {
3416
3478
  const file = `${a.name}.md`;
3417
- writeFileSync9(join10(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
3479
+ writeFileSync10(join11(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
3418
3480
  written.push(file);
3419
3481
  }
3420
3482
  recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
@@ -3797,13 +3859,13 @@ Wired to namespace '${slug}'. Restart your AI client (or reload MCP) to pick it
3797
3859
  }
3798
3860
 
3799
3861
  // src/commands/onboard.ts
3800
- import { existsSync as existsSync10 } from "fs";
3801
- import { basename as basename2, join as join12 } from "path";
3862
+ import { existsSync as existsSync11 } from "fs";
3863
+ import { basename as basename2, join as join13 } from "path";
3802
3864
 
3803
3865
  // src/commands/fanout.ts
3804
3866
  import { spawnSync } from "child_process";
3805
- import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
3806
- import { isAbsolute, join as join11, resolve } from "path";
3867
+ import { existsSync as existsSync10, readFileSync as readFileSync8, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
3868
+ import { isAbsolute, join as join12, resolve } from "path";
3807
3869
  var ICON = {
3808
3870
  refresh: "\u21BB",
3809
3871
  bind: "+",
@@ -3823,21 +3885,21 @@ function discoverChildren(root) {
3823
3885
  const out = [];
3824
3886
  for (const name of names.sort()) {
3825
3887
  if (name.startsWith(".") || name === "node_modules") continue;
3826
- const dir = join11(root, name);
3888
+ const dir = join12(root, name);
3827
3889
  try {
3828
3890
  if (!statSync3(dir).isDirectory()) continue;
3829
3891
  } catch {
3830
3892
  continue;
3831
3893
  }
3832
- if (existsSync9(join11(dir, ".git")) || committedBindingPath(dir)) out.push(name);
3894
+ if (existsSync10(join12(dir, ".git")) || committedBindingPath(dir)) out.push(name);
3833
3895
  }
3834
3896
  return out;
3835
3897
  }
3836
3898
  function readManifest(path) {
3837
- if (!existsSync9(path)) return null;
3899
+ if (!existsSync10(path)) return null;
3838
3900
  let parsed;
3839
3901
  try {
3840
- parsed = JSON.parse(readFileSync7(path, "utf8"));
3902
+ parsed = JSON.parse(readFileSync8(path, "utf8"));
3841
3903
  } catch (err2) {
3842
3904
  throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
3843
3905
  }
@@ -4203,10 +4265,10 @@ async function chooseScope(scopeFlag, yes) {
4203
4265
  }
4204
4266
  async function planRecurseChild(entry, root, client, opts) {
4205
4267
  const dir = resolveChildDir(entry.path, root);
4206
- if (!existsSync10(dir)) {
4268
+ if (!existsSync11(dir)) {
4207
4269
  return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
4208
4270
  }
4209
- if (existsSync10(join12(dir, ".sechroom.json"))) {
4271
+ if (existsSync11(join13(dir, ".sechroom.json"))) {
4210
4272
  return {
4211
4273
  label: entry.path,
4212
4274
  dir,
@@ -4279,7 +4341,7 @@ This fan-out will pin the same lane in every repo:
4279
4341
  async function runRecurse(cfg, g, opts) {
4280
4342
  const { yes, dryRun, json } = opts;
4281
4343
  const root = process.cwd();
4282
- const manifestPath = join12(root, ".sechroom", "repos.json");
4344
+ const manifestPath = join13(root, ".sechroom", "repos.json");
4283
4345
  const fromManifest = readManifest(manifestPath);
4284
4346
  const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
4285
4347
  const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
@@ -4791,23 +4853,23 @@ Examples:
4791
4853
 
4792
4854
  // src/commands/reset.ts
4793
4855
  import { homedir as homedir4 } from "os";
4794
- import { join as join13 } from "path";
4795
- import { existsSync as existsSync11, readFileSync as readFileSync8, rmSync as rmSync3 } from "fs";
4856
+ import { join as join14 } from "path";
4857
+ import { existsSync as existsSync12, readFileSync as readFileSync9, rmSync as rmSync3 } from "fs";
4796
4858
  var SKILLS_LOCK2 = ".sechroom-skills.json";
4797
- var localSkillsDir = () => join13(process.cwd(), ".claude", "skills");
4798
- var globalSkillsDir = () => join13(homedir4(), ".claude", "skills");
4799
- var localAgentsDir = () => join13(process.cwd(), ".claude", "agents");
4800
- var globalAgentsDir = () => join13(homedir4(), ".claude", "agents");
4859
+ var localSkillsDir = () => join14(process.cwd(), ".claude", "skills");
4860
+ var globalSkillsDir = () => join14(homedir4(), ".claude", "skills");
4861
+ var localAgentsDir = () => join14(process.cwd(), ".claude", "agents");
4862
+ var globalAgentsDir = () => join14(homedir4(), ".claude", "agents");
4801
4863
  function removeMaterialisedSkills(dir) {
4802
4864
  const removed = [];
4803
- const lockPath = join13(dir, SKILLS_LOCK2);
4804
- if (!existsSync11(lockPath)) return removed;
4865
+ const lockPath = join14(dir, SKILLS_LOCK2);
4866
+ if (!existsSync12(lockPath)) return removed;
4805
4867
  try {
4806
- const lock = JSON.parse(readFileSync8(lockPath, "utf8"));
4868
+ const lock = JSON.parse(readFileSync9(lockPath, "utf8"));
4807
4869
  for (const entry of Object.values(lock)) {
4808
4870
  for (const name of entry.skills ?? []) {
4809
- const p = join13(dir, name);
4810
- if (existsSync11(p)) {
4871
+ const p = join14(dir, name);
4872
+ if (existsSync12(p)) {
4811
4873
  rmSync3(p, { recursive: true, force: true });
4812
4874
  removed.push(p);
4813
4875
  }
@@ -4852,18 +4914,18 @@ function registerReset(program2) {
4852
4914
  }
4853
4915
  }
4854
4916
  const removed = [];
4855
- const stateDir = join13(process.cwd(), ".sechroom");
4856
- if (existsSync11(stateDir)) {
4917
+ const stateDir = join14(process.cwd(), ".sechroom");
4918
+ if (existsSync12(stateDir)) {
4857
4919
  rmSync3(stateDir, { recursive: true, force: true });
4858
4920
  removed.push(stateDir);
4859
4921
  }
4860
- const legacyCfg = join13(process.cwd(), ".sechroom.json");
4861
- if (existsSync11(legacyCfg)) {
4922
+ const legacyCfg = join14(process.cwd(), ".sechroom.json");
4923
+ if (existsSync12(legacyCfg)) {
4862
4924
  rmSync3(legacyCfg, { force: true });
4863
4925
  removed.push(legacyCfg);
4864
4926
  }
4865
- const legacySem = join13(process.cwd(), ".sem");
4866
- if (existsSync11(legacySem)) {
4927
+ const legacySem = join14(process.cwd(), ".sem");
4928
+ if (existsSync12(legacySem)) {
4867
4929
  rmSync3(legacySem, { force: true });
4868
4930
  removed.push(legacySem);
4869
4931
  }
@@ -4889,8 +4951,8 @@ function registerReset(program2) {
4889
4951
  }
4890
4952
 
4891
4953
  // src/commands/skills.ts
4892
- import { existsSync as existsSync12, mkdirSync as mkdirSync10, statSync as statSync4, writeFileSync as writeFileSync10 } from "fs";
4893
- import { join as join14 } from "path";
4954
+ import { existsSync as existsSync13, mkdirSync as mkdirSync11, statSync as statSync4, writeFileSync as writeFileSync11 } from "fs";
4955
+ import { join as join15 } from "path";
4894
4956
  function filenameFromDisposition(header) {
4895
4957
  if (!header) return void 0;
4896
4958
  const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
@@ -4898,11 +4960,11 @@ function filenameFromDisposition(header) {
4898
4960
  }
4899
4961
  function resolveOutputPath(output, serverFilename) {
4900
4962
  const filename = serverFilename || "skills.zip";
4901
- if (!output) return join14(process.cwd(), filename);
4902
- const looksLikeDir = output.endsWith("/") || existsSync12(output) && statSync4(output).isDirectory();
4963
+ if (!output) return join15(process.cwd(), filename);
4964
+ const looksLikeDir = output.endsWith("/") || existsSync13(output) && statSync4(output).isDirectory();
4903
4965
  if (looksLikeDir) {
4904
- mkdirSync10(output, { recursive: true });
4905
- return join14(output, filename);
4966
+ mkdirSync11(output, { recursive: true });
4967
+ return join15(output, filename);
4906
4968
  }
4907
4969
  return output;
4908
4970
  }
@@ -4933,7 +4995,7 @@ async function downloadZip(label, call, output) {
4933
4995
  const buf = Buffer.from(res.data);
4934
4996
  const filename = filenameFromDisposition(res.response.headers.get("content-disposition")) ?? "skills.zip";
4935
4997
  const path = resolveOutputPath(output, filename);
4936
- writeFileSync10(path, buf);
4998
+ writeFileSync11(path, buf);
4937
4999
  return { path, bytes: buf.length, filename };
4938
5000
  }
4939
5001
  function registerSkills(program2) {
@@ -5101,12 +5163,12 @@ Examples:
5101
5163
  }
5102
5164
 
5103
5165
  // src/commands/sweep.ts
5104
- import { existsSync as existsSync13 } from "fs";
5105
- import { dirname as dirname8, join as join15, resolve as resolve2 } from "path";
5106
- var DEFAULT_MANIFEST = join15(".sechroom", "repos.json");
5166
+ import { existsSync as existsSync14 } from "fs";
5167
+ import { dirname as dirname9, join as join16, resolve as resolve2 } from "path";
5168
+ var DEFAULT_MANIFEST = join16(".sechroom", "repos.json");
5107
5169
  function planEntry(entry, root) {
5108
5170
  const dir = resolveChildDir(entry.path, root);
5109
- if (!existsSync13(dir)) {
5171
+ if (!existsSync14(dir)) {
5110
5172
  return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
5111
5173
  }
5112
5174
  if (committedBindingPath(dir)) {
@@ -5182,7 +5244,7 @@ Examples:
5182
5244
  `);
5183
5245
  return;
5184
5246
  }
5185
- const root = dirname8(dirname8(manifestPath));
5247
+ const root = dirname9(dirname9(manifestPath));
5186
5248
  const plans = repos.map((entry) => planEntry(entry, root));
5187
5249
  if (!json) {
5188
5250
  process.stderr.write(
@@ -5201,13 +5263,13 @@ Examples:
5201
5263
 
5202
5264
  // src/commands/telemetry.ts
5203
5265
  import {
5204
- existsSync as existsSync14,
5205
- mkdirSync as mkdirSync11,
5206
- readFileSync as readFileSync9,
5266
+ existsSync as existsSync15,
5267
+ mkdirSync as mkdirSync12,
5268
+ readFileSync as readFileSync10,
5207
5269
  rmSync as rmSync4,
5208
- writeFileSync as writeFileSync11
5270
+ writeFileSync as writeFileSync12
5209
5271
  } from "fs";
5210
- import { dirname as dirname9, join as join16 } from "path";
5272
+ import { dirname as dirname10, join as join17 } from "path";
5211
5273
  function registerTelemetry(program2) {
5212
5274
  const telemetry = program2.command("telemetry").description(
5213
5275
  "Emit WLP run telemetry (an executor leg's progress events) into a decomposition run"
@@ -5277,14 +5339,14 @@ function registerTelemetry(program2) {
5277
5339
  "Decomposition id this session executes"
5278
5340
  ).requiredOption("--task <id>", "Task id this session executes").action((opts, cmd) => {
5279
5341
  const json = Boolean(cmd.optsWithGlobals().json);
5280
- const dir = join16(process.cwd(), ".sechroom");
5281
- mkdirSync11(dir, { recursive: true });
5282
- const path = join16(dir, BINDING_FILE);
5342
+ const dir = join17(process.cwd(), ".sechroom");
5343
+ mkdirSync12(dir, { recursive: true });
5344
+ const path = join17(dir, BINDING_FILE);
5283
5345
  const binding = {
5284
5346
  decompositionId: opts.decomposition,
5285
5347
  taskId: opts.task
5286
5348
  };
5287
- writeFileSync11(path, JSON.stringify(binding, null, 2) + "\n");
5349
+ writeFileSync12(path, JSON.stringify(binding, null, 2) + "\n");
5288
5350
  ensureStateDirIgnored(process.cwd());
5289
5351
  if (json) {
5290
5352
  emit({ bound: true, ...binding, path }, true);
@@ -5299,8 +5361,8 @@ function registerTelemetry(program2) {
5299
5361
  });
5300
5362
  telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
5301
5363
  const json = Boolean(cmd.optsWithGlobals().json);
5302
- const path = join16(process.cwd(), ".sechroom", BINDING_FILE);
5303
- const existed = existsSync14(path);
5364
+ const path = join17(process.cwd(), ".sechroom", BINDING_FILE);
5365
+ const existed = existsSync15(path);
5304
5366
  if (existed) rmSync4(path);
5305
5367
  if (json) emit({ unbound: existed, path }, true);
5306
5368
  else
@@ -5414,11 +5476,11 @@ async function postTelemetry(cfg, decompositionId, events) {
5414
5476
  function findBinding(start) {
5415
5477
  let dir = start;
5416
5478
  for (; ; ) {
5417
- const path = join16(dir, ".sechroom", BINDING_FILE);
5418
- if (existsSync14(path)) {
5479
+ const path = join17(dir, ".sechroom", BINDING_FILE);
5480
+ if (existsSync15(path)) {
5419
5481
  try {
5420
5482
  const b = JSON.parse(
5421
- readFileSync9(path, "utf8")
5483
+ readFileSync10(path, "utf8")
5422
5484
  );
5423
5485
  if (b.decompositionId && b.taskId)
5424
5486
  return { decompositionId: b.decompositionId, taskId: b.taskId };
@@ -5426,18 +5488,18 @@ function findBinding(start) {
5426
5488
  }
5427
5489
  return null;
5428
5490
  }
5429
- const parent = dirname9(dir);
5491
+ const parent = dirname10(dir);
5430
5492
  if (parent === dir) return null;
5431
5493
  dir = parent;
5432
5494
  }
5433
5495
  }
5434
5496
  function parseTranscript(path) {
5435
- if (!existsSync14(path)) return null;
5497
+ if (!existsSync15(path)) return null;
5436
5498
  let tokensIn = 0;
5437
5499
  let tokensOut = 0;
5438
5500
  let contextUsed = 0;
5439
5501
  let model = "";
5440
- for (const line of readFileSync9(path, "utf8").split("\n")) {
5502
+ for (const line of readFileSync10(path, "utf8").split("\n")) {
5441
5503
  if (!line.trim()) continue;
5442
5504
  let obj;
5443
5505
  try {
@@ -5694,7 +5756,7 @@ Examples:
5694
5756
  function resolveVersion() {
5695
5757
  try {
5696
5758
  const pkg = JSON.parse(
5697
- readFileSync10(new URL("../package.json", import.meta.url), "utf8")
5759
+ readFileSync11(new URL("../package.json", import.meta.url), "utf8")
5698
5760
  );
5699
5761
  return pkg.version ?? "0.0.0";
5700
5762
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sechroom/cli",
3
- "version": "2026.7.2",
3
+ "version": "2026.7.3",
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",