@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.
Files changed (2) hide show
  1. package/dist/index.js +345 -120
  2. 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 mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
1779
- import { dirname as dirname2 } from "path";
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
- mkdirSync2(dirname2(path), { recursive: true });
2131
+ mkdirSync3(dirname3(path), { recursive: true });
1954
2132
  }
1955
2133
  function readOr(path, fallback) {
1956
2134
  try {
1957
- return readFileSync2(path, "utf8");
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 = existsSync2(path);
2142
+ const existed = existsSync3(path);
1965
2143
  let current = {};
1966
2144
  if (existed) {
1967
2145
  try {
1968
- current = JSON.parse(readFileSync2(path, "utf8"));
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
- writeFileSync2(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
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 = existsSync2(path);
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
- writeFileSync2(path, next, { mode: 384 });
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 = existsSync2(path);
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
- writeFileSync2(path, next);
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
- writeFileSync2(proposedPath, next);
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 existsSync3 } from "fs";
2285
+ import { existsSync as existsSync4 } from "fs";
2108
2286
  import { homedir as homedir2 } from "os";
2109
- import { dirname as dirname3, join as join2 } from "path";
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 join2(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
2291
+ return join3(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
2114
2292
  case "win32":
2115
- return join2(process.env.APPDATA ?? join2(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
2293
+ return join3(process.env.APPDATA ?? join3(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
2116
2294
  default:
2117
- return join2(home, ".config", "Claude", "claude_desktop_config.json");
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: join2(cwd, ".mcp.json"), format: "json" },
2127
- instruction: { surfaceKey: "claude-code", path: join2(cwd, "CLAUDE.md") }
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: join2(home, ".claude", "CLAUDE.md") }
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: join2(home, ".codex", "config.toml"), format: "toml" },
2139
- instruction: { surfaceKey: "chatgpt", path: join2(cwd, "AGENTS.md") }
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: join2(cwd, ".cursor", "mcp.json"), format: "json" },
2145
- instruction: { surfaceKey: "chatgpt", path: join2(cwd, "AGENTS.md") }
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 (existsSync3(join2(home, ".claude"))) detected.push("claude-code");
2155
- if (existsSync3(dirname3(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
2156
- if (existsSync3(join2(home, ".codex"))) detected.push("codex");
2157
- if (existsSync3(join2(home, ".cursor")) || existsSync3(join2(cwd, ".cursor"))) detected.push("cursor");
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 mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
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/sem.ts
2167
- import { dirname as dirname4, join as join3 } from "path";
2168
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
2169
- var SEM_FILE = ".sem";
2170
- function localSemPath(cwd = process.cwd()) {
2171
- return join3(cwd, SEM_FILE);
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 resolveSemPathForRead(start = process.cwd()) {
2174
- let dir = start;
2175
- while (true) {
2176
- const candidate = join3(dir, SEM_FILE);
2177
- if (existsSync4(candidate)) return candidate;
2178
- const parent = dirname4(dir);
2179
- if (parent === dir) return void 0;
2180
- dir = parent;
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
- function parseSem(text) {
2184
- const out = {};
2185
- for (const raw of text.split("\n")) {
2186
- const line = raw.trim();
2187
- if (!line || line.startsWith("#")) continue;
2188
- const eq = line.indexOf("=");
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
- return out;
2195
- }
2196
- function serializeSem(values) {
2197
- const header = "# sechroom lane pin (per-location fallback) \u2014 resolved at runtime by operator skills.\n";
2198
- const body = Object.entries(values).map(([k, v]) => `${k} = ${v}`).join("\n");
2199
- return header + body + "\n";
2200
- }
2201
- function readSem(path) {
2202
- const p = path ?? resolveSemPathForRead();
2203
- if (!p || !existsSync4(p)) return void 0;
2204
- return { path: p, values: parseSem(readFileSync3(p, "utf8")) };
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
- mkdirSync3(join4(dir, name), { recursive: true });
2252
- writeFileSync3(join4(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
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
- if (readSem()) return;
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 <client>").description(`Write only the MCP config for a client (${ALL_CLIENT_KEYS.join(", ")})`).option("--dry-run", "print what would be written without writing", false).action(async (client, opts, cmd) => {
2393
- await runSingle(client, cmd, { dryRun: Boolean(opts.dryRun), mcp: true, agentFiles: false });
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 <client>").description(`Write only the agent instruction file for a client (${ALL_CLIENT_KEYS.join(", ")})`).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)").action(async (client, opts, cmd) => {
2396
- await runSingle(client, cmd, { dryRun: Boolean(opts.dryRun), mcp: false, agentFiles: true, copy: opts.copy });
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 runSingle(client, cmd, opts) {
2587
+ async function runClients(clients, cmd, opts) {
2400
2588
  const cfg = resolveConfig(cmd.optsWithGlobals());
2401
2589
  const targets = clientTargets(process.cwd());
2402
- const target = targets[client];
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, [client], personalWorkspaceId, copyChoice(opts));
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, client, actions }, true);
2418
- } else {
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 { dirname as dirname5, join as join5 } from "path";
2814
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, rmSync as rmSync2, existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
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
- mkdirSync4(join5(dir, name), { recursive: true });
2898
- writeFileSync4(join5(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
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
- mkdirSync4(dir, { recursive: true });
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
- writeFileSync4(lockPath, JSON.stringify(lock, null, 2) + "\n");
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
- writeFileSync4(lockPath, JSON.stringify(lock, null, 2) + "\n");
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
- mkdirSync4(dirname5(target), { recursive: true });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sechroom/cli",
3
- "version": "2026.6.16",
3
+ "version": "2026.6.18",
4
4
  "description": "Sechroom CLI — a thin, generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",