@sechroom/cli 2026.6.18 → 2026.6.20
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.
- package/dist/index.js +564 -264
- 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
|
|
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
|
|
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
|
|
104
|
+
const readPath = findLocalConfigPath();
|
|
96
105
|
let current = {};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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 = ".
|
|
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
|
-
|
|
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
|
-
|
|
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 ===
|
|
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
|
|
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}
|
|
1675
|
+
appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
|
|
1633
1676
|
`);
|
|
1634
1677
|
} else {
|
|
1635
|
-
writeFileSync2(target.path,
|
|
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,130 @@ 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 surfaces = detectHookSurfaces(cwd);
|
|
2020
|
+
return surfaces.length > 0 ? surfaces : ["claude", "codex"];
|
|
2021
|
+
}
|
|
2022
|
+
function describe(result, dryRun) {
|
|
2023
|
+
if (result.status === "current") return ` \u2713 ${result.path} (already configured)`;
|
|
2024
|
+
const verb = dryRun ? "would" : result.status === "created" ? "created" : "updated";
|
|
2025
|
+
return ` \u2713 ${result.path} (${dryRun ? `${verb} ${result.status === "created" ? "create" : "update"}` : verb})`;
|
|
2026
|
+
}
|
|
2027
|
+
var HOOK_SURFACE_LABEL = {
|
|
2028
|
+
claude: "Claude Code",
|
|
2029
|
+
codex: "Codex"
|
|
2030
|
+
};
|
|
2031
|
+
function installHookSurfaces(surfaces, opts) {
|
|
2032
|
+
const out = [];
|
|
2033
|
+
for (const surface of surfaces) {
|
|
2034
|
+
if (surface === "claude") {
|
|
2035
|
+
const path = opts.local ? join4(opts.cwd, ".claude", "settings.json") : join4(opts.home, ".claude", "settings.json");
|
|
2036
|
+
out.push({ surface, results: [installHooksJson(path, opts.dryRun)] });
|
|
2037
|
+
} else {
|
|
2038
|
+
const hooksJson = installHooksJson(join4(opts.home, ".codex", "hooks.json"), opts.dryRun);
|
|
2039
|
+
const featureFlag = installCodexFeatureFlag(join4(opts.home, ".codex", "config.toml"), opts.dryRun);
|
|
2040
|
+
out.push({ surface, results: [hooksJson, featureFlag] });
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
return out;
|
|
2044
|
+
}
|
|
2045
|
+
function detectHookSurfaces(cwd) {
|
|
2046
|
+
const detected = detectInstalledClients(cwd);
|
|
2047
|
+
const surfaces = [];
|
|
2048
|
+
if (detected.includes("claude-code")) surfaces.push("claude");
|
|
2049
|
+
if (detected.includes("codex")) surfaces.push("codex");
|
|
2050
|
+
return surfaces;
|
|
2051
|
+
}
|
|
1700
2052
|
function registerHook(program2) {
|
|
1701
2053
|
const hook = program2.command("hook").description("Agent-lifecycle hook adapter (Claude Code / Codex) \u2014 bridges hooks to continuity");
|
|
1702
2054
|
hook.addHelpText(
|
|
1703
2055
|
"after",
|
|
1704
2056
|
`
|
|
1705
2057
|
Examples:
|
|
1706
|
-
#
|
|
2058
|
+
# SessionStart (load): inject the lane's latest snapshot as context.
|
|
1707
2059
|
$ echo '{"hook_event_name":"SessionStart","cwd":"'"$PWD"'"}' | sechroom hook session-start
|
|
1708
|
-
|
|
2060
|
+
# PreCompact (save): snapshot from ./${INTENT_FILE} before the agent compacts.
|
|
2061
|
+
$ echo '{"hook_event_name":"PreCompact","cwd":"'"$PWD"'"}' | sechroom hook pre-compact
|
|
2062
|
+
# Wire both hooks into the installed surface(s)' config (no hand-editing):
|
|
2063
|
+
$ sechroom hook install auto-detect Claude Code / Codex
|
|
2064
|
+
$ sechroom hook install --surface codex Codex only
|
|
2065
|
+
$ sechroom hook install --local --dry-run preview the project .claude/settings.json
|
|
1709
2066
|
|
|
1710
2067
|
Lane source (high -> low): --lane > SECHROOM_LANE > ./.sem code-lane (D-binding-5).
|
|
1711
|
-
Fail-soft: no lane / no auth / API error -> exit 0,
|
|
2068
|
+
Fail-soft: no lane / no auth / no-or-partial intent file / API error -> exit 0, never blocks.`
|
|
1712
2069
|
);
|
|
1713
2070
|
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
2071
|
try {
|
|
@@ -1734,6 +2091,77 @@ Fail-soft: no lane / no auth / API error -> exit 0, no context injected, never b
|
|
|
1734
2091
|
return process.exit(0);
|
|
1735
2092
|
}
|
|
1736
2093
|
});
|
|
2094
|
+
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) => {
|
|
2095
|
+
try {
|
|
2096
|
+
const raw = await readStdin();
|
|
2097
|
+
const input = parseHookInput(raw);
|
|
2098
|
+
const cwd = input.cwd ?? process.cwd();
|
|
2099
|
+
const lane = resolveLane(opts.lane, input.cwd);
|
|
2100
|
+
if (!lane) return process.exit(0);
|
|
2101
|
+
const intent = readIntent(cwd);
|
|
2102
|
+
if (!intent || !hasRequiredIntent(intent)) return process.exit(0);
|
|
2103
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2104
|
+
const client = await makeClient(cfg);
|
|
2105
|
+
await client.POST("/continuity/snapshots", {
|
|
2106
|
+
body: {
|
|
2107
|
+
laneId: lane,
|
|
2108
|
+
scope: opts.scope ?? intent.scope ?? "compaction",
|
|
2109
|
+
currentObjective: intent.objective,
|
|
2110
|
+
currentState: intent.state,
|
|
2111
|
+
lastMeaningfulAction: intent.lastAction,
|
|
2112
|
+
nextIntendedAction: intent.nextAction,
|
|
2113
|
+
resumeInstruction: intent.resumeInstruction,
|
|
2114
|
+
activeConstraints: intent.constraints ?? null,
|
|
2115
|
+
openQuestions: intent.questions ?? null,
|
|
2116
|
+
surfaceMarkers: intent.surfaceMarkers ?? null,
|
|
2117
|
+
relevantArtifactIds: intent.artifacts ?? null,
|
|
2118
|
+
confidence: intent.confidence ?? null,
|
|
2119
|
+
// Compaction is infrequent, so the FR-051 clobber guard doesn't bite;
|
|
2120
|
+
// Acknowledge lets a within-window checkpoint land on the lane.
|
|
2121
|
+
concurrentSessionPolicy: "Acknowledge"
|
|
2122
|
+
}
|
|
2123
|
+
});
|
|
2124
|
+
return process.exit(0);
|
|
2125
|
+
} catch {
|
|
2126
|
+
return process.exit(0);
|
|
2127
|
+
}
|
|
2128
|
+
});
|
|
2129
|
+
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) => {
|
|
2130
|
+
const dryRun = Boolean(opts.dryRun);
|
|
2131
|
+
const cwd = process.cwd();
|
|
2132
|
+
let surfaces;
|
|
2133
|
+
try {
|
|
2134
|
+
surfaces = resolveSurfaces(opts.surface, cwd);
|
|
2135
|
+
} catch (err2) {
|
|
2136
|
+
process.stderr.write(`${err2.message}
|
|
2137
|
+
`);
|
|
2138
|
+
return process.exit(2);
|
|
2139
|
+
}
|
|
2140
|
+
const results = [];
|
|
2141
|
+
try {
|
|
2142
|
+
const installed = installHookSurfaces(surfaces, { dryRun, local: opts.local, cwd, home: homedir3() });
|
|
2143
|
+
for (const { surface, results: surfaceResults } of installed) {
|
|
2144
|
+
process.stdout.write(`${HOOK_SURFACE_LABEL[surface]}:
|
|
2145
|
+
`);
|
|
2146
|
+
for (const r of surfaceResults) {
|
|
2147
|
+
results.push(r);
|
|
2148
|
+
process.stdout.write(describe(r, dryRun) + "\n");
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
} catch (err2) {
|
|
2152
|
+
process.stderr.write(`hook install failed: ${err2.message}
|
|
2153
|
+
`);
|
|
2154
|
+
return process.exit(1);
|
|
2155
|
+
}
|
|
2156
|
+
if (dryRun) {
|
|
2157
|
+
process.stdout.write("\n(dry run \u2014 no files were written.)\n");
|
|
2158
|
+
} else if (results.every((r) => r.status === "current")) {
|
|
2159
|
+
process.stdout.write("\nAlready up to date \u2014 nothing to change.\n");
|
|
2160
|
+
} else {
|
|
2161
|
+
process.stdout.write("\nRestart (or reload) your agent for the hooks to take effect.\n");
|
|
2162
|
+
}
|
|
2163
|
+
return process.exit(0);
|
|
2164
|
+
});
|
|
1737
2165
|
}
|
|
1738
2166
|
|
|
1739
2167
|
// src/commands/account.ts
|
|
@@ -1953,129 +2381,8 @@ Examples:
|
|
|
1953
2381
|
|
|
1954
2382
|
// src/setup/apply.ts
|
|
1955
2383
|
import { createHash as createHash2 } from "crypto";
|
|
1956
|
-
import { mkdirSync as
|
|
1957
|
-
import { dirname as
|
|
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
|
|
2384
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
|
|
2385
|
+
import { dirname as dirname5 } from "path";
|
|
2079
2386
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
2080
2387
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
2081
2388
|
function normalizeBody(s) {
|
|
@@ -2128,22 +2435,22 @@ function parseManagedBlock(content, block) {
|
|
|
2128
2435
|
return null;
|
|
2129
2436
|
}
|
|
2130
2437
|
function ensureDir2(path) {
|
|
2131
|
-
|
|
2438
|
+
mkdirSync4(dirname5(path), { recursive: true });
|
|
2132
2439
|
}
|
|
2133
2440
|
function readOr(path, fallback) {
|
|
2134
2441
|
try {
|
|
2135
|
-
return
|
|
2442
|
+
return readFileSync4(path, "utf8");
|
|
2136
2443
|
} catch {
|
|
2137
2444
|
return fallback;
|
|
2138
2445
|
}
|
|
2139
2446
|
}
|
|
2140
2447
|
function mergeMcpJson(path, snippet, dryRun) {
|
|
2141
2448
|
const incoming = JSON.parse(snippet);
|
|
2142
|
-
const existed =
|
|
2449
|
+
const existed = existsSync5(path);
|
|
2143
2450
|
let current = {};
|
|
2144
2451
|
if (existed) {
|
|
2145
2452
|
try {
|
|
2146
|
-
current = JSON.parse(
|
|
2453
|
+
current = JSON.parse(readFileSync4(path, "utf8"));
|
|
2147
2454
|
} catch {
|
|
2148
2455
|
return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
|
|
2149
2456
|
}
|
|
@@ -2151,26 +2458,26 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
2151
2458
|
current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
|
|
2152
2459
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
2153
2460
|
ensureDir2(path);
|
|
2154
|
-
|
|
2461
|
+
writeFileSync4(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
2155
2462
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
2156
2463
|
}
|
|
2157
2464
|
function mergeCodexToml(path, snippet, dryRun) {
|
|
2158
|
-
const existed =
|
|
2465
|
+
const existed = existsSync5(path);
|
|
2159
2466
|
let body = readOr(path, "");
|
|
2160
2467
|
body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
|
|
2161
2468
|
const trimmed = body.trim();
|
|
2162
2469
|
const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
|
|
2163
2470
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
2164
2471
|
ensureDir2(path);
|
|
2165
|
-
|
|
2472
|
+
writeFileSync4(path, next, { mode: 384 });
|
|
2166
2473
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
2167
2474
|
}
|
|
2168
2475
|
function writeInstructionBlock(path, write, dryRun) {
|
|
2169
|
-
const existed =
|
|
2476
|
+
const existed = existsSync5(path);
|
|
2170
2477
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
2171
2478
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
2172
2479
|
ensureDir2(path);
|
|
2173
|
-
|
|
2480
|
+
writeFileSync4(path, next);
|
|
2174
2481
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
2175
2482
|
}
|
|
2176
2483
|
function computeBlockFile(current, write) {
|
|
@@ -2211,7 +2518,7 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
2211
2518
|
const next = computeBlockFile(current, write);
|
|
2212
2519
|
if (!dryRun) {
|
|
2213
2520
|
ensureDir2(proposedPath);
|
|
2214
|
-
|
|
2521
|
+
writeFileSync4(proposedPath, next);
|
|
2215
2522
|
}
|
|
2216
2523
|
return {
|
|
2217
2524
|
kind: "instruction",
|
|
@@ -2281,65 +2588,10 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
2281
2588
|
return actions;
|
|
2282
2589
|
}
|
|
2283
2590
|
|
|
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
2591
|
// src/setup/skills-offer.ts
|
|
2340
|
-
import { mkdirSync as
|
|
2341
|
-
import { homedir as
|
|
2342
|
-
import { join as
|
|
2592
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
2593
|
+
import { homedir as homedir4 } from "os";
|
|
2594
|
+
import { join as join5 } from "path";
|
|
2343
2595
|
|
|
2344
2596
|
// src/setup/lane-pin.ts
|
|
2345
2597
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -2365,7 +2617,7 @@ function writePin(code, design) {
|
|
|
2365
2617
|
if (design) values["design-lane"] = design;
|
|
2366
2618
|
if (Object.keys(values).length === 0) return;
|
|
2367
2619
|
const target = writeSem(values);
|
|
2368
|
-
process.stderr.write(`${ok("\u2713")} lane pin written \u2192 ${target} ${style.dim("(./.
|
|
2620
|
+
process.stderr.write(`${ok("\u2713")} lane pin written \u2192 ${target} ${style.dim("(./.sechroom/lane.json, git-ignored)")}
|
|
2369
2621
|
`);
|
|
2370
2622
|
}
|
|
2371
2623
|
async function ensureLanePin(cfg, opts) {
|
|
@@ -2454,14 +2706,14 @@ async function maybeOfferSkills(cfg, personalWorkspaceId, opts) {
|
|
|
2454
2706
|
Found ${style.bold(String(names.length))} operator skill(s) installed in your workspace: ${names.join(", ")}.
|
|
2455
2707
|
`
|
|
2456
2708
|
);
|
|
2457
|
-
const dir =
|
|
2709
|
+
const dir = join5(homedir4(), ".claude", "skills");
|
|
2458
2710
|
const materialise = opts.yes ? true : canPrompt() ? await promptYesNo(`Write them to ${dir}/ so ${surface} can use them?`) : false;
|
|
2459
2711
|
if (!materialise) return;
|
|
2460
2712
|
const written = [];
|
|
2461
2713
|
for (const [name, m] of byName) {
|
|
2462
2714
|
const body = m.text ?? m.Text ?? "";
|
|
2463
|
-
|
|
2464
|
-
|
|
2715
|
+
mkdirSync5(join5(dir, name), { recursive: true });
|
|
2716
|
+
writeFileSync5(join5(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
|
|
2465
2717
|
written.push(name);
|
|
2466
2718
|
}
|
|
2467
2719
|
process.stderr.write(`${style.green("\u2713")} wrote ${written.length} skill(s) to ${dir}
|
|
@@ -2613,6 +2865,43 @@ async function runClients(clients, cmd, opts) {
|
|
|
2613
2865
|
process.stdout.write(opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n");
|
|
2614
2866
|
}
|
|
2615
2867
|
|
|
2868
|
+
// src/setup/hooks-offer.ts
|
|
2869
|
+
import { homedir as homedir5 } from "os";
|
|
2870
|
+
async function maybeOfferHooks(opts) {
|
|
2871
|
+
if (opts.dryRun) return;
|
|
2872
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2873
|
+
const surfaces = detectHookSurfaces(cwd);
|
|
2874
|
+
if (surfaces.length === 0) return;
|
|
2875
|
+
const names = surfaces.map((s) => HOOK_SURFACE_LABEL[s]).join(" + ");
|
|
2876
|
+
process.stderr.write(
|
|
2877
|
+
`
|
|
2878
|
+
Sechroom can wire continuity lifecycle hooks into ${style.bold(names)} so your agent
|
|
2879
|
+
auto-resumes where you left off and checkpoints working state before compacting.
|
|
2880
|
+
`
|
|
2881
|
+
);
|
|
2882
|
+
const install = opts.yes ? true : canPrompt() ? await promptYesNo(`Install the continuity hooks for ${names}?`) : false;
|
|
2883
|
+
if (!install) return;
|
|
2884
|
+
try {
|
|
2885
|
+
const installed = installHookSurfaces(surfaces, { dryRun: false, cwd, home: homedir5() });
|
|
2886
|
+
let changed = false;
|
|
2887
|
+
for (const { surface, results } of installed) {
|
|
2888
|
+
for (const r of results) {
|
|
2889
|
+
if (r.status !== "current") changed = true;
|
|
2890
|
+
const verb = r.status === "current" ? "already configured" : r.status === "created" ? "created" : "updated";
|
|
2891
|
+
process.stderr.write(`${style.green("\u2713")} ${HOOK_SURFACE_LABEL[surface]}: ${r.path} (${verb})
|
|
2892
|
+
`);
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
if (changed) {
|
|
2896
|
+
process.stderr.write(`${style.dim("Restart (or reload) your agent for the hooks to take effect.")}
|
|
2897
|
+
`);
|
|
2898
|
+
}
|
|
2899
|
+
} catch (err2) {
|
|
2900
|
+
process.stderr.write(`${style.dim(`(skipped hook install: ${err2.message})`)}
|
|
2901
|
+
`);
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2616
2905
|
// src/commands/onboard.ts
|
|
2617
2906
|
var DEFAULT_BASE_URL2 = "https://app.sechroom.ai/api";
|
|
2618
2907
|
function systemTimezone() {
|
|
@@ -2791,7 +3080,7 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
2791
3080
|
"Where should this tenant + base URL be saved?",
|
|
2792
3081
|
[
|
|
2793
3082
|
{ label: "Globally", value: "global", hint: "all projects on this machine" },
|
|
2794
|
-
{ label: "This directory", value: "local", hint: ".sechroom.json \u2014 project + subdirs" }
|
|
3083
|
+
{ label: "This directory", value: "local", hint: ".sechroom/config.json \u2014 project + subdirs" }
|
|
2795
3084
|
],
|
|
2796
3085
|
local.path ? "local" : "global"
|
|
2797
3086
|
) === "local";
|
|
@@ -2867,14 +3156,14 @@ async function chooseClients(clientFlag, yes, cwd) {
|
|
|
2867
3156
|
return picks.length > 0 ? picks : preselected;
|
|
2868
3157
|
}
|
|
2869
3158
|
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(
|
|
3159
|
+
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
3160
|
"after",
|
|
2872
3161
|
`
|
|
2873
3162
|
Examples:
|
|
2874
3163
|
$ sechroom onboard guided, interactive (asks where to save config + how to wire)
|
|
2875
3164
|
$ sechroom onboard --cli-only just the CLI \u2014 no .mcp.json, no agent files
|
|
2876
3165
|
$ sechroom onboard --no-mcp agent instructions only, skip MCP config
|
|
2877
|
-
$ sechroom onboard --local save tenant + base URL to ./.sechroom.json
|
|
3166
|
+
$ sechroom onboard --local save tenant + base URL to ./.sechroom/config.json
|
|
2878
3167
|
$ sechroom onboard --workspace wsp_XX bind this directory to a workspace (no pick prompt)
|
|
2879
3168
|
$ sechroom onboard --refresh refresh out-of-date instruction blocks in place
|
|
2880
3169
|
$ sechroom onboard --check CI/pre-commit: nonzero exit if instructions are out of date
|
|
@@ -2904,7 +3193,10 @@ Examples:
|
|
|
2904
3193
|
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, clients: [] }, true);
|
|
2905
3194
|
return;
|
|
2906
3195
|
}
|
|
2907
|
-
if (!dryRun)
|
|
3196
|
+
if (!dryRun) {
|
|
3197
|
+
await ensureLanePin(cfg, { yes, dryRun, clients: detectInstalledClients(process.cwd()) });
|
|
3198
|
+
await maybeOfferHooks({ yes, dryRun, cwd: process.cwd() });
|
|
3199
|
+
}
|
|
2908
3200
|
process.stdout.write(
|
|
2909
3201
|
`
|
|
2910
3202
|
${style.bold("Done.")} The CLI is configured for ${style.cyan(cfg.tenant)} \u2014 no AI-client files written.
|
|
@@ -2962,6 +3254,9 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
2962
3254
|
if (!json && !dryRun) {
|
|
2963
3255
|
await maybeOfferSkills(cfg, personalWorkspaceId, { yes, dryRun, surface: "claude-code" });
|
|
2964
3256
|
}
|
|
3257
|
+
if (!json && !dryRun) {
|
|
3258
|
+
await maybeOfferHooks({ yes, dryRun, cwd: process.cwd() });
|
|
3259
|
+
}
|
|
2965
3260
|
if (json) {
|
|
2966
3261
|
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, eval: evalCounts, clients: result }, true);
|
|
2967
3262
|
return;
|
|
@@ -3034,14 +3329,14 @@ ${style.bold("Next:")} paste this into your AI agent to get going \u2014
|
|
|
3034
3329
|
}
|
|
3035
3330
|
|
|
3036
3331
|
// src/commands/skills.ts
|
|
3037
|
-
import { homedir as
|
|
3038
|
-
import { join as
|
|
3039
|
-
import { mkdirSync as
|
|
3332
|
+
import { homedir as homedir6 } from "os";
|
|
3333
|
+
import { join as join6 } from "path";
|
|
3334
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, rmSync as rmSync2, existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
3040
3335
|
var DEFAULT_SLUG = "operator-skills";
|
|
3041
3336
|
var ROLE_TAGS = ["sechroom:role:skill-template", "role:skill-template"];
|
|
3042
3337
|
var LOCK = ".sechroom-skills.json";
|
|
3043
3338
|
function skillsDir(global) {
|
|
3044
|
-
return global ?
|
|
3339
|
+
return global ? join6(homedir6(), ".claude", "skills") : join6(process.cwd(), ".claude", "skills");
|
|
3045
3340
|
}
|
|
3046
3341
|
function tagValue2(tags, prefix) {
|
|
3047
3342
|
return (tags ?? []).find((t) => t.startsWith(prefix))?.slice(prefix.length);
|
|
@@ -3119,15 +3414,15 @@ Examples:
|
|
|
3119
3414
|
const name = tagValue2(tags, "skill:");
|
|
3120
3415
|
if (!name) continue;
|
|
3121
3416
|
const body = m.text ?? m.Text ?? "";
|
|
3122
|
-
|
|
3123
|
-
|
|
3417
|
+
mkdirSync6(join6(dir, name), { recursive: true });
|
|
3418
|
+
writeFileSync6(join6(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
|
|
3124
3419
|
written.push(name);
|
|
3125
3420
|
}
|
|
3126
|
-
|
|
3127
|
-
const lockPath =
|
|
3128
|
-
const lock =
|
|
3421
|
+
mkdirSync6(dir, { recursive: true });
|
|
3422
|
+
const lockPath = join6(dir, LOCK);
|
|
3423
|
+
const lock = existsSync6(lockPath) ? JSON.parse(readFileSync5(lockPath, "utf8")) : {};
|
|
3129
3424
|
lock[slug] = { surface: opts.surface, version, instance: wantInstance, skills: written.sort() };
|
|
3130
|
-
|
|
3425
|
+
writeFileSync6(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
3131
3426
|
if (opts.json) return emit({ slug, version, instance: wantInstance, surface: opts.surface, dir, installed: written }, true);
|
|
3132
3427
|
const instanceNote = opts.instance ? ` (${opts.instance})` : "";
|
|
3133
3428
|
console.log(style.green(`Installed ${slug}@${version}${instanceNote} \u2014 ${written.length} skill(s) \u2192 ${dir}`));
|
|
@@ -3149,28 +3444,28 @@ Examples:
|
|
|
3149
3444
|
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
3445
|
const slug = slugArg || DEFAULT_SLUG;
|
|
3151
3446
|
const dir = skillsDir(!opts.local);
|
|
3152
|
-
const lockPath =
|
|
3153
|
-
if (!
|
|
3154
|
-
const lock = JSON.parse(
|
|
3447
|
+
const lockPath = join6(dir, LOCK);
|
|
3448
|
+
if (!existsSync6(lockPath)) fail(`No skills lockfile at ${lockPath}; nothing to clean.`);
|
|
3449
|
+
const lock = JSON.parse(readFileSync5(lockPath, "utf8"));
|
|
3155
3450
|
const entry = lock[slug];
|
|
3156
3451
|
if (!entry) fail(`No installed record for '${slug}' in ${lockPath}.`);
|
|
3157
3452
|
const removed = [];
|
|
3158
3453
|
for (const name of entry.skills) {
|
|
3159
|
-
const skillPath =
|
|
3160
|
-
if (
|
|
3454
|
+
const skillPath = join6(dir, name);
|
|
3455
|
+
if (existsSync6(skillPath)) {
|
|
3161
3456
|
rmSync2(skillPath, { recursive: true, force: true });
|
|
3162
3457
|
removed.push(name);
|
|
3163
3458
|
}
|
|
3164
3459
|
}
|
|
3165
3460
|
delete lock[slug];
|
|
3166
|
-
|
|
3461
|
+
writeFileSync6(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
3167
3462
|
if (opts.json) return emit({ slug, removed, dir }, true);
|
|
3168
3463
|
console.log(style.green(`Removed ${removed.length} skill(s) for ${slug} from ${dir}`));
|
|
3169
3464
|
});
|
|
3170
|
-
skills.command("set-lane").description("Write this checkout's lane pin to a local ./.
|
|
3465
|
+
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
3466
|
if (!opts.codeLane && !opts.designLane) fail("Provide --code-lane and/or --design-lane.");
|
|
3172
3467
|
const target = localSemPath();
|
|
3173
|
-
const values =
|
|
3468
|
+
const values = readLocalSemValues();
|
|
3174
3469
|
if (opts.codeLane) values["code-lane"] = opts.codeLane;
|
|
3175
3470
|
if (opts.designLane) values["design-lane"] = opts.designLane;
|
|
3176
3471
|
writeSem(values, target);
|
|
@@ -3178,12 +3473,12 @@ Examples:
|
|
|
3178
3473
|
console.log(style.green(`Wrote lane pin \u2192 ${target} ${style.dim("(git-ignored)")}`));
|
|
3179
3474
|
Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
|
|
3180
3475
|
});
|
|
3181
|
-
skills.command("lane").description("Show the lane pin resolved from ./.
|
|
3476
|
+
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
3477
|
const json = cmd.optsWithGlobals().json;
|
|
3183
3478
|
const found = readSem();
|
|
3184
3479
|
if (!found) {
|
|
3185
3480
|
if (json) return emit({ path: null, values: {} }, true);
|
|
3186
|
-
return console.log(style.dim(`No ./.
|
|
3481
|
+
return console.log(style.dim(`No ./.sechroom/lane.json pin in this checkout. Run 'sechroom skills set-lane'.`));
|
|
3187
3482
|
}
|
|
3188
3483
|
if (json) return emit(found, true);
|
|
3189
3484
|
console.log(style.dim(`from ${found.path}`));
|
|
@@ -3252,22 +3547,22 @@ Examples:
|
|
|
3252
3547
|
}
|
|
3253
3548
|
|
|
3254
3549
|
// src/commands/reset.ts
|
|
3255
|
-
import { homedir as
|
|
3256
|
-
import { join as
|
|
3257
|
-
import { existsSync as
|
|
3550
|
+
import { homedir as homedir7 } from "os";
|
|
3551
|
+
import { join as join7 } from "path";
|
|
3552
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, rmSync as rmSync3 } from "fs";
|
|
3258
3553
|
var SKILLS_LOCK = ".sechroom-skills.json";
|
|
3259
|
-
var localSkillsDir = () =>
|
|
3260
|
-
var globalSkillsDir = () =>
|
|
3554
|
+
var localSkillsDir = () => join7(process.cwd(), ".claude", "skills");
|
|
3555
|
+
var globalSkillsDir = () => join7(homedir7(), ".claude", "skills");
|
|
3261
3556
|
function removeMaterialisedSkills(dir) {
|
|
3262
3557
|
const removed = [];
|
|
3263
|
-
const lockPath =
|
|
3264
|
-
if (!
|
|
3558
|
+
const lockPath = join7(dir, SKILLS_LOCK);
|
|
3559
|
+
if (!existsSync7(lockPath)) return removed;
|
|
3265
3560
|
try {
|
|
3266
|
-
const lock = JSON.parse(
|
|
3561
|
+
const lock = JSON.parse(readFileSync6(lockPath, "utf8"));
|
|
3267
3562
|
for (const entry of Object.values(lock)) {
|
|
3268
3563
|
for (const name of entry.skills ?? []) {
|
|
3269
|
-
const p =
|
|
3270
|
-
if (
|
|
3564
|
+
const p = join7(dir, name);
|
|
3565
|
+
if (existsSync7(p)) {
|
|
3271
3566
|
rmSync3(p, { recursive: true, force: true });
|
|
3272
3567
|
removed.push(p);
|
|
3273
3568
|
}
|
|
@@ -3287,26 +3582,31 @@ function registerReset(program2) {
|
|
|
3287
3582
|
removed ? style.green("Signed out \u2014 auth token removed.") : style.dim("Already signed out (no token).")
|
|
3288
3583
|
);
|
|
3289
3584
|
});
|
|
3290
|
-
program2.command("reset").description("Reset LOCAL CLI state for this directory (./.sechroom.json
|
|
3585
|
+
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
3586
|
const json = cmd.optsWithGlobals().json;
|
|
3292
3587
|
const global = Boolean(opts.global);
|
|
3293
3588
|
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
|
|
3589
|
+
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
3590
|
if (!await promptYesNo(`Remove ${scope}?`)) {
|
|
3296
3591
|
if (!json) console.log(style.dim("Cancelled."));
|
|
3297
3592
|
return;
|
|
3298
3593
|
}
|
|
3299
3594
|
}
|
|
3300
3595
|
const removed = [];
|
|
3301
|
-
const
|
|
3302
|
-
if (
|
|
3303
|
-
rmSync3(
|
|
3304
|
-
removed.push(
|
|
3596
|
+
const stateDir = join7(process.cwd(), ".sechroom");
|
|
3597
|
+
if (existsSync7(stateDir)) {
|
|
3598
|
+
rmSync3(stateDir, { recursive: true, force: true });
|
|
3599
|
+
removed.push(stateDir);
|
|
3600
|
+
}
|
|
3601
|
+
const legacyCfg = join7(process.cwd(), ".sechroom.json");
|
|
3602
|
+
if (existsSync7(legacyCfg)) {
|
|
3603
|
+
rmSync3(legacyCfg, { force: true });
|
|
3604
|
+
removed.push(legacyCfg);
|
|
3305
3605
|
}
|
|
3306
|
-
const
|
|
3307
|
-
if (
|
|
3308
|
-
rmSync3(
|
|
3309
|
-
removed.push(
|
|
3606
|
+
const legacySem = join7(process.cwd(), ".sem");
|
|
3607
|
+
if (existsSync7(legacySem)) {
|
|
3608
|
+
rmSync3(legacySem, { force: true });
|
|
3609
|
+
removed.push(legacySem);
|
|
3310
3610
|
}
|
|
3311
3611
|
removed.push(...removeMaterialisedSkills(localSkillsDir()));
|
|
3312
3612
|
if (global) {
|
|
@@ -3331,7 +3631,7 @@ function registerReset(program2) {
|
|
|
3331
3631
|
function resolveVersion() {
|
|
3332
3632
|
try {
|
|
3333
3633
|
const pkg = JSON.parse(
|
|
3334
|
-
|
|
3634
|
+
readFileSync7(new URL("../package.json", import.meta.url), "utf8")
|
|
3335
3635
|
);
|
|
3336
3636
|
return pkg.version ?? "0.0.0";
|
|
3337
3637
|
} catch {
|
|
@@ -3347,7 +3647,7 @@ Examples:
|
|
|
3347
3647
|
$ sechroom onboard guided first-run: configure, sign in, wire this project
|
|
3348
3648
|
$ sechroom login sign in via browser (OAuth + PKCE)
|
|
3349
3649
|
$ sechroom config set tenant ocd set your tenant (global)
|
|
3350
|
-
$ sechroom config set --local tenant cli-smoke pin tenant for this directory (.sechroom.json)
|
|
3650
|
+
$ sechroom config set --local tenant cli-smoke pin tenant for this directory (.sechroom/config.json)
|
|
3351
3651
|
$ sechroom config show resolved config + which source won
|
|
3352
3652
|
|
|
3353
3653
|
$ sechroom memory create --text "a note" --title "Note" --tag idea
|
|
@@ -3359,7 +3659,7 @@ Examples:
|
|
|
3359
3659
|
$ sechroom --json memory search "auth" compact JSON for scripts and agents
|
|
3360
3660
|
$ SECHROOM_TOKEN=<bearer> sechroom --json memory get mem_XXXX headless
|
|
3361
3661
|
|
|
3362
|
-
Config precedence (high -> low): --flag > env (SECHROOM_*) > ./.sechroom.json > global > default.
|
|
3662
|
+
Config precedence (high -> low): --flag > env (SECHROOM_*) > ./.sechroom/config.json (legacy ./.sechroom.json) > global > default.
|
|
3363
3663
|
Run 'sechroom <command> --help' for command-specific examples.`
|
|
3364
3664
|
);
|
|
3365
3665
|
program.hook("preAction", (_thisCmd, actionCmd) => {
|
|
@@ -3385,11 +3685,11 @@ config.addHelpText(
|
|
|
3385
3685
|
Examples:
|
|
3386
3686
|
$ sechroom config set baseUrl https://app.sechroom.ai/api prod (staging: https://staging.app.sechroom.ai/api)
|
|
3387
3687
|
$ sechroom config set tenant ocd
|
|
3388
|
-
$ sechroom config set --local tenant cli-smoke this dir + subdirs (.sechroom.json)
|
|
3688
|
+
$ sechroom config set --local tenant cli-smoke this dir + subdirs (.sechroom/config.json)
|
|
3389
3689
|
$ sechroom config set clientId dyn-XXXX global-only escape hatch (no DCR endpoint)
|
|
3390
3690
|
$ sechroom config show --json`
|
|
3391
3691
|
);
|
|
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) => {
|
|
3692
|
+
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
3693
|
if (opts.local) {
|
|
3394
3694
|
if (!["baseUrl", "tenant", "workspaceId", "defaultProjectId"].includes(key)) {
|
|
3395
3695
|
process.stderr.write(`--local supports only: baseUrl | tenant | workspaceId | defaultProjectId (clientId is global)
|
package/package.json
CHANGED