@sechroom/cli 2026.8.1 → 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 +1347 -229
  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
  }
@@ -8975,7 +9022,7 @@ function herdrRequest(socketPath, method, params, timeoutMs = 1e4) {
8975
9022
  });
8976
9023
  }
8977
9024
  function herdrStream(socketPath, subscriptions, onEvent, options = {}) {
8978
- const { signal, onWarning, subscribeTimeoutMs = 1e4 } = options;
9025
+ const { signal, onWarning, onReady, subscribeTimeoutMs = 1e4 } = options;
8979
9026
  return new Promise((resolvePromise, rejectPromise) => {
8980
9027
  if (signal?.aborted) {
8981
9028
  resolvePromise();
@@ -9017,6 +9064,7 @@ function herdrStream(socketPath, subscriptions, onEvent, options = {}) {
9017
9064
  if (!acked) {
9018
9065
  acked = true;
9019
9066
  clearTimeout(ackTimer);
9067
+ onReady?.();
9020
9068
  }
9021
9069
  const classified = classifyStreamLine(line, id);
9022
9070
  if (classified.kind === "ack") continue;
@@ -9191,9 +9239,12 @@ function indexAgents(agents) {
9191
9239
  return new Map(agents.map((agent) => [agent.pane_id, agent]));
9192
9240
  }
9193
9241
  function buildWatchSubscriptions(agents) {
9194
- return agents.flatMap((agent) => [
9195
- { type: "pane.agent_status_changed", pane_id: agent.pane_id },
9196
- { type: "pane.exited", pane_id: agent.pane_id }
9242
+ return buildPaneSubscriptions(agents.map((agent) => agent.pane_id));
9243
+ }
9244
+ function buildPaneSubscriptions(paneIds) {
9245
+ return [...new Set(paneIds)].flatMap((pane_id) => [
9246
+ { type: "pane.agent_status_changed", pane_id },
9247
+ { type: "pane.exited", pane_id }
9197
9248
  ]);
9198
9249
  }
9199
9250
  function normalizeWatchEvent(raw) {
@@ -9385,6 +9436,7 @@ async function runWait(ports, targets, options) {
9385
9436
  const remaining = new Set(targetIds);
9386
9437
  let seed = /* @__PURE__ */ new Map();
9387
9438
  let attempt = 0;
9439
+ const subscriptions = buildPaneSubscriptions(targetIds);
9388
9440
  const settle = (agent, paneId, status) => {
9389
9441
  if (!remaining.has(paneId)) return;
9390
9442
  const record = {
@@ -9403,67 +9455,124 @@ async function runWait(ports, targets, options) {
9403
9455
  try {
9404
9456
  while (!controller.signal.aborted) {
9405
9457
  try {
9406
- const listed = await raceAbort(
9407
- ports.request("agent.list", {})
9408
- );
9409
- if (listed === aborted) break;
9410
- const current = listed.agents ?? [];
9411
- const fresh = indexAgents(current);
9412
- for (const paneId of [...remaining]) {
9413
- const currentAgent = fresh.get(paneId);
9414
- if (currentAgent === void 0) {
9415
- settle(seed.get(paneId), paneId, "exited");
9416
- continue;
9417
- }
9418
- const status = currentAgent.agent_status?.trim().toLowerCase();
9419
- if (status !== void 0 && statuses.has(status)) {
9420
- settle(currentAgent, paneId, currentAgent.agent_status ?? status);
9421
- }
9422
- }
9423
- seed = fresh;
9424
- if (completed || remaining.size === 0) return;
9425
- const subscriptions = buildWatchSubscriptions(
9426
- current.filter((agent) => remaining.has(agent.pane_id))
9427
- );
9428
9458
  let carriedTraffic = false;
9429
9459
  let relistNeeded = false;
9460
+ let reconciled = false;
9461
+ const bufferedEvents = [];
9430
9462
  const startedAt = now();
9431
9463
  const cycle = new AbortController();
9432
9464
  const stopCycle = () => cycle.abort();
9433
9465
  controller.signal.addEventListener("abort", stopCycle, { once: true });
9466
+ let streamReady = false;
9467
+ let streamFailed = false;
9468
+ let streamFailure;
9469
+ let resolveReady;
9470
+ let rejectReady;
9471
+ const ready = new Promise((resolve9, reject) => {
9472
+ resolveReady = resolve9;
9473
+ rejectReady = reject;
9474
+ });
9475
+ const handleEvent = (event) => {
9476
+ if (!remaining.has(event.paneId)) return;
9477
+ const known = seed.get(event.paneId);
9478
+ if (event.type === "pane.exited") {
9479
+ settle(known, event.paneId, "exited");
9480
+ return;
9481
+ }
9482
+ const status = event.to?.trim().toLowerCase();
9483
+ if (known && event.to) known.agent_status = event.to;
9484
+ if (status !== void 0 && statuses.has(status)) {
9485
+ settle(known, event.paneId, event.to ?? status);
9486
+ }
9487
+ };
9488
+ let streamFinished;
9434
9489
  try {
9435
- const streamed = await raceAbort(
9436
- ports.stream(
9437
- subscriptions,
9438
- (raw) => {
9439
- carriedTraffic = true;
9440
- const event = normalizeWatchEvent(raw);
9441
- if (event === null || needsRelistFallback(event)) {
9442
- relistNeeded = true;
9443
- cycle.abort();
9444
- return;
9445
- }
9446
- if (!remaining.has(event.paneId)) return;
9447
- const known = seed.get(event.paneId);
9448
- if (event.type === "pane.exited") {
9449
- settle(known, event.paneId, "exited");
9450
- return;
9451
- }
9452
- const status = event.to?.trim().toLowerCase();
9453
- if (known && event.to) known.agent_status = event.to;
9454
- if (status !== void 0 && statuses.has(status)) {
9455
- settle(known, event.paneId, event.to ?? status);
9456
- }
9490
+ const stream = ports.stream(
9491
+ subscriptions,
9492
+ (raw) => {
9493
+ carriedTraffic = true;
9494
+ if (!streamReady) {
9495
+ streamReady = true;
9496
+ resolveReady();
9497
+ }
9498
+ const event = normalizeWatchEvent(raw);
9499
+ if (event === null || needsRelistFallback(event)) {
9500
+ relistNeeded = true;
9501
+ cycle.abort();
9502
+ return;
9503
+ }
9504
+ if (!reconciled) {
9505
+ bufferedEvents.push(event);
9506
+ return;
9507
+ }
9508
+ handleEvent(event);
9509
+ },
9510
+ {
9511
+ signal: cycle.signal,
9512
+ onReady: () => {
9513
+ streamReady = true;
9514
+ resolveReady();
9457
9515
  },
9458
- {
9459
- signal: cycle.signal,
9460
- onWarning: (message) => warn2(`herdr wait: ${message}`)
9516
+ onWarning: (message) => warn2(`herdr wait: ${message}`)
9517
+ }
9518
+ );
9519
+ streamFinished = stream.then(
9520
+ () => {
9521
+ if (!streamReady) {
9522
+ rejectReady(
9523
+ new Error("Herdr subscription closed before it was ready")
9524
+ );
9461
9525
  }
9462
- )
9526
+ },
9527
+ (error) => {
9528
+ streamFailed = true;
9529
+ streamFailure = error;
9530
+ if (!streamReady) rejectReady(error);
9531
+ }
9532
+ );
9533
+ } catch (error) {
9534
+ streamFailed = true;
9535
+ streamFailure = error;
9536
+ rejectReady(error);
9537
+ streamFinished = Promise.resolve();
9538
+ }
9539
+ try {
9540
+ const readyResult = await raceAbort(ready);
9541
+ if (readyResult === aborted) break;
9542
+ const listed = await raceAbort(
9543
+ ports.request("agent.list", {})
9463
9544
  );
9545
+ if (listed === aborted) break;
9546
+ const current = listed.agents ?? [];
9547
+ const fresh = indexAgents(current);
9548
+ for (const paneId of [...remaining]) {
9549
+ const currentAgent = fresh.get(paneId);
9550
+ if (currentAgent === void 0) {
9551
+ settle(seed.get(paneId), paneId, "exited");
9552
+ }
9553
+ }
9554
+ seed = fresh;
9555
+ reconciled = true;
9556
+ for (const paneId of [...remaining]) {
9557
+ const currentAgent = fresh.get(paneId);
9558
+ if (currentAgent === void 0) continue;
9559
+ for (const event of bufferedEvents) {
9560
+ if (event.paneId === paneId) handleEvent(event);
9561
+ }
9562
+ if (!remaining.has(paneId)) continue;
9563
+ const status = currentAgent.agent_status?.trim().toLowerCase();
9564
+ if (status !== void 0 && statuses.has(status)) {
9565
+ settle(currentAgent, paneId, currentAgent.agent_status ?? status);
9566
+ }
9567
+ }
9568
+ if (completed || remaining.size === 0) return;
9569
+ if (relistNeeded) continue;
9570
+ const streamed = await raceAbort(streamFinished);
9464
9571
  if (streamed === aborted) break;
9572
+ if (streamFailed) throw streamFailure;
9465
9573
  } finally {
9466
9574
  controller.signal.removeEventListener("abort", stopCycle);
9575
+ cycle.abort();
9467
9576
  }
9468
9577
  if (completed) return;
9469
9578
  if (relistNeeded) continue;
@@ -10055,6 +10164,41 @@ function resolveCreateBody(textOpt, fileOpt) {
10055
10164
  );
10056
10165
  return { text: text2, defaultTitle };
10057
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
+ }
10058
10202
  function registerMemory(program2) {
10059
10203
  const memory = program2.command("memory").description("Create, read, and search memories");
10060
10204
  memory.addHelpText(
@@ -10309,7 +10453,16 @@ ${plan.rows.map(
10309
10453
  false
10310
10454
  );
10311
10455
  });
10312
- 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) => {
10313
10466
  const cfg = resolveConfig(cmd.optsWithGlobals());
10314
10467
  const data = await runApi("Fetching memory", async () => {
10315
10468
  const client = await makeClient(cfg);
@@ -10317,6 +10470,11 @@ ${plan.rows.map(
10317
10470
  params: { path: { memoryId } }
10318
10471
  });
10319
10472
  });
10473
+ if (opts.withRelationships) {
10474
+ const relationships = await fetchAllMemoryRelationships(cfg, memoryId);
10475
+ emit({ ...data, relationships }, cmd.optsWithGlobals().json);
10476
+ return;
10477
+ }
10320
10478
  emit(data, cmd.optsWithGlobals().json);
10321
10479
  });
10322
10480
  memory.command("search <query>").description(
@@ -10664,8 +10822,18 @@ ${plan.rows.map(
10664
10822
  }
10665
10823
 
10666
10824
  // src/setup/apply.ts
10667
- import { createHash as createHash5 } from "crypto";
10668
- 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";
10669
10837
  import { dirname as dirname15 } from "path";
10670
10838
  var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
10671
10839
  var MARKER_END = "<!-- @sechroom/cli:end";
@@ -10711,11 +10879,17 @@ function parseManagedBlock(content, block) {
10711
10879
  const keyed = content.match(keyedBlockRe(block));
10712
10880
  if (keyed) {
10713
10881
  const attrs = parseAttrs(keyed[0].slice(0, keyed[0].indexOf("\n")));
10714
- 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
+ };
10715
10888
  }
10716
10889
  if (block === "role-template") {
10717
10890
  const legacy = content.match(legacyBlockRe());
10718
- 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]) };
10719
10893
  }
10720
10894
  return null;
10721
10895
  }
@@ -10737,33 +10911,446 @@ function mergeMcpJson(path, snippet, dryRun) {
10737
10911
  try {
10738
10912
  current = JSON.parse(readFileSync16(path, "utf8"));
10739
10913
  } catch {
10740
- 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
+ };
10741
10920
  }
10742
10921
  }
10743
- current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
10922
+ current.mcpServers = {
10923
+ ...current.mcpServers ?? {},
10924
+ ...incoming.mcpServers ?? {}
10925
+ };
10744
10926
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
10745
10927
  ensureDir2(path);
10746
10928
  writeFileSync15(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
10747
10929
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
10748
10930
  }
10749
- 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 = {}) {
10750
11303
  const existed = existsSync13(path);
10751
- let body = readOr(path, "");
10752
- body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
10753
- const trimmed = body.trim();
10754
- 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
+ }
10755
11326
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
10756
- ensureDir2(path);
10757
- 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
+ }
10758
11339
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
10759
11340
  }
10760
11341
  function writeInstructionBlock(path, write, dryRun) {
10761
11342
  const existed = existsSync13(path);
10762
11343
  const next = computeBlockFile(readOr(path, ""), write);
10763
- 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 };
10764
11346
  ensureDir2(path);
10765
11347
  writeFileSync15(path, next);
10766
- 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
+ };
10767
11354
  }
10768
11355
  function computeBlockFile(current, write) {
10769
11356
  const rendered = renderBlock(write);
@@ -10812,7 +11399,13 @@ function applyBlock(path, write, mode, dryRun) {
10812
11399
  };
10813
11400
  }
10814
11401
  if (state === "current") {
10815
- 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
+ };
10816
11409
  }
10817
11410
  if (state === "drift" && mode !== "force") {
10818
11411
  const proposedPath = `${path}.proposed`;
@@ -10847,7 +11440,12 @@ async function applyClient(cfg, setup, target, opts) {
10847
11440
  const section = findSection(surface, target.mcp.sectionType);
10848
11441
  const snippet = sectionSnippet(section);
10849
11442
  if (!snippet) {
10850
- 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
+ });
10851
11449
  } else {
10852
11450
  actions.push(
10853
11451
  target.mcp.format === "toml" ? mergeCodexToml(target.mcp.path, snippet, dryRun) : mergeMcpJson(target.mcp.path, snippet, dryRun)
@@ -10858,25 +11456,44 @@ async function applyClient(cfg, setup, target, opts) {
10858
11456
  const surface = findSurface(setup, target.instruction.surfaceKey);
10859
11457
  const section = findSection(surface, SectionType.InstructionFile);
10860
11458
  if (!section) {
10861
- 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
+ });
10862
11465
  } else {
10863
11466
  const resolved = await withSpinner(
10864
11467
  `Resolving ${target.label} agent instructions`,
10865
11468
  () => resolveInstruction(cfg, section, opts.personalWorkspaceId)
10866
11469
  );
10867
11470
  if (!resolved) {
10868
- 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
+ });
10869
11477
  } else {
10870
11478
  const action = applyBlock(
10871
11479
  target.instruction.path,
10872
- { block: "role-template", body: resolved.body, source: resolved.sourceRef },
11480
+ {
11481
+ block: "role-template",
11482
+ body: resolved.body,
11483
+ source: resolved.sourceRef
11484
+ },
10873
11485
  mode,
10874
11486
  opts.dryRun
10875
11487
  );
10876
- 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
+ );
10877
11491
  }
10878
11492
  }
10879
- const conventionsSection = findSection(surface, SectionType.WorkspaceConventions);
11493
+ const conventionsSection = findSection(
11494
+ surface,
11495
+ SectionType.WorkspaceConventions
11496
+ );
10880
11497
  if (conventionsSection) {
10881
11498
  const conventions = await withSpinner(
10882
11499
  `Composing ${target.label} workspace conventions`,
@@ -10885,11 +11502,20 @@ async function applyClient(cfg, setup, target, opts) {
10885
11502
  if (conventions) {
10886
11503
  const action = applyBlock(
10887
11504
  target.instruction.path,
10888
- { 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
+ },
10889
11510
  mode,
10890
11511
  opts.dryRun
10891
11512
  );
10892
- 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
+ );
10893
11519
  }
10894
11520
  }
10895
11521
  }
@@ -11107,7 +11733,12 @@ function buildConventionDraft(title, rawKind, rawBody) {
11107
11733
 
11108
11734
  ${body}
11109
11735
  `,
11110
- 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
+ ]
11111
11742
  };
11112
11743
  }
11113
11744
  function copyChoice(opts) {
@@ -11120,9 +11751,16 @@ async function maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId,
11120
11751
  const instr = targets[key]?.instruction;
11121
11752
  if (!instr || seen.has(instr.surfaceKey)) continue;
11122
11753
  seen.add(instr.surfaceKey);
11123
- const section = findSection(findSurface(setup, instr.surfaceKey), SectionType.InstructionFile);
11754
+ const section = findSection(
11755
+ findSurface(setup, instr.surfaceKey),
11756
+ SectionType.InstructionFile
11757
+ );
11124
11758
  if (!section) continue;
11125
- const resolved = await resolveInstruction(cfg, section, personalWorkspaceId);
11759
+ const resolved = await resolveInstruction(
11760
+ cfg,
11761
+ section,
11762
+ personalWorkspaceId
11763
+ );
11126
11764
  if (!resolved || resolved.source === "override") continue;
11127
11765
  let make = choice === "yes";
11128
11766
  if (choice === "ask") {
@@ -11137,8 +11775,10 @@ version, the shared template stays clean, and you can discard back anytime.
11137
11775
  }
11138
11776
  if (make) {
11139
11777
  await createOverride(cfg, resolved, personalWorkspaceId);
11140
- process.stderr.write(`\u2713 personal copy created for ${instr.surfaceKey} \u2014 edit it on the Agent setup page or via the API.
11141
- `);
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
+ );
11142
11782
  }
11143
11783
  }
11144
11784
  }
@@ -11148,7 +11788,9 @@ function resolveClientKeys(raw) {
11148
11788
  if (tokens.includes("all")) return [...ALL_CLIENT_KEYS];
11149
11789
  for (const k of tokens) {
11150
11790
  if (!targets[k]) {
11151
- 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
+ );
11152
11794
  }
11153
11795
  }
11154
11796
  return [...new Set(tokens)];
@@ -11159,8 +11801,10 @@ ${client.label} (${client.key}):
11159
11801
  `);
11160
11802
  for (const a of actions) {
11161
11803
  const tag = a.status === "skipped" ? "skip" : a.status;
11162
- process.stdout.write(` [${tag}] ${a.kind}: ${a.path}${a.note ? ` \u2014 ${a.note}` : ""}
11163
- `);
11804
+ process.stdout.write(
11805
+ ` [${tag}] ${a.kind}: ${a.path}${a.note ? ` \u2014 ${a.note}` : ""}
11806
+ `
11807
+ );
11164
11808
  }
11165
11809
  }
11166
11810
  function resolveEvalMode(opts) {
@@ -11234,8 +11878,10 @@ function summarizeEval(result, mode, json, dryRun) {
11234
11878
  const { eval: counts } = buildCheckReport(result);
11235
11879
  if (json) return;
11236
11880
  if (!dryRun && counts.stale) {
11237
- process.stderr.write(`\u21BB refreshed ${counts.stale} section(s) the server had moved
11238
- `);
11881
+ process.stderr.write(
11882
+ `\u21BB refreshed ${counts.stale} section(s) the server had moved
11883
+ `
11884
+ );
11239
11885
  }
11240
11886
  if (!dryRun && counts.drift) {
11241
11887
  process.stderr.write(
@@ -11266,7 +11912,39 @@ async function resolveNamespaceChoice(cfg, flag) {
11266
11912
  return picked === GLOBAL_NAMESPACE ? null : picked;
11267
11913
  }
11268
11914
  function registerInit(program2) {
11269
- 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(
11270
11948
  "after",
11271
11949
  `
11272
11950
  Examples:
@@ -11281,7 +11959,9 @@ Examples:
11281
11959
  const mode = resolveEvalMode(opts);
11282
11960
  const check = mode === "check";
11283
11961
  if (check && opts.mcpOnly) {
11284
- 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
+ );
11285
11965
  }
11286
11966
  const namespaceSlug = await resolveNamespaceChoice(cfg, opts.namespace);
11287
11967
  const setup = await withSpinner(
@@ -11295,14 +11975,28 @@ Examples:
11295
11975
  } catch (err2) {
11296
11976
  return fail(err2.message);
11297
11977
  }
11298
- 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
+ });
11299
11983
  const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
11300
- 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
+ });
11301
11988
  const keys = resolveClientKeys(opts.client);
11302
11989
  const json = g.json;
11303
11990
  const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
11304
11991
  if (!opts.dryRun && !opts.mcpOnly && !check) {
11305
- 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
+ );
11306
12000
  }
11307
12001
  const result = [];
11308
12002
  for (const key of keys) {
@@ -11320,18 +12014,33 @@ Examples:
11320
12014
  summarizeEval(result, mode, Boolean(json), Boolean(opts.dryRun));
11321
12015
  if (!json && !opts.dryRun && !opts.mcpOnly && !check) {
11322
12016
  for (const t of claudeTargets) {
11323
- 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
+ });
11324
12023
  }
11325
12024
  }
11326
12025
  if (!json && !opts.dryRun && !opts.mcpOnly && !check) {
11327
- 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
+ });
11328
12034
  }
11329
12035
  if (json) {
11330
12036
  emit({ dryRun: Boolean(opts.dryRun), clients: result }, true);
11331
12037
  return;
11332
12038
  }
11333
12039
  const first = targets[keys[0]];
11334
- const surface = findSurface(setup, first.mcp?.surfaceKey ?? first.instruction?.surfaceKey ?? "");
12040
+ const surface = findSurface(
12041
+ setup,
12042
+ first.mcp?.surfaceKey ?? first.instruction?.surfaceKey ?? ""
12043
+ );
11335
12044
  const verify = findSection(surface, SectionType.Verify);
11336
12045
  if (verify?.description) {
11337
12046
  process.stdout.write(`
@@ -11345,13 +12054,64 @@ Next \u2014 verify: ${verify.description}
11345
12054
  }
11346
12055
  function registerSetup(program2, deps = {}) {
11347
12056
  const setup = program2.command("setup").description("Granular onboarding steps (init runs these together)");
11348
- 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) => {
11349
- await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: true, agentFiles: false, namespace: opts.namespace });
11350
- });
11351
- 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) => {
11352
- 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
+ });
11353
12101
  });
11354
- 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(
11355
12115
  "after",
11356
12116
  `
11357
12117
  The memo carries the two conventions a workspace-conventions section needs (FR-sechroom-236):
@@ -11367,10 +12127,15 @@ Examples:
11367
12127
  const cfg = resolveConfig(cmd.optsWithGlobals());
11368
12128
  const json = Boolean(cmd.optsWithGlobals().json);
11369
12129
  const title = titleParts.join(" ").trim();
11370
- 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
+ );
11371
12134
  const workspaceId = opts.workspace ?? cfg.workspaceId;
11372
12135
  if (!workspaceId)
11373
- 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
+ );
11374
12139
  const draft = buildConventionDraft(title, opts.kind, opts.body);
11375
12140
  if (opts.dryRun) {
11376
12141
  emit(
@@ -11411,7 +12176,10 @@ Examples:
11411
12176
  }
11412
12177
  if (opts.regen === false) {
11413
12178
  if (json) emit({ id: data.id, workspaceId, regen: false }, true);
11414
- 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
+ );
11415
12183
  return;
11416
12184
  }
11417
12185
  const regenerate = deps.regenerateConvention ?? runClients;
@@ -11440,7 +12208,14 @@ async function runClients(clients, cmd, opts) {
11440
12208
  );
11441
12209
  const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
11442
12210
  if (opts.agentFiles && !opts.dryRun && !check) {
11443
- 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
+ );
11444
12219
  }
11445
12220
  const json = g.json;
11446
12221
  const result = [];
@@ -11461,7 +12236,9 @@ async function runClients(clients, cmd, opts) {
11461
12236
  emit({ dryRun: opts.dryRun, clients: result }, true);
11462
12237
  return;
11463
12238
  }
11464
- 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
+ );
11465
12242
  }
11466
12243
 
11467
12244
  // src/commands/namespace.ts
@@ -11537,7 +12314,7 @@ import { basename as basename5, join as join21 } from "path";
11537
12314
 
11538
12315
  // src/commands/fanout.ts
11539
12316
  import { spawnSync } from "child_process";
11540
- 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";
11541
12318
  import { isAbsolute as isAbsolute3, join as join20, resolve as resolve7 } from "path";
11542
12319
  var ICON = {
11543
12320
  refresh: "\u21BB",
@@ -11560,7 +12337,7 @@ function discoverChildren(root) {
11560
12337
  if (name.startsWith(".") || name === "node_modules") continue;
11561
12338
  const dir = join20(root, name);
11562
12339
  try {
11563
- if (!statSync5(dir).isDirectory()) continue;
12340
+ if (!statSync6(dir).isDirectory()) continue;
11564
12341
  } catch {
11565
12342
  continue;
11566
12343
  }
@@ -11686,20 +12463,27 @@ function resolveBaseUrl(g) {
11686
12463
  return baseUrl.replace(/\/$/, "");
11687
12464
  }
11688
12465
  async function fetchWorkspaces(client) {
11689
- const { data, error } = await client.GET("/workspaces", { params: { query: { includeArchived: false } } });
11690
- 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)}`);
11691
12471
  const rows = data ?? [];
11692
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 }));
11693
12473
  }
11694
12474
  async function lookupWorkspace(client, id) {
11695
- 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
+ });
11696
12478
  if (error) return null;
11697
12479
  const env = data;
11698
12480
  const w = env?.item ?? env;
11699
12481
  return w?.id ? { id: w.id, name: w.name ?? id, parentId: w.parentId ?? null } : null;
11700
12482
  }
11701
12483
  async function warnIfProjectStray(client, projectId, workspaceId, json) {
11702
- 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
+ });
11703
12487
  if (error) return;
11704
12488
  const env = data;
11705
12489
  const owner = env?.item?.workspaceId;
@@ -11743,10 +12527,15 @@ function personalSubtreeIds(personalId, all) {
11743
12527
  async function pickWorkspace(client, opts = {}) {
11744
12528
  const promptLabel = opts.promptLabel ?? "Bind this directory to a workspace:";
11745
12529
  const dirName = opts.dirName ?? basename5(process.cwd());
11746
- const all = await withSpinner("Listing your workspaces", () => fetchWorkspaces(client));
12530
+ const all = await withSpinner(
12531
+ "Listing your workspaces",
12532
+ () => fetchWorkspaces(client)
12533
+ );
11747
12534
  if (all.length === 0) {
11748
- process.stderr.write(`no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
11749
- `);
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
+ );
11750
12539
  return void 0;
11751
12540
  }
11752
12541
  const byId = new Map(all.map((w) => [w.id, w]));
@@ -11761,19 +12550,35 @@ async function pickWorkspace(client, opts = {}) {
11761
12550
  const matched = candidates.filter(isMatch).sort(byPath);
11762
12551
  const rest = candidates.filter((w) => !isMatch(w)).sort(byPath);
11763
12552
  const choices = [
11764
- ...matched.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: style.dim(`matches "${dirName}"`) })),
11765
- ...rest.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: w.id })),
11766
- { 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
+ }
11767
12568
  ];
11768
12569
  const defaultValue = matched.length === 1 ? matched[0].id : SKIP;
11769
12570
  const chosen = candidates.length > 12 ? await promptAutocomplete(promptLabel, choices, defaultValue) : await promptSelect(promptLabel, choices, defaultValue);
11770
12571
  if (chosen === SKIP) return void 0;
11771
12572
  const picked = byId.get(chosen);
11772
- 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
+ );
11773
12576
  if (collisions.length > 0) {
11774
12577
  process.stderr.write(
11775
12578
  `${warn("\u26A0")} ${collisions.length} other workspace(s) have a similar name to ${style.cyan(workspacePath(picked, byId))}:
11776
- ` + 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") + `
11777
12582
  You picked ${style.dim(picked.id)} \u2014 re-run \`sechroom config set --local workspaceId <id>\` if that's wrong.
11778
12583
  `
11779
12584
  );
@@ -11800,10 +12605,17 @@ async function ensureTenant(baseUrl, g, opts) {
11800
12605
  const local = readLocalConfig();
11801
12606
  let tenant = g.tenant ?? process.env.SECHROOM_TENANT ?? local.tenant ?? persisted.tenant ?? "";
11802
12607
  if (!tenant) {
11803
- 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
+ });
11804
12614
  const { data, error } = await client.GET("/auth/me/tenants", {});
11805
12615
  if (error) {
11806
- 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
+ );
11807
12619
  }
11808
12620
  const tenants = data?.tenants ?? [];
11809
12621
  if (tenants.length === 0) {
@@ -11835,21 +12647,44 @@ async function ensureTenant(baseUrl, g, opts) {
11835
12647
  }
11836
12648
  }
11837
12649
  const existingWorkspace = local.workspaceId ?? persisted.workspaceId ?? void 0;
11838
- const wsClient = await makeClient({ baseUrl, tenant, account: resolveAccountAlias(), clientId: persisted.clientId });
11839
- const workspaceId = await resolveWorkspaceBinding(wsClient, existingWorkspace, {
11840
- yes: opts.yes,
11841
- json: opts.json,
11842
- workspace: opts.workspace
12650
+ const wsClient = await makeClient({
12651
+ baseUrl,
12652
+ tenant,
12653
+ account: resolveAccountAlias(),
12654
+ clientId: persisted.clientId
11843
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
+ );
11844
12665
  const defaultProjectId = local.defaultProjectId ?? persisted.defaultProjectId ?? void 0;
11845
- 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
+ );
11846
12673
  let storeLocal = Boolean(opts.local);
11847
12674
  if (!opts.local && canPrompt() && !opts.yes) {
11848
12675
  storeLocal = await promptSelect(
11849
12676
  "Where should this tenant + base URL be saved?",
11850
12677
  [
11851
- { label: "Globally", value: "global", hint: "all projects on this machine" },
11852
- { 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
+ }
11853
12688
  ],
11854
12689
  local.path ? "local" : "global"
11855
12690
  ) === "local";
@@ -11858,19 +12693,34 @@ async function ensureTenant(baseUrl, g, opts) {
11858
12693
  const patch = { baseUrl, tenant, ...workspaceId ? { workspaceId } : {} };
11859
12694
  if (storeLocal) {
11860
12695
  const path = writeLocalConfig(patch, { here: Boolean(opts.here) });
11861
- if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved to ${path} (directory-local)
11862
- `);
12696
+ if (!opts.json)
12697
+ process.stderr.write(
12698
+ `${ok("\u2713")} config saved to ${path} (directory-local)
12699
+ `
12700
+ );
11863
12701
  } else {
11864
12702
  writePersisted(patch);
11865
- if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved globally (~/.config/sechroom/config.json)
11866
- `);
12703
+ if (!opts.json)
12704
+ process.stderr.write(
12705
+ `${ok("\u2713")} config saved globally (~/.config/sechroom/config.json)
12706
+ `
12707
+ );
11867
12708
  }
11868
12709
  if (workspaceId && !existingWorkspace && !opts.json) {
11869
- process.stderr.write(`${ok("\u2713")} bound to workspace ${style.dim(workspaceId)}
11870
- `);
12710
+ process.stderr.write(
12711
+ `${ok("\u2713")} bound to workspace ${style.dim(workspaceId)}
12712
+ `
12713
+ );
11871
12714
  }
11872
12715
  }
11873
- 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
+ };
11874
12724
  }
11875
12725
  async function ensureAuth(cfg, yes) {
11876
12726
  if (process.env.SECHROOM_TOKEN) return;
@@ -11878,17 +12728,27 @@ async function ensureAuth(cfg, yes) {
11878
12728
  const usable = Boolean(cached?.accessToken) && (cached.expiresAt === void 0 || Date.now() < cached.expiresAt - 6e4 || Boolean(cached.refreshToken));
11879
12729
  if (usable) return;
11880
12730
  if (!canPrompt() || yes) {
11881
- 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
+ );
11882
12734
  }
11883
- 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
+ );
11884
12738
  await login(cfg);
11885
12739
  }
11886
12740
  async function ensureTimezone(cfg, opts) {
11887
12741
  const client = await makeClient(cfg);
11888
12742
  const { data, error } = await client.GET("/me/profile", {});
11889
- 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
+ };
11890
12749
  const current = data?.effectiveTimezone;
11891
- 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" };
11892
12752
  const system = systemTimezone();
11893
12753
  let tz = system;
11894
12754
  if (canPrompt() && !opts.yes) {
@@ -11900,12 +12760,18 @@ async function ensureTimezone(cfg, opts) {
11900
12760
  note: "no timezone set \u2014 re-run interactively or pass --yes to adopt the system timezone"
11901
12761
  };
11902
12762
  }
11903
- if (!tz) return { timezone: null, action: "skipped", note: "no timezone provided" };
12763
+ if (!tz)
12764
+ return { timezone: null, action: "skipped", note: "no timezone provided" };
11904
12765
  if (opts.dryRun) return { timezone: tz, action: "dry-run" };
11905
12766
  const { error: putErr } = await client.PUT("/me/profile", {
11906
12767
  body: { displayName: null, photoUrl: null, bio: null, timezone: tz }
11907
12768
  });
11908
- 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
+ };
11909
12775
  return { timezone: tz, action: "set" };
11910
12776
  }
11911
12777
  async function chooseClients(clientFlag, yes, cwd) {
@@ -11930,7 +12796,11 @@ async function chooseScope(scopeFlag, yes) {
11930
12796
  return promptSelect(
11931
12797
  "Install skills, agents, and hooks globally or just for this project?",
11932
12798
  [
11933
- { 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
+ },
11934
12804
  { label: "This project", value: "project", hint: "<repo>/.claude" }
11935
12805
  ],
11936
12806
  "global"
@@ -11939,7 +12809,13 @@ async function chooseScope(scopeFlag, yes) {
11939
12809
  async function planRecurseChild(entry, root, client, opts) {
11940
12810
  const dir = resolveChildDir(entry.path, root);
11941
12811
  if (!existsSync15(dir)) {
11942
- 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
+ };
11943
12819
  }
11944
12820
  if (existsSync15(join21(dir, ".sechroom.json"))) {
11945
12821
  return {
@@ -11960,20 +12836,40 @@ async function planRecurseChild(entry, root, client, opts) {
11960
12836
  };
11961
12837
  }
11962
12838
  if (opts.dryRun) {
11963
- 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
+ };
11964
12846
  }
11965
12847
  if (opts.yes || !canPrompt()) {
11966
- 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
+ };
11967
12855
  }
11968
- process.stderr.write(`
12856
+ process.stderr.write(
12857
+ `
11969
12858
  ${style.bold(entry.path)} ${style.dim("is not bound yet.")}
11970
- `);
12859
+ `
12860
+ );
11971
12861
  const ws = await pickWorkspace(client, {
11972
12862
  promptLabel: `Bind ${style.cyan(entry.path)} to a workspace:`,
11973
12863
  dirName: basename5(entry.path)
11974
12864
  });
11975
12865
  if (!ws) {
11976
- 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
+ };
11977
12873
  }
11978
12874
  return {
11979
12875
  label: entry.path,
@@ -11988,20 +12884,34 @@ async function resolveFanoutLane(cfg, opts) {
11988
12884
  let design = opts.designLane ?? process.env.SECHROOM_DESIGN_LANE;
11989
12885
  if (!code || !design) {
11990
12886
  const clients = detectInstalledClients(process.cwd());
11991
- const inferred = await inferLanes(cfg, clients.length ? clients : void 0);
12887
+ const inferred = await inferLanes(
12888
+ cfg,
12889
+ clients.length ? clients : void 0
12890
+ );
11992
12891
  code = code ?? inferred.code;
11993
12892
  design = design ?? inferred.design;
11994
12893
  }
11995
12894
  if (!opts.lane && !opts.yes && !opts.dryRun && canPrompt() && (code || design)) {
11996
- process.stderr.write(`
12895
+ process.stderr.write(
12896
+ `
11997
12897
  This fan-out will pin the same lane in every repo:
11998
- `);
11999
- if (code) process.stderr.write(` ${style.dim("code-lane")} = ${style.cyan(code)}
12000
- `);
12001
- if (design) process.stderr.write(` ${style.dim("design-lane")} = ${style.cyan(design)}
12002
- `);
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
+ );
12003
12910
  if (!await promptYesNo("Use this lane for all repos?")) {
12004
- 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;
12005
12915
  design = await promptText("Design-lane id (blank = skip)?", design ?? "") || void 0;
12006
12916
  }
12007
12917
  }
@@ -12019,30 +12929,112 @@ async function runRecurse(cfg, g, opts) {
12019
12929
  const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
12020
12930
  const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
12021
12931
  if (entries.length === 0) {
12022
- if (json) process.stdout.write(JSON.stringify({ recurse: true, root, repos: [] }) + "\n");
12023
- else process.stderr.write(`${warn("\u26A0")} no child repos found ${fromManifest ? `in ${manifestPath}` : `under ${root}`} \u2014 nothing to do.
12024
- `);
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
+ );
12025
12941
  return;
12026
12942
  }
12027
12943
  if (!json) {
12028
- process.stderr.write(`${style.bold("onboard --recurse")} ${style.dim(`(${entries.length} repo${entries.length === 1 ? "" : "s"} from ${sourceLabel})`)}
12029
- `);
12944
+ process.stderr.write(
12945
+ `${style.bold("onboard --recurse")} ${style.dim(`(${entries.length} repo${entries.length === 1 ? "" : "s"} from ${sourceLabel})`)}
12946
+ `
12947
+ );
12030
12948
  }
12031
- const lane = await resolveFanoutLane(cfg, { lane: opts.lane, designLane: opts.designLane, yes, dryRun });
12032
- if (!json && lane.code) process.stderr.write(`${ok("\u2713")} lane ${style.cyan(lane.code)}${lane.design ? ` ${style.dim(`/ ${lane.design}`)}` : ""} for every repo
12033
- `);
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
+ );
12034
12960
  const client = await makeClient(cfg);
12035
12961
  const plans = [];
12036
- for (const entry of entries) plans.push(await planRecurseChild(entry, root, client, { yes, dryRun }));
12037
- 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
+ });
12038
12969
  if (json) {
12039
- 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
+ );
12040
12973
  return;
12041
12974
  }
12042
12975
  summarizeFanout(results, { dryRun });
12043
12976
  }
12044
12977
  function registerOnboard(program2) {
12045
- 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(
12046
13038
  "after",
12047
13039
  `
12048
13040
  Examples:
@@ -12065,20 +13057,56 @@ Examples:
12065
13057
  const check = mode === "check";
12066
13058
  const yes = Boolean(opts.yes) || check;
12067
13059
  if (check && (opts.recurse || opts.cliOnly)) {
12068
- 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
+ );
12069
13063
  }
12070
13064
  if (opts.lane) process.env.SECHROOM_CODE_LANE = opts.lane;
12071
13065
  if (opts.designLane) process.env.SECHROOM_DESIGN_LANE = opts.designLane;
12072
13066
  if (opts.recurse) {
12073
13067
  const baseUrl2 = resolveBaseUrl(g);
12074
- await ensureAuth({ baseUrl: baseUrl2, tenant: "", account: resolveAccountAlias(), clientId: readPersisted().clientId }, yes);
12075
- const cfg2 = await ensureTenant(baseUrl2, g, { yes: true, json, persist: false });
12076
- 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
+ });
12077
13089
  return;
12078
13090
  }
12079
13091
  const baseUrl = resolveBaseUrl(g);
12080
- await ensureAuth({ baseUrl, tenant: "", account: resolveAccountAlias(), clientId: readPersisted().clientId }, yes);
12081
- 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
+ });
12082
13110
  const tz = await ensureTimezone(cfg, { yes, dryRun: dryRun || check });
12083
13111
  if (!json && tz.action !== "already-set") {
12084
13112
  const line = tz.action === "set" ? `${ok("\u2713")} timezone set to ${tz.timezone}
@@ -12088,21 +13116,48 @@ Examples:
12088
13116
  process.stderr.write(line);
12089
13117
  }
12090
13118
  const wire = await chooseWire(opts, yes);
12091
- const scope = await chooseScope(opts.scope, yes);
12092
- 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
+ });
12093
13124
  const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
12094
13125
  if (scope === "project" && g.claudeConfigDir && !json) {
12095
- process.stderr.write(`${style.dim("(--claude-config-dir is ignored for --scope project \u2014 project files are repo-relative)")}
12096
- `);
13126
+ process.stderr.write(
13127
+ `${style.dim("(--claude-config-dir is ignored for --scope project \u2014 project files are repo-relative)")}
13128
+ `
13129
+ );
12097
13130
  }
12098
13131
  if (wire === "cli-only") {
12099
13132
  if (json) {
12100
- 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
+ );
12101
13145
  return;
12102
13146
  }
12103
13147
  if (!dryRun) {
12104
- await ensureLanePin(cfg, { yes, dryRun, clients: detectInstalledClients(process.cwd()) });
12105
- 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
+ });
12106
13161
  }
12107
13162
  process.stdout.write(
12108
13163
  `
@@ -12113,12 +13168,36 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
12113
13168
  await printStarterPrompt("cli");
12114
13169
  return;
12115
13170
  }
12116
- const keys = await chooseClients(opts.client, yes, process.cwd());
12117
- const setup = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
12118
- 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
+ });
12119
13191
  const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
12120
13192
  if (!dryRun && !check) {
12121
- 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
+ );
12122
13201
  }
12123
13202
  const writeMcp = wire === "full";
12124
13203
  const result = [];
@@ -12135,16 +13214,11 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
12135
13214
  if (!json && !check) printActions(target, actions);
12136
13215
  }
12137
13216
  if (check) {
12138
- reportCheckAndExit(
12139
- result,
12140
- json,
12141
- "sechroom onboard --refresh",
12142
- {
12143
- baseUrl: cfg.baseUrl,
12144
- tenant: cfg.tenant,
12145
- workspaceId: cfg.workspaceId ?? null
12146
- }
12147
- );
13217
+ reportCheckAndExit(result, json, "sechroom onboard --refresh", {
13218
+ baseUrl: cfg.baseUrl,
13219
+ tenant: cfg.tenant,
13220
+ workspaceId: cfg.workspaceId ?? null
13221
+ });
12148
13222
  }
12149
13223
  const evalCounts = buildCheckReport(result).eval;
12150
13224
  if (!json && !dryRun) {
@@ -12152,19 +13226,45 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
12152
13226
  }
12153
13227
  if (!json && !dryRun) {
12154
13228
  for (const t of claudeTargets) {
12155
- 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
+ });
12156
13235
  }
12157
13236
  }
12158
13237
  if (!json && !dryRun) {
12159
- 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
+ });
12160
13246
  }
12161
13247
  if (json) {
12162
- 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
+ );
12163
13261
  return;
12164
13262
  }
12165
13263
  if (!dryRun && evalCounts.stale) {
12166
- process.stderr.write(`${style.cyan("\u21BB")} refreshed ${evalCounts.stale} section(s) the server had moved
12167
- `);
13264
+ process.stderr.write(
13265
+ `${style.cyan("\u21BB")} refreshed ${evalCounts.stale} section(s) the server had moved
13266
+ `
13267
+ );
12168
13268
  }
12169
13269
  if (!dryRun && evalCounts.drift) {
12170
13270
  process.stderr.write(
@@ -12173,7 +13273,9 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
12173
13273
  `
12174
13274
  );
12175
13275
  }
12176
- 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
+ );
12177
13279
  process.stdout.write(
12178
13280
  dryRun ? "\n(dry run \u2014 nothing written)\n" : !wroteSomething ? `
12179
13281
  ${style.bold("Done.")} Everything's already up to date.
@@ -12192,9 +13294,21 @@ async function chooseWire(opts, yes) {
12192
13294
  return promptSelect(
12193
13295
  "How should I set up Sechroom in this project?",
12194
13296
  [
12195
- { label: "Wire my AI client", value: "full", hint: "MCP server (.mcp.json) + agent instructions" },
12196
- { label: "Agent instructions only", value: "agent-only", hint: "skip MCP config" },
12197
- { 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
+ }
12198
13312
  ],
12199
13313
  "full"
12200
13314
  );
@@ -12216,7 +13330,9 @@ ${rule}
12216
13330
  }
12217
13331
  async function printStarterPrompt(mode, cfg) {
12218
13332
  if (mode === "cli") {
12219
- 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
+ ]);
12220
13336
  return;
12221
13337
  }
12222
13338
  let primary = FALLBACK_AGENT_PROMPT;
@@ -12228,7 +13344,9 @@ async function printStarterPrompt(mode, cfg) {
12228
13344
  } catch {
12229
13345
  }
12230
13346
  }
12231
- 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
+ ]);
12232
13350
  }
12233
13351
 
12234
13352
  // src/commands/project.ts
@@ -12543,7 +13661,7 @@ Examples:
12543
13661
  // src/commands/reset.ts
12544
13662
  import { homedir as homedir6 } from "os";
12545
13663
  import { join as join22 } from "path";
12546
- 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";
12547
13665
  var SKILLS_LOCK2 = ".sechroom-skills.json";
12548
13666
  var localSkillsDir = () => join22(process.cwd(), ".claude", "skills");
12549
13667
  var globalSkillsDir = () => join22(homedir6(), ".claude", "skills");
@@ -12559,14 +13677,14 @@ function removeMaterialisedSkills(dir) {
12559
13677
  for (const name of entry.skills ?? []) {
12560
13678
  const p = join22(dir, name);
12561
13679
  if (existsSync16(p)) {
12562
- rmSync7(p, { recursive: true, force: true });
13680
+ rmSync8(p, { recursive: true, force: true });
12563
13681
  removed.push(p);
12564
13682
  }
12565
13683
  }
12566
13684
  }
12567
13685
  } catch {
12568
13686
  }
12569
- rmSync7(lockPath, { force: true });
13687
+ rmSync8(lockPath, { force: true });
12570
13688
  removed.push(lockPath);
12571
13689
  return removed;
12572
13690
  }
@@ -12605,17 +13723,17 @@ function registerReset(program2) {
12605
13723
  const removed = [];
12606
13724
  const stateDir = join22(process.cwd(), ".sechroom");
12607
13725
  if (existsSync16(stateDir)) {
12608
- rmSync7(stateDir, { recursive: true, force: true });
13726
+ rmSync8(stateDir, { recursive: true, force: true });
12609
13727
  removed.push(stateDir);
12610
13728
  }
12611
13729
  const legacyCfg = join22(process.cwd(), ".sechroom.json");
12612
13730
  if (existsSync16(legacyCfg)) {
12613
- rmSync7(legacyCfg, { force: true });
13731
+ rmSync8(legacyCfg, { force: true });
12614
13732
  removed.push(legacyCfg);
12615
13733
  }
12616
13734
  const legacySem = join22(process.cwd(), ".sem");
12617
13735
  if (existsSync16(legacySem)) {
12618
- rmSync7(legacySem, { force: true });
13736
+ rmSync8(legacySem, { force: true });
12619
13737
  removed.push(legacySem);
12620
13738
  }
12621
13739
  removed.push(...removeMaterialisedSkills(localSkillsDir()));
@@ -12640,7 +13758,7 @@ function registerReset(program2) {
12640
13758
  }
12641
13759
 
12642
13760
  // src/commands/skills.ts
12643
- 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";
12644
13762
  import { join as join23 } from "path";
12645
13763
  function filenameFromDisposition(header) {
12646
13764
  if (!header) return void 0;
@@ -12650,7 +13768,7 @@ function filenameFromDisposition(header) {
12650
13768
  function resolveOutputPath(output, serverFilename) {
12651
13769
  const filename = serverFilename || "skills.zip";
12652
13770
  if (!output) return join23(process.cwd(), filename);
12653
- const looksLikeDir = output.endsWith("/") || existsSync17(output) && statSync6(output).isDirectory();
13771
+ const looksLikeDir = output.endsWith("/") || existsSync17(output) && statSync7(output).isDirectory();
12654
13772
  if (looksLikeDir) {
12655
13773
  mkdirSync18(output, { recursive: true });
12656
13774
  return join23(output, filename);