@sechroom/cli 2026.8.2-rc.047ac2c6d → 2026.8.2

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 +1233 -177
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4366,9 +4366,19 @@ import { dirname as dirname5, join as join7 } from "path";
4366
4366
  function claudeDesktopConfigPath(home) {
4367
4367
  switch (process.platform) {
4368
4368
  case "darwin":
4369
- return join7(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
4369
+ return join7(
4370
+ home,
4371
+ "Library",
4372
+ "Application Support",
4373
+ "Claude",
4374
+ "claude_desktop_config.json"
4375
+ );
4370
4376
  case "win32":
4371
- return join7(process.env.APPDATA ?? join7(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
4377
+ return join7(
4378
+ process.env.APPDATA ?? join7(home, "AppData", "Roaming"),
4379
+ "Claude",
4380
+ "claude_desktop_config.json"
4381
+ );
4372
4382
  default:
4373
4383
  return join7(home, ".config", "Claude", "claude_desktop_config.json");
4374
4384
  }
@@ -4376,30 +4386,53 @@ function claudeDesktopConfigPath(home) {
4376
4386
  function clientTargets(cwd, opts = {}) {
4377
4387
  const home = homedir3();
4378
4388
  const claudeDir = opts.claudeDir ?? join7(home, ".claude");
4379
- const codexHome = opts.codexHome ?? join7(home, ".codex");
4389
+ const codexHome = opts.codexHome === void 0 ? join7(home, ".codex") : opts.codexHome;
4380
4390
  return {
4381
4391
  "claude-code": {
4382
4392
  key: "claude-code",
4383
4393
  label: "Claude Code",
4384
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join7(cwd, ".mcp.json"), format: "json" },
4394
+ mcp: {
4395
+ surfaceKey: "claude-code",
4396
+ sectionType: SectionType.McpConfig,
4397
+ path: join7(cwd, ".mcp.json"),
4398
+ format: "json"
4399
+ },
4385
4400
  instruction: { surfaceKey: "claude-code", path: join7(cwd, "CLAUDE.md") }
4386
4401
  },
4387
4402
  "claude-desktop": {
4388
4403
  key: "claude-desktop",
4389
4404
  label: "Claude Desktop",
4390
- mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
4391
- instruction: { surfaceKey: "claude-desktop", path: join7(claudeDir, "CLAUDE.md") }
4405
+ mcp: {
4406
+ surfaceKey: "claude-desktop",
4407
+ sectionType: SectionType.McpConfig,
4408
+ path: claudeDesktopConfigPath(home),
4409
+ format: "json"
4410
+ },
4411
+ instruction: {
4412
+ surfaceKey: "claude-desktop",
4413
+ path: join7(claudeDir, "CLAUDE.md")
4414
+ }
4392
4415
  },
4393
4416
  codex: {
4394
4417
  key: "codex",
4395
4418
  label: "Codex CLI",
4396
- mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join7(codexHome, "config.toml"), format: "toml" },
4419
+ mcp: codexHome ? {
4420
+ surfaceKey: "chatgpt",
4421
+ sectionType: SectionType.McpConfigToml,
4422
+ path: join7(codexHome, "config.toml"),
4423
+ format: "toml"
4424
+ } : null,
4397
4425
  instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
4398
4426
  },
4399
4427
  cursor: {
4400
4428
  key: "cursor",
4401
4429
  label: "Cursor",
4402
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join7(cwd, ".cursor", "mcp.json"), format: "json" },
4430
+ mcp: {
4431
+ surfaceKey: "claude-code",
4432
+ sectionType: SectionType.McpConfig,
4433
+ path: join7(cwd, ".cursor", "mcp.json"),
4434
+ format: "json"
4435
+ },
4403
4436
  instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
4404
4437
  },
4405
4438
  antigravity: {
@@ -4411,20 +4444,34 @@ function clientTargets(cwd, opts = {}) {
4411
4444
  // `type` — comes from the `antigravity` server surface, so we don't
4412
4445
  // hardcode it here. Instructions go in the project `AGENTS.md`
4413
4446
  // (cross-tool, shared with Codex/Cursor).
4414
- mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path: join7(home, ".gemini", "config", "mcp_config.json"), format: "json" },
4447
+ mcp: {
4448
+ surfaceKey: "antigravity",
4449
+ sectionType: SectionType.McpConfig,
4450
+ path: join7(home, ".gemini", "config", "mcp_config.json"),
4451
+ format: "json"
4452
+ },
4415
4453
  instruction: { surfaceKey: "antigravity", path: join7(cwd, "AGENTS.md") }
4416
4454
  }
4417
4455
  };
4418
4456
  }
4419
- var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor", "antigravity"];
4457
+ var ALL_CLIENT_KEYS = [
4458
+ "claude-code",
4459
+ "claude-desktop",
4460
+ "codex",
4461
+ "cursor",
4462
+ "antigravity"
4463
+ ];
4420
4464
  var DEFAULT_CLIENT_KEY = "claude-code";
4421
4465
  function detectInstalledClients(cwd) {
4422
4466
  const home = homedir3();
4423
4467
  const detected = [];
4424
- if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir))) detected.push("claude-code");
4425
- if (existsSync5(dirname5(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
4468
+ if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir)))
4469
+ detected.push("claude-code");
4470
+ if (existsSync5(dirname5(claudeDesktopConfigPath(home))))
4471
+ detected.push("claude-desktop");
4426
4472
  if (resolveCodexHomes({}).some((d) => existsSync5(d))) detected.push("codex");
4427
- if (existsSync5(join7(home, ".cursor")) || existsSync5(join7(cwd, ".cursor"))) detected.push("cursor");
4473
+ if (existsSync5(join7(home, ".cursor")) || existsSync5(join7(cwd, ".cursor")))
4474
+ detected.push("cursor");
4428
4475
  if (existsSync5(join7(home, ".gemini"))) detected.push("antigravity");
4429
4476
  return detected;
4430
4477
  }
@@ -10117,6 +10164,41 @@ function resolveCreateBody(textOpt, fileOpt) {
10117
10164
  );
10118
10165
  return { text: text2, defaultTitle };
10119
10166
  }
10167
+ async function fetchAllMemoryRelationships(cfg, memoryId) {
10168
+ const pages = [];
10169
+ let page = 1;
10170
+ while (true) {
10171
+ const current = await runApi(
10172
+ `Fetching relationships (page ${page})`,
10173
+ async () => {
10174
+ const client = await makeClient(cfg);
10175
+ return client.GET("/memories/{memoryId}/relationships", {
10176
+ params: { path: { memoryId }, query: { page } }
10177
+ });
10178
+ }
10179
+ );
10180
+ pages.push(current);
10181
+ const hasNextPage = current.hasNextPage ?? (current.pageCount !== void 0 ? current.page < current.pageCount : current.page * current.pageSize < current.count);
10182
+ if (!hasNextPage) break;
10183
+ page = current.page + 1;
10184
+ }
10185
+ const first = pages[0];
10186
+ if (!first || pages.length === 1) return first;
10187
+ const items = pages.flatMap((current) => current.items);
10188
+ return {
10189
+ ...first,
10190
+ items,
10191
+ page: 1,
10192
+ pageSize: items.length,
10193
+ pageCount: 1,
10194
+ hasPreviousPage: false,
10195
+ hasNextPage: false,
10196
+ isFirstPage: true,
10197
+ isLastPage: true,
10198
+ firstItemOnPage: 1,
10199
+ lastItemOnPage: items.length
10200
+ };
10201
+ }
10120
10202
  function registerMemory(program2) {
10121
10203
  const memory = program2.command("memory").description("Create, read, and search memories");
10122
10204
  memory.addHelpText(
@@ -10371,7 +10453,16 @@ ${plan.rows.map(
10371
10453
  false
10372
10454
  );
10373
10455
  });
10374
- memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").action(async (memoryId, _opts, cmd) => {
10456
+ const get = memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").option(
10457
+ "--with-relationships",
10458
+ "Include the memory's relationships in the response",
10459
+ false
10460
+ ).addHelpText(
10461
+ "after",
10462
+ `
10463
+ Note: CLI memory get omits relationships by default; MCP get_memory includes them. Use --with-relationships to read all relationship pages, or relationship list <memoryId> to inspect edges.`
10464
+ );
10465
+ get.action(async (memoryId, opts, cmd) => {
10375
10466
  const cfg = resolveConfig(cmd.optsWithGlobals());
10376
10467
  const data = await runApi("Fetching memory", async () => {
10377
10468
  const client = await makeClient(cfg);
@@ -10379,6 +10470,11 @@ ${plan.rows.map(
10379
10470
  params: { path: { memoryId } }
10380
10471
  });
10381
10472
  });
10473
+ if (opts.withRelationships) {
10474
+ const relationships = await fetchAllMemoryRelationships(cfg, memoryId);
10475
+ emit({ ...data, relationships }, cmd.optsWithGlobals().json);
10476
+ return;
10477
+ }
10382
10478
  emit(data, cmd.optsWithGlobals().json);
10383
10479
  });
10384
10480
  memory.command("search <query>").description(
@@ -10726,8 +10822,18 @@ ${plan.rows.map(
10726
10822
  }
10727
10823
 
10728
10824
  // src/setup/apply.ts
10729
- import { createHash as createHash5 } from "crypto";
10730
- import { mkdirSync as mkdirSync16, readFileSync as readFileSync16, writeFileSync as writeFileSync15, existsSync as existsSync13 } from "fs";
10825
+ import { createHash as createHash5, randomUUID as randomUUID3 } from "crypto";
10826
+ import {
10827
+ chmodSync as chmodSync2,
10828
+ copyFileSync,
10829
+ existsSync as existsSync13,
10830
+ mkdirSync as mkdirSync16,
10831
+ readFileSync as readFileSync16,
10832
+ renameSync as renameSync4,
10833
+ rmSync as rmSync7,
10834
+ statSync as statSync5,
10835
+ writeFileSync as writeFileSync15
10836
+ } from "fs";
10731
10837
  import { dirname as dirname15 } from "path";
10732
10838
  var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
10733
10839
  var MARKER_END = "<!-- @sechroom/cli:end";
@@ -10773,11 +10879,17 @@ function parseManagedBlock(content, block) {
10773
10879
  const keyed = content.match(keyedBlockRe(block));
10774
10880
  if (keyed) {
10775
10881
  const attrs = parseAttrs(keyed[0].slice(0, keyed[0].indexOf("\n")));
10776
- return { block, source: attrs.source ?? null, sha256: attrs.sha256 ?? null, body: innerBody(keyed[0]) };
10882
+ return {
10883
+ block,
10884
+ source: attrs.source ?? null,
10885
+ sha256: attrs.sha256 ?? null,
10886
+ body: innerBody(keyed[0])
10887
+ };
10777
10888
  }
10778
10889
  if (block === "role-template") {
10779
10890
  const legacy = content.match(legacyBlockRe());
10780
- if (legacy) return { block, source: null, sha256: null, body: innerBody(legacy[0]) };
10891
+ if (legacy)
10892
+ return { block, source: null, sha256: null, body: innerBody(legacy[0]) };
10781
10893
  }
10782
10894
  return null;
10783
10895
  }
@@ -10799,33 +10911,446 @@ function mergeMcpJson(path, snippet, dryRun) {
10799
10911
  try {
10800
10912
  current = JSON.parse(readFileSync16(path, "utf8"));
10801
10913
  } catch {
10802
- return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
10914
+ return {
10915
+ kind: "mcp",
10916
+ path,
10917
+ status: "skipped",
10918
+ note: "existing file isn't valid JSON \u2014 left untouched"
10919
+ };
10803
10920
  }
10804
10921
  }
10805
- current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
10922
+ current.mcpServers = {
10923
+ ...current.mcpServers ?? {},
10924
+ ...incoming.mcpServers ?? {}
10925
+ };
10806
10926
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
10807
10927
  ensureDir2(path);
10808
10928
  writeFileSync15(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
10809
10929
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
10810
10930
  }
10811
- function mergeCodexToml(path, snippet, dryRun) {
10931
+ var CODEX_MCP_TABLE = "mcp_servers.sechroom";
10932
+ function decodeTomlBasicString(value) {
10933
+ let decoded = "";
10934
+ for (let i = 0; i < value.length; i += 1) {
10935
+ const char = value[i];
10936
+ if (char !== "\\") {
10937
+ decoded += char;
10938
+ continue;
10939
+ }
10940
+ const escaped = value[++i];
10941
+ if (escaped === void 0) throw new Error("unterminated TOML escape");
10942
+ const simple = {
10943
+ b: "\b",
10944
+ f: "\f",
10945
+ n: "\n",
10946
+ r: "\r",
10947
+ t: " ",
10948
+ '"': '"',
10949
+ "\\": "\\"
10950
+ };
10951
+ if (escaped in simple) {
10952
+ decoded += simple[escaped];
10953
+ continue;
10954
+ }
10955
+ const digits = escaped === "u" ? 4 : escaped === "U" ? 8 : 0;
10956
+ if (digits === 0) throw new Error(`invalid TOML escape '\\\\${escaped}'`);
10957
+ const code = value.slice(i + 1, i + 1 + digits);
10958
+ if (!new RegExp(`^[0-9a-fA-F]{${digits}}$`).test(code))
10959
+ throw new Error(`invalid TOML Unicode escape '\\\\${escaped}${code}'`);
10960
+ decoded += String.fromCodePoint(Number.parseInt(code, 16));
10961
+ i += digits;
10962
+ }
10963
+ return decoded;
10964
+ }
10965
+ function normalizeTomlKey(raw) {
10966
+ const segments = [];
10967
+ let i = 0;
10968
+ while (i < raw.length) {
10969
+ while (/\s/.test(raw[i] ?? "")) i += 1;
10970
+ if (i >= raw.length) break;
10971
+ const quote = raw[i];
10972
+ let segment;
10973
+ if (quote === '"' || quote === "'") {
10974
+ i += 1;
10975
+ const start = i;
10976
+ let escaped = false;
10977
+ while (i < raw.length) {
10978
+ const char = raw[i];
10979
+ if (quote === '"' && escaped) {
10980
+ escaped = false;
10981
+ i += 1;
10982
+ continue;
10983
+ }
10984
+ if (quote === '"' && char === "\\") {
10985
+ escaped = true;
10986
+ i += 1;
10987
+ continue;
10988
+ }
10989
+ if (char === quote) break;
10990
+ i += 1;
10991
+ }
10992
+ if (raw[i] !== quote) throw new Error(`unterminated TOML key '${raw}'`);
10993
+ const encoded = raw.slice(start, i);
10994
+ segment = quote === '"' ? decodeTomlBasicString(encoded) : encoded;
10995
+ i += 1;
10996
+ } else {
10997
+ const start = i;
10998
+ while (i < raw.length && /[A-Za-z0-9_-]/.test(raw[i])) i += 1;
10999
+ segment = raw.slice(start, i);
11000
+ if (!segment) throw new Error(`invalid TOML key '${raw.trim()}'`);
11001
+ }
11002
+ segments.push(segment);
11003
+ while (/\s/.test(raw[i] ?? "")) i += 1;
11004
+ if (i >= raw.length) break;
11005
+ if (raw[i] !== ".") throw new Error(`invalid TOML key '${raw.trim()}'`);
11006
+ i += 1;
11007
+ }
11008
+ if (segments.length === 0) throw new Error("empty TOML key");
11009
+ return segments;
11010
+ }
11011
+ function sameTomlKey(left, right) {
11012
+ return left.length === right.length && left.every((segment, index) => segment === right[index]);
11013
+ }
11014
+ function isTomlKeyPrefix(prefix, candidate) {
11015
+ return prefix.length < candidate.length && prefix.every((segment, index) => segment === candidate[index]);
11016
+ }
11017
+ function tomlKeyLabel(key) {
11018
+ return key.join(".");
11019
+ }
11020
+ function stripTomlComment(line) {
11021
+ let quote = null;
11022
+ let escaped = false;
11023
+ for (let i = 0; i < line.length; i += 1) {
11024
+ const rest = line.slice(i);
11025
+ const char = line[i];
11026
+ if (quote === "multiline-basic") {
11027
+ if (rest.startsWith('"""') && !escaped) {
11028
+ escaped = false;
11029
+ quote = null;
11030
+ i += 2;
11031
+ continue;
11032
+ }
11033
+ if (escaped) escaped = false;
11034
+ else if (char === "\\") escaped = true;
11035
+ continue;
11036
+ }
11037
+ if (quote === "multiline-literal") {
11038
+ if (rest.startsWith("'''")) {
11039
+ i += 2;
11040
+ quote = null;
11041
+ }
11042
+ continue;
11043
+ }
11044
+ if (quote === "basic") {
11045
+ if (escaped) escaped = false;
11046
+ else if (char === "\\") escaped = true;
11047
+ else if (char === '"') quote = null;
11048
+ continue;
11049
+ }
11050
+ if (quote === "literal") {
11051
+ if (char === "'") quote = null;
11052
+ continue;
11053
+ }
11054
+ if (rest.startsWith('"""')) {
11055
+ quote = "multiline-basic";
11056
+ i += 2;
11057
+ } else if (rest.startsWith("'''")) {
11058
+ quote = "multiline-literal";
11059
+ i += 2;
11060
+ } else if (char === '"') quote = "basic";
11061
+ else if (char === "'") quote = "literal";
11062
+ else if (char === "#") return line.slice(0, i);
11063
+ }
11064
+ return line;
11065
+ }
11066
+ function scanTomlValue(value, state) {
11067
+ for (let i = 0; i < value.length; i += 1) {
11068
+ const rest = value.slice(i);
11069
+ const char = value[i];
11070
+ if (state.quote === "multiline-basic") {
11071
+ if (rest.startsWith('"""') && !state.escaped) {
11072
+ state.quote = null;
11073
+ i += 2;
11074
+ continue;
11075
+ }
11076
+ if (state.escaped) state.escaped = false;
11077
+ else if (char === "\\") state.escaped = true;
11078
+ continue;
11079
+ }
11080
+ if (state.quote === "multiline-literal") {
11081
+ if (rest.startsWith("'''")) {
11082
+ state.quote = null;
11083
+ i += 2;
11084
+ }
11085
+ continue;
11086
+ }
11087
+ if (state.quote === "basic") {
11088
+ if (state.escaped) state.escaped = false;
11089
+ else if (char === "\\") state.escaped = true;
11090
+ else if (char === '"') state.quote = null;
11091
+ continue;
11092
+ }
11093
+ if (state.quote === "literal") {
11094
+ if (char === "'") state.quote = null;
11095
+ continue;
11096
+ }
11097
+ if (char === "#") break;
11098
+ if (rest.startsWith('"""')) {
11099
+ state.quote = "multiline-basic";
11100
+ i += 2;
11101
+ } else if (rest.startsWith("'''")) {
11102
+ state.quote = "multiline-literal";
11103
+ i += 2;
11104
+ } else if (char === '"') state.quote = "basic";
11105
+ else if (char === "'") state.quote = "literal";
11106
+ else if (char === "[" || char === "{") state.containers.push(char);
11107
+ else if (char === "]" || char === "}") {
11108
+ const expected = char === "]" ? "[" : "{";
11109
+ if (state.containers.pop() !== expected)
11110
+ throw new Error(`unbalanced TOML delimiter '${char}'`);
11111
+ }
11112
+ }
11113
+ }
11114
+ function parseTomlHeader(rawLine) {
11115
+ const line = stripTomlComment(rawLine).trim();
11116
+ const array = line.startsWith("[[") && line.endsWith("]]");
11117
+ const table = !array && line.startsWith("[") && line.endsWith("]");
11118
+ if (!array && !table) return null;
11119
+ const prefix = array ? 2 : 1;
11120
+ const suffix = array ? 2 : 1;
11121
+ const rawName = line.slice(prefix, line.length - suffix).trim();
11122
+ if (!rawName) throw new Error("empty TOML table name");
11123
+ return { name: normalizeTomlKey(rawName), array };
11124
+ }
11125
+ function findTomlEquals(line) {
11126
+ let quote = null;
11127
+ let escaped = false;
11128
+ for (let i = 0; i < line.length; i += 1) {
11129
+ const char = line[i];
11130
+ if (quote === "basic") {
11131
+ if (escaped) escaped = false;
11132
+ else if (char === "\\") escaped = true;
11133
+ else if (char === '"') quote = null;
11134
+ continue;
11135
+ }
11136
+ if (quote === "literal") {
11137
+ if (char === "'") quote = null;
11138
+ continue;
11139
+ }
11140
+ if (char === '"') quote = "basic";
11141
+ else if (char === "'") quote = "literal";
11142
+ else if (char === "=") return i;
11143
+ }
11144
+ return -1;
11145
+ }
11146
+ function arrayTableContext(name, arrays) {
11147
+ let best = null;
11148
+ for (let i = arrays.length - 1; i >= 0; i -= 1) {
11149
+ const candidate = arrays[i];
11150
+ if (isTomlKeyPrefix(candidate.key, name)) {
11151
+ if (!best || candidate.key.length > best.key.length) best = candidate;
11152
+ }
11153
+ }
11154
+ return best;
11155
+ }
11156
+ function tomlTableIdentity(key, context) {
11157
+ return `${context?.identity ?? "root"}:${JSON.stringify(key)}`;
11158
+ }
11159
+ function isTomlScalar(value) {
11160
+ const decimal = "[+-]?(?:0|[1-9](?:_?[0-9])*)(?:\\.[0-9](?:_?[0-9])*)?(?:[eE][+-]?[0-9](?:_?[0-9])*)?";
11161
+ const number = new RegExp(
11162
+ `^(?:${decimal}|[+-]?0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[+-]?0o[0-7](?:_?[0-7])*|[+-]?0b[01](?:_?[01])*|[+-]?(?:inf|nan))$`
11163
+ );
11164
+ const datetime = /^(?:[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]+)?|[0-9]{4}-[0-9]{2}-[0-9]{2}(?:(?:[Tt ]|[Tt])[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]+)?(?:Z|[+-][0-9]{2}:[0-9]{2})?)?)$/;
11165
+ return number.test(value) || datetime.test(value) || /^(?:true|false)$/.test(value);
11166
+ }
11167
+ function validateTomlValueShape(value) {
11168
+ const token = stripTomlComment(value).trim();
11169
+ if (!token) throw new Error("empty TOML value");
11170
+ if (token.startsWith('"') || token.startsWith("'")) {
11171
+ const quote = token[0];
11172
+ if (quote && token.endsWith(quote)) return;
11173
+ }
11174
+ if (token.startsWith("[") && token.endsWith("]")) return;
11175
+ if (token.startsWith("{") && token.endsWith("}")) return;
11176
+ if (isTomlScalar(token)) return;
11177
+ throw new Error(`invalid TOML value '${token}'`);
11178
+ }
11179
+ function validateToml(content) {
11180
+ const tables = /* @__PURE__ */ new Set();
11181
+ const keys = /* @__PURE__ */ new Set();
11182
+ const arrayInstances = [];
11183
+ let currentTable = { identity: "root" };
11184
+ let valueState = {
11185
+ containers: [],
11186
+ quote: null,
11187
+ escaped: false
11188
+ };
11189
+ for (const rawLine of content.replace(/\r\n/g, "\n").split("\n")) {
11190
+ if (valueState.quote || valueState.containers.length > 0) {
11191
+ scanTomlValue(rawLine, valueState);
11192
+ continue;
11193
+ }
11194
+ const line = stripTomlComment(rawLine).trim();
11195
+ if (!line) continue;
11196
+ const header = parseTomlHeader(rawLine);
11197
+ if (header) {
11198
+ if (header.array) {
11199
+ const context = arrayTableContext(header.name, arrayInstances);
11200
+ const parentIdentity = context?.identity ?? null;
11201
+ const previous = [...arrayInstances].reverse().find(
11202
+ (candidate) => sameTomlKey(candidate.key, header.name) && candidate.parentIdentity === parentIdentity
11203
+ );
11204
+ const count = (previous?.count ?? 0) + 1;
11205
+ const instance = {
11206
+ key: header.name,
11207
+ parentIdentity,
11208
+ identity: `${parentIdentity ?? "root"}:${JSON.stringify(header.name)}#${count}`,
11209
+ count
11210
+ };
11211
+ arrayInstances.push(instance);
11212
+ currentTable = { identity: instance.identity };
11213
+ } else {
11214
+ const context = arrayTableContext(header.name, arrayInstances);
11215
+ currentTable = {
11216
+ identity: tomlTableIdentity(header.name, context)
11217
+ };
11218
+ if (tables.has(currentTable.identity))
11219
+ throw new Error(
11220
+ `duplicate TOML table '${tomlKeyLabel(header.name)}'`
11221
+ );
11222
+ tables.add(currentTable.identity);
11223
+ }
11224
+ continue;
11225
+ }
11226
+ const equals = findTomlEquals(line);
11227
+ if (equals <= 0) throw new Error(`invalid TOML line '${rawLine}'`);
11228
+ const key = normalizeTomlKey(line.slice(0, equals).trim());
11229
+ const value = line.slice(equals + 1).trim();
11230
+ if (!value || value.startsWith("#"))
11231
+ throw new Error(`empty TOML value for '${tomlKeyLabel(key)}'`);
11232
+ const fullKey = `${currentTable.identity}\0${JSON.stringify(key)}`;
11233
+ if (keys.has(fullKey))
11234
+ throw new Error(`duplicate TOML key '${tomlKeyLabel(key)}'`);
11235
+ keys.add(fullKey);
11236
+ scanTomlValue(line.slice(equals + 1), valueState);
11237
+ if (valueState.quote === "basic" || valueState.quote === "literal")
11238
+ throw new Error("unterminated TOML string");
11239
+ if (!valueState.quote && valueState.containers.length === 0)
11240
+ validateTomlValueShape(value);
11241
+ }
11242
+ if (valueState.quote) throw new Error("unterminated TOML string");
11243
+ if (valueState.containers.length > 0)
11244
+ throw new Error("unbalanced TOML value");
11245
+ }
11246
+ function isTomlTableHeader(line) {
11247
+ try {
11248
+ return parseTomlHeader(line) !== null;
11249
+ } catch {
11250
+ return false;
11251
+ }
11252
+ }
11253
+ function replaceTomlTable(content, tableName, replacement) {
11254
+ const target = normalizeTomlKey(tableName);
11255
+ const lines = content.replace(/\r\n/g, "\n").split("\n");
11256
+ const replacementLines = replacement.trim().split("\n");
11257
+ const kept = [];
11258
+ let i = 0;
11259
+ while (i < lines.length) {
11260
+ const header = parseTomlHeader(lines[i]);
11261
+ if (header && (sameTomlKey(header.name, target) || isTomlKeyPrefix(target, header.name))) {
11262
+ i += 1;
11263
+ while (i < lines.length && !isTomlTableHeader(lines[i])) i += 1;
11264
+ continue;
11265
+ }
11266
+ kept.push(lines[i]);
11267
+ i += 1;
11268
+ }
11269
+ const base = kept.join("\n").trim();
11270
+ return `${base.length > 0 ? `${base}
11271
+
11272
+ ` : ""}${replacementLines.join("\n")}
11273
+ `;
11274
+ }
11275
+ var defaultTomlFileOperations = {
11276
+ copyFileSync,
11277
+ renameSync: renameSync4
11278
+ };
11279
+ function writeTomlAtomic(path, content, fileOperations = {}) {
11280
+ ensureDir2(path);
11281
+ const operations = { ...defaultTomlFileOperations, ...fileOperations };
11282
+ const temporary = `${path}.sechroom-${process.pid}-${randomUUID3()}.tmp`;
11283
+ const backup = `${path}.bak`;
11284
+ const backupTemporary = `${backup}.sechroom-${process.pid}-${randomUUID3()}.tmp`;
11285
+ const mode = existsSync13(path) ? statSync5(path).mode & 4095 : 384;
11286
+ try {
11287
+ writeFileSync15(temporary, content, { mode });
11288
+ chmodSync2(temporary, mode);
11289
+ validateToml(readFileSync16(temporary, "utf8"));
11290
+ if (existsSync13(path) && !existsSync13(backup)) {
11291
+ operations.copyFileSync(path, backupTemporary);
11292
+ chmodSync2(backupTemporary, mode);
11293
+ validateToml(readFileSync16(backupTemporary, "utf8"));
11294
+ operations.renameSync(backupTemporary, backup);
11295
+ }
11296
+ operations.renameSync(temporary, path);
11297
+ } finally {
11298
+ rmSync7(temporary, { force: true });
11299
+ rmSync7(backupTemporary, { force: true });
11300
+ }
11301
+ }
11302
+ function mergeCodexToml(path, snippet, dryRun, fileOperations = {}) {
10812
11303
  const existed = existsSync13(path);
10813
- let body = readOr(path, "");
10814
- body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
10815
- const trimmed = body.trim();
10816
- const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
11304
+ const body = readOr(path, "");
11305
+ try {
11306
+ validateToml(body);
11307
+ } catch (error) {
11308
+ return {
11309
+ kind: "mcp",
11310
+ path,
11311
+ status: "skipped",
11312
+ note: `existing TOML is invalid \u2014 left untouched (${error.message})`
11313
+ };
11314
+ }
11315
+ const next = replaceTomlTable(body, CODEX_MCP_TABLE, snippet);
11316
+ try {
11317
+ validateToml(next);
11318
+ } catch (error) {
11319
+ return {
11320
+ kind: "mcp",
11321
+ path,
11322
+ status: "skipped",
11323
+ note: `generated TOML is invalid \u2014 left untouched (${error.message})`
11324
+ };
11325
+ }
10817
11326
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
10818
- ensureDir2(path);
10819
- writeFileSync15(path, next, { mode: 384 });
11327
+ if (next === body)
11328
+ return { kind: "mcp", path, status: existed ? "current" : "created" };
11329
+ try {
11330
+ writeTomlAtomic(path, next, fileOperations);
11331
+ } catch (error) {
11332
+ return {
11333
+ kind: "mcp",
11334
+ path,
11335
+ status: "skipped",
11336
+ note: `could not replace TOML \u2014 left untouched (${error.message})`
11337
+ };
11338
+ }
10820
11339
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
10821
11340
  }
10822
11341
  function writeInstructionBlock(path, write, dryRun) {
10823
11342
  const existed = existsSync13(path);
10824
11343
  const next = computeBlockFile(readOr(path, ""), write);
10825
- if (dryRun) return { kind: "instruction", path, status: "dry-run", block: write.block };
11344
+ if (dryRun)
11345
+ return { kind: "instruction", path, status: "dry-run", block: write.block };
10826
11346
  ensureDir2(path);
10827
11347
  writeFileSync15(path, next);
10828
- return { kind: "instruction", path, status: existed ? "merged" : "created", block: write.block };
11348
+ return {
11349
+ kind: "instruction",
11350
+ path,
11351
+ status: existed ? "merged" : "created",
11352
+ block: write.block
11353
+ };
10829
11354
  }
10830
11355
  function computeBlockFile(current, write) {
10831
11356
  const rendered = renderBlock(write);
@@ -10874,7 +11399,13 @@ function applyBlock(path, write, mode, dryRun) {
10874
11399
  };
10875
11400
  }
10876
11401
  if (state === "current") {
10877
- return { kind: "instruction", path, status: "current", eval: "current", block: write.block };
11402
+ return {
11403
+ kind: "instruction",
11404
+ path,
11405
+ status: "current",
11406
+ eval: "current",
11407
+ block: write.block
11408
+ };
10878
11409
  }
10879
11410
  if (state === "drift" && mode !== "force") {
10880
11411
  const proposedPath = `${path}.proposed`;
@@ -10909,7 +11440,12 @@ async function applyClient(cfg, setup, target, opts) {
10909
11440
  const section = findSection(surface, target.mcp.sectionType);
10910
11441
  const snippet = sectionSnippet(section);
10911
11442
  if (!snippet) {
10912
- actions.push({ kind: "mcp", path: target.mcp.path, status: "skipped", note: `no ${target.mcp.sectionType} section on surface '${target.mcp.surfaceKey}'` });
11443
+ actions.push({
11444
+ kind: "mcp",
11445
+ path: target.mcp.path,
11446
+ status: "skipped",
11447
+ note: `no ${target.mcp.sectionType} section on surface '${target.mcp.surfaceKey}'`
11448
+ });
10913
11449
  } else {
10914
11450
  actions.push(
10915
11451
  target.mcp.format === "toml" ? mergeCodexToml(target.mcp.path, snippet, dryRun) : mergeMcpJson(target.mcp.path, snippet, dryRun)
@@ -10920,25 +11456,44 @@ async function applyClient(cfg, setup, target, opts) {
10920
11456
  const surface = findSurface(setup, target.instruction.surfaceKey);
10921
11457
  const section = findSection(surface, SectionType.InstructionFile);
10922
11458
  if (!section) {
10923
- actions.push({ kind: "instruction", path: target.instruction.path, status: "skipped", note: `no instruction-file section on surface '${target.instruction.surfaceKey}'` });
11459
+ actions.push({
11460
+ kind: "instruction",
11461
+ path: target.instruction.path,
11462
+ status: "skipped",
11463
+ note: `no instruction-file section on surface '${target.instruction.surfaceKey}'`
11464
+ });
10924
11465
  } else {
10925
11466
  const resolved = await withSpinner(
10926
11467
  `Resolving ${target.label} agent instructions`,
10927
11468
  () => resolveInstruction(cfg, section, opts.personalWorkspaceId)
10928
11469
  );
10929
11470
  if (!resolved) {
10930
- actions.push({ kind: "instruction", path: target.instruction.path, status: "skipped", note: "no role template found in this tenant \u2014 install the SEM Starter bundle, then re-run `sechroom setup agent-files`" });
11471
+ actions.push({
11472
+ kind: "instruction",
11473
+ path: target.instruction.path,
11474
+ status: "skipped",
11475
+ note: "no role template found in this tenant \u2014 install the SEM Starter bundle, then re-run `sechroom setup agent-files`"
11476
+ });
10931
11477
  } else {
10932
11478
  const action = applyBlock(
10933
11479
  target.instruction.path,
10934
- { block: "role-template", body: resolved.body, source: resolved.sourceRef },
11480
+ {
11481
+ block: "role-template",
11482
+ body: resolved.body,
11483
+ source: resolved.sourceRef
11484
+ },
10935
11485
  mode,
10936
11486
  opts.dryRun
10937
11487
  );
10938
- actions.push(resolved.source === "override" && action.status !== "current" ? { ...action, note: action.note ?? "your personal copy" } : action);
11488
+ actions.push(
11489
+ resolved.source === "override" && action.status !== "current" ? { ...action, note: action.note ?? "your personal copy" } : action
11490
+ );
10939
11491
  }
10940
11492
  }
10941
- const conventionsSection = findSection(surface, SectionType.WorkspaceConventions);
11493
+ const conventionsSection = findSection(
11494
+ surface,
11495
+ SectionType.WorkspaceConventions
11496
+ );
10942
11497
  if (conventionsSection) {
10943
11498
  const conventions = await withSpinner(
10944
11499
  `Composing ${target.label} workspace conventions`,
@@ -10947,11 +11502,20 @@ async function applyClient(cfg, setup, target, opts) {
10947
11502
  if (conventions) {
10948
11503
  const action = applyBlock(
10949
11504
  target.instruction.path,
10950
- { block: "workspace-conventions", body: conventions.body, source: `workspace:${cfg.workspaceId ?? ""}` },
11505
+ {
11506
+ block: "workspace-conventions",
11507
+ body: conventions.body,
11508
+ source: `workspace:${cfg.workspaceId ?? ""}`
11509
+ },
10951
11510
  mode,
10952
11511
  opts.dryRun
10953
11512
  );
10954
- actions.push(action.status === "current" ? action : { ...action, note: action.note ?? `workspace conventions (${conventions.refs.length})` });
11513
+ actions.push(
11514
+ action.status === "current" ? action : {
11515
+ ...action,
11516
+ note: action.note ?? `workspace conventions (${conventions.refs.length})`
11517
+ }
11518
+ );
10955
11519
  }
10956
11520
  }
10957
11521
  }
@@ -11169,7 +11733,12 @@ function buildConventionDraft(title, rawKind, rawBody) {
11169
11733
 
11170
11734
  ${body}
11171
11735
  `,
11172
- tags: ["agent-setup-bundle", "scope:sechroom", `kind:${kind}`, "archetype:document"]
11736
+ tags: [
11737
+ "agent-setup-bundle",
11738
+ "scope:sechroom",
11739
+ `kind:${kind}`,
11740
+ "archetype:document"
11741
+ ]
11173
11742
  };
11174
11743
  }
11175
11744
  function copyChoice(opts) {
@@ -11182,9 +11751,16 @@ async function maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId,
11182
11751
  const instr = targets[key]?.instruction;
11183
11752
  if (!instr || seen.has(instr.surfaceKey)) continue;
11184
11753
  seen.add(instr.surfaceKey);
11185
- const section = findSection(findSurface(setup, instr.surfaceKey), SectionType.InstructionFile);
11754
+ const section = findSection(
11755
+ findSurface(setup, instr.surfaceKey),
11756
+ SectionType.InstructionFile
11757
+ );
11186
11758
  if (!section) continue;
11187
- const resolved = await resolveInstruction(cfg, section, personalWorkspaceId);
11759
+ const resolved = await resolveInstruction(
11760
+ cfg,
11761
+ section,
11762
+ personalWorkspaceId
11763
+ );
11188
11764
  if (!resolved || resolved.source === "override") continue;
11189
11765
  let make = choice === "yes";
11190
11766
  if (choice === "ask") {
@@ -11199,8 +11775,10 @@ version, the shared template stays clean, and you can discard back anytime.
11199
11775
  }
11200
11776
  if (make) {
11201
11777
  await createOverride(cfg, resolved, personalWorkspaceId);
11202
- process.stderr.write(`\u2713 personal copy created for ${instr.surfaceKey} \u2014 edit it on the Agent setup page or via the API.
11203
- `);
11778
+ process.stderr.write(
11779
+ `\u2713 personal copy created for ${instr.surfaceKey} \u2014 edit it on the Agent setup page or via the API.
11780
+ `
11781
+ );
11204
11782
  }
11205
11783
  }
11206
11784
  }
@@ -11210,7 +11788,9 @@ function resolveClientKeys(raw) {
11210
11788
  if (tokens.includes("all")) return [...ALL_CLIENT_KEYS];
11211
11789
  for (const k of tokens) {
11212
11790
  if (!targets[k]) {
11213
- fail(`unknown client '${k}'. Known: ${ALL_CLIENT_KEYS.join(", ")}, or 'all'.`);
11791
+ fail(
11792
+ `unknown client '${k}'. Known: ${ALL_CLIENT_KEYS.join(", ")}, or 'all'.`
11793
+ );
11214
11794
  }
11215
11795
  }
11216
11796
  return [...new Set(tokens)];
@@ -11221,8 +11801,10 @@ ${client.label} (${client.key}):
11221
11801
  `);
11222
11802
  for (const a of actions) {
11223
11803
  const tag = a.status === "skipped" ? "skip" : a.status;
11224
- process.stdout.write(` [${tag}] ${a.kind}: ${a.path}${a.note ? ` \u2014 ${a.note}` : ""}
11225
- `);
11804
+ process.stdout.write(
11805
+ ` [${tag}] ${a.kind}: ${a.path}${a.note ? ` \u2014 ${a.note}` : ""}
11806
+ `
11807
+ );
11226
11808
  }
11227
11809
  }
11228
11810
  function resolveEvalMode(opts) {
@@ -11296,8 +11878,10 @@ function summarizeEval(result, mode, json, dryRun) {
11296
11878
  const { eval: counts } = buildCheckReport(result);
11297
11879
  if (json) return;
11298
11880
  if (!dryRun && counts.stale) {
11299
- process.stderr.write(`\u21BB refreshed ${counts.stale} section(s) the server had moved
11300
- `);
11881
+ process.stderr.write(
11882
+ `\u21BB refreshed ${counts.stale} section(s) the server had moved
11883
+ `
11884
+ );
11301
11885
  }
11302
11886
  if (!dryRun && counts.drift) {
11303
11887
  process.stderr.write(
@@ -11328,7 +11912,39 @@ async function resolveNamespaceChoice(cfg, flag) {
11328
11912
  return picked === GLOBAL_NAMESPACE ? null : picked;
11329
11913
  }
11330
11914
  function registerInit(program2) {
11331
- program2.command("init").description("Wire this project for sechroom: write MCP config + agent instruction files from the server's setup descriptors").option("--client <list...>", `clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all'`, DEFAULT_CLIENT_KEY).option("--scope <scope>", "install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default global", "global").option("--dry-run", "print what would be written without writing", false).option("--mcp-only", "only write MCP config (skip agent files)", false).option("--agent-files-only", "only write agent instruction files (skip MCP config)", false).option("--copy", "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)").option("--namespace <slug>", "MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)").option("--refresh", "refresh out-of-date agent-file blocks in place (local edits preserved to .proposed)", false).option("--force", "rewrite agent-file managed blocks, overwriting local edits inside the markers", false).option("--check", "report whether agent files would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing", false).addHelpText(
11915
+ program2.command("init").description(
11916
+ "Wire this project for sechroom: write MCP config + agent instruction files from the server's setup descriptors"
11917
+ ).option(
11918
+ "--client <list...>",
11919
+ `clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all'`,
11920
+ DEFAULT_CLIENT_KEY
11921
+ ).option(
11922
+ "--scope <scope>",
11923
+ "install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default global",
11924
+ "global"
11925
+ ).option("--dry-run", "print what would be written without writing", false).option("--mcp-only", "only write MCP config (skip agent files)", false).option(
11926
+ "--agent-files-only",
11927
+ "only write agent instruction files (skip MCP config)",
11928
+ false
11929
+ ).option(
11930
+ "--copy",
11931
+ "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)"
11932
+ ).option(
11933
+ "--namespace <slug>",
11934
+ "MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)"
11935
+ ).option(
11936
+ "--refresh",
11937
+ "refresh out-of-date agent-file blocks in place (local edits preserved to .proposed)",
11938
+ false
11939
+ ).option(
11940
+ "--force",
11941
+ "rewrite agent-file managed blocks, overwriting local edits inside the markers",
11942
+ false
11943
+ ).option(
11944
+ "--check",
11945
+ "report whether agent files would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
11946
+ false
11947
+ ).addHelpText(
11332
11948
  "after",
11333
11949
  `
11334
11950
  Examples:
@@ -11343,7 +11959,9 @@ Examples:
11343
11959
  const mode = resolveEvalMode(opts);
11344
11960
  const check = mode === "check";
11345
11961
  if (check && opts.mcpOnly) {
11346
- fail("--check inspects agent files and cannot be combined with --mcp-only.");
11962
+ fail(
11963
+ "--check inspects agent files and cannot be combined with --mcp-only."
11964
+ );
11347
11965
  }
11348
11966
  const namespaceSlug = await resolveNamespaceChoice(cfg, opts.namespace);
11349
11967
  const setup = await withSpinner(
@@ -11357,14 +11975,28 @@ Examples:
11357
11975
  } catch (err2) {
11358
11976
  return fail(err2.message);
11359
11977
  }
11360
- const claudeTargets = resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd: process.cwd() });
11978
+ const claudeTargets = resolveClaudeTargets({
11979
+ override: g.claudeConfigDir,
11980
+ scope,
11981
+ cwd: process.cwd()
11982
+ });
11361
11983
  const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
11362
- const targets = clientTargets(process.cwd(), { claudeDir: claudeTargets[0]?.dir, codexHome: codexHomes[0] });
11984
+ const targets = clientTargets(process.cwd(), {
11985
+ claudeDir: claudeTargets[0]?.dir,
11986
+ codexHome: codexHomes[0] ?? null
11987
+ });
11363
11988
  const keys = resolveClientKeys(opts.client);
11364
11989
  const json = g.json;
11365
11990
  const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
11366
11991
  if (!opts.dryRun && !opts.mcpOnly && !check) {
11367
- await maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, copyChoice(opts));
11992
+ await maybeOfferCopies(
11993
+ cfg,
11994
+ setup,
11995
+ targets,
11996
+ keys,
11997
+ personalWorkspaceId,
11998
+ copyChoice(opts)
11999
+ );
11368
12000
  }
11369
12001
  const result = [];
11370
12002
  for (const key of keys) {
@@ -11382,18 +12014,33 @@ Examples:
11382
12014
  summarizeEval(result, mode, Boolean(json), Boolean(opts.dryRun));
11383
12015
  if (!json && !opts.dryRun && !opts.mcpOnly && !check) {
11384
12016
  for (const t of claudeTargets) {
11385
- await maybeOfferSkills(cfg, personalWorkspaceId, { yes: false, dryRun: Boolean(opts.dryRun), surface: "claude-code", configDir: t.dir });
12017
+ await maybeOfferSkills(cfg, personalWorkspaceId, {
12018
+ yes: false,
12019
+ dryRun: Boolean(opts.dryRun),
12020
+ surface: "claude-code",
12021
+ configDir: t.dir
12022
+ });
11386
12023
  }
11387
12024
  }
11388
12025
  if (!json && !opts.dryRun && !opts.mcpOnly && !check) {
11389
- await maybeOfferHooks({ yes: false, dryRun: Boolean(opts.dryRun), cwd: process.cwd(), scope, claudeConfigDir: g.claudeConfigDir, codexHome: g.codexHome });
12026
+ await maybeOfferHooks({
12027
+ yes: false,
12028
+ dryRun: Boolean(opts.dryRun),
12029
+ cwd: process.cwd(),
12030
+ scope,
12031
+ claudeConfigDir: g.claudeConfigDir,
12032
+ codexHome: g.codexHome
12033
+ });
11390
12034
  }
11391
12035
  if (json) {
11392
12036
  emit({ dryRun: Boolean(opts.dryRun), clients: result }, true);
11393
12037
  return;
11394
12038
  }
11395
12039
  const first = targets[keys[0]];
11396
- const surface = findSurface(setup, first.mcp?.surfaceKey ?? first.instruction?.surfaceKey ?? "");
12040
+ const surface = findSurface(
12041
+ setup,
12042
+ first.mcp?.surfaceKey ?? first.instruction?.surfaceKey ?? ""
12043
+ );
11397
12044
  const verify = findSection(surface, SectionType.Verify);
11398
12045
  if (verify?.description) {
11399
12046
  process.stdout.write(`
@@ -11407,13 +12054,64 @@ Next \u2014 verify: ${verify.description}
11407
12054
  }
11408
12055
  function registerSetup(program2, deps = {}) {
11409
12056
  const setup = program2.command("setup").description("Granular onboarding steps (init runs these together)");
11410
- 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).option("--namespace <slug>", "MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)").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) => {
11411
- await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: true, agentFiles: false, namespace: opts.namespace });
11412
- });
11413
- 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)").option("--refresh", "refresh out-of-date blocks in place (local edits preserved to .proposed)", false).option("--force", "rewrite managed blocks, overwriting local edits inside the markers", false).option("--check", "report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing", false).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 --check CI gate: nonzero exit if out of date\n $ sechroom setup agent-files claude-code --force overwrite local edits in the managed block").action(async (clients, opts, cmd) => {
11414
- await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: false, agentFiles: true, copy: opts.copy, mode: resolveEvalMode(opts) });
12057
+ setup.command("mcp <clients...>").description(
12058
+ `Write only the MCP config for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`
12059
+ ).option("--dry-run", "print what would be written without writing", false).option(
12060
+ "--namespace <slug>",
12061
+ "MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)"
12062
+ ).addHelpText(
12063
+ "after",
12064
+ "\nExamples:\n $ sechroom setup mcp codex\n $ sechroom setup mcp claude-code codex\n $ sechroom setup mcp all"
12065
+ ).action(async (clients, opts, cmd) => {
12066
+ await runClients(clients, cmd, {
12067
+ dryRun: Boolean(opts.dryRun),
12068
+ mcp: true,
12069
+ agentFiles: false,
12070
+ namespace: opts.namespace
12071
+ });
12072
+ });
12073
+ setup.command("agent-files <clients...>").description(
12074
+ `Write only the agent instruction file(s) for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`
12075
+ ).option("--dry-run", "print what would be written without writing", false).option(
12076
+ "--copy",
12077
+ "make a personal copy you can edit (default: prompt on a TTY, else skip)"
12078
+ ).option(
12079
+ "--refresh",
12080
+ "refresh out-of-date blocks in place (local edits preserved to .proposed)",
12081
+ false
12082
+ ).option(
12083
+ "--force",
12084
+ "rewrite managed blocks, overwriting local edits inside the markers",
12085
+ false
12086
+ ).option(
12087
+ "--check",
12088
+ "report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
12089
+ false
12090
+ ).addHelpText(
12091
+ "after",
12092
+ "\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 --check CI gate: nonzero exit if out of date\n $ sechroom setup agent-files claude-code --force overwrite local edits in the managed block"
12093
+ ).action(async (clients, opts, cmd) => {
12094
+ await runClients(clients, cmd, {
12095
+ dryRun: Boolean(opts.dryRun),
12096
+ mcp: false,
12097
+ agentFiles: true,
12098
+ copy: opts.copy,
12099
+ mode: resolveEvalMode(opts)
12100
+ });
11415
12101
  });
11416
- setup.command("new-convention <title...>").description("Scaffold a workspace-conventions section: author a correctly-tagged memo (header as first body line) + regen the agent files").option("--kind <kind>", "reference | standard (orders the section; reference first)", "reference").option("--workspace <id>", "workspace to author in (default: the bound workspace)").option("--body <markdown>", "section body (default: a TODO scaffold to edit later)").option("--no-regen", "skip the agent-files regen after authoring").option("--dry-run", "print what would be authored; write nothing", false).addHelpText(
12102
+ setup.command("new-convention <title...>").description(
12103
+ "Scaffold a workspace-conventions section: author a correctly-tagged memo (header as first body line) + regen the agent files"
12104
+ ).option(
12105
+ "--kind <kind>",
12106
+ "reference | standard (orders the section; reference first)",
12107
+ "reference"
12108
+ ).option(
12109
+ "--workspace <id>",
12110
+ "workspace to author in (default: the bound workspace)"
12111
+ ).option(
12112
+ "--body <markdown>",
12113
+ "section body (default: a TODO scaffold to edit later)"
12114
+ ).option("--no-regen", "skip the agent-files regen after authoring").option("--dry-run", "print what would be authored; write nothing", false).addHelpText(
11417
12115
  "after",
11418
12116
  `
11419
12117
  The memo carries the two conventions a workspace-conventions section needs (FR-sechroom-236):
@@ -11429,10 +12127,15 @@ Examples:
11429
12127
  const cfg = resolveConfig(cmd.optsWithGlobals());
11430
12128
  const json = Boolean(cmd.optsWithGlobals().json);
11431
12129
  const title = titleParts.join(" ").trim();
11432
- if (!title) fail('a section title is required, e.g. `sechroom setup new-convention "Deploy runbook"`.');
12130
+ if (!title)
12131
+ fail(
12132
+ 'a section title is required, e.g. `sechroom setup new-convention "Deploy runbook"`.'
12133
+ );
11433
12134
  const workspaceId = opts.workspace ?? cfg.workspaceId;
11434
12135
  if (!workspaceId)
11435
- fail("no workspace \u2014 pass --workspace <id> or bind one (`sechroom config set --local workspaceId <id>`).");
12136
+ fail(
12137
+ "no workspace \u2014 pass --workspace <id> or bind one (`sechroom config set --local workspaceId <id>`)."
12138
+ );
11436
12139
  const draft = buildConventionDraft(title, opts.kind, opts.body);
11437
12140
  if (opts.dryRun) {
11438
12141
  emit(
@@ -11473,7 +12176,10 @@ Examples:
11473
12176
  }
11474
12177
  if (opts.regen === false) {
11475
12178
  if (json) emit({ id: data.id, workspaceId, regen: false }, true);
11476
- else process.stdout.write("Skipped regen (--no-regen). Run `sechroom setup agent-files all` to apply.\n");
12179
+ else
12180
+ process.stdout.write(
12181
+ "Skipped regen (--no-regen). Run `sechroom setup agent-files all` to apply.\n"
12182
+ );
11477
12183
  return;
11478
12184
  }
11479
12185
  const regenerate = deps.regenerateConvention ?? runClients;
@@ -11502,7 +12208,14 @@ async function runClients(clients, cmd, opts) {
11502
12208
  );
11503
12209
  const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
11504
12210
  if (opts.agentFiles && !opts.dryRun && !check) {
11505
- await maybeOfferCopies(cfg, setupData, targets, keys, personalWorkspaceId, copyChoice(opts));
12211
+ await maybeOfferCopies(
12212
+ cfg,
12213
+ setupData,
12214
+ targets,
12215
+ keys,
12216
+ personalWorkspaceId,
12217
+ copyChoice(opts)
12218
+ );
11506
12219
  }
11507
12220
  const json = g.json;
11508
12221
  const result = [];
@@ -11523,7 +12236,9 @@ async function runClients(clients, cmd, opts) {
11523
12236
  emit({ dryRun: opts.dryRun, clients: result }, true);
11524
12237
  return;
11525
12238
  }
11526
- process.stdout.write(opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n");
12239
+ process.stdout.write(
12240
+ opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n"
12241
+ );
11527
12242
  }
11528
12243
 
11529
12244
  // src/commands/namespace.ts
@@ -11599,7 +12314,7 @@ import { basename as basename5, join as join21 } from "path";
11599
12314
 
11600
12315
  // src/commands/fanout.ts
11601
12316
  import { spawnSync } from "child_process";
11602
- import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
12317
+ import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync4, statSync as statSync6 } from "fs";
11603
12318
  import { isAbsolute as isAbsolute3, join as join20, resolve as resolve7 } from "path";
11604
12319
  var ICON = {
11605
12320
  refresh: "\u21BB",
@@ -11622,7 +12337,7 @@ function discoverChildren(root) {
11622
12337
  if (name.startsWith(".") || name === "node_modules") continue;
11623
12338
  const dir = join20(root, name);
11624
12339
  try {
11625
- if (!statSync5(dir).isDirectory()) continue;
12340
+ if (!statSync6(dir).isDirectory()) continue;
11626
12341
  } catch {
11627
12342
  continue;
11628
12343
  }
@@ -11748,20 +12463,27 @@ function resolveBaseUrl(g) {
11748
12463
  return baseUrl.replace(/\/$/, "");
11749
12464
  }
11750
12465
  async function fetchWorkspaces(client) {
11751
- const { data, error } = await client.GET("/workspaces", { params: { query: { includeArchived: false } } });
11752
- if (error) throw new Error(`Couldn't list your workspaces: ${JSON.stringify(error)}`);
12466
+ const { data, error } = await client.GET("/workspaces", {
12467
+ params: { query: { includeArchived: false } }
12468
+ });
12469
+ if (error)
12470
+ throw new Error(`Couldn't list your workspaces: ${JSON.stringify(error)}`);
11753
12471
  const rows = data ?? [];
11754
12472
  return rows.map((r) => r.item ?? r).filter((w) => Boolean(w?.id && w?.name)).map((w) => ({ id: w.id, name: w.name, parentId: w.parentId ?? null }));
11755
12473
  }
11756
12474
  async function lookupWorkspace(client, id) {
11757
- const { data, error } = await client.GET("/workspaces/{workspaceId}", { params: { path: { workspaceId: id } } });
12475
+ const { data, error } = await client.GET("/workspaces/{workspaceId}", {
12476
+ params: { path: { workspaceId: id } }
12477
+ });
11758
12478
  if (error) return null;
11759
12479
  const env = data;
11760
12480
  const w = env?.item ?? env;
11761
12481
  return w?.id ? { id: w.id, name: w.name ?? id, parentId: w.parentId ?? null } : null;
11762
12482
  }
11763
12483
  async function warnIfProjectStray(client, projectId, workspaceId, json) {
11764
- const { data, error } = await client.GET("/projects/{projectId}", { params: { path: { projectId } } });
12484
+ const { data, error } = await client.GET("/projects/{projectId}", {
12485
+ params: { path: { projectId } }
12486
+ });
11765
12487
  if (error) return;
11766
12488
  const env = data;
11767
12489
  const owner = env?.item?.workspaceId;
@@ -11805,10 +12527,15 @@ function personalSubtreeIds(personalId, all) {
11805
12527
  async function pickWorkspace(client, opts = {}) {
11806
12528
  const promptLabel = opts.promptLabel ?? "Bind this directory to a workspace:";
11807
12529
  const dirName = opts.dirName ?? basename5(process.cwd());
11808
- const all = await withSpinner("Listing your workspaces", () => fetchWorkspaces(client));
12530
+ const all = await withSpinner(
12531
+ "Listing your workspaces",
12532
+ () => fetchWorkspaces(client)
12533
+ );
11809
12534
  if (all.length === 0) {
11810
- process.stderr.write(`no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
11811
- `);
12535
+ process.stderr.write(
12536
+ `no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
12537
+ `
12538
+ );
11812
12539
  return void 0;
11813
12540
  }
11814
12541
  const byId = new Map(all.map((w) => [w.id, w]));
@@ -11823,19 +12550,35 @@ async function pickWorkspace(client, opts = {}) {
11823
12550
  const matched = candidates.filter(isMatch).sort(byPath);
11824
12551
  const rest = candidates.filter((w) => !isMatch(w)).sort(byPath);
11825
12552
  const choices = [
11826
- ...matched.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: style.dim(`matches "${dirName}"`) })),
11827
- ...rest.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: w.id })),
11828
- { label: style.dim("skip \u2014 don't bind a workspace"), value: SKIP, hint: void 0 }
12553
+ ...matched.map((w) => ({
12554
+ label: workspacePath(w, byId),
12555
+ value: w.id,
12556
+ hint: style.dim(`matches "${dirName}"`)
12557
+ })),
12558
+ ...rest.map((w) => ({
12559
+ label: workspacePath(w, byId),
12560
+ value: w.id,
12561
+ hint: w.id
12562
+ })),
12563
+ {
12564
+ label: style.dim("skip \u2014 don't bind a workspace"),
12565
+ value: SKIP,
12566
+ hint: void 0
12567
+ }
11829
12568
  ];
11830
12569
  const defaultValue = matched.length === 1 ? matched[0].id : SKIP;
11831
12570
  const chosen = candidates.length > 12 ? await promptAutocomplete(promptLabel, choices, defaultValue) : await promptSelect(promptLabel, choices, defaultValue);
11832
12571
  if (chosen === SKIP) return void 0;
11833
12572
  const picked = byId.get(chosen);
11834
- const collisions = all.filter((w) => w.id !== picked.id && namesCollide(w.name, picked.name));
12573
+ const collisions = all.filter(
12574
+ (w) => w.id !== picked.id && namesCollide(w.name, picked.name)
12575
+ );
11835
12576
  if (collisions.length > 0) {
11836
12577
  process.stderr.write(
11837
12578
  `${warn("\u26A0")} ${collisions.length} other workspace(s) have a similar name to ${style.cyan(workspacePath(picked, byId))}:
11838
- ` + collisions.map((w) => ` ${style.dim(workspacePath(w, byId))} ${style.dim(`(${w.id})`)}`).join("\n") + `
12579
+ ` + collisions.map(
12580
+ (w) => ` ${style.dim(workspacePath(w, byId))} ${style.dim(`(${w.id})`)}`
12581
+ ).join("\n") + `
11839
12582
  You picked ${style.dim(picked.id)} \u2014 re-run \`sechroom config set --local workspaceId <id>\` if that's wrong.
11840
12583
  `
11841
12584
  );
@@ -11862,10 +12605,17 @@ async function ensureTenant(baseUrl, g, opts) {
11862
12605
  const local = readLocalConfig();
11863
12606
  let tenant = g.tenant ?? process.env.SECHROOM_TENANT ?? local.tenant ?? persisted.tenant ?? "";
11864
12607
  if (!tenant) {
11865
- const client = await makeClient({ baseUrl, tenant: "", account: resolveAccountAlias(), clientId: persisted.clientId });
12608
+ const client = await makeClient({
12609
+ baseUrl,
12610
+ tenant: "",
12611
+ account: resolveAccountAlias(),
12612
+ clientId: persisted.clientId
12613
+ });
11866
12614
  const { data, error } = await client.GET("/auth/me/tenants", {});
11867
12615
  if (error) {
11868
- fail(`Couldn't list your tenants: ${JSON.stringify(error)}. Pass --tenant <id> to skip this.`);
12616
+ fail(
12617
+ `Couldn't list your tenants: ${JSON.stringify(error)}. Pass --tenant <id> to skip this.`
12618
+ );
11869
12619
  }
11870
12620
  const tenants = data?.tenants ?? [];
11871
12621
  if (tenants.length === 0) {
@@ -11897,21 +12647,44 @@ async function ensureTenant(baseUrl, g, opts) {
11897
12647
  }
11898
12648
  }
11899
12649
  const existingWorkspace = local.workspaceId ?? persisted.workspaceId ?? void 0;
11900
- const wsClient = await makeClient({ baseUrl, tenant, account: resolveAccountAlias(), clientId: persisted.clientId });
11901
- const workspaceId = await resolveWorkspaceBinding(wsClient, existingWorkspace, {
11902
- yes: opts.yes,
11903
- json: opts.json,
11904
- workspace: opts.workspace
12650
+ const wsClient = await makeClient({
12651
+ baseUrl,
12652
+ tenant,
12653
+ account: resolveAccountAlias(),
12654
+ clientId: persisted.clientId
11905
12655
  });
12656
+ const workspaceId = await resolveWorkspaceBinding(
12657
+ wsClient,
12658
+ existingWorkspace,
12659
+ {
12660
+ yes: opts.yes,
12661
+ json: opts.json,
12662
+ workspace: opts.workspace
12663
+ }
12664
+ );
11906
12665
  const defaultProjectId = local.defaultProjectId ?? persisted.defaultProjectId ?? void 0;
11907
- if (defaultProjectId && workspaceId) await warnIfProjectStray(wsClient, defaultProjectId, workspaceId, opts.json);
12666
+ if (defaultProjectId && workspaceId)
12667
+ await warnIfProjectStray(
12668
+ wsClient,
12669
+ defaultProjectId,
12670
+ workspaceId,
12671
+ opts.json
12672
+ );
11908
12673
  let storeLocal = Boolean(opts.local);
11909
12674
  if (!opts.local && canPrompt() && !opts.yes) {
11910
12675
  storeLocal = await promptSelect(
11911
12676
  "Where should this tenant + base URL be saved?",
11912
12677
  [
11913
- { label: "Globally", value: "global", hint: "all projects on this machine" },
11914
- { label: "This directory", value: "local", hint: ".sechroom.json \u2014 committed, project + subdirs" }
12678
+ {
12679
+ label: "Globally",
12680
+ value: "global",
12681
+ hint: "all projects on this machine"
12682
+ },
12683
+ {
12684
+ label: "This directory",
12685
+ value: "local",
12686
+ hint: ".sechroom.json \u2014 committed, project + subdirs"
12687
+ }
11915
12688
  ],
11916
12689
  local.path ? "local" : "global"
11917
12690
  ) === "local";
@@ -11920,19 +12693,34 @@ async function ensureTenant(baseUrl, g, opts) {
11920
12693
  const patch = { baseUrl, tenant, ...workspaceId ? { workspaceId } : {} };
11921
12694
  if (storeLocal) {
11922
12695
  const path = writeLocalConfig(patch, { here: Boolean(opts.here) });
11923
- if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved to ${path} (directory-local)
11924
- `);
12696
+ if (!opts.json)
12697
+ process.stderr.write(
12698
+ `${ok("\u2713")} config saved to ${path} (directory-local)
12699
+ `
12700
+ );
11925
12701
  } else {
11926
12702
  writePersisted(patch);
11927
- if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved globally (~/.config/sechroom/config.json)
11928
- `);
12703
+ if (!opts.json)
12704
+ process.stderr.write(
12705
+ `${ok("\u2713")} config saved globally (~/.config/sechroom/config.json)
12706
+ `
12707
+ );
11929
12708
  }
11930
12709
  if (workspaceId && !existingWorkspace && !opts.json) {
11931
- process.stderr.write(`${ok("\u2713")} bound to workspace ${style.dim(workspaceId)}
11932
- `);
12710
+ process.stderr.write(
12711
+ `${ok("\u2713")} bound to workspace ${style.dim(workspaceId)}
12712
+ `
12713
+ );
11933
12714
  }
11934
12715
  }
11935
- return { baseUrl, tenant, account: resolveAccountAlias(), workspaceId, defaultProjectId, clientId: persisted.clientId };
12716
+ return {
12717
+ baseUrl,
12718
+ tenant,
12719
+ account: resolveAccountAlias(),
12720
+ workspaceId,
12721
+ defaultProjectId,
12722
+ clientId: persisted.clientId
12723
+ };
11936
12724
  }
11937
12725
  async function ensureAuth(cfg, yes) {
11938
12726
  if (process.env.SECHROOM_TOKEN) return;
@@ -11940,17 +12728,27 @@ async function ensureAuth(cfg, yes) {
11940
12728
  const usable = Boolean(cached?.accessToken) && (cached.expiresAt === void 0 || Date.now() < cached.expiresAt - 6e4 || Boolean(cached.refreshToken));
11941
12729
  if (usable) return;
11942
12730
  if (!canPrompt() || yes) {
11943
- fail("Not signed in. Run `sechroom login` first, or set SECHROOM_TOKEN for headless use.");
12731
+ fail(
12732
+ "Not signed in. Run `sechroom login` first, or set SECHROOM_TOKEN for headless use."
12733
+ );
11944
12734
  }
11945
- process.stderr.write("\nNot signed in \u2014 opening the browser to authenticate.\n");
12735
+ process.stderr.write(
12736
+ "\nNot signed in \u2014 opening the browser to authenticate.\n"
12737
+ );
11946
12738
  await login(cfg);
11947
12739
  }
11948
12740
  async function ensureTimezone(cfg, opts) {
11949
12741
  const client = await makeClient(cfg);
11950
12742
  const { data, error } = await client.GET("/me/profile", {});
11951
- if (error) return { timezone: null, action: "skipped", note: "could not read profile" };
12743
+ if (error)
12744
+ return {
12745
+ timezone: null,
12746
+ action: "skipped",
12747
+ note: "could not read profile"
12748
+ };
11952
12749
  const current = data?.effectiveTimezone;
11953
- if (current && current.trim().length > 0) return { timezone: current, action: "already-set" };
12750
+ if (current && current.trim().length > 0)
12751
+ return { timezone: current, action: "already-set" };
11954
12752
  const system = systemTimezone();
11955
12753
  let tz = system;
11956
12754
  if (canPrompt() && !opts.yes) {
@@ -11962,12 +12760,18 @@ async function ensureTimezone(cfg, opts) {
11962
12760
  note: "no timezone set \u2014 re-run interactively or pass --yes to adopt the system timezone"
11963
12761
  };
11964
12762
  }
11965
- if (!tz) return { timezone: null, action: "skipped", note: "no timezone provided" };
12763
+ if (!tz)
12764
+ return { timezone: null, action: "skipped", note: "no timezone provided" };
11966
12765
  if (opts.dryRun) return { timezone: tz, action: "dry-run" };
11967
12766
  const { error: putErr } = await client.PUT("/me/profile", {
11968
12767
  body: { displayName: null, photoUrl: null, bio: null, timezone: tz }
11969
12768
  });
11970
- if (putErr) return { timezone: tz, action: "skipped", note: `update failed: ${JSON.stringify(putErr)}` };
12769
+ if (putErr)
12770
+ return {
12771
+ timezone: tz,
12772
+ action: "skipped",
12773
+ note: `update failed: ${JSON.stringify(putErr)}`
12774
+ };
11971
12775
  return { timezone: tz, action: "set" };
11972
12776
  }
11973
12777
  async function chooseClients(clientFlag, yes, cwd) {
@@ -11992,7 +12796,11 @@ async function chooseScope(scopeFlag, yes) {
11992
12796
  return promptSelect(
11993
12797
  "Install skills, agents, and hooks globally or just for this project?",
11994
12798
  [
11995
- { label: "Globally", value: "global", hint: "~/.claude (or CLAUDE_CONFIG_DIR) \u2014 all projects" },
12799
+ {
12800
+ label: "Globally",
12801
+ value: "global",
12802
+ hint: "~/.claude (or CLAUDE_CONFIG_DIR) \u2014 all projects"
12803
+ },
11996
12804
  { label: "This project", value: "project", hint: "<repo>/.claude" }
11997
12805
  ],
11998
12806
  "global"
@@ -12001,7 +12809,13 @@ async function chooseScope(scopeFlag, yes) {
12001
12809
  async function planRecurseChild(entry, root, client, opts) {
12002
12810
  const dir = resolveChildDir(entry.path, root);
12003
12811
  if (!existsSync15(dir)) {
12004
- return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
12812
+ return {
12813
+ label: entry.path,
12814
+ dir,
12815
+ disposition: "skip-missing",
12816
+ argv: [],
12817
+ reason: "directory does not exist"
12818
+ };
12005
12819
  }
12006
12820
  if (existsSync15(join21(dir, ".sechroom.json"))) {
12007
12821
  return {
@@ -12022,20 +12836,40 @@ async function planRecurseChild(entry, root, client, opts) {
12022
12836
  };
12023
12837
  }
12024
12838
  if (opts.dryRun) {
12025
- return { label: entry.path, dir, disposition: "bind", argv: ["onboard", "--yes", "--local", "--workspace", "<prompt>"], reason: "unbound \u2014 would prompt for a workspace" };
12839
+ return {
12840
+ label: entry.path,
12841
+ dir,
12842
+ disposition: "bind",
12843
+ argv: ["onboard", "--yes", "--local", "--workspace", "<prompt>"],
12844
+ reason: "unbound \u2014 would prompt for a workspace"
12845
+ };
12026
12846
  }
12027
12847
  if (opts.yes || !canPrompt()) {
12028
- return { label: entry.path, dir, disposition: "skip-unbound", argv: [], reason: "unbound + no workspace (run interactively, or add it to ./.sechroom/repos.json)" };
12848
+ return {
12849
+ label: entry.path,
12850
+ dir,
12851
+ disposition: "skip-unbound",
12852
+ argv: [],
12853
+ reason: "unbound + no workspace (run interactively, or add it to ./.sechroom/repos.json)"
12854
+ };
12029
12855
  }
12030
- process.stderr.write(`
12856
+ process.stderr.write(
12857
+ `
12031
12858
  ${style.bold(entry.path)} ${style.dim("is not bound yet.")}
12032
- `);
12859
+ `
12860
+ );
12033
12861
  const ws = await pickWorkspace(client, {
12034
12862
  promptLabel: `Bind ${style.cyan(entry.path)} to a workspace:`,
12035
12863
  dirName: basename5(entry.path)
12036
12864
  });
12037
12865
  if (!ws) {
12038
- return { label: entry.path, dir, disposition: "skip-unbound", argv: [], reason: "unbound \u2014 no workspace chosen (skipped)" };
12866
+ return {
12867
+ label: entry.path,
12868
+ dir,
12869
+ disposition: "skip-unbound",
12870
+ argv: [],
12871
+ reason: "unbound \u2014 no workspace chosen (skipped)"
12872
+ };
12039
12873
  }
12040
12874
  return {
12041
12875
  label: entry.path,
@@ -12050,20 +12884,34 @@ async function resolveFanoutLane(cfg, opts) {
12050
12884
  let design = opts.designLane ?? process.env.SECHROOM_DESIGN_LANE;
12051
12885
  if (!code || !design) {
12052
12886
  const clients = detectInstalledClients(process.cwd());
12053
- const inferred = await inferLanes(cfg, clients.length ? clients : void 0);
12887
+ const inferred = await inferLanes(
12888
+ cfg,
12889
+ clients.length ? clients : void 0
12890
+ );
12054
12891
  code = code ?? inferred.code;
12055
12892
  design = design ?? inferred.design;
12056
12893
  }
12057
12894
  if (!opts.lane && !opts.yes && !opts.dryRun && canPrompt() && (code || design)) {
12058
- process.stderr.write(`
12895
+ process.stderr.write(
12896
+ `
12059
12897
  This fan-out will pin the same lane in every repo:
12060
- `);
12061
- if (code) process.stderr.write(` ${style.dim("code-lane")} = ${style.cyan(code)}
12062
- `);
12063
- if (design) process.stderr.write(` ${style.dim("design-lane")} = ${style.cyan(design)}
12064
- `);
12898
+ `
12899
+ );
12900
+ if (code)
12901
+ process.stderr.write(
12902
+ ` ${style.dim("code-lane")} = ${style.cyan(code)}
12903
+ `
12904
+ );
12905
+ if (design)
12906
+ process.stderr.write(
12907
+ ` ${style.dim("design-lane")} = ${style.cyan(design)}
12908
+ `
12909
+ );
12065
12910
  if (!await promptYesNo("Use this lane for all repos?")) {
12066
- code = await promptText("Code-lane id (blank = let each repo infer)?", code ?? "") || void 0;
12911
+ code = await promptText(
12912
+ "Code-lane id (blank = let each repo infer)?",
12913
+ code ?? ""
12914
+ ) || void 0;
12067
12915
  design = await promptText("Design-lane id (blank = skip)?", design ?? "") || void 0;
12068
12916
  }
12069
12917
  }
@@ -12081,30 +12929,112 @@ async function runRecurse(cfg, g, opts) {
12081
12929
  const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
12082
12930
  const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
12083
12931
  if (entries.length === 0) {
12084
- if (json) process.stdout.write(JSON.stringify({ recurse: true, root, repos: [] }) + "\n");
12085
- else process.stderr.write(`${warn("\u26A0")} no child repos found ${fromManifest ? `in ${manifestPath}` : `under ${root}`} \u2014 nothing to do.
12086
- `);
12932
+ if (json)
12933
+ process.stdout.write(
12934
+ JSON.stringify({ recurse: true, root, repos: [] }) + "\n"
12935
+ );
12936
+ else
12937
+ process.stderr.write(
12938
+ `${warn("\u26A0")} no child repos found ${fromManifest ? `in ${manifestPath}` : `under ${root}`} \u2014 nothing to do.
12939
+ `
12940
+ );
12087
12941
  return;
12088
12942
  }
12089
12943
  if (!json) {
12090
- process.stderr.write(`${style.bold("onboard --recurse")} ${style.dim(`(${entries.length} repo${entries.length === 1 ? "" : "s"} from ${sourceLabel})`)}
12091
- `);
12944
+ process.stderr.write(
12945
+ `${style.bold("onboard --recurse")} ${style.dim(`(${entries.length} repo${entries.length === 1 ? "" : "s"} from ${sourceLabel})`)}
12946
+ `
12947
+ );
12092
12948
  }
12093
- const lane = await resolveFanoutLane(cfg, { lane: opts.lane, designLane: opts.designLane, yes, dryRun });
12094
- if (!json && lane.code) process.stderr.write(`${ok("\u2713")} lane ${style.cyan(lane.code)}${lane.design ? ` ${style.dim(`/ ${lane.design}`)}` : ""} for every repo
12095
- `);
12949
+ const lane = await resolveFanoutLane(cfg, {
12950
+ lane: opts.lane,
12951
+ designLane: opts.designLane,
12952
+ yes,
12953
+ dryRun
12954
+ });
12955
+ if (!json && lane.code)
12956
+ process.stderr.write(
12957
+ `${ok("\u2713")} lane ${style.cyan(lane.code)}${lane.design ? ` ${style.dim(`/ ${lane.design}`)}` : ""} for every repo
12958
+ `
12959
+ );
12096
12960
  const client = await makeClient(cfg);
12097
12961
  const plans = [];
12098
- for (const entry of entries) plans.push(await planRecurseChild(entry, root, client, { yes, dryRun }));
12099
- const results = runChildren(plans, { globals: passthroughGlobals(g), dryRun, json });
12962
+ for (const entry of entries)
12963
+ plans.push(await planRecurseChild(entry, root, client, { yes, dryRun }));
12964
+ const results = runChildren(plans, {
12965
+ globals: passthroughGlobals(g),
12966
+ dryRun,
12967
+ json
12968
+ });
12100
12969
  if (json) {
12101
- process.stdout.write(JSON.stringify({ recurse: true, root, dryRun, repos: results }) + "\n");
12970
+ process.stdout.write(
12971
+ JSON.stringify({ recurse: true, root, dryRun, repos: results }) + "\n"
12972
+ );
12102
12973
  return;
12103
12974
  }
12104
12975
  summarizeFanout(results, { dryRun });
12105
12976
  }
12106
12977
  function registerOnboard(program2) {
12107
- program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--recurse", "orchestration-root mode: onboard every child repo under this dir (auto-discovered, or from ./.sechroom/repos.json) \u2014 refreshes bound repos, prompts a workspace per new one", false).option("--lane <id>", "set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo").option("--design-lane <id>", "set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child").option("--client <list...>", `clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`).option("--scope <scope>", "install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default: prompt, else global").option("--local", "save the binding (tenant + base URL + workspace) to a committed .sechroom.json in this repo instead of the global config", false).option("--here", "with --local: write the binding at THIS directory even when a parent already carries one \u2014 binds a subtree (e.g. a monorepo's frontend/) to its own workspace", false).option("--workspace <id>", "bind this directory to a workspace (skips the interactive workspace pick)").option("--cli-only", "configure the CLI only \u2014 don't wire any AI client (no MCP config, no agent files)", false).option("--no-mcp", "skip the MCP server config (.mcp.json etc.); still write the agent instruction files").option("--copy", "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)").option("--dry-run", "walk through without writing files or changing the profile", false).option("--refresh", "re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)", false).option("--force", "rewrite every managed block, overwriting local edits inside the markers (content outside untouched)", false).option("--check", "report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing", false).option("-y, --yes", "non-interactive: accept defaults (system timezone, detected clients, global config, full wire)", false).addHelpText(
12978
+ program2.command("onboard").description(
12979
+ "Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project"
12980
+ ).option(
12981
+ "--recurse",
12982
+ "orchestration-root mode: onboard every child repo under this dir (auto-discovered, or from ./.sechroom/repos.json) \u2014 refreshes bound repos, prompts a workspace per new one",
12983
+ false
12984
+ ).option(
12985
+ "--lane <id>",
12986
+ "set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo"
12987
+ ).option(
12988
+ "--design-lane <id>",
12989
+ "set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child"
12990
+ ).option(
12991
+ "--client <list...>",
12992
+ `clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`
12993
+ ).option(
12994
+ "--scope <scope>",
12995
+ "install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default: prompt, else global"
12996
+ ).option(
12997
+ "--local",
12998
+ "save the binding (tenant + base URL + workspace) to a committed .sechroom.json in this repo instead of the global config",
12999
+ false
13000
+ ).option(
13001
+ "--here",
13002
+ "with --local: write the binding at THIS directory even when a parent already carries one \u2014 binds a subtree (e.g. a monorepo's frontend/) to its own workspace",
13003
+ false
13004
+ ).option(
13005
+ "--workspace <id>",
13006
+ "bind this directory to a workspace (skips the interactive workspace pick)"
13007
+ ).option(
13008
+ "--cli-only",
13009
+ "configure the CLI only \u2014 don't wire any AI client (no MCP config, no agent files)",
13010
+ false
13011
+ ).option(
13012
+ "--no-mcp",
13013
+ "skip the MCP server config (.mcp.json etc.); still write the agent instruction files"
13014
+ ).option(
13015
+ "--copy",
13016
+ "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)"
13017
+ ).option(
13018
+ "--dry-run",
13019
+ "walk through without writing files or changing the profile",
13020
+ false
13021
+ ).option(
13022
+ "--refresh",
13023
+ "re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)",
13024
+ false
13025
+ ).option(
13026
+ "--force",
13027
+ "rewrite every managed block, overwriting local edits inside the markers (content outside untouched)",
13028
+ false
13029
+ ).option(
13030
+ "--check",
13031
+ "report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
13032
+ false
13033
+ ).option(
13034
+ "-y, --yes",
13035
+ "non-interactive: accept defaults (system timezone, detected clients, global config, full wire)",
13036
+ false
13037
+ ).addHelpText(
12108
13038
  "after",
12109
13039
  `
12110
13040
  Examples:
@@ -12127,20 +13057,56 @@ Examples:
12127
13057
  const check = mode === "check";
12128
13058
  const yes = Boolean(opts.yes) || check;
12129
13059
  if (check && (opts.recurse || opts.cliOnly)) {
12130
- fail("--check inspects this project's agent files and cannot be combined with --recurse or --cli-only.");
13060
+ fail(
13061
+ "--check inspects this project's agent files and cannot be combined with --recurse or --cli-only."
13062
+ );
12131
13063
  }
12132
13064
  if (opts.lane) process.env.SECHROOM_CODE_LANE = opts.lane;
12133
13065
  if (opts.designLane) process.env.SECHROOM_DESIGN_LANE = opts.designLane;
12134
13066
  if (opts.recurse) {
12135
13067
  const baseUrl2 = resolveBaseUrl(g);
12136
- await ensureAuth({ baseUrl: baseUrl2, tenant: "", account: resolveAccountAlias(), clientId: readPersisted().clientId }, yes);
12137
- const cfg2 = await ensureTenant(baseUrl2, g, { yes: true, json, persist: false });
12138
- await runRecurse(cfg2, g, { yes, dryRun, json, lane: opts.lane, designLane: opts.designLane });
13068
+ await ensureAuth(
13069
+ {
13070
+ baseUrl: baseUrl2,
13071
+ tenant: "",
13072
+ account: resolveAccountAlias(),
13073
+ clientId: readPersisted().clientId
13074
+ },
13075
+ yes
13076
+ );
13077
+ const cfg2 = await ensureTenant(baseUrl2, g, {
13078
+ yes: true,
13079
+ json,
13080
+ persist: false
13081
+ });
13082
+ await runRecurse(cfg2, g, {
13083
+ yes,
13084
+ dryRun,
13085
+ json,
13086
+ lane: opts.lane,
13087
+ designLane: opts.designLane
13088
+ });
12139
13089
  return;
12140
13090
  }
12141
13091
  const baseUrl = resolveBaseUrl(g);
12142
- await ensureAuth({ baseUrl, tenant: "", account: resolveAccountAlias(), clientId: readPersisted().clientId }, yes);
12143
- const cfg = await ensureTenant(baseUrl, g, { yes, json, local: Boolean(opts.local), workspace: opts.workspace, persist: !check });
13092
+ await ensureAuth(
13093
+ {
13094
+ baseUrl,
13095
+ tenant: "",
13096
+ account: resolveAccountAlias(),
13097
+ clientId: readPersisted().clientId
13098
+ },
13099
+ yes
13100
+ );
13101
+ const scope = await chooseScope(opts.scope, yes);
13102
+ const cfg = await ensureTenant(baseUrl, g, {
13103
+ yes,
13104
+ json,
13105
+ local: Boolean(opts.local) || scope === "project",
13106
+ here: scope === "project" ? true : Boolean(opts.here),
13107
+ workspace: opts.workspace,
13108
+ persist: !check
13109
+ });
12144
13110
  const tz = await ensureTimezone(cfg, { yes, dryRun: dryRun || check });
12145
13111
  if (!json && tz.action !== "already-set") {
12146
13112
  const line = tz.action === "set" ? `${ok("\u2713")} timezone set to ${tz.timezone}
@@ -12150,21 +13116,48 @@ Examples:
12150
13116
  process.stderr.write(line);
12151
13117
  }
12152
13118
  const wire = await chooseWire(opts, yes);
12153
- const scope = await chooseScope(opts.scope, yes);
12154
- const claudeTargets = resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd: process.cwd() });
13119
+ const claudeTargets = resolveClaudeTargets({
13120
+ override: g.claudeConfigDir,
13121
+ scope,
13122
+ cwd: process.cwd()
13123
+ });
12155
13124
  const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
12156
13125
  if (scope === "project" && g.claudeConfigDir && !json) {
12157
- process.stderr.write(`${style.dim("(--claude-config-dir is ignored for --scope project \u2014 project files are repo-relative)")}
12158
- `);
13126
+ process.stderr.write(
13127
+ `${style.dim("(--claude-config-dir is ignored for --scope project \u2014 project files are repo-relative)")}
13128
+ `
13129
+ );
12159
13130
  }
12160
13131
  if (wire === "cli-only") {
12161
13132
  if (json) {
12162
- emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, clients: [] }, true);
13133
+ emit(
13134
+ {
13135
+ dryRun,
13136
+ baseUrl: cfg.baseUrl,
13137
+ tenant: cfg.tenant,
13138
+ workspaceId: cfg.workspaceId ?? null,
13139
+ timezone: tz,
13140
+ wire,
13141
+ clients: []
13142
+ },
13143
+ true
13144
+ );
12163
13145
  return;
12164
13146
  }
12165
13147
  if (!dryRun) {
12166
- await ensureLanePin(cfg, { yes, dryRun, clients: detectInstalledClients(process.cwd()) });
12167
- await maybeOfferHooks({ yes, dryRun, cwd: process.cwd(), scope, claudeConfigDir: g.claudeConfigDir, codexHome: g.codexHome });
13148
+ await ensureLanePin(cfg, {
13149
+ yes,
13150
+ dryRun,
13151
+ clients: detectInstalledClients(process.cwd())
13152
+ });
13153
+ await maybeOfferHooks({
13154
+ yes,
13155
+ dryRun,
13156
+ cwd: process.cwd(),
13157
+ scope,
13158
+ claudeConfigDir: g.claudeConfigDir,
13159
+ codexHome: g.codexHome
13160
+ });
12168
13161
  }
12169
13162
  process.stdout.write(
12170
13163
  `
@@ -12175,12 +13168,36 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
12175
13168
  await printStarterPrompt("cli");
12176
13169
  return;
12177
13170
  }
12178
- const keys = await chooseClients(opts.client, yes, process.cwd());
12179
- const setup = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
12180
- const targets = clientTargets(process.cwd(), { claudeDir: claudeTargets[0]?.dir, codexHome: codexHomes[0] });
13171
+ const requestedKeys = await chooseClients(
13172
+ opts.client,
13173
+ yes,
13174
+ process.cwd()
13175
+ );
13176
+ const keys = scope === "project" ? requestedKeys.filter((key) => key !== "codex") : requestedKeys;
13177
+ if (scope === "project" && requestedKeys.includes("codex") && !json) {
13178
+ process.stderr.write(
13179
+ `${style.dim("Codex has no project scope \u2014 skipped (use --scope global for Codex).")}
13180
+ `
13181
+ );
13182
+ }
13183
+ const setup = await withSpinner(
13184
+ "Fetching setup descriptors",
13185
+ () => fetchSetup(cfg)
13186
+ );
13187
+ const targets = clientTargets(process.cwd(), {
13188
+ claudeDir: claudeTargets[0]?.dir,
13189
+ codexHome: codexHomes[0] ?? null
13190
+ });
12181
13191
  const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
12182
13192
  if (!dryRun && !check) {
12183
- await maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, copyChoice(opts));
13193
+ await maybeOfferCopies(
13194
+ cfg,
13195
+ setup,
13196
+ targets,
13197
+ keys,
13198
+ personalWorkspaceId,
13199
+ copyChoice(opts)
13200
+ );
12184
13201
  }
12185
13202
  const writeMcp = wire === "full";
12186
13203
  const result = [];
@@ -12197,16 +13214,11 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
12197
13214
  if (!json && !check) printActions(target, actions);
12198
13215
  }
12199
13216
  if (check) {
12200
- reportCheckAndExit(
12201
- result,
12202
- json,
12203
- "sechroom onboard --refresh",
12204
- {
12205
- baseUrl: cfg.baseUrl,
12206
- tenant: cfg.tenant,
12207
- workspaceId: cfg.workspaceId ?? null
12208
- }
12209
- );
13217
+ reportCheckAndExit(result, json, "sechroom onboard --refresh", {
13218
+ baseUrl: cfg.baseUrl,
13219
+ tenant: cfg.tenant,
13220
+ workspaceId: cfg.workspaceId ?? null
13221
+ });
12210
13222
  }
12211
13223
  const evalCounts = buildCheckReport(result).eval;
12212
13224
  if (!json && !dryRun) {
@@ -12214,19 +13226,45 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
12214
13226
  }
12215
13227
  if (!json && !dryRun) {
12216
13228
  for (const t of claudeTargets) {
12217
- await maybeOfferSkills(cfg, personalWorkspaceId, { yes, dryRun, surface: "claude-code", configDir: t.dir });
13229
+ await maybeOfferSkills(cfg, personalWorkspaceId, {
13230
+ yes,
13231
+ dryRun,
13232
+ surface: "claude-code",
13233
+ configDir: t.dir
13234
+ });
12218
13235
  }
12219
13236
  }
12220
13237
  if (!json && !dryRun) {
12221
- await maybeOfferHooks({ yes, dryRun, cwd: process.cwd(), scope, claudeConfigDir: g.claudeConfigDir, codexHome: g.codexHome });
13238
+ await maybeOfferHooks({
13239
+ yes,
13240
+ dryRun,
13241
+ cwd: process.cwd(),
13242
+ scope,
13243
+ claudeConfigDir: g.claudeConfigDir,
13244
+ codexHome: g.codexHome
13245
+ });
12222
13246
  }
12223
13247
  if (json) {
12224
- emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, eval: evalCounts, clients: result }, true);
13248
+ emit(
13249
+ {
13250
+ dryRun,
13251
+ baseUrl: cfg.baseUrl,
13252
+ tenant: cfg.tenant,
13253
+ workspaceId: cfg.workspaceId ?? null,
13254
+ timezone: tz,
13255
+ wire,
13256
+ eval: evalCounts,
13257
+ clients: result
13258
+ },
13259
+ true
13260
+ );
12225
13261
  return;
12226
13262
  }
12227
13263
  if (!dryRun && evalCounts.stale) {
12228
- process.stderr.write(`${style.cyan("\u21BB")} refreshed ${evalCounts.stale} section(s) the server had moved
12229
- `);
13264
+ process.stderr.write(
13265
+ `${style.cyan("\u21BB")} refreshed ${evalCounts.stale} section(s) the server had moved
13266
+ `
13267
+ );
12230
13268
  }
12231
13269
  if (!dryRun && evalCounts.drift) {
12232
13270
  process.stderr.write(
@@ -12235,7 +13273,9 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
12235
13273
  `
12236
13274
  );
12237
13275
  }
12238
- const wroteSomething = result.some(({ actions }) => actions.some((a) => a.status === "created" || a.status === "merged"));
13276
+ const wroteSomething = result.some(
13277
+ ({ actions }) => actions.some((a) => a.status === "created" || a.status === "merged")
13278
+ );
12239
13279
  process.stdout.write(
12240
13280
  dryRun ? "\n(dry run \u2014 nothing written)\n" : !wroteSomething ? `
12241
13281
  ${style.bold("Done.")} Everything's already up to date.
@@ -12254,9 +13294,21 @@ async function chooseWire(opts, yes) {
12254
13294
  return promptSelect(
12255
13295
  "How should I set up Sechroom in this project?",
12256
13296
  [
12257
- { label: "Wire my AI client", value: "full", hint: "MCP server (.mcp.json) + agent instructions" },
12258
- { label: "Agent instructions only", value: "agent-only", hint: "skip MCP config" },
12259
- { label: "CLI only", value: "cli-only", hint: "don't write any AI-client files" }
13297
+ {
13298
+ label: "Wire my AI client",
13299
+ value: "full",
13300
+ hint: "MCP server (.mcp.json) + agent instructions"
13301
+ },
13302
+ {
13303
+ label: "Agent instructions only",
13304
+ value: "agent-only",
13305
+ hint: "skip MCP config"
13306
+ },
13307
+ {
13308
+ label: "CLI only",
13309
+ value: "cli-only",
13310
+ hint: "don't write any AI-client files"
13311
+ }
12260
13312
  ],
12261
13313
  "full"
12262
13314
  );
@@ -12278,7 +13330,9 @@ ${rule}
12278
13330
  }
12279
13331
  async function printStarterPrompt(mode, cfg) {
12280
13332
  if (mode === "cli") {
12281
- printNextStepBlock("Next \u2014 pick up where you left off:", [style.cyan("sechroom continuity resume-me")]);
13333
+ printNextStepBlock("Next \u2014 pick up where you left off:", [
13334
+ style.cyan("sechroom continuity resume-me")
13335
+ ]);
12282
13336
  return;
12283
13337
  }
12284
13338
  let primary = FALLBACK_AGENT_PROMPT;
@@ -12290,7 +13344,9 @@ async function printStarterPrompt(mode, cfg) {
12290
13344
  } catch {
12291
13345
  }
12292
13346
  }
12293
- printNextStepBlock("Next \u2014 paste this into your AI agent to get going:", [style.cyan(`"${primary}"`)]);
13347
+ printNextStepBlock("Next \u2014 paste this into your AI agent to get going:", [
13348
+ style.cyan(`"${primary}"`)
13349
+ ]);
12294
13350
  }
12295
13351
 
12296
13352
  // src/commands/project.ts
@@ -12605,7 +13661,7 @@ Examples:
12605
13661
  // src/commands/reset.ts
12606
13662
  import { homedir as homedir6 } from "os";
12607
13663
  import { join as join22 } from "path";
12608
- import { existsSync as existsSync16, readFileSync as readFileSync18, rmSync as rmSync7 } from "fs";
13664
+ import { existsSync as existsSync16, readFileSync as readFileSync18, rmSync as rmSync8 } from "fs";
12609
13665
  var SKILLS_LOCK2 = ".sechroom-skills.json";
12610
13666
  var localSkillsDir = () => join22(process.cwd(), ".claude", "skills");
12611
13667
  var globalSkillsDir = () => join22(homedir6(), ".claude", "skills");
@@ -12621,14 +13677,14 @@ function removeMaterialisedSkills(dir) {
12621
13677
  for (const name of entry.skills ?? []) {
12622
13678
  const p = join22(dir, name);
12623
13679
  if (existsSync16(p)) {
12624
- rmSync7(p, { recursive: true, force: true });
13680
+ rmSync8(p, { recursive: true, force: true });
12625
13681
  removed.push(p);
12626
13682
  }
12627
13683
  }
12628
13684
  }
12629
13685
  } catch {
12630
13686
  }
12631
- rmSync7(lockPath, { force: true });
13687
+ rmSync8(lockPath, { force: true });
12632
13688
  removed.push(lockPath);
12633
13689
  return removed;
12634
13690
  }
@@ -12667,17 +13723,17 @@ function registerReset(program2) {
12667
13723
  const removed = [];
12668
13724
  const stateDir = join22(process.cwd(), ".sechroom");
12669
13725
  if (existsSync16(stateDir)) {
12670
- rmSync7(stateDir, { recursive: true, force: true });
13726
+ rmSync8(stateDir, { recursive: true, force: true });
12671
13727
  removed.push(stateDir);
12672
13728
  }
12673
13729
  const legacyCfg = join22(process.cwd(), ".sechroom.json");
12674
13730
  if (existsSync16(legacyCfg)) {
12675
- rmSync7(legacyCfg, { force: true });
13731
+ rmSync8(legacyCfg, { force: true });
12676
13732
  removed.push(legacyCfg);
12677
13733
  }
12678
13734
  const legacySem = join22(process.cwd(), ".sem");
12679
13735
  if (existsSync16(legacySem)) {
12680
- rmSync7(legacySem, { force: true });
13736
+ rmSync8(legacySem, { force: true });
12681
13737
  removed.push(legacySem);
12682
13738
  }
12683
13739
  removed.push(...removeMaterialisedSkills(localSkillsDir()));
@@ -12702,7 +13758,7 @@ function registerReset(program2) {
12702
13758
  }
12703
13759
 
12704
13760
  // src/commands/skills.ts
12705
- import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as statSync6, writeFileSync as writeFileSync17 } from "fs";
13761
+ import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as statSync7, writeFileSync as writeFileSync17 } from "fs";
12706
13762
  import { join as join23 } from "path";
12707
13763
  function filenameFromDisposition(header) {
12708
13764
  if (!header) return void 0;
@@ -12712,7 +13768,7 @@ function filenameFromDisposition(header) {
12712
13768
  function resolveOutputPath(output, serverFilename) {
12713
13769
  const filename = serverFilename || "skills.zip";
12714
13770
  if (!output) return join23(process.cwd(), filename);
12715
- const looksLikeDir = output.endsWith("/") || existsSync17(output) && statSync6(output).isDirectory();
13771
+ const looksLikeDir = output.endsWith("/") || existsSync17(output) && statSync7(output).isDirectory();
12716
13772
  if (looksLikeDir) {
12717
13773
  mkdirSync18(output, { recursive: true });
12718
13774
  return join23(output, filename);