@sechroom/cli 2026.6.18 → 2026.6.19

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 +509 -263
  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 readFileSync6 } from "fs";
4
+ import { readFileSync as readFileSync7 } from "fs";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/auth.ts
@@ -11,12 +11,14 @@ import open from "open";
11
11
 
12
12
  // src/config.ts
13
13
  import { homedir } from "os";
14
- import { join, dirname } from "path";
14
+ import { join, dirname, basename } from "path";
15
15
  import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from "fs";
16
16
  var CONFIG_DIR = join(homedir(), ".config", "sechroom");
17
17
  var CONFIG_FILE = join(CONFIG_DIR, "config.json");
18
18
  var TOKEN_FILE = join(CONFIG_DIR, "token.json");
19
- var LOCAL_CONFIG_NAME = ".sechroom.json";
19
+ var STATE_DIR_NAME = ".sechroom";
20
+ var LOCAL_CONFIG_NAME = join(STATE_DIR_NAME, "config.json");
21
+ var LEGACY_LOCAL_CONFIG_NAME = ".sechroom.json";
20
22
  var DEFAULT_BASE_URL = "https://app.sechroom.ai/api";
21
23
  var LOCAL_CONFIG_SCHEMA_VERSION = 2;
22
24
  function ensureDir() {
@@ -69,11 +71,18 @@ function findLocalConfigPath(start = process.cwd()) {
69
71
  for (; ; ) {
70
72
  const candidate = join(dir, LOCAL_CONFIG_NAME);
71
73
  if (existsSync(candidate)) return candidate;
74
+ const legacy = join(dir, LEGACY_LOCAL_CONFIG_NAME);
75
+ if (existsSync(legacy)) return legacy;
72
76
  const parent = dirname(dir);
73
77
  if (parent === dir) return void 0;
74
78
  dir = parent;
75
79
  }
76
80
  }
81
+ function localConfigWritePath(resolvedPath) {
82
+ const baseDir = resolvedPath ? dirname(resolvedPath) : process.cwd();
83
+ const dir = basename(baseDir) === STATE_DIR_NAME ? dirname(baseDir) : baseDir;
84
+ return join(dir, LOCAL_CONFIG_NAME);
85
+ }
77
86
  function readLocalConfig() {
78
87
  const path = findLocalConfigPath();
79
88
  if (!path) return {};
@@ -92,12 +101,16 @@ function readLocalConfig() {
92
101
  }
93
102
  }
94
103
  function writeLocalConfig(patch) {
95
- const path = findLocalConfigPath() ?? join(process.cwd(), LOCAL_CONFIG_NAME);
104
+ const readPath = findLocalConfigPath();
96
105
  let current = {};
97
- try {
98
- current = JSON.parse(readFileSync(path, "utf8"));
99
- } catch {
106
+ if (readPath) {
107
+ try {
108
+ current = JSON.parse(readFileSync(readPath, "utf8"));
109
+ } catch {
110
+ }
100
111
  }
112
+ const path = localConfigWritePath(readPath);
113
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
101
114
  const next = { ...current, ...patch, schemaVersion: LOCAL_CONFIG_SCHEMA_VERSION };
102
115
  writeFileSync(path, JSON.stringify(next, null, 2), { mode: 384 });
103
116
  return path;
@@ -1558,10 +1571,17 @@ Examples:
1558
1571
  });
1559
1572
  }
1560
1573
 
1574
+ // src/commands/hook.ts
1575
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
1576
+ import { homedir as homedir3 } from "os";
1577
+ import { dirname as dirname4, join as join4 } from "path";
1578
+
1561
1579
  // src/sem.ts
1562
- import { dirname as dirname2, join as join2 } from "path";
1580
+ import { basename as basename2, dirname as dirname2, join as join2 } from "path";
1563
1581
  import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1564
- var SEM_FILE = ".sem";
1582
+ var SEM_FILE = join2(".sechroom", "lane.json");
1583
+ var LEGACY_SEM_FILE = ".sem";
1584
+ var STATE_DIR_NAME2 = ".sechroom";
1565
1585
  function localSemPath(cwd = process.cwd()) {
1566
1586
  return join2(cwd, SEM_FILE);
1567
1587
  }
@@ -1570,6 +1590,8 @@ function resolveSemPathForRead(start = process.cwd()) {
1570
1590
  while (true) {
1571
1591
  const candidate = join2(dir, SEM_FILE);
1572
1592
  if (existsSync2(candidate)) return candidate;
1593
+ const legacy = join2(dir, LEGACY_SEM_FILE);
1594
+ if (existsSync2(legacy)) return legacy;
1573
1595
  const parent = dirname2(dir);
1574
1596
  if (parent === dir) return void 0;
1575
1597
  dir = parent;
@@ -1589,15 +1611,35 @@ function parseSem(text) {
1589
1611
  return out;
1590
1612
  }
1591
1613
  function serializeSem(values) {
1592
- const header = "# sechroom lane pin (per-location fallback) \u2014 resolved at runtime by operator skills.\n";
1593
- const body = Object.entries(values).map(([k, v]) => `${k} = ${v}`).join("\n");
1594
- return header + body + "\n";
1614
+ return JSON.stringify(values, null, 2) + "\n";
1595
1615
  }
1596
1616
  function readSem(path) {
1597
1617
  const p = path ?? resolveSemPathForRead();
1598
1618
  if (!p || !existsSync2(p)) return void 0;
1599
- return { path: p, values: parseSem(readFileSync2(p, "utf8")) };
1619
+ const text = readFileSync2(p, "utf8");
1620
+ const values = basename2(p) === LEGACY_SEM_FILE ? parseSem(text) : parseLaneJson(text);
1621
+ return { path: p, values };
1622
+ }
1623
+ function readLocalSemValues(cwd = process.cwd()) {
1624
+ const next = join2(cwd, SEM_FILE);
1625
+ if (existsSync2(next)) return readSem(next)?.values ?? {};
1626
+ const legacy = join2(cwd, LEGACY_SEM_FILE);
1627
+ if (existsSync2(legacy)) return readSem(legacy)?.values ?? {};
1628
+ return {};
1629
+ }
1630
+ function parseLaneJson(text) {
1631
+ try {
1632
+ const parsed = JSON.parse(text);
1633
+ const out = {};
1634
+ for (const [k, v] of Object.entries(parsed)) {
1635
+ if (typeof v === "string") out[k] = v;
1636
+ }
1637
+ return out;
1638
+ } catch {
1639
+ return {};
1640
+ }
1600
1641
  }
1642
+ var STATE_DIR_IGNORE = `${STATE_DIR_NAME2}/`;
1601
1643
  function writeSem(values, path = localSemPath()) {
1602
1644
  mkdirSync2(dirname2(path), { recursive: true });
1603
1645
  writeFileSync2(path, serializeSem(values));
@@ -1607,7 +1649,7 @@ function writeSem(values, path = localSemPath()) {
1607
1649
  function ignoresSem(content) {
1608
1650
  return content.split("\n").some((line) => {
1609
1651
  const t = line.trim();
1610
- return t === ".sem" || t === "/.sem" || t === "**/.sem";
1652
+ return t === STATE_DIR_NAME2 || t === STATE_DIR_IGNORE || t === `/${STATE_DIR_NAME2}` || t === `/${STATE_DIR_IGNORE}` || t === `**/${STATE_DIR_NAME2}` || t === `**/${STATE_DIR_IGNORE}`;
1611
1653
  });
1612
1654
  }
1613
1655
  function resolveGitignoreTarget(startDir) {
@@ -1624,20 +1666,198 @@ function resolveGitignoreTarget(startDir) {
1624
1666
  }
1625
1667
  function ensureSemIgnored(semPath) {
1626
1668
  try {
1627
- const target = resolveGitignoreTarget(dirname2(semPath));
1669
+ const checkoutDir = dirname2(dirname2(semPath));
1670
+ const target = resolveGitignoreTarget(checkoutDir);
1628
1671
  if (target.exists) {
1629
1672
  const content = readFileSync2(target.path, "utf8");
1630
1673
  if (ignoresSem(content)) return;
1631
1674
  const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
1632
- appendFileSync(target.path, `${sep}.sem
1675
+ appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
1633
1676
  `);
1634
1677
  } else {
1635
- writeFileSync2(target.path, ".sem\n");
1678
+ writeFileSync2(target.path, `${STATE_DIR_IGNORE}
1679
+ `);
1636
1680
  }
1637
1681
  } catch {
1638
1682
  }
1639
1683
  }
1640
1684
 
1685
+ // src/setup/clients.ts
1686
+ import { existsSync as existsSync3 } from "fs";
1687
+ import { homedir as homedir2 } from "os";
1688
+ import { dirname as dirname3, join as join3 } from "path";
1689
+
1690
+ // src/setup/operator-surface.ts
1691
+ var SectionType = {
1692
+ McpConfig: "mcp-config",
1693
+ McpConfigToml: "mcp-config-toml",
1694
+ InstructionFile: "instruction-file",
1695
+ ProjectConfig: "project-config",
1696
+ Verify: "verify",
1697
+ /** SBC-999 — workspace-pinned conventions, emitted only when the request
1698
+ * carried a workspaceId and that workspace has agent-setup-bundle memories. */
1699
+ WorkspaceConventions: "workspace-conventions"
1700
+ };
1701
+ async function fetchSetup(cfg) {
1702
+ const client = await makeClient(cfg);
1703
+ const { data, error } = await client.GET(
1704
+ "/operator-surface/setup",
1705
+ cfg.workspaceId ? { params: { query: { workspaceId: cfg.workspaceId } } } : {}
1706
+ );
1707
+ if (error) throw new Error(`GET /operator-surface/setup failed: ${JSON.stringify(error)}`);
1708
+ return data;
1709
+ }
1710
+ function findSurface(setup, surfaceKey) {
1711
+ return setup.surfaces.find((s) => s.surfaceKey === surfaceKey);
1712
+ }
1713
+ function findSection(surface, sectionType) {
1714
+ return surface?.sections.find((s) => s.sectionType === sectionType);
1715
+ }
1716
+ function sectionSnippet(section) {
1717
+ if (!section) return null;
1718
+ for (const step of section.steps) {
1719
+ if (step.copyValue) return step.copyValue;
1720
+ if (step.codeSnippet) return step.codeSnippet;
1721
+ }
1722
+ return null;
1723
+ }
1724
+ function parseTagArtifactId(id) {
1725
+ if (!id.startsWith("tag:")) return null;
1726
+ const tags = id.slice("tag:".length).split(",").map((t) => t.trim()).filter((t) => t.length > 0);
1727
+ return tags.length > 0 ? tags : null;
1728
+ }
1729
+ async function getPersonalWorkspaceId(cfg) {
1730
+ const client = await makeClient(cfg);
1731
+ const { data } = await client.GET("/me/personal-workspace", {});
1732
+ return data?.workspaceId ?? null;
1733
+ }
1734
+ async function fetchMemoryFields(cfg, id) {
1735
+ const client = await makeClient(cfg);
1736
+ const { data } = await client.GET("/memories/{memoryId}", { params: { path: { memoryId: id } } });
1737
+ const env = data;
1738
+ const m = env?.item ?? env;
1739
+ if (!m) return null;
1740
+ const version = typeof m.currentVersion === "string" ? Number(m.currentVersion) : m.currentVersion;
1741
+ return { text: m.text, title: m.title, tags: m.tags, version: Number.isFinite(version) ? version : void 0 };
1742
+ }
1743
+ async function resolveInstruction(cfg, section, personalWorkspaceId) {
1744
+ const client = await makeClient(cfg);
1745
+ for (const artifact of section.artifacts) {
1746
+ const tags = parseTagArtifactId(artifact.id);
1747
+ if (!tags) continue;
1748
+ const { data } = await client.POST("/memories/search", {
1749
+ body: { query: null, textQuery: null, semanticQuery: null, hybrid: false, limit: 1, includeArchived: false, includeSystem: false, tags }
1750
+ });
1751
+ const hits = data ?? [];
1752
+ if (hits.length === 0) continue;
1753
+ const templateId = hits[0].id;
1754
+ const template = await fetchMemoryFields(cfg, templateId);
1755
+ if (typeof template?.text !== "string" || template.text.length === 0) continue;
1756
+ const templateTags = template.tags ?? tags;
1757
+ if (personalWorkspaceId) {
1758
+ const { data: ovr } = await client.POST("/memories/search", {
1759
+ body: { query: null, textQuery: null, semanticQuery: null, hybrid: false, limit: 1, includeArchived: false, includeSystem: false, tags: ["sechroom:role:override", `sechroom:template-ref:${templateId}`], owner: { type: "Workspace", id: personalWorkspaceId } }
1760
+ });
1761
+ const ovrHits = ovr ?? [];
1762
+ if (ovrHits.length > 0) {
1763
+ const override = await fetchMemoryFields(cfg, ovrHits[0].id);
1764
+ if (typeof override?.text === "string" && override.text.length > 0) {
1765
+ return { title: override.title ?? template.title ?? artifact.title, body: override.text, source: "override", templateId, templateTags, sourceRef: `${ovrHits[0].id}@v${override.version ?? 1}` };
1766
+ }
1767
+ }
1768
+ }
1769
+ return { title: template.title ?? artifact.title, body: template.text, source: "template", templateId, templateTags, sourceRef: `${templateId}@v${template.version ?? 1}` };
1770
+ }
1771
+ return null;
1772
+ }
1773
+ async function resolveWorkspaceConventions(cfg, section) {
1774
+ const parts = [];
1775
+ const refs = [];
1776
+ for (const artifact of section.artifacts) {
1777
+ if (parseTagArtifactId(artifact.id)) continue;
1778
+ const mem = await fetchMemoryFields(cfg, artifact.id);
1779
+ if (typeof mem?.text === "string" && mem.text.trim().length > 0) {
1780
+ parts.push(mem.text.trim());
1781
+ refs.push(`${artifact.id}@v${mem.version ?? 1}`);
1782
+ }
1783
+ }
1784
+ if (parts.length === 0) return null;
1785
+ return { body: parts.join("\n\n---\n\n"), refs };
1786
+ }
1787
+ async function createOverride(cfg, template, personalWorkspaceId) {
1788
+ const client = await makeClient(cfg);
1789
+ const overrideTags = template.templateTags.filter(
1790
+ (t) => t !== "sechroom:role:template" && !t.startsWith("sechroom:bundle:") && !t.startsWith("sechroom:template-ref:")
1791
+ );
1792
+ overrideTags.push("sechroom:role:override", `sechroom:template-ref:${template.templateId}`);
1793
+ const { error } = await client.POST("/memories", {
1794
+ body: {
1795
+ text: template.body,
1796
+ type: "reference",
1797
+ content: "{}",
1798
+ confidence: 1,
1799
+ source: "cli-agent-instructions-customize",
1800
+ archetype: "Document",
1801
+ title: template.title ?? null,
1802
+ tags: overrideTags,
1803
+ owner: { type: "Workspace", id: personalWorkspaceId }
1804
+ }
1805
+ });
1806
+ if (error) throw new Error(`creating personal copy failed: ${JSON.stringify(error)}`);
1807
+ }
1808
+
1809
+ // src/setup/clients.ts
1810
+ function claudeDesktopConfigPath(home) {
1811
+ switch (process.platform) {
1812
+ case "darwin":
1813
+ return join3(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
1814
+ case "win32":
1815
+ return join3(process.env.APPDATA ?? join3(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
1816
+ default:
1817
+ return join3(home, ".config", "Claude", "claude_desktop_config.json");
1818
+ }
1819
+ }
1820
+ function clientTargets(cwd) {
1821
+ const home = homedir2();
1822
+ return {
1823
+ "claude-code": {
1824
+ key: "claude-code",
1825
+ label: "Claude Code",
1826
+ mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join3(cwd, ".mcp.json"), format: "json" },
1827
+ instruction: { surfaceKey: "claude-code", path: join3(cwd, "CLAUDE.md") }
1828
+ },
1829
+ "claude-desktop": {
1830
+ key: "claude-desktop",
1831
+ label: "Claude Desktop",
1832
+ mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
1833
+ instruction: { surfaceKey: "claude-desktop", path: join3(home, ".claude", "CLAUDE.md") }
1834
+ },
1835
+ codex: {
1836
+ key: "codex",
1837
+ label: "Codex CLI",
1838
+ mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join3(home, ".codex", "config.toml"), format: "toml" },
1839
+ instruction: { surfaceKey: "chatgpt", path: join3(cwd, "AGENTS.md") }
1840
+ },
1841
+ cursor: {
1842
+ key: "cursor",
1843
+ label: "Cursor",
1844
+ mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join3(cwd, ".cursor", "mcp.json"), format: "json" },
1845
+ instruction: { surfaceKey: "chatgpt", path: join3(cwd, "AGENTS.md") }
1846
+ }
1847
+ };
1848
+ }
1849
+ var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor"];
1850
+ var DEFAULT_CLIENT_KEY = "claude-code";
1851
+ function detectInstalledClients(cwd) {
1852
+ const home = homedir2();
1853
+ const detected = [];
1854
+ if (existsSync3(join3(home, ".claude"))) detected.push("claude-code");
1855
+ if (existsSync3(dirname3(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
1856
+ if (existsSync3(join3(home, ".codex"))) detected.push("codex");
1857
+ if (existsSync3(join3(home, ".cursor")) || existsSync3(join3(cwd, ".cursor"))) detected.push("cursor");
1858
+ return detected;
1859
+ }
1860
+
1641
1861
  // src/commands/hook.ts
1642
1862
  async function readStdin() {
1643
1863
  if (process.stdin.isTTY) return "";
@@ -1661,6 +1881,31 @@ function resolveLane(flagLane, cwd) {
1661
1881
  const sem = readSem(resolveSemPathForRead(start));
1662
1882
  return sem?.values["code-lane"];
1663
1883
  }
1884
+ var INTENT_FILE = join4(".sechroom", "continuity.json");
1885
+ function resolveIntentPath(start) {
1886
+ let dir = start;
1887
+ for (; ; ) {
1888
+ const candidate = join4(dir, INTENT_FILE);
1889
+ if (existsSync4(candidate)) return candidate;
1890
+ const parent = dirname4(dir);
1891
+ if (parent === dir) return void 0;
1892
+ dir = parent;
1893
+ }
1894
+ }
1895
+ function readIntent(start) {
1896
+ const path = resolveIntentPath(start);
1897
+ if (!path) return void 0;
1898
+ try {
1899
+ return JSON.parse(readFileSync3(path, "utf8"));
1900
+ } catch {
1901
+ return void 0;
1902
+ }
1903
+ }
1904
+ function hasRequiredIntent(i) {
1905
+ return Boolean(
1906
+ i.objective?.trim() && i.state?.trim() && i.lastAction?.trim() && i.nextAction?.trim() && i.resumeInstruction?.trim()
1907
+ );
1908
+ }
1664
1909
  function formatContext(bundle, lane) {
1665
1910
  const s = bundle?.latestSnapshot;
1666
1911
  if (!s) return null;
@@ -1697,18 +1942,108 @@ function emitSessionStart(additionalContext) {
1697
1942
  }) + "\n"
1698
1943
  );
1699
1944
  }
1945
+ var HOOK_COMMANDS = {
1946
+ SessionStart: "sechroom hook session-start",
1947
+ PreCompact: "sechroom hook pre-compact"
1948
+ };
1949
+ var HOOK_EVENTS = ["SessionStart", "PreCompact"];
1950
+ function hasHookCommand(config2, event, command) {
1951
+ const groups = config2.hooks?.[event] ?? [];
1952
+ return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
1953
+ }
1954
+ function mergeHooks(config2) {
1955
+ config2.hooks ??= {};
1956
+ let added = 0;
1957
+ for (const event of HOOK_EVENTS) {
1958
+ const command = HOOK_COMMANDS[event];
1959
+ if (hasHookCommand(config2, event, command)) continue;
1960
+ const groups = config2.hooks[event] ??= [];
1961
+ groups.push({ hooks: [{ type: "command", command }] });
1962
+ added += 1;
1963
+ }
1964
+ return added;
1965
+ }
1966
+ function readJsonConfig(path) {
1967
+ if (!existsSync4(path)) return {};
1968
+ const raw = readFileSync3(path, "utf8");
1969
+ if (!raw.trim()) return {};
1970
+ return JSON.parse(raw);
1971
+ }
1972
+ function installHooksJson(path, dryRun) {
1973
+ const existed = existsSync4(path) && readFileSync3(path, "utf8").trim().length > 0;
1974
+ const config2 = readJsonConfig(path);
1975
+ const added = mergeHooks(config2);
1976
+ if (added === 0 && existed) return { path, status: "current" };
1977
+ if (!dryRun) {
1978
+ mkdirSync3(dirname4(path), { recursive: true });
1979
+ writeFileSync3(path, JSON.stringify(config2, null, 2) + "\n");
1980
+ }
1981
+ return { path, status: existed ? "merged" : "created" };
1982
+ }
1983
+ function ensureCodexFeaturesHooks(content) {
1984
+ const lines = content.split("\n");
1985
+ const headerIdx = lines.findIndex((l) => l.trim() === "[features]");
1986
+ if (headerIdx === -1) {
1987
+ const base = content.length === 0 || content.endsWith("\n") ? content : content + "\n";
1988
+ return { next: base + "\n[features]\nhooks = true\n", changed: true };
1989
+ }
1990
+ for (let i = headerIdx + 1; i < lines.length; i += 1) {
1991
+ const trimmed = lines[i].trim();
1992
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) break;
1993
+ const m = lines[i].match(/^(\s*)hooks(\s*)=(\s*)(.*)$/);
1994
+ if (!m) continue;
1995
+ const value = m[4].replace(/\s*#.*$/, "").trim();
1996
+ if (value === "true") return { next: content, changed: false };
1997
+ lines[i] = `${m[1]}hooks${m[2]}=${m[3]}true`;
1998
+ return { next: lines.join("\n"), changed: true };
1999
+ }
2000
+ lines.splice(headerIdx + 1, 0, "hooks = true");
2001
+ return { next: lines.join("\n"), changed: true };
2002
+ }
2003
+ function installCodexFeatureFlag(path, dryRun) {
2004
+ const existed = existsSync4(path);
2005
+ const content = existed ? readFileSync3(path, "utf8") : "";
2006
+ const { next, changed } = ensureCodexFeaturesHooks(content);
2007
+ if (!changed) return { path, status: "current" };
2008
+ if (!dryRun) {
2009
+ mkdirSync3(dirname4(path), { recursive: true });
2010
+ writeFileSync3(path, next);
2011
+ }
2012
+ return { path, status: existed ? "merged" : "created" };
2013
+ }
2014
+ function resolveSurfaces(surface, cwd) {
2015
+ if (surface === "claude") return ["claude"];
2016
+ if (surface === "codex") return ["codex"];
2017
+ if (surface === "both") return ["claude", "codex"];
2018
+ if (surface) throw new Error(`--surface must be one of claude | codex | both (got '${surface}')`);
2019
+ const detected = detectInstalledClients(cwd);
2020
+ const surfaces = [];
2021
+ if (detected.includes("claude-code")) surfaces.push("claude");
2022
+ if (detected.includes("codex")) surfaces.push("codex");
2023
+ return surfaces.length > 0 ? surfaces : ["claude", "codex"];
2024
+ }
2025
+ function describe(result, dryRun) {
2026
+ if (result.status === "current") return ` \u2713 ${result.path} (already configured)`;
2027
+ const verb = dryRun ? "would" : result.status === "created" ? "created" : "updated";
2028
+ return ` \u2713 ${result.path} (${dryRun ? `${verb} ${result.status === "created" ? "create" : "update"}` : verb})`;
2029
+ }
1700
2030
  function registerHook(program2) {
1701
2031
  const hook = program2.command("hook").description("Agent-lifecycle hook adapter (Claude Code / Codex) \u2014 bridges hooks to continuity");
1702
2032
  hook.addHelpText(
1703
2033
  "after",
1704
2034
  `
1705
2035
  Examples:
1706
- # Wire into a SessionStart hook (the surface pipes the hook payload on stdin):
2036
+ # SessionStart (load): inject the lane's latest snapshot as context.
1707
2037
  $ echo '{"hook_event_name":"SessionStart","cwd":"'"$PWD"'"}' | sechroom hook session-start
1708
- $ sechroom hook session-start --lane claude-code-chris override the .sem pin
2038
+ # PreCompact (save): snapshot from ./${INTENT_FILE} before the agent compacts.
2039
+ $ echo '{"hook_event_name":"PreCompact","cwd":"'"$PWD"'"}' | sechroom hook pre-compact
2040
+ # Wire both hooks into the installed surface(s)' config (no hand-editing):
2041
+ $ sechroom hook install auto-detect Claude Code / Codex
2042
+ $ sechroom hook install --surface codex Codex only
2043
+ $ sechroom hook install --local --dry-run preview the project .claude/settings.json
1709
2044
 
1710
2045
  Lane source (high -> low): --lane > SECHROOM_LANE > ./.sem code-lane (D-binding-5).
1711
- Fail-soft: no lane / no auth / API error -> exit 0, no context injected, never blocks.`
2046
+ Fail-soft: no lane / no auth / no-or-partial intent file / API error -> exit 0, never blocks.`
1712
2047
  );
1713
2048
  hook.command("session-start").description("Resume the checkout's lane and emit continuity context for a SessionStart hook").option("--lane <laneId>", "Override the resolved lane (else SECHROOM_LANE, else ./.sem code-lane)").option("--surface <surface>", "Target surface: claude | codex (output is identical for session-start)", "claude").option("--max-artifacts <n>", "Cap artifacts in the resume bundle").action(async (opts, cmd) => {
1714
2049
  try {
@@ -1734,6 +2069,88 @@ Fail-soft: no lane / no auth / API error -> exit 0, no context injected, never b
1734
2069
  return process.exit(0);
1735
2070
  }
1736
2071
  });
2072
+ hook.command("pre-compact").description("Save a continuity snapshot from the agent-maintained intent file on a PreCompact hook").option("--lane <laneId>", "Override the resolved lane (else SECHROOM_LANE, else ./.sem code-lane)").option("--scope <scope>", "Snapshot scope (else the intent file's `scope`, else 'compaction')").option("--surface <surface>", "Target surface: claude | codex (lifecycle-only on both)", "claude").action(async (opts, cmd) => {
2073
+ try {
2074
+ const raw = await readStdin();
2075
+ const input = parseHookInput(raw);
2076
+ const cwd = input.cwd ?? process.cwd();
2077
+ const lane = resolveLane(opts.lane, input.cwd);
2078
+ if (!lane) return process.exit(0);
2079
+ const intent = readIntent(cwd);
2080
+ if (!intent || !hasRequiredIntent(intent)) return process.exit(0);
2081
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2082
+ const client = await makeClient(cfg);
2083
+ await client.POST("/continuity/snapshots", {
2084
+ body: {
2085
+ laneId: lane,
2086
+ scope: opts.scope ?? intent.scope ?? "compaction",
2087
+ currentObjective: intent.objective,
2088
+ currentState: intent.state,
2089
+ lastMeaningfulAction: intent.lastAction,
2090
+ nextIntendedAction: intent.nextAction,
2091
+ resumeInstruction: intent.resumeInstruction,
2092
+ activeConstraints: intent.constraints ?? null,
2093
+ openQuestions: intent.questions ?? null,
2094
+ surfaceMarkers: intent.surfaceMarkers ?? null,
2095
+ relevantArtifactIds: intent.artifacts ?? null,
2096
+ confidence: intent.confidence ?? null,
2097
+ // Compaction is infrequent, so the FR-051 clobber guard doesn't bite;
2098
+ // Acknowledge lets a within-window checkpoint land on the lane.
2099
+ concurrentSessionPolicy: "Acknowledge"
2100
+ }
2101
+ });
2102
+ return process.exit(0);
2103
+ } catch {
2104
+ return process.exit(0);
2105
+ }
2106
+ });
2107
+ hook.command("install").description("Wire the session-start + pre-compact hooks into Claude Code and/or Codex config").option("--surface <surface>", "Target surface: claude | codex | both (default: auto-detect installed surfaces)").option("--local", "Claude Code only: write <cwd>/.claude/settings.json instead of ~/.claude/settings.json").option("--dry-run", "Print what would change; write nothing").action((opts) => {
2108
+ const dryRun = Boolean(opts.dryRun);
2109
+ const cwd = process.cwd();
2110
+ let surfaces;
2111
+ try {
2112
+ surfaces = resolveSurfaces(opts.surface, cwd);
2113
+ } catch (err2) {
2114
+ process.stderr.write(`${err2.message}
2115
+ `);
2116
+ return process.exit(2);
2117
+ }
2118
+ const home = homedir3();
2119
+ const results = [];
2120
+ try {
2121
+ for (const surface of surfaces) {
2122
+ if (surface === "claude") {
2123
+ const path = opts.local ? join4(cwd, ".claude", "settings.json") : join4(home, ".claude", "settings.json");
2124
+ process.stdout.write(`Claude Code:
2125
+ `);
2126
+ const r = installHooksJson(path, dryRun);
2127
+ results.push(r);
2128
+ process.stdout.write(describe(r, dryRun) + "\n");
2129
+ } else {
2130
+ process.stdout.write(`Codex:
2131
+ `);
2132
+ const hooksJson = installHooksJson(join4(home, ".codex", "hooks.json"), dryRun);
2133
+ results.push(hooksJson);
2134
+ process.stdout.write(describe(hooksJson, dryRun) + "\n");
2135
+ const featureFlag = installCodexFeatureFlag(join4(home, ".codex", "config.toml"), dryRun);
2136
+ results.push(featureFlag);
2137
+ process.stdout.write(describe(featureFlag, dryRun) + "\n");
2138
+ }
2139
+ }
2140
+ } catch (err2) {
2141
+ process.stderr.write(`hook install failed: ${err2.message}
2142
+ `);
2143
+ return process.exit(1);
2144
+ }
2145
+ if (dryRun) {
2146
+ process.stdout.write("\n(dry run \u2014 no files were written.)\n");
2147
+ } else if (results.every((r) => r.status === "current")) {
2148
+ process.stdout.write("\nAlready up to date \u2014 nothing to change.\n");
2149
+ } else {
2150
+ process.stdout.write("\nRestart (or reload) your agent for the hooks to take effect.\n");
2151
+ }
2152
+ return process.exit(0);
2153
+ });
1737
2154
  }
1738
2155
 
1739
2156
  // src/commands/account.ts
@@ -1953,129 +2370,8 @@ Examples:
1953
2370
 
1954
2371
  // src/setup/apply.ts
1955
2372
  import { createHash as createHash2 } from "crypto";
1956
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3 } from "fs";
1957
- import { dirname as dirname3 } from "path";
1958
-
1959
- // src/setup/operator-surface.ts
1960
- var SectionType = {
1961
- McpConfig: "mcp-config",
1962
- McpConfigToml: "mcp-config-toml",
1963
- InstructionFile: "instruction-file",
1964
- ProjectConfig: "project-config",
1965
- Verify: "verify",
1966
- /** SBC-999 — workspace-pinned conventions, emitted only when the request
1967
- * carried a workspaceId and that workspace has agent-setup-bundle memories. */
1968
- WorkspaceConventions: "workspace-conventions"
1969
- };
1970
- async function fetchSetup(cfg) {
1971
- const client = await makeClient(cfg);
1972
- const { data, error } = await client.GET(
1973
- "/operator-surface/setup",
1974
- cfg.workspaceId ? { params: { query: { workspaceId: cfg.workspaceId } } } : {}
1975
- );
1976
- if (error) throw new Error(`GET /operator-surface/setup failed: ${JSON.stringify(error)}`);
1977
- return data;
1978
- }
1979
- function findSurface(setup, surfaceKey) {
1980
- return setup.surfaces.find((s) => s.surfaceKey === surfaceKey);
1981
- }
1982
- function findSection(surface, sectionType) {
1983
- return surface?.sections.find((s) => s.sectionType === sectionType);
1984
- }
1985
- function sectionSnippet(section) {
1986
- if (!section) return null;
1987
- for (const step of section.steps) {
1988
- if (step.copyValue) return step.copyValue;
1989
- if (step.codeSnippet) return step.codeSnippet;
1990
- }
1991
- return null;
1992
- }
1993
- function parseTagArtifactId(id) {
1994
- if (!id.startsWith("tag:")) return null;
1995
- const tags = id.slice("tag:".length).split(",").map((t) => t.trim()).filter((t) => t.length > 0);
1996
- return tags.length > 0 ? tags : null;
1997
- }
1998
- async function getPersonalWorkspaceId(cfg) {
1999
- const client = await makeClient(cfg);
2000
- const { data } = await client.GET("/me/personal-workspace", {});
2001
- return data?.workspaceId ?? null;
2002
- }
2003
- async function fetchMemoryFields(cfg, id) {
2004
- const client = await makeClient(cfg);
2005
- const { data } = await client.GET("/memories/{memoryId}", { params: { path: { memoryId: id } } });
2006
- const env = data;
2007
- const m = env?.item ?? env;
2008
- if (!m) return null;
2009
- const version = typeof m.currentVersion === "string" ? Number(m.currentVersion) : m.currentVersion;
2010
- return { text: m.text, title: m.title, tags: m.tags, version: Number.isFinite(version) ? version : void 0 };
2011
- }
2012
- async function resolveInstruction(cfg, section, personalWorkspaceId) {
2013
- const client = await makeClient(cfg);
2014
- for (const artifact of section.artifacts) {
2015
- const tags = parseTagArtifactId(artifact.id);
2016
- if (!tags) continue;
2017
- const { data } = await client.POST("/memories/search", {
2018
- body: { query: null, textQuery: null, semanticQuery: null, hybrid: false, limit: 1, includeArchived: false, includeSystem: false, tags }
2019
- });
2020
- const hits = data ?? [];
2021
- if (hits.length === 0) continue;
2022
- const templateId = hits[0].id;
2023
- const template = await fetchMemoryFields(cfg, templateId);
2024
- if (typeof template?.text !== "string" || template.text.length === 0) continue;
2025
- const templateTags = template.tags ?? tags;
2026
- if (personalWorkspaceId) {
2027
- const { data: ovr } = await client.POST("/memories/search", {
2028
- body: { query: null, textQuery: null, semanticQuery: null, hybrid: false, limit: 1, includeArchived: false, includeSystem: false, tags: ["sechroom:role:override", `sechroom:template-ref:${templateId}`], owner: { type: "Workspace", id: personalWorkspaceId } }
2029
- });
2030
- const ovrHits = ovr ?? [];
2031
- if (ovrHits.length > 0) {
2032
- const override = await fetchMemoryFields(cfg, ovrHits[0].id);
2033
- if (typeof override?.text === "string" && override.text.length > 0) {
2034
- return { title: override.title ?? template.title ?? artifact.title, body: override.text, source: "override", templateId, templateTags, sourceRef: `${ovrHits[0].id}@v${override.version ?? 1}` };
2035
- }
2036
- }
2037
- }
2038
- return { title: template.title ?? artifact.title, body: template.text, source: "template", templateId, templateTags, sourceRef: `${templateId}@v${template.version ?? 1}` };
2039
- }
2040
- return null;
2041
- }
2042
- async function resolveWorkspaceConventions(cfg, section) {
2043
- const parts = [];
2044
- const refs = [];
2045
- for (const artifact of section.artifacts) {
2046
- if (parseTagArtifactId(artifact.id)) continue;
2047
- const mem = await fetchMemoryFields(cfg, artifact.id);
2048
- if (typeof mem?.text === "string" && mem.text.trim().length > 0) {
2049
- parts.push(mem.text.trim());
2050
- refs.push(`${artifact.id}@v${mem.version ?? 1}`);
2051
- }
2052
- }
2053
- if (parts.length === 0) return null;
2054
- return { body: parts.join("\n\n---\n\n"), refs };
2055
- }
2056
- async function createOverride(cfg, template, personalWorkspaceId) {
2057
- const client = await makeClient(cfg);
2058
- const overrideTags = template.templateTags.filter(
2059
- (t) => t !== "sechroom:role:template" && !t.startsWith("sechroom:bundle:") && !t.startsWith("sechroom:template-ref:")
2060
- );
2061
- overrideTags.push("sechroom:role:override", `sechroom:template-ref:${template.templateId}`);
2062
- const { error } = await client.POST("/memories", {
2063
- body: {
2064
- text: template.body,
2065
- type: "reference",
2066
- content: "{}",
2067
- confidence: 1,
2068
- source: "cli-agent-instructions-customize",
2069
- archetype: "Document",
2070
- title: template.title ?? null,
2071
- tags: overrideTags,
2072
- owner: { type: "Workspace", id: personalWorkspaceId }
2073
- }
2074
- });
2075
- if (error) throw new Error(`creating personal copy failed: ${JSON.stringify(error)}`);
2076
- }
2077
-
2078
- // src/setup/apply.ts
2373
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
2374
+ import { dirname as dirname5 } from "path";
2079
2375
  var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
2080
2376
  var MARKER_END = "<!-- @sechroom/cli:end";
2081
2377
  function normalizeBody(s) {
@@ -2128,22 +2424,22 @@ function parseManagedBlock(content, block) {
2128
2424
  return null;
2129
2425
  }
2130
2426
  function ensureDir2(path) {
2131
- mkdirSync3(dirname3(path), { recursive: true });
2427
+ mkdirSync4(dirname5(path), { recursive: true });
2132
2428
  }
2133
2429
  function readOr(path, fallback) {
2134
2430
  try {
2135
- return readFileSync3(path, "utf8");
2431
+ return readFileSync4(path, "utf8");
2136
2432
  } catch {
2137
2433
  return fallback;
2138
2434
  }
2139
2435
  }
2140
2436
  function mergeMcpJson(path, snippet, dryRun) {
2141
2437
  const incoming = JSON.parse(snippet);
2142
- const existed = existsSync3(path);
2438
+ const existed = existsSync5(path);
2143
2439
  let current = {};
2144
2440
  if (existed) {
2145
2441
  try {
2146
- current = JSON.parse(readFileSync3(path, "utf8"));
2442
+ current = JSON.parse(readFileSync4(path, "utf8"));
2147
2443
  } catch {
2148
2444
  return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
2149
2445
  }
@@ -2151,26 +2447,26 @@ function mergeMcpJson(path, snippet, dryRun) {
2151
2447
  current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
2152
2448
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
2153
2449
  ensureDir2(path);
2154
- writeFileSync3(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
2450
+ writeFileSync4(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
2155
2451
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
2156
2452
  }
2157
2453
  function mergeCodexToml(path, snippet, dryRun) {
2158
- const existed = existsSync3(path);
2454
+ const existed = existsSync5(path);
2159
2455
  let body = readOr(path, "");
2160
2456
  body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
2161
2457
  const trimmed = body.trim();
2162
2458
  const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
2163
2459
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
2164
2460
  ensureDir2(path);
2165
- writeFileSync3(path, next, { mode: 384 });
2461
+ writeFileSync4(path, next, { mode: 384 });
2166
2462
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
2167
2463
  }
2168
2464
  function writeInstructionBlock(path, write, dryRun) {
2169
- const existed = existsSync3(path);
2465
+ const existed = existsSync5(path);
2170
2466
  const next = computeBlockFile(readOr(path, ""), write);
2171
2467
  if (dryRun) return { kind: "instruction", path, status: "dry-run" };
2172
2468
  ensureDir2(path);
2173
- writeFileSync3(path, next);
2469
+ writeFileSync4(path, next);
2174
2470
  return { kind: "instruction", path, status: existed ? "merged" : "created" };
2175
2471
  }
2176
2472
  function computeBlockFile(current, write) {
@@ -2211,7 +2507,7 @@ function applyBlock(path, write, mode, dryRun) {
2211
2507
  const next = computeBlockFile(current, write);
2212
2508
  if (!dryRun) {
2213
2509
  ensureDir2(proposedPath);
2214
- writeFileSync3(proposedPath, next);
2510
+ writeFileSync4(proposedPath, next);
2215
2511
  }
2216
2512
  return {
2217
2513
  kind: "instruction",
@@ -2281,65 +2577,10 @@ async function applyClient(cfg, setup, target, opts) {
2281
2577
  return actions;
2282
2578
  }
2283
2579
 
2284
- // src/setup/clients.ts
2285
- import { existsSync as existsSync4 } from "fs";
2286
- import { homedir as homedir2 } from "os";
2287
- import { dirname as dirname4, join as join3 } from "path";
2288
- function claudeDesktopConfigPath(home) {
2289
- switch (process.platform) {
2290
- case "darwin":
2291
- return join3(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
2292
- case "win32":
2293
- return join3(process.env.APPDATA ?? join3(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
2294
- default:
2295
- return join3(home, ".config", "Claude", "claude_desktop_config.json");
2296
- }
2297
- }
2298
- function clientTargets(cwd) {
2299
- const home = homedir2();
2300
- return {
2301
- "claude-code": {
2302
- key: "claude-code",
2303
- label: "Claude Code",
2304
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join3(cwd, ".mcp.json"), format: "json" },
2305
- instruction: { surfaceKey: "claude-code", path: join3(cwd, "CLAUDE.md") }
2306
- },
2307
- "claude-desktop": {
2308
- key: "claude-desktop",
2309
- label: "Claude Desktop",
2310
- mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
2311
- instruction: { surfaceKey: "claude-desktop", path: join3(home, ".claude", "CLAUDE.md") }
2312
- },
2313
- codex: {
2314
- key: "codex",
2315
- label: "Codex CLI",
2316
- mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join3(home, ".codex", "config.toml"), format: "toml" },
2317
- instruction: { surfaceKey: "chatgpt", path: join3(cwd, "AGENTS.md") }
2318
- },
2319
- cursor: {
2320
- key: "cursor",
2321
- label: "Cursor",
2322
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join3(cwd, ".cursor", "mcp.json"), format: "json" },
2323
- instruction: { surfaceKey: "chatgpt", path: join3(cwd, "AGENTS.md") }
2324
- }
2325
- };
2326
- }
2327
- var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor"];
2328
- var DEFAULT_CLIENT_KEY = "claude-code";
2329
- function detectInstalledClients(cwd) {
2330
- const home = homedir2();
2331
- const detected = [];
2332
- if (existsSync4(join3(home, ".claude"))) detected.push("claude-code");
2333
- if (existsSync4(dirname4(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
2334
- if (existsSync4(join3(home, ".codex"))) detected.push("codex");
2335
- if (existsSync4(join3(home, ".cursor")) || existsSync4(join3(cwd, ".cursor"))) detected.push("cursor");
2336
- return detected;
2337
- }
2338
-
2339
2580
  // src/setup/skills-offer.ts
2340
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
2341
- import { homedir as homedir3 } from "os";
2342
- import { join as join4 } from "path";
2581
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
2582
+ import { homedir as homedir4 } from "os";
2583
+ import { join as join5 } from "path";
2343
2584
 
2344
2585
  // src/setup/lane-pin.ts
2345
2586
  var CODE_LANE_PREFIX_BY_CLIENT = {
@@ -2365,7 +2606,7 @@ function writePin(code, design) {
2365
2606
  if (design) values["design-lane"] = design;
2366
2607
  if (Object.keys(values).length === 0) return;
2367
2608
  const target = writeSem(values);
2368
- process.stderr.write(`${ok("\u2713")} lane pin written \u2192 ${target} ${style.dim("(./.sem, git-ignored)")}
2609
+ process.stderr.write(`${ok("\u2713")} lane pin written \u2192 ${target} ${style.dim("(./.sechroom/lane.json, git-ignored)")}
2369
2610
  `);
2370
2611
  }
2371
2612
  async function ensureLanePin(cfg, opts) {
@@ -2454,14 +2695,14 @@ async function maybeOfferSkills(cfg, personalWorkspaceId, opts) {
2454
2695
  Found ${style.bold(String(names.length))} operator skill(s) installed in your workspace: ${names.join(", ")}.
2455
2696
  `
2456
2697
  );
2457
- const dir = join4(homedir3(), ".claude", "skills");
2698
+ const dir = join5(homedir4(), ".claude", "skills");
2458
2699
  const materialise = opts.yes ? true : canPrompt() ? await promptYesNo(`Write them to ${dir}/ so ${surface} can use them?`) : false;
2459
2700
  if (!materialise) return;
2460
2701
  const written = [];
2461
2702
  for (const [name, m] of byName) {
2462
2703
  const body = m.text ?? m.Text ?? "";
2463
- mkdirSync4(join4(dir, name), { recursive: true });
2464
- writeFileSync4(join4(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
2704
+ mkdirSync5(join5(dir, name), { recursive: true });
2705
+ writeFileSync5(join5(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
2465
2706
  written.push(name);
2466
2707
  }
2467
2708
  process.stderr.write(`${style.green("\u2713")} wrote ${written.length} skill(s) to ${dir}
@@ -2791,7 +3032,7 @@ async function ensureTenant(baseUrl, g, opts) {
2791
3032
  "Where should this tenant + base URL be saved?",
2792
3033
  [
2793
3034
  { label: "Globally", value: "global", hint: "all projects on this machine" },
2794
- { label: "This directory", value: "local", hint: ".sechroom.json \u2014 project + subdirs" }
3035
+ { label: "This directory", value: "local", hint: ".sechroom/config.json \u2014 project + subdirs" }
2795
3036
  ],
2796
3037
  local.path ? "local" : "global"
2797
3038
  ) === "local";
@@ -2867,14 +3108,14 @@ async function chooseClients(clientFlag, yes, cwd) {
2867
3108
  return picks.length > 0 ? picks : preselected;
2868
3109
  }
2869
3110
  function registerOnboard(program2) {
2870
- program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--client <list>", `comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`).option("--local", "save tenant + base URL to a directory-local .sechroom.json instead of the global config", false).option("--workspace <id>", "bind this directory to a workspace (skips the interactive workspace pick)").option("--cli-only", "configure the CLI only \u2014 don't wire any AI client (no MCP config, no agent files)", false).option("--no-mcp", "skip the MCP server config (.mcp.json etc.); still write the agent instruction files").option("--copy", "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)").option("--dry-run", "walk through without writing files or changing the profile", false).option("--refresh", "re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)", false).option("--force", "rewrite every managed block, overwriting local edits inside the markers (content outside untouched)", false).option("--check", "report whether anything would change and exit (0 = all current, 1 = stale/drift/absent); writes nothing", false).option("-y, --yes", "non-interactive: accept defaults (system timezone, detected clients, global config, full wire)", false).addHelpText(
3111
+ program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--client <list>", `comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`).option("--local", "save tenant + base URL to a directory-local .sechroom/config.json instead of the global config", false).option("--workspace <id>", "bind this directory to a workspace (skips the interactive workspace pick)").option("--cli-only", "configure the CLI only \u2014 don't wire any AI client (no MCP config, no agent files)", false).option("--no-mcp", "skip the MCP server config (.mcp.json etc.); still write the agent instruction files").option("--copy", "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)").option("--dry-run", "walk through without writing files or changing the profile", false).option("--refresh", "re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)", false).option("--force", "rewrite every managed block, overwriting local edits inside the markers (content outside untouched)", false).option("--check", "report whether anything would change and exit (0 = all current, 1 = stale/drift/absent); writes nothing", false).option("-y, --yes", "non-interactive: accept defaults (system timezone, detected clients, global config, full wire)", false).addHelpText(
2871
3112
  "after",
2872
3113
  `
2873
3114
  Examples:
2874
3115
  $ sechroom onboard guided, interactive (asks where to save config + how to wire)
2875
3116
  $ sechroom onboard --cli-only just the CLI \u2014 no .mcp.json, no agent files
2876
3117
  $ sechroom onboard --no-mcp agent instructions only, skip MCP config
2877
- $ sechroom onboard --local save tenant + base URL to ./.sechroom.json
3118
+ $ sechroom onboard --local save tenant + base URL to ./.sechroom/config.json
2878
3119
  $ sechroom onboard --workspace wsp_XX bind this directory to a workspace (no pick prompt)
2879
3120
  $ sechroom onboard --refresh refresh out-of-date instruction blocks in place
2880
3121
  $ sechroom onboard --check CI/pre-commit: nonzero exit if instructions are out of date
@@ -3034,14 +3275,14 @@ ${style.bold("Next:")} paste this into your AI agent to get going \u2014
3034
3275
  }
3035
3276
 
3036
3277
  // src/commands/skills.ts
3037
- import { homedir as homedir4 } from "os";
3038
- import { join as join5 } from "path";
3039
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, rmSync as rmSync2, existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
3278
+ import { homedir as homedir5 } from "os";
3279
+ import { join as join6 } from "path";
3280
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, rmSync as rmSync2, existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
3040
3281
  var DEFAULT_SLUG = "operator-skills";
3041
3282
  var ROLE_TAGS = ["sechroom:role:skill-template", "role:skill-template"];
3042
3283
  var LOCK = ".sechroom-skills.json";
3043
3284
  function skillsDir(global) {
3044
- return global ? join5(homedir4(), ".claude", "skills") : join5(process.cwd(), ".claude", "skills");
3285
+ return global ? join6(homedir5(), ".claude", "skills") : join6(process.cwd(), ".claude", "skills");
3045
3286
  }
3046
3287
  function tagValue2(tags, prefix) {
3047
3288
  return (tags ?? []).find((t) => t.startsWith(prefix))?.slice(prefix.length);
@@ -3119,15 +3360,15 @@ Examples:
3119
3360
  const name = tagValue2(tags, "skill:");
3120
3361
  if (!name) continue;
3121
3362
  const body = m.text ?? m.Text ?? "";
3122
- mkdirSync5(join5(dir, name), { recursive: true });
3123
- writeFileSync5(join5(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
3363
+ mkdirSync6(join6(dir, name), { recursive: true });
3364
+ writeFileSync6(join6(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
3124
3365
  written.push(name);
3125
3366
  }
3126
- mkdirSync5(dir, { recursive: true });
3127
- const lockPath = join5(dir, LOCK);
3128
- const lock = existsSync5(lockPath) ? JSON.parse(readFileSync4(lockPath, "utf8")) : {};
3367
+ mkdirSync6(dir, { recursive: true });
3368
+ const lockPath = join6(dir, LOCK);
3369
+ const lock = existsSync6(lockPath) ? JSON.parse(readFileSync5(lockPath, "utf8")) : {};
3129
3370
  lock[slug] = { surface: opts.surface, version, instance: wantInstance, skills: written.sort() };
3130
- writeFileSync5(lockPath, JSON.stringify(lock, null, 2) + "\n");
3371
+ writeFileSync6(lockPath, JSON.stringify(lock, null, 2) + "\n");
3131
3372
  if (opts.json) return emit({ slug, version, instance: wantInstance, surface: opts.surface, dir, installed: written }, true);
3132
3373
  const instanceNote = opts.instance ? ` (${opts.instance})` : "";
3133
3374
  console.log(style.green(`Installed ${slug}@${version}${instanceNote} \u2014 ${written.length} skill(s) \u2192 ${dir}`));
@@ -3149,28 +3390,28 @@ Examples:
3149
3390
  skills.command("clean [slug]").description(`Remove materialised skill files written by install (default ${DEFAULT_SLUG})`).option("--local", "clean ./.claude/skills instead of ~/.claude/skills").option("--json", "machine output").action(async (slugArg, opts) => {
3150
3391
  const slug = slugArg || DEFAULT_SLUG;
3151
3392
  const dir = skillsDir(!opts.local);
3152
- const lockPath = join5(dir, LOCK);
3153
- if (!existsSync5(lockPath)) fail(`No skills lockfile at ${lockPath}; nothing to clean.`);
3154
- const lock = JSON.parse(readFileSync4(lockPath, "utf8"));
3393
+ const lockPath = join6(dir, LOCK);
3394
+ if (!existsSync6(lockPath)) fail(`No skills lockfile at ${lockPath}; nothing to clean.`);
3395
+ const lock = JSON.parse(readFileSync5(lockPath, "utf8"));
3155
3396
  const entry = lock[slug];
3156
3397
  if (!entry) fail(`No installed record for '${slug}' in ${lockPath}.`);
3157
3398
  const removed = [];
3158
3399
  for (const name of entry.skills) {
3159
- const skillPath = join5(dir, name);
3160
- if (existsSync5(skillPath)) {
3400
+ const skillPath = join6(dir, name);
3401
+ if (existsSync6(skillPath)) {
3161
3402
  rmSync2(skillPath, { recursive: true, force: true });
3162
3403
  removed.push(name);
3163
3404
  }
3164
3405
  }
3165
3406
  delete lock[slug];
3166
- writeFileSync5(lockPath, JSON.stringify(lock, null, 2) + "\n");
3407
+ writeFileSync6(lockPath, JSON.stringify(lock, null, 2) + "\n");
3167
3408
  if (opts.json) return emit({ slug, removed, dir }, true);
3168
3409
  console.log(style.green(`Removed ${removed.length} skill(s) for ${slug} from ${dir}`));
3169
3410
  });
3170
- skills.command("set-lane").description("Write this checkout's lane pin to a local ./.sem file (read at runtime by skills)").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action((opts, cmd) => {
3411
+ skills.command("set-lane").description("Write this checkout's lane pin to a local ./.sechroom/lane.json file (read at runtime by skills)").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action((opts, cmd) => {
3171
3412
  if (!opts.codeLane && !opts.designLane) fail("Provide --code-lane and/or --design-lane.");
3172
3413
  const target = localSemPath();
3173
- const values = readSem(target)?.values ?? {};
3414
+ const values = readLocalSemValues();
3174
3415
  if (opts.codeLane) values["code-lane"] = opts.codeLane;
3175
3416
  if (opts.designLane) values["design-lane"] = opts.designLane;
3176
3417
  writeSem(values, target);
@@ -3178,12 +3419,12 @@ Examples:
3178
3419
  console.log(style.green(`Wrote lane pin \u2192 ${target} ${style.dim("(git-ignored)")}`));
3179
3420
  Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
3180
3421
  });
3181
- skills.command("lane").description("Show the lane pin resolved from ./.sem (nearest in this checkout)").option("--json", "machine output").action((opts, cmd) => {
3422
+ skills.command("lane").description("Show the lane pin resolved from ./.sechroom/lane.json (nearest in this checkout; legacy ./.sem honoured)").option("--json", "machine output").action((opts, cmd) => {
3182
3423
  const json = cmd.optsWithGlobals().json;
3183
3424
  const found = readSem();
3184
3425
  if (!found) {
3185
3426
  if (json) return emit({ path: null, values: {} }, true);
3186
- return console.log(style.dim(`No ./.sem pin in this checkout. Run 'sechroom skills set-lane'.`));
3427
+ return console.log(style.dim(`No ./.sechroom/lane.json pin in this checkout. Run 'sechroom skills set-lane'.`));
3187
3428
  }
3188
3429
  if (json) return emit(found, true);
3189
3430
  console.log(style.dim(`from ${found.path}`));
@@ -3252,22 +3493,22 @@ Examples:
3252
3493
  }
3253
3494
 
3254
3495
  // src/commands/reset.ts
3255
- import { homedir as homedir5 } from "os";
3256
- import { join as join6 } from "path";
3257
- import { existsSync as existsSync6, readFileSync as readFileSync5, rmSync as rmSync3 } from "fs";
3496
+ import { homedir as homedir6 } from "os";
3497
+ import { join as join7 } from "path";
3498
+ import { existsSync as existsSync7, readFileSync as readFileSync6, rmSync as rmSync3 } from "fs";
3258
3499
  var SKILLS_LOCK = ".sechroom-skills.json";
3259
- var localSkillsDir = () => join6(process.cwd(), ".claude", "skills");
3260
- var globalSkillsDir = () => join6(homedir5(), ".claude", "skills");
3500
+ var localSkillsDir = () => join7(process.cwd(), ".claude", "skills");
3501
+ var globalSkillsDir = () => join7(homedir6(), ".claude", "skills");
3261
3502
  function removeMaterialisedSkills(dir) {
3262
3503
  const removed = [];
3263
- const lockPath = join6(dir, SKILLS_LOCK);
3264
- if (!existsSync6(lockPath)) return removed;
3504
+ const lockPath = join7(dir, SKILLS_LOCK);
3505
+ if (!existsSync7(lockPath)) return removed;
3265
3506
  try {
3266
- const lock = JSON.parse(readFileSync5(lockPath, "utf8"));
3507
+ const lock = JSON.parse(readFileSync6(lockPath, "utf8"));
3267
3508
  for (const entry of Object.values(lock)) {
3268
3509
  for (const name of entry.skills ?? []) {
3269
- const p = join6(dir, name);
3270
- if (existsSync6(p)) {
3510
+ const p = join7(dir, name);
3511
+ if (existsSync7(p)) {
3271
3512
  rmSync3(p, { recursive: true, force: true });
3272
3513
  removed.push(p);
3273
3514
  }
@@ -3287,26 +3528,31 @@ function registerReset(program2) {
3287
3528
  removed ? style.green("Signed out \u2014 auth token removed.") : style.dim("Already signed out (no token).")
3288
3529
  );
3289
3530
  });
3290
- program2.command("reset").description("Reset LOCAL CLI state for this directory (./.sechroom.json, ./.sem, ./.claude/skills); --global also wipes the machine-wide token + config + ~/.claude/skills").option("--global", "also remove the global auth token, config, and ~/.claude/skills").option("-y, --yes", "don't prompt for confirmation").option("--json", "machine output").action(async (opts, cmd) => {
3531
+ program2.command("reset").description("Reset LOCAL CLI state for this directory (./.sechroom/, legacy ./.sechroom.json + ./.sem, ./.claude/skills); --global also wipes the machine-wide token + config + ~/.claude/skills").option("--global", "also remove the global auth token, config, and ~/.claude/skills").option("-y, --yes", "don't prompt for confirmation").option("--json", "machine output").action(async (opts, cmd) => {
3291
3532
  const json = cmd.optsWithGlobals().json;
3292
3533
  const global = Boolean(opts.global);
3293
3534
  if (!opts.yes && canPrompt()) {
3294
- const scope = global ? "this directory's local state AND your global auth token + config + ~/.claude/skills" : "this directory's local state (./.sechroom.json, ./.sem, ./.claude/skills)";
3535
+ const scope = global ? "this directory's local state AND your global auth token + config + ~/.claude/skills" : "this directory's local state (./.sechroom/, legacy ./.sechroom.json + ./.sem, ./.claude/skills)";
3295
3536
  if (!await promptYesNo(`Remove ${scope}?`)) {
3296
3537
  if (!json) console.log(style.dim("Cancelled."));
3297
3538
  return;
3298
3539
  }
3299
3540
  }
3300
3541
  const removed = [];
3301
- const localCfg = join6(process.cwd(), ".sechroom.json");
3302
- if (existsSync6(localCfg)) {
3303
- rmSync3(localCfg, { force: true });
3304
- removed.push(localCfg);
3542
+ const stateDir = join7(process.cwd(), ".sechroom");
3543
+ if (existsSync7(stateDir)) {
3544
+ rmSync3(stateDir, { recursive: true, force: true });
3545
+ removed.push(stateDir);
3546
+ }
3547
+ const legacyCfg = join7(process.cwd(), ".sechroom.json");
3548
+ if (existsSync7(legacyCfg)) {
3549
+ rmSync3(legacyCfg, { force: true });
3550
+ removed.push(legacyCfg);
3305
3551
  }
3306
- const sem = localSemPath();
3307
- if (existsSync6(sem)) {
3308
- rmSync3(sem, { force: true });
3309
- removed.push(sem);
3552
+ const legacySem = join7(process.cwd(), ".sem");
3553
+ if (existsSync7(legacySem)) {
3554
+ rmSync3(legacySem, { force: true });
3555
+ removed.push(legacySem);
3310
3556
  }
3311
3557
  removed.push(...removeMaterialisedSkills(localSkillsDir()));
3312
3558
  if (global) {
@@ -3331,7 +3577,7 @@ function registerReset(program2) {
3331
3577
  function resolveVersion() {
3332
3578
  try {
3333
3579
  const pkg = JSON.parse(
3334
- readFileSync6(new URL("../package.json", import.meta.url), "utf8")
3580
+ readFileSync7(new URL("../package.json", import.meta.url), "utf8")
3335
3581
  );
3336
3582
  return pkg.version ?? "0.0.0";
3337
3583
  } catch {
@@ -3347,7 +3593,7 @@ Examples:
3347
3593
  $ sechroom onboard guided first-run: configure, sign in, wire this project
3348
3594
  $ sechroom login sign in via browser (OAuth + PKCE)
3349
3595
  $ sechroom config set tenant ocd set your tenant (global)
3350
- $ sechroom config set --local tenant cli-smoke pin tenant for this directory (.sechroom.json)
3596
+ $ sechroom config set --local tenant cli-smoke pin tenant for this directory (.sechroom/config.json)
3351
3597
  $ sechroom config show resolved config + which source won
3352
3598
 
3353
3599
  $ sechroom memory create --text "a note" --title "Note" --tag idea
@@ -3359,7 +3605,7 @@ Examples:
3359
3605
  $ sechroom --json memory search "auth" compact JSON for scripts and agents
3360
3606
  $ SECHROOM_TOKEN=<bearer> sechroom --json memory get mem_XXXX headless
3361
3607
 
3362
- Config precedence (high -> low): --flag > env (SECHROOM_*) > ./.sechroom.json > global > default.
3608
+ Config precedence (high -> low): --flag > env (SECHROOM_*) > ./.sechroom/config.json (legacy ./.sechroom.json) > global > default.
3363
3609
  Run 'sechroom <command> --help' for command-specific examples.`
3364
3610
  );
3365
3611
  program.hook("preAction", (_thisCmd, actionCmd) => {
@@ -3385,11 +3631,11 @@ config.addHelpText(
3385
3631
  Examples:
3386
3632
  $ sechroom config set baseUrl https://app.sechroom.ai/api prod (staging: https://staging.app.sechroom.ai/api)
3387
3633
  $ sechroom config set tenant ocd
3388
- $ sechroom config set --local tenant cli-smoke this dir + subdirs (.sechroom.json)
3634
+ $ sechroom config set --local tenant cli-smoke this dir + subdirs (.sechroom/config.json)
3389
3635
  $ sechroom config set clientId dyn-XXXX global-only escape hatch (no DCR endpoint)
3390
3636
  $ sechroom config show --json`
3391
3637
  );
3392
- config.command("set <key> <value>").description("Set baseUrl | tenant | workspaceId | defaultProjectId | clientId (clientId is global-only)").option("--local", "Write to the directory-local .sechroom.json (nearest up the tree, else cwd) instead of the global config").action((key, value, opts) => {
3638
+ config.command("set <key> <value>").description("Set baseUrl | tenant | workspaceId | defaultProjectId | clientId (clientId is global-only)").option("--local", "Write to the directory-local .sechroom/config.json (nearest up the tree, else cwd; migrates a legacy .sechroom.json) instead of the global config").action((key, value, opts) => {
3393
3639
  if (opts.local) {
3394
3640
  if (!["baseUrl", "tenant", "workspaceId", "defaultProjectId"].includes(key)) {
3395
3641
  process.stderr.write(`--local supports only: baseUrl | tenant | workspaceId | defaultProjectId (clientId is global)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sechroom/cli",
3
- "version": "2026.6.18",
3
+ "version": "2026.6.19",
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",