@sechroom/cli 2026.6.17 → 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.
- package/dist/index.js +863 -396
- 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,96 +1571,678 @@ Examples:
|
|
|
1558
1571
|
});
|
|
1559
1572
|
}
|
|
1560
1573
|
|
|
1561
|
-
// src/commands/
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
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
|
+
|
|
1579
|
+
// src/sem.ts
|
|
1580
|
+
import { basename as basename2, dirname as dirname2, join as join2 } from "path";
|
|
1581
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1582
|
+
var SEM_FILE = join2(".sechroom", "lane.json");
|
|
1583
|
+
var LEGACY_SEM_FILE = ".sem";
|
|
1584
|
+
var STATE_DIR_NAME2 = ".sechroom";
|
|
1585
|
+
function localSemPath(cwd = process.cwd()) {
|
|
1586
|
+
return join2(cwd, SEM_FILE);
|
|
1587
|
+
}
|
|
1588
|
+
function resolveSemPathForRead(start = process.cwd()) {
|
|
1589
|
+
let dir = start;
|
|
1590
|
+
while (true) {
|
|
1591
|
+
const candidate = join2(dir, SEM_FILE);
|
|
1592
|
+
if (existsSync2(candidate)) return candidate;
|
|
1593
|
+
const legacy = join2(dir, LEGACY_SEM_FILE);
|
|
1594
|
+
if (existsSync2(legacy)) return legacy;
|
|
1595
|
+
const parent = dirname2(dir);
|
|
1596
|
+
if (parent === dir) return void 0;
|
|
1597
|
+
dir = parent;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
function parseSem(text) {
|
|
1601
|
+
const out = {};
|
|
1602
|
+
for (const raw of text.split("\n")) {
|
|
1603
|
+
const line = raw.trim();
|
|
1604
|
+
if (!line || line.startsWith("#")) continue;
|
|
1605
|
+
const eq = line.indexOf("=");
|
|
1606
|
+
if (eq === -1) continue;
|
|
1607
|
+
const key = line.slice(0, eq).trim();
|
|
1608
|
+
const value = line.slice(eq + 1).trim();
|
|
1609
|
+
if (key) out[key] = value;
|
|
1610
|
+
}
|
|
1611
|
+
return out;
|
|
1612
|
+
}
|
|
1613
|
+
function serializeSem(values) {
|
|
1614
|
+
return JSON.stringify(values, null, 2) + "\n";
|
|
1615
|
+
}
|
|
1616
|
+
function readSem(path) {
|
|
1617
|
+
const p = path ?? resolveSemPathForRead();
|
|
1618
|
+
if (!p || !existsSync2(p)) return void 0;
|
|
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
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
var STATE_DIR_IGNORE = `${STATE_DIR_NAME2}/`;
|
|
1643
|
+
function writeSem(values, path = localSemPath()) {
|
|
1644
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1645
|
+
writeFileSync2(path, serializeSem(values));
|
|
1646
|
+
ensureSemIgnored(path);
|
|
1647
|
+
return path;
|
|
1648
|
+
}
|
|
1649
|
+
function ignoresSem(content) {
|
|
1650
|
+
return content.split("\n").some((line) => {
|
|
1651
|
+
const t = line.trim();
|
|
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}`;
|
|
1592
1653
|
});
|
|
1593
1654
|
}
|
|
1594
|
-
function
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
"
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1655
|
+
function resolveGitignoreTarget(startDir) {
|
|
1656
|
+
let dir = startDir;
|
|
1657
|
+
for (; ; ) {
|
|
1658
|
+
const gi = join2(dir, ".gitignore");
|
|
1659
|
+
if (existsSync2(gi)) return { path: gi, exists: true };
|
|
1660
|
+
const parent = dirname2(dir);
|
|
1661
|
+
if (existsSync2(join2(dir, ".git")) || parent === dir) {
|
|
1662
|
+
return { path: join2(startDir, ".gitignore"), exists: false };
|
|
1663
|
+
}
|
|
1664
|
+
dir = parent;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
function ensureSemIgnored(semPath) {
|
|
1668
|
+
try {
|
|
1669
|
+
const checkoutDir = dirname2(dirname2(semPath));
|
|
1670
|
+
const target = resolveGitignoreTarget(checkoutDir);
|
|
1671
|
+
if (target.exists) {
|
|
1672
|
+
const content = readFileSync2(target.path, "utf8");
|
|
1673
|
+
if (ignoresSem(content)) return;
|
|
1674
|
+
const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
1675
|
+
appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
|
|
1676
|
+
`);
|
|
1677
|
+
} else {
|
|
1678
|
+
writeFileSync2(target.path, `${STATE_DIR_IGNORE}
|
|
1679
|
+
`);
|
|
1680
|
+
}
|
|
1681
|
+
} catch {
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
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 } } } : {}
|
|
1605
1706
|
);
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
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 }
|
|
1611
1750
|
});
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
const
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
timezone: opts.timezone ?? null,
|
|
1622
|
-
bio: opts.bio ?? null,
|
|
1623
|
-
photoUrl: opts.photoUrl ?? null
|
|
1624
|
-
}
|
|
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 } }
|
|
1625
1760
|
});
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
const data = await runApi("Fetching feed", async () => {
|
|
1632
|
-
const client = await makeClient(cfg);
|
|
1633
|
-
return client.GET("/me/memories/feed", {
|
|
1634
|
-
params: {
|
|
1635
|
-
query: {
|
|
1636
|
-
limit: Number(opts.limit),
|
|
1637
|
-
includeArchived: Boolean(opts.includeArchived),
|
|
1638
|
-
includeText: Boolean(opts.includeText),
|
|
1639
|
-
...opts.cursor ? { cursor: opts.cursor } : {},
|
|
1640
|
-
...opts.query ? { query: opts.query } : {},
|
|
1641
|
-
...opts.filterTags ? { filterTags: opts.filterTags } : {}
|
|
1642
|
-
}
|
|
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}` };
|
|
1643
1766
|
}
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
|
|
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
|
+
}
|
|
1647
1805
|
});
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
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
|
+
|
|
1861
|
+
// src/commands/hook.ts
|
|
1862
|
+
async function readStdin() {
|
|
1863
|
+
if (process.stdin.isTTY) return "";
|
|
1864
|
+
const chunks = [];
|
|
1865
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
1866
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1867
|
+
}
|
|
1868
|
+
function parseHookInput(raw) {
|
|
1869
|
+
if (!raw.trim()) return {};
|
|
1870
|
+
try {
|
|
1871
|
+
return JSON.parse(raw);
|
|
1872
|
+
} catch {
|
|
1873
|
+
return {};
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
function resolveLane(flagLane, cwd) {
|
|
1877
|
+
if (flagLane) return flagLane;
|
|
1878
|
+
const env = process.env.SECHROOM_LANE;
|
|
1879
|
+
if (env) return env;
|
|
1880
|
+
const start = cwd ?? process.cwd();
|
|
1881
|
+
const sem = readSem(resolveSemPathForRead(start));
|
|
1882
|
+
return sem?.values["code-lane"];
|
|
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
|
+
}
|
|
1909
|
+
function formatContext(bundle, lane) {
|
|
1910
|
+
const s = bundle?.latestSnapshot;
|
|
1911
|
+
if (!s) return null;
|
|
1912
|
+
const lines = [];
|
|
1913
|
+
lines.push(`[sechroom continuity \u2014 resumed lane ${lane}]`);
|
|
1914
|
+
if (s.currentObjective) lines.push(`Objective: ${s.currentObjective}`);
|
|
1915
|
+
if (s.currentState) lines.push(`State: ${s.currentState}`);
|
|
1916
|
+
if (s.lastMeaningfulAction) lines.push(`Last action: ${s.lastMeaningfulAction}`);
|
|
1917
|
+
if (s.nextIntendedAction) lines.push(`Next: ${s.nextIntendedAction}`);
|
|
1918
|
+
if (s.resumeInstruction) lines.push(`Resume: ${s.resumeInstruction}`);
|
|
1919
|
+
const constraints = s.activeConstraints ?? [];
|
|
1920
|
+
if (constraints.length) {
|
|
1921
|
+
lines.push("Active constraints:");
|
|
1922
|
+
for (const c of constraints) lines.push(` - ${c}`);
|
|
1923
|
+
}
|
|
1924
|
+
const questions = s.openQuestions ?? [];
|
|
1925
|
+
if (questions.length) {
|
|
1926
|
+
lines.push("Open questions:");
|
|
1927
|
+
for (const q of questions) lines.push(` - ${q}`);
|
|
1928
|
+
}
|
|
1929
|
+
const artifacts = s.relevantArtifactIds ?? [];
|
|
1930
|
+
if (artifacts.length) lines.push(`Relevant artifacts: ${artifacts.join(", ")}`);
|
|
1931
|
+
const marker = [s.id, s.createdAt].filter(Boolean).join(" @ ");
|
|
1932
|
+
if (marker) lines.push(`(snapshot ${marker})`);
|
|
1933
|
+
return lines.join("\n");
|
|
1934
|
+
}
|
|
1935
|
+
function emitSessionStart(additionalContext) {
|
|
1936
|
+
process.stdout.write(
|
|
1937
|
+
JSON.stringify({
|
|
1938
|
+
hookSpecificOutput: {
|
|
1939
|
+
hookEventName: "SessionStart",
|
|
1940
|
+
additionalContext
|
|
1941
|
+
}
|
|
1942
|
+
}) + "\n"
|
|
1943
|
+
);
|
|
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
|
+
}
|
|
2030
|
+
function registerHook(program2) {
|
|
2031
|
+
const hook = program2.command("hook").description("Agent-lifecycle hook adapter (Claude Code / Codex) \u2014 bridges hooks to continuity");
|
|
2032
|
+
hook.addHelpText(
|
|
2033
|
+
"after",
|
|
2034
|
+
`
|
|
2035
|
+
Examples:
|
|
2036
|
+
# SessionStart (load): inject the lane's latest snapshot as context.
|
|
2037
|
+
$ echo '{"hook_event_name":"SessionStart","cwd":"'"$PWD"'"}' | sechroom hook session-start
|
|
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
|
|
2044
|
+
|
|
2045
|
+
Lane source (high -> low): --lane > SECHROOM_LANE > ./.sem code-lane (D-binding-5).
|
|
2046
|
+
Fail-soft: no lane / no auth / no-or-partial intent file / API error -> exit 0, never blocks.`
|
|
2047
|
+
);
|
|
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) => {
|
|
2049
|
+
try {
|
|
2050
|
+
const raw = await readStdin();
|
|
2051
|
+
const input = parseHookInput(raw);
|
|
2052
|
+
const lane = resolveLane(opts.lane, input.cwd);
|
|
2053
|
+
if (!lane) return process.exit(0);
|
|
2054
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2055
|
+
const client = await makeClient(cfg);
|
|
2056
|
+
const { data } = await client.POST("/continuity/resume/lane", {
|
|
2057
|
+
body: {
|
|
2058
|
+
laneId: lane,
|
|
2059
|
+
workspaceId: null,
|
|
2060
|
+
maxArtifacts: opts.maxArtifacts != null ? Number(opts.maxArtifacts) : null,
|
|
2061
|
+
includeLookingAtMyself: null,
|
|
2062
|
+
changedSince: null
|
|
2063
|
+
}
|
|
2064
|
+
});
|
|
2065
|
+
const context = formatContext(data, lane);
|
|
2066
|
+
if (context) emitSessionStart(context);
|
|
2067
|
+
return process.exit(0);
|
|
2068
|
+
} catch {
|
|
2069
|
+
return process.exit(0);
|
|
2070
|
+
}
|
|
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
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
// src/commands/account.ts
|
|
2157
|
+
function registerId(program2) {
|
|
2158
|
+
const id = program2.command("id").description("Allocate human-authored id sequences (FR-*, D-*)");
|
|
2159
|
+
id.addHelpText(
|
|
2160
|
+
"after",
|
|
2161
|
+
`
|
|
2162
|
+
Examples:
|
|
2163
|
+
$ sechroom id next FR sechroom allocate the next FR-sechroom-NNN id
|
|
2164
|
+
$ sechroom id next D Backend allocate the next D-Backend-NNN id
|
|
2165
|
+
$ sechroom id peek FR sechroom inspect the sequence without consuming
|
|
2166
|
+
$ sechroom id peek FR sechroom --json`
|
|
2167
|
+
);
|
|
2168
|
+
id.command("next <namespaceKind> <scope>").description("Allocate the next id in a sequence (POST /id-registry/allocate)").action(async (namespaceKind, scope, _opts, cmd) => {
|
|
2169
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2170
|
+
const data = await runApi("Allocating id", async () => {
|
|
2171
|
+
const client = await makeClient(cfg);
|
|
2172
|
+
return client.POST("/id-registry/allocate", {
|
|
2173
|
+
body: { namespaceKind, scope, clientNonce: null }
|
|
2174
|
+
});
|
|
2175
|
+
});
|
|
2176
|
+
emitAction(`allocated ${style.bold(data.id)} ${style.dim(`(seq ${data.seq})`)}`, data, cmd.optsWithGlobals().json);
|
|
2177
|
+
});
|
|
2178
|
+
id.command("peek <namespaceKind> <scope>").description("Inspect a sequence without consuming (GET /id-registry/state)").action(async (namespaceKind, scope, _opts, cmd) => {
|
|
2179
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2180
|
+
const data = await runApi("Peeking id sequence", async () => {
|
|
2181
|
+
const client = await makeClient(cfg);
|
|
2182
|
+
return client.GET("/id-registry/state", {
|
|
2183
|
+
params: { query: { namespaceKind, scope } }
|
|
2184
|
+
});
|
|
2185
|
+
});
|
|
2186
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
function registerAccount(program2) {
|
|
2190
|
+
const account = program2.command("account").description("Your profile, feeds, and review queue");
|
|
2191
|
+
account.addHelpText(
|
|
2192
|
+
"after",
|
|
2193
|
+
`
|
|
2194
|
+
Examples:
|
|
2195
|
+
$ sechroom account profile
|
|
2196
|
+
$ sechroom account set-profile --display-name "Chris" --timezone "Europe/London"
|
|
2197
|
+
$ sechroom account feed --limit 20
|
|
2198
|
+
$ sechroom account reviews --status Pending
|
|
2199
|
+
$ sechroom account lookup-batch mem_XXXX wsp_YYYY --json`
|
|
2200
|
+
);
|
|
2201
|
+
account.command("profile").description("Show your resolved profile (GET /me/profile)").action(async (_opts, cmd) => {
|
|
2202
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2203
|
+
const data = await runApi("Fetching profile", async () => {
|
|
2204
|
+
const client = await makeClient(cfg);
|
|
2205
|
+
return client.GET("/me/profile");
|
|
2206
|
+
});
|
|
2207
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
2208
|
+
});
|
|
2209
|
+
account.command("set-profile").description("Update your profile (PUT /me/profile)").option("--display-name <displayName>", "Display name").option("--timezone <timezone>", "IANA timezone (e.g. Europe/London)").option("--bio <bio>", "Short bio").option("--photo-url <photoUrl>", "Avatar / photo URL").action(async (opts, cmd) => {
|
|
2210
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2211
|
+
const data = await runApi("Updating profile", async () => {
|
|
2212
|
+
const client = await makeClient(cfg);
|
|
2213
|
+
return client.PUT("/me/profile", {
|
|
2214
|
+
body: {
|
|
2215
|
+
displayName: opts.displayName ?? null,
|
|
2216
|
+
timezone: opts.timezone ?? null,
|
|
2217
|
+
bio: opts.bio ?? null,
|
|
2218
|
+
photoUrl: opts.photoUrl ?? null
|
|
2219
|
+
}
|
|
2220
|
+
});
|
|
2221
|
+
});
|
|
2222
|
+
emitAction("updated profile", data, cmd.optsWithGlobals().json);
|
|
2223
|
+
});
|
|
2224
|
+
account.command("feed").description("Your recent memory feed (GET /me/memories/feed)").option("--limit <n>", "Max results", "20").option("--cursor <cursor>", "Opaque paging cursor").option("--query <query>", "Free-text filter").option("--filter-tags <tags>", "Comma-separated tag filter").option("--include-archived", "Include archived memories", false).option("--include-text", "Include memory body text", false).action(async (opts, cmd) => {
|
|
2225
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2226
|
+
const data = await runApi("Fetching feed", async () => {
|
|
2227
|
+
const client = await makeClient(cfg);
|
|
2228
|
+
return client.GET("/me/memories/feed", {
|
|
2229
|
+
params: {
|
|
2230
|
+
query: {
|
|
2231
|
+
limit: Number(opts.limit),
|
|
2232
|
+
includeArchived: Boolean(opts.includeArchived),
|
|
2233
|
+
includeText: Boolean(opts.includeText),
|
|
2234
|
+
...opts.cursor ? { cursor: opts.cursor } : {},
|
|
2235
|
+
...opts.query ? { query: opts.query } : {},
|
|
2236
|
+
...opts.filterTags ? { filterTags: opts.filterTags } : {}
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
});
|
|
2240
|
+
});
|
|
2241
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
2242
|
+
});
|
|
2243
|
+
account.command("reviews").description("Your review queue (GET /reviews)").option("--status <status>", "Pending | Resolved | Empty").option("--scope-kind <scopeKind>", "Memory | Project | Workspace").option("--scope-target-id <id>", "Scope target id").option("--limit <n>", "Max results", "20").action(async (opts, cmd) => {
|
|
2244
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2245
|
+
const data = await runApi("Fetching reviews", async () => {
|
|
1651
2246
|
const client = await makeClient(cfg);
|
|
1652
2247
|
return client.GET("/reviews", {
|
|
1653
2248
|
params: {
|
|
@@ -1775,129 +2370,8 @@ Examples:
|
|
|
1775
2370
|
|
|
1776
2371
|
// src/setup/apply.ts
|
|
1777
2372
|
import { createHash as createHash2 } from "crypto";
|
|
1778
|
-
import { mkdirSync as
|
|
1779
|
-
import { dirname as
|
|
1780
|
-
|
|
1781
|
-
// src/setup/operator-surface.ts
|
|
1782
|
-
var SectionType = {
|
|
1783
|
-
McpConfig: "mcp-config",
|
|
1784
|
-
McpConfigToml: "mcp-config-toml",
|
|
1785
|
-
InstructionFile: "instruction-file",
|
|
1786
|
-
ProjectConfig: "project-config",
|
|
1787
|
-
Verify: "verify",
|
|
1788
|
-
/** SBC-999 — workspace-pinned conventions, emitted only when the request
|
|
1789
|
-
* carried a workspaceId and that workspace has agent-setup-bundle memories. */
|
|
1790
|
-
WorkspaceConventions: "workspace-conventions"
|
|
1791
|
-
};
|
|
1792
|
-
async function fetchSetup(cfg) {
|
|
1793
|
-
const client = await makeClient(cfg);
|
|
1794
|
-
const { data, error } = await client.GET(
|
|
1795
|
-
"/operator-surface/setup",
|
|
1796
|
-
cfg.workspaceId ? { params: { query: { workspaceId: cfg.workspaceId } } } : {}
|
|
1797
|
-
);
|
|
1798
|
-
if (error) throw new Error(`GET /operator-surface/setup failed: ${JSON.stringify(error)}`);
|
|
1799
|
-
return data;
|
|
1800
|
-
}
|
|
1801
|
-
function findSurface(setup, surfaceKey) {
|
|
1802
|
-
return setup.surfaces.find((s) => s.surfaceKey === surfaceKey);
|
|
1803
|
-
}
|
|
1804
|
-
function findSection(surface, sectionType) {
|
|
1805
|
-
return surface?.sections.find((s) => s.sectionType === sectionType);
|
|
1806
|
-
}
|
|
1807
|
-
function sectionSnippet(section) {
|
|
1808
|
-
if (!section) return null;
|
|
1809
|
-
for (const step of section.steps) {
|
|
1810
|
-
if (step.copyValue) return step.copyValue;
|
|
1811
|
-
if (step.codeSnippet) return step.codeSnippet;
|
|
1812
|
-
}
|
|
1813
|
-
return null;
|
|
1814
|
-
}
|
|
1815
|
-
function parseTagArtifactId(id) {
|
|
1816
|
-
if (!id.startsWith("tag:")) return null;
|
|
1817
|
-
const tags = id.slice("tag:".length).split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
1818
|
-
return tags.length > 0 ? tags : null;
|
|
1819
|
-
}
|
|
1820
|
-
async function getPersonalWorkspaceId(cfg) {
|
|
1821
|
-
const client = await makeClient(cfg);
|
|
1822
|
-
const { data } = await client.GET("/me/personal-workspace", {});
|
|
1823
|
-
return data?.workspaceId ?? null;
|
|
1824
|
-
}
|
|
1825
|
-
async function fetchMemoryFields(cfg, id) {
|
|
1826
|
-
const client = await makeClient(cfg);
|
|
1827
|
-
const { data } = await client.GET("/memories/{memoryId}", { params: { path: { memoryId: id } } });
|
|
1828
|
-
const env = data;
|
|
1829
|
-
const m = env?.item ?? env;
|
|
1830
|
-
if (!m) return null;
|
|
1831
|
-
const version = typeof m.currentVersion === "string" ? Number(m.currentVersion) : m.currentVersion;
|
|
1832
|
-
return { text: m.text, title: m.title, tags: m.tags, version: Number.isFinite(version) ? version : void 0 };
|
|
1833
|
-
}
|
|
1834
|
-
async function resolveInstruction(cfg, section, personalWorkspaceId) {
|
|
1835
|
-
const client = await makeClient(cfg);
|
|
1836
|
-
for (const artifact of section.artifacts) {
|
|
1837
|
-
const tags = parseTagArtifactId(artifact.id);
|
|
1838
|
-
if (!tags) continue;
|
|
1839
|
-
const { data } = await client.POST("/memories/search", {
|
|
1840
|
-
body: { query: null, textQuery: null, semanticQuery: null, hybrid: false, limit: 1, includeArchived: false, includeSystem: false, tags }
|
|
1841
|
-
});
|
|
1842
|
-
const hits = data ?? [];
|
|
1843
|
-
if (hits.length === 0) continue;
|
|
1844
|
-
const templateId = hits[0].id;
|
|
1845
|
-
const template = await fetchMemoryFields(cfg, templateId);
|
|
1846
|
-
if (typeof template?.text !== "string" || template.text.length === 0) continue;
|
|
1847
|
-
const templateTags = template.tags ?? tags;
|
|
1848
|
-
if (personalWorkspaceId) {
|
|
1849
|
-
const { data: ovr } = await client.POST("/memories/search", {
|
|
1850
|
-
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 } }
|
|
1851
|
-
});
|
|
1852
|
-
const ovrHits = ovr ?? [];
|
|
1853
|
-
if (ovrHits.length > 0) {
|
|
1854
|
-
const override = await fetchMemoryFields(cfg, ovrHits[0].id);
|
|
1855
|
-
if (typeof override?.text === "string" && override.text.length > 0) {
|
|
1856
|
-
return { title: override.title ?? template.title ?? artifact.title, body: override.text, source: "override", templateId, templateTags, sourceRef: `${ovrHits[0].id}@v${override.version ?? 1}` };
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
return { title: template.title ?? artifact.title, body: template.text, source: "template", templateId, templateTags, sourceRef: `${templateId}@v${template.version ?? 1}` };
|
|
1861
|
-
}
|
|
1862
|
-
return null;
|
|
1863
|
-
}
|
|
1864
|
-
async function resolveWorkspaceConventions(cfg, section) {
|
|
1865
|
-
const parts = [];
|
|
1866
|
-
const refs = [];
|
|
1867
|
-
for (const artifact of section.artifacts) {
|
|
1868
|
-
if (parseTagArtifactId(artifact.id)) continue;
|
|
1869
|
-
const mem = await fetchMemoryFields(cfg, artifact.id);
|
|
1870
|
-
if (typeof mem?.text === "string" && mem.text.trim().length > 0) {
|
|
1871
|
-
parts.push(mem.text.trim());
|
|
1872
|
-
refs.push(`${artifact.id}@v${mem.version ?? 1}`);
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
if (parts.length === 0) return null;
|
|
1876
|
-
return { body: parts.join("\n\n---\n\n"), refs };
|
|
1877
|
-
}
|
|
1878
|
-
async function createOverride(cfg, template, personalWorkspaceId) {
|
|
1879
|
-
const client = await makeClient(cfg);
|
|
1880
|
-
const overrideTags = template.templateTags.filter(
|
|
1881
|
-
(t) => t !== "sechroom:role:template" && !t.startsWith("sechroom:bundle:") && !t.startsWith("sechroom:template-ref:")
|
|
1882
|
-
);
|
|
1883
|
-
overrideTags.push("sechroom:role:override", `sechroom:template-ref:${template.templateId}`);
|
|
1884
|
-
const { error } = await client.POST("/memories", {
|
|
1885
|
-
body: {
|
|
1886
|
-
text: template.body,
|
|
1887
|
-
type: "reference",
|
|
1888
|
-
content: "{}",
|
|
1889
|
-
confidence: 1,
|
|
1890
|
-
source: "cli-agent-instructions-customize",
|
|
1891
|
-
archetype: "Document",
|
|
1892
|
-
title: template.title ?? null,
|
|
1893
|
-
tags: overrideTags,
|
|
1894
|
-
owner: { type: "Workspace", id: personalWorkspaceId }
|
|
1895
|
-
}
|
|
1896
|
-
});
|
|
1897
|
-
if (error) throw new Error(`creating personal copy failed: ${JSON.stringify(error)}`);
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
// 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";
|
|
1901
2375
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
1902
2376
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
1903
2377
|
function normalizeBody(s) {
|
|
@@ -1950,22 +2424,22 @@ function parseManagedBlock(content, block) {
|
|
|
1950
2424
|
return null;
|
|
1951
2425
|
}
|
|
1952
2426
|
function ensureDir2(path) {
|
|
1953
|
-
|
|
2427
|
+
mkdirSync4(dirname5(path), { recursive: true });
|
|
1954
2428
|
}
|
|
1955
2429
|
function readOr(path, fallback) {
|
|
1956
2430
|
try {
|
|
1957
|
-
return
|
|
2431
|
+
return readFileSync4(path, "utf8");
|
|
1958
2432
|
} catch {
|
|
1959
2433
|
return fallback;
|
|
1960
2434
|
}
|
|
1961
2435
|
}
|
|
1962
2436
|
function mergeMcpJson(path, snippet, dryRun) {
|
|
1963
2437
|
const incoming = JSON.parse(snippet);
|
|
1964
|
-
const existed =
|
|
2438
|
+
const existed = existsSync5(path);
|
|
1965
2439
|
let current = {};
|
|
1966
2440
|
if (existed) {
|
|
1967
2441
|
try {
|
|
1968
|
-
current = JSON.parse(
|
|
2442
|
+
current = JSON.parse(readFileSync4(path, "utf8"));
|
|
1969
2443
|
} catch {
|
|
1970
2444
|
return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
|
|
1971
2445
|
}
|
|
@@ -1973,26 +2447,26 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
1973
2447
|
current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
|
|
1974
2448
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
1975
2449
|
ensureDir2(path);
|
|
1976
|
-
|
|
2450
|
+
writeFileSync4(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
1977
2451
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
1978
2452
|
}
|
|
1979
2453
|
function mergeCodexToml(path, snippet, dryRun) {
|
|
1980
|
-
const existed =
|
|
2454
|
+
const existed = existsSync5(path);
|
|
1981
2455
|
let body = readOr(path, "");
|
|
1982
2456
|
body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
|
|
1983
2457
|
const trimmed = body.trim();
|
|
1984
2458
|
const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
|
|
1985
2459
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
1986
2460
|
ensureDir2(path);
|
|
1987
|
-
|
|
2461
|
+
writeFileSync4(path, next, { mode: 384 });
|
|
1988
2462
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
1989
2463
|
}
|
|
1990
2464
|
function writeInstructionBlock(path, write, dryRun) {
|
|
1991
|
-
const existed =
|
|
2465
|
+
const existed = existsSync5(path);
|
|
1992
2466
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
1993
2467
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
1994
2468
|
ensureDir2(path);
|
|
1995
|
-
|
|
2469
|
+
writeFileSync4(path, next);
|
|
1996
2470
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
1997
2471
|
}
|
|
1998
2472
|
function computeBlockFile(current, write) {
|
|
@@ -2033,7 +2507,7 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
2033
2507
|
const next = computeBlockFile(current, write);
|
|
2034
2508
|
if (!dryRun) {
|
|
2035
2509
|
ensureDir2(proposedPath);
|
|
2036
|
-
|
|
2510
|
+
writeFileSync4(proposedPath, next);
|
|
2037
2511
|
}
|
|
2038
2512
|
return {
|
|
2039
2513
|
kind: "instruction",
|
|
@@ -2103,105 +2577,84 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
2103
2577
|
return actions;
|
|
2104
2578
|
}
|
|
2105
2579
|
|
|
2106
|
-
// src/setup/clients.ts
|
|
2107
|
-
import { existsSync as existsSync3 } from "fs";
|
|
2108
|
-
import { homedir as homedir2 } from "os";
|
|
2109
|
-
import { dirname as dirname3, join as join2 } from "path";
|
|
2110
|
-
function claudeDesktopConfigPath(home) {
|
|
2111
|
-
switch (process.platform) {
|
|
2112
|
-
case "darwin":
|
|
2113
|
-
return join2(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
2114
|
-
case "win32":
|
|
2115
|
-
return join2(process.env.APPDATA ?? join2(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
2116
|
-
default:
|
|
2117
|
-
return join2(home, ".config", "Claude", "claude_desktop_config.json");
|
|
2118
|
-
}
|
|
2119
|
-
}
|
|
2120
|
-
function clientTargets(cwd) {
|
|
2121
|
-
const home = homedir2();
|
|
2122
|
-
return {
|
|
2123
|
-
"claude-code": {
|
|
2124
|
-
key: "claude-code",
|
|
2125
|
-
label: "Claude Code",
|
|
2126
|
-
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join2(cwd, ".mcp.json"), format: "json" },
|
|
2127
|
-
instruction: { surfaceKey: "claude-code", path: join2(cwd, "CLAUDE.md") }
|
|
2128
|
-
},
|
|
2129
|
-
"claude-desktop": {
|
|
2130
|
-
key: "claude-desktop",
|
|
2131
|
-
label: "Claude Desktop",
|
|
2132
|
-
mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
|
|
2133
|
-
instruction: { surfaceKey: "claude-desktop", path: join2(home, ".claude", "CLAUDE.md") }
|
|
2134
|
-
},
|
|
2135
|
-
codex: {
|
|
2136
|
-
key: "codex",
|
|
2137
|
-
label: "Codex CLI",
|
|
2138
|
-
mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join2(home, ".codex", "config.toml"), format: "toml" },
|
|
2139
|
-
instruction: { surfaceKey: "chatgpt", path: join2(cwd, "AGENTS.md") }
|
|
2140
|
-
},
|
|
2141
|
-
cursor: {
|
|
2142
|
-
key: "cursor",
|
|
2143
|
-
label: "Cursor",
|
|
2144
|
-
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join2(cwd, ".cursor", "mcp.json"), format: "json" },
|
|
2145
|
-
instruction: { surfaceKey: "chatgpt", path: join2(cwd, "AGENTS.md") }
|
|
2146
|
-
}
|
|
2147
|
-
};
|
|
2148
|
-
}
|
|
2149
|
-
var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor"];
|
|
2150
|
-
var DEFAULT_CLIENT_KEY = "claude-code";
|
|
2151
|
-
function detectInstalledClients(cwd) {
|
|
2152
|
-
const home = homedir2();
|
|
2153
|
-
const detected = [];
|
|
2154
|
-
if (existsSync3(join2(home, ".claude"))) detected.push("claude-code");
|
|
2155
|
-
if (existsSync3(dirname3(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
|
|
2156
|
-
if (existsSync3(join2(home, ".codex"))) detected.push("codex");
|
|
2157
|
-
if (existsSync3(join2(home, ".cursor")) || existsSync3(join2(cwd, ".cursor"))) detected.push("cursor");
|
|
2158
|
-
return detected;
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2161
2580
|
// src/setup/skills-offer.ts
|
|
2162
|
-
import { mkdirSync as
|
|
2163
|
-
import { homedir as
|
|
2164
|
-
import { join as
|
|
2581
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
2582
|
+
import { homedir as homedir4 } from "os";
|
|
2583
|
+
import { join as join5 } from "path";
|
|
2165
2584
|
|
|
2166
|
-
// src/
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2585
|
+
// src/setup/lane-pin.ts
|
|
2586
|
+
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
2587
|
+
"claude-code": "claude-code",
|
|
2588
|
+
"claude-desktop": "claude-code",
|
|
2589
|
+
cursor: "claude-code",
|
|
2590
|
+
codex: "codex"
|
|
2591
|
+
};
|
|
2592
|
+
var CLIENT_PRIORITY = ["claude-code", "claude-desktop", "cursor", "codex"];
|
|
2593
|
+
function handleFromDisplayName(name) {
|
|
2594
|
+
if (!name) return void 0;
|
|
2595
|
+
const localPart = name.trim().split("@")[0] ?? "";
|
|
2596
|
+
const first = localPart.split(/[\s._-]+/)[0]?.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2597
|
+
return first || void 0;
|
|
2598
|
+
}
|
|
2599
|
+
function codeLanePrefix(clients) {
|
|
2600
|
+
for (const c of CLIENT_PRIORITY) if (clients.includes(c)) return CODE_LANE_PREFIX_BY_CLIENT[c];
|
|
2601
|
+
return "claude-code";
|
|
2602
|
+
}
|
|
2603
|
+
function writePin(code, design) {
|
|
2604
|
+
const values = {};
|
|
2605
|
+
if (code) values["code-lane"] = code;
|
|
2606
|
+
if (design) values["design-lane"] = design;
|
|
2607
|
+
if (Object.keys(values).length === 0) return;
|
|
2608
|
+
const target = writeSem(values);
|
|
2609
|
+
process.stderr.write(`${ok("\u2713")} lane pin written \u2192 ${target} ${style.dim("(./.sechroom/lane.json, git-ignored)")}
|
|
2610
|
+
`);
|
|
2172
2611
|
}
|
|
2173
|
-
function
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2612
|
+
async function ensureLanePin(cfg, opts) {
|
|
2613
|
+
if (opts.dryRun) return;
|
|
2614
|
+
if (readSem()) return;
|
|
2615
|
+
let wf;
|
|
2616
|
+
let profile;
|
|
2617
|
+
try {
|
|
2618
|
+
const client = await makeClient(cfg);
|
|
2619
|
+
[wf, profile] = await Promise.all([
|
|
2620
|
+
client.GET("/me/workflow-preferences", {}).then((r) => r.data).catch(() => void 0),
|
|
2621
|
+
client.GET("/me/profile", {}).then((r) => r.data).catch(() => void 0)
|
|
2622
|
+
]);
|
|
2623
|
+
} catch {
|
|
2181
2624
|
}
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
const
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
if (
|
|
2188
|
-
|
|
2189
|
-
if (eq === -1) continue;
|
|
2190
|
-
const key = line.slice(0, eq).trim();
|
|
2191
|
-
const value = line.slice(eq + 1).trim();
|
|
2192
|
-
if (key) out[key] = value;
|
|
2625
|
+
const handle = handleFromDisplayName(profile?.effectiveDisplayName);
|
|
2626
|
+
const prefix = codeLanePrefix(opts.clients ?? ["claude-code"]);
|
|
2627
|
+
const codeGuess = wf?.defaultCodeLane ?? (handle ? `${prefix}-${handle}` : void 0);
|
|
2628
|
+
const designGuess = wf?.defaultDesignLane ?? (handle ? `claude-design-${handle}` : void 0);
|
|
2629
|
+
if (!canPrompt() || opts.yes) {
|
|
2630
|
+
if (opts.yes && (codeGuess || designGuess)) writePin(codeGuess, designGuess);
|
|
2631
|
+
return;
|
|
2193
2632
|
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2633
|
+
if (codeGuess || designGuess) {
|
|
2634
|
+
process.stderr.write(
|
|
2635
|
+
`
|
|
2636
|
+
I can pin this checkout's lane so operator skills + the continuity hook resolve your identity:
|
|
2637
|
+
`
|
|
2638
|
+
);
|
|
2639
|
+
if (codeGuess) process.stderr.write(` ${style.dim("code-lane")} = ${style.cyan(codeGuess)}
|
|
2640
|
+
`);
|
|
2641
|
+
if (designGuess) process.stderr.write(` ${style.dim("design-lane")} = ${style.cyan(designGuess)}
|
|
2642
|
+
`);
|
|
2643
|
+
if (await promptYesNo("Pin these?")) {
|
|
2644
|
+
writePin(codeGuess, designGuess);
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
const code = await promptText("Code-lane id (e.g. claude-code-you, blank to skip)?", codeGuess ?? "");
|
|
2649
|
+
const design = await promptText("Design-lane id (e.g. claude-design-you, blank to skip)?", designGuess ?? "");
|
|
2650
|
+
if (!code && !design) {
|
|
2651
|
+
process.stderr.write(
|
|
2652
|
+
` ${style.dim("skipped \u2014 set later with")} ${style.cyan("sechroom skills set-lane --code-lane \u2026 --design-lane \u2026")}
|
|
2653
|
+
`
|
|
2654
|
+
);
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
writePin(code || void 0, design || void 0);
|
|
2205
2658
|
}
|
|
2206
2659
|
|
|
2207
2660
|
// src/setup/skills-offer.ts
|
|
@@ -2242,43 +2695,19 @@ async function maybeOfferSkills(cfg, personalWorkspaceId, opts) {
|
|
|
2242
2695
|
Found ${style.bold(String(names.length))} operator skill(s) installed in your workspace: ${names.join(", ")}.
|
|
2243
2696
|
`
|
|
2244
2697
|
);
|
|
2245
|
-
const dir =
|
|
2698
|
+
const dir = join5(homedir4(), ".claude", "skills");
|
|
2246
2699
|
const materialise = opts.yes ? true : canPrompt() ? await promptYesNo(`Write them to ${dir}/ so ${surface} can use them?`) : false;
|
|
2247
2700
|
if (!materialise) return;
|
|
2248
2701
|
const written = [];
|
|
2249
2702
|
for (const [name, m] of byName) {
|
|
2250
2703
|
const body = m.text ?? m.Text ?? "";
|
|
2251
|
-
|
|
2252
|
-
|
|
2704
|
+
mkdirSync5(join5(dir, name), { recursive: true });
|
|
2705
|
+
writeFileSync5(join5(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
|
|
2253
2706
|
written.push(name);
|
|
2254
2707
|
}
|
|
2255
2708
|
process.stderr.write(`${style.green("\u2713")} wrote ${written.length} skill(s) to ${dir}
|
|
2256
2709
|
`);
|
|
2257
|
-
|
|
2258
|
-
const setLane = opts.yes ? false : canPrompt() ? await promptYesNo("Set your lane now so the skills can resolve their identity slots?") : false;
|
|
2259
|
-
if (!setLane) {
|
|
2260
|
-
process.stderr.write(
|
|
2261
|
-
` ${style.dim("run")} ${style.cyan("sechroom skills set-lane --code-lane \u2026 --design-lane \u2026")} ${style.dim("when ready.")}
|
|
2262
|
-
`
|
|
2263
|
-
);
|
|
2264
|
-
return;
|
|
2265
|
-
}
|
|
2266
|
-
let wf;
|
|
2267
|
-
try {
|
|
2268
|
-
const client = await makeClient(cfg);
|
|
2269
|
-
wf = await client.GET("/me/workflow-preferences", {}).then((r) => r.data).catch(() => void 0);
|
|
2270
|
-
} catch {
|
|
2271
|
-
}
|
|
2272
|
-
const code = await promptText("Code-lane id (e.g. claude-code-you)?", wf?.defaultCodeLane);
|
|
2273
|
-
const design = await promptText("Design-lane id (e.g. claude-design-you)?", wf?.defaultDesignLane);
|
|
2274
|
-
const values = {};
|
|
2275
|
-
if (code) values["code-lane"] = code;
|
|
2276
|
-
if (design) values["design-lane"] = design;
|
|
2277
|
-
if (Object.keys(values).length === 0) return;
|
|
2278
|
-
const target = localSemPath();
|
|
2279
|
-
writeFileSync3(target, serializeSem(values));
|
|
2280
|
-
process.stderr.write(`${style.green("\u2713")} lane pin written \u2192 ${target}
|
|
2281
|
-
`);
|
|
2710
|
+
await ensureLanePin(cfg, { yes: opts.yes, dryRun: opts.dryRun, clients: [surface] });
|
|
2282
2711
|
}
|
|
2283
2712
|
|
|
2284
2713
|
// src/commands/setup.ts
|
|
@@ -2603,7 +3032,7 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
2603
3032
|
"Where should this tenant + base URL be saved?",
|
|
2604
3033
|
[
|
|
2605
3034
|
{ label: "Globally", value: "global", hint: "all projects on this machine" },
|
|
2606
|
-
{ label: "This directory", value: "local", hint: ".sechroom.json \u2014 project + subdirs" }
|
|
3035
|
+
{ label: "This directory", value: "local", hint: ".sechroom/config.json \u2014 project + subdirs" }
|
|
2607
3036
|
],
|
|
2608
3037
|
local.path ? "local" : "global"
|
|
2609
3038
|
) === "local";
|
|
@@ -2679,14 +3108,14 @@ async function chooseClients(clientFlag, yes, cwd) {
|
|
|
2679
3108
|
return picks.length > 0 ? picks : preselected;
|
|
2680
3109
|
}
|
|
2681
3110
|
function registerOnboard(program2) {
|
|
2682
|
-
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(
|
|
2683
3112
|
"after",
|
|
2684
3113
|
`
|
|
2685
3114
|
Examples:
|
|
2686
3115
|
$ sechroom onboard guided, interactive (asks where to save config + how to wire)
|
|
2687
3116
|
$ sechroom onboard --cli-only just the CLI \u2014 no .mcp.json, no agent files
|
|
2688
3117
|
$ sechroom onboard --no-mcp agent instructions only, skip MCP config
|
|
2689
|
-
$ sechroom onboard --local save tenant + base URL to ./.sechroom.json
|
|
3118
|
+
$ sechroom onboard --local save tenant + base URL to ./.sechroom/config.json
|
|
2690
3119
|
$ sechroom onboard --workspace wsp_XX bind this directory to a workspace (no pick prompt)
|
|
2691
3120
|
$ sechroom onboard --refresh refresh out-of-date instruction blocks in place
|
|
2692
3121
|
$ sechroom onboard --check CI/pre-commit: nonzero exit if instructions are out of date
|
|
@@ -2716,12 +3145,14 @@ Examples:
|
|
|
2716
3145
|
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, clients: [] }, true);
|
|
2717
3146
|
return;
|
|
2718
3147
|
}
|
|
3148
|
+
if (!dryRun) await ensureLanePin(cfg, { yes, dryRun, clients: detectInstalledClients(process.cwd()) });
|
|
2719
3149
|
process.stdout.write(
|
|
2720
3150
|
`
|
|
2721
3151
|
${style.bold("Done.")} The CLI is configured for ${style.cyan(cfg.tenant)} \u2014 no AI-client files written.
|
|
2722
3152
|
Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom --help")}
|
|
2723
3153
|
`
|
|
2724
3154
|
);
|
|
3155
|
+
await printStarterPrompt("cli");
|
|
2725
3156
|
return;
|
|
2726
3157
|
}
|
|
2727
3158
|
const keys = await chooseClients(opts.client, yes, process.cwd());
|
|
@@ -2766,6 +3197,9 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
2766
3197
|
}
|
|
2767
3198
|
process.exit(wouldChange === 0 ? 0 : 1);
|
|
2768
3199
|
}
|
|
3200
|
+
if (!json && !dryRun) {
|
|
3201
|
+
await ensureLanePin(cfg, { yes, dryRun, clients: keys });
|
|
3202
|
+
}
|
|
2769
3203
|
if (!json && !dryRun) {
|
|
2770
3204
|
await maybeOfferSkills(cfg, personalWorkspaceId, { yes, dryRun, surface: "claude-code" });
|
|
2771
3205
|
}
|
|
@@ -2794,6 +3228,7 @@ ${style.bold("Done.")} Restart your AI client (or reload MCP) to pick up the new
|
|
|
2794
3228
|
${style.bold("Done.")} Agent instructions written (no MCP config).
|
|
2795
3229
|
`
|
|
2796
3230
|
);
|
|
3231
|
+
if (!dryRun) await printStarterPrompt("agent", cfg);
|
|
2797
3232
|
});
|
|
2798
3233
|
}
|
|
2799
3234
|
async function chooseWire(opts, yes) {
|
|
@@ -2811,16 +3246,43 @@ async function chooseWire(opts, yes) {
|
|
|
2811
3246
|
}
|
|
2812
3247
|
return opts.mcp === false ? "agent-only" : "full";
|
|
2813
3248
|
}
|
|
3249
|
+
var FALLBACK_AGENT_PROMPT = "Resume my sechroom continuity, summarise what I was last working on, then suggest the next step.";
|
|
3250
|
+
async function printStarterPrompt(mode, cfg) {
|
|
3251
|
+
if (mode === "cli") {
|
|
3252
|
+
process.stdout.write(
|
|
3253
|
+
`
|
|
3254
|
+
${style.bold("Next:")} pick up where you left off \u2014
|
|
3255
|
+
${style.cyan("sechroom continuity resume-me")}
|
|
3256
|
+
`
|
|
3257
|
+
);
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
let primary = FALLBACK_AGENT_PROMPT;
|
|
3261
|
+
if (cfg) {
|
|
3262
|
+
try {
|
|
3263
|
+
const client = await makeClient(cfg);
|
|
3264
|
+
const { data } = await client.GET("/me/onboarding/starter-prompt", {});
|
|
3265
|
+
if (data?.primary) primary = data.primary;
|
|
3266
|
+
} catch {
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
process.stdout.write(
|
|
3270
|
+
`
|
|
3271
|
+
${style.bold("Next:")} paste this into your AI agent to get going \u2014
|
|
3272
|
+
${style.cyan(`"${primary}"`)}
|
|
3273
|
+
`
|
|
3274
|
+
);
|
|
3275
|
+
}
|
|
2814
3276
|
|
|
2815
3277
|
// src/commands/skills.ts
|
|
2816
|
-
import { homedir as
|
|
2817
|
-
import {
|
|
2818
|
-
import { mkdirSync as
|
|
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";
|
|
2819
3281
|
var DEFAULT_SLUG = "operator-skills";
|
|
2820
3282
|
var ROLE_TAGS = ["sechroom:role:skill-template", "role:skill-template"];
|
|
2821
3283
|
var LOCK = ".sechroom-skills.json";
|
|
2822
3284
|
function skillsDir(global) {
|
|
2823
|
-
return global ?
|
|
3285
|
+
return global ? join6(homedir5(), ".claude", "skills") : join6(process.cwd(), ".claude", "skills");
|
|
2824
3286
|
}
|
|
2825
3287
|
function tagValue2(tags, prefix) {
|
|
2826
3288
|
return (tags ?? []).find((t) => t.startsWith(prefix))?.slice(prefix.length);
|
|
@@ -2898,15 +3360,15 @@ Examples:
|
|
|
2898
3360
|
const name = tagValue2(tags, "skill:");
|
|
2899
3361
|
if (!name) continue;
|
|
2900
3362
|
const body = m.text ?? m.Text ?? "";
|
|
2901
|
-
|
|
2902
|
-
|
|
3363
|
+
mkdirSync6(join6(dir, name), { recursive: true });
|
|
3364
|
+
writeFileSync6(join6(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
|
|
2903
3365
|
written.push(name);
|
|
2904
3366
|
}
|
|
2905
|
-
|
|
2906
|
-
const lockPath =
|
|
2907
|
-
const lock =
|
|
3367
|
+
mkdirSync6(dir, { recursive: true });
|
|
3368
|
+
const lockPath = join6(dir, LOCK);
|
|
3369
|
+
const lock = existsSync6(lockPath) ? JSON.parse(readFileSync5(lockPath, "utf8")) : {};
|
|
2908
3370
|
lock[slug] = { surface: opts.surface, version, instance: wantInstance, skills: written.sort() };
|
|
2909
|
-
|
|
3371
|
+
writeFileSync6(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
2910
3372
|
if (opts.json) return emit({ slug, version, instance: wantInstance, surface: opts.surface, dir, installed: written }, true);
|
|
2911
3373
|
const instanceNote = opts.instance ? ` (${opts.instance})` : "";
|
|
2912
3374
|
console.log(style.green(`Installed ${slug}@${version}${instanceNote} \u2014 ${written.length} skill(s) \u2192 ${dir}`));
|
|
@@ -2928,42 +3390,41 @@ Examples:
|
|
|
2928
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) => {
|
|
2929
3391
|
const slug = slugArg || DEFAULT_SLUG;
|
|
2930
3392
|
const dir = skillsDir(!opts.local);
|
|
2931
|
-
const lockPath =
|
|
2932
|
-
if (!
|
|
2933
|
-
const lock = JSON.parse(
|
|
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"));
|
|
2934
3396
|
const entry = lock[slug];
|
|
2935
3397
|
if (!entry) fail(`No installed record for '${slug}' in ${lockPath}.`);
|
|
2936
3398
|
const removed = [];
|
|
2937
3399
|
for (const name of entry.skills) {
|
|
2938
|
-
const skillPath =
|
|
2939
|
-
if (
|
|
3400
|
+
const skillPath = join6(dir, name);
|
|
3401
|
+
if (existsSync6(skillPath)) {
|
|
2940
3402
|
rmSync2(skillPath, { recursive: true, force: true });
|
|
2941
3403
|
removed.push(name);
|
|
2942
3404
|
}
|
|
2943
3405
|
}
|
|
2944
3406
|
delete lock[slug];
|
|
2945
|
-
|
|
3407
|
+
writeFileSync6(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
2946
3408
|
if (opts.json) return emit({ slug, removed, dir }, true);
|
|
2947
3409
|
console.log(style.green(`Removed ${removed.length} skill(s) for ${slug} from ${dir}`));
|
|
2948
3410
|
});
|
|
2949
|
-
skills.command("set-lane").description("Write this checkout's lane pin to a local ./.
|
|
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) => {
|
|
2950
3412
|
if (!opts.codeLane && !opts.designLane) fail("Provide --code-lane and/or --design-lane.");
|
|
2951
3413
|
const target = localSemPath();
|
|
2952
|
-
const values =
|
|
3414
|
+
const values = readLocalSemValues();
|
|
2953
3415
|
if (opts.codeLane) values["code-lane"] = opts.codeLane;
|
|
2954
3416
|
if (opts.designLane) values["design-lane"] = opts.designLane;
|
|
2955
|
-
|
|
2956
|
-
writeFileSync4(target, serializeSem(values));
|
|
3417
|
+
writeSem(values, target);
|
|
2957
3418
|
if (cmd.optsWithGlobals().json) return emit({ path: target, values }, true);
|
|
2958
|
-
console.log(style.green(`Wrote lane pin \u2192 ${target}`));
|
|
3419
|
+
console.log(style.green(`Wrote lane pin \u2192 ${target} ${style.dim("(git-ignored)")}`));
|
|
2959
3420
|
Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
|
|
2960
3421
|
});
|
|
2961
|
-
skills.command("lane").description("Show the lane pin resolved from ./.
|
|
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) => {
|
|
2962
3423
|
const json = cmd.optsWithGlobals().json;
|
|
2963
3424
|
const found = readSem();
|
|
2964
3425
|
if (!found) {
|
|
2965
3426
|
if (json) return emit({ path: null, values: {} }, true);
|
|
2966
|
-
return console.log(style.dim(`No ./.
|
|
3427
|
+
return console.log(style.dim(`No ./.sechroom/lane.json pin in this checkout. Run 'sechroom skills set-lane'.`));
|
|
2967
3428
|
}
|
|
2968
3429
|
if (json) return emit(found, true);
|
|
2969
3430
|
console.log(style.dim(`from ${found.path}`));
|
|
@@ -3032,22 +3493,22 @@ Examples:
|
|
|
3032
3493
|
}
|
|
3033
3494
|
|
|
3034
3495
|
// src/commands/reset.ts
|
|
3035
|
-
import { homedir as
|
|
3036
|
-
import { join as
|
|
3037
|
-
import { existsSync as
|
|
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";
|
|
3038
3499
|
var SKILLS_LOCK = ".sechroom-skills.json";
|
|
3039
|
-
var localSkillsDir = () =>
|
|
3040
|
-
var globalSkillsDir = () =>
|
|
3500
|
+
var localSkillsDir = () => join7(process.cwd(), ".claude", "skills");
|
|
3501
|
+
var globalSkillsDir = () => join7(homedir6(), ".claude", "skills");
|
|
3041
3502
|
function removeMaterialisedSkills(dir) {
|
|
3042
3503
|
const removed = [];
|
|
3043
|
-
const lockPath =
|
|
3044
|
-
if (!
|
|
3504
|
+
const lockPath = join7(dir, SKILLS_LOCK);
|
|
3505
|
+
if (!existsSync7(lockPath)) return removed;
|
|
3045
3506
|
try {
|
|
3046
|
-
const lock = JSON.parse(
|
|
3507
|
+
const lock = JSON.parse(readFileSync6(lockPath, "utf8"));
|
|
3047
3508
|
for (const entry of Object.values(lock)) {
|
|
3048
3509
|
for (const name of entry.skills ?? []) {
|
|
3049
|
-
const p =
|
|
3050
|
-
if (
|
|
3510
|
+
const p = join7(dir, name);
|
|
3511
|
+
if (existsSync7(p)) {
|
|
3051
3512
|
rmSync3(p, { recursive: true, force: true });
|
|
3052
3513
|
removed.push(p);
|
|
3053
3514
|
}
|
|
@@ -3067,26 +3528,31 @@ function registerReset(program2) {
|
|
|
3067
3528
|
removed ? style.green("Signed out \u2014 auth token removed.") : style.dim("Already signed out (no token).")
|
|
3068
3529
|
);
|
|
3069
3530
|
});
|
|
3070
|
-
program2.command("reset").description("Reset LOCAL CLI state for this directory (./.sechroom.json
|
|
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) => {
|
|
3071
3532
|
const json = cmd.optsWithGlobals().json;
|
|
3072
3533
|
const global = Boolean(opts.global);
|
|
3073
3534
|
if (!opts.yes && canPrompt()) {
|
|
3074
|
-
const scope = global ? "this directory's local state AND your global auth token + config + ~/.claude/skills" : "this directory's local state (./.sechroom.json
|
|
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)";
|
|
3075
3536
|
if (!await promptYesNo(`Remove ${scope}?`)) {
|
|
3076
3537
|
if (!json) console.log(style.dim("Cancelled."));
|
|
3077
3538
|
return;
|
|
3078
3539
|
}
|
|
3079
3540
|
}
|
|
3080
3541
|
const removed = [];
|
|
3081
|
-
const
|
|
3082
|
-
if (
|
|
3083
|
-
rmSync3(
|
|
3084
|
-
removed.push(
|
|
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);
|
|
3085
3551
|
}
|
|
3086
|
-
const
|
|
3087
|
-
if (
|
|
3088
|
-
rmSync3(
|
|
3089
|
-
removed.push(
|
|
3552
|
+
const legacySem = join7(process.cwd(), ".sem");
|
|
3553
|
+
if (existsSync7(legacySem)) {
|
|
3554
|
+
rmSync3(legacySem, { force: true });
|
|
3555
|
+
removed.push(legacySem);
|
|
3090
3556
|
}
|
|
3091
3557
|
removed.push(...removeMaterialisedSkills(localSkillsDir()));
|
|
3092
3558
|
if (global) {
|
|
@@ -3111,7 +3577,7 @@ function registerReset(program2) {
|
|
|
3111
3577
|
function resolveVersion() {
|
|
3112
3578
|
try {
|
|
3113
3579
|
const pkg = JSON.parse(
|
|
3114
|
-
|
|
3580
|
+
readFileSync7(new URL("../package.json", import.meta.url), "utf8")
|
|
3115
3581
|
);
|
|
3116
3582
|
return pkg.version ?? "0.0.0";
|
|
3117
3583
|
} catch {
|
|
@@ -3127,7 +3593,7 @@ Examples:
|
|
|
3127
3593
|
$ sechroom onboard guided first-run: configure, sign in, wire this project
|
|
3128
3594
|
$ sechroom login sign in via browser (OAuth + PKCE)
|
|
3129
3595
|
$ sechroom config set tenant ocd set your tenant (global)
|
|
3130
|
-
$ 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)
|
|
3131
3597
|
$ sechroom config show resolved config + which source won
|
|
3132
3598
|
|
|
3133
3599
|
$ sechroom memory create --text "a note" --title "Note" --tag idea
|
|
@@ -3139,7 +3605,7 @@ Examples:
|
|
|
3139
3605
|
$ sechroom --json memory search "auth" compact JSON for scripts and agents
|
|
3140
3606
|
$ SECHROOM_TOKEN=<bearer> sechroom --json memory get mem_XXXX headless
|
|
3141
3607
|
|
|
3142
|
-
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.
|
|
3143
3609
|
Run 'sechroom <command> --help' for command-specific examples.`
|
|
3144
3610
|
);
|
|
3145
3611
|
program.hook("preAction", (_thisCmd, actionCmd) => {
|
|
@@ -3165,11 +3631,11 @@ config.addHelpText(
|
|
|
3165
3631
|
Examples:
|
|
3166
3632
|
$ sechroom config set baseUrl https://app.sechroom.ai/api prod (staging: https://staging.app.sechroom.ai/api)
|
|
3167
3633
|
$ sechroom config set tenant ocd
|
|
3168
|
-
$ 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)
|
|
3169
3635
|
$ sechroom config set clientId dyn-XXXX global-only escape hatch (no DCR endpoint)
|
|
3170
3636
|
$ sechroom config show --json`
|
|
3171
3637
|
);
|
|
3172
|
-
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) => {
|
|
3173
3639
|
if (opts.local) {
|
|
3174
3640
|
if (!["baseUrl", "tenant", "workspaceId", "defaultProjectId"].includes(key)) {
|
|
3175
3641
|
process.stderr.write(`--local supports only: baseUrl | tenant | workspaceId | defaultProjectId (clientId is global)
|
|
@@ -3221,6 +3687,7 @@ registerWorkspace(program);
|
|
|
3221
3687
|
registerProject(program);
|
|
3222
3688
|
registerFiling(program);
|
|
3223
3689
|
registerContinuity(program);
|
|
3690
|
+
registerHook(program);
|
|
3224
3691
|
registerId(program);
|
|
3225
3692
|
registerAccount(program);
|
|
3226
3693
|
registerChat(program);
|