@sechroom/cli 2026.8.3-rc.fc3795ae7 → 2026.8.3

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 +1667 -320
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -642,6 +642,17 @@ function emitAction(summary, data, json) {
642
642
  process.stdout.write(`${ok("\u2713")} ${summary}
643
643
  `);
644
644
  }
645
+ var JSON_NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
646
+ function confidenceWireValue(value) {
647
+ if (value === null || value === void 0 || value === "") return null;
648
+ if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
649
+ const raw = String(value).trim();
650
+ if (JSON_NUMBER.test(raw)) {
651
+ const numeric = Number(raw);
652
+ if (Number.isFinite(numeric)) return numeric;
653
+ }
654
+ return String(value);
655
+ }
645
656
  var GOVERNANCE_QUEUED_PROBLEM_TYPE = "https://sechroom.dev/problems/governance-review-queued";
646
657
  function isGovernanceQueued(body) {
647
658
  return typeof body === "object" && body !== null && "type" in body && body.type === GOVERNANCE_QUEUED_PROBLEM_TYPE;
@@ -688,23 +699,57 @@ function formatFailureMessage(error) {
688
699
  if (error.cause instanceof Error && error.cause.message) {
689
700
  msg += `: ${error.cause.message}`;
690
701
  }
691
- } else if (typeof error === "object" && error !== null && "title" in error) {
702
+ } else if (isRecord(error)) {
692
703
  const problem = error;
693
- msg = String(problem.title);
694
- if (problem.errors && typeof problem.errors === "object") {
695
- const detail = Object.entries(problem.errors).flatMap(
696
- ([field, msgs]) => (Array.isArray(msgs) ? msgs : [msgs]).map((m) => ` ${field}: ${String(m)}`)
697
- );
698
- if (detail.length > 0) msg += `
699
- ${detail.join("\n")}`;
704
+ const title = typeof problem.title === "string" && problem.title.length > 0 ? problem.title : void 0;
705
+ const problemDetail = typeof problem.detail === "string" && problem.detail.length > 0 ? problem.detail : void 0;
706
+ const fieldErrors = formatProblemErrors(problem.errors);
707
+ const structuredErrors = fieldErrors.length === 0 ? formatStructuredViolations(problem.violations) : [];
708
+ const parts = [
709
+ ...title ? [title] : [],
710
+ ...problemDetail ? [problemDetail] : [],
711
+ ...fieldErrors,
712
+ ...structuredErrors
713
+ ];
714
+ if (parts.length > 0) {
715
+ if (title && !problemDetail && fieldErrors.length === 0 && structuredErrors.length === 0) {
716
+ parts.push("No additional error detail was returned by the API.");
717
+ }
718
+ msg = parts.join("\n");
719
+ } else {
720
+ const serialized = JSON.stringify(error);
721
+ msg = serialized && serialized !== "{}" ? serialized : "The API returned an empty error response.";
700
722
  }
701
- } else if (typeof error === "object" && error !== null) {
702
- msg = JSON.stringify(error);
703
723
  } else {
704
724
  msg = String(error);
705
725
  }
706
726
  return msg;
707
727
  }
728
+ function isRecord(value) {
729
+ return typeof value === "object" && value !== null && !Array.isArray(value);
730
+ }
731
+ function formatProblemErrors(errors) {
732
+ if (!isRecord(errors)) return [];
733
+ return Object.entries(errors).flatMap(
734
+ ([field, messages]) => (Array.isArray(messages) ? messages : [messages]).map(
735
+ (message) => ` ${field}: ${formatErrorValue(message)}`
736
+ )
737
+ );
738
+ }
739
+ function formatStructuredViolations(violations) {
740
+ if (!Array.isArray(violations)) return [];
741
+ return violations.flatMap((violation) => {
742
+ if (!isRecord(violation)) return [formatErrorValue(violation)];
743
+ const field = typeof violation.field === "string" ? violation.field : "validation";
744
+ const message = typeof violation.message === "string" ? violation.message : formatErrorValue(violation);
745
+ return [` ${field}: ${message}`];
746
+ });
747
+ }
748
+ function formatErrorValue(value) {
749
+ if (typeof value === "string") return value;
750
+ const serialized = JSON.stringify(value);
751
+ return serialized ?? String(value);
752
+ }
708
753
  function fail(error) {
709
754
  const msg = formatFailureMessage(error);
710
755
  process.stderr.write(`error: ${msg}
@@ -4366,9 +4411,19 @@ import { dirname as dirname5, join as join7 } from "path";
4366
4411
  function claudeDesktopConfigPath(home) {
4367
4412
  switch (process.platform) {
4368
4413
  case "darwin":
4369
- return join7(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
4414
+ return join7(
4415
+ home,
4416
+ "Library",
4417
+ "Application Support",
4418
+ "Claude",
4419
+ "claude_desktop_config.json"
4420
+ );
4370
4421
  case "win32":
4371
- return join7(process.env.APPDATA ?? join7(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
4422
+ return join7(
4423
+ process.env.APPDATA ?? join7(home, "AppData", "Roaming"),
4424
+ "Claude",
4425
+ "claude_desktop_config.json"
4426
+ );
4372
4427
  default:
4373
4428
  return join7(home, ".config", "Claude", "claude_desktop_config.json");
4374
4429
  }
@@ -4376,30 +4431,53 @@ function claudeDesktopConfigPath(home) {
4376
4431
  function clientTargets(cwd, opts = {}) {
4377
4432
  const home = homedir3();
4378
4433
  const claudeDir = opts.claudeDir ?? join7(home, ".claude");
4379
- const codexHome = opts.codexHome ?? join7(home, ".codex");
4434
+ const codexHome = opts.codexHome === void 0 ? join7(home, ".codex") : opts.codexHome;
4380
4435
  return {
4381
4436
  "claude-code": {
4382
4437
  key: "claude-code",
4383
4438
  label: "Claude Code",
4384
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join7(cwd, ".mcp.json"), format: "json" },
4439
+ mcp: {
4440
+ surfaceKey: "claude-code",
4441
+ sectionType: SectionType.McpConfig,
4442
+ path: join7(cwd, ".mcp.json"),
4443
+ format: "json"
4444
+ },
4385
4445
  instruction: { surfaceKey: "claude-code", path: join7(cwd, "CLAUDE.md") }
4386
4446
  },
4387
4447
  "claude-desktop": {
4388
4448
  key: "claude-desktop",
4389
4449
  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") }
4450
+ mcp: {
4451
+ surfaceKey: "claude-desktop",
4452
+ sectionType: SectionType.McpConfig,
4453
+ path: claudeDesktopConfigPath(home),
4454
+ format: "json"
4455
+ },
4456
+ instruction: {
4457
+ surfaceKey: "claude-desktop",
4458
+ path: join7(claudeDir, "CLAUDE.md")
4459
+ }
4392
4460
  },
4393
4461
  codex: {
4394
4462
  key: "codex",
4395
4463
  label: "Codex CLI",
4396
- mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join7(codexHome, "config.toml"), format: "toml" },
4464
+ mcp: codexHome ? {
4465
+ surfaceKey: "chatgpt",
4466
+ sectionType: SectionType.McpConfigToml,
4467
+ path: join7(codexHome, "config.toml"),
4468
+ format: "toml"
4469
+ } : null,
4397
4470
  instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
4398
4471
  },
4399
4472
  cursor: {
4400
4473
  key: "cursor",
4401
4474
  label: "Cursor",
4402
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join7(cwd, ".cursor", "mcp.json"), format: "json" },
4475
+ mcp: {
4476
+ surfaceKey: "claude-code",
4477
+ sectionType: SectionType.McpConfig,
4478
+ path: join7(cwd, ".cursor", "mcp.json"),
4479
+ format: "json"
4480
+ },
4403
4481
  instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
4404
4482
  },
4405
4483
  antigravity: {
@@ -4411,20 +4489,34 @@ function clientTargets(cwd, opts = {}) {
4411
4489
  // `type` — comes from the `antigravity` server surface, so we don't
4412
4490
  // hardcode it here. Instructions go in the project `AGENTS.md`
4413
4491
  // (cross-tool, shared with Codex/Cursor).
4414
- mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path: join7(home, ".gemini", "config", "mcp_config.json"), format: "json" },
4492
+ mcp: {
4493
+ surfaceKey: "antigravity",
4494
+ sectionType: SectionType.McpConfig,
4495
+ path: join7(home, ".gemini", "config", "mcp_config.json"),
4496
+ format: "json"
4497
+ },
4415
4498
  instruction: { surfaceKey: "antigravity", path: join7(cwd, "AGENTS.md") }
4416
4499
  }
4417
4500
  };
4418
4501
  }
4419
- var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor", "antigravity"];
4502
+ var ALL_CLIENT_KEYS = [
4503
+ "claude-code",
4504
+ "claude-desktop",
4505
+ "codex",
4506
+ "cursor",
4507
+ "antigravity"
4508
+ ];
4420
4509
  var DEFAULT_CLIENT_KEY = "claude-code";
4421
4510
  function detectInstalledClients(cwd) {
4422
4511
  const home = homedir3();
4423
4512
  const detected = [];
4424
- if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir))) detected.push("claude-code");
4425
- if (existsSync5(dirname5(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
4513
+ if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir)))
4514
+ detected.push("claude-code");
4515
+ if (existsSync5(dirname5(claudeDesktopConfigPath(home))))
4516
+ detected.push("claude-desktop");
4426
4517
  if (resolveCodexHomes({}).some((d) => existsSync5(d))) detected.push("codex");
4427
- if (existsSync5(join7(home, ".cursor")) || existsSync5(join7(cwd, ".cursor"))) detected.push("cursor");
4518
+ if (existsSync5(join7(home, ".cursor")) || existsSync5(join7(cwd, ".cursor")))
4519
+ detected.push("cursor");
4428
4520
  if (existsSync5(join7(home, ".gemini"))) detected.push("antigravity");
4429
4521
  return detected;
4430
4522
  }
@@ -5751,6 +5843,7 @@ Call sechroom_lifecycle_signal at each phase boundary (start/work/verify/closeou
5751
5843
  }
5752
5844
 
5753
5845
  // src/commands/executor.ts
5846
+ var DEFAULT_CLAIM_POLICY = "restricted";
5754
5847
  function executorSubscriptionInput(name) {
5755
5848
  return {
5756
5849
  name,
@@ -5812,8 +5905,8 @@ function registerExecutor(program2) {
5812
5905
  "Capability operation keys claimed by this instance"
5813
5906
  ).option(
5814
5907
  "--claim-policy <policy>",
5815
- "open | restricted (restricted only claims preferred/own-lane/allow-listed-tag work)",
5816
- "open"
5908
+ "open | restricted (restricted only claims preferred/own-lane/allow-listed-tag work; default restricted)",
5909
+ DEFAULT_CLAIM_POLICY
5817
5910
  ).option(
5818
5911
  "--claim-tag <tag...>",
5819
5912
  "Task tag this instance accepts under --claim-policy restricted"
@@ -5828,7 +5921,12 @@ function registerExecutor(program2) {
5828
5921
  "--subscription-name <name>",
5829
5922
  "SignalR delivery binding name",
5830
5923
  "executor-dispatch"
5831
- ).option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 600).option(
5924
+ ).option(
5925
+ "--ttl <seconds>",
5926
+ "Advertisement TTL (30 to tenant maximum)",
5927
+ parseInteger,
5928
+ 600
5929
+ ).option(
5832
5930
  "--task-lease-ttl <seconds>",
5833
5931
  "Task lease TTL (60-86400)",
5834
5932
  parseInteger,
@@ -5915,7 +6013,7 @@ function registerExecutor(program2) {
5915
6013
  taskLeaseTtlSeconds: opts.taskLeaseTtl,
5916
6014
  modelId: opts.modelId,
5917
6015
  effortLabel: opts.effortLabel,
5918
- claimPolicy: (opts.claimPolicy ?? "open").toLowerCase() === "restricted" ? "restricted" : "open",
6016
+ claimPolicy: parseClaimPolicy(opts.claimPolicy) === "Restricted" ? "restricted" : "open",
5919
6017
  claimTags: opts.claimTag ?? [],
5920
6018
  excludeTags: opts.excludeTag ?? [],
5921
6019
  relayId: opts.relay,
@@ -6036,8 +6134,8 @@ function registerExecutor(program2) {
6036
6134
  "Capability operation keys claimed by this instance"
6037
6135
  ).option(
6038
6136
  "--claim-policy <policy>",
6039
- "open | restricted (restricted only claims preferred/own-lane/allow-listed-tag work)",
6040
- "open"
6137
+ "open | restricted (restricted only claims preferred/own-lane/allow-listed-tag work; default restricted)",
6138
+ DEFAULT_CLAIM_POLICY
6041
6139
  ).option(
6042
6140
  "--claim-tag <tag...>",
6043
6141
  "Task tag this instance accepts under --claim-policy restricted"
@@ -6048,7 +6146,12 @@ function registerExecutor(program2) {
6048
6146
  "--activation-mode <mode>",
6049
6147
  "attached | detached \u2014 detached marks a fleet run as a service that outlives its shell (default attached)",
6050
6148
  "attached"
6051
- ).option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).option(
6149
+ ).option(
6150
+ "--ttl <seconds>",
6151
+ "Advertisement TTL (30 to tenant maximum)",
6152
+ parseInteger,
6153
+ 120
6154
+ ).option(
6052
6155
  "--task-lease-ttl <seconds>",
6053
6156
  "Task lease TTL (60-86400)",
6054
6157
  parseInteger,
@@ -6104,7 +6207,12 @@ function registerExecutor(program2) {
6104
6207
  `)
6105
6208
  );
6106
6209
  });
6107
- executor.command("refresh <id>").description("Refresh one advertisement lease once").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (id, opts, cmd) => {
6210
+ executor.command("refresh <id>").description("Refresh one advertisement lease once").option(
6211
+ "--ttl <seconds>",
6212
+ "Advertisement TTL (30 to tenant maximum)",
6213
+ parseInteger,
6214
+ 120
6215
+ ).action(async (id, opts, cmd) => {
6108
6216
  const data = await refreshExecutorInstance(
6109
6217
  resolveConfig(cmd.optsWithGlobals()),
6110
6218
  id,
@@ -6112,7 +6220,12 @@ function registerExecutor(program2) {
6112
6220
  );
6113
6221
  emit(data, Boolean(cmd.optsWithGlobals().json));
6114
6222
  });
6115
- executor.command("heartbeat <id>").description("Keep an advertisement alive until interrupted").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).option("--interval <seconds>", "Refresh interval", parseInteger, 40).action(async (id, opts, cmd) => {
6223
+ executor.command("heartbeat <id>").description("Keep an advertisement alive until interrupted").option(
6224
+ "--ttl <seconds>",
6225
+ "Advertisement TTL (30 to tenant maximum)",
6226
+ parseInteger,
6227
+ 120
6228
+ ).option("--interval <seconds>", "Refresh interval", parseInteger, 40).action(async (id, opts, cmd) => {
6116
6229
  if (opts.interval >= opts.ttl)
6117
6230
  fail("heartbeat interval must be shorter than the TTL");
6118
6231
  const cfg = resolveConfig(cmd.optsWithGlobals());
@@ -6274,12 +6387,12 @@ function parseRuntimeKind(value) {
6274
6387
  }
6275
6388
  }
6276
6389
  function parseClaimPolicy(value) {
6277
- switch ((value ?? "open").trim().toLowerCase()) {
6390
+ switch ((value ?? DEFAULT_CLAIM_POLICY).trim().toLowerCase()) {
6278
6391
  case "":
6279
- case "open":
6280
- return "Open";
6281
6392
  case "restricted":
6282
6393
  return "Restricted";
6394
+ case "open":
6395
+ return "Open";
6283
6396
  default:
6284
6397
  return fail("claim-policy must be open or restricted");
6285
6398
  }
@@ -7607,6 +7720,7 @@ function resolveLane(flagLane, cwd) {
7607
7720
  return applyWorktreeLaneSuffix(base, start);
7608
7721
  }
7609
7722
  var INTENT_FILE = join15(".sechroom", "continuity.json");
7723
+ var LOCAL_DRY_RUN_VALIDATION_WARNING = "LOCAL-ONLY \u2014 NOT SERVER-VALIDATED";
7610
7724
  function resolveIntentPath(start) {
7611
7725
  let dir = start;
7612
7726
  for (; ; ) {
@@ -7631,6 +7745,16 @@ function hasRequiredIntent(i) {
7631
7745
  i.objective?.trim() && i.state?.trim() && i.lastAction?.trim() && i.nextAction?.trim() && i.resumeInstruction?.trim()
7632
7746
  );
7633
7747
  }
7748
+ function localDryRunMissingFields(i) {
7749
+ const required = [
7750
+ ["objective", "--objective"],
7751
+ ["state", "--state"],
7752
+ ["lastAction", "--last-action"],
7753
+ ["nextAction", "--next-action"],
7754
+ ["resumeInstruction", "--resume-instruction"]
7755
+ ];
7756
+ return required.filter(([key]) => !String(i[key] ?? "").trim()).map(([, flag]) => flag);
7757
+ }
7634
7758
  async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScope, opts) {
7635
7759
  const lane = resolveLane(laneFlag, cwd);
7636
7760
  if (!lane) return false;
@@ -7652,7 +7776,9 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
7652
7776
  openQuestions: intent.questions ?? null,
7653
7777
  surfaceMarkers: intent.surfaceMarkers ?? null,
7654
7778
  relevantArtifactIds: intent.artifacts ?? null,
7655
- confidence: intent.confidence ?? null,
7779
+ // Preserve invalid JSON-string confidence tokens for the server's semantic
7780
+ // validator; never coerce them through Number/NaN/null.
7781
+ confidence: confidenceWireValue(intent.confidence),
7656
7782
  // Frequent triggers (compaction, session-end) land within the FR-051 4h
7657
7783
  // window; Acknowledge lets the checkpoint persist on the lane.
7658
7784
  concurrentSessionPolicy: "Acknowledge"
@@ -7947,12 +8073,18 @@ Fail-soft: failures exit 0 and never block; session-context refresh failures ren
7947
8073
  function registerCheckpoint(program2) {
7948
8074
  program2.command("checkpoint").description(
7949
8075
  "Checkpoint working state: create a continuity snapshot (server-validated) AND sync ./.sechroom/continuity.json in one step"
7950
- ).option("--lane <laneId>", "Lane id (else SECHROOM_LANE, else ./.sechroom/lane.json code-lane)").option("--scope <scope>", "Snapshot scope (else the file's scope, else 'session')").option("--objective <text>", "Current objective").option("--state <text>", "Current state").option("--last-action <text>", "Last meaningful action").option("--next-action <text>", "Next intended action").option("--resume-instruction <text>", "Resume instruction").option("--constraint <text...>", "Active constraints (repeatable)").option("--question <text...>", "Open questions (repeatable)").option("--surface-marker <text...>", "Surface markers (repeatable)").option("--artifact <id...>", "Relevant artifact ids (repeatable)").option("--confidence <n>", "Confidence 0..1").option("--dry-run", "validate + print the snapshot payload without creating it or writing the file", false).addHelpText(
8076
+ ).option("--lane <laneId>", "Lane id (else SECHROOM_LANE, else ./.sechroom/lane.json code-lane)").option("--scope <scope>", "Snapshot scope (else the file's scope, else 'session')").option("--objective <text>", "Current objective").option("--state <text>", "Current state").option("--last-action <text>", "Last meaningful action").option("--next-action <text>", "Next intended action").option("--resume-instruction <text>", "Resume instruction").option("--constraint <text...>", "Active constraints (repeatable)").option("--question <text...>", "Open questions (repeatable)").option("--surface-marker <text...>", "Surface markers (repeatable)").option("--artifact <id...>", "Relevant artifact ids (repeatable)").option("--confidence <n>", "Confidence 0..1").option(
8077
+ "--dry-run",
8078
+ "run the local required-field check and print the payload (LOCAL-ONLY; NOT SERVER-VALIDATED)",
8079
+ false
8080
+ ).addHelpText(
7951
8081
  "after",
7952
8082
  `
7953
8083
  File-first: reads ./.sechroom/continuity.json (kept current as you work) as the base; any flag
7954
8084
  overrides that field. The snapshot is created FIRST (server-validated), then the local file is
7955
8085
  written/normalized with the returned snapshotId. Lane: --lane > SECHROOM_LANE > ./.sechroom/lane.json code-lane.
8086
+ --dry-run performs only the local required-field check and prints a payload. Its output is
8087
+ explicitly LOCAL-ONLY; NOT SERVER-VALIDATED, so it does not establish write-path parity.
7956
8088
 
7957
8089
  Examples:
7958
8090
  $ sechroom checkpoint snapshot from ./.sechroom/continuity.json, then sync it
@@ -7975,7 +8107,9 @@ Examples:
7975
8107
  questions: opts.question ?? base.questions,
7976
8108
  surfaceMarkers: opts.surfaceMarker ?? base.surfaceMarkers,
7977
8109
  artifacts: opts.artifact ?? base.artifacts,
7978
- confidence: opts.confidence != null ? Number(opts.confidence) : base.confidence
8110
+ // Keep the raw token until the server's shared validator sees it. In particular,
8111
+ // Number("high") -> NaN -> JSON null would silently discard the operator's input.
8112
+ confidence: opts.confidence != null ? opts.confidence : base.confidence
7979
8113
  };
7980
8114
  const lane = resolveLane(opts.lane, cwd);
7981
8115
  if (!lane) {
@@ -7983,19 +8117,6 @@ Examples:
7983
8117
  "no lane resolved \u2014 pass --lane, set SECHROOM_LANE, or pin one in ./.sechroom/lane.json (code-lane). See `sechroom lane`."
7984
8118
  );
7985
8119
  }
7986
- const required = [
7987
- ["objective", "--objective"],
7988
- ["state", "--state"],
7989
- ["lastAction", "--last-action"],
7990
- ["nextAction", "--next-action"],
7991
- ["resumeInstruction", "--resume-instruction"]
7992
- ];
7993
- const missing = required.filter(([k]) => !String(merged[k] ?? "").trim()).map(([, flag]) => flag);
7994
- if (missing.length > 0) {
7995
- fail(
7996
- `missing required field(s): ${missing.join(", ")} \u2014 supply via flag or in ./.sechroom/continuity.json`
7997
- );
7998
- }
7999
8120
  const scope = merged.scope ?? "session";
8000
8121
  const body = {
8001
8122
  laneId: lane,
@@ -8009,13 +8130,33 @@ Examples:
8009
8130
  openQuestions: merged.questions ?? null,
8010
8131
  surfaceMarkers: merged.surfaceMarkers ?? null,
8011
8132
  relevantArtifactIds: merged.artifacts ?? null,
8012
- confidence: merged.confidence ?? null,
8133
+ confidence: confidenceWireValue(merged.confidence),
8013
8134
  // Explicit checkpoints are often within the FR-051 4h window; Acknowledge
8014
8135
  // lets one land on the lane (matches `hook pre-compact`).
8015
8136
  concurrentSessionPolicy: "Acknowledge"
8016
8137
  };
8017
8138
  if (opts.dryRun) {
8018
- emit({ dryRun: true, lane, scope, wouldCreate: body }, json);
8139
+ const missing = localDryRunMissingFields(merged);
8140
+ if (missing.length > 0) {
8141
+ fail(
8142
+ `LOCAL-ONLY CHECK \u2014 NOT SERVER-VALIDATED: missing required field(s): ${missing.join(", ")} \u2014 supply via flag or in ./.sechroom/continuity.json`
8143
+ );
8144
+ }
8145
+ emit(
8146
+ {
8147
+ dryRun: true,
8148
+ validation: {
8149
+ mode: "local-only",
8150
+ serverValidated: false,
8151
+ checks: ["required-fields"],
8152
+ warning: LOCAL_DRY_RUN_VALIDATION_WARNING
8153
+ },
8154
+ lane,
8155
+ scope,
8156
+ wouldCreate: body
8157
+ },
8158
+ json
8159
+ );
8019
8160
  return;
8020
8161
  }
8021
8162
  const data = await runApi("Creating snapshot", async () => {
@@ -8252,7 +8393,7 @@ Displaced snapshot recovery surfaces:
8252
8393
  openQuestions: opts.question ?? null,
8253
8394
  surfaceMarkers: opts.surfaceMarker ?? null,
8254
8395
  relevantArtifactIds: opts.artifact ?? null,
8255
- confidence: opts.confidence != null ? Number(opts.confidence) : null
8396
+ confidence: confidenceWireValue(opts.confidence)
8256
8397
  }
8257
8398
  });
8258
8399
  });
@@ -8401,6 +8542,96 @@ function snapshotGetNotFoundHint(includeAll, status) {
8401
8542
 
8402
8543
  // src/commands/work-plan.ts
8403
8544
  import { readFile as readFile2 } from "fs/promises";
8545
+
8546
+ // src/paging.ts
8547
+ var MAX_AUTO_PAGES = 100;
8548
+ var asNumber = (value) => value === void 0 || value === "" ? void 0 : Number(value);
8549
+ function shouldAutoPage(opts) {
8550
+ if (opts.autoPage === false) return false;
8551
+ return asNumber(opts.page) === void 0;
8552
+ }
8553
+ function singlePageQuery(opts) {
8554
+ const page = asNumber(opts.page);
8555
+ const pageSize = asNumber(opts.pageSize);
8556
+ return {
8557
+ ...page === void 0 ? {} : { page },
8558
+ ...pageSize === void 0 ? {} : { pageSize }
8559
+ };
8560
+ }
8561
+ async function fetchAllPages(fetchPage, opts = {}, label = "results") {
8562
+ const pages = [];
8563
+ let page = 1;
8564
+ let truncated = false;
8565
+ for (; ; ) {
8566
+ const current = await fetchPage(page);
8567
+ pages.push(current);
8568
+ if (!hasNextPage(current)) break;
8569
+ if (pages.length >= MAX_AUTO_PAGES) {
8570
+ truncated = true;
8571
+ break;
8572
+ }
8573
+ page = current.page + 1;
8574
+ }
8575
+ const first = pages[0];
8576
+ if (!first) throw new Error("paged read returned no envelope at all");
8577
+ if (truncated) {
8578
+ if (!isQuiet()) {
8579
+ process.stderr.write(
8580
+ `${warn("!")} Stopped after ${MAX_AUTO_PAGES} pages \u2014 more ${label} remain. ${style.dim("Narrow the filters, or read a specific window with --page/--page-size.")}
8581
+ `
8582
+ );
8583
+ }
8584
+ const partial = pages.flatMap((p) => p.items);
8585
+ return {
8586
+ ...first,
8587
+ items: partial,
8588
+ page: 1,
8589
+ pageSize: partial.length,
8590
+ // Pages OF THIS SIZE, so the number stays consistent with the pageSize just reported.
8591
+ pageCount: partial.length === 0 ? 1 : Math.ceil(first.count / partial.length),
8592
+ hasPreviousPage: false,
8593
+ hasNextPage: true,
8594
+ isFirstPage: true,
8595
+ isLastPage: false,
8596
+ firstItemOnPage: partial.length === 0 ? 0 : 1,
8597
+ lastItemOnPage: partial.length
8598
+ };
8599
+ }
8600
+ if (pages.length === 1) return first;
8601
+ const items = pages.flatMap((current) => current.items);
8602
+ return {
8603
+ ...first,
8604
+ items,
8605
+ page: 1,
8606
+ pageSize: items.length,
8607
+ pageCount: 1,
8608
+ hasPreviousPage: false,
8609
+ hasNextPage: false,
8610
+ isFirstPage: true,
8611
+ isLastPage: true,
8612
+ firstItemOnPage: items.length === 0 ? 0 : 1,
8613
+ lastItemOnPage: items.length
8614
+ };
8615
+ }
8616
+ function hasNextPage(current) {
8617
+ if (current.hasNextPage !== void 0) return current.hasNextPage;
8618
+ if (current.pageCount !== void 0) return current.page < current.pageCount;
8619
+ return current.page * current.pageSize < current.count;
8620
+ }
8621
+ var PAGE_OPTION = [
8622
+ "--page <n>",
8623
+ "Page number (reads only that page instead of all)"
8624
+ ];
8625
+ var PAGE_SIZE_OPTION = [
8626
+ "--page-size <n>",
8627
+ "Page size used while paging"
8628
+ ];
8629
+ var NO_AUTO_PAGE_OPTION = [
8630
+ "--no-auto-page",
8631
+ "Read only the first page instead of walking every page"
8632
+ ];
8633
+
8634
+ // src/commands/work-plan.ts
8404
8635
  function registerWorkPlan(program2) {
8405
8636
  const workPlan = program2.command("work-plan").description(
8406
8637
  "Drive a work plan: create one from a brief, then execute / accept / reject"
@@ -8594,22 +8825,27 @@ Examples:
8594
8825
  globals.json
8595
8826
  );
8596
8827
  });
8597
- workPlan.command("list").description("List work plans, newest-first (GET /decompositions)").option("--status <status>", "Filter by decomposition status").option("--brief <briefId>", "Filter by work-brief memory id").option("--page <n>", "1-based page number", (v) => Number.parseInt(v, 10)).option("--page-size <n>", "Page size", (v) => Number.parseInt(v, 10)).action(async (opts, cmd) => {
8828
+ workPlan.command("list").description(
8829
+ "List work plans, newest-first (GET /decompositions). Walks every page by default."
8830
+ ).option("--status <status>", "Filter by decomposition status").option("--brief <briefId>", "Filter by work-brief memory id").option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (opts, cmd) => {
8598
8831
  const globals = cmd.optsWithGlobals();
8599
8832
  const cfg = resolveConfig(globals);
8600
- const data = await runApi("Listing work plans", async () => {
8833
+ const readPage = (query) => runApi("Listing work plans", async () => {
8601
8834
  const client = await makeClient(cfg);
8602
8835
  return client.GET("/decompositions", {
8603
8836
  params: {
8604
- query: {
8605
- status: opts.status,
8606
- briefId: opts.brief,
8607
- page: opts.page,
8608
- pageSize: opts.pageSize
8609
- }
8837
+ query: { status: opts.status, briefId: opts.brief, ...query }
8610
8838
  }
8611
8839
  });
8612
8840
  });
8841
+ const data = shouldAutoPage(opts) ? await fetchAllPages(
8842
+ (page) => readPage({
8843
+ page,
8844
+ ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
8845
+ }),
8846
+ opts,
8847
+ "work plans"
8848
+ ) : await readPage(singlePageQuery(opts));
8613
8849
  emitAction(
8614
8850
  `listed ${style.bold(String(data.items.length))} of ${data.count} work plan(s)`,
8615
8851
  data,
@@ -8697,35 +8933,53 @@ Examples:
8697
8933
  $ sechroom filing reject fsg_XXXX --reason "wrong workspace"
8698
8934
  $ sechroom filing edit-and-accept fsg_XXXX --target-kind Workspace --existing-target-id wsp_XXXX`
8699
8935
  );
8700
- filing.command("suggestions").description("List filing suggestions (GET /filing/suggestions)").option("--memory-id <memoryId>", "Filter to a single memory's suggestions").option(
8936
+ filing.command("suggestions").description(
8937
+ "List filing suggestions (GET /filing/suggestions). Walks every page by default."
8938
+ ).option("--memory-id <memoryId>", "Filter to a single memory's suggestions").option(
8701
8939
  "--status <status>",
8702
8940
  "Generating | Pending | Accepted | Rejected | EditedAndAccepted | Deferred | Invalidated"
8703
- ).option("--page <n>", "Page number").option("--page-size <n>", "Page size").action(async (opts, cmd) => {
8941
+ ).option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (opts, cmd) => {
8704
8942
  const cfg = resolveConfig(cmd.optsWithGlobals());
8705
- const data = await runApi("Listing filing suggestions", async () => {
8943
+ const filters = {
8944
+ ...opts.memoryId ? { memoryId: opts.memoryId } : {},
8945
+ ...opts.status ? {
8946
+ status: opts.status
8947
+ } : {}
8948
+ };
8949
+ const readPage = (query) => runApi("Listing filing suggestions", async () => {
8706
8950
  const client = await makeClient(cfg);
8707
8951
  return client.GET("/filing/suggestions", {
8708
- params: {
8709
- query: {
8710
- ...opts.memoryId ? { memoryId: opts.memoryId } : {},
8711
- ...opts.status ? { status: opts.status } : {},
8712
- ...opts.page ? { page: Number(opts.page) } : {},
8713
- ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
8714
- }
8715
- }
8952
+ params: { query: { ...filters, ...query } }
8716
8953
  });
8717
8954
  });
8955
+ const data = shouldAutoPage(opts) ? await fetchAllPages(
8956
+ (page) => readPage({
8957
+ page,
8958
+ ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
8959
+ }),
8960
+ opts,
8961
+ "filing suggestions"
8962
+ ) : await readPage(singlePageQuery(opts));
8718
8963
  emit(data, cmd.optsWithGlobals().json);
8719
8964
  });
8720
- filing.command("get <id>").description("Fetch a filing suggestion by id (GET /filing/suggestions/{id})").action(async (id, _opts, cmd) => {
8965
+ filing.command("get <id>").description(
8966
+ "Fetch a filing suggestion by id (GET /filing/suggestions/{id})"
8967
+ ).action(async (id, _opts, cmd) => {
8721
8968
  const cfg = resolveConfig(cmd.optsWithGlobals());
8722
8969
  const data = await runApi("Fetching filing suggestion", async () => {
8723
8970
  const client = await makeClient(cfg);
8724
- return client.GET("/filing/suggestions/{id}", { params: { path: { id } } });
8971
+ return client.GET("/filing/suggestions/{id}", {
8972
+ params: { path: { id } }
8973
+ });
8725
8974
  });
8726
8975
  emit(data, cmd.optsWithGlobals().json);
8727
8976
  });
8728
- filing.command("preview").description("Preview a filing suggestion for a memory id or ad-hoc shape (POST /filing/suggestions/preview)").option("--memory-id <memoryId>", "Preview filing for an existing memory").option("--text <text>", "Ad-hoc memory body text (instead of --memory-id)").option("--title <title>", "Ad-hoc memory title").option("--tag <tag...>", "Ad-hoc memory tags (repeatable)").option("--type <type>", "Ad-hoc memory type", "reference").option("--scope-workspace <workspaceId>", "Scope the preview to a workspace").action(async (opts, cmd) => {
8977
+ filing.command("preview").description(
8978
+ "Preview a filing suggestion for a memory id or ad-hoc shape (POST /filing/suggestions/preview)"
8979
+ ).option("--memory-id <memoryId>", "Preview filing for an existing memory").option("--text <text>", "Ad-hoc memory body text (instead of --memory-id)").option("--title <title>", "Ad-hoc memory title").option("--tag <tag...>", "Ad-hoc memory tags (repeatable)").option("--type <type>", "Ad-hoc memory type", "reference").option(
8980
+ "--scope-workspace <workspaceId>",
8981
+ "Scope the preview to a workspace"
8982
+ ).action(async (opts, cmd) => {
8729
8983
  const cfg = resolveConfig(cmd.optsWithGlobals());
8730
8984
  const memory = opts.text ? {
8731
8985
  text: opts.text,
@@ -8745,15 +8999,26 @@ Examples:
8745
8999
  });
8746
9000
  emit(data, cmd.optsWithGlobals().json);
8747
9001
  });
8748
- filing.command("accept <id>").description("Accept a filing suggestion (POST /filing/suggestions/{id}/accept)").action(async (id, _opts, cmd) => {
9002
+ filing.command("accept <id>").description(
9003
+ "Accept a filing suggestion (POST /filing/suggestions/{id}/accept)"
9004
+ ).action(async (id, _opts, cmd) => {
8749
9005
  const cfg = resolveConfig(cmd.optsWithGlobals());
8750
9006
  const data = await runApi("Accepting filing suggestion", async () => {
8751
9007
  const client = await makeClient(cfg);
8752
- return client.POST("/filing/suggestions/{id}/accept", { params: { path: { id } }, body: {} });
9008
+ return client.POST("/filing/suggestions/{id}/accept", {
9009
+ params: { path: { id } },
9010
+ body: {}
9011
+ });
8753
9012
  });
8754
- emitAction(`accepted filing suggestion ${style.bold(id)}`, data, cmd.optsWithGlobals().json);
9013
+ emitAction(
9014
+ `accepted filing suggestion ${style.bold(id)}`,
9015
+ data,
9016
+ cmd.optsWithGlobals().json
9017
+ );
8755
9018
  });
8756
- filing.command("reject <id>").description("Reject a filing suggestion (POST /filing/suggestions/{id}/reject)").option("--reason <reason>", "Why the suggestion was rejected").option("--reason-code <code>", "Structured reason code").action(async (id, opts, cmd) => {
9019
+ filing.command("reject <id>").description(
9020
+ "Reject a filing suggestion (POST /filing/suggestions/{id}/reject)"
9021
+ ).option("--reason <reason>", "Why the suggestion was rejected").option("--reason-code <code>", "Structured reason code").action(async (id, opts, cmd) => {
8757
9022
  const cfg = resolveConfig(cmd.optsWithGlobals());
8758
9023
  const data = await runApi("Rejecting filing suggestion", async () => {
8759
9024
  const client = await makeClient(cfg);
@@ -8765,9 +9030,18 @@ Examples:
8765
9030
  }
8766
9031
  });
8767
9032
  });
8768
- emitAction(`rejected filing suggestion ${style.bold(id)}`, data, cmd.optsWithGlobals().json);
9033
+ emitAction(
9034
+ `rejected filing suggestion ${style.bold(id)}`,
9035
+ data,
9036
+ cmd.optsWithGlobals().json
9037
+ );
8769
9038
  });
8770
- filing.command("defer <id>").description("Defer a filing suggestion (POST /filing/suggestions/{id}/defer)").option("--until <iso>", "Defer until an ISO-8601 timestamp (defaults to indefinite)").action(async (id, opts, cmd) => {
9039
+ filing.command("defer <id>").description(
9040
+ "Defer a filing suggestion (POST /filing/suggestions/{id}/defer)"
9041
+ ).option(
9042
+ "--until <iso>",
9043
+ "Defer until an ISO-8601 timestamp (defaults to indefinite)"
9044
+ ).action(async (id, opts, cmd) => {
8771
9045
  const cfg = resolveConfig(cmd.optsWithGlobals());
8772
9046
  const data = await runApi("Deferring filing suggestion", async () => {
8773
9047
  const client = await makeClient(cfg);
@@ -8776,25 +9050,44 @@ Examples:
8776
9050
  body: { until: opts.until ?? null }
8777
9051
  });
8778
9052
  });
8779
- emitAction(`deferred filing suggestion ${style.bold(id)}`, data, cmd.optsWithGlobals().json);
9053
+ emitAction(
9054
+ `deferred filing suggestion ${style.bold(id)}`,
9055
+ data,
9056
+ cmd.optsWithGlobals().json
9057
+ );
8780
9058
  });
8781
- filing.command("edit-and-accept <id>").description("Override the target then accept (POST /filing/suggestions/{id}/edit-and-accept)").option("--target-kind <kind>", "Workspace | Project").option("--existing-target-id <id>", "File into an existing workspace/project").option("--new-name <name>", "Create a new target with this name").option("--new-description <text>", "Description for the new target").option("--new-parent-workspace <workspaceId>", "Parent workspace for a new project").option("--memory-id <memoryId...>", "Override the memory set (repeatable)").action(async (id, opts, cmd) => {
9059
+ filing.command("edit-and-accept <id>").description(
9060
+ "Override the target then accept (POST /filing/suggestions/{id}/edit-and-accept)"
9061
+ ).option("--target-kind <kind>", "Workspace | Project").option(
9062
+ "--existing-target-id <id>",
9063
+ "File into an existing workspace/project"
9064
+ ).option("--new-name <name>", "Create a new target with this name").option("--new-description <text>", "Description for the new target").option(
9065
+ "--new-parent-workspace <workspaceId>",
9066
+ "Parent workspace for a new project"
9067
+ ).option("--memory-id <memoryId...>", "Override the memory set (repeatable)").action(async (id, opts, cmd) => {
8782
9068
  const cfg = resolveConfig(cmd.optsWithGlobals());
8783
- const data = await runApi("Editing and accepting filing suggestion", async () => {
8784
- const client = await makeClient(cfg);
8785
- return client.POST("/filing/suggestions/{id}/edit-and-accept", {
8786
- params: { path: { id } },
8787
- body: {
8788
- targetKind: opts.targetKind ?? null,
8789
- existingTargetId: opts.existingTargetId ?? null,
8790
- newName: opts.newName ?? null,
8791
- newDescription: opts.newDescription ?? null,
8792
- newParentWorkspaceId: opts.newParentWorkspace ?? null,
8793
- overrideMemoryIds: opts.memoryId ?? null
8794
- }
8795
- });
8796
- });
8797
- emitAction(`edited & accepted filing suggestion ${style.bold(id)}`, data, cmd.optsWithGlobals().json);
9069
+ const data = await runApi(
9070
+ "Editing and accepting filing suggestion",
9071
+ async () => {
9072
+ const client = await makeClient(cfg);
9073
+ return client.POST("/filing/suggestions/{id}/edit-and-accept", {
9074
+ params: { path: { id } },
9075
+ body: {
9076
+ targetKind: opts.targetKind ?? null,
9077
+ existingTargetId: opts.existingTargetId ?? null,
9078
+ newName: opts.newName ?? null,
9079
+ newDescription: opts.newDescription ?? null,
9080
+ newParentWorkspaceId: opts.newParentWorkspace ?? null,
9081
+ overrideMemoryIds: opts.memoryId ?? null
9082
+ }
9083
+ });
9084
+ }
9085
+ );
9086
+ emitAction(
9087
+ `edited & accepted filing suggestion ${style.bold(id)}`,
9088
+ data,
9089
+ cmd.optsWithGlobals().json
9090
+ );
8798
9091
  });
8799
9092
  }
8800
9093
 
@@ -10117,6 +10410,18 @@ function resolveCreateBody(textOpt, fileOpt) {
10117
10410
  );
10118
10411
  return { text: text2, defaultTitle };
10119
10412
  }
10413
+ async function fetchAllMemoryRelationships(cfg, memoryId) {
10414
+ return await fetchAllPages(
10415
+ (page) => runApi(`Fetching relationships (page ${page})`, async () => {
10416
+ const client = await makeClient(cfg);
10417
+ return client.GET("/memories/{memoryId}/relationships", {
10418
+ params: { path: { memoryId }, query: { page } }
10419
+ });
10420
+ }),
10421
+ {},
10422
+ "relationships"
10423
+ );
10424
+ }
10120
10425
  function registerMemory(program2) {
10121
10426
  const memory = program2.command("memory").description("Create, read, and search memories");
10122
10427
  memory.addHelpText(
@@ -10371,7 +10676,16 @@ ${plan.rows.map(
10371
10676
  false
10372
10677
  );
10373
10678
  });
10374
- memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").action(async (memoryId, _opts, cmd) => {
10679
+ const get = memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").option(
10680
+ "--with-relationships",
10681
+ "Include the memory's relationships in the response",
10682
+ false
10683
+ ).addHelpText(
10684
+ "after",
10685
+ `
10686
+ 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.`
10687
+ );
10688
+ get.action(async (memoryId, opts, cmd) => {
10375
10689
  const cfg = resolveConfig(cmd.optsWithGlobals());
10376
10690
  const data = await runApi("Fetching memory", async () => {
10377
10691
  const client = await makeClient(cfg);
@@ -10379,6 +10693,11 @@ ${plan.rows.map(
10379
10693
  params: { path: { memoryId } }
10380
10694
  });
10381
10695
  });
10696
+ if (opts.withRelationships) {
10697
+ const relationships = await fetchAllMemoryRelationships(cfg, memoryId);
10698
+ emit({ ...data, relationships }, cmd.optsWithGlobals().json);
10699
+ return;
10700
+ }
10382
10701
  emit(data, cmd.optsWithGlobals().json);
10383
10702
  });
10384
10703
  memory.command("search <query>").description(
@@ -10601,21 +10920,28 @@ ${plan.rows.map(
10601
10920
  cmd.optsWithGlobals().json
10602
10921
  );
10603
10922
  });
10604
- memory.command("list-archived").description("List archived memories (GET /memories/archived)").option("--workspace <workspaceId>", "Scope to a workspace").option("--project <projectId>", "Scope to a project").option("--page <n>", "Page number").option("--page-size <n>", "Page size").action(async (opts, cmd) => {
10923
+ memory.command("list-archived").description(
10924
+ "List archived memories (GET /memories/archived). Walks every page by default."
10925
+ ).option("--workspace <workspaceId>", "Scope to a workspace").option("--project <projectId>", "Scope to a project").option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (opts, cmd) => {
10605
10926
  const cfg = resolveConfig(cmd.optsWithGlobals());
10606
- const data = await runApi("Listing archived memories", async () => {
10927
+ const filters = {
10928
+ ...opts.workspace ? { workspaceId: opts.workspace } : {},
10929
+ ...opts.project ? { projectId: opts.project } : {}
10930
+ };
10931
+ const readPage = (query) => runApi("Listing archived memories", async () => {
10607
10932
  const client = await makeClient(cfg);
10608
10933
  return client.GET("/memories/archived", {
10609
- params: {
10610
- query: {
10611
- ...opts.workspace ? { workspaceId: opts.workspace } : {},
10612
- ...opts.project ? { projectId: opts.project } : {},
10613
- ...opts.page ? { page: Number(opts.page) } : {},
10614
- ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
10615
- }
10616
- }
10934
+ params: { query: { ...filters, ...query } }
10617
10935
  });
10618
10936
  });
10937
+ const data = shouldAutoPage(opts) ? await fetchAllPages(
10938
+ (page) => readPage({
10939
+ page,
10940
+ ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
10941
+ }),
10942
+ opts,
10943
+ "archived memories"
10944
+ ) : await readPage(singlePageQuery(opts));
10619
10945
  emit(data, cmd.optsWithGlobals().json);
10620
10946
  });
10621
10947
  memory.command("versions <memoryId>").description("List a memory's versions (GET /memories/{memoryId}/versions)").action(async (memoryId, _opts, cmd) => {
@@ -10726,8 +11052,18 @@ ${plan.rows.map(
10726
11052
  }
10727
11053
 
10728
11054
  // src/setup/apply.ts
10729
- import { createHash as createHash5 } from "crypto";
10730
- import { mkdirSync as mkdirSync16, readFileSync as readFileSync16, writeFileSync as writeFileSync15, existsSync as existsSync13 } from "fs";
11055
+ import { createHash as createHash5, randomUUID as randomUUID3 } from "crypto";
11056
+ import {
11057
+ chmodSync as chmodSync2,
11058
+ copyFileSync,
11059
+ existsSync as existsSync13,
11060
+ mkdirSync as mkdirSync16,
11061
+ readFileSync as readFileSync16,
11062
+ renameSync as renameSync4,
11063
+ rmSync as rmSync7,
11064
+ statSync as statSync5,
11065
+ writeFileSync as writeFileSync15
11066
+ } from "fs";
10731
11067
  import { dirname as dirname15 } from "path";
10732
11068
  var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
10733
11069
  var MARKER_END = "<!-- @sechroom/cli:end";
@@ -10773,11 +11109,17 @@ function parseManagedBlock(content, block) {
10773
11109
  const keyed = content.match(keyedBlockRe(block));
10774
11110
  if (keyed) {
10775
11111
  const attrs = parseAttrs(keyed[0].slice(0, keyed[0].indexOf("\n")));
10776
- return { block, source: attrs.source ?? null, sha256: attrs.sha256 ?? null, body: innerBody(keyed[0]) };
11112
+ return {
11113
+ block,
11114
+ source: attrs.source ?? null,
11115
+ sha256: attrs.sha256 ?? null,
11116
+ body: innerBody(keyed[0])
11117
+ };
10777
11118
  }
10778
11119
  if (block === "role-template") {
10779
11120
  const legacy = content.match(legacyBlockRe());
10780
- if (legacy) return { block, source: null, sha256: null, body: innerBody(legacy[0]) };
11121
+ if (legacy)
11122
+ return { block, source: null, sha256: null, body: innerBody(legacy[0]) };
10781
11123
  }
10782
11124
  return null;
10783
11125
  }
@@ -10799,33 +11141,446 @@ function mergeMcpJson(path, snippet, dryRun) {
10799
11141
  try {
10800
11142
  current = JSON.parse(readFileSync16(path, "utf8"));
10801
11143
  } catch {
10802
- return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
11144
+ return {
11145
+ kind: "mcp",
11146
+ path,
11147
+ status: "skipped",
11148
+ note: "existing file isn't valid JSON \u2014 left untouched"
11149
+ };
10803
11150
  }
10804
11151
  }
10805
- current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
11152
+ current.mcpServers = {
11153
+ ...current.mcpServers ?? {},
11154
+ ...incoming.mcpServers ?? {}
11155
+ };
10806
11156
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
10807
11157
  ensureDir2(path);
10808
11158
  writeFileSync15(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
10809
11159
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
10810
11160
  }
10811
- function mergeCodexToml(path, snippet, dryRun) {
11161
+ var CODEX_MCP_TABLE = "mcp_servers.sechroom";
11162
+ function decodeTomlBasicString(value) {
11163
+ let decoded = "";
11164
+ for (let i = 0; i < value.length; i += 1) {
11165
+ const char = value[i];
11166
+ if (char !== "\\") {
11167
+ decoded += char;
11168
+ continue;
11169
+ }
11170
+ const escaped = value[++i];
11171
+ if (escaped === void 0) throw new Error("unterminated TOML escape");
11172
+ const simple = {
11173
+ b: "\b",
11174
+ f: "\f",
11175
+ n: "\n",
11176
+ r: "\r",
11177
+ t: " ",
11178
+ '"': '"',
11179
+ "\\": "\\"
11180
+ };
11181
+ if (escaped in simple) {
11182
+ decoded += simple[escaped];
11183
+ continue;
11184
+ }
11185
+ const digits = escaped === "u" ? 4 : escaped === "U" ? 8 : 0;
11186
+ if (digits === 0) throw new Error(`invalid TOML escape '\\\\${escaped}'`);
11187
+ const code = value.slice(i + 1, i + 1 + digits);
11188
+ if (!new RegExp(`^[0-9a-fA-F]{${digits}}$`).test(code))
11189
+ throw new Error(`invalid TOML Unicode escape '\\\\${escaped}${code}'`);
11190
+ decoded += String.fromCodePoint(Number.parseInt(code, 16));
11191
+ i += digits;
11192
+ }
11193
+ return decoded;
11194
+ }
11195
+ function normalizeTomlKey(raw) {
11196
+ const segments = [];
11197
+ let i = 0;
11198
+ while (i < raw.length) {
11199
+ while (/\s/.test(raw[i] ?? "")) i += 1;
11200
+ if (i >= raw.length) break;
11201
+ const quote = raw[i];
11202
+ let segment;
11203
+ if (quote === '"' || quote === "'") {
11204
+ i += 1;
11205
+ const start = i;
11206
+ let escaped = false;
11207
+ while (i < raw.length) {
11208
+ const char = raw[i];
11209
+ if (quote === '"' && escaped) {
11210
+ escaped = false;
11211
+ i += 1;
11212
+ continue;
11213
+ }
11214
+ if (quote === '"' && char === "\\") {
11215
+ escaped = true;
11216
+ i += 1;
11217
+ continue;
11218
+ }
11219
+ if (char === quote) break;
11220
+ i += 1;
11221
+ }
11222
+ if (raw[i] !== quote) throw new Error(`unterminated TOML key '${raw}'`);
11223
+ const encoded = raw.slice(start, i);
11224
+ segment = quote === '"' ? decodeTomlBasicString(encoded) : encoded;
11225
+ i += 1;
11226
+ } else {
11227
+ const start = i;
11228
+ while (i < raw.length && /[A-Za-z0-9_-]/.test(raw[i])) i += 1;
11229
+ segment = raw.slice(start, i);
11230
+ if (!segment) throw new Error(`invalid TOML key '${raw.trim()}'`);
11231
+ }
11232
+ segments.push(segment);
11233
+ while (/\s/.test(raw[i] ?? "")) i += 1;
11234
+ if (i >= raw.length) break;
11235
+ if (raw[i] !== ".") throw new Error(`invalid TOML key '${raw.trim()}'`);
11236
+ i += 1;
11237
+ }
11238
+ if (segments.length === 0) throw new Error("empty TOML key");
11239
+ return segments;
11240
+ }
11241
+ function sameTomlKey(left, right) {
11242
+ return left.length === right.length && left.every((segment, index) => segment === right[index]);
11243
+ }
11244
+ function isTomlKeyPrefix(prefix, candidate) {
11245
+ return prefix.length < candidate.length && prefix.every((segment, index) => segment === candidate[index]);
11246
+ }
11247
+ function tomlKeyLabel(key) {
11248
+ return key.join(".");
11249
+ }
11250
+ function stripTomlComment(line) {
11251
+ let quote = null;
11252
+ let escaped = false;
11253
+ for (let i = 0; i < line.length; i += 1) {
11254
+ const rest = line.slice(i);
11255
+ const char = line[i];
11256
+ if (quote === "multiline-basic") {
11257
+ if (rest.startsWith('"""') && !escaped) {
11258
+ escaped = false;
11259
+ quote = null;
11260
+ i += 2;
11261
+ continue;
11262
+ }
11263
+ if (escaped) escaped = false;
11264
+ else if (char === "\\") escaped = true;
11265
+ continue;
11266
+ }
11267
+ if (quote === "multiline-literal") {
11268
+ if (rest.startsWith("'''")) {
11269
+ i += 2;
11270
+ quote = null;
11271
+ }
11272
+ continue;
11273
+ }
11274
+ if (quote === "basic") {
11275
+ if (escaped) escaped = false;
11276
+ else if (char === "\\") escaped = true;
11277
+ else if (char === '"') quote = null;
11278
+ continue;
11279
+ }
11280
+ if (quote === "literal") {
11281
+ if (char === "'") quote = null;
11282
+ continue;
11283
+ }
11284
+ if (rest.startsWith('"""')) {
11285
+ quote = "multiline-basic";
11286
+ i += 2;
11287
+ } else if (rest.startsWith("'''")) {
11288
+ quote = "multiline-literal";
11289
+ i += 2;
11290
+ } else if (char === '"') quote = "basic";
11291
+ else if (char === "'") quote = "literal";
11292
+ else if (char === "#") return line.slice(0, i);
11293
+ }
11294
+ return line;
11295
+ }
11296
+ function scanTomlValue(value, state) {
11297
+ for (let i = 0; i < value.length; i += 1) {
11298
+ const rest = value.slice(i);
11299
+ const char = value[i];
11300
+ if (state.quote === "multiline-basic") {
11301
+ if (rest.startsWith('"""') && !state.escaped) {
11302
+ state.quote = null;
11303
+ i += 2;
11304
+ continue;
11305
+ }
11306
+ if (state.escaped) state.escaped = false;
11307
+ else if (char === "\\") state.escaped = true;
11308
+ continue;
11309
+ }
11310
+ if (state.quote === "multiline-literal") {
11311
+ if (rest.startsWith("'''")) {
11312
+ state.quote = null;
11313
+ i += 2;
11314
+ }
11315
+ continue;
11316
+ }
11317
+ if (state.quote === "basic") {
11318
+ if (state.escaped) state.escaped = false;
11319
+ else if (char === "\\") state.escaped = true;
11320
+ else if (char === '"') state.quote = null;
11321
+ continue;
11322
+ }
11323
+ if (state.quote === "literal") {
11324
+ if (char === "'") state.quote = null;
11325
+ continue;
11326
+ }
11327
+ if (char === "#") break;
11328
+ if (rest.startsWith('"""')) {
11329
+ state.quote = "multiline-basic";
11330
+ i += 2;
11331
+ } else if (rest.startsWith("'''")) {
11332
+ state.quote = "multiline-literal";
11333
+ i += 2;
11334
+ } else if (char === '"') state.quote = "basic";
11335
+ else if (char === "'") state.quote = "literal";
11336
+ else if (char === "[" || char === "{") state.containers.push(char);
11337
+ else if (char === "]" || char === "}") {
11338
+ const expected = char === "]" ? "[" : "{";
11339
+ if (state.containers.pop() !== expected)
11340
+ throw new Error(`unbalanced TOML delimiter '${char}'`);
11341
+ }
11342
+ }
11343
+ }
11344
+ function parseTomlHeader(rawLine) {
11345
+ const line = stripTomlComment(rawLine).trim();
11346
+ const array = line.startsWith("[[") && line.endsWith("]]");
11347
+ const table = !array && line.startsWith("[") && line.endsWith("]");
11348
+ if (!array && !table) return null;
11349
+ const prefix = array ? 2 : 1;
11350
+ const suffix = array ? 2 : 1;
11351
+ const rawName = line.slice(prefix, line.length - suffix).trim();
11352
+ if (!rawName) throw new Error("empty TOML table name");
11353
+ return { name: normalizeTomlKey(rawName), array };
11354
+ }
11355
+ function findTomlEquals(line) {
11356
+ let quote = null;
11357
+ let escaped = false;
11358
+ for (let i = 0; i < line.length; i += 1) {
11359
+ const char = line[i];
11360
+ if (quote === "basic") {
11361
+ if (escaped) escaped = false;
11362
+ else if (char === "\\") escaped = true;
11363
+ else if (char === '"') quote = null;
11364
+ continue;
11365
+ }
11366
+ if (quote === "literal") {
11367
+ if (char === "'") quote = null;
11368
+ continue;
11369
+ }
11370
+ if (char === '"') quote = "basic";
11371
+ else if (char === "'") quote = "literal";
11372
+ else if (char === "=") return i;
11373
+ }
11374
+ return -1;
11375
+ }
11376
+ function arrayTableContext(name, arrays) {
11377
+ let best = null;
11378
+ for (let i = arrays.length - 1; i >= 0; i -= 1) {
11379
+ const candidate = arrays[i];
11380
+ if (isTomlKeyPrefix(candidate.key, name)) {
11381
+ if (!best || candidate.key.length > best.key.length) best = candidate;
11382
+ }
11383
+ }
11384
+ return best;
11385
+ }
11386
+ function tomlTableIdentity(key, context) {
11387
+ return `${context?.identity ?? "root"}:${JSON.stringify(key)}`;
11388
+ }
11389
+ function isTomlScalar(value) {
11390
+ const decimal = "[+-]?(?:0|[1-9](?:_?[0-9])*)(?:\\.[0-9](?:_?[0-9])*)?(?:[eE][+-]?[0-9](?:_?[0-9])*)?";
11391
+ const number = new RegExp(
11392
+ `^(?:${decimal}|[+-]?0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[+-]?0o[0-7](?:_?[0-7])*|[+-]?0b[01](?:_?[01])*|[+-]?(?:inf|nan))$`
11393
+ );
11394
+ 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})?)?)$/;
11395
+ return number.test(value) || datetime.test(value) || /^(?:true|false)$/.test(value);
11396
+ }
11397
+ function validateTomlValueShape(value) {
11398
+ const token = stripTomlComment(value).trim();
11399
+ if (!token) throw new Error("empty TOML value");
11400
+ if (token.startsWith('"') || token.startsWith("'")) {
11401
+ const quote = token[0];
11402
+ if (quote && token.endsWith(quote)) return;
11403
+ }
11404
+ if (token.startsWith("[") && token.endsWith("]")) return;
11405
+ if (token.startsWith("{") && token.endsWith("}")) return;
11406
+ if (isTomlScalar(token)) return;
11407
+ throw new Error(`invalid TOML value '${token}'`);
11408
+ }
11409
+ function validateToml(content) {
11410
+ const tables = /* @__PURE__ */ new Set();
11411
+ const keys = /* @__PURE__ */ new Set();
11412
+ const arrayInstances = [];
11413
+ let currentTable = { identity: "root" };
11414
+ let valueState = {
11415
+ containers: [],
11416
+ quote: null,
11417
+ escaped: false
11418
+ };
11419
+ for (const rawLine of content.replace(/\r\n/g, "\n").split("\n")) {
11420
+ if (valueState.quote || valueState.containers.length > 0) {
11421
+ scanTomlValue(rawLine, valueState);
11422
+ continue;
11423
+ }
11424
+ const line = stripTomlComment(rawLine).trim();
11425
+ if (!line) continue;
11426
+ const header = parseTomlHeader(rawLine);
11427
+ if (header) {
11428
+ if (header.array) {
11429
+ const context = arrayTableContext(header.name, arrayInstances);
11430
+ const parentIdentity = context?.identity ?? null;
11431
+ const previous = [...arrayInstances].reverse().find(
11432
+ (candidate) => sameTomlKey(candidate.key, header.name) && candidate.parentIdentity === parentIdentity
11433
+ );
11434
+ const count = (previous?.count ?? 0) + 1;
11435
+ const instance = {
11436
+ key: header.name,
11437
+ parentIdentity,
11438
+ identity: `${parentIdentity ?? "root"}:${JSON.stringify(header.name)}#${count}`,
11439
+ count
11440
+ };
11441
+ arrayInstances.push(instance);
11442
+ currentTable = { identity: instance.identity };
11443
+ } else {
11444
+ const context = arrayTableContext(header.name, arrayInstances);
11445
+ currentTable = {
11446
+ identity: tomlTableIdentity(header.name, context)
11447
+ };
11448
+ if (tables.has(currentTable.identity))
11449
+ throw new Error(
11450
+ `duplicate TOML table '${tomlKeyLabel(header.name)}'`
11451
+ );
11452
+ tables.add(currentTable.identity);
11453
+ }
11454
+ continue;
11455
+ }
11456
+ const equals = findTomlEquals(line);
11457
+ if (equals <= 0) throw new Error(`invalid TOML line '${rawLine}'`);
11458
+ const key = normalizeTomlKey(line.slice(0, equals).trim());
11459
+ const value = line.slice(equals + 1).trim();
11460
+ if (!value || value.startsWith("#"))
11461
+ throw new Error(`empty TOML value for '${tomlKeyLabel(key)}'`);
11462
+ const fullKey = `${currentTable.identity}\0${JSON.stringify(key)}`;
11463
+ if (keys.has(fullKey))
11464
+ throw new Error(`duplicate TOML key '${tomlKeyLabel(key)}'`);
11465
+ keys.add(fullKey);
11466
+ scanTomlValue(line.slice(equals + 1), valueState);
11467
+ if (valueState.quote === "basic" || valueState.quote === "literal")
11468
+ throw new Error("unterminated TOML string");
11469
+ if (!valueState.quote && valueState.containers.length === 0)
11470
+ validateTomlValueShape(value);
11471
+ }
11472
+ if (valueState.quote) throw new Error("unterminated TOML string");
11473
+ if (valueState.containers.length > 0)
11474
+ throw new Error("unbalanced TOML value");
11475
+ }
11476
+ function isTomlTableHeader(line) {
11477
+ try {
11478
+ return parseTomlHeader(line) !== null;
11479
+ } catch {
11480
+ return false;
11481
+ }
11482
+ }
11483
+ function replaceTomlTable(content, tableName, replacement) {
11484
+ const target = normalizeTomlKey(tableName);
11485
+ const lines = content.replace(/\r\n/g, "\n").split("\n");
11486
+ const replacementLines = replacement.trim().split("\n");
11487
+ const kept = [];
11488
+ let i = 0;
11489
+ while (i < lines.length) {
11490
+ const header = parseTomlHeader(lines[i]);
11491
+ if (header && (sameTomlKey(header.name, target) || isTomlKeyPrefix(target, header.name))) {
11492
+ i += 1;
11493
+ while (i < lines.length && !isTomlTableHeader(lines[i])) i += 1;
11494
+ continue;
11495
+ }
11496
+ kept.push(lines[i]);
11497
+ i += 1;
11498
+ }
11499
+ const base = kept.join("\n").trim();
11500
+ return `${base.length > 0 ? `${base}
11501
+
11502
+ ` : ""}${replacementLines.join("\n")}
11503
+ `;
11504
+ }
11505
+ var defaultTomlFileOperations = {
11506
+ copyFileSync,
11507
+ renameSync: renameSync4
11508
+ };
11509
+ function writeTomlAtomic(path, content, fileOperations = {}) {
11510
+ ensureDir2(path);
11511
+ const operations = { ...defaultTomlFileOperations, ...fileOperations };
11512
+ const temporary = `${path}.sechroom-${process.pid}-${randomUUID3()}.tmp`;
11513
+ const backup = `${path}.bak`;
11514
+ const backupTemporary = `${backup}.sechroom-${process.pid}-${randomUUID3()}.tmp`;
11515
+ const mode = existsSync13(path) ? statSync5(path).mode & 4095 : 384;
11516
+ try {
11517
+ writeFileSync15(temporary, content, { mode });
11518
+ chmodSync2(temporary, mode);
11519
+ validateToml(readFileSync16(temporary, "utf8"));
11520
+ if (existsSync13(path) && !existsSync13(backup)) {
11521
+ operations.copyFileSync(path, backupTemporary);
11522
+ chmodSync2(backupTemporary, mode);
11523
+ validateToml(readFileSync16(backupTemporary, "utf8"));
11524
+ operations.renameSync(backupTemporary, backup);
11525
+ }
11526
+ operations.renameSync(temporary, path);
11527
+ } finally {
11528
+ rmSync7(temporary, { force: true });
11529
+ rmSync7(backupTemporary, { force: true });
11530
+ }
11531
+ }
11532
+ function mergeCodexToml(path, snippet, dryRun, fileOperations = {}) {
10812
11533
  const existed = existsSync13(path);
10813
- let body = readOr(path, "");
10814
- body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
10815
- const trimmed = body.trim();
10816
- const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
11534
+ const body = readOr(path, "");
11535
+ try {
11536
+ validateToml(body);
11537
+ } catch (error) {
11538
+ return {
11539
+ kind: "mcp",
11540
+ path,
11541
+ status: "skipped",
11542
+ note: `existing TOML is invalid \u2014 left untouched (${error.message})`
11543
+ };
11544
+ }
11545
+ const next = replaceTomlTable(body, CODEX_MCP_TABLE, snippet);
11546
+ try {
11547
+ validateToml(next);
11548
+ } catch (error) {
11549
+ return {
11550
+ kind: "mcp",
11551
+ path,
11552
+ status: "skipped",
11553
+ note: `generated TOML is invalid \u2014 left untouched (${error.message})`
11554
+ };
11555
+ }
10817
11556
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
10818
- ensureDir2(path);
10819
- writeFileSync15(path, next, { mode: 384 });
11557
+ if (next === body)
11558
+ return { kind: "mcp", path, status: existed ? "current" : "created" };
11559
+ try {
11560
+ writeTomlAtomic(path, next, fileOperations);
11561
+ } catch (error) {
11562
+ return {
11563
+ kind: "mcp",
11564
+ path,
11565
+ status: "skipped",
11566
+ note: `could not replace TOML \u2014 left untouched (${error.message})`
11567
+ };
11568
+ }
10820
11569
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
10821
11570
  }
10822
11571
  function writeInstructionBlock(path, write, dryRun) {
10823
11572
  const existed = existsSync13(path);
10824
11573
  const next = computeBlockFile(readOr(path, ""), write);
10825
- if (dryRun) return { kind: "instruction", path, status: "dry-run", block: write.block };
11574
+ if (dryRun)
11575
+ return { kind: "instruction", path, status: "dry-run", block: write.block };
10826
11576
  ensureDir2(path);
10827
11577
  writeFileSync15(path, next);
10828
- return { kind: "instruction", path, status: existed ? "merged" : "created", block: write.block };
11578
+ return {
11579
+ kind: "instruction",
11580
+ path,
11581
+ status: existed ? "merged" : "created",
11582
+ block: write.block
11583
+ };
10829
11584
  }
10830
11585
  function computeBlockFile(current, write) {
10831
11586
  const rendered = renderBlock(write);
@@ -10874,7 +11629,13 @@ function applyBlock(path, write, mode, dryRun) {
10874
11629
  };
10875
11630
  }
10876
11631
  if (state === "current") {
10877
- return { kind: "instruction", path, status: "current", eval: "current", block: write.block };
11632
+ return {
11633
+ kind: "instruction",
11634
+ path,
11635
+ status: "current",
11636
+ eval: "current",
11637
+ block: write.block
11638
+ };
10878
11639
  }
10879
11640
  if (state === "drift" && mode !== "force") {
10880
11641
  const proposedPath = `${path}.proposed`;
@@ -10909,7 +11670,12 @@ async function applyClient(cfg, setup, target, opts) {
10909
11670
  const section = findSection(surface, target.mcp.sectionType);
10910
11671
  const snippet = sectionSnippet(section);
10911
11672
  if (!snippet) {
10912
- actions.push({ kind: "mcp", path: target.mcp.path, status: "skipped", note: `no ${target.mcp.sectionType} section on surface '${target.mcp.surfaceKey}'` });
11673
+ actions.push({
11674
+ kind: "mcp",
11675
+ path: target.mcp.path,
11676
+ status: "skipped",
11677
+ note: `no ${target.mcp.sectionType} section on surface '${target.mcp.surfaceKey}'`
11678
+ });
10913
11679
  } else {
10914
11680
  actions.push(
10915
11681
  target.mcp.format === "toml" ? mergeCodexToml(target.mcp.path, snippet, dryRun) : mergeMcpJson(target.mcp.path, snippet, dryRun)
@@ -10920,25 +11686,44 @@ async function applyClient(cfg, setup, target, opts) {
10920
11686
  const surface = findSurface(setup, target.instruction.surfaceKey);
10921
11687
  const section = findSection(surface, SectionType.InstructionFile);
10922
11688
  if (!section) {
10923
- actions.push({ kind: "instruction", path: target.instruction.path, status: "skipped", note: `no instruction-file section on surface '${target.instruction.surfaceKey}'` });
11689
+ actions.push({
11690
+ kind: "instruction",
11691
+ path: target.instruction.path,
11692
+ status: "skipped",
11693
+ note: `no instruction-file section on surface '${target.instruction.surfaceKey}'`
11694
+ });
10924
11695
  } else {
10925
11696
  const resolved = await withSpinner(
10926
11697
  `Resolving ${target.label} agent instructions`,
10927
11698
  () => resolveInstruction(cfg, section, opts.personalWorkspaceId)
10928
11699
  );
10929
11700
  if (!resolved) {
10930
- actions.push({ kind: "instruction", path: target.instruction.path, status: "skipped", note: "no role template found in this tenant \u2014 install the SEM Starter bundle, then re-run `sechroom setup agent-files`" });
11701
+ actions.push({
11702
+ kind: "instruction",
11703
+ path: target.instruction.path,
11704
+ status: "skipped",
11705
+ note: "no role template found in this tenant \u2014 install the SEM Starter bundle, then re-run `sechroom setup agent-files`"
11706
+ });
10931
11707
  } else {
10932
11708
  const action = applyBlock(
10933
11709
  target.instruction.path,
10934
- { block: "role-template", body: resolved.body, source: resolved.sourceRef },
11710
+ {
11711
+ block: "role-template",
11712
+ body: resolved.body,
11713
+ source: resolved.sourceRef
11714
+ },
10935
11715
  mode,
10936
11716
  opts.dryRun
10937
11717
  );
10938
- actions.push(resolved.source === "override" && action.status !== "current" ? { ...action, note: action.note ?? "your personal copy" } : action);
11718
+ actions.push(
11719
+ resolved.source === "override" && action.status !== "current" ? { ...action, note: action.note ?? "your personal copy" } : action
11720
+ );
10939
11721
  }
10940
11722
  }
10941
- const conventionsSection = findSection(surface, SectionType.WorkspaceConventions);
11723
+ const conventionsSection = findSection(
11724
+ surface,
11725
+ SectionType.WorkspaceConventions
11726
+ );
10942
11727
  if (conventionsSection) {
10943
11728
  const conventions = await withSpinner(
10944
11729
  `Composing ${target.label} workspace conventions`,
@@ -10947,11 +11732,20 @@ async function applyClient(cfg, setup, target, opts) {
10947
11732
  if (conventions) {
10948
11733
  const action = applyBlock(
10949
11734
  target.instruction.path,
10950
- { block: "workspace-conventions", body: conventions.body, source: `workspace:${cfg.workspaceId ?? ""}` },
11735
+ {
11736
+ block: "workspace-conventions",
11737
+ body: conventions.body,
11738
+ source: `workspace:${cfg.workspaceId ?? ""}`
11739
+ },
10951
11740
  mode,
10952
11741
  opts.dryRun
10953
11742
  );
10954
- actions.push(action.status === "current" ? action : { ...action, note: action.note ?? `workspace conventions (${conventions.refs.length})` });
11743
+ actions.push(
11744
+ action.status === "current" ? action : {
11745
+ ...action,
11746
+ note: action.note ?? `workspace conventions (${conventions.refs.length})`
11747
+ }
11748
+ );
10955
11749
  }
10956
11750
  }
10957
11751
  }
@@ -11169,7 +11963,12 @@ function buildConventionDraft(title, rawKind, rawBody) {
11169
11963
 
11170
11964
  ${body}
11171
11965
  `,
11172
- tags: ["agent-setup-bundle", "scope:sechroom", `kind:${kind}`, "archetype:document"]
11966
+ tags: [
11967
+ "agent-setup-bundle",
11968
+ "scope:sechroom",
11969
+ `kind:${kind}`,
11970
+ "archetype:document"
11971
+ ]
11173
11972
  };
11174
11973
  }
11175
11974
  function copyChoice(opts) {
@@ -11182,9 +11981,16 @@ async function maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId,
11182
11981
  const instr = targets[key]?.instruction;
11183
11982
  if (!instr || seen.has(instr.surfaceKey)) continue;
11184
11983
  seen.add(instr.surfaceKey);
11185
- const section = findSection(findSurface(setup, instr.surfaceKey), SectionType.InstructionFile);
11984
+ const section = findSection(
11985
+ findSurface(setup, instr.surfaceKey),
11986
+ SectionType.InstructionFile
11987
+ );
11186
11988
  if (!section) continue;
11187
- const resolved = await resolveInstruction(cfg, section, personalWorkspaceId);
11989
+ const resolved = await resolveInstruction(
11990
+ cfg,
11991
+ section,
11992
+ personalWorkspaceId
11993
+ );
11188
11994
  if (!resolved || resolved.source === "override") continue;
11189
11995
  let make = choice === "yes";
11190
11996
  if (choice === "ask") {
@@ -11199,8 +12005,10 @@ version, the shared template stays clean, and you can discard back anytime.
11199
12005
  }
11200
12006
  if (make) {
11201
12007
  await createOverride(cfg, resolved, personalWorkspaceId);
11202
- process.stderr.write(`\u2713 personal copy created for ${instr.surfaceKey} \u2014 edit it on the Agent setup page or via the API.
11203
- `);
12008
+ process.stderr.write(
12009
+ `\u2713 personal copy created for ${instr.surfaceKey} \u2014 edit it on the Agent setup page or via the API.
12010
+ `
12011
+ );
11204
12012
  }
11205
12013
  }
11206
12014
  }
@@ -11210,7 +12018,9 @@ function resolveClientKeys(raw) {
11210
12018
  if (tokens.includes("all")) return [...ALL_CLIENT_KEYS];
11211
12019
  for (const k of tokens) {
11212
12020
  if (!targets[k]) {
11213
- fail(`unknown client '${k}'. Known: ${ALL_CLIENT_KEYS.join(", ")}, or 'all'.`);
12021
+ fail(
12022
+ `unknown client '${k}'. Known: ${ALL_CLIENT_KEYS.join(", ")}, or 'all'.`
12023
+ );
11214
12024
  }
11215
12025
  }
11216
12026
  return [...new Set(tokens)];
@@ -11221,8 +12031,10 @@ ${client.label} (${client.key}):
11221
12031
  `);
11222
12032
  for (const a of actions) {
11223
12033
  const tag = a.status === "skipped" ? "skip" : a.status;
11224
- process.stdout.write(` [${tag}] ${a.kind}: ${a.path}${a.note ? ` \u2014 ${a.note}` : ""}
11225
- `);
12034
+ process.stdout.write(
12035
+ ` [${tag}] ${a.kind}: ${a.path}${a.note ? ` \u2014 ${a.note}` : ""}
12036
+ `
12037
+ );
11226
12038
  }
11227
12039
  }
11228
12040
  function resolveEvalMode(opts) {
@@ -11296,8 +12108,10 @@ function summarizeEval(result, mode, json, dryRun) {
11296
12108
  const { eval: counts } = buildCheckReport(result);
11297
12109
  if (json) return;
11298
12110
  if (!dryRun && counts.stale) {
11299
- process.stderr.write(`\u21BB refreshed ${counts.stale} section(s) the server had moved
11300
- `);
12111
+ process.stderr.write(
12112
+ `\u21BB refreshed ${counts.stale} section(s) the server had moved
12113
+ `
12114
+ );
11301
12115
  }
11302
12116
  if (!dryRun && counts.drift) {
11303
12117
  process.stderr.write(
@@ -11328,7 +12142,39 @@ async function resolveNamespaceChoice(cfg, flag) {
11328
12142
  return picked === GLOBAL_NAMESPACE ? null : picked;
11329
12143
  }
11330
12144
  function registerInit(program2) {
11331
- program2.command("init").description("Wire this project for sechroom: write MCP config + agent instruction files from the server's setup descriptors").option("--client <list...>", `clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all'`, DEFAULT_CLIENT_KEY).option("--scope <scope>", "install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default global", "global").option("--dry-run", "print what would be written without writing", false).option("--mcp-only", "only write MCP config (skip agent files)", false).option("--agent-files-only", "only write agent instruction files (skip MCP config)", false).option("--copy", "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)").option("--namespace <slug>", "MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)").option("--refresh", "refresh out-of-date agent-file blocks in place (local edits preserved to .proposed)", false).option("--force", "rewrite agent-file managed blocks, overwriting local edits inside the markers", false).option("--check", "report whether agent files would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing", false).addHelpText(
12145
+ program2.command("init").description(
12146
+ "Wire this project for sechroom: write MCP config + agent instruction files from the server's setup descriptors"
12147
+ ).option(
12148
+ "--client <list...>",
12149
+ `clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all'`,
12150
+ DEFAULT_CLIENT_KEY
12151
+ ).option(
12152
+ "--scope <scope>",
12153
+ "install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default global",
12154
+ "global"
12155
+ ).option("--dry-run", "print what would be written without writing", false).option("--mcp-only", "only write MCP config (skip agent files)", false).option(
12156
+ "--agent-files-only",
12157
+ "only write agent instruction files (skip MCP config)",
12158
+ false
12159
+ ).option(
12160
+ "--copy",
12161
+ "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)"
12162
+ ).option(
12163
+ "--namespace <slug>",
12164
+ "MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)"
12165
+ ).option(
12166
+ "--refresh",
12167
+ "refresh out-of-date agent-file blocks in place (local edits preserved to .proposed)",
12168
+ false
12169
+ ).option(
12170
+ "--force",
12171
+ "rewrite agent-file managed blocks, overwriting local edits inside the markers",
12172
+ false
12173
+ ).option(
12174
+ "--check",
12175
+ "report whether agent files would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
12176
+ false
12177
+ ).addHelpText(
11332
12178
  "after",
11333
12179
  `
11334
12180
  Examples:
@@ -11343,7 +12189,9 @@ Examples:
11343
12189
  const mode = resolveEvalMode(opts);
11344
12190
  const check = mode === "check";
11345
12191
  if (check && opts.mcpOnly) {
11346
- fail("--check inspects agent files and cannot be combined with --mcp-only.");
12192
+ fail(
12193
+ "--check inspects agent files and cannot be combined with --mcp-only."
12194
+ );
11347
12195
  }
11348
12196
  const namespaceSlug = await resolveNamespaceChoice(cfg, opts.namespace);
11349
12197
  const setup = await withSpinner(
@@ -11357,14 +12205,28 @@ Examples:
11357
12205
  } catch (err2) {
11358
12206
  return fail(err2.message);
11359
12207
  }
11360
- const claudeTargets = resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd: process.cwd() });
12208
+ const claudeTargets = resolveClaudeTargets({
12209
+ override: g.claudeConfigDir,
12210
+ scope,
12211
+ cwd: process.cwd()
12212
+ });
11361
12213
  const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
11362
- const targets = clientTargets(process.cwd(), { claudeDir: claudeTargets[0]?.dir, codexHome: codexHomes[0] });
12214
+ const targets = clientTargets(process.cwd(), {
12215
+ claudeDir: claudeTargets[0]?.dir,
12216
+ codexHome: codexHomes[0] ?? null
12217
+ });
11363
12218
  const keys = resolveClientKeys(opts.client);
11364
12219
  const json = g.json;
11365
12220
  const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
11366
12221
  if (!opts.dryRun && !opts.mcpOnly && !check) {
11367
- await maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, copyChoice(opts));
12222
+ await maybeOfferCopies(
12223
+ cfg,
12224
+ setup,
12225
+ targets,
12226
+ keys,
12227
+ personalWorkspaceId,
12228
+ copyChoice(opts)
12229
+ );
11368
12230
  }
11369
12231
  const result = [];
11370
12232
  for (const key of keys) {
@@ -11382,18 +12244,33 @@ Examples:
11382
12244
  summarizeEval(result, mode, Boolean(json), Boolean(opts.dryRun));
11383
12245
  if (!json && !opts.dryRun && !opts.mcpOnly && !check) {
11384
12246
  for (const t of claudeTargets) {
11385
- await maybeOfferSkills(cfg, personalWorkspaceId, { yes: false, dryRun: Boolean(opts.dryRun), surface: "claude-code", configDir: t.dir });
12247
+ await maybeOfferSkills(cfg, personalWorkspaceId, {
12248
+ yes: false,
12249
+ dryRun: Boolean(opts.dryRun),
12250
+ surface: "claude-code",
12251
+ configDir: t.dir
12252
+ });
11386
12253
  }
11387
12254
  }
11388
12255
  if (!json && !opts.dryRun && !opts.mcpOnly && !check) {
11389
- await maybeOfferHooks({ yes: false, dryRun: Boolean(opts.dryRun), cwd: process.cwd(), scope, claudeConfigDir: g.claudeConfigDir, codexHome: g.codexHome });
12256
+ await maybeOfferHooks({
12257
+ yes: false,
12258
+ dryRun: Boolean(opts.dryRun),
12259
+ cwd: process.cwd(),
12260
+ scope,
12261
+ claudeConfigDir: g.claudeConfigDir,
12262
+ codexHome: g.codexHome
12263
+ });
11390
12264
  }
11391
12265
  if (json) {
11392
12266
  emit({ dryRun: Boolean(opts.dryRun), clients: result }, true);
11393
12267
  return;
11394
12268
  }
11395
12269
  const first = targets[keys[0]];
11396
- const surface = findSurface(setup, first.mcp?.surfaceKey ?? first.instruction?.surfaceKey ?? "");
12270
+ const surface = findSurface(
12271
+ setup,
12272
+ first.mcp?.surfaceKey ?? first.instruction?.surfaceKey ?? ""
12273
+ );
11397
12274
  const verify = findSection(surface, SectionType.Verify);
11398
12275
  if (verify?.description) {
11399
12276
  process.stdout.write(`
@@ -11407,13 +12284,64 @@ Next \u2014 verify: ${verify.description}
11407
12284
  }
11408
12285
  function registerSetup(program2, deps = {}) {
11409
12286
  const setup = program2.command("setup").description("Granular onboarding steps (init runs these together)");
11410
- setup.command("mcp <clients...>").description(`Write only the MCP config for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`).option("--dry-run", "print what would be written without writing", false).option("--namespace <slug>", "MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)").addHelpText("after", "\nExamples:\n $ sechroom setup mcp codex\n $ sechroom setup mcp claude-code codex\n $ sechroom setup mcp all").action(async (clients, opts, cmd) => {
11411
- await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: true, agentFiles: false, namespace: opts.namespace });
11412
- });
11413
- setup.command("agent-files <clients...>").description(`Write only the agent instruction file(s) for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`).option("--dry-run", "print what would be written without writing", false).option("--copy", "make a personal copy you can edit (default: prompt on a TTY, else skip)").option("--refresh", "refresh out-of-date blocks in place (local edits preserved to .proposed)", false).option("--force", "rewrite managed blocks, overwriting local edits inside the markers", false).option("--check", "report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing", false).addHelpText("after", "\nExamples:\n $ sechroom setup agent-files claude-code CLAUDE.md\n $ sechroom setup agent-files claude-code codex CLAUDE.md + AGENTS.md in one run\n $ sechroom setup agent-files all --check CI gate: nonzero exit if out of date\n $ sechroom setup agent-files claude-code --force overwrite local edits in the managed block").action(async (clients, opts, cmd) => {
11414
- await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: false, agentFiles: true, copy: opts.copy, mode: resolveEvalMode(opts) });
12287
+ setup.command("mcp <clients...>").description(
12288
+ `Write only the MCP config for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`
12289
+ ).option("--dry-run", "print what would be written without writing", false).option(
12290
+ "--namespace <slug>",
12291
+ "MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)"
12292
+ ).addHelpText(
12293
+ "after",
12294
+ "\nExamples:\n $ sechroom setup mcp codex\n $ sechroom setup mcp claude-code codex\n $ sechroom setup mcp all"
12295
+ ).action(async (clients, opts, cmd) => {
12296
+ await runClients(clients, cmd, {
12297
+ dryRun: Boolean(opts.dryRun),
12298
+ mcp: true,
12299
+ agentFiles: false,
12300
+ namespace: opts.namespace
12301
+ });
12302
+ });
12303
+ setup.command("agent-files <clients...>").description(
12304
+ `Write only the agent instruction file(s) for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`
12305
+ ).option("--dry-run", "print what would be written without writing", false).option(
12306
+ "--copy",
12307
+ "make a personal copy you can edit (default: prompt on a TTY, else skip)"
12308
+ ).option(
12309
+ "--refresh",
12310
+ "refresh out-of-date blocks in place (local edits preserved to .proposed)",
12311
+ false
12312
+ ).option(
12313
+ "--force",
12314
+ "rewrite managed blocks, overwriting local edits inside the markers",
12315
+ false
12316
+ ).option(
12317
+ "--check",
12318
+ "report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
12319
+ false
12320
+ ).addHelpText(
12321
+ "after",
12322
+ "\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"
12323
+ ).action(async (clients, opts, cmd) => {
12324
+ await runClients(clients, cmd, {
12325
+ dryRun: Boolean(opts.dryRun),
12326
+ mcp: false,
12327
+ agentFiles: true,
12328
+ copy: opts.copy,
12329
+ mode: resolveEvalMode(opts)
12330
+ });
11415
12331
  });
11416
- setup.command("new-convention <title...>").description("Scaffold a workspace-conventions section: author a correctly-tagged memo (header as first body line) + regen the agent files").option("--kind <kind>", "reference | standard (orders the section; reference first)", "reference").option("--workspace <id>", "workspace to author in (default: the bound workspace)").option("--body <markdown>", "section body (default: a TODO scaffold to edit later)").option("--no-regen", "skip the agent-files regen after authoring").option("--dry-run", "print what would be authored; write nothing", false).addHelpText(
12332
+ setup.command("new-convention <title...>").description(
12333
+ "Scaffold a workspace-conventions section: author a correctly-tagged memo (header as first body line) + regen the agent files"
12334
+ ).option(
12335
+ "--kind <kind>",
12336
+ "reference | standard (orders the section; reference first)",
12337
+ "reference"
12338
+ ).option(
12339
+ "--workspace <id>",
12340
+ "workspace to author in (default: the bound workspace)"
12341
+ ).option(
12342
+ "--body <markdown>",
12343
+ "section body (default: a TODO scaffold to edit later)"
12344
+ ).option("--no-regen", "skip the agent-files regen after authoring").option("--dry-run", "print what would be authored; write nothing", false).addHelpText(
11417
12345
  "after",
11418
12346
  `
11419
12347
  The memo carries the two conventions a workspace-conventions section needs (FR-sechroom-236):
@@ -11429,10 +12357,15 @@ Examples:
11429
12357
  const cfg = resolveConfig(cmd.optsWithGlobals());
11430
12358
  const json = Boolean(cmd.optsWithGlobals().json);
11431
12359
  const title = titleParts.join(" ").trim();
11432
- if (!title) fail('a section title is required, e.g. `sechroom setup new-convention "Deploy runbook"`.');
12360
+ if (!title)
12361
+ fail(
12362
+ 'a section title is required, e.g. `sechroom setup new-convention "Deploy runbook"`.'
12363
+ );
11433
12364
  const workspaceId = opts.workspace ?? cfg.workspaceId;
11434
12365
  if (!workspaceId)
11435
- fail("no workspace \u2014 pass --workspace <id> or bind one (`sechroom config set --local workspaceId <id>`).");
12366
+ fail(
12367
+ "no workspace \u2014 pass --workspace <id> or bind one (`sechroom config set --local workspaceId <id>`)."
12368
+ );
11436
12369
  const draft = buildConventionDraft(title, opts.kind, opts.body);
11437
12370
  if (opts.dryRun) {
11438
12371
  emit(
@@ -11473,7 +12406,10 @@ Examples:
11473
12406
  }
11474
12407
  if (opts.regen === false) {
11475
12408
  if (json) emit({ id: data.id, workspaceId, regen: false }, true);
11476
- else process.stdout.write("Skipped regen (--no-regen). Run `sechroom setup agent-files all` to apply.\n");
12409
+ else
12410
+ process.stdout.write(
12411
+ "Skipped regen (--no-regen). Run `sechroom setup agent-files all` to apply.\n"
12412
+ );
11477
12413
  return;
11478
12414
  }
11479
12415
  const regenerate = deps.regenerateConvention ?? runClients;
@@ -11502,7 +12438,14 @@ async function runClients(clients, cmd, opts) {
11502
12438
  );
11503
12439
  const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
11504
12440
  if (opts.agentFiles && !opts.dryRun && !check) {
11505
- await maybeOfferCopies(cfg, setupData, targets, keys, personalWorkspaceId, copyChoice(opts));
12441
+ await maybeOfferCopies(
12442
+ cfg,
12443
+ setupData,
12444
+ targets,
12445
+ keys,
12446
+ personalWorkspaceId,
12447
+ copyChoice(opts)
12448
+ );
11506
12449
  }
11507
12450
  const json = g.json;
11508
12451
  const result = [];
@@ -11523,7 +12466,9 @@ async function runClients(clients, cmd, opts) {
11523
12466
  emit({ dryRun: opts.dryRun, clients: result }, true);
11524
12467
  return;
11525
12468
  }
11526
- process.stdout.write(opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n");
12469
+ process.stdout.write(
12470
+ opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n"
12471
+ );
11527
12472
  }
11528
12473
 
11529
12474
  // src/commands/namespace.ts
@@ -11599,7 +12544,7 @@ import { basename as basename5, join as join21 } from "path";
11599
12544
 
11600
12545
  // src/commands/fanout.ts
11601
12546
  import { spawnSync } from "child_process";
11602
- import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
12547
+ import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync4, statSync as statSync6 } from "fs";
11603
12548
  import { isAbsolute as isAbsolute3, join as join20, resolve as resolve7 } from "path";
11604
12549
  var ICON = {
11605
12550
  refresh: "\u21BB",
@@ -11622,7 +12567,7 @@ function discoverChildren(root) {
11622
12567
  if (name.startsWith(".") || name === "node_modules") continue;
11623
12568
  const dir = join20(root, name);
11624
12569
  try {
11625
- if (!statSync5(dir).isDirectory()) continue;
12570
+ if (!statSync6(dir).isDirectory()) continue;
11626
12571
  } catch {
11627
12572
  continue;
11628
12573
  }
@@ -11748,20 +12693,27 @@ function resolveBaseUrl(g) {
11748
12693
  return baseUrl.replace(/\/$/, "");
11749
12694
  }
11750
12695
  async function fetchWorkspaces(client) {
11751
- const { data, error } = await client.GET("/workspaces", { params: { query: { includeArchived: false } } });
11752
- if (error) throw new Error(`Couldn't list your workspaces: ${JSON.stringify(error)}`);
12696
+ const { data, error } = await client.GET("/workspaces", {
12697
+ params: { query: { includeArchived: false } }
12698
+ });
12699
+ if (error)
12700
+ throw new Error(`Couldn't list your workspaces: ${JSON.stringify(error)}`);
11753
12701
  const rows = data ?? [];
11754
12702
  return rows.map((r) => r.item ?? r).filter((w) => Boolean(w?.id && w?.name)).map((w) => ({ id: w.id, name: w.name, parentId: w.parentId ?? null }));
11755
12703
  }
11756
12704
  async function lookupWorkspace(client, id) {
11757
- const { data, error } = await client.GET("/workspaces/{workspaceId}", { params: { path: { workspaceId: id } } });
12705
+ const { data, error } = await client.GET("/workspaces/{workspaceId}", {
12706
+ params: { path: { workspaceId: id } }
12707
+ });
11758
12708
  if (error) return null;
11759
12709
  const env = data;
11760
12710
  const w = env?.item ?? env;
11761
12711
  return w?.id ? { id: w.id, name: w.name ?? id, parentId: w.parentId ?? null } : null;
11762
12712
  }
11763
12713
  async function warnIfProjectStray(client, projectId, workspaceId, json) {
11764
- const { data, error } = await client.GET("/projects/{projectId}", { params: { path: { projectId } } });
12714
+ const { data, error } = await client.GET("/projects/{projectId}", {
12715
+ params: { path: { projectId } }
12716
+ });
11765
12717
  if (error) return;
11766
12718
  const env = data;
11767
12719
  const owner = env?.item?.workspaceId;
@@ -11805,10 +12757,15 @@ function personalSubtreeIds(personalId, all) {
11805
12757
  async function pickWorkspace(client, opts = {}) {
11806
12758
  const promptLabel = opts.promptLabel ?? "Bind this directory to a workspace:";
11807
12759
  const dirName = opts.dirName ?? basename5(process.cwd());
11808
- const all = await withSpinner("Listing your workspaces", () => fetchWorkspaces(client));
12760
+ const all = await withSpinner(
12761
+ "Listing your workspaces",
12762
+ () => fetchWorkspaces(client)
12763
+ );
11809
12764
  if (all.length === 0) {
11810
- process.stderr.write(`no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
11811
- `);
12765
+ process.stderr.write(
12766
+ `no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
12767
+ `
12768
+ );
11812
12769
  return void 0;
11813
12770
  }
11814
12771
  const byId = new Map(all.map((w) => [w.id, w]));
@@ -11823,19 +12780,35 @@ async function pickWorkspace(client, opts = {}) {
11823
12780
  const matched = candidates.filter(isMatch).sort(byPath);
11824
12781
  const rest = candidates.filter((w) => !isMatch(w)).sort(byPath);
11825
12782
  const choices = [
11826
- ...matched.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: style.dim(`matches "${dirName}"`) })),
11827
- ...rest.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: w.id })),
11828
- { label: style.dim("skip \u2014 don't bind a workspace"), value: SKIP, hint: void 0 }
12783
+ ...matched.map((w) => ({
12784
+ label: workspacePath(w, byId),
12785
+ value: w.id,
12786
+ hint: style.dim(`matches "${dirName}"`)
12787
+ })),
12788
+ ...rest.map((w) => ({
12789
+ label: workspacePath(w, byId),
12790
+ value: w.id,
12791
+ hint: w.id
12792
+ })),
12793
+ {
12794
+ label: style.dim("skip \u2014 don't bind a workspace"),
12795
+ value: SKIP,
12796
+ hint: void 0
12797
+ }
11829
12798
  ];
11830
12799
  const defaultValue = matched.length === 1 ? matched[0].id : SKIP;
11831
12800
  const chosen = candidates.length > 12 ? await promptAutocomplete(promptLabel, choices, defaultValue) : await promptSelect(promptLabel, choices, defaultValue);
11832
12801
  if (chosen === SKIP) return void 0;
11833
12802
  const picked = byId.get(chosen);
11834
- const collisions = all.filter((w) => w.id !== picked.id && namesCollide(w.name, picked.name));
12803
+ const collisions = all.filter(
12804
+ (w) => w.id !== picked.id && namesCollide(w.name, picked.name)
12805
+ );
11835
12806
  if (collisions.length > 0) {
11836
12807
  process.stderr.write(
11837
12808
  `${warn("\u26A0")} ${collisions.length} other workspace(s) have a similar name to ${style.cyan(workspacePath(picked, byId))}:
11838
- ` + collisions.map((w) => ` ${style.dim(workspacePath(w, byId))} ${style.dim(`(${w.id})`)}`).join("\n") + `
12809
+ ` + collisions.map(
12810
+ (w) => ` ${style.dim(workspacePath(w, byId))} ${style.dim(`(${w.id})`)}`
12811
+ ).join("\n") + `
11839
12812
  You picked ${style.dim(picked.id)} \u2014 re-run \`sechroom config set --local workspaceId <id>\` if that's wrong.
11840
12813
  `
11841
12814
  );
@@ -11862,10 +12835,17 @@ async function ensureTenant(baseUrl, g, opts) {
11862
12835
  const local = readLocalConfig();
11863
12836
  let tenant = g.tenant ?? process.env.SECHROOM_TENANT ?? local.tenant ?? persisted.tenant ?? "";
11864
12837
  if (!tenant) {
11865
- const client = await makeClient({ baseUrl, tenant: "", account: resolveAccountAlias(), clientId: persisted.clientId });
12838
+ const client = await makeClient({
12839
+ baseUrl,
12840
+ tenant: "",
12841
+ account: resolveAccountAlias(),
12842
+ clientId: persisted.clientId
12843
+ });
11866
12844
  const { data, error } = await client.GET("/auth/me/tenants", {});
11867
12845
  if (error) {
11868
- fail(`Couldn't list your tenants: ${JSON.stringify(error)}. Pass --tenant <id> to skip this.`);
12846
+ fail(
12847
+ `Couldn't list your tenants: ${JSON.stringify(error)}. Pass --tenant <id> to skip this.`
12848
+ );
11869
12849
  }
11870
12850
  const tenants = data?.tenants ?? [];
11871
12851
  if (tenants.length === 0) {
@@ -11897,21 +12877,44 @@ async function ensureTenant(baseUrl, g, opts) {
11897
12877
  }
11898
12878
  }
11899
12879
  const existingWorkspace = local.workspaceId ?? persisted.workspaceId ?? void 0;
11900
- const wsClient = await makeClient({ baseUrl, tenant, account: resolveAccountAlias(), clientId: persisted.clientId });
11901
- const workspaceId = await resolveWorkspaceBinding(wsClient, existingWorkspace, {
11902
- yes: opts.yes,
11903
- json: opts.json,
11904
- workspace: opts.workspace
12880
+ const wsClient = await makeClient({
12881
+ baseUrl,
12882
+ tenant,
12883
+ account: resolveAccountAlias(),
12884
+ clientId: persisted.clientId
11905
12885
  });
12886
+ const workspaceId = await resolveWorkspaceBinding(
12887
+ wsClient,
12888
+ existingWorkspace,
12889
+ {
12890
+ yes: opts.yes,
12891
+ json: opts.json,
12892
+ workspace: opts.workspace
12893
+ }
12894
+ );
11906
12895
  const defaultProjectId = local.defaultProjectId ?? persisted.defaultProjectId ?? void 0;
11907
- if (defaultProjectId && workspaceId) await warnIfProjectStray(wsClient, defaultProjectId, workspaceId, opts.json);
12896
+ if (defaultProjectId && workspaceId)
12897
+ await warnIfProjectStray(
12898
+ wsClient,
12899
+ defaultProjectId,
12900
+ workspaceId,
12901
+ opts.json
12902
+ );
11908
12903
  let storeLocal = Boolean(opts.local);
11909
12904
  if (!opts.local && canPrompt() && !opts.yes) {
11910
12905
  storeLocal = await promptSelect(
11911
12906
  "Where should this tenant + base URL be saved?",
11912
12907
  [
11913
- { label: "Globally", value: "global", hint: "all projects on this machine" },
11914
- { label: "This directory", value: "local", hint: ".sechroom.json \u2014 committed, project + subdirs" }
12908
+ {
12909
+ label: "Globally",
12910
+ value: "global",
12911
+ hint: "all projects on this machine"
12912
+ },
12913
+ {
12914
+ label: "This directory",
12915
+ value: "local",
12916
+ hint: ".sechroom.json \u2014 committed, project + subdirs"
12917
+ }
11915
12918
  ],
11916
12919
  local.path ? "local" : "global"
11917
12920
  ) === "local";
@@ -11920,19 +12923,34 @@ async function ensureTenant(baseUrl, g, opts) {
11920
12923
  const patch = { baseUrl, tenant, ...workspaceId ? { workspaceId } : {} };
11921
12924
  if (storeLocal) {
11922
12925
  const path = writeLocalConfig(patch, { here: Boolean(opts.here) });
11923
- if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved to ${path} (directory-local)
11924
- `);
12926
+ if (!opts.json)
12927
+ process.stderr.write(
12928
+ `${ok("\u2713")} config saved to ${path} (directory-local)
12929
+ `
12930
+ );
11925
12931
  } else {
11926
12932
  writePersisted(patch);
11927
- if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved globally (~/.config/sechroom/config.json)
11928
- `);
12933
+ if (!opts.json)
12934
+ process.stderr.write(
12935
+ `${ok("\u2713")} config saved globally (~/.config/sechroom/config.json)
12936
+ `
12937
+ );
11929
12938
  }
11930
12939
  if (workspaceId && !existingWorkspace && !opts.json) {
11931
- process.stderr.write(`${ok("\u2713")} bound to workspace ${style.dim(workspaceId)}
11932
- `);
12940
+ process.stderr.write(
12941
+ `${ok("\u2713")} bound to workspace ${style.dim(workspaceId)}
12942
+ `
12943
+ );
11933
12944
  }
11934
12945
  }
11935
- return { baseUrl, tenant, account: resolveAccountAlias(), workspaceId, defaultProjectId, clientId: persisted.clientId };
12946
+ return {
12947
+ baseUrl,
12948
+ tenant,
12949
+ account: resolveAccountAlias(),
12950
+ workspaceId,
12951
+ defaultProjectId,
12952
+ clientId: persisted.clientId
12953
+ };
11936
12954
  }
11937
12955
  async function ensureAuth(cfg, yes) {
11938
12956
  if (process.env.SECHROOM_TOKEN) return;
@@ -11940,17 +12958,27 @@ async function ensureAuth(cfg, yes) {
11940
12958
  const usable = Boolean(cached?.accessToken) && (cached.expiresAt === void 0 || Date.now() < cached.expiresAt - 6e4 || Boolean(cached.refreshToken));
11941
12959
  if (usable) return;
11942
12960
  if (!canPrompt() || yes) {
11943
- fail("Not signed in. Run `sechroom login` first, or set SECHROOM_TOKEN for headless use.");
12961
+ fail(
12962
+ "Not signed in. Run `sechroom login` first, or set SECHROOM_TOKEN for headless use."
12963
+ );
11944
12964
  }
11945
- process.stderr.write("\nNot signed in \u2014 opening the browser to authenticate.\n");
12965
+ process.stderr.write(
12966
+ "\nNot signed in \u2014 opening the browser to authenticate.\n"
12967
+ );
11946
12968
  await login(cfg);
11947
12969
  }
11948
12970
  async function ensureTimezone(cfg, opts) {
11949
12971
  const client = await makeClient(cfg);
11950
12972
  const { data, error } = await client.GET("/me/profile", {});
11951
- if (error) return { timezone: null, action: "skipped", note: "could not read profile" };
12973
+ if (error)
12974
+ return {
12975
+ timezone: null,
12976
+ action: "skipped",
12977
+ note: "could not read profile"
12978
+ };
11952
12979
  const current = data?.effectiveTimezone;
11953
- if (current && current.trim().length > 0) return { timezone: current, action: "already-set" };
12980
+ if (current && current.trim().length > 0)
12981
+ return { timezone: current, action: "already-set" };
11954
12982
  const system = systemTimezone();
11955
12983
  let tz = system;
11956
12984
  if (canPrompt() && !opts.yes) {
@@ -11962,12 +12990,18 @@ async function ensureTimezone(cfg, opts) {
11962
12990
  note: "no timezone set \u2014 re-run interactively or pass --yes to adopt the system timezone"
11963
12991
  };
11964
12992
  }
11965
- if (!tz) return { timezone: null, action: "skipped", note: "no timezone provided" };
12993
+ if (!tz)
12994
+ return { timezone: null, action: "skipped", note: "no timezone provided" };
11966
12995
  if (opts.dryRun) return { timezone: tz, action: "dry-run" };
11967
12996
  const { error: putErr } = await client.PUT("/me/profile", {
11968
12997
  body: { displayName: null, photoUrl: null, bio: null, timezone: tz }
11969
12998
  });
11970
- if (putErr) return { timezone: tz, action: "skipped", note: `update failed: ${JSON.stringify(putErr)}` };
12999
+ if (putErr)
13000
+ return {
13001
+ timezone: tz,
13002
+ action: "skipped",
13003
+ note: `update failed: ${JSON.stringify(putErr)}`
13004
+ };
11971
13005
  return { timezone: tz, action: "set" };
11972
13006
  }
11973
13007
  async function chooseClients(clientFlag, yes, cwd) {
@@ -11992,7 +13026,11 @@ async function chooseScope(scopeFlag, yes) {
11992
13026
  return promptSelect(
11993
13027
  "Install skills, agents, and hooks globally or just for this project?",
11994
13028
  [
11995
- { label: "Globally", value: "global", hint: "~/.claude (or CLAUDE_CONFIG_DIR) \u2014 all projects" },
13029
+ {
13030
+ label: "Globally",
13031
+ value: "global",
13032
+ hint: "~/.claude (or CLAUDE_CONFIG_DIR) \u2014 all projects"
13033
+ },
11996
13034
  { label: "This project", value: "project", hint: "<repo>/.claude" }
11997
13035
  ],
11998
13036
  "global"
@@ -12001,7 +13039,13 @@ async function chooseScope(scopeFlag, yes) {
12001
13039
  async function planRecurseChild(entry, root, client, opts) {
12002
13040
  const dir = resolveChildDir(entry.path, root);
12003
13041
  if (!existsSync15(dir)) {
12004
- return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
13042
+ return {
13043
+ label: entry.path,
13044
+ dir,
13045
+ disposition: "skip-missing",
13046
+ argv: [],
13047
+ reason: "directory does not exist"
13048
+ };
12005
13049
  }
12006
13050
  if (existsSync15(join21(dir, ".sechroom.json"))) {
12007
13051
  return {
@@ -12022,20 +13066,40 @@ async function planRecurseChild(entry, root, client, opts) {
12022
13066
  };
12023
13067
  }
12024
13068
  if (opts.dryRun) {
12025
- return { label: entry.path, dir, disposition: "bind", argv: ["onboard", "--yes", "--local", "--workspace", "<prompt>"], reason: "unbound \u2014 would prompt for a workspace" };
13069
+ return {
13070
+ label: entry.path,
13071
+ dir,
13072
+ disposition: "bind",
13073
+ argv: ["onboard", "--yes", "--local", "--workspace", "<prompt>"],
13074
+ reason: "unbound \u2014 would prompt for a workspace"
13075
+ };
12026
13076
  }
12027
13077
  if (opts.yes || !canPrompt()) {
12028
- return { label: entry.path, dir, disposition: "skip-unbound", argv: [], reason: "unbound + no workspace (run interactively, or add it to ./.sechroom/repos.json)" };
13078
+ return {
13079
+ label: entry.path,
13080
+ dir,
13081
+ disposition: "skip-unbound",
13082
+ argv: [],
13083
+ reason: "unbound + no workspace (run interactively, or add it to ./.sechroom/repos.json)"
13084
+ };
12029
13085
  }
12030
- process.stderr.write(`
13086
+ process.stderr.write(
13087
+ `
12031
13088
  ${style.bold(entry.path)} ${style.dim("is not bound yet.")}
12032
- `);
13089
+ `
13090
+ );
12033
13091
  const ws = await pickWorkspace(client, {
12034
13092
  promptLabel: `Bind ${style.cyan(entry.path)} to a workspace:`,
12035
13093
  dirName: basename5(entry.path)
12036
13094
  });
12037
13095
  if (!ws) {
12038
- return { label: entry.path, dir, disposition: "skip-unbound", argv: [], reason: "unbound \u2014 no workspace chosen (skipped)" };
13096
+ return {
13097
+ label: entry.path,
13098
+ dir,
13099
+ disposition: "skip-unbound",
13100
+ argv: [],
13101
+ reason: "unbound \u2014 no workspace chosen (skipped)"
13102
+ };
12039
13103
  }
12040
13104
  return {
12041
13105
  label: entry.path,
@@ -12050,20 +13114,34 @@ async function resolveFanoutLane(cfg, opts) {
12050
13114
  let design = opts.designLane ?? process.env.SECHROOM_DESIGN_LANE;
12051
13115
  if (!code || !design) {
12052
13116
  const clients = detectInstalledClients(process.cwd());
12053
- const inferred = await inferLanes(cfg, clients.length ? clients : void 0);
13117
+ const inferred = await inferLanes(
13118
+ cfg,
13119
+ clients.length ? clients : void 0
13120
+ );
12054
13121
  code = code ?? inferred.code;
12055
13122
  design = design ?? inferred.design;
12056
13123
  }
12057
13124
  if (!opts.lane && !opts.yes && !opts.dryRun && canPrompt() && (code || design)) {
12058
- process.stderr.write(`
13125
+ process.stderr.write(
13126
+ `
12059
13127
  This fan-out will pin the same lane in every repo:
12060
- `);
12061
- if (code) process.stderr.write(` ${style.dim("code-lane")} = ${style.cyan(code)}
12062
- `);
12063
- if (design) process.stderr.write(` ${style.dim("design-lane")} = ${style.cyan(design)}
12064
- `);
13128
+ `
13129
+ );
13130
+ if (code)
13131
+ process.stderr.write(
13132
+ ` ${style.dim("code-lane")} = ${style.cyan(code)}
13133
+ `
13134
+ );
13135
+ if (design)
13136
+ process.stderr.write(
13137
+ ` ${style.dim("design-lane")} = ${style.cyan(design)}
13138
+ `
13139
+ );
12065
13140
  if (!await promptYesNo("Use this lane for all repos?")) {
12066
- code = await promptText("Code-lane id (blank = let each repo infer)?", code ?? "") || void 0;
13141
+ code = await promptText(
13142
+ "Code-lane id (blank = let each repo infer)?",
13143
+ code ?? ""
13144
+ ) || void 0;
12067
13145
  design = await promptText("Design-lane id (blank = skip)?", design ?? "") || void 0;
12068
13146
  }
12069
13147
  }
@@ -12081,30 +13159,112 @@ async function runRecurse(cfg, g, opts) {
12081
13159
  const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
12082
13160
  const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
12083
13161
  if (entries.length === 0) {
12084
- if (json) process.stdout.write(JSON.stringify({ recurse: true, root, repos: [] }) + "\n");
12085
- else process.stderr.write(`${warn("\u26A0")} no child repos found ${fromManifest ? `in ${manifestPath}` : `under ${root}`} \u2014 nothing to do.
12086
- `);
13162
+ if (json)
13163
+ process.stdout.write(
13164
+ JSON.stringify({ recurse: true, root, repos: [] }) + "\n"
13165
+ );
13166
+ else
13167
+ process.stderr.write(
13168
+ `${warn("\u26A0")} no child repos found ${fromManifest ? `in ${manifestPath}` : `under ${root}`} \u2014 nothing to do.
13169
+ `
13170
+ );
12087
13171
  return;
12088
13172
  }
12089
13173
  if (!json) {
12090
- process.stderr.write(`${style.bold("onboard --recurse")} ${style.dim(`(${entries.length} repo${entries.length === 1 ? "" : "s"} from ${sourceLabel})`)}
12091
- `);
13174
+ process.stderr.write(
13175
+ `${style.bold("onboard --recurse")} ${style.dim(`(${entries.length} repo${entries.length === 1 ? "" : "s"} from ${sourceLabel})`)}
13176
+ `
13177
+ );
12092
13178
  }
12093
- const lane = await resolveFanoutLane(cfg, { lane: opts.lane, designLane: opts.designLane, yes, dryRun });
12094
- if (!json && lane.code) process.stderr.write(`${ok("\u2713")} lane ${style.cyan(lane.code)}${lane.design ? ` ${style.dim(`/ ${lane.design}`)}` : ""} for every repo
12095
- `);
13179
+ const lane = await resolveFanoutLane(cfg, {
13180
+ lane: opts.lane,
13181
+ designLane: opts.designLane,
13182
+ yes,
13183
+ dryRun
13184
+ });
13185
+ if (!json && lane.code)
13186
+ process.stderr.write(
13187
+ `${ok("\u2713")} lane ${style.cyan(lane.code)}${lane.design ? ` ${style.dim(`/ ${lane.design}`)}` : ""} for every repo
13188
+ `
13189
+ );
12096
13190
  const client = await makeClient(cfg);
12097
13191
  const plans = [];
12098
- for (const entry of entries) plans.push(await planRecurseChild(entry, root, client, { yes, dryRun }));
12099
- const results = runChildren(plans, { globals: passthroughGlobals(g), dryRun, json });
13192
+ for (const entry of entries)
13193
+ plans.push(await planRecurseChild(entry, root, client, { yes, dryRun }));
13194
+ const results = runChildren(plans, {
13195
+ globals: passthroughGlobals(g),
13196
+ dryRun,
13197
+ json
13198
+ });
12100
13199
  if (json) {
12101
- process.stdout.write(JSON.stringify({ recurse: true, root, dryRun, repos: results }) + "\n");
13200
+ process.stdout.write(
13201
+ JSON.stringify({ recurse: true, root, dryRun, repos: results }) + "\n"
13202
+ );
12102
13203
  return;
12103
13204
  }
12104
13205
  summarizeFanout(results, { dryRun });
12105
13206
  }
12106
13207
  function registerOnboard(program2) {
12107
- program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--recurse", "orchestration-root mode: onboard every child repo under this dir (auto-discovered, or from ./.sechroom/repos.json) \u2014 refreshes bound repos, prompts a workspace per new one", false).option("--lane <id>", "set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo").option("--design-lane <id>", "set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child").option("--client <list...>", `clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`).option("--scope <scope>", "install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default: prompt, else global").option("--local", "save the binding (tenant + base URL + workspace) to a committed .sechroom.json in this repo instead of the global config", false).option("--here", "with --local: write the binding at THIS directory even when a parent already carries one \u2014 binds a subtree (e.g. a monorepo's frontend/) to its own workspace", false).option("--workspace <id>", "bind this directory to a workspace (skips the interactive workspace pick)").option("--cli-only", "configure the CLI only \u2014 don't wire any AI client (no MCP config, no agent files)", false).option("--no-mcp", "skip the MCP server config (.mcp.json etc.); still write the agent instruction files").option("--copy", "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)").option("--dry-run", "walk through without writing files or changing the profile", false).option("--refresh", "re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)", false).option("--force", "rewrite every managed block, overwriting local edits inside the markers (content outside untouched)", false).option("--check", "report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing", false).option("-y, --yes", "non-interactive: accept defaults (system timezone, detected clients, global config, full wire)", false).addHelpText(
13208
+ program2.command("onboard").description(
13209
+ "Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project"
13210
+ ).option(
13211
+ "--recurse",
13212
+ "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",
13213
+ false
13214
+ ).option(
13215
+ "--lane <id>",
13216
+ "set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo"
13217
+ ).option(
13218
+ "--design-lane <id>",
13219
+ "set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child"
13220
+ ).option(
13221
+ "--client <list...>",
13222
+ `clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`
13223
+ ).option(
13224
+ "--scope <scope>",
13225
+ "install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default: prompt, else global"
13226
+ ).option(
13227
+ "--local",
13228
+ "save the binding (tenant + base URL + workspace) to a committed .sechroom.json in this repo instead of the global config",
13229
+ false
13230
+ ).option(
13231
+ "--here",
13232
+ "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",
13233
+ false
13234
+ ).option(
13235
+ "--workspace <id>",
13236
+ "bind this directory to a workspace (skips the interactive workspace pick)"
13237
+ ).option(
13238
+ "--cli-only",
13239
+ "configure the CLI only \u2014 don't wire any AI client (no MCP config, no agent files)",
13240
+ false
13241
+ ).option(
13242
+ "--no-mcp",
13243
+ "skip the MCP server config (.mcp.json etc.); still write the agent instruction files"
13244
+ ).option(
13245
+ "--copy",
13246
+ "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)"
13247
+ ).option(
13248
+ "--dry-run",
13249
+ "walk through without writing files or changing the profile",
13250
+ false
13251
+ ).option(
13252
+ "--refresh",
13253
+ "re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)",
13254
+ false
13255
+ ).option(
13256
+ "--force",
13257
+ "rewrite every managed block, overwriting local edits inside the markers (content outside untouched)",
13258
+ false
13259
+ ).option(
13260
+ "--check",
13261
+ "report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
13262
+ false
13263
+ ).option(
13264
+ "-y, --yes",
13265
+ "non-interactive: accept defaults (system timezone, detected clients, global config, full wire)",
13266
+ false
13267
+ ).addHelpText(
12108
13268
  "after",
12109
13269
  `
12110
13270
  Examples:
@@ -12127,20 +13287,56 @@ Examples:
12127
13287
  const check = mode === "check";
12128
13288
  const yes = Boolean(opts.yes) || check;
12129
13289
  if (check && (opts.recurse || opts.cliOnly)) {
12130
- fail("--check inspects this project's agent files and cannot be combined with --recurse or --cli-only.");
13290
+ fail(
13291
+ "--check inspects this project's agent files and cannot be combined with --recurse or --cli-only."
13292
+ );
12131
13293
  }
12132
13294
  if (opts.lane) process.env.SECHROOM_CODE_LANE = opts.lane;
12133
13295
  if (opts.designLane) process.env.SECHROOM_DESIGN_LANE = opts.designLane;
12134
13296
  if (opts.recurse) {
12135
13297
  const baseUrl2 = resolveBaseUrl(g);
12136
- await ensureAuth({ baseUrl: baseUrl2, tenant: "", account: resolveAccountAlias(), clientId: readPersisted().clientId }, yes);
12137
- const cfg2 = await ensureTenant(baseUrl2, g, { yes: true, json, persist: false });
12138
- await runRecurse(cfg2, g, { yes, dryRun, json, lane: opts.lane, designLane: opts.designLane });
13298
+ await ensureAuth(
13299
+ {
13300
+ baseUrl: baseUrl2,
13301
+ tenant: "",
13302
+ account: resolveAccountAlias(),
13303
+ clientId: readPersisted().clientId
13304
+ },
13305
+ yes
13306
+ );
13307
+ const cfg2 = await ensureTenant(baseUrl2, g, {
13308
+ yes: true,
13309
+ json,
13310
+ persist: false
13311
+ });
13312
+ await runRecurse(cfg2, g, {
13313
+ yes,
13314
+ dryRun,
13315
+ json,
13316
+ lane: opts.lane,
13317
+ designLane: opts.designLane
13318
+ });
12139
13319
  return;
12140
13320
  }
12141
13321
  const baseUrl = resolveBaseUrl(g);
12142
- await ensureAuth({ baseUrl, tenant: "", account: resolveAccountAlias(), clientId: readPersisted().clientId }, yes);
12143
- const cfg = await ensureTenant(baseUrl, g, { yes, json, local: Boolean(opts.local), workspace: opts.workspace, persist: !check });
13322
+ await ensureAuth(
13323
+ {
13324
+ baseUrl,
13325
+ tenant: "",
13326
+ account: resolveAccountAlias(),
13327
+ clientId: readPersisted().clientId
13328
+ },
13329
+ yes
13330
+ );
13331
+ const scope = await chooseScope(opts.scope, yes);
13332
+ const cfg = await ensureTenant(baseUrl, g, {
13333
+ yes,
13334
+ json,
13335
+ local: Boolean(opts.local) || scope === "project",
13336
+ here: scope === "project" ? true : Boolean(opts.here),
13337
+ workspace: opts.workspace,
13338
+ persist: !check
13339
+ });
12144
13340
  const tz = await ensureTimezone(cfg, { yes, dryRun: dryRun || check });
12145
13341
  if (!json && tz.action !== "already-set") {
12146
13342
  const line = tz.action === "set" ? `${ok("\u2713")} timezone set to ${tz.timezone}
@@ -12150,21 +13346,48 @@ Examples:
12150
13346
  process.stderr.write(line);
12151
13347
  }
12152
13348
  const wire = await chooseWire(opts, yes);
12153
- const scope = await chooseScope(opts.scope, yes);
12154
- const claudeTargets = resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd: process.cwd() });
13349
+ const claudeTargets = resolveClaudeTargets({
13350
+ override: g.claudeConfigDir,
13351
+ scope,
13352
+ cwd: process.cwd()
13353
+ });
12155
13354
  const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
12156
13355
  if (scope === "project" && g.claudeConfigDir && !json) {
12157
- process.stderr.write(`${style.dim("(--claude-config-dir is ignored for --scope project \u2014 project files are repo-relative)")}
12158
- `);
13356
+ process.stderr.write(
13357
+ `${style.dim("(--claude-config-dir is ignored for --scope project \u2014 project files are repo-relative)")}
13358
+ `
13359
+ );
12159
13360
  }
12160
13361
  if (wire === "cli-only") {
12161
13362
  if (json) {
12162
- emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, clients: [] }, true);
13363
+ emit(
13364
+ {
13365
+ dryRun,
13366
+ baseUrl: cfg.baseUrl,
13367
+ tenant: cfg.tenant,
13368
+ workspaceId: cfg.workspaceId ?? null,
13369
+ timezone: tz,
13370
+ wire,
13371
+ clients: []
13372
+ },
13373
+ true
13374
+ );
12163
13375
  return;
12164
13376
  }
12165
13377
  if (!dryRun) {
12166
- await ensureLanePin(cfg, { yes, dryRun, clients: detectInstalledClients(process.cwd()) });
12167
- await maybeOfferHooks({ yes, dryRun, cwd: process.cwd(), scope, claudeConfigDir: g.claudeConfigDir, codexHome: g.codexHome });
13378
+ await ensureLanePin(cfg, {
13379
+ yes,
13380
+ dryRun,
13381
+ clients: detectInstalledClients(process.cwd())
13382
+ });
13383
+ await maybeOfferHooks({
13384
+ yes,
13385
+ dryRun,
13386
+ cwd: process.cwd(),
13387
+ scope,
13388
+ claudeConfigDir: g.claudeConfigDir,
13389
+ codexHome: g.codexHome
13390
+ });
12168
13391
  }
12169
13392
  process.stdout.write(
12170
13393
  `
@@ -12175,12 +13398,36 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
12175
13398
  await printStarterPrompt("cli");
12176
13399
  return;
12177
13400
  }
12178
- const keys = await chooseClients(opts.client, yes, process.cwd());
12179
- const setup = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
12180
- const targets = clientTargets(process.cwd(), { claudeDir: claudeTargets[0]?.dir, codexHome: codexHomes[0] });
13401
+ const requestedKeys = await chooseClients(
13402
+ opts.client,
13403
+ yes,
13404
+ process.cwd()
13405
+ );
13406
+ const keys = scope === "project" ? requestedKeys.filter((key) => key !== "codex") : requestedKeys;
13407
+ if (scope === "project" && requestedKeys.includes("codex") && !json) {
13408
+ process.stderr.write(
13409
+ `${style.dim("Codex has no project scope \u2014 skipped (use --scope global for Codex).")}
13410
+ `
13411
+ );
13412
+ }
13413
+ const setup = await withSpinner(
13414
+ "Fetching setup descriptors",
13415
+ () => fetchSetup(cfg)
13416
+ );
13417
+ const targets = clientTargets(process.cwd(), {
13418
+ claudeDir: claudeTargets[0]?.dir,
13419
+ codexHome: codexHomes[0] ?? null
13420
+ });
12181
13421
  const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
12182
13422
  if (!dryRun && !check) {
12183
- await maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, copyChoice(opts));
13423
+ await maybeOfferCopies(
13424
+ cfg,
13425
+ setup,
13426
+ targets,
13427
+ keys,
13428
+ personalWorkspaceId,
13429
+ copyChoice(opts)
13430
+ );
12184
13431
  }
12185
13432
  const writeMcp = wire === "full";
12186
13433
  const result = [];
@@ -12197,16 +13444,11 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
12197
13444
  if (!json && !check) printActions(target, actions);
12198
13445
  }
12199
13446
  if (check) {
12200
- reportCheckAndExit(
12201
- result,
12202
- json,
12203
- "sechroom onboard --refresh",
12204
- {
12205
- baseUrl: cfg.baseUrl,
12206
- tenant: cfg.tenant,
12207
- workspaceId: cfg.workspaceId ?? null
12208
- }
12209
- );
13447
+ reportCheckAndExit(result, json, "sechroom onboard --refresh", {
13448
+ baseUrl: cfg.baseUrl,
13449
+ tenant: cfg.tenant,
13450
+ workspaceId: cfg.workspaceId ?? null
13451
+ });
12210
13452
  }
12211
13453
  const evalCounts = buildCheckReport(result).eval;
12212
13454
  if (!json && !dryRun) {
@@ -12214,19 +13456,45 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
12214
13456
  }
12215
13457
  if (!json && !dryRun) {
12216
13458
  for (const t of claudeTargets) {
12217
- await maybeOfferSkills(cfg, personalWorkspaceId, { yes, dryRun, surface: "claude-code", configDir: t.dir });
13459
+ await maybeOfferSkills(cfg, personalWorkspaceId, {
13460
+ yes,
13461
+ dryRun,
13462
+ surface: "claude-code",
13463
+ configDir: t.dir
13464
+ });
12218
13465
  }
12219
13466
  }
12220
13467
  if (!json && !dryRun) {
12221
- await maybeOfferHooks({ yes, dryRun, cwd: process.cwd(), scope, claudeConfigDir: g.claudeConfigDir, codexHome: g.codexHome });
13468
+ await maybeOfferHooks({
13469
+ yes,
13470
+ dryRun,
13471
+ cwd: process.cwd(),
13472
+ scope,
13473
+ claudeConfigDir: g.claudeConfigDir,
13474
+ codexHome: g.codexHome
13475
+ });
12222
13476
  }
12223
13477
  if (json) {
12224
- emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, eval: evalCounts, clients: result }, true);
13478
+ emit(
13479
+ {
13480
+ dryRun,
13481
+ baseUrl: cfg.baseUrl,
13482
+ tenant: cfg.tenant,
13483
+ workspaceId: cfg.workspaceId ?? null,
13484
+ timezone: tz,
13485
+ wire,
13486
+ eval: evalCounts,
13487
+ clients: result
13488
+ },
13489
+ true
13490
+ );
12225
13491
  return;
12226
13492
  }
12227
13493
  if (!dryRun && evalCounts.stale) {
12228
- process.stderr.write(`${style.cyan("\u21BB")} refreshed ${evalCounts.stale} section(s) the server had moved
12229
- `);
13494
+ process.stderr.write(
13495
+ `${style.cyan("\u21BB")} refreshed ${evalCounts.stale} section(s) the server had moved
13496
+ `
13497
+ );
12230
13498
  }
12231
13499
  if (!dryRun && evalCounts.drift) {
12232
13500
  process.stderr.write(
@@ -12235,7 +13503,9 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
12235
13503
  `
12236
13504
  );
12237
13505
  }
12238
- const wroteSomething = result.some(({ actions }) => actions.some((a) => a.status === "created" || a.status === "merged"));
13506
+ const wroteSomething = result.some(
13507
+ ({ actions }) => actions.some((a) => a.status === "created" || a.status === "merged")
13508
+ );
12239
13509
  process.stdout.write(
12240
13510
  dryRun ? "\n(dry run \u2014 nothing written)\n" : !wroteSomething ? `
12241
13511
  ${style.bold("Done.")} Everything's already up to date.
@@ -12254,9 +13524,21 @@ async function chooseWire(opts, yes) {
12254
13524
  return promptSelect(
12255
13525
  "How should I set up Sechroom in this project?",
12256
13526
  [
12257
- { label: "Wire my AI client", value: "full", hint: "MCP server (.mcp.json) + agent instructions" },
12258
- { label: "Agent instructions only", value: "agent-only", hint: "skip MCP config" },
12259
- { label: "CLI only", value: "cli-only", hint: "don't write any AI-client files" }
13527
+ {
13528
+ label: "Wire my AI client",
13529
+ value: "full",
13530
+ hint: "MCP server (.mcp.json) + agent instructions"
13531
+ },
13532
+ {
13533
+ label: "Agent instructions only",
13534
+ value: "agent-only",
13535
+ hint: "skip MCP config"
13536
+ },
13537
+ {
13538
+ label: "CLI only",
13539
+ value: "cli-only",
13540
+ hint: "don't write any AI-client files"
13541
+ }
12260
13542
  ],
12261
13543
  "full"
12262
13544
  );
@@ -12278,7 +13560,9 @@ ${rule}
12278
13560
  }
12279
13561
  async function printStarterPrompt(mode, cfg) {
12280
13562
  if (mode === "cli") {
12281
- printNextStepBlock("Next \u2014 pick up where you left off:", [style.cyan("sechroom continuity resume-me")]);
13563
+ printNextStepBlock("Next \u2014 pick up where you left off:", [
13564
+ style.cyan("sechroom continuity resume-me")
13565
+ ]);
12282
13566
  return;
12283
13567
  }
12284
13568
  let primary = FALLBACK_AGENT_PROMPT;
@@ -12290,7 +13574,9 @@ async function printStarterPrompt(mode, cfg) {
12290
13574
  } catch {
12291
13575
  }
12292
13576
  }
12293
- printNextStepBlock("Next \u2014 paste this into your AI agent to get going:", [style.cyan(`"${primary}"`)]);
13577
+ printNextStepBlock("Next \u2014 paste this into your AI agent to get going:", [
13578
+ style.cyan(`"${primary}"`)
13579
+ ]);
12294
13580
  }
12295
13581
 
12296
13582
  // src/commands/project.ts
@@ -12443,7 +13729,13 @@ Examples:
12443
13729
  $ sechroom relationship suggestions --status Pending --memory mem_XXXX
12444
13730
  $ sechroom relationship suggestion accept rsg_XXXX`
12445
13731
  );
12446
- relationship.command("create <fromMemoryId> <toMemoryId>").description("Create a relationship (POST /memories/{memoryId}/relationships)").option("--type <type>", "Relationship type (Reference, Related, Parent, Child, Follows, \u2026)", "Reference").option(
13732
+ relationship.command("create <fromMemoryId> <toMemoryId>").description(
13733
+ "Create a relationship (POST /memories/{memoryId}/relationships)"
13734
+ ).option(
13735
+ "--type <type>",
13736
+ "Relationship type (Reference, Related, Parent, Child, Follows, \u2026)",
13737
+ "Reference"
13738
+ ).option(
12447
13739
  "--to-version <number>",
12448
13740
  "Version of the target memory to pin the edge to (defaults to the target's current version)"
12449
13741
  ).action(async (fromMemoryId, toMemoryId, opts, cmd) => {
@@ -12453,15 +13745,21 @@ Examples:
12453
13745
  if (opts.toVersion !== void 0) {
12454
13746
  toVersion = Number(opts.toVersion);
12455
13747
  if (!Number.isInteger(toVersion) || toVersion < 1) {
12456
- fail(`--to-version must be a positive integer (got '${opts.toVersion}').`);
13748
+ fail(
13749
+ `--to-version must be a positive integer (got '${opts.toVersion}').`
13750
+ );
12457
13751
  }
12458
13752
  } else {
12459
13753
  const target = await runApi(
12460
13754
  "Resolving target version",
12461
- async () => client.GET("/memories/{memoryId}", { params: { path: { memoryId: toMemoryId } } })
13755
+ async () => client.GET("/memories/{memoryId}", {
13756
+ params: { path: { memoryId: toMemoryId } }
13757
+ })
12462
13758
  );
12463
13759
  if (typeof target.item?.currentVersion !== "number") {
12464
- fail(`Could not resolve the current version of ${toMemoryId}; pass --to-version explicitly.`);
13760
+ fail(
13761
+ `Could not resolve the current version of ${toMemoryId}; pass --to-version explicitly.`
13762
+ );
12465
13763
  }
12466
13764
  toVersion = target.item.currentVersion;
12467
13765
  }
@@ -12485,22 +13783,28 @@ Examples:
12485
13783
  cmd.optsWithGlobals().json
12486
13784
  );
12487
13785
  });
12488
- relationship.command("list <memoryId>").description("List a memory's relationships (GET /memories/{memoryId}/relationships)").option("--direction <direction>", "Both | Outbound | Inbound").option("--include-deleted", "Include deleted relationships", false).option("--page <n>", "Page number").option("--page-size <n>", "Page size").action(async (memoryId, opts, cmd) => {
13786
+ relationship.command("list <memoryId>").description(
13787
+ "List a memory's relationships (GET /memories/{memoryId}/relationships). Walks every page by default."
13788
+ ).option("--direction <direction>", "Both | Outbound | Inbound").option("--include-deleted", "Include deleted relationships", false).option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (memoryId, opts, cmd) => {
12489
13789
  const cfg = resolveConfig(cmd.optsWithGlobals());
12490
- const data = await runApi("Listing relationships", async () => {
13790
+ const filters = {
13791
+ ...opts.direction ? { direction: opts.direction } : {},
13792
+ ...opts.includeDeleted ? { includeDeleted: true } : {}
13793
+ };
13794
+ const readPage = (query) => runApi("Listing relationships", async () => {
12491
13795
  const client = await makeClient(cfg);
12492
13796
  return client.GET("/memories/{memoryId}/relationships", {
12493
- params: {
12494
- path: { memoryId },
12495
- query: {
12496
- ...opts.direction ? { direction: opts.direction } : {},
12497
- ...opts.includeDeleted ? { includeDeleted: true } : {},
12498
- ...opts.page ? { page: Number(opts.page) } : {},
12499
- ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
12500
- }
12501
- }
13797
+ params: { path: { memoryId }, query: { ...filters, ...query } }
12502
13798
  });
12503
13799
  });
13800
+ const data = shouldAutoPage(opts) ? await fetchAllPages(
13801
+ (page) => readPage({
13802
+ page,
13803
+ ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
13804
+ }),
13805
+ opts,
13806
+ "relationships"
13807
+ ) : await readPage(singlePageQuery(opts));
12504
13808
  emit(data, cmd.optsWithGlobals().json);
12505
13809
  });
12506
13810
  relationship.command("delete <id>").description("Delete a relationship (DELETE /relationships/{id})").action(async (id, _opts, cmd) => {
@@ -12512,9 +13816,15 @@ Examples:
12512
13816
  body: {}
12513
13817
  });
12514
13818
  });
12515
- emitAction(`deleted relationship ${style.bold(id)}`, data, cmd.optsWithGlobals().json);
13819
+ emitAction(
13820
+ `deleted relationship ${style.bold(id)}`,
13821
+ data,
13822
+ cmd.optsWithGlobals().json
13823
+ );
12516
13824
  });
12517
- relationship.command("suggest <memoryId>").description("Generate relationship suggestions for a memory (POST /memories/{memoryId}/suggest-relationships)").option("--limit <n>", "Max suggestions to generate").action(async (memoryId, opts, cmd) => {
13825
+ relationship.command("suggest <memoryId>").description(
13826
+ "Generate relationship suggestions for a memory (POST /memories/{memoryId}/suggest-relationships)"
13827
+ ).option("--limit <n>", "Max suggestions to generate").action(async (memoryId, opts, cmd) => {
12518
13828
  const cfg = resolveConfig(cmd.optsWithGlobals());
12519
13829
  const data = await runApi("Suggesting relationships", async () => {
12520
13830
  const client = await makeClient(cfg);
@@ -12531,38 +13841,51 @@ Examples:
12531
13841
  cmd.optsWithGlobals().json
12532
13842
  );
12533
13843
  });
12534
- relationship.command("suggestions").description("List relationship suggestions (GET /relationship-suggestions)").option("--memory <memoryId>", "Filter to a memory").option(
13844
+ relationship.command("suggestions").description(
13845
+ "List relationship suggestions (GET /relationship-suggestions)"
13846
+ ).option("--memory <memoryId>", "Filter to a memory").option(
12535
13847
  "--status <status>",
12536
13848
  "Pending | Accepted | EditedAndAccepted | Rejected | Superseded | Deferred | Invalidated"
12537
- ).option("--page <n>", "Page number").option("--page-size <n>", "Page size").action(async (opts, cmd) => {
13849
+ ).option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (opts, cmd) => {
12538
13850
  const cfg = resolveConfig(cmd.optsWithGlobals());
12539
- const data = await runApi("Listing suggestions", async () => {
13851
+ const filters = {
13852
+ ...opts.memory ? { memoryId: opts.memory } : {},
13853
+ ...opts.status ? {
13854
+ status: opts.status
13855
+ } : {}
13856
+ };
13857
+ const readPage = (query) => runApi("Listing suggestions", async () => {
12540
13858
  const client = await makeClient(cfg);
12541
13859
  return client.GET("/relationship-suggestions", {
12542
- params: {
12543
- query: {
12544
- ...opts.memory ? { memoryId: opts.memory } : {},
12545
- ...opts.status ? {
12546
- status: opts.status
12547
- } : {},
12548
- ...opts.page ? { page: Number(opts.page) } : {},
12549
- ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
12550
- }
12551
- }
13860
+ params: { query: { ...filters, ...query } }
12552
13861
  });
12553
13862
  });
13863
+ const data = shouldAutoPage(opts) ? await fetchAllPages(
13864
+ (page) => readPage({
13865
+ page,
13866
+ ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
13867
+ }),
13868
+ opts,
13869
+ "suggestions"
13870
+ ) : await readPage(singlePageQuery(opts));
12554
13871
  emit(data, cmd.optsWithGlobals().json);
12555
13872
  });
12556
13873
  const suggestion = relationship.command("suggestion").description("Inspect and decide on a single relationship suggestion");
12557
- suggestion.command("get <id>").description("Fetch a suggestion by id (GET /relationship-suggestions/{id})").action(async (id, _opts, cmd) => {
13874
+ suggestion.command("get <id>").description(
13875
+ "Fetch a suggestion by id (GET /relationship-suggestions/{id})"
13876
+ ).action(async (id, _opts, cmd) => {
12558
13877
  const cfg = resolveConfig(cmd.optsWithGlobals());
12559
13878
  const data = await runApi("Fetching suggestion", async () => {
12560
13879
  const client = await makeClient(cfg);
12561
- return client.GET("/relationship-suggestions/{id}", { params: { path: { id } } });
13880
+ return client.GET("/relationship-suggestions/{id}", {
13881
+ params: { path: { id } }
13882
+ });
12562
13883
  });
12563
13884
  emit(data, cmd.optsWithGlobals().json);
12564
13885
  });
12565
- suggestion.command("accept <id>").description("Accept a suggestion (POST /relationship-suggestions/{instanceId}/accept)").action(async (id, _opts, cmd) => {
13886
+ suggestion.command("accept <id>").description(
13887
+ "Accept a suggestion (POST /relationship-suggestions/{instanceId}/accept)"
13888
+ ).action(async (id, _opts, cmd) => {
12566
13889
  const cfg = resolveConfig(cmd.optsWithGlobals());
12567
13890
  const data = await runApi("Accepting suggestion", async () => {
12568
13891
  const client = await makeClient(cfg);
@@ -12571,9 +13894,15 @@ Examples:
12571
13894
  body: {}
12572
13895
  });
12573
13896
  });
12574
- emitAction(`accepted suggestion ${style.bold(id)}`, data, cmd.optsWithGlobals().json);
13897
+ emitAction(
13898
+ `accepted suggestion ${style.bold(id)}`,
13899
+ data,
13900
+ cmd.optsWithGlobals().json
13901
+ );
12575
13902
  });
12576
- suggestion.command("reject <id>").description("Reject a suggestion (POST /relationship-suggestions/{instanceId}/reject)").option("--reason <reason>", "Why it's being rejected").option("--reason-code <code>", "Structured reason code").action(async (id, opts, cmd) => {
13903
+ suggestion.command("reject <id>").description(
13904
+ "Reject a suggestion (POST /relationship-suggestions/{instanceId}/reject)"
13905
+ ).option("--reason <reason>", "Why it's being rejected").option("--reason-code <code>", "Structured reason code").action(async (id, opts, cmd) => {
12577
13906
  const cfg = resolveConfig(cmd.optsWithGlobals());
12578
13907
  const data = await runApi("Rejecting suggestion", async () => {
12579
13908
  const client = await makeClient(cfg);
@@ -12585,9 +13914,18 @@ Examples:
12585
13914
  }
12586
13915
  });
12587
13916
  });
12588
- emitAction(`rejected suggestion ${style.bold(id)}`, data, cmd.optsWithGlobals().json);
13917
+ emitAction(
13918
+ `rejected suggestion ${style.bold(id)}`,
13919
+ data,
13920
+ cmd.optsWithGlobals().json
13921
+ );
12589
13922
  });
12590
- suggestion.command("defer <id>").description("Defer a suggestion (POST /relationship-suggestions/{id}/defer)").option("--until <iso>", "Defer until this ISO date-time (omit to defer indefinitely)").action(async (id, opts, cmd) => {
13923
+ suggestion.command("defer <id>").description(
13924
+ "Defer a suggestion (POST /relationship-suggestions/{id}/defer)"
13925
+ ).option(
13926
+ "--until <iso>",
13927
+ "Defer until this ISO date-time (omit to defer indefinitely)"
13928
+ ).action(async (id, opts, cmd) => {
12591
13929
  const cfg = resolveConfig(cmd.optsWithGlobals());
12592
13930
  const data = await runApi("Deferring suggestion", async () => {
12593
13931
  const client = await makeClient(cfg);
@@ -12598,14 +13936,18 @@ Examples:
12598
13936
  }
12599
13937
  });
12600
13938
  });
12601
- emitAction(`deferred suggestion ${style.bold(id)}`, data, cmd.optsWithGlobals().json);
13939
+ emitAction(
13940
+ `deferred suggestion ${style.bold(id)}`,
13941
+ data,
13942
+ cmd.optsWithGlobals().json
13943
+ );
12602
13944
  });
12603
13945
  }
12604
13946
 
12605
13947
  // src/commands/reset.ts
12606
13948
  import { homedir as homedir6 } from "os";
12607
13949
  import { join as join22 } from "path";
12608
- import { existsSync as existsSync16, readFileSync as readFileSync18, rmSync as rmSync7 } from "fs";
13950
+ import { existsSync as existsSync16, readFileSync as readFileSync18, rmSync as rmSync8 } from "fs";
12609
13951
  var SKILLS_LOCK2 = ".sechroom-skills.json";
12610
13952
  var localSkillsDir = () => join22(process.cwd(), ".claude", "skills");
12611
13953
  var globalSkillsDir = () => join22(homedir6(), ".claude", "skills");
@@ -12621,14 +13963,14 @@ function removeMaterialisedSkills(dir) {
12621
13963
  for (const name of entry.skills ?? []) {
12622
13964
  const p = join22(dir, name);
12623
13965
  if (existsSync16(p)) {
12624
- rmSync7(p, { recursive: true, force: true });
13966
+ rmSync8(p, { recursive: true, force: true });
12625
13967
  removed.push(p);
12626
13968
  }
12627
13969
  }
12628
13970
  }
12629
13971
  } catch {
12630
13972
  }
12631
- rmSync7(lockPath, { force: true });
13973
+ rmSync8(lockPath, { force: true });
12632
13974
  removed.push(lockPath);
12633
13975
  return removed;
12634
13976
  }
@@ -12667,17 +14009,17 @@ function registerReset(program2) {
12667
14009
  const removed = [];
12668
14010
  const stateDir = join22(process.cwd(), ".sechroom");
12669
14011
  if (existsSync16(stateDir)) {
12670
- rmSync7(stateDir, { recursive: true, force: true });
14012
+ rmSync8(stateDir, { recursive: true, force: true });
12671
14013
  removed.push(stateDir);
12672
14014
  }
12673
14015
  const legacyCfg = join22(process.cwd(), ".sechroom.json");
12674
14016
  if (existsSync16(legacyCfg)) {
12675
- rmSync7(legacyCfg, { force: true });
14017
+ rmSync8(legacyCfg, { force: true });
12676
14018
  removed.push(legacyCfg);
12677
14019
  }
12678
14020
  const legacySem = join22(process.cwd(), ".sem");
12679
14021
  if (existsSync16(legacySem)) {
12680
- rmSync7(legacySem, { force: true });
14022
+ rmSync8(legacySem, { force: true });
12681
14023
  removed.push(legacySem);
12682
14024
  }
12683
14025
  removed.push(...removeMaterialisedSkills(localSkillsDir()));
@@ -12702,7 +14044,7 @@ function registerReset(program2) {
12702
14044
  }
12703
14045
 
12704
14046
  // src/commands/skills.ts
12705
- import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as statSync6, writeFileSync as writeFileSync17 } from "fs";
14047
+ import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as statSync7, writeFileSync as writeFileSync17 } from "fs";
12706
14048
  import { join as join23 } from "path";
12707
14049
  function filenameFromDisposition(header) {
12708
14050
  if (!header) return void 0;
@@ -12712,7 +14054,7 @@ function filenameFromDisposition(header) {
12712
14054
  function resolveOutputPath(output, serverFilename) {
12713
14055
  const filename = serverFilename || "skills.zip";
12714
14056
  if (!output) return join23(process.cwd(), filename);
12715
- const looksLikeDir = output.endsWith("/") || existsSync17(output) && statSync6(output).isDirectory();
14057
+ const looksLikeDir = output.endsWith("/") || existsSync17(output) && statSync7(output).isDirectory();
12716
14058
  if (looksLikeDir) {
12717
14059
  mkdirSync18(output, { recursive: true });
12718
14060
  return join23(output, filename);
@@ -13311,17 +14653,15 @@ Examples:
13311
14653
  $ sechroom work-task mark-no-residue mem_XXXX --decomposition wlp_XXXX
13312
14654
  $ sechroom work-task residue-produce mem_XXXX --file residue.json`
13313
14655
  );
13314
- workTask.command("list").description("List work tasks, newest-first (GET /work-tasks)").option("--shape <shape>", "Filter: bare | managed").option(
14656
+ workTask.command("list").description(
14657
+ "List work tasks, newest-first (GET /work-tasks). Walks every page by default."
14658
+ ).option("--shape <shape>", "Filter: bare | managed").option(
13315
14659
  "--lane <lane>",
13316
14660
  "Filter by dispatch-lane value, e.g. claude-code-chris"
13317
- ).option("--status <status>", "Filter by status value, e.g. in-progress").option("--page <n>", "1-based page number", (v) => Number.parseInt(v, 10)).option(
13318
- "--page-size <n>",
13319
- "Page size (default 50, capped 200)",
13320
- (v) => Number.parseInt(v, 10)
13321
- ).action(async (opts, cmd) => {
14661
+ ).option("--status <status>", "Filter by status value, e.g. in-progress").option(...PAGE_OPTION).option(...PAGE_SIZE_OPTION).option(...NO_AUTO_PAGE_OPTION).action(async (opts, cmd) => {
13322
14662
  const globals = cmd.optsWithGlobals();
13323
14663
  const cfg = resolveConfig(globals);
13324
- const data = await runApi("Listing work tasks", async () => {
14664
+ const readPage = (query) => runApi("Listing work tasks", async () => {
13325
14665
  const client = await makeClient(cfg);
13326
14666
  return client.GET("/work-tasks", {
13327
14667
  params: {
@@ -13329,12 +14669,19 @@ Examples:
13329
14669
  shape: opts.shape,
13330
14670
  lane: opts.lane,
13331
14671
  status: opts.status,
13332
- page: opts.page,
13333
- pageSize: opts.pageSize
14672
+ ...query
13334
14673
  }
13335
14674
  }
13336
14675
  });
13337
14676
  });
14677
+ const data = shouldAutoPage(opts) ? await fetchAllPages(
14678
+ (page) => readPage({
14679
+ page,
14680
+ ...opts.pageSize ? { pageSize: Number(opts.pageSize) } : {}
14681
+ }),
14682
+ opts,
14683
+ "tasks"
14684
+ ) : await readPage(singlePageQuery(opts));
13338
14685
  emitAction(
13339
14686
  `listed ${style.bold(String(data.items.length))} of ${data.count} task(s)`,
13340
14687
  data,