@sechroom/cli 2026.6.16 → 2026.6.18
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 +345 -120
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1558,6 +1558,184 @@ Examples:
|
|
|
1558
1558
|
});
|
|
1559
1559
|
}
|
|
1560
1560
|
|
|
1561
|
+
// src/sem.ts
|
|
1562
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
1563
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1564
|
+
var SEM_FILE = ".sem";
|
|
1565
|
+
function localSemPath(cwd = process.cwd()) {
|
|
1566
|
+
return join2(cwd, SEM_FILE);
|
|
1567
|
+
}
|
|
1568
|
+
function resolveSemPathForRead(start = process.cwd()) {
|
|
1569
|
+
let dir = start;
|
|
1570
|
+
while (true) {
|
|
1571
|
+
const candidate = join2(dir, SEM_FILE);
|
|
1572
|
+
if (existsSync2(candidate)) return candidate;
|
|
1573
|
+
const parent = dirname2(dir);
|
|
1574
|
+
if (parent === dir) return void 0;
|
|
1575
|
+
dir = parent;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
function parseSem(text) {
|
|
1579
|
+
const out = {};
|
|
1580
|
+
for (const raw of text.split("\n")) {
|
|
1581
|
+
const line = raw.trim();
|
|
1582
|
+
if (!line || line.startsWith("#")) continue;
|
|
1583
|
+
const eq = line.indexOf("=");
|
|
1584
|
+
if (eq === -1) continue;
|
|
1585
|
+
const key = line.slice(0, eq).trim();
|
|
1586
|
+
const value = line.slice(eq + 1).trim();
|
|
1587
|
+
if (key) out[key] = value;
|
|
1588
|
+
}
|
|
1589
|
+
return out;
|
|
1590
|
+
}
|
|
1591
|
+
function serializeSem(values) {
|
|
1592
|
+
const header = "# sechroom lane pin (per-location fallback) \u2014 resolved at runtime by operator skills.\n";
|
|
1593
|
+
const body = Object.entries(values).map(([k, v]) => `${k} = ${v}`).join("\n");
|
|
1594
|
+
return header + body + "\n";
|
|
1595
|
+
}
|
|
1596
|
+
function readSem(path) {
|
|
1597
|
+
const p = path ?? resolveSemPathForRead();
|
|
1598
|
+
if (!p || !existsSync2(p)) return void 0;
|
|
1599
|
+
return { path: p, values: parseSem(readFileSync2(p, "utf8")) };
|
|
1600
|
+
}
|
|
1601
|
+
function writeSem(values, path = localSemPath()) {
|
|
1602
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1603
|
+
writeFileSync2(path, serializeSem(values));
|
|
1604
|
+
ensureSemIgnored(path);
|
|
1605
|
+
return path;
|
|
1606
|
+
}
|
|
1607
|
+
function ignoresSem(content) {
|
|
1608
|
+
return content.split("\n").some((line) => {
|
|
1609
|
+
const t = line.trim();
|
|
1610
|
+
return t === ".sem" || t === "/.sem" || t === "**/.sem";
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
function resolveGitignoreTarget(startDir) {
|
|
1614
|
+
let dir = startDir;
|
|
1615
|
+
for (; ; ) {
|
|
1616
|
+
const gi = join2(dir, ".gitignore");
|
|
1617
|
+
if (existsSync2(gi)) return { path: gi, exists: true };
|
|
1618
|
+
const parent = dirname2(dir);
|
|
1619
|
+
if (existsSync2(join2(dir, ".git")) || parent === dir) {
|
|
1620
|
+
return { path: join2(startDir, ".gitignore"), exists: false };
|
|
1621
|
+
}
|
|
1622
|
+
dir = parent;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
function ensureSemIgnored(semPath) {
|
|
1626
|
+
try {
|
|
1627
|
+
const target = resolveGitignoreTarget(dirname2(semPath));
|
|
1628
|
+
if (target.exists) {
|
|
1629
|
+
const content = readFileSync2(target.path, "utf8");
|
|
1630
|
+
if (ignoresSem(content)) return;
|
|
1631
|
+
const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
1632
|
+
appendFileSync(target.path, `${sep}.sem
|
|
1633
|
+
`);
|
|
1634
|
+
} else {
|
|
1635
|
+
writeFileSync2(target.path, ".sem\n");
|
|
1636
|
+
}
|
|
1637
|
+
} catch {
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// src/commands/hook.ts
|
|
1642
|
+
async function readStdin() {
|
|
1643
|
+
if (process.stdin.isTTY) return "";
|
|
1644
|
+
const chunks = [];
|
|
1645
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
1646
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1647
|
+
}
|
|
1648
|
+
function parseHookInput(raw) {
|
|
1649
|
+
if (!raw.trim()) return {};
|
|
1650
|
+
try {
|
|
1651
|
+
return JSON.parse(raw);
|
|
1652
|
+
} catch {
|
|
1653
|
+
return {};
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
function resolveLane(flagLane, cwd) {
|
|
1657
|
+
if (flagLane) return flagLane;
|
|
1658
|
+
const env = process.env.SECHROOM_LANE;
|
|
1659
|
+
if (env) return env;
|
|
1660
|
+
const start = cwd ?? process.cwd();
|
|
1661
|
+
const sem = readSem(resolveSemPathForRead(start));
|
|
1662
|
+
return sem?.values["code-lane"];
|
|
1663
|
+
}
|
|
1664
|
+
function formatContext(bundle, lane) {
|
|
1665
|
+
const s = bundle?.latestSnapshot;
|
|
1666
|
+
if (!s) return null;
|
|
1667
|
+
const lines = [];
|
|
1668
|
+
lines.push(`[sechroom continuity \u2014 resumed lane ${lane}]`);
|
|
1669
|
+
if (s.currentObjective) lines.push(`Objective: ${s.currentObjective}`);
|
|
1670
|
+
if (s.currentState) lines.push(`State: ${s.currentState}`);
|
|
1671
|
+
if (s.lastMeaningfulAction) lines.push(`Last action: ${s.lastMeaningfulAction}`);
|
|
1672
|
+
if (s.nextIntendedAction) lines.push(`Next: ${s.nextIntendedAction}`);
|
|
1673
|
+
if (s.resumeInstruction) lines.push(`Resume: ${s.resumeInstruction}`);
|
|
1674
|
+
const constraints = s.activeConstraints ?? [];
|
|
1675
|
+
if (constraints.length) {
|
|
1676
|
+
lines.push("Active constraints:");
|
|
1677
|
+
for (const c of constraints) lines.push(` - ${c}`);
|
|
1678
|
+
}
|
|
1679
|
+
const questions = s.openQuestions ?? [];
|
|
1680
|
+
if (questions.length) {
|
|
1681
|
+
lines.push("Open questions:");
|
|
1682
|
+
for (const q of questions) lines.push(` - ${q}`);
|
|
1683
|
+
}
|
|
1684
|
+
const artifacts = s.relevantArtifactIds ?? [];
|
|
1685
|
+
if (artifacts.length) lines.push(`Relevant artifacts: ${artifacts.join(", ")}`);
|
|
1686
|
+
const marker = [s.id, s.createdAt].filter(Boolean).join(" @ ");
|
|
1687
|
+
if (marker) lines.push(`(snapshot ${marker})`);
|
|
1688
|
+
return lines.join("\n");
|
|
1689
|
+
}
|
|
1690
|
+
function emitSessionStart(additionalContext) {
|
|
1691
|
+
process.stdout.write(
|
|
1692
|
+
JSON.stringify({
|
|
1693
|
+
hookSpecificOutput: {
|
|
1694
|
+
hookEventName: "SessionStart",
|
|
1695
|
+
additionalContext
|
|
1696
|
+
}
|
|
1697
|
+
}) + "\n"
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
function registerHook(program2) {
|
|
1701
|
+
const hook = program2.command("hook").description("Agent-lifecycle hook adapter (Claude Code / Codex) \u2014 bridges hooks to continuity");
|
|
1702
|
+
hook.addHelpText(
|
|
1703
|
+
"after",
|
|
1704
|
+
`
|
|
1705
|
+
Examples:
|
|
1706
|
+
# Wire into a SessionStart hook (the surface pipes the hook payload on stdin):
|
|
1707
|
+
$ echo '{"hook_event_name":"SessionStart","cwd":"'"$PWD"'"}' | sechroom hook session-start
|
|
1708
|
+
$ sechroom hook session-start --lane claude-code-chris override the .sem pin
|
|
1709
|
+
|
|
1710
|
+
Lane source (high -> low): --lane > SECHROOM_LANE > ./.sem code-lane (D-binding-5).
|
|
1711
|
+
Fail-soft: no lane / no auth / API error -> exit 0, no context injected, never blocks.`
|
|
1712
|
+
);
|
|
1713
|
+
hook.command("session-start").description("Resume the checkout's lane and emit continuity context for a SessionStart hook").option("--lane <laneId>", "Override the resolved lane (else SECHROOM_LANE, else ./.sem code-lane)").option("--surface <surface>", "Target surface: claude | codex (output is identical for session-start)", "claude").option("--max-artifacts <n>", "Cap artifacts in the resume bundle").action(async (opts, cmd) => {
|
|
1714
|
+
try {
|
|
1715
|
+
const raw = await readStdin();
|
|
1716
|
+
const input = parseHookInput(raw);
|
|
1717
|
+
const lane = resolveLane(opts.lane, input.cwd);
|
|
1718
|
+
if (!lane) return process.exit(0);
|
|
1719
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1720
|
+
const client = await makeClient(cfg);
|
|
1721
|
+
const { data } = await client.POST("/continuity/resume/lane", {
|
|
1722
|
+
body: {
|
|
1723
|
+
laneId: lane,
|
|
1724
|
+
workspaceId: null,
|
|
1725
|
+
maxArtifacts: opts.maxArtifacts != null ? Number(opts.maxArtifacts) : null,
|
|
1726
|
+
includeLookingAtMyself: null,
|
|
1727
|
+
changedSince: null
|
|
1728
|
+
}
|
|
1729
|
+
});
|
|
1730
|
+
const context = formatContext(data, lane);
|
|
1731
|
+
if (context) emitSessionStart(context);
|
|
1732
|
+
return process.exit(0);
|
|
1733
|
+
} catch {
|
|
1734
|
+
return process.exit(0);
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1561
1739
|
// src/commands/account.ts
|
|
1562
1740
|
function registerId(program2) {
|
|
1563
1741
|
const id = program2.command("id").description("Allocate human-authored id sequences (FR-*, D-*)");
|
|
@@ -1775,8 +1953,8 @@ Examples:
|
|
|
1775
1953
|
|
|
1776
1954
|
// src/setup/apply.ts
|
|
1777
1955
|
import { createHash as createHash2 } from "crypto";
|
|
1778
|
-
import { mkdirSync as
|
|
1779
|
-
import { dirname as
|
|
1956
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3 } from "fs";
|
|
1957
|
+
import { dirname as dirname3 } from "path";
|
|
1780
1958
|
|
|
1781
1959
|
// src/setup/operator-surface.ts
|
|
1782
1960
|
var SectionType = {
|
|
@@ -1950,22 +2128,22 @@ function parseManagedBlock(content, block) {
|
|
|
1950
2128
|
return null;
|
|
1951
2129
|
}
|
|
1952
2130
|
function ensureDir2(path) {
|
|
1953
|
-
|
|
2131
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
1954
2132
|
}
|
|
1955
2133
|
function readOr(path, fallback) {
|
|
1956
2134
|
try {
|
|
1957
|
-
return
|
|
2135
|
+
return readFileSync3(path, "utf8");
|
|
1958
2136
|
} catch {
|
|
1959
2137
|
return fallback;
|
|
1960
2138
|
}
|
|
1961
2139
|
}
|
|
1962
2140
|
function mergeMcpJson(path, snippet, dryRun) {
|
|
1963
2141
|
const incoming = JSON.parse(snippet);
|
|
1964
|
-
const existed =
|
|
2142
|
+
const existed = existsSync3(path);
|
|
1965
2143
|
let current = {};
|
|
1966
2144
|
if (existed) {
|
|
1967
2145
|
try {
|
|
1968
|
-
current = JSON.parse(
|
|
2146
|
+
current = JSON.parse(readFileSync3(path, "utf8"));
|
|
1969
2147
|
} catch {
|
|
1970
2148
|
return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
|
|
1971
2149
|
}
|
|
@@ -1973,26 +2151,26 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
1973
2151
|
current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
|
|
1974
2152
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
1975
2153
|
ensureDir2(path);
|
|
1976
|
-
|
|
2154
|
+
writeFileSync3(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
1977
2155
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
1978
2156
|
}
|
|
1979
2157
|
function mergeCodexToml(path, snippet, dryRun) {
|
|
1980
|
-
const existed =
|
|
2158
|
+
const existed = existsSync3(path);
|
|
1981
2159
|
let body = readOr(path, "");
|
|
1982
2160
|
body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
|
|
1983
2161
|
const trimmed = body.trim();
|
|
1984
2162
|
const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
|
|
1985
2163
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
1986
2164
|
ensureDir2(path);
|
|
1987
|
-
|
|
2165
|
+
writeFileSync3(path, next, { mode: 384 });
|
|
1988
2166
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
1989
2167
|
}
|
|
1990
2168
|
function writeInstructionBlock(path, write, dryRun) {
|
|
1991
|
-
const existed =
|
|
2169
|
+
const existed = existsSync3(path);
|
|
1992
2170
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
1993
2171
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
1994
2172
|
ensureDir2(path);
|
|
1995
|
-
|
|
2173
|
+
writeFileSync3(path, next);
|
|
1996
2174
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
1997
2175
|
}
|
|
1998
2176
|
function computeBlockFile(current, write) {
|
|
@@ -2033,7 +2211,7 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
2033
2211
|
const next = computeBlockFile(current, write);
|
|
2034
2212
|
if (!dryRun) {
|
|
2035
2213
|
ensureDir2(proposedPath);
|
|
2036
|
-
|
|
2214
|
+
writeFileSync3(proposedPath, next);
|
|
2037
2215
|
}
|
|
2038
2216
|
return {
|
|
2039
2217
|
kind: "instruction",
|
|
@@ -2104,17 +2282,17 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
2104
2282
|
}
|
|
2105
2283
|
|
|
2106
2284
|
// src/setup/clients.ts
|
|
2107
|
-
import { existsSync as
|
|
2285
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2108
2286
|
import { homedir as homedir2 } from "os";
|
|
2109
|
-
import { dirname as
|
|
2287
|
+
import { dirname as dirname4, join as join3 } from "path";
|
|
2110
2288
|
function claudeDesktopConfigPath(home) {
|
|
2111
2289
|
switch (process.platform) {
|
|
2112
2290
|
case "darwin":
|
|
2113
|
-
return
|
|
2291
|
+
return join3(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
2114
2292
|
case "win32":
|
|
2115
|
-
return
|
|
2293
|
+
return join3(process.env.APPDATA ?? join3(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
2116
2294
|
default:
|
|
2117
|
-
return
|
|
2295
|
+
return join3(home, ".config", "Claude", "claude_desktop_config.json");
|
|
2118
2296
|
}
|
|
2119
2297
|
}
|
|
2120
2298
|
function clientTargets(cwd) {
|
|
@@ -2123,26 +2301,26 @@ function clientTargets(cwd) {
|
|
|
2123
2301
|
"claude-code": {
|
|
2124
2302
|
key: "claude-code",
|
|
2125
2303
|
label: "Claude Code",
|
|
2126
|
-
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path:
|
|
2127
|
-
instruction: { surfaceKey: "claude-code", path:
|
|
2304
|
+
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join3(cwd, ".mcp.json"), format: "json" },
|
|
2305
|
+
instruction: { surfaceKey: "claude-code", path: join3(cwd, "CLAUDE.md") }
|
|
2128
2306
|
},
|
|
2129
2307
|
"claude-desktop": {
|
|
2130
2308
|
key: "claude-desktop",
|
|
2131
2309
|
label: "Claude Desktop",
|
|
2132
2310
|
mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
|
|
2133
|
-
instruction: { surfaceKey: "claude-desktop", path:
|
|
2311
|
+
instruction: { surfaceKey: "claude-desktop", path: join3(home, ".claude", "CLAUDE.md") }
|
|
2134
2312
|
},
|
|
2135
2313
|
codex: {
|
|
2136
2314
|
key: "codex",
|
|
2137
2315
|
label: "Codex CLI",
|
|
2138
|
-
mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path:
|
|
2139
|
-
instruction: { surfaceKey: "chatgpt", path:
|
|
2316
|
+
mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join3(home, ".codex", "config.toml"), format: "toml" },
|
|
2317
|
+
instruction: { surfaceKey: "chatgpt", path: join3(cwd, "AGENTS.md") }
|
|
2140
2318
|
},
|
|
2141
2319
|
cursor: {
|
|
2142
2320
|
key: "cursor",
|
|
2143
2321
|
label: "Cursor",
|
|
2144
|
-
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path:
|
|
2145
|
-
instruction: { surfaceKey: "chatgpt", path:
|
|
2322
|
+
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join3(cwd, ".cursor", "mcp.json"), format: "json" },
|
|
2323
|
+
instruction: { surfaceKey: "chatgpt", path: join3(cwd, "AGENTS.md") }
|
|
2146
2324
|
}
|
|
2147
2325
|
};
|
|
2148
2326
|
}
|
|
@@ -2151,57 +2329,91 @@ var DEFAULT_CLIENT_KEY = "claude-code";
|
|
|
2151
2329
|
function detectInstalledClients(cwd) {
|
|
2152
2330
|
const home = homedir2();
|
|
2153
2331
|
const detected = [];
|
|
2154
|
-
if (
|
|
2155
|
-
if (
|
|
2156
|
-
if (
|
|
2157
|
-
if (
|
|
2332
|
+
if (existsSync4(join3(home, ".claude"))) detected.push("claude-code");
|
|
2333
|
+
if (existsSync4(dirname4(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
|
|
2334
|
+
if (existsSync4(join3(home, ".codex"))) detected.push("codex");
|
|
2335
|
+
if (existsSync4(join3(home, ".cursor")) || existsSync4(join3(cwd, ".cursor"))) detected.push("cursor");
|
|
2158
2336
|
return detected;
|
|
2159
2337
|
}
|
|
2160
2338
|
|
|
2161
2339
|
// src/setup/skills-offer.ts
|
|
2162
|
-
import { mkdirSync as
|
|
2340
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
2163
2341
|
import { homedir as homedir3 } from "os";
|
|
2164
2342
|
import { join as join4 } from "path";
|
|
2165
2343
|
|
|
2166
|
-
// src/
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2344
|
+
// src/setup/lane-pin.ts
|
|
2345
|
+
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
2346
|
+
"claude-code": "claude-code",
|
|
2347
|
+
"claude-desktop": "claude-code",
|
|
2348
|
+
cursor: "claude-code",
|
|
2349
|
+
codex: "codex"
|
|
2350
|
+
};
|
|
2351
|
+
var CLIENT_PRIORITY = ["claude-code", "claude-desktop", "cursor", "codex"];
|
|
2352
|
+
function handleFromDisplayName(name) {
|
|
2353
|
+
if (!name) return void 0;
|
|
2354
|
+
const localPart = name.trim().split("@")[0] ?? "";
|
|
2355
|
+
const first = localPart.split(/[\s._-]+/)[0]?.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2356
|
+
return first || void 0;
|
|
2357
|
+
}
|
|
2358
|
+
function codeLanePrefix(clients) {
|
|
2359
|
+
for (const c of CLIENT_PRIORITY) if (clients.includes(c)) return CODE_LANE_PREFIX_BY_CLIENT[c];
|
|
2360
|
+
return "claude-code";
|
|
2361
|
+
}
|
|
2362
|
+
function writePin(code, design) {
|
|
2363
|
+
const values = {};
|
|
2364
|
+
if (code) values["code-lane"] = code;
|
|
2365
|
+
if (design) values["design-lane"] = design;
|
|
2366
|
+
if (Object.keys(values).length === 0) return;
|
|
2367
|
+
const target = writeSem(values);
|
|
2368
|
+
process.stderr.write(`${ok("\u2713")} lane pin written \u2192 ${target} ${style.dim("(./.sem, git-ignored)")}
|
|
2369
|
+
`);
|
|
2172
2370
|
}
|
|
2173
|
-
function
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2371
|
+
async function ensureLanePin(cfg, opts) {
|
|
2372
|
+
if (opts.dryRun) return;
|
|
2373
|
+
if (readSem()) return;
|
|
2374
|
+
let wf;
|
|
2375
|
+
let profile;
|
|
2376
|
+
try {
|
|
2377
|
+
const client = await makeClient(cfg);
|
|
2378
|
+
[wf, profile] = await Promise.all([
|
|
2379
|
+
client.GET("/me/workflow-preferences", {}).then((r) => r.data).catch(() => void 0),
|
|
2380
|
+
client.GET("/me/profile", {}).then((r) => r.data).catch(() => void 0)
|
|
2381
|
+
]);
|
|
2382
|
+
} catch {
|
|
2181
2383
|
}
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
const
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
if (
|
|
2188
|
-
|
|
2189
|
-
if (eq === -1) continue;
|
|
2190
|
-
const key = line.slice(0, eq).trim();
|
|
2191
|
-
const value = line.slice(eq + 1).trim();
|
|
2192
|
-
if (key) out[key] = value;
|
|
2384
|
+
const handle = handleFromDisplayName(profile?.effectiveDisplayName);
|
|
2385
|
+
const prefix = codeLanePrefix(opts.clients ?? ["claude-code"]);
|
|
2386
|
+
const codeGuess = wf?.defaultCodeLane ?? (handle ? `${prefix}-${handle}` : void 0);
|
|
2387
|
+
const designGuess = wf?.defaultDesignLane ?? (handle ? `claude-design-${handle}` : void 0);
|
|
2388
|
+
if (!canPrompt() || opts.yes) {
|
|
2389
|
+
if (opts.yes && (codeGuess || designGuess)) writePin(codeGuess, designGuess);
|
|
2390
|
+
return;
|
|
2193
2391
|
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2392
|
+
if (codeGuess || designGuess) {
|
|
2393
|
+
process.stderr.write(
|
|
2394
|
+
`
|
|
2395
|
+
I can pin this checkout's lane so operator skills + the continuity hook resolve your identity:
|
|
2396
|
+
`
|
|
2397
|
+
);
|
|
2398
|
+
if (codeGuess) process.stderr.write(` ${style.dim("code-lane")} = ${style.cyan(codeGuess)}
|
|
2399
|
+
`);
|
|
2400
|
+
if (designGuess) process.stderr.write(` ${style.dim("design-lane")} = ${style.cyan(designGuess)}
|
|
2401
|
+
`);
|
|
2402
|
+
if (await promptYesNo("Pin these?")) {
|
|
2403
|
+
writePin(codeGuess, designGuess);
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
const code = await promptText("Code-lane id (e.g. claude-code-you, blank to skip)?", codeGuess ?? "");
|
|
2408
|
+
const design = await promptText("Design-lane id (e.g. claude-design-you, blank to skip)?", designGuess ?? "");
|
|
2409
|
+
if (!code && !design) {
|
|
2410
|
+
process.stderr.write(
|
|
2411
|
+
` ${style.dim("skipped \u2014 set later with")} ${style.cyan("sechroom skills set-lane --code-lane \u2026 --design-lane \u2026")}
|
|
2412
|
+
`
|
|
2413
|
+
);
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
writePin(code || void 0, design || void 0);
|
|
2205
2417
|
}
|
|
2206
2418
|
|
|
2207
2419
|
// src/setup/skills-offer.ts
|
|
@@ -2248,37 +2460,13 @@ Found ${style.bold(String(names.length))} operator skill(s) installed in your wo
|
|
|
2248
2460
|
const written = [];
|
|
2249
2461
|
for (const [name, m] of byName) {
|
|
2250
2462
|
const body = m.text ?? m.Text ?? "";
|
|
2251
|
-
|
|
2252
|
-
|
|
2463
|
+
mkdirSync4(join4(dir, name), { recursive: true });
|
|
2464
|
+
writeFileSync4(join4(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
|
|
2253
2465
|
written.push(name);
|
|
2254
2466
|
}
|
|
2255
2467
|
process.stderr.write(`${style.green("\u2713")} wrote ${written.length} skill(s) to ${dir}
|
|
2256
2468
|
`);
|
|
2257
|
-
|
|
2258
|
-
const setLane = opts.yes ? false : canPrompt() ? await promptYesNo("Set your lane now so the skills can resolve their identity slots?") : false;
|
|
2259
|
-
if (!setLane) {
|
|
2260
|
-
process.stderr.write(
|
|
2261
|
-
` ${style.dim("run")} ${style.cyan("sechroom skills set-lane --code-lane \u2026 --design-lane \u2026")} ${style.dim("when ready.")}
|
|
2262
|
-
`
|
|
2263
|
-
);
|
|
2264
|
-
return;
|
|
2265
|
-
}
|
|
2266
|
-
let wf;
|
|
2267
|
-
try {
|
|
2268
|
-
const client = await makeClient(cfg);
|
|
2269
|
-
wf = await client.GET("/me/workflow-preferences", {}).then((r) => r.data).catch(() => void 0);
|
|
2270
|
-
} catch {
|
|
2271
|
-
}
|
|
2272
|
-
const code = await promptText("Code-lane id (e.g. claude-code-you)?", wf?.defaultCodeLane);
|
|
2273
|
-
const design = await promptText("Design-lane id (e.g. claude-design-you)?", wf?.defaultDesignLane);
|
|
2274
|
-
const values = {};
|
|
2275
|
-
if (code) values["code-lane"] = code;
|
|
2276
|
-
if (design) values["design-lane"] = design;
|
|
2277
|
-
if (Object.keys(values).length === 0) return;
|
|
2278
|
-
const target = localSemPath();
|
|
2279
|
-
writeFileSync3(target, serializeSem(values));
|
|
2280
|
-
process.stderr.write(`${style.green("\u2713")} lane pin written \u2192 ${target}
|
|
2281
|
-
`);
|
|
2469
|
+
await ensureLanePin(cfg, { yes: opts.yes, dryRun: opts.dryRun, clients: [surface] });
|
|
2282
2470
|
}
|
|
2283
2471
|
|
|
2284
2472
|
// src/commands/setup.ts
|
|
@@ -2389,36 +2577,40 @@ Next \u2014 verify: ${verify.description}
|
|
|
2389
2577
|
}
|
|
2390
2578
|
function registerSetup(program2) {
|
|
2391
2579
|
const setup = program2.command("setup").description("Granular onboarding steps (init runs these together)");
|
|
2392
|
-
setup.command("mcp <
|
|
2393
|
-
await
|
|
2580
|
+
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) => {
|
|
2581
|
+
await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: true, agentFiles: false });
|
|
2394
2582
|
});
|
|
2395
|
-
setup.command("agent-files <
|
|
2396
|
-
await
|
|
2583
|
+
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) => {
|
|
2584
|
+
await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: false, agentFiles: true, copy: opts.copy });
|
|
2397
2585
|
});
|
|
2398
2586
|
}
|
|
2399
|
-
async function
|
|
2587
|
+
async function runClients(clients, cmd, opts) {
|
|
2400
2588
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2401
2589
|
const targets = clientTargets(process.cwd());
|
|
2402
|
-
const
|
|
2403
|
-
if (!target) fail(`unknown client '${client}'. Known: ${ALL_CLIENT_KEYS.join(", ")}.`);
|
|
2590
|
+
const keys = resolveClientKeys(clients.join(","));
|
|
2404
2591
|
const setupData = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
|
|
2405
2592
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
2406
2593
|
if (opts.agentFiles && !opts.dryRun) {
|
|
2407
|
-
await maybeOfferCopies(cfg, setupData, targets,
|
|
2594
|
+
await maybeOfferCopies(cfg, setupData, targets, keys, personalWorkspaceId, copyChoice(opts));
|
|
2408
2595
|
}
|
|
2409
|
-
const actions = await applyClient(cfg, setupData, target, {
|
|
2410
|
-
dryRun: opts.dryRun,
|
|
2411
|
-
mcp: opts.mcp,
|
|
2412
|
-
agentFiles: opts.agentFiles,
|
|
2413
|
-
personalWorkspaceId
|
|
2414
|
-
});
|
|
2415
2596
|
const json = cmd.optsWithGlobals().json;
|
|
2597
|
+
const result = [];
|
|
2598
|
+
for (const key of keys) {
|
|
2599
|
+
const target = targets[key];
|
|
2600
|
+
const actions = await applyClient(cfg, setupData, target, {
|
|
2601
|
+
dryRun: opts.dryRun,
|
|
2602
|
+
mcp: opts.mcp,
|
|
2603
|
+
agentFiles: opts.agentFiles,
|
|
2604
|
+
personalWorkspaceId
|
|
2605
|
+
});
|
|
2606
|
+
result.push({ client: key, actions });
|
|
2607
|
+
if (!json) printActions(target, actions);
|
|
2608
|
+
}
|
|
2416
2609
|
if (json) {
|
|
2417
|
-
emit({ dryRun: opts.dryRun,
|
|
2418
|
-
|
|
2419
|
-
printActions(target, actions);
|
|
2420
|
-
process.stdout.write(opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n");
|
|
2610
|
+
emit({ dryRun: opts.dryRun, clients: result }, true);
|
|
2611
|
+
return;
|
|
2421
2612
|
}
|
|
2613
|
+
process.stdout.write(opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n");
|
|
2422
2614
|
}
|
|
2423
2615
|
|
|
2424
2616
|
// src/commands/onboard.ts
|
|
@@ -2712,12 +2904,14 @@ Examples:
|
|
|
2712
2904
|
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, clients: [] }, true);
|
|
2713
2905
|
return;
|
|
2714
2906
|
}
|
|
2907
|
+
if (!dryRun) await ensureLanePin(cfg, { yes, dryRun, clients: detectInstalledClients(process.cwd()) });
|
|
2715
2908
|
process.stdout.write(
|
|
2716
2909
|
`
|
|
2717
2910
|
${style.bold("Done.")} The CLI is configured for ${style.cyan(cfg.tenant)} \u2014 no AI-client files written.
|
|
2718
2911
|
Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom --help")}
|
|
2719
2912
|
`
|
|
2720
2913
|
);
|
|
2914
|
+
await printStarterPrompt("cli");
|
|
2721
2915
|
return;
|
|
2722
2916
|
}
|
|
2723
2917
|
const keys = await chooseClients(opts.client, yes, process.cwd());
|
|
@@ -2762,6 +2956,9 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
2762
2956
|
}
|
|
2763
2957
|
process.exit(wouldChange === 0 ? 0 : 1);
|
|
2764
2958
|
}
|
|
2959
|
+
if (!json && !dryRun) {
|
|
2960
|
+
await ensureLanePin(cfg, { yes, dryRun, clients: keys });
|
|
2961
|
+
}
|
|
2765
2962
|
if (!json && !dryRun) {
|
|
2766
2963
|
await maybeOfferSkills(cfg, personalWorkspaceId, { yes, dryRun, surface: "claude-code" });
|
|
2767
2964
|
}
|
|
@@ -2790,6 +2987,7 @@ ${style.bold("Done.")} Restart your AI client (or reload MCP) to pick up the new
|
|
|
2790
2987
|
${style.bold("Done.")} Agent instructions written (no MCP config).
|
|
2791
2988
|
`
|
|
2792
2989
|
);
|
|
2990
|
+
if (!dryRun) await printStarterPrompt("agent", cfg);
|
|
2793
2991
|
});
|
|
2794
2992
|
}
|
|
2795
2993
|
async function chooseWire(opts, yes) {
|
|
@@ -2807,11 +3005,38 @@ async function chooseWire(opts, yes) {
|
|
|
2807
3005
|
}
|
|
2808
3006
|
return opts.mcp === false ? "agent-only" : "full";
|
|
2809
3007
|
}
|
|
3008
|
+
var FALLBACK_AGENT_PROMPT = "Resume my sechroom continuity, summarise what I was last working on, then suggest the next step.";
|
|
3009
|
+
async function printStarterPrompt(mode, cfg) {
|
|
3010
|
+
if (mode === "cli") {
|
|
3011
|
+
process.stdout.write(
|
|
3012
|
+
`
|
|
3013
|
+
${style.bold("Next:")} pick up where you left off \u2014
|
|
3014
|
+
${style.cyan("sechroom continuity resume-me")}
|
|
3015
|
+
`
|
|
3016
|
+
);
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
let primary = FALLBACK_AGENT_PROMPT;
|
|
3020
|
+
if (cfg) {
|
|
3021
|
+
try {
|
|
3022
|
+
const client = await makeClient(cfg);
|
|
3023
|
+
const { data } = await client.GET("/me/onboarding/starter-prompt", {});
|
|
3024
|
+
if (data?.primary) primary = data.primary;
|
|
3025
|
+
} catch {
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
process.stdout.write(
|
|
3029
|
+
`
|
|
3030
|
+
${style.bold("Next:")} paste this into your AI agent to get going \u2014
|
|
3031
|
+
${style.cyan(`"${primary}"`)}
|
|
3032
|
+
`
|
|
3033
|
+
);
|
|
3034
|
+
}
|
|
2810
3035
|
|
|
2811
3036
|
// src/commands/skills.ts
|
|
2812
3037
|
import { homedir as homedir4 } from "os";
|
|
2813
|
-
import {
|
|
2814
|
-
import { mkdirSync as
|
|
3038
|
+
import { join as join5 } from "path";
|
|
3039
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, rmSync as rmSync2, existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
2815
3040
|
var DEFAULT_SLUG = "operator-skills";
|
|
2816
3041
|
var ROLE_TAGS = ["sechroom:role:skill-template", "role:skill-template"];
|
|
2817
3042
|
var LOCK = ".sechroom-skills.json";
|
|
@@ -2894,15 +3119,15 @@ Examples:
|
|
|
2894
3119
|
const name = tagValue2(tags, "skill:");
|
|
2895
3120
|
if (!name) continue;
|
|
2896
3121
|
const body = m.text ?? m.Text ?? "";
|
|
2897
|
-
|
|
2898
|
-
|
|
3122
|
+
mkdirSync5(join5(dir, name), { recursive: true });
|
|
3123
|
+
writeFileSync5(join5(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
|
|
2899
3124
|
written.push(name);
|
|
2900
3125
|
}
|
|
2901
|
-
|
|
3126
|
+
mkdirSync5(dir, { recursive: true });
|
|
2902
3127
|
const lockPath = join5(dir, LOCK);
|
|
2903
3128
|
const lock = existsSync5(lockPath) ? JSON.parse(readFileSync4(lockPath, "utf8")) : {};
|
|
2904
3129
|
lock[slug] = { surface: opts.surface, version, instance: wantInstance, skills: written.sort() };
|
|
2905
|
-
|
|
3130
|
+
writeFileSync5(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
2906
3131
|
if (opts.json) return emit({ slug, version, instance: wantInstance, surface: opts.surface, dir, installed: written }, true);
|
|
2907
3132
|
const instanceNote = opts.instance ? ` (${opts.instance})` : "";
|
|
2908
3133
|
console.log(style.green(`Installed ${slug}@${version}${instanceNote} \u2014 ${written.length} skill(s) \u2192 ${dir}`));
|
|
@@ -2938,7 +3163,7 @@ Examples:
|
|
|
2938
3163
|
}
|
|
2939
3164
|
}
|
|
2940
3165
|
delete lock[slug];
|
|
2941
|
-
|
|
3166
|
+
writeFileSync5(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
2942
3167
|
if (opts.json) return emit({ slug, removed, dir }, true);
|
|
2943
3168
|
console.log(style.green(`Removed ${removed.length} skill(s) for ${slug} from ${dir}`));
|
|
2944
3169
|
});
|
|
@@ -2948,10 +3173,9 @@ Examples:
|
|
|
2948
3173
|
const values = readSem(target)?.values ?? {};
|
|
2949
3174
|
if (opts.codeLane) values["code-lane"] = opts.codeLane;
|
|
2950
3175
|
if (opts.designLane) values["design-lane"] = opts.designLane;
|
|
2951
|
-
|
|
2952
|
-
writeFileSync4(target, serializeSem(values));
|
|
3176
|
+
writeSem(values, target);
|
|
2953
3177
|
if (cmd.optsWithGlobals().json) return emit({ path: target, values }, true);
|
|
2954
|
-
console.log(style.green(`Wrote lane pin \u2192 ${target}`));
|
|
3178
|
+
console.log(style.green(`Wrote lane pin \u2192 ${target} ${style.dim("(git-ignored)")}`));
|
|
2955
3179
|
Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
|
|
2956
3180
|
});
|
|
2957
3181
|
skills.command("lane").description("Show the lane pin resolved from ./.sem (nearest in this checkout)").option("--json", "machine output").action((opts, cmd) => {
|
|
@@ -3217,6 +3441,7 @@ registerWorkspace(program);
|
|
|
3217
3441
|
registerProject(program);
|
|
3218
3442
|
registerFiling(program);
|
|
3219
3443
|
registerContinuity(program);
|
|
3444
|
+
registerHook(program);
|
|
3220
3445
|
registerId(program);
|
|
3221
3446
|
registerAccount(program);
|
|
3222
3447
|
registerChat(program);
|
package/package.json
CHANGED