@sechroom/cli 2026.6.12 → 2026.6.13-rc.651c5f10
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 +1691 -460
- 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 readFileSync8 } from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/auth.ts
|
|
@@ -16,8 +16,12 @@ 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 BASELINE_CONFIG_NAME = ".sechroom.json";
|
|
21
|
+
var OVERRIDE_CONFIG_NAME = join(STATE_DIR_NAME, "config.json");
|
|
22
|
+
var BINDING_FIELDS = ["schemaVersion", "baseUrl", "tenant", "workspaceId", "defaultProjectId"];
|
|
20
23
|
var DEFAULT_BASE_URL = "https://app.sechroom.ai/api";
|
|
24
|
+
var LOCAL_CONFIG_SCHEMA_VERSION = 2;
|
|
21
25
|
function ensureDir() {
|
|
22
26
|
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
23
27
|
}
|
|
@@ -63,35 +67,55 @@ function clearPersisted() {
|
|
|
63
67
|
rmSync(CONFIG_FILE);
|
|
64
68
|
return CONFIG_FILE;
|
|
65
69
|
}
|
|
66
|
-
function
|
|
70
|
+
function readJsonConfig(path) {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
73
|
+
} catch {
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function findConfigHome(start = process.cwd()) {
|
|
67
78
|
let dir = start;
|
|
68
79
|
for (; ; ) {
|
|
69
|
-
|
|
70
|
-
if (existsSync(candidate)) return candidate;
|
|
80
|
+
if (existsSync(join(dir, BASELINE_CONFIG_NAME)) || existsSync(join(dir, OVERRIDE_CONFIG_NAME))) return dir;
|
|
71
81
|
const parent = dirname(dir);
|
|
72
82
|
if (parent === dir) return void 0;
|
|
73
83
|
dir = parent;
|
|
74
84
|
}
|
|
75
85
|
}
|
|
76
86
|
function readLocalConfig() {
|
|
77
|
-
const
|
|
78
|
-
if (!
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
const home = findConfigHome();
|
|
88
|
+
if (!home) return {};
|
|
89
|
+
const baselinePath = join(home, BASELINE_CONFIG_NAME);
|
|
90
|
+
const overridePath = join(home, OVERRIDE_CONFIG_NAME);
|
|
91
|
+
const merged = { ...readJsonConfig(baselinePath) ?? {}, ...readJsonConfig(overridePath) ?? {} };
|
|
92
|
+
return {
|
|
93
|
+
schemaVersion: merged.schemaVersion,
|
|
94
|
+
baseUrl: merged.baseUrl,
|
|
95
|
+
tenant: merged.tenant,
|
|
96
|
+
workspaceId: merged.workspaceId,
|
|
97
|
+
defaultProjectId: merged.defaultProjectId,
|
|
98
|
+
path: existsSync(baselinePath) ? baselinePath : overridePath
|
|
99
|
+
};
|
|
85
100
|
}
|
|
86
101
|
function writeLocalConfig(patch) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
const home = findConfigHome() ?? process.cwd();
|
|
103
|
+
const baselinePath = join(home, BASELINE_CONFIG_NAME);
|
|
104
|
+
const overridePath = join(home, OVERRIDE_CONFIG_NAME);
|
|
105
|
+
const current = readJsonConfig(baselinePath) ?? {};
|
|
106
|
+
const next = { ...current, ...patch, schemaVersion: LOCAL_CONFIG_SCHEMA_VERSION };
|
|
107
|
+
writeFileSync(baselinePath, JSON.stringify(next, null, 2), { mode: 420 });
|
|
108
|
+
const override = readJsonConfig(overridePath);
|
|
109
|
+
if (override) {
|
|
110
|
+
for (const f of BINDING_FIELDS) delete override[f];
|
|
111
|
+
if (Object.keys(override).length === 0) rmSync(overridePath, { force: true });
|
|
112
|
+
else writeFileSync(overridePath, JSON.stringify(override, null, 2), { mode: 384 });
|
|
92
113
|
}
|
|
93
|
-
|
|
94
|
-
|
|
114
|
+
return baselinePath;
|
|
115
|
+
}
|
|
116
|
+
function committedBindingPath(dir) {
|
|
117
|
+
const p = join(dir, BASELINE_CONFIG_NAME);
|
|
118
|
+
return existsSync(p) ? p : void 0;
|
|
95
119
|
}
|
|
96
120
|
function resolveConfig(flags) {
|
|
97
121
|
const local = readLocalConfig();
|
|
@@ -103,7 +127,9 @@ function resolveConfig(flags) {
|
|
|
103
127
|
"No tenant set. The Sechroom API rejects untenanted requests (HTTP 400). Pass --tenant <id>, set SECHROOM_TENANT, run `sechroom config set tenant <id>`, or `sechroom config set --local tenant <id>` for this directory."
|
|
104
128
|
);
|
|
105
129
|
}
|
|
106
|
-
|
|
130
|
+
const workspaceId = process.env.SECHROOM_WORKSPACE ?? local.workspaceId ?? persisted.workspaceId ?? void 0;
|
|
131
|
+
const defaultProjectId = local.defaultProjectId ?? persisted.defaultProjectId ?? void 0;
|
|
132
|
+
return { baseUrl: baseUrl.replace(/\/$/, ""), tenant, workspaceId, defaultProjectId, clientId: persisted.clientId };
|
|
107
133
|
}
|
|
108
134
|
function describeConfig(flags) {
|
|
109
135
|
const local = readLocalConfig();
|
|
@@ -121,6 +147,7 @@ function describeConfig(flags) {
|
|
|
121
147
|
return {
|
|
122
148
|
baseUrl: { value: baseUrl.value, source: baseUrl.source },
|
|
123
149
|
tenant: pick(flags.tenant, process.env.SECHROOM_TENANT, local.tenant, g.tenant),
|
|
150
|
+
workspaceId: pick(void 0, process.env.SECHROOM_WORKSPACE, local.workspaceId, g.workspaceId),
|
|
124
151
|
localPath: local.path
|
|
125
152
|
};
|
|
126
153
|
}
|
|
@@ -302,6 +329,7 @@ var style = {
|
|
|
302
329
|
cyan: wrap(36, 39)
|
|
303
330
|
};
|
|
304
331
|
var ok = (s) => style.green(s);
|
|
332
|
+
var warn = (s) => style.yellow(s);
|
|
305
333
|
var err = (s) => style.red(s);
|
|
306
334
|
function active() {
|
|
307
335
|
return !quiet && Boolean(process.stderr.isTTY);
|
|
@@ -347,8 +375,8 @@ async function promptYesNo(question) {
|
|
|
347
375
|
const { createInterface } = await import("readline");
|
|
348
376
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
349
377
|
try {
|
|
350
|
-
const answer = await new Promise((
|
|
351
|
-
rl.question(`${question} [y/N] `,
|
|
378
|
+
const answer = await new Promise((resolve3) => {
|
|
379
|
+
rl.question(`${question} [y/N] `, resolve3);
|
|
352
380
|
});
|
|
353
381
|
return /^y(es)?$/i.test(answer.trim());
|
|
354
382
|
} finally {
|
|
@@ -361,8 +389,8 @@ async function promptText(question, def) {
|
|
|
361
389
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
362
390
|
try {
|
|
363
391
|
const suffix = def ? ` [${def}]` : "";
|
|
364
|
-
const answer = await new Promise((
|
|
365
|
-
rl.question(`${question}${suffix} `,
|
|
392
|
+
const answer = await new Promise((resolve3) => {
|
|
393
|
+
rl.question(`${question}${suffix} `, resolve3);
|
|
366
394
|
});
|
|
367
395
|
const trimmed = answer.trim();
|
|
368
396
|
return trimmed.length > 0 ? trimmed : def ?? "";
|
|
@@ -388,8 +416,8 @@ async function promptSelect(question, choices, def) {
|
|
|
388
416
|
process.stderr.write(` ${marker} ${style.bold(String(i + 1))}. ${c.label}${hint}
|
|
389
417
|
`);
|
|
390
418
|
});
|
|
391
|
-
const answer = await new Promise((
|
|
392
|
-
rl.question(`Choose ${style.dim(`[${defIdx + 1}]`)} `,
|
|
419
|
+
const answer = await new Promise((resolve3) => {
|
|
420
|
+
rl.question(`Choose ${style.dim(`[${defIdx + 1}]`)} `, resolve3);
|
|
393
421
|
});
|
|
394
422
|
const trimmed = answer.trim();
|
|
395
423
|
if (!trimmed) return choices[defIdx].value;
|
|
@@ -421,8 +449,8 @@ async function promptMultiSelect(question, choices, preselected = []) {
|
|
|
421
449
|
process.stderr.write(` ${box} ${style.bold(String(i + 1))}. ${c.label}${hint}
|
|
422
450
|
`);
|
|
423
451
|
});
|
|
424
|
-
const answer = await new Promise((
|
|
425
|
-
rl.question(`Select ${style.dim("[Enter = \u25C9]")} `,
|
|
452
|
+
const answer = await new Promise((resolve3) => {
|
|
453
|
+
rl.question(`Select ${style.dim("[Enter = \u25C9]")} `, resolve3);
|
|
426
454
|
});
|
|
427
455
|
const trimmed = answer.trim().toLowerCase();
|
|
428
456
|
if (!trimmed) return preValues();
|
|
@@ -1545,106 +1573,729 @@ Examples:
|
|
|
1545
1573
|
});
|
|
1546
1574
|
}
|
|
1547
1575
|
|
|
1548
|
-
// src/commands/
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1576
|
+
// src/commands/hook.ts
|
|
1577
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1578
|
+
import { homedir as homedir3 } from "os";
|
|
1579
|
+
import { delimiter, dirname as dirname4, join as join4 } from "path";
|
|
1580
|
+
|
|
1581
|
+
// src/sem.ts
|
|
1582
|
+
import { basename as basename2, dirname as dirname2, join as join2 } from "path";
|
|
1583
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1584
|
+
var SEM_FILE = join2(".sechroom", "lane.json");
|
|
1585
|
+
var LEGACY_SEM_FILE = ".sem";
|
|
1586
|
+
var STATE_DIR_NAME2 = ".sechroom";
|
|
1587
|
+
function localSemPath(cwd = process.cwd()) {
|
|
1588
|
+
return join2(cwd, SEM_FILE);
|
|
1589
|
+
}
|
|
1590
|
+
function resolveSemPathForRead(start = process.cwd()) {
|
|
1591
|
+
let dir = start;
|
|
1592
|
+
while (true) {
|
|
1593
|
+
const candidate = join2(dir, SEM_FILE);
|
|
1594
|
+
if (existsSync2(candidate)) return candidate;
|
|
1595
|
+
const legacy = join2(dir, LEGACY_SEM_FILE);
|
|
1596
|
+
if (existsSync2(legacy)) return legacy;
|
|
1597
|
+
const parent = dirname2(dir);
|
|
1598
|
+
if (parent === dir) return void 0;
|
|
1599
|
+
dir = parent;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
function parseSem(text) {
|
|
1603
|
+
const out = {};
|
|
1604
|
+
for (const raw of text.split("\n")) {
|
|
1605
|
+
const line = raw.trim();
|
|
1606
|
+
if (!line || line.startsWith("#")) continue;
|
|
1607
|
+
const eq = line.indexOf("=");
|
|
1608
|
+
if (eq === -1) continue;
|
|
1609
|
+
const key = line.slice(0, eq).trim();
|
|
1610
|
+
const value = line.slice(eq + 1).trim();
|
|
1611
|
+
if (key) out[key] = value;
|
|
1612
|
+
}
|
|
1613
|
+
return out;
|
|
1614
|
+
}
|
|
1615
|
+
function serializeSem(values) {
|
|
1616
|
+
return JSON.stringify(values, null, 2) + "\n";
|
|
1617
|
+
}
|
|
1618
|
+
function readSem(path) {
|
|
1619
|
+
const p = path ?? resolveSemPathForRead();
|
|
1620
|
+
if (!p || !existsSync2(p)) return void 0;
|
|
1621
|
+
const text = readFileSync2(p, "utf8");
|
|
1622
|
+
const values = basename2(p) === LEGACY_SEM_FILE ? parseSem(text) : parseLaneJson(text);
|
|
1623
|
+
return { path: p, values };
|
|
1624
|
+
}
|
|
1625
|
+
function readLocalSemValues(cwd = process.cwd()) {
|
|
1626
|
+
const next = join2(cwd, SEM_FILE);
|
|
1627
|
+
if (existsSync2(next)) return readSem(next)?.values ?? {};
|
|
1628
|
+
const legacy = join2(cwd, LEGACY_SEM_FILE);
|
|
1629
|
+
if (existsSync2(legacy)) return readSem(legacy)?.values ?? {};
|
|
1630
|
+
return {};
|
|
1631
|
+
}
|
|
1632
|
+
function parseLaneJson(text) {
|
|
1633
|
+
try {
|
|
1634
|
+
const parsed = JSON.parse(text);
|
|
1635
|
+
const out = {};
|
|
1636
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
1637
|
+
if (typeof v === "string") out[k] = v;
|
|
1638
|
+
}
|
|
1639
|
+
return out;
|
|
1640
|
+
} catch {
|
|
1641
|
+
return {};
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
var STATE_DIR_IGNORE = `${STATE_DIR_NAME2}/`;
|
|
1645
|
+
function writeSem(values, path = localSemPath()) {
|
|
1646
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1647
|
+
writeFileSync2(path, serializeSem(values));
|
|
1648
|
+
ensureSemIgnored(path);
|
|
1649
|
+
return path;
|
|
1650
|
+
}
|
|
1651
|
+
function ignoresSem(content) {
|
|
1652
|
+
return content.split("\n").some((line) => {
|
|
1653
|
+
const t = line.trim();
|
|
1654
|
+
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}`;
|
|
1579
1655
|
});
|
|
1580
1656
|
}
|
|
1581
|
-
function
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
"
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1657
|
+
function inGitRepo(startDir) {
|
|
1658
|
+
let dir = startDir;
|
|
1659
|
+
for (; ; ) {
|
|
1660
|
+
if (existsSync2(join2(dir, ".git"))) return true;
|
|
1661
|
+
const parent = dirname2(dir);
|
|
1662
|
+
if (parent === dir) return false;
|
|
1663
|
+
dir = parent;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
function resolveGitignoreTarget(startDir) {
|
|
1667
|
+
let dir = startDir;
|
|
1668
|
+
for (; ; ) {
|
|
1669
|
+
const gi = join2(dir, ".gitignore");
|
|
1670
|
+
if (existsSync2(gi)) return { path: gi, exists: true };
|
|
1671
|
+
const parent = dirname2(dir);
|
|
1672
|
+
if (existsSync2(join2(dir, ".git")) || parent === dir) {
|
|
1673
|
+
return { path: join2(startDir, ".gitignore"), exists: false };
|
|
1674
|
+
}
|
|
1675
|
+
dir = parent;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
function ensureSemIgnored(semPath) {
|
|
1679
|
+
try {
|
|
1680
|
+
const checkoutDir = dirname2(dirname2(semPath));
|
|
1681
|
+
if (!inGitRepo(checkoutDir)) return;
|
|
1682
|
+
const target = resolveGitignoreTarget(checkoutDir);
|
|
1683
|
+
if (target.exists) {
|
|
1684
|
+
const content = readFileSync2(target.path, "utf8");
|
|
1685
|
+
if (ignoresSem(content)) return;
|
|
1686
|
+
const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
1687
|
+
appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
|
|
1688
|
+
`);
|
|
1689
|
+
} else {
|
|
1690
|
+
writeFileSync2(target.path, `${STATE_DIR_IGNORE}
|
|
1691
|
+
`);
|
|
1692
|
+
}
|
|
1693
|
+
} catch {
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// src/setup/clients.ts
|
|
1698
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1699
|
+
import { homedir as homedir2 } from "os";
|
|
1700
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
1701
|
+
|
|
1702
|
+
// src/setup/operator-surface.ts
|
|
1703
|
+
var SectionType = {
|
|
1704
|
+
McpConfig: "mcp-config",
|
|
1705
|
+
McpConfigToml: "mcp-config-toml",
|
|
1706
|
+
InstructionFile: "instruction-file",
|
|
1707
|
+
ProjectConfig: "project-config",
|
|
1708
|
+
Verify: "verify",
|
|
1709
|
+
/** SBC-999 — workspace-pinned conventions, emitted only when the request
|
|
1710
|
+
* carried a workspaceId and that workspace has agent-setup-bundle memories. */
|
|
1711
|
+
WorkspaceConventions: "workspace-conventions"
|
|
1712
|
+
};
|
|
1713
|
+
async function fetchSetup(cfg) {
|
|
1714
|
+
const client = await makeClient(cfg);
|
|
1715
|
+
const { data, error } = await client.GET(
|
|
1716
|
+
"/operator-surface/setup",
|
|
1717
|
+
cfg.workspaceId ? { params: { query: { workspaceId: cfg.workspaceId } } } : {}
|
|
1592
1718
|
);
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1719
|
+
if (error) throw new Error(`GET /operator-surface/setup failed: ${JSON.stringify(error)}`);
|
|
1720
|
+
return data;
|
|
1721
|
+
}
|
|
1722
|
+
function findSurface(setup, surfaceKey) {
|
|
1723
|
+
return setup.surfaces.find((s) => s.surfaceKey === surfaceKey);
|
|
1724
|
+
}
|
|
1725
|
+
function findSection(surface, sectionType) {
|
|
1726
|
+
return surface?.sections.find((s) => s.sectionType === sectionType);
|
|
1727
|
+
}
|
|
1728
|
+
function sectionSnippet(section) {
|
|
1729
|
+
if (!section) return null;
|
|
1730
|
+
for (const step of section.steps) {
|
|
1731
|
+
if (step.copyValue) return step.copyValue;
|
|
1732
|
+
if (step.codeSnippet) return step.codeSnippet;
|
|
1733
|
+
}
|
|
1734
|
+
return null;
|
|
1735
|
+
}
|
|
1736
|
+
function parseTagArtifactId(id) {
|
|
1737
|
+
if (!id.startsWith("tag:")) return null;
|
|
1738
|
+
const tags = id.slice("tag:".length).split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
1739
|
+
return tags.length > 0 ? tags : null;
|
|
1740
|
+
}
|
|
1741
|
+
async function getPersonalWorkspaceId(cfg) {
|
|
1742
|
+
const client = await makeClient(cfg);
|
|
1743
|
+
const { data } = await client.GET("/me/personal-workspace", {});
|
|
1744
|
+
return data?.workspaceId ?? null;
|
|
1745
|
+
}
|
|
1746
|
+
async function fetchMemoryFields(cfg, id) {
|
|
1747
|
+
const client = await makeClient(cfg);
|
|
1748
|
+
const { data } = await client.GET("/memories/{memoryId}", { params: { path: { memoryId: id } } });
|
|
1749
|
+
const env = data;
|
|
1750
|
+
const m = env?.item ?? env;
|
|
1751
|
+
if (!m) return null;
|
|
1752
|
+
const version = typeof m.currentVersion === "string" ? Number(m.currentVersion) : m.currentVersion;
|
|
1753
|
+
return { text: m.text, title: m.title, tags: m.tags, version: Number.isFinite(version) ? version : void 0 };
|
|
1754
|
+
}
|
|
1755
|
+
async function resolveInstruction(cfg, section, personalWorkspaceId) {
|
|
1756
|
+
const client = await makeClient(cfg);
|
|
1757
|
+
for (const artifact of section.artifacts) {
|
|
1758
|
+
const tags = parseTagArtifactId(artifact.id);
|
|
1759
|
+
if (!tags) continue;
|
|
1760
|
+
const { data } = await client.POST("/memories/search", {
|
|
1761
|
+
body: { query: null, textQuery: null, semanticQuery: null, hybrid: false, limit: 1, includeArchived: false, includeSystem: false, tags }
|
|
1598
1762
|
});
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
const
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
timezone: opts.timezone ?? null,
|
|
1609
|
-
bio: opts.bio ?? null,
|
|
1610
|
-
photoUrl: opts.photoUrl ?? null
|
|
1611
|
-
}
|
|
1763
|
+
const hits = data ?? [];
|
|
1764
|
+
if (hits.length === 0) continue;
|
|
1765
|
+
const templateId = hits[0].id;
|
|
1766
|
+
const template = await fetchMemoryFields(cfg, templateId);
|
|
1767
|
+
if (typeof template?.text !== "string" || template.text.length === 0) continue;
|
|
1768
|
+
const templateTags = template.tags ?? tags;
|
|
1769
|
+
if (personalWorkspaceId) {
|
|
1770
|
+
const { data: ovr } = await client.POST("/memories/search", {
|
|
1771
|
+
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 } }
|
|
1612
1772
|
});
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
const data = await runApi("Fetching feed", async () => {
|
|
1619
|
-
const client = await makeClient(cfg);
|
|
1620
|
-
return client.GET("/me/memories/feed", {
|
|
1621
|
-
params: {
|
|
1622
|
-
query: {
|
|
1623
|
-
limit: Number(opts.limit),
|
|
1624
|
-
includeArchived: Boolean(opts.includeArchived),
|
|
1625
|
-
includeText: Boolean(opts.includeText),
|
|
1626
|
-
...opts.cursor ? { cursor: opts.cursor } : {},
|
|
1627
|
-
...opts.query ? { query: opts.query } : {},
|
|
1628
|
-
...opts.filterTags ? { filterTags: opts.filterTags } : {}
|
|
1629
|
-
}
|
|
1773
|
+
const ovrHits = ovr ?? [];
|
|
1774
|
+
if (ovrHits.length > 0) {
|
|
1775
|
+
const override = await fetchMemoryFields(cfg, ovrHits[0].id);
|
|
1776
|
+
if (typeof override?.text === "string" && override.text.length > 0) {
|
|
1777
|
+
return { title: override.title ?? template.title ?? artifact.title, body: override.text, source: "override", templateId, templateTags, sourceRef: `${ovrHits[0].id}@v${override.version ?? 1}` };
|
|
1630
1778
|
}
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return { title: template.title ?? artifact.title, body: template.text, source: "template", templateId, templateTags, sourceRef: `${templateId}@v${template.version ?? 1}` };
|
|
1782
|
+
}
|
|
1783
|
+
return null;
|
|
1784
|
+
}
|
|
1785
|
+
async function resolveWorkspaceConventions(cfg, section) {
|
|
1786
|
+
const parts = [];
|
|
1787
|
+
const refs = [];
|
|
1788
|
+
for (const artifact of section.artifacts) {
|
|
1789
|
+
if (parseTagArtifactId(artifact.id)) continue;
|
|
1790
|
+
const mem = await fetchMemoryFields(cfg, artifact.id);
|
|
1791
|
+
if (typeof mem?.text === "string" && mem.text.trim().length > 0) {
|
|
1792
|
+
parts.push(mem.text.trim());
|
|
1793
|
+
refs.push(`${artifact.id}@v${mem.version ?? 1}`);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
if (parts.length === 0) return null;
|
|
1797
|
+
return { body: parts.join("\n\n---\n\n"), refs };
|
|
1798
|
+
}
|
|
1799
|
+
async function createOverride(cfg, template, personalWorkspaceId) {
|
|
1800
|
+
const client = await makeClient(cfg);
|
|
1801
|
+
const overrideTags = template.templateTags.filter(
|
|
1802
|
+
(t) => t !== "sechroom:role:template" && !t.startsWith("sechroom:bundle:") && !t.startsWith("sechroom:template-ref:")
|
|
1803
|
+
);
|
|
1804
|
+
overrideTags.push("sechroom:role:override", `sechroom:template-ref:${template.templateId}`);
|
|
1805
|
+
const { error } = await client.POST("/memories", {
|
|
1806
|
+
body: {
|
|
1807
|
+
text: template.body,
|
|
1808
|
+
type: "reference",
|
|
1809
|
+
content: "{}",
|
|
1810
|
+
confidence: 1,
|
|
1811
|
+
source: "cli-agent-instructions-customize",
|
|
1812
|
+
archetype: "Document",
|
|
1813
|
+
title: template.title ?? null,
|
|
1814
|
+
tags: overrideTags,
|
|
1815
|
+
owner: { type: "Workspace", id: personalWorkspaceId }
|
|
1816
|
+
}
|
|
1634
1817
|
});
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1818
|
+
if (error) throw new Error(`creating personal copy failed: ${JSON.stringify(error)}`);
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// src/setup/clients.ts
|
|
1822
|
+
function claudeDesktopConfigPath(home) {
|
|
1823
|
+
switch (process.platform) {
|
|
1824
|
+
case "darwin":
|
|
1825
|
+
return join3(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
1826
|
+
case "win32":
|
|
1827
|
+
return join3(process.env.APPDATA ?? join3(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
1828
|
+
default:
|
|
1829
|
+
return join3(home, ".config", "Claude", "claude_desktop_config.json");
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
function clientTargets(cwd) {
|
|
1833
|
+
const home = homedir2();
|
|
1834
|
+
return {
|
|
1835
|
+
"claude-code": {
|
|
1836
|
+
key: "claude-code",
|
|
1837
|
+
label: "Claude Code",
|
|
1838
|
+
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join3(cwd, ".mcp.json"), format: "json" },
|
|
1839
|
+
instruction: { surfaceKey: "claude-code", path: join3(cwd, "CLAUDE.md") }
|
|
1840
|
+
},
|
|
1841
|
+
"claude-desktop": {
|
|
1842
|
+
key: "claude-desktop",
|
|
1843
|
+
label: "Claude Desktop",
|
|
1844
|
+
mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
|
|
1845
|
+
instruction: { surfaceKey: "claude-desktop", path: join3(home, ".claude", "CLAUDE.md") }
|
|
1846
|
+
},
|
|
1847
|
+
codex: {
|
|
1848
|
+
key: "codex",
|
|
1849
|
+
label: "Codex CLI",
|
|
1850
|
+
mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join3(home, ".codex", "config.toml"), format: "toml" },
|
|
1851
|
+
instruction: { surfaceKey: "chatgpt", path: join3(cwd, "AGENTS.md") }
|
|
1852
|
+
},
|
|
1853
|
+
cursor: {
|
|
1854
|
+
key: "cursor",
|
|
1855
|
+
label: "Cursor",
|
|
1856
|
+
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join3(cwd, ".cursor", "mcp.json"), format: "json" },
|
|
1857
|
+
instruction: { surfaceKey: "chatgpt", path: join3(cwd, "AGENTS.md") }
|
|
1858
|
+
}
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor"];
|
|
1862
|
+
var DEFAULT_CLIENT_KEY = "claude-code";
|
|
1863
|
+
function detectInstalledClients(cwd) {
|
|
1864
|
+
const home = homedir2();
|
|
1865
|
+
const detected = [];
|
|
1866
|
+
if (existsSync3(join3(home, ".claude"))) detected.push("claude-code");
|
|
1867
|
+
if (existsSync3(dirname3(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
|
|
1868
|
+
if (existsSync3(join3(home, ".codex"))) detected.push("codex");
|
|
1869
|
+
if (existsSync3(join3(home, ".cursor")) || existsSync3(join3(cwd, ".cursor"))) detected.push("cursor");
|
|
1870
|
+
return detected;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// src/commands/hook.ts
|
|
1874
|
+
async function readStdin() {
|
|
1875
|
+
if (process.stdin.isTTY) return "";
|
|
1876
|
+
const chunks = [];
|
|
1877
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
1878
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1879
|
+
}
|
|
1880
|
+
function parseHookInput(raw) {
|
|
1881
|
+
if (!raw.trim()) return {};
|
|
1882
|
+
try {
|
|
1883
|
+
return JSON.parse(raw);
|
|
1884
|
+
} catch {
|
|
1885
|
+
return {};
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
function resolveLane(flagLane, cwd) {
|
|
1889
|
+
if (flagLane) return flagLane;
|
|
1890
|
+
const env = process.env.SECHROOM_LANE;
|
|
1891
|
+
if (env) return env;
|
|
1892
|
+
const start = cwd ?? process.cwd();
|
|
1893
|
+
const sem = readSem(resolveSemPathForRead(start));
|
|
1894
|
+
return sem?.values["code-lane"];
|
|
1895
|
+
}
|
|
1896
|
+
var INTENT_FILE = join4(".sechroom", "continuity.json");
|
|
1897
|
+
function resolveIntentPath(start) {
|
|
1898
|
+
let dir = start;
|
|
1899
|
+
for (; ; ) {
|
|
1900
|
+
const candidate = join4(dir, INTENT_FILE);
|
|
1901
|
+
if (existsSync4(candidate)) return candidate;
|
|
1902
|
+
const parent = dirname4(dir);
|
|
1903
|
+
if (parent === dir) return void 0;
|
|
1904
|
+
dir = parent;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
function readIntent(start) {
|
|
1908
|
+
const path = resolveIntentPath(start);
|
|
1909
|
+
if (!path) return void 0;
|
|
1910
|
+
try {
|
|
1911
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
1912
|
+
} catch {
|
|
1913
|
+
return void 0;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
function hasRequiredIntent(i) {
|
|
1917
|
+
return Boolean(
|
|
1918
|
+
i.objective?.trim() && i.state?.trim() && i.lastAction?.trim() && i.nextAction?.trim() && i.resumeInstruction?.trim()
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
function formatContext(bundle, lane) {
|
|
1922
|
+
const s = bundle?.latestSnapshot;
|
|
1923
|
+
if (!s) return null;
|
|
1924
|
+
const lines = [];
|
|
1925
|
+
lines.push(`[sechroom continuity \u2014 resumed lane ${lane}]`);
|
|
1926
|
+
if (s.currentObjective) lines.push(`Objective: ${s.currentObjective}`);
|
|
1927
|
+
if (s.currentState) lines.push(`State: ${s.currentState}`);
|
|
1928
|
+
if (s.lastMeaningfulAction) lines.push(`Last action: ${s.lastMeaningfulAction}`);
|
|
1929
|
+
if (s.nextIntendedAction) lines.push(`Next: ${s.nextIntendedAction}`);
|
|
1930
|
+
if (s.resumeInstruction) lines.push(`Resume: ${s.resumeInstruction}`);
|
|
1931
|
+
const constraints = s.activeConstraints ?? [];
|
|
1932
|
+
if (constraints.length) {
|
|
1933
|
+
lines.push("Active constraints:");
|
|
1934
|
+
for (const c of constraints) lines.push(` - ${c}`);
|
|
1935
|
+
}
|
|
1936
|
+
const questions = s.openQuestions ?? [];
|
|
1937
|
+
if (questions.length) {
|
|
1938
|
+
lines.push("Open questions:");
|
|
1939
|
+
for (const q of questions) lines.push(` - ${q}`);
|
|
1940
|
+
}
|
|
1941
|
+
const artifacts = s.relevantArtifactIds ?? [];
|
|
1942
|
+
if (artifacts.length) lines.push(`Relevant artifacts: ${artifacts.join(", ")}`);
|
|
1943
|
+
const marker = [s.id, s.createdAt].filter(Boolean).join(" @ ");
|
|
1944
|
+
if (marker) lines.push(`(snapshot ${marker})`);
|
|
1945
|
+
return lines.join("\n");
|
|
1946
|
+
}
|
|
1947
|
+
function emitSessionStart(additionalContext) {
|
|
1948
|
+
process.stdout.write(
|
|
1949
|
+
JSON.stringify({
|
|
1950
|
+
hookSpecificOutput: {
|
|
1951
|
+
hookEventName: "SessionStart",
|
|
1952
|
+
additionalContext
|
|
1953
|
+
}
|
|
1954
|
+
}) + "\n"
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
var HOOK_COMMANDS = {
|
|
1958
|
+
SessionStart: "sechroom hook session-start",
|
|
1959
|
+
PreCompact: "sechroom hook pre-compact"
|
|
1960
|
+
};
|
|
1961
|
+
var HOOK_EVENTS = ["SessionStart", "PreCompact"];
|
|
1962
|
+
function hasHookCommand(config2, event, command) {
|
|
1963
|
+
const groups = config2.hooks?.[event] ?? [];
|
|
1964
|
+
return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
|
|
1965
|
+
}
|
|
1966
|
+
function mergeHooks(config2) {
|
|
1967
|
+
config2.hooks ??= {};
|
|
1968
|
+
let added = 0;
|
|
1969
|
+
for (const event of HOOK_EVENTS) {
|
|
1970
|
+
const command = HOOK_COMMANDS[event];
|
|
1971
|
+
if (hasHookCommand(config2, event, command)) continue;
|
|
1972
|
+
const groups = config2.hooks[event] ??= [];
|
|
1973
|
+
groups.push({ hooks: [{ type: "command", command }] });
|
|
1974
|
+
added += 1;
|
|
1975
|
+
}
|
|
1976
|
+
return added;
|
|
1977
|
+
}
|
|
1978
|
+
function readJsonConfig2(path) {
|
|
1979
|
+
if (!existsSync4(path)) return {};
|
|
1980
|
+
const raw = readFileSync3(path, "utf8");
|
|
1981
|
+
if (!raw.trim()) return {};
|
|
1982
|
+
return JSON.parse(raw);
|
|
1983
|
+
}
|
|
1984
|
+
function installHooksJson(path, dryRun) {
|
|
1985
|
+
const existed = existsSync4(path) && readFileSync3(path, "utf8").trim().length > 0;
|
|
1986
|
+
const config2 = readJsonConfig2(path);
|
|
1987
|
+
const added = mergeHooks(config2);
|
|
1988
|
+
if (added === 0 && existed) return { path, status: "current" };
|
|
1989
|
+
if (!dryRun) {
|
|
1990
|
+
mkdirSync3(dirname4(path), { recursive: true });
|
|
1991
|
+
writeFileSync3(path, JSON.stringify(config2, null, 2) + "\n");
|
|
1992
|
+
}
|
|
1993
|
+
return { path, status: existed ? "merged" : "created" };
|
|
1994
|
+
}
|
|
1995
|
+
function ensureCodexFeaturesHooks(content) {
|
|
1996
|
+
const lines = content.split("\n");
|
|
1997
|
+
const headerIdx = lines.findIndex((l) => l.trim() === "[features]");
|
|
1998
|
+
if (headerIdx === -1) {
|
|
1999
|
+
const base = content.length === 0 || content.endsWith("\n") ? content : content + "\n";
|
|
2000
|
+
return { next: base + "\n[features]\nhooks = true\n", changed: true };
|
|
2001
|
+
}
|
|
2002
|
+
for (let i = headerIdx + 1; i < lines.length; i += 1) {
|
|
2003
|
+
const trimmed = lines[i].trim();
|
|
2004
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) break;
|
|
2005
|
+
const m = lines[i].match(/^(\s*)hooks(\s*)=(\s*)(.*)$/);
|
|
2006
|
+
if (!m) continue;
|
|
2007
|
+
const value = m[4].replace(/\s*#.*$/, "").trim();
|
|
2008
|
+
if (value === "true") return { next: content, changed: false };
|
|
2009
|
+
lines[i] = `${m[1]}hooks${m[2]}=${m[3]}true`;
|
|
2010
|
+
return { next: lines.join("\n"), changed: true };
|
|
2011
|
+
}
|
|
2012
|
+
lines.splice(headerIdx + 1, 0, "hooks = true");
|
|
2013
|
+
return { next: lines.join("\n"), changed: true };
|
|
2014
|
+
}
|
|
2015
|
+
function installCodexFeatureFlag(path, dryRun) {
|
|
2016
|
+
const existed = existsSync4(path);
|
|
2017
|
+
const content = existed ? readFileSync3(path, "utf8") : "";
|
|
2018
|
+
const { next, changed } = ensureCodexFeaturesHooks(content);
|
|
2019
|
+
if (!changed) return { path, status: "current" };
|
|
2020
|
+
if (!dryRun) {
|
|
2021
|
+
mkdirSync3(dirname4(path), { recursive: true });
|
|
2022
|
+
writeFileSync3(path, next);
|
|
2023
|
+
}
|
|
2024
|
+
return { path, status: existed ? "merged" : "created" };
|
|
2025
|
+
}
|
|
2026
|
+
function resolveSurfaces(surface, cwd) {
|
|
2027
|
+
if (surface === "claude") return ["claude"];
|
|
2028
|
+
if (surface === "codex") return ["codex"];
|
|
2029
|
+
if (surface === "both") return ["claude", "codex"];
|
|
2030
|
+
if (surface) throw new Error(`--surface must be one of claude | codex | both (got '${surface}')`);
|
|
2031
|
+
const surfaces = detectHookSurfaces(cwd);
|
|
2032
|
+
return surfaces.length > 0 ? surfaces : ["claude", "codex"];
|
|
2033
|
+
}
|
|
2034
|
+
function describe(result, dryRun) {
|
|
2035
|
+
if (result.status === "current") return ` \u2713 ${result.path} (already configured)`;
|
|
2036
|
+
const verb = dryRun ? "would" : result.status === "created" ? "created" : "updated";
|
|
2037
|
+
return ` \u2713 ${result.path} (${dryRun ? `${verb} ${result.status === "created" ? "create" : "update"}` : verb})`;
|
|
2038
|
+
}
|
|
2039
|
+
var HOOK_SURFACE_LABEL = {
|
|
2040
|
+
claude: "Claude Code",
|
|
2041
|
+
codex: "Codex"
|
|
2042
|
+
};
|
|
2043
|
+
function installHookSurfaces(surfaces, opts) {
|
|
2044
|
+
const out = [];
|
|
2045
|
+
for (const surface of surfaces) {
|
|
2046
|
+
if (surface === "claude") {
|
|
2047
|
+
const path = opts.local ? join4(opts.cwd, ".claude", "settings.json") : join4(opts.home, ".claude", "settings.json");
|
|
2048
|
+
out.push({ surface, results: [installHooksJson(path, opts.dryRun)] });
|
|
2049
|
+
} else {
|
|
2050
|
+
const hooksJson = installHooksJson(join4(opts.home, ".codex", "hooks.json"), opts.dryRun);
|
|
2051
|
+
const featureFlag = installCodexFeatureFlag(join4(opts.home, ".codex", "config.toml"), opts.dryRun);
|
|
2052
|
+
out.push({ surface, results: [hooksJson, featureFlag] });
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
return out;
|
|
2056
|
+
}
|
|
2057
|
+
function detectHookSurfaces(cwd) {
|
|
2058
|
+
const detected = detectInstalledClients(cwd);
|
|
2059
|
+
const surfaces = [];
|
|
2060
|
+
if (detected.includes("claude-code")) surfaces.push("claude");
|
|
2061
|
+
if (detected.includes("codex")) surfaces.push("codex");
|
|
2062
|
+
return surfaces;
|
|
2063
|
+
}
|
|
2064
|
+
function isSechroomOnPath() {
|
|
2065
|
+
const pathEnv = process.env.PATH ?? "";
|
|
2066
|
+
if (!pathEnv) return false;
|
|
2067
|
+
const names = process.platform === "win32" ? ["sechroom.cmd", "sechroom.exe", "sechroom.bat", "sechroom"] : ["sechroom"];
|
|
2068
|
+
for (const dir of pathEnv.split(delimiter)) {
|
|
2069
|
+
if (!dir) continue;
|
|
2070
|
+
for (const name of names) {
|
|
2071
|
+
if (existsSync4(join4(dir, name))) return true;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
return false;
|
|
2075
|
+
}
|
|
2076
|
+
function warnIfSechroomNotOnPath(write = (s) => void process.stderr.write(s)) {
|
|
2077
|
+
if (isSechroomOnPath()) return false;
|
|
2078
|
+
write(
|
|
2079
|
+
"\n\u26A0 `sechroom` isn't on your PATH. The hooks run a bare `sechroom hook \u2026` command\n when your agent fires them, so a non-global install (npx / local) will fail at\n that point. Install globally so the command resolves:\n npm i -g @sechroom/cli\n"
|
|
2080
|
+
);
|
|
2081
|
+
return true;
|
|
2082
|
+
}
|
|
2083
|
+
function registerHook(program2) {
|
|
2084
|
+
const hook = program2.command("hook").description("Agent-lifecycle hook adapter (Claude Code / Codex) \u2014 bridges hooks to continuity");
|
|
2085
|
+
hook.addHelpText(
|
|
2086
|
+
"after",
|
|
2087
|
+
`
|
|
2088
|
+
Examples:
|
|
2089
|
+
# SessionStart (load): inject the lane's latest snapshot as context.
|
|
2090
|
+
$ echo '{"hook_event_name":"SessionStart","cwd":"'"$PWD"'"}' | sechroom hook session-start
|
|
2091
|
+
# PreCompact (save): snapshot from ./${INTENT_FILE} before the agent compacts.
|
|
2092
|
+
$ echo '{"hook_event_name":"PreCompact","cwd":"'"$PWD"'"}' | sechroom hook pre-compact
|
|
2093
|
+
# Wire both hooks into the installed surface(s)' config (no hand-editing):
|
|
2094
|
+
$ sechroom hook install auto-detect Claude Code / Codex
|
|
2095
|
+
$ sechroom hook install --surface codex Codex only
|
|
2096
|
+
$ sechroom hook install --local --dry-run preview the project .claude/settings.json
|
|
2097
|
+
|
|
2098
|
+
Lane source (high -> low): --lane > SECHROOM_LANE > ./.sem code-lane (D-binding-5).
|
|
2099
|
+
Fail-soft: no lane / no auth / no-or-partial intent file / API error -> exit 0, never blocks.`
|
|
2100
|
+
);
|
|
2101
|
+
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) => {
|
|
2102
|
+
try {
|
|
2103
|
+
const raw = await readStdin();
|
|
2104
|
+
const input = parseHookInput(raw);
|
|
2105
|
+
const lane = resolveLane(opts.lane, input.cwd);
|
|
2106
|
+
if (!lane) return process.exit(0);
|
|
2107
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2108
|
+
const client = await makeClient(cfg);
|
|
2109
|
+
const { data } = await client.POST("/continuity/resume/lane", {
|
|
2110
|
+
body: {
|
|
2111
|
+
laneId: lane,
|
|
2112
|
+
workspaceId: null,
|
|
2113
|
+
maxArtifacts: opts.maxArtifacts != null ? Number(opts.maxArtifacts) : null,
|
|
2114
|
+
includeLookingAtMyself: null,
|
|
2115
|
+
changedSince: null
|
|
2116
|
+
}
|
|
2117
|
+
});
|
|
2118
|
+
const context = formatContext(data, lane);
|
|
2119
|
+
if (context) emitSessionStart(context);
|
|
2120
|
+
return process.exit(0);
|
|
2121
|
+
} catch {
|
|
2122
|
+
return process.exit(0);
|
|
2123
|
+
}
|
|
2124
|
+
});
|
|
2125
|
+
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) => {
|
|
2126
|
+
try {
|
|
2127
|
+
const raw = await readStdin();
|
|
2128
|
+
const input = parseHookInput(raw);
|
|
2129
|
+
const cwd = input.cwd ?? process.cwd();
|
|
2130
|
+
const lane = resolveLane(opts.lane, input.cwd);
|
|
2131
|
+
if (!lane) return process.exit(0);
|
|
2132
|
+
const intent = readIntent(cwd);
|
|
2133
|
+
if (!intent || !hasRequiredIntent(intent)) return process.exit(0);
|
|
2134
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2135
|
+
const client = await makeClient(cfg);
|
|
2136
|
+
await client.POST("/continuity/snapshots", {
|
|
2137
|
+
body: {
|
|
2138
|
+
laneId: lane,
|
|
2139
|
+
scope: opts.scope ?? intent.scope ?? "compaction",
|
|
2140
|
+
currentObjective: intent.objective,
|
|
2141
|
+
currentState: intent.state,
|
|
2142
|
+
lastMeaningfulAction: intent.lastAction,
|
|
2143
|
+
nextIntendedAction: intent.nextAction,
|
|
2144
|
+
resumeInstruction: intent.resumeInstruction,
|
|
2145
|
+
activeConstraints: intent.constraints ?? null,
|
|
2146
|
+
openQuestions: intent.questions ?? null,
|
|
2147
|
+
surfaceMarkers: intent.surfaceMarkers ?? null,
|
|
2148
|
+
relevantArtifactIds: intent.artifacts ?? null,
|
|
2149
|
+
confidence: intent.confidence ?? null,
|
|
2150
|
+
// Compaction is infrequent, so the FR-051 clobber guard doesn't bite;
|
|
2151
|
+
// Acknowledge lets a within-window checkpoint land on the lane.
|
|
2152
|
+
concurrentSessionPolicy: "Acknowledge"
|
|
2153
|
+
}
|
|
2154
|
+
});
|
|
2155
|
+
return process.exit(0);
|
|
2156
|
+
} catch {
|
|
2157
|
+
return process.exit(0);
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2160
|
+
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) => {
|
|
2161
|
+
const dryRun = Boolean(opts.dryRun);
|
|
2162
|
+
const cwd = process.cwd();
|
|
2163
|
+
let surfaces;
|
|
2164
|
+
try {
|
|
2165
|
+
surfaces = resolveSurfaces(opts.surface, cwd);
|
|
2166
|
+
} catch (err2) {
|
|
2167
|
+
process.stderr.write(`${err2.message}
|
|
2168
|
+
`);
|
|
2169
|
+
return process.exit(2);
|
|
2170
|
+
}
|
|
2171
|
+
const results = [];
|
|
2172
|
+
try {
|
|
2173
|
+
const installed = installHookSurfaces(surfaces, { dryRun, local: opts.local, cwd, home: homedir3() });
|
|
2174
|
+
for (const { surface, results: surfaceResults } of installed) {
|
|
2175
|
+
process.stdout.write(`${HOOK_SURFACE_LABEL[surface]}:
|
|
2176
|
+
`);
|
|
2177
|
+
for (const r of surfaceResults) {
|
|
2178
|
+
results.push(r);
|
|
2179
|
+
process.stdout.write(describe(r, dryRun) + "\n");
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
} catch (err2) {
|
|
2183
|
+
process.stderr.write(`hook install failed: ${err2.message}
|
|
2184
|
+
`);
|
|
2185
|
+
return process.exit(1);
|
|
2186
|
+
}
|
|
2187
|
+
if (dryRun) {
|
|
2188
|
+
process.stdout.write("\n(dry run \u2014 no files were written.)\n");
|
|
2189
|
+
} else if (results.every((r) => r.status === "current")) {
|
|
2190
|
+
process.stdout.write("\nAlready up to date \u2014 nothing to change.\n");
|
|
2191
|
+
} else {
|
|
2192
|
+
process.stdout.write("\nRestart (or reload) your agent for the hooks to take effect.\n");
|
|
2193
|
+
}
|
|
2194
|
+
warnIfSechroomNotOnPath();
|
|
2195
|
+
return process.exit(0);
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// src/commands/account.ts
|
|
2200
|
+
function registerId(program2) {
|
|
2201
|
+
const id = program2.command("id").description("Allocate human-authored id sequences (FR-*, D-*)");
|
|
2202
|
+
id.addHelpText(
|
|
2203
|
+
"after",
|
|
2204
|
+
`
|
|
2205
|
+
Examples:
|
|
2206
|
+
$ sechroom id next FR sechroom allocate the next FR-sechroom-NNN id
|
|
2207
|
+
$ sechroom id next D Backend allocate the next D-Backend-NNN id
|
|
2208
|
+
$ sechroom id peek FR sechroom inspect the sequence without consuming
|
|
2209
|
+
$ sechroom id peek FR sechroom --json`
|
|
2210
|
+
);
|
|
2211
|
+
id.command("next <namespaceKind> <scope>").description("Allocate the next id in a sequence (POST /id-registry/allocate)").action(async (namespaceKind, scope, _opts, cmd) => {
|
|
2212
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2213
|
+
const data = await runApi("Allocating id", async () => {
|
|
2214
|
+
const client = await makeClient(cfg);
|
|
2215
|
+
return client.POST("/id-registry/allocate", {
|
|
2216
|
+
body: { namespaceKind, scope, clientNonce: null }
|
|
2217
|
+
});
|
|
2218
|
+
});
|
|
2219
|
+
emitAction(`allocated ${style.bold(data.id)} ${style.dim(`(seq ${data.seq})`)}`, data, cmd.optsWithGlobals().json);
|
|
2220
|
+
});
|
|
2221
|
+
id.command("peek <namespaceKind> <scope>").description("Inspect a sequence without consuming (GET /id-registry/state)").action(async (namespaceKind, scope, _opts, cmd) => {
|
|
2222
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2223
|
+
const data = await runApi("Peeking id sequence", async () => {
|
|
2224
|
+
const client = await makeClient(cfg);
|
|
2225
|
+
return client.GET("/id-registry/state", {
|
|
2226
|
+
params: { query: { namespaceKind, scope } }
|
|
2227
|
+
});
|
|
2228
|
+
});
|
|
2229
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
function registerAccount(program2) {
|
|
2233
|
+
const account = program2.command("account").description("Your profile, feeds, and review queue");
|
|
2234
|
+
account.addHelpText(
|
|
2235
|
+
"after",
|
|
2236
|
+
`
|
|
2237
|
+
Examples:
|
|
2238
|
+
$ sechroom account profile
|
|
2239
|
+
$ sechroom account set-profile --display-name "Chris" --timezone "Europe/London"
|
|
2240
|
+
$ sechroom account feed --limit 20
|
|
2241
|
+
$ sechroom account reviews --status Pending
|
|
2242
|
+
$ sechroom account lookup-batch mem_XXXX wsp_YYYY --json`
|
|
2243
|
+
);
|
|
2244
|
+
account.command("profile").description("Show your resolved profile (GET /me/profile)").action(async (_opts, cmd) => {
|
|
2245
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2246
|
+
const data = await runApi("Fetching profile", async () => {
|
|
2247
|
+
const client = await makeClient(cfg);
|
|
2248
|
+
return client.GET("/me/profile");
|
|
2249
|
+
});
|
|
2250
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
2251
|
+
});
|
|
2252
|
+
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) => {
|
|
2253
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2254
|
+
const data = await runApi("Updating profile", async () => {
|
|
2255
|
+
const client = await makeClient(cfg);
|
|
2256
|
+
return client.PUT("/me/profile", {
|
|
2257
|
+
body: {
|
|
2258
|
+
displayName: opts.displayName ?? null,
|
|
2259
|
+
timezone: opts.timezone ?? null,
|
|
2260
|
+
bio: opts.bio ?? null,
|
|
2261
|
+
photoUrl: opts.photoUrl ?? null
|
|
2262
|
+
}
|
|
2263
|
+
});
|
|
2264
|
+
});
|
|
2265
|
+
emitAction("updated profile", data, cmd.optsWithGlobals().json);
|
|
2266
|
+
});
|
|
2267
|
+
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) => {
|
|
2268
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2269
|
+
const data = await runApi("Fetching feed", async () => {
|
|
2270
|
+
const client = await makeClient(cfg);
|
|
2271
|
+
return client.GET("/me/memories/feed", {
|
|
2272
|
+
params: {
|
|
2273
|
+
query: {
|
|
2274
|
+
limit: Number(opts.limit),
|
|
2275
|
+
includeArchived: Boolean(opts.includeArchived),
|
|
2276
|
+
includeText: Boolean(opts.includeText),
|
|
2277
|
+
...opts.cursor ? { cursor: opts.cursor } : {},
|
|
2278
|
+
...opts.query ? { query: opts.query } : {},
|
|
2279
|
+
...opts.filterTags ? { filterTags: opts.filterTags } : {}
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
});
|
|
2283
|
+
});
|
|
2284
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
2285
|
+
});
|
|
2286
|
+
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) => {
|
|
2287
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2288
|
+
const data = await runApi("Fetching reviews", async () => {
|
|
2289
|
+
const client = await makeClient(cfg);
|
|
2290
|
+
return client.GET("/reviews", {
|
|
2291
|
+
params: {
|
|
2292
|
+
query: {
|
|
2293
|
+
limit: Number(opts.limit),
|
|
2294
|
+
...opts.status ? { status: opts.status } : {},
|
|
2295
|
+
...opts.scopeKind ? { scopeKind: opts.scopeKind } : {},
|
|
2296
|
+
...opts.scopeTargetId ? { scopeTargetId: opts.scopeTargetId } : {}
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
1648
2299
|
});
|
|
1649
2300
|
});
|
|
1650
2301
|
emit(data, cmd.optsWithGlobals().json);
|
|
@@ -1761,125 +2412,77 @@ Examples:
|
|
|
1761
2412
|
}
|
|
1762
2413
|
|
|
1763
2414
|
// src/setup/apply.ts
|
|
1764
|
-
import {
|
|
1765
|
-
import {
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
var
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
InstructionFile: "instruction-file",
|
|
1772
|
-
ProjectConfig: "project-config",
|
|
1773
|
-
Verify: "verify"
|
|
1774
|
-
};
|
|
1775
|
-
async function fetchSetup(cfg) {
|
|
1776
|
-
const client = await makeClient(cfg);
|
|
1777
|
-
const { data, error } = await client.GET("/operator-surface/setup", {});
|
|
1778
|
-
if (error) throw new Error(`GET /operator-surface/setup failed: ${JSON.stringify(error)}`);
|
|
1779
|
-
return data;
|
|
2415
|
+
import { createHash as createHash2 } from "crypto";
|
|
2416
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
|
|
2417
|
+
import { dirname as dirname5 } from "path";
|
|
2418
|
+
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
2419
|
+
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
2420
|
+
function normalizeBody(s) {
|
|
2421
|
+
return s.replace(/\r\n/g, "\n").trim();
|
|
1780
2422
|
}
|
|
1781
|
-
function
|
|
1782
|
-
return
|
|
2423
|
+
function bodySha256(body) {
|
|
2424
|
+
return createHash2("sha256").update(normalizeBody(body), "utf8").digest("hex");
|
|
1783
2425
|
}
|
|
1784
|
-
function
|
|
1785
|
-
|
|
2426
|
+
function renderBlock(write) {
|
|
2427
|
+
const body = normalizeBody(write.body);
|
|
2428
|
+
const attrs = [`block=${write.block}`];
|
|
2429
|
+
if (write.source) attrs.push(`source=${write.source}`);
|
|
2430
|
+
attrs.push(`sha256=${bodySha256(body)}`);
|
|
2431
|
+
return `${MARKER_BEGIN} ${attrs.join(" ")} -->
|
|
2432
|
+
${body}
|
|
2433
|
+
${MARKER_END} block=${write.block} -->
|
|
2434
|
+
`;
|
|
1786
2435
|
}
|
|
1787
|
-
function
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
}
|
|
1793
|
-
return null;
|
|
2436
|
+
function keyedBlockRe(block) {
|
|
2437
|
+
const b = escapeRe(block);
|
|
2438
|
+
return new RegExp(
|
|
2439
|
+
`${escapeRe(MARKER_BEGIN)}[^\\n]*?\\bblock=${b}\\b[^\\n]*?-->\\n[\\s\\S]*?${escapeRe(MARKER_END)}[^\\n]*?\\bblock=${b}\\b[^\\n]*?-->\\n?`
|
|
2440
|
+
);
|
|
1794
2441
|
}
|
|
1795
|
-
function
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
2442
|
+
function legacyBlockRe() {
|
|
2443
|
+
return new RegExp(
|
|
2444
|
+
`${escapeRe(MARKER_BEGIN)}(?:(?!block=)[^\\n])*?-->\\n[\\s\\S]*?${escapeRe(MARKER_END)}(?:(?!block=)[^\\n])*?-->\\n?`
|
|
2445
|
+
);
|
|
1799
2446
|
}
|
|
1800
|
-
|
|
1801
|
-
const
|
|
1802
|
-
const
|
|
1803
|
-
return
|
|
2447
|
+
function parseAttrs(beginLine) {
|
|
2448
|
+
const attrs = {};
|
|
2449
|
+
for (const m of beginLine.matchAll(/(\w+)=(\S+)/g)) attrs[m[1]] = m[2];
|
|
2450
|
+
return attrs;
|
|
1804
2451
|
}
|
|
1805
|
-
|
|
1806
|
-
const
|
|
1807
|
-
const
|
|
1808
|
-
|
|
1809
|
-
return env?.item ?? env ?? null;
|
|
2452
|
+
function innerBody(segment) {
|
|
2453
|
+
const firstNl = segment.indexOf("\n");
|
|
2454
|
+
const endIdx = segment.lastIndexOf(MARKER_END);
|
|
2455
|
+
return segment.slice(firstNl + 1, endIdx).replace(/\n$/, "");
|
|
1810
2456
|
}
|
|
1811
|
-
|
|
1812
|
-
const
|
|
1813
|
-
|
|
1814
|
-
const
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
if (hits.length === 0) continue;
|
|
1821
|
-
const templateId = hits[0].id;
|
|
1822
|
-
const template = await fetchMemoryFields(cfg, templateId);
|
|
1823
|
-
if (typeof template?.text !== "string" || template.text.length === 0) continue;
|
|
1824
|
-
const templateTags = template.tags ?? tags;
|
|
1825
|
-
if (personalWorkspaceId) {
|
|
1826
|
-
const { data: ovr } = await client.POST("/memories/search", {
|
|
1827
|
-
body: { query: null, textQuery: null, semanticQuery: "role override", hybrid: true, limit: 1, includeArchived: false, includeSystem: false, tags: ["sechroom:role:override", `sechroom:template-ref:${templateId}`], owner: { type: "Workspace", id: personalWorkspaceId } }
|
|
1828
|
-
});
|
|
1829
|
-
const ovrHits = ovr ?? [];
|
|
1830
|
-
if (ovrHits.length > 0) {
|
|
1831
|
-
const override = await fetchMemoryFields(cfg, ovrHits[0].id);
|
|
1832
|
-
if (typeof override?.text === "string" && override.text.length > 0) {
|
|
1833
|
-
return { title: override.title ?? template.title ?? artifact.title, body: override.text, source: "override", templateId, templateTags };
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
return { title: template.title ?? artifact.title, body: template.text, source: "template", templateId, templateTags };
|
|
2457
|
+
function parseManagedBlock(content, block) {
|
|
2458
|
+
const keyed = content.match(keyedBlockRe(block));
|
|
2459
|
+
if (keyed) {
|
|
2460
|
+
const attrs = parseAttrs(keyed[0].slice(0, keyed[0].indexOf("\n")));
|
|
2461
|
+
return { block, source: attrs.source ?? null, sha256: attrs.sha256 ?? null, body: innerBody(keyed[0]) };
|
|
2462
|
+
}
|
|
2463
|
+
if (block === "role-template") {
|
|
2464
|
+
const legacy = content.match(legacyBlockRe());
|
|
2465
|
+
if (legacy) return { block, source: null, sha256: null, body: innerBody(legacy[0]) };
|
|
1838
2466
|
}
|
|
1839
2467
|
return null;
|
|
1840
2468
|
}
|
|
1841
|
-
async function createOverride(cfg, template, personalWorkspaceId) {
|
|
1842
|
-
const client = await makeClient(cfg);
|
|
1843
|
-
const overrideTags = template.templateTags.filter(
|
|
1844
|
-
(t) => t !== "sechroom:role:template" && !t.startsWith("sechroom:bundle:") && !t.startsWith("sechroom:template-ref:")
|
|
1845
|
-
);
|
|
1846
|
-
overrideTags.push("sechroom:role:override", `sechroom:template-ref:${template.templateId}`);
|
|
1847
|
-
const { error } = await client.POST("/memories", {
|
|
1848
|
-
body: {
|
|
1849
|
-
text: template.body,
|
|
1850
|
-
type: "reference",
|
|
1851
|
-
content: "{}",
|
|
1852
|
-
confidence: 1,
|
|
1853
|
-
source: "cli-agent-instructions-customize",
|
|
1854
|
-
archetype: "Document",
|
|
1855
|
-
title: template.title ?? null,
|
|
1856
|
-
tags: overrideTags,
|
|
1857
|
-
owner: { type: "Workspace", id: personalWorkspaceId }
|
|
1858
|
-
}
|
|
1859
|
-
});
|
|
1860
|
-
if (error) throw new Error(`creating personal copy failed: ${JSON.stringify(error)}`);
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
// src/setup/apply.ts
|
|
1864
|
-
var BLOCK_BEGIN = "<!-- @sechroom/cli:begin (managed \u2014 re-run `sechroom setup agent-files` to refresh) -->";
|
|
1865
|
-
var BLOCK_END = "<!-- @sechroom/cli:end -->";
|
|
1866
2469
|
function ensureDir2(path) {
|
|
1867
|
-
|
|
2470
|
+
mkdirSync4(dirname5(path), { recursive: true });
|
|
1868
2471
|
}
|
|
1869
2472
|
function readOr(path, fallback) {
|
|
1870
2473
|
try {
|
|
1871
|
-
return
|
|
2474
|
+
return readFileSync4(path, "utf8");
|
|
1872
2475
|
} catch {
|
|
1873
2476
|
return fallback;
|
|
1874
2477
|
}
|
|
1875
2478
|
}
|
|
1876
2479
|
function mergeMcpJson(path, snippet, dryRun) {
|
|
1877
2480
|
const incoming = JSON.parse(snippet);
|
|
1878
|
-
const existed =
|
|
2481
|
+
const existed = existsSync5(path);
|
|
1879
2482
|
let current = {};
|
|
1880
2483
|
if (existed) {
|
|
1881
2484
|
try {
|
|
1882
|
-
current = JSON.parse(
|
|
2485
|
+
current = JSON.parse(readFileSync4(path, "utf8"));
|
|
1883
2486
|
} catch {
|
|
1884
2487
|
return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
|
|
1885
2488
|
}
|
|
@@ -1887,46 +2490,88 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
1887
2490
|
current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
|
|
1888
2491
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
1889
2492
|
ensureDir2(path);
|
|
1890
|
-
|
|
2493
|
+
writeFileSync4(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
1891
2494
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
1892
2495
|
}
|
|
1893
2496
|
function mergeCodexToml(path, snippet, dryRun) {
|
|
1894
|
-
const existed =
|
|
2497
|
+
const existed = existsSync5(path);
|
|
1895
2498
|
let body = readOr(path, "");
|
|
1896
2499
|
body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
|
|
1897
2500
|
const trimmed = body.trim();
|
|
1898
2501
|
const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
|
|
1899
2502
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
1900
2503
|
ensureDir2(path);
|
|
1901
|
-
|
|
2504
|
+
writeFileSync4(path, next, { mode: 384 });
|
|
1902
2505
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
1903
2506
|
}
|
|
1904
|
-
function writeInstructionBlock(path,
|
|
1905
|
-
const
|
|
1906
|
-
|
|
1907
|
-
${BLOCK_END}
|
|
1908
|
-
`;
|
|
1909
|
-
const existed = existsSync2(path);
|
|
1910
|
-
const current = readOr(path, "");
|
|
1911
|
-
let next;
|
|
1912
|
-
const re = new RegExp(`${escapeRe(BLOCK_BEGIN)}[\\s\\S]*?${escapeRe(BLOCK_END)}\\n?`);
|
|
1913
|
-
if (re.test(current)) {
|
|
1914
|
-
next = current.replace(re, block);
|
|
1915
|
-
} else {
|
|
1916
|
-
next = current.trim().length > 0 ? `${current.trimEnd()}
|
|
1917
|
-
|
|
1918
|
-
${block}` : block;
|
|
1919
|
-
}
|
|
2507
|
+
function writeInstructionBlock(path, write, dryRun) {
|
|
2508
|
+
const existed = existsSync5(path);
|
|
2509
|
+
const next = computeBlockFile(readOr(path, ""), write);
|
|
1920
2510
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
1921
2511
|
ensureDir2(path);
|
|
1922
|
-
|
|
2512
|
+
writeFileSync4(path, next);
|
|
1923
2513
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
1924
2514
|
}
|
|
2515
|
+
function computeBlockFile(current, write) {
|
|
2516
|
+
const rendered = renderBlock(write);
|
|
2517
|
+
const keyed = keyedBlockRe(write.block);
|
|
2518
|
+
if (keyed.test(current)) return current.replace(keyed, rendered);
|
|
2519
|
+
if (write.block === "role-template" && legacyBlockRe().test(current)) {
|
|
2520
|
+
return current.replace(legacyBlockRe(), rendered);
|
|
2521
|
+
}
|
|
2522
|
+
return current.trim().length > 0 ? `${current.trimEnd()}
|
|
2523
|
+
|
|
2524
|
+
${rendered}` : rendered;
|
|
2525
|
+
}
|
|
2526
|
+
function evaluateBlock(content, block, serverBody) {
|
|
2527
|
+
const onDisk = parseManagedBlock(content, block);
|
|
2528
|
+
if (!onDisk) return "absent";
|
|
2529
|
+
const actual = bodySha256(onDisk.body);
|
|
2530
|
+
if (onDisk.sha256 && actual !== onDisk.sha256) return "drift";
|
|
2531
|
+
return actual === bodySha256(serverBody) ? "current" : "stale";
|
|
2532
|
+
}
|
|
2533
|
+
function applyBlock(path, write, mode, dryRun) {
|
|
2534
|
+
const current = readOr(path, "");
|
|
2535
|
+
const state = evaluateBlock(current, write.block, write.body);
|
|
2536
|
+
if (mode === "check") {
|
|
2537
|
+
return {
|
|
2538
|
+
kind: "instruction",
|
|
2539
|
+
path,
|
|
2540
|
+
status: state === "current" ? "current" : "skipped",
|
|
2541
|
+
eval: state,
|
|
2542
|
+
note: state === "current" ? void 0 : `would ${state === "absent" ? "write" : "refresh"} (${state})`
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
if (state === "current") {
|
|
2546
|
+
return { kind: "instruction", path, status: "current", eval: "current" };
|
|
2547
|
+
}
|
|
2548
|
+
if (state === "drift" && mode !== "force") {
|
|
2549
|
+
const proposedPath = `${path}.proposed`;
|
|
2550
|
+
const next = computeBlockFile(current, write);
|
|
2551
|
+
if (!dryRun) {
|
|
2552
|
+
ensureDir2(proposedPath);
|
|
2553
|
+
writeFileSync4(proposedPath, next);
|
|
2554
|
+
}
|
|
2555
|
+
return {
|
|
2556
|
+
kind: "instruction",
|
|
2557
|
+
path,
|
|
2558
|
+
status: "skipped",
|
|
2559
|
+
eval: "drift",
|
|
2560
|
+
proposedPath,
|
|
2561
|
+
note: `local edits \u2014 wrote ${proposedPath} (original left untouched)`
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
const action = writeInstructionBlock(path, write, dryRun);
|
|
2565
|
+
const note = state === "stale" ? "refreshed" : state === "drift" ? "overwrote local edits" : void 0;
|
|
2566
|
+
return { ...action, eval: state, note: note ?? action.note };
|
|
2567
|
+
}
|
|
1925
2568
|
function escapeRe(s) {
|
|
1926
2569
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1927
2570
|
}
|
|
1928
2571
|
async function applyClient(cfg, setup, target, opts) {
|
|
1929
2572
|
const actions = [];
|
|
2573
|
+
const mode = opts.mode ?? "apply";
|
|
2574
|
+
const dryRun = opts.dryRun || mode === "check";
|
|
1930
2575
|
if (opts.mcp && target.mcp) {
|
|
1931
2576
|
const surface = findSurface(setup, target.mcp.surfaceKey);
|
|
1932
2577
|
const section = findSection(surface, target.mcp.sectionType);
|
|
@@ -1935,7 +2580,7 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
1935
2580
|
actions.push({ kind: "mcp", path: target.mcp.path, status: "skipped", note: `no ${target.mcp.sectionType} section on surface '${target.mcp.surfaceKey}'` });
|
|
1936
2581
|
} else {
|
|
1937
2582
|
actions.push(
|
|
1938
|
-
target.mcp.format === "toml" ? mergeCodexToml(target.mcp.path, snippet,
|
|
2583
|
+
target.mcp.format === "toml" ? mergeCodexToml(target.mcp.path, snippet, dryRun) : mergeMcpJson(target.mcp.path, snippet, dryRun)
|
|
1939
2584
|
);
|
|
1940
2585
|
}
|
|
1941
2586
|
}
|
|
@@ -1949,113 +2594,153 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
1949
2594
|
if (!resolved) {
|
|
1950
2595
|
actions.push({ kind: "instruction", path: target.instruction.path, status: "skipped", note: "no role template found in this tenant \u2014 install the SEM Starter bundle, then re-run `sechroom setup agent-files`" });
|
|
1951
2596
|
} else {
|
|
1952
|
-
const action =
|
|
1953
|
-
|
|
2597
|
+
const action = applyBlock(
|
|
2598
|
+
target.instruction.path,
|
|
2599
|
+
{ block: "role-template", body: resolved.body, source: resolved.sourceRef },
|
|
2600
|
+
mode,
|
|
2601
|
+
opts.dryRun
|
|
2602
|
+
);
|
|
2603
|
+
actions.push(resolved.source === "override" && action.status !== "current" ? { ...action, note: action.note ?? "your personal copy" } : action);
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
const conventionsSection = findSection(surface, SectionType.WorkspaceConventions);
|
|
2607
|
+
if (conventionsSection) {
|
|
2608
|
+
const conventions = await resolveWorkspaceConventions(cfg, conventionsSection);
|
|
2609
|
+
if (conventions) {
|
|
2610
|
+
const action = applyBlock(
|
|
2611
|
+
target.instruction.path,
|
|
2612
|
+
{ block: "workspace-conventions", body: conventions.body, source: `workspace:${cfg.workspaceId ?? ""}` },
|
|
2613
|
+
mode,
|
|
2614
|
+
opts.dryRun
|
|
2615
|
+
);
|
|
2616
|
+
actions.push(action.status === "current" ? action : { ...action, note: action.note ?? `workspace conventions (${conventions.refs.length})` });
|
|
1954
2617
|
}
|
|
1955
2618
|
}
|
|
1956
2619
|
}
|
|
1957
2620
|
return actions;
|
|
1958
2621
|
}
|
|
1959
2622
|
|
|
1960
|
-
// src/setup/
|
|
1961
|
-
import {
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
|
|
1987
|
-
instruction: { surfaceKey: "claude-desktop", path: join2(home, ".claude", "CLAUDE.md") }
|
|
1988
|
-
},
|
|
1989
|
-
codex: {
|
|
1990
|
-
key: "codex",
|
|
1991
|
-
label: "Codex CLI",
|
|
1992
|
-
mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join2(home, ".codex", "config.toml"), format: "toml" },
|
|
1993
|
-
instruction: { surfaceKey: "chatgpt", path: join2(cwd, "AGENTS.md") }
|
|
1994
|
-
},
|
|
1995
|
-
cursor: {
|
|
1996
|
-
key: "cursor",
|
|
1997
|
-
label: "Cursor",
|
|
1998
|
-
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join2(cwd, ".cursor", "mcp.json"), format: "json" },
|
|
1999
|
-
instruction: { surfaceKey: "chatgpt", path: join2(cwd, "AGENTS.md") }
|
|
2623
|
+
// src/setup/hooks-offer.ts
|
|
2624
|
+
import { homedir as homedir4 } from "os";
|
|
2625
|
+
async function maybeOfferHooks(opts) {
|
|
2626
|
+
if (opts.dryRun) return;
|
|
2627
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2628
|
+
const surfaces = detectHookSurfaces(cwd);
|
|
2629
|
+
if (surfaces.length === 0) return;
|
|
2630
|
+
const names = surfaces.map((s) => HOOK_SURFACE_LABEL[s]).join(" + ");
|
|
2631
|
+
process.stderr.write(
|
|
2632
|
+
`
|
|
2633
|
+
Sechroom can wire continuity lifecycle hooks into ${style.bold(names)} so your agent
|
|
2634
|
+
auto-resumes where you left off and checkpoints working state before compacting.
|
|
2635
|
+
`
|
|
2636
|
+
);
|
|
2637
|
+
const install = opts.yes ? true : canPrompt() ? await promptYesNo(`Install the continuity hooks for ${names}?`) : false;
|
|
2638
|
+
if (!install) return;
|
|
2639
|
+
try {
|
|
2640
|
+
const installed = installHookSurfaces(surfaces, { dryRun: false, cwd, home: homedir4() });
|
|
2641
|
+
let changed = false;
|
|
2642
|
+
for (const { surface, results } of installed) {
|
|
2643
|
+
for (const r of results) {
|
|
2644
|
+
if (r.status !== "current") changed = true;
|
|
2645
|
+
const verb = r.status === "current" ? "already configured" : r.status === "created" ? "created" : "updated";
|
|
2646
|
+
process.stderr.write(`${style.green("\u2713")} ${HOOK_SURFACE_LABEL[surface]}: ${r.path} (${verb})
|
|
2647
|
+
`);
|
|
2648
|
+
}
|
|
2000
2649
|
}
|
|
2001
|
-
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
if (existsSync3(join2(home, ".codex"))) detected.push("codex");
|
|
2011
|
-
if (existsSync3(join2(home, ".cursor")) || existsSync3(join2(cwd, ".cursor"))) detected.push("cursor");
|
|
2012
|
-
return detected;
|
|
2650
|
+
if (changed) {
|
|
2651
|
+
process.stderr.write(`${style.dim("Restart (or reload) your agent for the hooks to take effect.")}
|
|
2652
|
+
`);
|
|
2653
|
+
}
|
|
2654
|
+
warnIfSechroomNotOnPath();
|
|
2655
|
+
} catch (err2) {
|
|
2656
|
+
process.stderr.write(`${style.dim(`(skipped hook install: ${err2.message})`)}
|
|
2657
|
+
`);
|
|
2658
|
+
}
|
|
2013
2659
|
}
|
|
2014
2660
|
|
|
2015
2661
|
// src/setup/skills-offer.ts
|
|
2016
|
-
import { mkdirSync as
|
|
2017
|
-
import { homedir as
|
|
2018
|
-
import { join as
|
|
2662
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
2663
|
+
import { homedir as homedir5 } from "os";
|
|
2664
|
+
import { join as join5 } from "path";
|
|
2019
2665
|
|
|
2020
|
-
// src/
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2666
|
+
// src/setup/lane-pin.ts
|
|
2667
|
+
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
2668
|
+
"claude-code": "claude-code",
|
|
2669
|
+
"claude-desktop": "claude-code",
|
|
2670
|
+
cursor: "claude-code",
|
|
2671
|
+
codex: "codex"
|
|
2672
|
+
};
|
|
2673
|
+
var CLIENT_PRIORITY = ["claude-code", "claude-desktop", "cursor", "codex"];
|
|
2674
|
+
function handleFromDisplayName(name) {
|
|
2675
|
+
if (!name) return void 0;
|
|
2676
|
+
const localPart = name.trim().split("@")[0] ?? "";
|
|
2677
|
+
const first = localPart.split(/[\s._-]+/)[0]?.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2678
|
+
return first || void 0;
|
|
2026
2679
|
}
|
|
2027
|
-
function
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
const candidate = join3(dir, SEM_FILE);
|
|
2031
|
-
if (existsSync4(candidate)) return candidate;
|
|
2032
|
-
const parent = dirname4(dir);
|
|
2033
|
-
if (parent === dir) return void 0;
|
|
2034
|
-
dir = parent;
|
|
2035
|
-
}
|
|
2680
|
+
function codeLanePrefix(clients) {
|
|
2681
|
+
for (const c of CLIENT_PRIORITY) if (clients.includes(c)) return CODE_LANE_PREFIX_BY_CLIENT[c];
|
|
2682
|
+
return "claude-code";
|
|
2036
2683
|
}
|
|
2037
|
-
function
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2684
|
+
async function inferLanes(cfg, clients) {
|
|
2685
|
+
let wf;
|
|
2686
|
+
let profile;
|
|
2687
|
+
try {
|
|
2688
|
+
const client = await makeClient(cfg);
|
|
2689
|
+
[wf, profile] = await Promise.all([
|
|
2690
|
+
client.GET("/me/workflow-preferences", {}).then((r) => r.data).catch(() => void 0),
|
|
2691
|
+
client.GET("/me/profile", {}).then((r) => r.data).catch(() => void 0)
|
|
2692
|
+
]);
|
|
2693
|
+
} catch {
|
|
2047
2694
|
}
|
|
2048
|
-
|
|
2695
|
+
const handle = handleFromDisplayName(profile?.effectiveDisplayName);
|
|
2696
|
+
const prefix = codeLanePrefix(clients ?? ["claude-code"]);
|
|
2697
|
+
return {
|
|
2698
|
+
code: process.env.SECHROOM_CODE_LANE ?? wf?.defaultCodeLane ?? (handle ? `${prefix}-${handle}` : void 0),
|
|
2699
|
+
design: process.env.SECHROOM_DESIGN_LANE ?? wf?.defaultDesignLane ?? (handle ? `claude-design-${handle}` : void 0)
|
|
2700
|
+
};
|
|
2049
2701
|
}
|
|
2050
|
-
function
|
|
2051
|
-
const
|
|
2052
|
-
|
|
2053
|
-
|
|
2702
|
+
function writePin(code, design) {
|
|
2703
|
+
const values = {};
|
|
2704
|
+
if (code) values["code-lane"] = code;
|
|
2705
|
+
if (design) values["design-lane"] = design;
|
|
2706
|
+
if (Object.keys(values).length === 0) return;
|
|
2707
|
+
const target = writeSem(values);
|
|
2708
|
+
process.stderr.write(`${ok("\u2713")} lane pin written \u2192 ${target} ${style.dim("(./.sechroom/lane.json, git-ignored)")}
|
|
2709
|
+
`);
|
|
2054
2710
|
}
|
|
2055
|
-
function
|
|
2056
|
-
|
|
2057
|
-
if (
|
|
2058
|
-
|
|
2711
|
+
async function ensureLanePin(cfg, opts) {
|
|
2712
|
+
if (opts.dryRun) return;
|
|
2713
|
+
if (readSem()) return;
|
|
2714
|
+
const { code: codeGuess, design: designGuess } = await inferLanes(cfg, opts.clients);
|
|
2715
|
+
if (!canPrompt() || opts.yes) {
|
|
2716
|
+
if (opts.yes && (codeGuess || designGuess)) writePin(codeGuess, designGuess);
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
if (codeGuess || designGuess) {
|
|
2720
|
+
process.stderr.write(
|
|
2721
|
+
`
|
|
2722
|
+
I can pin this checkout's lane so operator skills + the continuity hook resolve your identity:
|
|
2723
|
+
`
|
|
2724
|
+
);
|
|
2725
|
+
if (codeGuess) process.stderr.write(` ${style.dim("code-lane")} = ${style.cyan(codeGuess)}
|
|
2726
|
+
`);
|
|
2727
|
+
if (designGuess) process.stderr.write(` ${style.dim("design-lane")} = ${style.cyan(designGuess)}
|
|
2728
|
+
`);
|
|
2729
|
+
if (await promptYesNo("Pin these?")) {
|
|
2730
|
+
writePin(codeGuess, designGuess);
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
const code = await promptText("Code-lane id (e.g. claude-code-you, blank to skip)?", codeGuess ?? "");
|
|
2735
|
+
const design = await promptText("Design-lane id (e.g. claude-design-you, blank to skip)?", designGuess ?? "");
|
|
2736
|
+
if (!code && !design) {
|
|
2737
|
+
process.stderr.write(
|
|
2738
|
+
` ${style.dim("skipped \u2014 set later with")} ${style.cyan("sechroom skills set-lane --code-lane \u2026 --design-lane \u2026")}
|
|
2739
|
+
`
|
|
2740
|
+
);
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
writePin(code || void 0, design || void 0);
|
|
2059
2744
|
}
|
|
2060
2745
|
|
|
2061
2746
|
// src/setup/skills-offer.ts
|
|
@@ -2096,43 +2781,19 @@ async function maybeOfferSkills(cfg, personalWorkspaceId, opts) {
|
|
|
2096
2781
|
Found ${style.bold(String(names.length))} operator skill(s) installed in your workspace: ${names.join(", ")}.
|
|
2097
2782
|
`
|
|
2098
2783
|
);
|
|
2099
|
-
const dir =
|
|
2784
|
+
const dir = join5(homedir5(), ".claude", "skills");
|
|
2100
2785
|
const materialise = opts.yes ? true : canPrompt() ? await promptYesNo(`Write them to ${dir}/ so ${surface} can use them?`) : false;
|
|
2101
2786
|
if (!materialise) return;
|
|
2102
2787
|
const written = [];
|
|
2103
2788
|
for (const [name, m] of byName) {
|
|
2104
2789
|
const body = m.text ?? m.Text ?? "";
|
|
2105
|
-
|
|
2106
|
-
|
|
2790
|
+
mkdirSync5(join5(dir, name), { recursive: true });
|
|
2791
|
+
writeFileSync5(join5(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
|
|
2107
2792
|
written.push(name);
|
|
2108
2793
|
}
|
|
2109
2794
|
process.stderr.write(`${style.green("\u2713")} wrote ${written.length} skill(s) to ${dir}
|
|
2110
2795
|
`);
|
|
2111
|
-
|
|
2112
|
-
const setLane = opts.yes ? false : canPrompt() ? await promptYesNo("Set your lane now so the skills can resolve their identity slots?") : false;
|
|
2113
|
-
if (!setLane) {
|
|
2114
|
-
process.stderr.write(
|
|
2115
|
-
` ${style.dim("run")} ${style.cyan("sechroom skills set-lane --code-lane \u2026 --design-lane \u2026")} ${style.dim("when ready.")}
|
|
2116
|
-
`
|
|
2117
|
-
);
|
|
2118
|
-
return;
|
|
2119
|
-
}
|
|
2120
|
-
let wf;
|
|
2121
|
-
try {
|
|
2122
|
-
const client = await makeClient(cfg);
|
|
2123
|
-
wf = await client.GET("/me/workflow-preferences", {}).then((r) => r.data).catch(() => void 0);
|
|
2124
|
-
} catch {
|
|
2125
|
-
}
|
|
2126
|
-
const code = await promptText("Code-lane id (e.g. claude-code-you)?", wf?.defaultCodeLane);
|
|
2127
|
-
const design = await promptText("Design-lane id (e.g. claude-design-you)?", wf?.defaultDesignLane);
|
|
2128
|
-
const values = {};
|
|
2129
|
-
if (code) values["code-lane"] = code;
|
|
2130
|
-
if (design) values["design-lane"] = design;
|
|
2131
|
-
if (Object.keys(values).length === 0) return;
|
|
2132
|
-
const target = localSemPath();
|
|
2133
|
-
writeFileSync3(target, serializeSem(values));
|
|
2134
|
-
process.stderr.write(`${style.green("\u2713")} lane pin written \u2192 ${target}
|
|
2135
|
-
`);
|
|
2796
|
+
await ensureLanePin(cfg, { yes: opts.yes, dryRun: opts.dryRun, clients: [surface] });
|
|
2136
2797
|
}
|
|
2137
2798
|
|
|
2138
2799
|
// src/commands/setup.ts
|
|
@@ -2224,6 +2885,9 @@ Examples:
|
|
|
2224
2885
|
if (!json && !opts.dryRun && !opts.mcpOnly) {
|
|
2225
2886
|
await maybeOfferSkills(cfg, personalWorkspaceId, { yes: false, dryRun: Boolean(opts.dryRun), surface: "claude-code" });
|
|
2226
2887
|
}
|
|
2888
|
+
if (!json && !opts.dryRun && !opts.mcpOnly) {
|
|
2889
|
+
await maybeOfferHooks({ yes: false, dryRun: Boolean(opts.dryRun), cwd: process.cwd() });
|
|
2890
|
+
}
|
|
2227
2891
|
if (json) {
|
|
2228
2892
|
emit({ dryRun: Boolean(opts.dryRun), clients: result }, true);
|
|
2229
2893
|
return;
|
|
@@ -2243,36 +2907,147 @@ Next \u2014 verify: ${verify.description}
|
|
|
2243
2907
|
}
|
|
2244
2908
|
function registerSetup(program2) {
|
|
2245
2909
|
const setup = program2.command("setup").description("Granular onboarding steps (init runs these together)");
|
|
2246
|
-
setup.command("mcp <
|
|
2247
|
-
await
|
|
2910
|
+
setup.command("mcp <clients...>").description(`Write only the MCP config for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`).option("--dry-run", "print what would be written without writing", false).addHelpText("after", "\nExamples:\n $ sechroom setup mcp codex\n $ sechroom setup mcp claude-code codex\n $ sechroom setup mcp all").action(async (clients, opts, cmd) => {
|
|
2911
|
+
await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: true, agentFiles: false });
|
|
2248
2912
|
});
|
|
2249
|
-
setup.command("agent-files <
|
|
2250
|
-
await
|
|
2913
|
+
setup.command("agent-files <clients...>").description(`Write only the agent instruction file(s) for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`).option("--dry-run", "print what would be written without writing", false).option("--copy", "make a personal copy you can edit (default: prompt on a TTY, else skip)").addHelpText("after", "\nExamples:\n $ sechroom setup agent-files claude-code CLAUDE.md\n $ sechroom setup agent-files claude-code codex CLAUDE.md + AGENTS.md in one run\n $ sechroom setup agent-files all").action(async (clients, opts, cmd) => {
|
|
2914
|
+
await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: false, agentFiles: true, copy: opts.copy });
|
|
2251
2915
|
});
|
|
2252
2916
|
}
|
|
2253
|
-
async function
|
|
2917
|
+
async function runClients(clients, cmd, opts) {
|
|
2254
2918
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2255
2919
|
const targets = clientTargets(process.cwd());
|
|
2256
|
-
const
|
|
2257
|
-
if (!target) fail(`unknown client '${client}'. Known: ${ALL_CLIENT_KEYS.join(", ")}.`);
|
|
2920
|
+
const keys = resolveClientKeys(clients.join(","));
|
|
2258
2921
|
const setupData = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
|
|
2259
2922
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
2260
2923
|
if (opts.agentFiles && !opts.dryRun) {
|
|
2261
|
-
await maybeOfferCopies(cfg, setupData, targets,
|
|
2924
|
+
await maybeOfferCopies(cfg, setupData, targets, keys, personalWorkspaceId, copyChoice(opts));
|
|
2262
2925
|
}
|
|
2263
|
-
const actions = await applyClient(cfg, setupData, target, {
|
|
2264
|
-
dryRun: opts.dryRun,
|
|
2265
|
-
mcp: opts.mcp,
|
|
2266
|
-
agentFiles: opts.agentFiles,
|
|
2267
|
-
personalWorkspaceId
|
|
2268
|
-
});
|
|
2269
2926
|
const json = cmd.optsWithGlobals().json;
|
|
2927
|
+
const result = [];
|
|
2928
|
+
for (const key of keys) {
|
|
2929
|
+
const target = targets[key];
|
|
2930
|
+
const actions = await applyClient(cfg, setupData, target, {
|
|
2931
|
+
dryRun: opts.dryRun,
|
|
2932
|
+
mcp: opts.mcp,
|
|
2933
|
+
agentFiles: opts.agentFiles,
|
|
2934
|
+
personalWorkspaceId
|
|
2935
|
+
});
|
|
2936
|
+
result.push({ client: key, actions });
|
|
2937
|
+
if (!json) printActions(target, actions);
|
|
2938
|
+
}
|
|
2270
2939
|
if (json) {
|
|
2271
|
-
emit({ dryRun: opts.dryRun,
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2940
|
+
emit({ dryRun: opts.dryRun, clients: result }, true);
|
|
2941
|
+
return;
|
|
2942
|
+
}
|
|
2943
|
+
process.stdout.write(opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n");
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
// src/commands/onboard.ts
|
|
2947
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2948
|
+
import { join as join7 } from "path";
|
|
2949
|
+
|
|
2950
|
+
// src/commands/fanout.ts
|
|
2951
|
+
import { spawnSync } from "child_process";
|
|
2952
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync, statSync } from "fs";
|
|
2953
|
+
import { isAbsolute, join as join6, resolve } from "path";
|
|
2954
|
+
var ICON = {
|
|
2955
|
+
refresh: "\u21BB",
|
|
2956
|
+
bind: "+",
|
|
2957
|
+
"skip-missing": "\u2013",
|
|
2958
|
+
"skip-unbound": "\u26A0"
|
|
2959
|
+
};
|
|
2960
|
+
function resolveChildDir(path, root) {
|
|
2961
|
+
return isAbsolute(path) ? path : resolve(root, path);
|
|
2962
|
+
}
|
|
2963
|
+
function discoverChildren(root) {
|
|
2964
|
+
let names;
|
|
2965
|
+
try {
|
|
2966
|
+
names = readdirSync(root);
|
|
2967
|
+
} catch {
|
|
2968
|
+
return [];
|
|
2275
2969
|
}
|
|
2970
|
+
const out = [];
|
|
2971
|
+
for (const name of names.sort()) {
|
|
2972
|
+
if (name.startsWith(".") || name === "node_modules") continue;
|
|
2973
|
+
const dir = join6(root, name);
|
|
2974
|
+
try {
|
|
2975
|
+
if (!statSync(dir).isDirectory()) continue;
|
|
2976
|
+
} catch {
|
|
2977
|
+
continue;
|
|
2978
|
+
}
|
|
2979
|
+
if (existsSync6(join6(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
2980
|
+
}
|
|
2981
|
+
return out;
|
|
2982
|
+
}
|
|
2983
|
+
function readManifest(path) {
|
|
2984
|
+
if (!existsSync6(path)) return null;
|
|
2985
|
+
let parsed;
|
|
2986
|
+
try {
|
|
2987
|
+
parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
2988
|
+
} catch (err2) {
|
|
2989
|
+
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
2990
|
+
}
|
|
2991
|
+
return Array.isArray(parsed.repos) ? parsed.repos.filter((r) => r && typeof r.path === "string") : [];
|
|
2992
|
+
}
|
|
2993
|
+
function passthroughGlobals(g) {
|
|
2994
|
+
const out = [];
|
|
2995
|
+
if (g.baseUrl) out.push("--base-url", g.baseUrl);
|
|
2996
|
+
if (g.tenant) out.push("--tenant", g.tenant);
|
|
2997
|
+
return out;
|
|
2998
|
+
}
|
|
2999
|
+
function runChildren(plans, o) {
|
|
3000
|
+
const { globals, dryRun, json } = o;
|
|
3001
|
+
const results = [];
|
|
3002
|
+
for (const plan of plans) {
|
|
3003
|
+
const runs = plan.argv.length > 0;
|
|
3004
|
+
const argv = runs ? [...globals, ...plan.argv] : [];
|
|
3005
|
+
if (!json) {
|
|
3006
|
+
process.stderr.write(` ${ICON[plan.disposition]} ${style.cyan(plan.label)} ${style.dim(plan.reason)}
|
|
3007
|
+
`);
|
|
3008
|
+
if (runs && dryRun) process.stderr.write(` ${style.dim(`would run: sechroom ${argv.join(" ")}`)}
|
|
3009
|
+
`);
|
|
3010
|
+
}
|
|
3011
|
+
let exitCode = runs ? 0 : null;
|
|
3012
|
+
if (runs && !dryRun) {
|
|
3013
|
+
const res = spawnSync(process.execPath, [process.argv[1], ...argv], {
|
|
3014
|
+
cwd: plan.dir,
|
|
3015
|
+
stdio: json ? "ignore" : "inherit"
|
|
3016
|
+
});
|
|
3017
|
+
exitCode = res.status;
|
|
3018
|
+
if (!json) {
|
|
3019
|
+
process.stderr.write(
|
|
3020
|
+
exitCode === 0 ? ` ${ok("\u2713")} ${style.dim("onboard ok")}
|
|
3021
|
+
` : ` ${warn("\u2717")} ${style.dim(`onboard exited ${exitCode ?? "signal"}`)}
|
|
3022
|
+
`
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
results.push({
|
|
3027
|
+
path: plan.label,
|
|
3028
|
+
dir: plan.dir,
|
|
3029
|
+
disposition: plan.disposition,
|
|
3030
|
+
ran: runs && !dryRun,
|
|
3031
|
+
exitCode,
|
|
3032
|
+
reason: plan.reason
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
3035
|
+
return results;
|
|
3036
|
+
}
|
|
3037
|
+
function summarizeFanout(results, o) {
|
|
3038
|
+
const ran = results.filter((r) => r.ran);
|
|
3039
|
+
const failed = ran.filter((r) => r.exitCode !== 0);
|
|
3040
|
+
const skipped = results.filter((r) => r.disposition.startsWith("skip"));
|
|
3041
|
+
const wouldRun = results.filter((r) => !r.disposition.startsWith("skip"));
|
|
3042
|
+
const tally = (o.dryRun ? [wouldRun.length ? `${wouldRun.length} would onboard` : null, skipped.length ? `${skipped.length} would skip` : null] : [
|
|
3043
|
+
ran.length ? `${ran.length - failed.length}/${ran.length} onboarded` : null,
|
|
3044
|
+
skipped.length ? `${skipped.length} skipped` : null,
|
|
3045
|
+
failed.length ? `${failed.length} failed` : null
|
|
3046
|
+
]).filter(Boolean).join(", ");
|
|
3047
|
+
process.stderr.write(`
|
|
3048
|
+
${failed.length ? warn("\u26A0") : ok("\u2713")} ${tally || "nothing to do"}${o.dryRun ? style.dim(" (dry run)") : ""}
|
|
3049
|
+
`);
|
|
3050
|
+
if (failed.length) process.exit(1);
|
|
2276
3051
|
}
|
|
2277
3052
|
|
|
2278
3053
|
// src/commands/onboard.ts
|
|
@@ -2284,41 +3059,197 @@ function systemTimezone() {
|
|
|
2284
3059
|
return "UTC";
|
|
2285
3060
|
}
|
|
2286
3061
|
}
|
|
2287
|
-
|
|
3062
|
+
function editDistance(a, b) {
|
|
3063
|
+
const m = a.length;
|
|
3064
|
+
const n = b.length;
|
|
3065
|
+
if (m === 0) return n;
|
|
3066
|
+
if (n === 0) return m;
|
|
3067
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
3068
|
+
for (let i = 1; i <= m; i++) {
|
|
3069
|
+
const curr = [i];
|
|
3070
|
+
for (let j = 1; j <= n; j++) {
|
|
3071
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
3072
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
3073
|
+
}
|
|
3074
|
+
prev = curr;
|
|
3075
|
+
}
|
|
3076
|
+
return prev[n];
|
|
3077
|
+
}
|
|
3078
|
+
function namesCollide(a, b) {
|
|
3079
|
+
const x = a.trim().toLowerCase();
|
|
3080
|
+
const y = b.trim().toLowerCase();
|
|
3081
|
+
return x === y || editDistance(x, y) <= 1;
|
|
3082
|
+
}
|
|
3083
|
+
function workspacePath(ws, byId) {
|
|
3084
|
+
const parts = [];
|
|
3085
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3086
|
+
let cur = ws;
|
|
3087
|
+
while (cur && !seen.has(cur.id)) {
|
|
3088
|
+
seen.add(cur.id);
|
|
3089
|
+
parts.unshift(cur.name);
|
|
3090
|
+
cur = cur.parentId ? byId.get(cur.parentId) : void 0;
|
|
3091
|
+
}
|
|
3092
|
+
return parts.join(" / ");
|
|
3093
|
+
}
|
|
3094
|
+
function resolveBaseUrl(g) {
|
|
2288
3095
|
const persisted = readPersisted();
|
|
2289
3096
|
const local = readLocalConfig();
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
3097
|
+
const baseUrl = g.baseUrl ?? process.env.SECHROOM_BASE_URL ?? local.baseUrl ?? persisted.baseUrl ?? DEFAULT_BASE_URL2;
|
|
3098
|
+
return baseUrl.replace(/\/$/, "");
|
|
3099
|
+
}
|
|
3100
|
+
async function fetchWorkspaces(client) {
|
|
3101
|
+
const { data, error } = await client.GET("/workspaces", { params: { query: { includeArchived: false } } });
|
|
3102
|
+
if (error) throw new Error(`Couldn't list your workspaces: ${JSON.stringify(error)}`);
|
|
3103
|
+
const rows = data ?? [];
|
|
3104
|
+
return rows.map((r) => r.item ?? r).filter((w) => Boolean(w?.id && w?.name)).map((w) => ({ id: w.id, name: w.name, parentId: w.parentId ?? null }));
|
|
3105
|
+
}
|
|
3106
|
+
async function lookupWorkspace(client, id) {
|
|
3107
|
+
const { data, error } = await client.GET("/workspaces/{workspaceId}", { params: { path: { workspaceId: id } } });
|
|
3108
|
+
if (error) return null;
|
|
3109
|
+
const env = data;
|
|
3110
|
+
const w = env?.item ?? env;
|
|
3111
|
+
return w?.id ? { id: w.id, name: w.name ?? id, parentId: w.parentId ?? null } : null;
|
|
3112
|
+
}
|
|
3113
|
+
async function warnIfProjectStray(client, projectId, workspaceId, json) {
|
|
3114
|
+
const { data, error } = await client.GET("/projects/{projectId}", { params: { path: { projectId } } });
|
|
3115
|
+
if (error) return;
|
|
3116
|
+
const env = data;
|
|
3117
|
+
const owner = env?.item?.workspaceId;
|
|
3118
|
+
if (owner && owner !== workspaceId && !json) {
|
|
3119
|
+
process.stderr.write(
|
|
3120
|
+
`${warn("\u26A0")} defaultProjectId ${style.dim(projectId)} belongs to a different workspace (${style.dim(owner)}), not ${style.dim(workspaceId)} \u2014 leaving it as-is.
|
|
3121
|
+
`
|
|
3122
|
+
);
|
|
2294
3123
|
}
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
3124
|
+
}
|
|
3125
|
+
async function pickWorkspace(client, promptLabel = "Bind this directory to a workspace:") {
|
|
3126
|
+
const all = await withSpinner("Listing your workspaces", () => fetchWorkspaces(client));
|
|
3127
|
+
if (all.length === 0) {
|
|
3128
|
+
process.stderr.write(`no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
|
|
3129
|
+
`);
|
|
3130
|
+
return void 0;
|
|
3131
|
+
}
|
|
3132
|
+
const byId = new Map(all.map((w) => [w.id, w]));
|
|
3133
|
+
let pool = all;
|
|
3134
|
+
if (all.length > 12) {
|
|
3135
|
+
const q = (await promptText(`Filter ${all.length} workspaces (substring, Enter to list all)?`, "")).trim().toLowerCase();
|
|
3136
|
+
if (q) {
|
|
3137
|
+
const hits = all.filter((w) => `${w.name} ${workspacePath(w, byId)}`.toLowerCase().includes(q));
|
|
3138
|
+
if (hits.length > 0) pool = hits;
|
|
3139
|
+
else process.stderr.write(`no match for "${q}" \u2014 listing all
|
|
3140
|
+
`);
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
const SKIP = "__skip__";
|
|
3144
|
+
const choices = [
|
|
3145
|
+
...pool.slice().sort((a, b) => workspacePath(a, byId).localeCompare(workspacePath(b, byId))).map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: w.id })),
|
|
3146
|
+
{ label: style.dim("skip \u2014 don't bind a workspace"), value: SKIP, hint: void 0 }
|
|
3147
|
+
];
|
|
3148
|
+
const chosen = await promptSelect(promptLabel, choices, SKIP);
|
|
3149
|
+
if (chosen === SKIP) return void 0;
|
|
3150
|
+
const picked = byId.get(chosen);
|
|
3151
|
+
const collisions = all.filter((w) => w.id !== picked.id && namesCollide(w.name, picked.name));
|
|
3152
|
+
if (collisions.length > 0) {
|
|
3153
|
+
process.stderr.write(
|
|
3154
|
+
`${warn("\u26A0")} ${collisions.length} other workspace(s) have a similar name to ${style.cyan(workspacePath(picked, byId))}:
|
|
3155
|
+
` + collisions.map((w) => ` ${style.dim(workspacePath(w, byId))} ${style.dim(`(${w.id})`)}`).join("\n") + `
|
|
3156
|
+
You picked ${style.dim(picked.id)} \u2014 re-run \`sechroom config set --local workspaceId <id>\` if that's wrong.
|
|
3157
|
+
`
|
|
2299
3158
|
);
|
|
2300
3159
|
}
|
|
3160
|
+
return chosen;
|
|
3161
|
+
}
|
|
3162
|
+
async function resolveWorkspaceBinding(client, existing, opts) {
|
|
3163
|
+
if (opts.workspace) {
|
|
3164
|
+
const found = await lookupWorkspace(client, opts.workspace);
|
|
3165
|
+
if (!found && !opts.json) {
|
|
3166
|
+
process.stderr.write(
|
|
3167
|
+
`${warn("\u26A0")} workspace ${style.dim(opts.workspace)} not found (or you lack access) \u2014 binding it anyway.
|
|
3168
|
+
`
|
|
3169
|
+
);
|
|
3170
|
+
}
|
|
3171
|
+
return opts.workspace;
|
|
3172
|
+
}
|
|
3173
|
+
if (existing) return existing;
|
|
3174
|
+
if (!canPrompt() || opts.yes) return void 0;
|
|
3175
|
+
return pickWorkspace(client);
|
|
3176
|
+
}
|
|
3177
|
+
async function ensureTenant(baseUrl, g, opts) {
|
|
3178
|
+
const persisted = readPersisted();
|
|
3179
|
+
const local = readLocalConfig();
|
|
3180
|
+
let tenant = g.tenant ?? process.env.SECHROOM_TENANT ?? local.tenant ?? persisted.tenant ?? "";
|
|
3181
|
+
if (!tenant) {
|
|
3182
|
+
const client = await makeClient({ baseUrl, tenant: "", clientId: persisted.clientId });
|
|
3183
|
+
const { data, error } = await client.GET("/auth/me/tenants", {});
|
|
3184
|
+
if (error) {
|
|
3185
|
+
fail(`Couldn't list your tenants: ${JSON.stringify(error)}. Pass --tenant <id> to skip this.`);
|
|
3186
|
+
}
|
|
3187
|
+
const tenants = data?.tenants ?? [];
|
|
3188
|
+
if (tenants.length === 0) {
|
|
3189
|
+
fail(
|
|
3190
|
+
"You're signed in, but your account isn't a member of any tenant yet. Ask an admin to add you (or create one in the app), then re-run \u2014 or pass --tenant <id>."
|
|
3191
|
+
);
|
|
3192
|
+
} else if (tenants.length === 1) {
|
|
3193
|
+
tenant = tenants[0].key;
|
|
3194
|
+
if (!opts.json) {
|
|
3195
|
+
process.stderr.write(
|
|
3196
|
+
`${ok("\u2713")} using your tenant ${style.cyan(tenants[0].label)} ${style.dim(`(${tenant})`)}
|
|
3197
|
+
`
|
|
3198
|
+
);
|
|
3199
|
+
}
|
|
3200
|
+
} else if (canPrompt() && !opts.yes) {
|
|
3201
|
+
tenant = await promptSelect(
|
|
3202
|
+
"You belong to several tenants \u2014 pick one:",
|
|
3203
|
+
tenants.map((t) => ({ label: t.label, value: t.key, hint: t.key })),
|
|
3204
|
+
data?.defaultTenantKey ?? tenants[0].key
|
|
3205
|
+
);
|
|
3206
|
+
} else {
|
|
3207
|
+
tenant = data?.defaultTenantKey ?? tenants[0].key;
|
|
3208
|
+
if (!opts.json) {
|
|
3209
|
+
process.stderr.write(
|
|
3210
|
+
`using tenant ${tenant} (${tenants.length} available \u2014 pass --tenant to choose another)
|
|
3211
|
+
`
|
|
3212
|
+
);
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
const existingWorkspace = local.workspaceId ?? persisted.workspaceId ?? void 0;
|
|
3217
|
+
const wsClient = await makeClient({ baseUrl, tenant, clientId: persisted.clientId });
|
|
3218
|
+
const workspaceId = await resolveWorkspaceBinding(wsClient, existingWorkspace, {
|
|
3219
|
+
yes: opts.yes,
|
|
3220
|
+
json: opts.json,
|
|
3221
|
+
workspace: opts.workspace
|
|
3222
|
+
});
|
|
3223
|
+
const defaultProjectId = local.defaultProjectId ?? persisted.defaultProjectId ?? void 0;
|
|
3224
|
+
if (defaultProjectId && workspaceId) await warnIfProjectStray(wsClient, defaultProjectId, workspaceId, opts.json);
|
|
2301
3225
|
let storeLocal = Boolean(opts.local);
|
|
2302
3226
|
if (!opts.local && canPrompt() && !opts.yes) {
|
|
2303
3227
|
storeLocal = await promptSelect(
|
|
2304
3228
|
"Where should this tenant + base URL be saved?",
|
|
2305
3229
|
[
|
|
2306
3230
|
{ label: "Globally", value: "global", hint: "all projects on this machine" },
|
|
2307
|
-
{ label: "This directory", value: "local", hint: ".sechroom.json \u2014 project + subdirs" }
|
|
3231
|
+
{ label: "This directory", value: "local", hint: ".sechroom.json \u2014 committed, project + subdirs" }
|
|
2308
3232
|
],
|
|
2309
3233
|
local.path ? "local" : "global"
|
|
2310
3234
|
) === "local";
|
|
2311
3235
|
}
|
|
2312
|
-
if (
|
|
2313
|
-
const
|
|
2314
|
-
if (
|
|
3236
|
+
if (opts.persist !== false) {
|
|
3237
|
+
const patch = { baseUrl, tenant, ...workspaceId ? { workspaceId } : {} };
|
|
3238
|
+
if (storeLocal) {
|
|
3239
|
+
const path = writeLocalConfig(patch);
|
|
3240
|
+
if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved to ${path} (directory-local)
|
|
2315
3241
|
`);
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
3242
|
+
} else {
|
|
3243
|
+
writePersisted(patch);
|
|
3244
|
+
if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved globally (~/.config/sechroom/config.json)
|
|
3245
|
+
`);
|
|
3246
|
+
}
|
|
3247
|
+
if (workspaceId && !existingWorkspace && !opts.json) {
|
|
3248
|
+
process.stderr.write(`${ok("\u2713")} bound to workspace ${style.dim(workspaceId)}
|
|
2319
3249
|
`);
|
|
3250
|
+
}
|
|
2320
3251
|
}
|
|
2321
|
-
return { baseUrl, tenant, clientId: persisted.clientId };
|
|
3252
|
+
return { baseUrl, tenant, workspaceId, defaultProjectId, clientId: persisted.clientId };
|
|
2322
3253
|
}
|
|
2323
3254
|
async function ensureAuth(cfg, yes) {
|
|
2324
3255
|
if (process.env.SECHROOM_TOKEN) return;
|
|
@@ -2372,25 +3303,144 @@ async function chooseClients(clientFlag, yes, cwd) {
|
|
|
2372
3303
|
);
|
|
2373
3304
|
return picks.length > 0 ? picks : preselected;
|
|
2374
3305
|
}
|
|
3306
|
+
async function planRecurseChild(entry, root, client, opts) {
|
|
3307
|
+
const dir = resolveChildDir(entry.path, root);
|
|
3308
|
+
if (!existsSync7(dir)) {
|
|
3309
|
+
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
3310
|
+
}
|
|
3311
|
+
if (existsSync7(join7(dir, ".sechroom.json"))) {
|
|
3312
|
+
return {
|
|
3313
|
+
label: entry.path,
|
|
3314
|
+
dir,
|
|
3315
|
+
disposition: "refresh",
|
|
3316
|
+
argv: ["onboard", "--refresh", "--yes"],
|
|
3317
|
+
reason: "bound (committed .sechroom.json) \u2014 refresh in place"
|
|
3318
|
+
};
|
|
3319
|
+
}
|
|
3320
|
+
if (entry.workspaceId) {
|
|
3321
|
+
return {
|
|
3322
|
+
label: entry.path,
|
|
3323
|
+
dir,
|
|
3324
|
+
disposition: "bind",
|
|
3325
|
+
argv: ["onboard", "--yes", "--local", "--workspace", entry.workspaceId],
|
|
3326
|
+
reason: `unbound \u2014 bind to ${entry.workspaceId}`
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
3329
|
+
if (opts.dryRun) {
|
|
3330
|
+
return { label: entry.path, dir, disposition: "bind", argv: ["onboard", "--yes", "--local", "--workspace", "<prompt>"], reason: "unbound \u2014 would prompt for a workspace" };
|
|
3331
|
+
}
|
|
3332
|
+
if (opts.yes || !canPrompt()) {
|
|
3333
|
+
return { label: entry.path, dir, disposition: "skip-unbound", argv: [], reason: "unbound + no workspace (run interactively, or add it to ./.sechroom/repos.json)" };
|
|
3334
|
+
}
|
|
3335
|
+
process.stderr.write(`
|
|
3336
|
+
${style.bold(entry.path)} ${style.dim("is not bound yet.")}
|
|
3337
|
+
`);
|
|
3338
|
+
const ws = await pickWorkspace(client, `Bind ${style.cyan(entry.path)} to a workspace:`);
|
|
3339
|
+
if (!ws) {
|
|
3340
|
+
return { label: entry.path, dir, disposition: "skip-unbound", argv: [], reason: "unbound \u2014 no workspace chosen (skipped)" };
|
|
3341
|
+
}
|
|
3342
|
+
return {
|
|
3343
|
+
label: entry.path,
|
|
3344
|
+
dir,
|
|
3345
|
+
disposition: "bind",
|
|
3346
|
+
argv: ["onboard", "--yes", "--local", "--workspace", ws],
|
|
3347
|
+
reason: `unbound \u2014 bind to ${ws}`
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
3350
|
+
async function resolveFanoutLane(cfg, opts) {
|
|
3351
|
+
let code = opts.lane ?? process.env.SECHROOM_CODE_LANE;
|
|
3352
|
+
let design = opts.designLane ?? process.env.SECHROOM_DESIGN_LANE;
|
|
3353
|
+
if (!code || !design) {
|
|
3354
|
+
const clients = detectInstalledClients(process.cwd());
|
|
3355
|
+
const inferred = await inferLanes(cfg, clients.length ? clients : void 0);
|
|
3356
|
+
code = code ?? inferred.code;
|
|
3357
|
+
design = design ?? inferred.design;
|
|
3358
|
+
}
|
|
3359
|
+
if (!opts.lane && !opts.yes && !opts.dryRun && canPrompt() && (code || design)) {
|
|
3360
|
+
process.stderr.write(`
|
|
3361
|
+
This fan-out will pin the same lane in every repo:
|
|
3362
|
+
`);
|
|
3363
|
+
if (code) process.stderr.write(` ${style.dim("code-lane")} = ${style.cyan(code)}
|
|
3364
|
+
`);
|
|
3365
|
+
if (design) process.stderr.write(` ${style.dim("design-lane")} = ${style.cyan(design)}
|
|
3366
|
+
`);
|
|
3367
|
+
if (!await promptYesNo("Use this lane for all repos?")) {
|
|
3368
|
+
code = await promptText("Code-lane id (blank = let each repo infer)?", code ?? "") || void 0;
|
|
3369
|
+
design = await promptText("Design-lane id (blank = skip)?", design ?? "") || void 0;
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
if (code) process.env.SECHROOM_CODE_LANE = code;
|
|
3373
|
+
else delete process.env.SECHROOM_CODE_LANE;
|
|
3374
|
+
if (design) process.env.SECHROOM_DESIGN_LANE = design;
|
|
3375
|
+
else delete process.env.SECHROOM_DESIGN_LANE;
|
|
3376
|
+
return { code, design };
|
|
3377
|
+
}
|
|
3378
|
+
async function runRecurse(cfg, g, opts) {
|
|
3379
|
+
const { yes, dryRun, json } = opts;
|
|
3380
|
+
const root = process.cwd();
|
|
3381
|
+
const manifestPath = join7(root, ".sechroom", "repos.json");
|
|
3382
|
+
const fromManifest = readManifest(manifestPath);
|
|
3383
|
+
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
3384
|
+
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
3385
|
+
if (entries.length === 0) {
|
|
3386
|
+
if (json) process.stdout.write(JSON.stringify({ recurse: true, root, repos: [] }) + "\n");
|
|
3387
|
+
else process.stderr.write(`${warn("\u26A0")} no child repos found ${fromManifest ? `in ${manifestPath}` : `under ${root}`} \u2014 nothing to do.
|
|
3388
|
+
`);
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3391
|
+
if (!json) {
|
|
3392
|
+
process.stderr.write(`${style.bold("onboard --recurse")} ${style.dim(`(${entries.length} repo${entries.length === 1 ? "" : "s"} from ${sourceLabel})`)}
|
|
3393
|
+
`);
|
|
3394
|
+
}
|
|
3395
|
+
const lane = await resolveFanoutLane(cfg, { lane: opts.lane, designLane: opts.designLane, yes, dryRun });
|
|
3396
|
+
if (!json && lane.code) process.stderr.write(`${ok("\u2713")} lane ${style.cyan(lane.code)}${lane.design ? ` ${style.dim(`/ ${lane.design}`)}` : ""} for every repo
|
|
3397
|
+
`);
|
|
3398
|
+
const client = await makeClient(cfg);
|
|
3399
|
+
const plans = [];
|
|
3400
|
+
for (const entry of entries) plans.push(await planRecurseChild(entry, root, client, { yes, dryRun }));
|
|
3401
|
+
const results = runChildren(plans, { globals: passthroughGlobals(g), dryRun, json });
|
|
3402
|
+
if (json) {
|
|
3403
|
+
process.stdout.write(JSON.stringify({ recurse: true, root, dryRun, repos: results }) + "\n");
|
|
3404
|
+
return;
|
|
3405
|
+
}
|
|
3406
|
+
summarizeFanout(results, { dryRun });
|
|
3407
|
+
}
|
|
2375
3408
|
function registerOnboard(program2) {
|
|
2376
|
-
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
|
|
3409
|
+
program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--recurse", "orchestration-root mode: onboard every child repo under this dir (auto-discovered, or from ./.sechroom/repos.json) \u2014 refreshes bound repos, prompts a workspace per new one", false).option("--lane <id>", "set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo").option("--design-lane <id>", "set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child").option("--client <list>", `comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`).option("--local", "save the binding (tenant + base URL + workspace) to a committed .sechroom.json in this repo 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(
|
|
2377
3410
|
"after",
|
|
2378
3411
|
`
|
|
2379
3412
|
Examples:
|
|
2380
3413
|
$ sechroom onboard guided, interactive (asks where to save config + how to wire)
|
|
2381
3414
|
$ sechroom onboard --cli-only just the CLI \u2014 no .mcp.json, no agent files
|
|
2382
3415
|
$ sechroom onboard --no-mcp agent instructions only, skip MCP config
|
|
2383
|
-
$ sechroom onboard --local save tenant + base URL to ./.sechroom.json
|
|
3416
|
+
$ sechroom onboard --local save tenant + base URL to a committed ./.sechroom.json
|
|
3417
|
+
$ sechroom onboard --workspace wsp_XX bind this directory to a workspace (no pick prompt)
|
|
3418
|
+
$ sechroom onboard --recurse orchestration root: onboard every child repo under this dir
|
|
3419
|
+
$ sechroom onboard --recurse --lane claude-code-you pin one lane across every repo in the tree
|
|
3420
|
+
$ sechroom onboard --refresh refresh out-of-date instruction blocks in place
|
|
3421
|
+
$ sechroom onboard --check CI/pre-commit: nonzero exit if instructions are out of date
|
|
2384
3422
|
$ sechroom onboard --yes non-interactive: defaults + global config + full wire
|
|
2385
3423
|
$ sechroom onboard --client all --dry-run preview wiring every client, write nothing`
|
|
2386
3424
|
).action(async (opts, cmd) => {
|
|
2387
3425
|
const g = cmd.optsWithGlobals();
|
|
2388
3426
|
const json = Boolean(g.json);
|
|
2389
|
-
const yes = Boolean(opts.yes);
|
|
2390
3427
|
const dryRun = Boolean(opts.dryRun);
|
|
2391
|
-
const
|
|
2392
|
-
|
|
2393
|
-
const
|
|
3428
|
+
const mode = opts.check ? "check" : opts.force ? "force" : "apply";
|
|
3429
|
+
const check = mode === "check";
|
|
3430
|
+
const yes = Boolean(opts.yes) || check;
|
|
3431
|
+
if (opts.lane) process.env.SECHROOM_CODE_LANE = opts.lane;
|
|
3432
|
+
if (opts.designLane) process.env.SECHROOM_DESIGN_LANE = opts.designLane;
|
|
3433
|
+
if (opts.recurse) {
|
|
3434
|
+
const baseUrl2 = resolveBaseUrl(g);
|
|
3435
|
+
await ensureAuth({ baseUrl: baseUrl2, tenant: "", clientId: readPersisted().clientId }, yes);
|
|
3436
|
+
const cfg2 = await ensureTenant(baseUrl2, g, { yes: true, json, persist: false });
|
|
3437
|
+
await runRecurse(cfg2, g, { yes, dryRun, json, lane: opts.lane, designLane: opts.designLane });
|
|
3438
|
+
return;
|
|
3439
|
+
}
|
|
3440
|
+
const baseUrl = resolveBaseUrl(g);
|
|
3441
|
+
await ensureAuth({ baseUrl, tenant: "", clientId: readPersisted().clientId }, yes);
|
|
3442
|
+
const cfg = await ensureTenant(baseUrl, g, { yes, json, local: Boolean(opts.local), workspace: opts.workspace, persist: !check });
|
|
3443
|
+
const tz = await ensureTimezone(cfg, { yes, dryRun: dryRun || check });
|
|
2394
3444
|
if (!json && tz.action !== "already-set") {
|
|
2395
3445
|
const line = tz.action === "set" ? `${ok("\u2713")} timezone set to ${tz.timezone}
|
|
2396
3446
|
` : tz.action === "dry-run" ? `(dry run \u2014 would set timezone to ${tz.timezone})
|
|
@@ -2401,22 +3451,27 @@ Examples:
|
|
|
2401
3451
|
const wire = await chooseWire(opts, yes);
|
|
2402
3452
|
if (wire === "cli-only") {
|
|
2403
3453
|
if (json) {
|
|
2404
|
-
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, timezone: tz, wire, clients: [] }, true);
|
|
3454
|
+
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, clients: [] }, true);
|
|
2405
3455
|
return;
|
|
2406
3456
|
}
|
|
3457
|
+
if (!dryRun) {
|
|
3458
|
+
await ensureLanePin(cfg, { yes, dryRun, clients: detectInstalledClients(process.cwd()) });
|
|
3459
|
+
await maybeOfferHooks({ yes, dryRun, cwd: process.cwd() });
|
|
3460
|
+
}
|
|
2407
3461
|
process.stdout.write(
|
|
2408
3462
|
`
|
|
2409
3463
|
${style.bold("Done.")} The CLI is configured for ${style.cyan(cfg.tenant)} \u2014 no AI-client files written.
|
|
2410
3464
|
Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom --help")}
|
|
2411
3465
|
`
|
|
2412
3466
|
);
|
|
3467
|
+
await printStarterPrompt("cli");
|
|
2413
3468
|
return;
|
|
2414
3469
|
}
|
|
2415
3470
|
const keys = await chooseClients(opts.client, yes, process.cwd());
|
|
2416
3471
|
const setup = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
|
|
2417
3472
|
const targets = clientTargets(process.cwd());
|
|
2418
3473
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
2419
|
-
if (!dryRun) {
|
|
3474
|
+
if (!dryRun && !check) {
|
|
2420
3475
|
await maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, copyChoice(opts));
|
|
2421
3476
|
}
|
|
2422
3477
|
const writeMcp = wire === "full";
|
|
@@ -2427,25 +3482,68 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
2427
3482
|
dryRun,
|
|
2428
3483
|
mcp: writeMcp,
|
|
2429
3484
|
agentFiles: true,
|
|
2430
|
-
personalWorkspaceId
|
|
3485
|
+
personalWorkspaceId,
|
|
3486
|
+
mode
|
|
2431
3487
|
});
|
|
2432
3488
|
result.push({ client: key, actions });
|
|
2433
|
-
if (!json) printActions(target, actions);
|
|
3489
|
+
if (!json && !check) printActions(target, actions);
|
|
3490
|
+
}
|
|
3491
|
+
const evalCounts = { current: 0, stale: 0, drift: 0, absent: 0 };
|
|
3492
|
+
for (const { actions } of result) for (const a of actions) if (a.eval) evalCounts[a.eval]++;
|
|
3493
|
+
const wouldChange = evalCounts.stale + evalCounts.drift + evalCounts.absent;
|
|
3494
|
+
if (check) {
|
|
3495
|
+
if (json) {
|
|
3496
|
+
emit({ check: true, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, eval: evalCounts, wouldChange, clients: result }, true);
|
|
3497
|
+
} else if (wouldChange === 0) {
|
|
3498
|
+
process.stdout.write(`${ok("\u2713")} all instruction blocks are up to date.
|
|
3499
|
+
`);
|
|
3500
|
+
} else {
|
|
3501
|
+
const bits = [];
|
|
3502
|
+
if (evalCounts.stale) bits.push(`${evalCounts.stale} out of date`);
|
|
3503
|
+
if (evalCounts.drift) bits.push(`${evalCounts.drift} with local edits`);
|
|
3504
|
+
if (evalCounts.absent) bits.push(`${evalCounts.absent} not yet written`);
|
|
3505
|
+
process.stderr.write(
|
|
3506
|
+
`${warn("\u26A0")} ${wouldChange} instruction block(s) would change: ${bits.join(", ")}. Run ${style.cyan("sechroom onboard --refresh")}.
|
|
3507
|
+
`
|
|
3508
|
+
);
|
|
3509
|
+
}
|
|
3510
|
+
process.exit(wouldChange === 0 ? 0 : 1);
|
|
3511
|
+
}
|
|
3512
|
+
if (!json && !dryRun) {
|
|
3513
|
+
await ensureLanePin(cfg, { yes, dryRun, clients: keys });
|
|
2434
3514
|
}
|
|
2435
3515
|
if (!json && !dryRun) {
|
|
2436
3516
|
await maybeOfferSkills(cfg, personalWorkspaceId, { yes, dryRun, surface: "claude-code" });
|
|
2437
3517
|
}
|
|
3518
|
+
if (!json && !dryRun) {
|
|
3519
|
+
await maybeOfferHooks({ yes, dryRun, cwd: process.cwd() });
|
|
3520
|
+
}
|
|
2438
3521
|
if (json) {
|
|
2439
|
-
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, timezone: tz, wire, clients: result }, true);
|
|
3522
|
+
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, eval: evalCounts, clients: result }, true);
|
|
2440
3523
|
return;
|
|
2441
3524
|
}
|
|
3525
|
+
if (!dryRun && evalCounts.stale) {
|
|
3526
|
+
process.stderr.write(`${style.cyan("\u21BB")} refreshed ${evalCounts.stale} section(s) the server had moved
|
|
3527
|
+
`);
|
|
3528
|
+
}
|
|
3529
|
+
if (!dryRun && evalCounts.drift) {
|
|
3530
|
+
process.stderr.write(
|
|
3531
|
+
mode === "force" ? `${warn("\u26A0")} overwrote ${evalCounts.drift} section(s) that had local edits (--force)
|
|
3532
|
+
` : `${warn("\u26A0")} ${evalCounts.drift} section(s) have local edits \u2014 wrote a .proposed file alongside (original untouched). Review + merge, or re-run with ${style.cyan("--force")}.
|
|
3533
|
+
`
|
|
3534
|
+
);
|
|
3535
|
+
}
|
|
3536
|
+
const wroteSomething = result.some(({ actions }) => actions.some((a) => a.status === "created" || a.status === "merged"));
|
|
2442
3537
|
process.stdout.write(
|
|
2443
|
-
dryRun ? "\n(dry run \u2014 nothing written)\n" :
|
|
3538
|
+
dryRun ? "\n(dry run \u2014 nothing written)\n" : !wroteSomething ? `
|
|
3539
|
+
${style.bold("Done.")} Everything's already up to date.
|
|
3540
|
+
` : writeMcp ? `
|
|
2444
3541
|
${style.bold("Done.")} Restart your AI client (or reload MCP) to pick up the new config.
|
|
2445
3542
|
` : `
|
|
2446
3543
|
${style.bold("Done.")} Agent instructions written (no MCP config).
|
|
2447
3544
|
`
|
|
2448
3545
|
);
|
|
3546
|
+
if (!dryRun) await printStarterPrompt("agent", cfg);
|
|
2449
3547
|
});
|
|
2450
3548
|
}
|
|
2451
3549
|
async function chooseWire(opts, yes) {
|
|
@@ -2463,16 +3561,142 @@ async function chooseWire(opts, yes) {
|
|
|
2463
3561
|
}
|
|
2464
3562
|
return opts.mcp === false ? "agent-only" : "full";
|
|
2465
3563
|
}
|
|
3564
|
+
var FALLBACK_AGENT_PROMPT = "Resume my sechroom continuity, summarise what I was last working on, then suggest the next step.";
|
|
3565
|
+
async function printStarterPrompt(mode, cfg) {
|
|
3566
|
+
if (mode === "cli") {
|
|
3567
|
+
process.stdout.write(
|
|
3568
|
+
`
|
|
3569
|
+
${style.bold("Next:")} pick up where you left off \u2014
|
|
3570
|
+
${style.cyan("sechroom continuity resume-me")}
|
|
3571
|
+
`
|
|
3572
|
+
);
|
|
3573
|
+
return;
|
|
3574
|
+
}
|
|
3575
|
+
let primary = FALLBACK_AGENT_PROMPT;
|
|
3576
|
+
if (cfg) {
|
|
3577
|
+
try {
|
|
3578
|
+
const client = await makeClient(cfg);
|
|
3579
|
+
const { data } = await client.GET("/me/onboarding/starter-prompt", {});
|
|
3580
|
+
if (data?.primary) primary = data.primary;
|
|
3581
|
+
} catch {
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
process.stdout.write(
|
|
3585
|
+
`
|
|
3586
|
+
${style.bold("Next:")} paste this into your AI agent to get going \u2014
|
|
3587
|
+
${style.cyan(`"${primary}"`)}
|
|
3588
|
+
`
|
|
3589
|
+
);
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
// src/commands/sweep.ts
|
|
3593
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3594
|
+
import { dirname as dirname6, join as join8, resolve as resolve2 } from "path";
|
|
3595
|
+
var DEFAULT_MANIFEST = join8(".sechroom", "repos.json");
|
|
3596
|
+
function planEntry(entry, root) {
|
|
3597
|
+
const dir = resolveChildDir(entry.path, root);
|
|
3598
|
+
if (!existsSync8(dir)) {
|
|
3599
|
+
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
3600
|
+
}
|
|
3601
|
+
if (committedBindingPath(dir)) {
|
|
3602
|
+
return {
|
|
3603
|
+
label: entry.path,
|
|
3604
|
+
dir,
|
|
3605
|
+
disposition: "refresh",
|
|
3606
|
+
argv: ["onboard", "--refresh", "--yes"],
|
|
3607
|
+
reason: "bound (committed .sechroom.json) \u2014 refresh in place"
|
|
3608
|
+
};
|
|
3609
|
+
}
|
|
3610
|
+
if (entry.workspaceId) {
|
|
3611
|
+
return {
|
|
3612
|
+
label: entry.path,
|
|
3613
|
+
dir,
|
|
3614
|
+
disposition: "bind",
|
|
3615
|
+
argv: ["onboard", "--yes", "--local", "--workspace", entry.workspaceId],
|
|
3616
|
+
reason: `unbound \u2014 bind to ${entry.workspaceId} + commit .sechroom.json`
|
|
3617
|
+
};
|
|
3618
|
+
}
|
|
3619
|
+
return {
|
|
3620
|
+
label: entry.path,
|
|
3621
|
+
dir,
|
|
3622
|
+
disposition: "skip-unbound",
|
|
3623
|
+
argv: [],
|
|
3624
|
+
reason: "unbound + no workspaceId in the manifest \u2014 add one or run `sechroom onboard` there"
|
|
3625
|
+
};
|
|
3626
|
+
}
|
|
3627
|
+
function registerSweep(program2) {
|
|
3628
|
+
program2.command("sweep").description("Non-interactive fan-out from ./.sechroom/repos.json (headless sibling of `onboard --recurse`)").option("--manifest <path>", "path to the repos manifest", DEFAULT_MANIFEST).option("--dry-run", "print the plan (per-repo disposition + the onboard command) without running anything", false).addHelpText(
|
|
3629
|
+
"after",
|
|
3630
|
+
`
|
|
3631
|
+
For an interactive, no-manifest run use ${"`sechroom onboard --recurse`"} instead \u2014 it
|
|
3632
|
+
auto-discovers the child repos and prompts for a workspace per new one. ${"`sweep`"} is
|
|
3633
|
+
the deterministic manifest-driven form for scripts / CI.
|
|
3634
|
+
|
|
3635
|
+
Manifest \u2014 ./.sechroom/repos.json (per-operator, gitignored, alongside lane.json):
|
|
3636
|
+
{
|
|
3637
|
+
"repos": [
|
|
3638
|
+
{ "path": "sechroom", "workspaceId": "wsp_XXXX" },
|
|
3639
|
+
{ "path": "../other-repo", "workspaceId": "wsp_YYYY" },
|
|
3640
|
+
{ "path": "already-bound" }
|
|
3641
|
+
]
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
Per repo (paths resolve relative to the manifest's root):
|
|
3645
|
+
${ICON.refresh} bound committed .sechroom.json present \u2192 onboard --refresh (manifest workspace ignored)
|
|
3646
|
+
${ICON.bind} unbound bind to the manifest workspaceId \u2192 onboard --local --workspace <id>
|
|
3647
|
+
${ICON["skip-missing"]} missing directory does not exist \u2192 skipped
|
|
3648
|
+
${ICON["skip-unbound"]} no workspace unbound + no workspaceId in manifest \u2192 skipped (add one, or onboard manually)
|
|
3649
|
+
|
|
3650
|
+
Examples:
|
|
3651
|
+
$ sechroom sweep --dry-run preview every repo's disposition, run nothing
|
|
3652
|
+
$ sechroom sweep onboard the whole tree from the root
|
|
3653
|
+
$ sechroom --tenant ocd sweep force a tenant for every child (else each resolves its own)`
|
|
3654
|
+
).action((opts, cmd) => {
|
|
3655
|
+
const g = cmd.optsWithGlobals();
|
|
3656
|
+
const json = Boolean(g.json);
|
|
3657
|
+
const dryRun = Boolean(opts.dryRun);
|
|
3658
|
+
const manifestPath = resolve2(opts.manifest);
|
|
3659
|
+
let repos;
|
|
3660
|
+
try {
|
|
3661
|
+
repos = readManifest(manifestPath);
|
|
3662
|
+
} catch (err2) {
|
|
3663
|
+
fail(err2 instanceof Error ? err2.message : String(err2));
|
|
3664
|
+
}
|
|
3665
|
+
if (repos === null) {
|
|
3666
|
+
fail(`no manifest at ${manifestPath} \u2014 create ./.sechroom/repos.json, or use \`sechroom onboard --recurse\` to auto-discover (see \`sechroom sweep --help\`).`);
|
|
3667
|
+
}
|
|
3668
|
+
if (repos.length === 0) {
|
|
3669
|
+
if (json) process.stdout.write(JSON.stringify({ manifest: manifestPath, repos: [] }) + "\n");
|
|
3670
|
+
else process.stderr.write(`${warn("\u26A0")} ${manifestPath} lists no repos \u2014 nothing to do.
|
|
3671
|
+
`);
|
|
3672
|
+
return;
|
|
3673
|
+
}
|
|
3674
|
+
const root = dirname6(dirname6(manifestPath));
|
|
3675
|
+
const plans = repos.map((entry) => planEntry(entry, root));
|
|
3676
|
+
if (!json) {
|
|
3677
|
+
process.stderr.write(
|
|
3678
|
+
`${style.bold("sweep")} ${style.dim(`(${plans.length} repo${plans.length === 1 ? "" : "s"} from ${manifestPath})`)}
|
|
3679
|
+
`
|
|
3680
|
+
);
|
|
3681
|
+
}
|
|
3682
|
+
const results = runChildren(plans, { globals: passthroughGlobals(g), dryRun, json });
|
|
3683
|
+
if (json) {
|
|
3684
|
+
process.stdout.write(JSON.stringify({ manifest: manifestPath, dryRun, repos: results }) + "\n");
|
|
3685
|
+
return;
|
|
3686
|
+
}
|
|
3687
|
+
summarizeFanout(results, { dryRun });
|
|
3688
|
+
});
|
|
3689
|
+
}
|
|
2466
3690
|
|
|
2467
3691
|
// src/commands/skills.ts
|
|
2468
|
-
import { homedir as
|
|
2469
|
-
import {
|
|
2470
|
-
import { mkdirSync as
|
|
3692
|
+
import { homedir as homedir6 } from "os";
|
|
3693
|
+
import { join as join9 } from "path";
|
|
3694
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, rmSync as rmSync2, existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
|
|
2471
3695
|
var DEFAULT_SLUG = "operator-skills";
|
|
2472
3696
|
var ROLE_TAGS = ["sechroom:role:skill-template", "role:skill-template"];
|
|
2473
3697
|
var LOCK = ".sechroom-skills.json";
|
|
2474
3698
|
function skillsDir(global) {
|
|
2475
|
-
return global ?
|
|
3699
|
+
return global ? join9(homedir6(), ".claude", "skills") : join9(process.cwd(), ".claude", "skills");
|
|
2476
3700
|
}
|
|
2477
3701
|
function tagValue2(tags, prefix) {
|
|
2478
3702
|
return (tags ?? []).find((t) => t.startsWith(prefix))?.slice(prefix.length);
|
|
@@ -2550,15 +3774,15 @@ Examples:
|
|
|
2550
3774
|
const name = tagValue2(tags, "skill:");
|
|
2551
3775
|
if (!name) continue;
|
|
2552
3776
|
const body = m.text ?? m.Text ?? "";
|
|
2553
|
-
|
|
2554
|
-
|
|
3777
|
+
mkdirSync6(join9(dir, name), { recursive: true });
|
|
3778
|
+
writeFileSync6(join9(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
|
|
2555
3779
|
written.push(name);
|
|
2556
3780
|
}
|
|
2557
|
-
|
|
2558
|
-
const lockPath =
|
|
2559
|
-
const lock =
|
|
3781
|
+
mkdirSync6(dir, { recursive: true });
|
|
3782
|
+
const lockPath = join9(dir, LOCK);
|
|
3783
|
+
const lock = existsSync9(lockPath) ? JSON.parse(readFileSync6(lockPath, "utf8")) : {};
|
|
2560
3784
|
lock[slug] = { surface: opts.surface, version, instance: wantInstance, skills: written.sort() };
|
|
2561
|
-
|
|
3785
|
+
writeFileSync6(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
2562
3786
|
if (opts.json) return emit({ slug, version, instance: wantInstance, surface: opts.surface, dir, installed: written }, true);
|
|
2563
3787
|
const instanceNote = opts.instance ? ` (${opts.instance})` : "";
|
|
2564
3788
|
console.log(style.green(`Installed ${slug}@${version}${instanceNote} \u2014 ${written.length} skill(s) \u2192 ${dir}`));
|
|
@@ -2580,42 +3804,41 @@ Examples:
|
|
|
2580
3804
|
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) => {
|
|
2581
3805
|
const slug = slugArg || DEFAULT_SLUG;
|
|
2582
3806
|
const dir = skillsDir(!opts.local);
|
|
2583
|
-
const lockPath =
|
|
2584
|
-
if (!
|
|
2585
|
-
const lock = JSON.parse(
|
|
3807
|
+
const lockPath = join9(dir, LOCK);
|
|
3808
|
+
if (!existsSync9(lockPath)) fail(`No skills lockfile at ${lockPath}; nothing to clean.`);
|
|
3809
|
+
const lock = JSON.parse(readFileSync6(lockPath, "utf8"));
|
|
2586
3810
|
const entry = lock[slug];
|
|
2587
3811
|
if (!entry) fail(`No installed record for '${slug}' in ${lockPath}.`);
|
|
2588
3812
|
const removed = [];
|
|
2589
3813
|
for (const name of entry.skills) {
|
|
2590
|
-
const skillPath =
|
|
2591
|
-
if (
|
|
3814
|
+
const skillPath = join9(dir, name);
|
|
3815
|
+
if (existsSync9(skillPath)) {
|
|
2592
3816
|
rmSync2(skillPath, { recursive: true, force: true });
|
|
2593
3817
|
removed.push(name);
|
|
2594
3818
|
}
|
|
2595
3819
|
}
|
|
2596
3820
|
delete lock[slug];
|
|
2597
|
-
|
|
3821
|
+
writeFileSync6(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
2598
3822
|
if (opts.json) return emit({ slug, removed, dir }, true);
|
|
2599
3823
|
console.log(style.green(`Removed ${removed.length} skill(s) for ${slug} from ${dir}`));
|
|
2600
3824
|
});
|
|
2601
|
-
skills.command("set-lane").description("Write this checkout's lane pin to a local ./.
|
|
3825
|
+
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) => {
|
|
2602
3826
|
if (!opts.codeLane && !opts.designLane) fail("Provide --code-lane and/or --design-lane.");
|
|
2603
3827
|
const target = localSemPath();
|
|
2604
|
-
const values =
|
|
3828
|
+
const values = readLocalSemValues();
|
|
2605
3829
|
if (opts.codeLane) values["code-lane"] = opts.codeLane;
|
|
2606
3830
|
if (opts.designLane) values["design-lane"] = opts.designLane;
|
|
2607
|
-
|
|
2608
|
-
writeFileSync4(target, serializeSem(values));
|
|
3831
|
+
writeSem(values, target);
|
|
2609
3832
|
if (cmd.optsWithGlobals().json) return emit({ path: target, values }, true);
|
|
2610
|
-
console.log(style.green(`Wrote lane pin \u2192 ${target}`));
|
|
3833
|
+
console.log(style.green(`Wrote lane pin \u2192 ${target} ${style.dim("(git-ignored)")}`));
|
|
2611
3834
|
Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
|
|
2612
3835
|
});
|
|
2613
|
-
skills.command("lane").description("Show the lane pin resolved from ./.
|
|
3836
|
+
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) => {
|
|
2614
3837
|
const json = cmd.optsWithGlobals().json;
|
|
2615
3838
|
const found = readSem();
|
|
2616
3839
|
if (!found) {
|
|
2617
3840
|
if (json) return emit({ path: null, values: {} }, true);
|
|
2618
|
-
return console.log(style.dim(`No ./.
|
|
3841
|
+
return console.log(style.dim(`No ./.sechroom/lane.json pin in this checkout. Run 'sechroom skills set-lane'.`));
|
|
2619
3842
|
}
|
|
2620
3843
|
if (json) return emit(found, true);
|
|
2621
3844
|
console.log(style.dim(`from ${found.path}`));
|
|
@@ -2684,22 +3907,22 @@ Examples:
|
|
|
2684
3907
|
}
|
|
2685
3908
|
|
|
2686
3909
|
// src/commands/reset.ts
|
|
2687
|
-
import { homedir as
|
|
2688
|
-
import { join as
|
|
2689
|
-
import { existsSync as
|
|
3910
|
+
import { homedir as homedir7 } from "os";
|
|
3911
|
+
import { join as join10 } from "path";
|
|
3912
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
|
|
2690
3913
|
var SKILLS_LOCK = ".sechroom-skills.json";
|
|
2691
|
-
var localSkillsDir = () =>
|
|
2692
|
-
var globalSkillsDir = () =>
|
|
3914
|
+
var localSkillsDir = () => join10(process.cwd(), ".claude", "skills");
|
|
3915
|
+
var globalSkillsDir = () => join10(homedir7(), ".claude", "skills");
|
|
2693
3916
|
function removeMaterialisedSkills(dir) {
|
|
2694
3917
|
const removed = [];
|
|
2695
|
-
const lockPath =
|
|
2696
|
-
if (!
|
|
3918
|
+
const lockPath = join10(dir, SKILLS_LOCK);
|
|
3919
|
+
if (!existsSync10(lockPath)) return removed;
|
|
2697
3920
|
try {
|
|
2698
|
-
const lock = JSON.parse(
|
|
3921
|
+
const lock = JSON.parse(readFileSync7(lockPath, "utf8"));
|
|
2699
3922
|
for (const entry of Object.values(lock)) {
|
|
2700
3923
|
for (const name of entry.skills ?? []) {
|
|
2701
|
-
const p =
|
|
2702
|
-
if (
|
|
3924
|
+
const p = join10(dir, name);
|
|
3925
|
+
if (existsSync10(p)) {
|
|
2703
3926
|
rmSync3(p, { recursive: true, force: true });
|
|
2704
3927
|
removed.push(p);
|
|
2705
3928
|
}
|
|
@@ -2719,26 +3942,31 @@ function registerReset(program2) {
|
|
|
2719
3942
|
removed ? style.green("Signed out \u2014 auth token removed.") : style.dim("Already signed out (no token).")
|
|
2720
3943
|
);
|
|
2721
3944
|
});
|
|
2722
|
-
program2.command("reset").description("Reset LOCAL CLI state for this directory (./.sechroom.json
|
|
3945
|
+
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) => {
|
|
2723
3946
|
const json = cmd.optsWithGlobals().json;
|
|
2724
3947
|
const global = Boolean(opts.global);
|
|
2725
3948
|
if (!opts.yes && canPrompt()) {
|
|
2726
|
-
const scope = global ? "this directory's local state AND your global auth token + config + ~/.claude/skills" : "this directory's local state (./.sechroom.json
|
|
3949
|
+
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)";
|
|
2727
3950
|
if (!await promptYesNo(`Remove ${scope}?`)) {
|
|
2728
3951
|
if (!json) console.log(style.dim("Cancelled."));
|
|
2729
3952
|
return;
|
|
2730
3953
|
}
|
|
2731
3954
|
}
|
|
2732
3955
|
const removed = [];
|
|
2733
|
-
const
|
|
2734
|
-
if (
|
|
2735
|
-
rmSync3(
|
|
2736
|
-
removed.push(
|
|
3956
|
+
const stateDir = join10(process.cwd(), ".sechroom");
|
|
3957
|
+
if (existsSync10(stateDir)) {
|
|
3958
|
+
rmSync3(stateDir, { recursive: true, force: true });
|
|
3959
|
+
removed.push(stateDir);
|
|
3960
|
+
}
|
|
3961
|
+
const legacyCfg = join10(process.cwd(), ".sechroom.json");
|
|
3962
|
+
if (existsSync10(legacyCfg)) {
|
|
3963
|
+
rmSync3(legacyCfg, { force: true });
|
|
3964
|
+
removed.push(legacyCfg);
|
|
2737
3965
|
}
|
|
2738
|
-
const
|
|
2739
|
-
if (
|
|
2740
|
-
rmSync3(
|
|
2741
|
-
removed.push(
|
|
3966
|
+
const legacySem = join10(process.cwd(), ".sem");
|
|
3967
|
+
if (existsSync10(legacySem)) {
|
|
3968
|
+
rmSync3(legacySem, { force: true });
|
|
3969
|
+
removed.push(legacySem);
|
|
2742
3970
|
}
|
|
2743
3971
|
removed.push(...removeMaterialisedSkills(localSkillsDir()));
|
|
2744
3972
|
if (global) {
|
|
@@ -2763,7 +3991,7 @@ function registerReset(program2) {
|
|
|
2763
3991
|
function resolveVersion() {
|
|
2764
3992
|
try {
|
|
2765
3993
|
const pkg = JSON.parse(
|
|
2766
|
-
|
|
3994
|
+
readFileSync8(new URL("../package.json", import.meta.url), "utf8")
|
|
2767
3995
|
);
|
|
2768
3996
|
return pkg.version ?? "0.0.0";
|
|
2769
3997
|
} catch {
|
|
@@ -2779,7 +4007,7 @@ Examples:
|
|
|
2779
4007
|
$ sechroom onboard guided first-run: configure, sign in, wire this project
|
|
2780
4008
|
$ sechroom login sign in via browser (OAuth + PKCE)
|
|
2781
4009
|
$ sechroom config set tenant ocd set your tenant (global)
|
|
2782
|
-
$ sechroom config set --local tenant cli-smoke pin tenant for this directory (.sechroom.json)
|
|
4010
|
+
$ sechroom config set --local tenant cli-smoke pin tenant for this directory (committed .sechroom.json)
|
|
2783
4011
|
$ sechroom config show resolved config + which source won
|
|
2784
4012
|
|
|
2785
4013
|
$ sechroom memory create --text "a note" --title "Note" --tag idea
|
|
@@ -2791,7 +4019,7 @@ Examples:
|
|
|
2791
4019
|
$ sechroom --json memory search "auth" compact JSON for scripts and agents
|
|
2792
4020
|
$ SECHROOM_TOKEN=<bearer> sechroom --json memory get mem_XXXX headless
|
|
2793
4021
|
|
|
2794
|
-
Config precedence (high -> low): --flag > env (SECHROOM_*) > ./.sechroom.json > global > default.
|
|
4022
|
+
Config precedence (high -> low): --flag > env (SECHROOM_*) > directory-local (committed ./.sechroom.json, shadowed per-field by the gitignored ./.sechroom/config.json override) > global > default.
|
|
2795
4023
|
Run 'sechroom <command> --help' for command-specific examples.`
|
|
2796
4024
|
);
|
|
2797
4025
|
program.hook("preAction", (_thisCmd, actionCmd) => {
|
|
@@ -2817,14 +4045,14 @@ config.addHelpText(
|
|
|
2817
4045
|
Examples:
|
|
2818
4046
|
$ sechroom config set baseUrl https://app.sechroom.ai/api prod (staging: https://staging.app.sechroom.ai/api)
|
|
2819
4047
|
$ sechroom config set tenant ocd
|
|
2820
|
-
$ sechroom config set --local tenant cli-smoke this dir + subdirs (.sechroom.json)
|
|
4048
|
+
$ sechroom config set --local tenant cli-smoke this dir + subdirs (committed .sechroom.json)
|
|
2821
4049
|
$ sechroom config set clientId dyn-XXXX global-only escape hatch (no DCR endpoint)
|
|
2822
4050
|
$ sechroom config show --json`
|
|
2823
4051
|
);
|
|
2824
|
-
config.command("set <key> <value>").description("Set baseUrl | tenant | clientId (clientId is global-only)").option("--local", "Write
|
|
4052
|
+
config.command("set <key> <value>").description("Set baseUrl | tenant | workspaceId | defaultProjectId | clientId (clientId is global-only)").option("--local", "Write the committed directory-local .sechroom.json (nearest up the tree, else cwd) instead of the global config").action((key, value, opts) => {
|
|
2825
4053
|
if (opts.local) {
|
|
2826
|
-
if (!["baseUrl", "tenant"].includes(key)) {
|
|
2827
|
-
process.stderr.write(`--local supports only: baseUrl | tenant (clientId is global)
|
|
4054
|
+
if (!["baseUrl", "tenant", "workspaceId", "defaultProjectId"].includes(key)) {
|
|
4055
|
+
process.stderr.write(`--local supports only: baseUrl | tenant | workspaceId | defaultProjectId (clientId is global)
|
|
2828
4056
|
`);
|
|
2829
4057
|
process.exit(1);
|
|
2830
4058
|
}
|
|
@@ -2833,8 +4061,8 @@ config.command("set <key> <value>").description("Set baseUrl | tenant | clientId
|
|
|
2833
4061
|
`);
|
|
2834
4062
|
return;
|
|
2835
4063
|
}
|
|
2836
|
-
if (!["baseUrl", "tenant", "clientId"].includes(key)) {
|
|
2837
|
-
process.stderr.write(`unknown key: ${key} (expected baseUrl | tenant | clientId)
|
|
4064
|
+
if (!["baseUrl", "tenant", "clientId", "workspaceId", "defaultProjectId"].includes(key)) {
|
|
4065
|
+
process.stderr.write(`unknown key: ${key} (expected baseUrl | tenant | workspaceId | defaultProjectId | clientId)
|
|
2838
4066
|
`);
|
|
2839
4067
|
process.exit(1);
|
|
2840
4068
|
}
|
|
@@ -2848,7 +4076,7 @@ config.command("show").description("Print resolved config + sources (flag > env
|
|
|
2848
4076
|
if (g.json) {
|
|
2849
4077
|
process.stdout.write(
|
|
2850
4078
|
JSON.stringify({
|
|
2851
|
-
resolved: { baseUrl: d.baseUrl, tenant: d.tenant },
|
|
4079
|
+
resolved: { baseUrl: d.baseUrl, tenant: d.tenant, workspaceId: d.workspaceId },
|
|
2852
4080
|
global: readPersisted(),
|
|
2853
4081
|
local: readLocalConfig()
|
|
2854
4082
|
}) + "\n"
|
|
@@ -2856,8 +4084,9 @@ config.command("show").description("Print resolved config + sources (flag > env
|
|
|
2856
4084
|
return;
|
|
2857
4085
|
}
|
|
2858
4086
|
process.stdout.write(
|
|
2859
|
-
`baseUrl:
|
|
2860
|
-
tenant:
|
|
4087
|
+
`baseUrl: ${d.baseUrl.value} [${d.baseUrl.source}]
|
|
4088
|
+
tenant: ${d.tenant.value ?? "(unset)"} [${d.tenant.source}]
|
|
4089
|
+
workspaceId: ${d.workspaceId.value ?? "(unset)"} [${d.workspaceId.source}]
|
|
2861
4090
|
|
|
2862
4091
|
global: ${JSON.stringify(readPersisted())}
|
|
2863
4092
|
local: ${d.localPath ?? "(none)"} ${JSON.stringify(readLocalConfig())}
|
|
@@ -2872,12 +4101,14 @@ registerWorkspace(program);
|
|
|
2872
4101
|
registerProject(program);
|
|
2873
4102
|
registerFiling(program);
|
|
2874
4103
|
registerContinuity(program);
|
|
4104
|
+
registerHook(program);
|
|
2875
4105
|
registerId(program);
|
|
2876
4106
|
registerAccount(program);
|
|
2877
4107
|
registerChat(program);
|
|
2878
4108
|
registerInit(program);
|
|
2879
4109
|
registerSetup(program);
|
|
2880
4110
|
registerOnboard(program);
|
|
4111
|
+
registerSweep(program);
|
|
2881
4112
|
registerSkills(program);
|
|
2882
4113
|
registerReset(program);
|
|
2883
4114
|
program.parseAsync().catch((err2) => {
|