@sechroom/cli 2026.6.17 → 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 +323 -102
- 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
|
|
@@ -2716,12 +2904,14 @@ Examples:
|
|
|
2716
2904
|
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, clients: [] }, true);
|
|
2717
2905
|
return;
|
|
2718
2906
|
}
|
|
2907
|
+
if (!dryRun) await ensureLanePin(cfg, { yes, dryRun, clients: detectInstalledClients(process.cwd()) });
|
|
2719
2908
|
process.stdout.write(
|
|
2720
2909
|
`
|
|
2721
2910
|
${style.bold("Done.")} The CLI is configured for ${style.cyan(cfg.tenant)} \u2014 no AI-client files written.
|
|
2722
2911
|
Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom --help")}
|
|
2723
2912
|
`
|
|
2724
2913
|
);
|
|
2914
|
+
await printStarterPrompt("cli");
|
|
2725
2915
|
return;
|
|
2726
2916
|
}
|
|
2727
2917
|
const keys = await chooseClients(opts.client, yes, process.cwd());
|
|
@@ -2766,6 +2956,9 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
2766
2956
|
}
|
|
2767
2957
|
process.exit(wouldChange === 0 ? 0 : 1);
|
|
2768
2958
|
}
|
|
2959
|
+
if (!json && !dryRun) {
|
|
2960
|
+
await ensureLanePin(cfg, { yes, dryRun, clients: keys });
|
|
2961
|
+
}
|
|
2769
2962
|
if (!json && !dryRun) {
|
|
2770
2963
|
await maybeOfferSkills(cfg, personalWorkspaceId, { yes, dryRun, surface: "claude-code" });
|
|
2771
2964
|
}
|
|
@@ -2794,6 +2987,7 @@ ${style.bold("Done.")} Restart your AI client (or reload MCP) to pick up the new
|
|
|
2794
2987
|
${style.bold("Done.")} Agent instructions written (no MCP config).
|
|
2795
2988
|
`
|
|
2796
2989
|
);
|
|
2990
|
+
if (!dryRun) await printStarterPrompt("agent", cfg);
|
|
2797
2991
|
});
|
|
2798
2992
|
}
|
|
2799
2993
|
async function chooseWire(opts, yes) {
|
|
@@ -2811,11 +3005,38 @@ async function chooseWire(opts, yes) {
|
|
|
2811
3005
|
}
|
|
2812
3006
|
return opts.mcp === false ? "agent-only" : "full";
|
|
2813
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
|
+
}
|
|
2814
3035
|
|
|
2815
3036
|
// src/commands/skills.ts
|
|
2816
3037
|
import { homedir as homedir4 } from "os";
|
|
2817
|
-
import {
|
|
2818
|
-
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";
|
|
2819
3040
|
var DEFAULT_SLUG = "operator-skills";
|
|
2820
3041
|
var ROLE_TAGS = ["sechroom:role:skill-template", "role:skill-template"];
|
|
2821
3042
|
var LOCK = ".sechroom-skills.json";
|
|
@@ -2898,15 +3119,15 @@ Examples:
|
|
|
2898
3119
|
const name = tagValue2(tags, "skill:");
|
|
2899
3120
|
if (!name) continue;
|
|
2900
3121
|
const body = m.text ?? m.Text ?? "";
|
|
2901
|
-
|
|
2902
|
-
|
|
3122
|
+
mkdirSync5(join5(dir, name), { recursive: true });
|
|
3123
|
+
writeFileSync5(join5(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
|
|
2903
3124
|
written.push(name);
|
|
2904
3125
|
}
|
|
2905
|
-
|
|
3126
|
+
mkdirSync5(dir, { recursive: true });
|
|
2906
3127
|
const lockPath = join5(dir, LOCK);
|
|
2907
3128
|
const lock = existsSync5(lockPath) ? JSON.parse(readFileSync4(lockPath, "utf8")) : {};
|
|
2908
3129
|
lock[slug] = { surface: opts.surface, version, instance: wantInstance, skills: written.sort() };
|
|
2909
|
-
|
|
3130
|
+
writeFileSync5(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
2910
3131
|
if (opts.json) return emit({ slug, version, instance: wantInstance, surface: opts.surface, dir, installed: written }, true);
|
|
2911
3132
|
const instanceNote = opts.instance ? ` (${opts.instance})` : "";
|
|
2912
3133
|
console.log(style.green(`Installed ${slug}@${version}${instanceNote} \u2014 ${written.length} skill(s) \u2192 ${dir}`));
|
|
@@ -2942,7 +3163,7 @@ Examples:
|
|
|
2942
3163
|
}
|
|
2943
3164
|
}
|
|
2944
3165
|
delete lock[slug];
|
|
2945
|
-
|
|
3166
|
+
writeFileSync5(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
2946
3167
|
if (opts.json) return emit({ slug, removed, dir }, true);
|
|
2947
3168
|
console.log(style.green(`Removed ${removed.length} skill(s) for ${slug} from ${dir}`));
|
|
2948
3169
|
});
|
|
@@ -2952,10 +3173,9 @@ Examples:
|
|
|
2952
3173
|
const values = readSem(target)?.values ?? {};
|
|
2953
3174
|
if (opts.codeLane) values["code-lane"] = opts.codeLane;
|
|
2954
3175
|
if (opts.designLane) values["design-lane"] = opts.designLane;
|
|
2955
|
-
|
|
2956
|
-
writeFileSync4(target, serializeSem(values));
|
|
3176
|
+
writeSem(values, target);
|
|
2957
3177
|
if (cmd.optsWithGlobals().json) return emit({ path: target, values }, true);
|
|
2958
|
-
console.log(style.green(`Wrote lane pin \u2192 ${target}`));
|
|
3178
|
+
console.log(style.green(`Wrote lane pin \u2192 ${target} ${style.dim("(git-ignored)")}`));
|
|
2959
3179
|
Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
|
|
2960
3180
|
});
|
|
2961
3181
|
skills.command("lane").description("Show the lane pin resolved from ./.sem (nearest in this checkout)").option("--json", "machine output").action((opts, cmd) => {
|
|
@@ -3221,6 +3441,7 @@ registerWorkspace(program);
|
|
|
3221
3441
|
registerProject(program);
|
|
3222
3442
|
registerFiling(program);
|
|
3223
3443
|
registerContinuity(program);
|
|
3444
|
+
registerHook(program);
|
|
3224
3445
|
registerId(program);
|
|
3225
3446
|
registerAccount(program);
|
|
3226
3447
|
registerChat(program);
|
package/package.json
CHANGED