@sechroom/cli 2026.6.6 → 2026.6.8

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 (3) hide show
  1. package/README.md +29 -0
  2. package/dist/index.js +277 -36
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -100,6 +100,13 @@ sechroom lookup sechroom:mem_XXXX --json # namespaced form also resolves
100
100
  sechroom --json memory get mem_XXXX # agent-friendly
101
101
  ```
102
102
 
103
+ **Output shape.** Mutating commands (`memory create`, `worklog append`) print a concise confirmation line — id + view URL — the way the MCP tools hand an LLM a result, instead of dumping the raw envelope. Pass `--json` for the full response body (the machine channel) on any command. Output is lightly colorized on a TTY and auto-plain when piped, under `--json`, or when `NO_COLOR` is set.
104
+
105
+ ```bash
106
+ sechroom memory create --text "a note" --title "Note"
107
+ # ✓ created memory mem_XXXX "Note" → https://sechroom.yi.ocd.codes/view/mem_XXXX
108
+ ```
109
+
103
110
  **Per-directory config.** A project dir can pin its own `tenant` + `baseUrl` in a local `.sechroom.json`, discovered by walking **up** from cwd (nearest wins, so any subdir inherits it). It overrides the global config — precedence: `--flag` > env > directory-local > global > default. `clientId` / auth state stays global.
104
111
 
105
112
  ```bash
@@ -137,6 +144,7 @@ Codex TOML table replace; instruction files use a managed marker block).
137
144
  sechroom init # Claude Code (default): .mcp.json + CLAUDE.md
138
145
  sechroom init --client all # claude-code, claude-desktop, codex, cursor
139
146
  sechroom init --client codex,cursor # a subset
147
+ sechroom init --mcp-only # just the MCP config (skip agent files)
140
148
  sechroom init --dry-run --json # preview the writes, no changes
141
149
 
142
150
  # granular pieces init orchestrates:
@@ -144,6 +152,27 @@ sechroom setup mcp claude-desktop # just the MCP config for one client
144
152
  sechroom setup agent-files codex # just the AGENTS.md instruction file
145
153
  ```
146
154
 
155
+ ### `sechroom onboard` — guided first run
156
+
157
+ `onboard` orchestrates the whole zero-to-wired path interactively: configure base
158
+ URL + tenant, sign in, set the profile timezone, then wire your AI client(s). Two
159
+ prompts make it fit how you actually work:
160
+
161
+ - **Where to save config** — globally (`~/.config/sechroom`, all projects) or a
162
+ directory-local `.sechroom.json` (this project + subdirs). Defaults to local
163
+ when a `.sechroom.json` already governs the dir.
164
+ - **How far to wire** — full (MCP server + agent instructions), agent
165
+ instructions only (skip `.mcp.json`), or **CLI only** (write nothing for AI
166
+ clients — for when you just want the `sechroom` command).
167
+
168
+ ```bash
169
+ sechroom onboard # interactive: asks where to save + how to wire
170
+ sechroom onboard --cli-only # just the CLI — no .mcp.json, no agent files
171
+ sechroom onboard --no-mcp # agent instructions only, skip MCP config
172
+ sechroom onboard --local # save tenant + base URL to ./.sechroom.json
173
+ sechroom onboard --yes # non-interactive: defaults + global config + full wire
174
+ ```
175
+
147
176
  Per client → where it writes:
148
177
 
149
178
  | client | MCP config | instruction file |
package/dist/index.js CHANGED
@@ -173,13 +173,13 @@ function startLoopback(state) {
173
173
  }
174
174
  const got = url.searchParams.get("code");
175
175
  const gotState = url.searchParams.get("state");
176
- const err = url.searchParams.get("error");
177
- res.writeHead(err ? 400 : 200, { "content-type": "text/html" });
176
+ const err2 = url.searchParams.get("error");
177
+ res.writeHead(err2 ? 400 : 200, { "content-type": "text/html" });
178
178
  res.end(
179
- err ? `<h3>Authorization failed: ${err}</h3>` : "<h3>Signed in to Sechroom.</h3><p>Return to the terminal.</p>"
179
+ err2 ? `<h3>Authorization failed: ${err2}</h3>` : "<h3>Signed in to Sechroom.</h3><p>Return to the terminal.</p>"
180
180
  );
181
181
  server.close();
182
- if (err) return rejectCode(new Error(`Authorization error: ${err}`));
182
+ if (err2) return rejectCode(new Error(`Authorization error: ${err2}`));
183
183
  if (!got) return rejectCode(new Error("No code in callback."));
184
184
  if (gotState !== state) return rejectCode(new Error("State mismatch \u2014 possible CSRF."));
185
185
  resolveCode(got);
@@ -277,6 +277,22 @@ var quiet = false;
277
277
  function setQuiet(q) {
278
278
  quiet = q;
279
279
  }
280
+ function colorOn() {
281
+ return !quiet && !process.env.NO_COLOR && process.env.FORCE_COLOR !== "0" && Boolean(process.stdout.isTTY);
282
+ }
283
+ function wrap(open2, close) {
284
+ return (s) => colorOn() ? `\x1B[${open2}m${s}\x1B[${close}m` : String(s);
285
+ }
286
+ var style = {
287
+ bold: wrap(1, 22),
288
+ dim: wrap(2, 22),
289
+ red: wrap(31, 39),
290
+ green: wrap(32, 39),
291
+ yellow: wrap(33, 39),
292
+ cyan: wrap(36, 39)
293
+ };
294
+ var ok = (s) => style.green(s);
295
+ var err = (s) => style.red(s);
280
296
  function active() {
281
297
  return !quiet && Boolean(process.stderr.isTTY);
282
298
  }
@@ -302,12 +318,12 @@ function spinner(text) {
302
318
  return {
303
319
  succeed(t) {
304
320
  clear();
305
- process.stderr.write(`\u2713 ${t ?? text}
321
+ process.stderr.write(`${ok("\u2713")} ${t ?? text}
306
322
  `);
307
323
  },
308
324
  fail(t) {
309
325
  clear();
310
- process.stderr.write(`\u2717 ${t ?? text}
326
+ process.stderr.write(`${err("\u2717")} ${t ?? text}
311
327
  `);
312
328
  },
313
329
  stop: clear
@@ -344,15 +360,88 @@ async function promptText(question, def) {
344
360
  rl.close();
345
361
  }
346
362
  }
363
+ async function promptSelect(question, choices, def) {
364
+ if (choices.length === 0) throw new Error("promptSelect: no choices");
365
+ const defIdx = Math.max(
366
+ 0,
367
+ def !== void 0 ? choices.findIndex((c) => c.value === def) : 0
368
+ );
369
+ if (!canPrompt()) return choices[defIdx].value;
370
+ const { createInterface } = await import("readline");
371
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
372
+ try {
373
+ process.stderr.write(`${style.bold(question)}
374
+ `);
375
+ choices.forEach((c, i) => {
376
+ const marker = i === defIdx ? style.cyan("\u203A") : " ";
377
+ const hint = c.hint ? ` ${style.dim(`\u2014 ${c.hint}`)}` : "";
378
+ process.stderr.write(` ${marker} ${style.bold(String(i + 1))}. ${c.label}${hint}
379
+ `);
380
+ });
381
+ const answer = await new Promise((resolve) => {
382
+ rl.question(`Choose ${style.dim(`[${defIdx + 1}]`)} `, resolve);
383
+ });
384
+ const trimmed = answer.trim();
385
+ if (!trimmed) return choices[defIdx].value;
386
+ const n = Number(trimmed);
387
+ if (Number.isInteger(n) && n >= 1 && n <= choices.length) return choices[n - 1].value;
388
+ const byLabel = choices.find(
389
+ (c) => c.label.toLowerCase().startsWith(trimmed.toLowerCase())
390
+ );
391
+ return byLabel ? byLabel.value : choices[defIdx].value;
392
+ } finally {
393
+ rl.close();
394
+ }
395
+ }
396
+ async function promptMultiSelect(question, choices, preselected = []) {
397
+ if (choices.length === 0) return [];
398
+ const pre = (v) => preselected.includes(v);
399
+ const preValues = () => choices.filter((c) => pre(c.value)).map((c) => c.value);
400
+ if (!canPrompt()) return preValues();
401
+ const { createInterface } = await import("readline");
402
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
403
+ try {
404
+ process.stderr.write(
405
+ `${style.bold(question)} ${style.dim("(numbers or 'all', comma-separated; Enter keeps \u25C9)")}
406
+ `
407
+ );
408
+ choices.forEach((c, i) => {
409
+ const box = pre(c.value) ? style.cyan("\u25C9") : "\u25CB";
410
+ const hint = c.hint ? ` ${style.dim(`\u2014 ${c.hint}`)}` : "";
411
+ process.stderr.write(` ${box} ${style.bold(String(i + 1))}. ${c.label}${hint}
412
+ `);
413
+ });
414
+ const answer = await new Promise((resolve) => {
415
+ rl.question(`Select ${style.dim("[Enter = \u25C9]")} `, resolve);
416
+ });
417
+ const trimmed = answer.trim().toLowerCase();
418
+ if (!trimmed) return preValues();
419
+ if (trimmed === "all") return choices.map((c) => c.value);
420
+ const picks = [];
421
+ for (const tok of trimmed.split(",").map((t) => t.trim()).filter(Boolean)) {
422
+ const n = Number(tok);
423
+ if (Number.isInteger(n) && n >= 1 && n <= choices.length) {
424
+ picks.push(choices[n - 1].value);
425
+ continue;
426
+ }
427
+ const byLabel = choices.find((c) => c.label.toLowerCase().startsWith(tok));
428
+ if (byLabel) picks.push(byLabel.value);
429
+ }
430
+ const uniq = [...new Set(picks)];
431
+ return uniq.length > 0 ? uniq : preValues();
432
+ } finally {
433
+ rl.close();
434
+ }
435
+ }
347
436
  async function withSpinner(text, fn) {
348
437
  const s = spinner(text);
349
438
  try {
350
439
  const result = await fn();
351
440
  s.succeed();
352
441
  return result;
353
- } catch (err) {
442
+ } catch (err2) {
354
443
  s.fail();
355
- throw err;
444
+ throw err2;
356
445
  }
357
446
  }
358
447
 
@@ -376,14 +465,25 @@ function emit(data, json) {
376
465
  process.stdout.write(JSON.stringify(data, null, 2) + "\n");
377
466
  }
378
467
  }
468
+ function publicUrl(url) {
469
+ return url.replace(/^https?:\/\/localhost:5012/, "https://sechroom.yi.ocd.codes");
470
+ }
471
+ function emitAction(summary, data, json) {
472
+ if (json) {
473
+ process.stdout.write(JSON.stringify(data) + "\n");
474
+ return;
475
+ }
476
+ process.stdout.write(`${ok("\u2713")} ${summary}
477
+ `);
478
+ }
379
479
  async function runApi(label, fn) {
380
480
  const s = spinner(label);
381
481
  let res;
382
482
  try {
383
483
  res = await fn();
384
- } catch (err) {
484
+ } catch (err2) {
385
485
  s.fail();
386
- fail(err);
486
+ fail(err2);
387
487
  }
388
488
  if (res.error !== void 0 && res.error !== null) {
389
489
  s.fail();
@@ -402,6 +502,16 @@ function fail(error) {
402
502
  // src/commands/memory.ts
403
503
  function registerMemory(program2) {
404
504
  const memory = program2.command("memory").description("Create, read, and search memories");
505
+ memory.addHelpText(
506
+ "after",
507
+ `
508
+ Examples:
509
+ $ sechroom memory create --text "first note" --type reference --tag idea --tag cli
510
+ $ sechroom memory create --text "filed note" --owner-type Workspace --owner-id wsp_XXXX
511
+ $ sechroom memory search "rate limiting" --limit 5 --tag kind:decision
512
+ $ sechroom memory search "auth flow" --workspace wsp_XXXX --json
513
+ $ sechroom memory get mem_XXXX --json`
514
+ );
405
515
  memory.command("create").description("Create a memory (POST /memories)").requiredOption("--text <text>", "Memory body text").option("--type <type>", "Memory type", "reference").option("--title <title>", "Optional title").option("--tag <tag...>", "Tags (repeatable)").option("--owner-type <ownerType>", "Workspace | Project | Unfiled", "Unfiled").option("--owner-id <ownerId>", "Owner id (required for Workspace/Project)").option("--source <source>", "Source / lane stamp", "cli").option("--confidence <n>", "Confidence 0..1", "1.0").action(async (opts, cmd) => {
406
516
  const cfg = resolveConfig(cmd.optsWithGlobals());
407
517
  const unfiled = String(opts.ownerType).toLowerCase() === "unfiled";
@@ -421,7 +531,9 @@ function registerMemory(program2) {
421
531
  }
422
532
  });
423
533
  });
424
- emit(data, cmd.optsWithGlobals().json);
534
+ const titlePart = opts.title ? ` ${style.dim(`"${opts.title}"`)}` : "";
535
+ const urlPart = data.url ? ` ${style.dim("\u2192")} ${publicUrl(data.url)}` : "";
536
+ emitAction(`created memory ${style.bold(data.id)}${titlePart}${urlPart}`, data, cmd.optsWithGlobals().json);
425
537
  });
426
538
  memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").action(async (memoryId, _opts, cmd) => {
427
539
  const cfg = resolveConfig(cmd.optsWithGlobals());
@@ -456,6 +568,13 @@ function registerMemory(program2) {
456
568
  // src/commands/worklog.ts
457
569
  function registerWorklog(program2) {
458
570
  const worklog = program2.command("worklog").description("Append to the daily work log");
571
+ worklog.addHelpText(
572
+ "after",
573
+ `
574
+ Examples:
575
+ $ sechroom worklog append --text "shipped CLI help + onboarding scope; PR #1430"
576
+ $ sechroom worklog append --text "smoke passed" --source claude-code-chris --title "CLI smoke"`
577
+ );
459
578
  worklog.command("append").description("Append a work-log entry (POST /operator-surface/work-log/append)").requiredOption("--text <text>", "Entry body (short bullets / pointers) \u2014 the bullet").option("--source <source>", "Lane stamp (e.g. claude-code-chris) \u2014 laneId", "cli").option("--workspace <workspaceId>", "Target work-log workspace (default: caller's daily log)").option("--title <title>", "Optional entry title").action(async (opts, cmd) => {
460
579
  const cfg = resolveConfig(cmd.optsWithGlobals());
461
580
  const data = await runApi("Appending work-log entry", async () => {
@@ -469,7 +588,7 @@ function registerWorklog(program2) {
469
588
  }
470
589
  });
471
590
  });
472
- emit(data, cmd.optsWithGlobals().json);
591
+ emitAction(`appended work-log entry ${style.bold(data.memoryId)}`, data, cmd.optsWithGlobals().json);
473
592
  });
474
593
  }
475
594
 
@@ -477,6 +596,14 @@ function registerWorklog(program2) {
477
596
  function registerLookup(program2) {
478
597
  program2.command("lookup <id>").description(
479
598
  "Resolve a sechroom id to its kind, title, and view URL (mem_\u2026/wsp_\u2026/prj_\u2026, unprefixed, or sechroom:<id>)"
599
+ ).addHelpText(
600
+ "after",
601
+ `
602
+ Examples:
603
+ $ sechroom lookup mem_XXXX a memory -> kind / title / view URL
604
+ $ sechroom lookup wsp_XXXX a workspace
605
+ $ sechroom lookup sechroom:mem_XXXX namespaced / portable form also resolves
606
+ $ sechroom lookup mem_XXXX --json`
480
607
  ).action(async (id, _opts, cmd) => {
481
608
  const cfg = resolveConfig(cmd.optsWithGlobals());
482
609
  const data = await runApi(`Resolving ${id}`, async () => {
@@ -799,7 +926,16 @@ ${client.label} (${client.key}):
799
926
  }
800
927
  }
801
928
  function registerInit(program2) {
802
- program2.command("init").description("Wire this project for sechroom: write MCP config + agent instruction files from the server's setup descriptors").option("--client <list>", `comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all'`, DEFAULT_CLIENT_KEY).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)").action(async (opts, cmd) => {
929
+ program2.command("init").description("Wire this project for sechroom: write MCP config + agent instruction files from the server's setup descriptors").option("--client <list>", `comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all'`, DEFAULT_CLIENT_KEY).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)").addHelpText(
930
+ "after",
931
+ `
932
+ Examples:
933
+ $ sechroom init Claude Code (default): ./.mcp.json + ./CLAUDE.md
934
+ $ sechroom init --client all claude-code, claude-desktop, codex, cursor
935
+ $ sechroom init --client codex,cursor
936
+ $ sechroom init --mcp-only just the MCP config (skip agent files)
937
+ $ sechroom init --dry-run --json preview the writes, change nothing`
938
+ ).action(async (opts, cmd) => {
803
939
  const cfg = resolveConfig(cmd.optsWithGlobals());
804
940
  const setup = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
805
941
  const targets = clientTargets(process.cwd());
@@ -881,11 +1017,12 @@ function systemTimezone() {
881
1017
  return "UTC";
882
1018
  }
883
1019
  }
884
- async function ensureConfig(g, yes) {
1020
+ async function ensureConfig(g, opts) {
885
1021
  const persisted = readPersisted();
886
- let baseUrl = g.baseUrl ?? process.env.SECHROOM_BASE_URL ?? persisted.baseUrl ?? DEFAULT_BASE_URL2;
887
- let tenant = g.tenant ?? process.env.SECHROOM_TENANT ?? persisted.tenant ?? "";
888
- if (canPrompt() && !yes) {
1022
+ const local = readLocalConfig();
1023
+ let baseUrl = g.baseUrl ?? process.env.SECHROOM_BASE_URL ?? local.baseUrl ?? persisted.baseUrl ?? DEFAULT_BASE_URL2;
1024
+ let tenant = g.tenant ?? process.env.SECHROOM_TENANT ?? local.tenant ?? persisted.tenant ?? "";
1025
+ if (canPrompt() && !opts.yes) {
889
1026
  baseUrl = await promptText("Sechroom API base URL?", baseUrl);
890
1027
  tenant = await promptText("Tenant id?", tenant || void 0);
891
1028
  }
@@ -895,7 +1032,26 @@ async function ensureConfig(g, yes) {
895
1032
  "No tenant set. Pass --tenant <id>, set SECHROOM_TENANT, or run `sechroom config set tenant <id>` \u2014 the API rejects untenanted requests (HTTP 400)."
896
1033
  );
897
1034
  }
898
- writePersisted({ baseUrl, tenant });
1035
+ let storeLocal = Boolean(opts.local);
1036
+ if (!opts.local && canPrompt() && !opts.yes) {
1037
+ storeLocal = await promptSelect(
1038
+ "Where should this tenant + base URL be saved?",
1039
+ [
1040
+ { label: "Globally", value: "global", hint: "all projects on this machine" },
1041
+ { label: "This directory", value: "local", hint: ".sechroom.json \u2014 project + subdirs" }
1042
+ ],
1043
+ local.path ? "local" : "global"
1044
+ ) === "local";
1045
+ }
1046
+ if (storeLocal) {
1047
+ const path = writeLocalConfig({ baseUrl, tenant });
1048
+ if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved to ${path} (directory-local)
1049
+ `);
1050
+ } else {
1051
+ writePersisted({ baseUrl, tenant });
1052
+ if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved globally (~/.config/sechroom/config.json)
1053
+ `);
1054
+ }
899
1055
  return { baseUrl, tenant, clientId: persisted.clientId };
900
1056
  }
901
1057
  async function ensureAuth(cfg, yes) {
@@ -937,33 +1093,59 @@ async function ensureTimezone(cfg, opts) {
937
1093
  async function chooseClients(clientFlag, yes, cwd) {
938
1094
  if (clientFlag) return resolveClientKeys(clientFlag);
939
1095
  const detected = detectInstalledClients(cwd);
940
- const fallback = (detected.length > 0 ? detected : [DEFAULT_CLIENT_KEY]).join(",");
941
- if (!canPrompt() || yes) return resolveClientKeys(fallback);
942
- process.stderr.write(
943
- `
944
- Available clients: ${ALL_CLIENT_KEYS.join(", ")}
945
- ` + (detected.length > 0 ? `Detected on this machine: ${detected.join(", ")}
946
- ` : "No clients auto-detected.\n")
1096
+ const preselected = detected.length > 0 ? detected : [DEFAULT_CLIENT_KEY];
1097
+ if (!canPrompt() || yes) return preselected;
1098
+ const picks = await promptMultiSelect(
1099
+ "Which AI clients should I wire?",
1100
+ ALL_CLIENT_KEYS.map((k) => ({
1101
+ label: k,
1102
+ value: k,
1103
+ hint: detected.includes(k) ? "detected" : void 0
1104
+ })),
1105
+ preselected
947
1106
  );
948
- const answer = await promptText("Which to wire? (comma-separated, or 'all')", fallback);
949
- return resolveClientKeys(answer);
1107
+ return picks.length > 0 ? picks : preselected;
950
1108
  }
951
1109
  function registerOnboard(program2) {
952
- program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--client <list>", `comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`).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("-y, --yes", "non-interactive: accept defaults (system timezone, detected clients)", false).action(async (opts, cmd) => {
1110
+ program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--client <list>", `comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`).option("--local", "save tenant + base URL to a directory-local .sechroom.json instead of the global config", false).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("-y, --yes", "non-interactive: accept defaults (system timezone, detected clients, global config, full wire)", false).addHelpText(
1111
+ "after",
1112
+ `
1113
+ Examples:
1114
+ $ sechroom onboard guided, interactive (asks where to save config + how to wire)
1115
+ $ sechroom onboard --cli-only just the CLI \u2014 no .mcp.json, no agent files
1116
+ $ sechroom onboard --no-mcp agent instructions only, skip MCP config
1117
+ $ sechroom onboard --local save tenant + base URL to ./.sechroom.json
1118
+ $ sechroom onboard --yes non-interactive: defaults + global config + full wire
1119
+ $ sechroom onboard --client all --dry-run preview wiring every client, write nothing`
1120
+ ).action(async (opts, cmd) => {
953
1121
  const g = cmd.optsWithGlobals();
954
1122
  const json = Boolean(g.json);
955
1123
  const yes = Boolean(opts.yes);
956
1124
  const dryRun = Boolean(opts.dryRun);
957
- const cfg = await ensureConfig(g, yes);
1125
+ const cfg = await ensureConfig(g, { yes, json, local: Boolean(opts.local) });
958
1126
  await ensureAuth(cfg, yes);
959
1127
  const tz = await ensureTimezone(cfg, { yes, dryRun });
960
1128
  if (!json && tz.action !== "already-set") {
961
- const line = tz.action === "set" ? `\u2713 timezone set to ${tz.timezone}
1129
+ const line = tz.action === "set" ? `${ok("\u2713")} timezone set to ${tz.timezone}
962
1130
  ` : tz.action === "dry-run" ? `(dry run \u2014 would set timezone to ${tz.timezone})
963
1131
  ` : `timezone not set \u2014 ${tz.note}
964
1132
  `;
965
1133
  process.stderr.write(line);
966
1134
  }
1135
+ const wire = await chooseWire(opts, yes);
1136
+ if (wire === "cli-only") {
1137
+ if (json) {
1138
+ emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, timezone: tz, wire, clients: [] }, true);
1139
+ return;
1140
+ }
1141
+ process.stdout.write(
1142
+ `
1143
+ ${style.bold("Done.")} The CLI is configured for ${style.cyan(cfg.tenant)} \u2014 no AI-client files written.
1144
+ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom --help")}
1145
+ `
1146
+ );
1147
+ return;
1148
+ }
967
1149
  const keys = await chooseClients(opts.client, yes, process.cwd());
968
1150
  const setup = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
969
1151
  const targets = clientTargets(process.cwd());
@@ -971,12 +1153,13 @@ function registerOnboard(program2) {
971
1153
  if (!dryRun) {
972
1154
  await maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, copyChoice(opts));
973
1155
  }
1156
+ const writeMcp = wire === "full";
974
1157
  const result = [];
975
1158
  for (const key of keys) {
976
1159
  const target = targets[key];
977
1160
  const actions = await applyClient(cfg, setup, target, {
978
1161
  dryRun,
979
- mcp: true,
1162
+ mcp: writeMcp,
980
1163
  agentFiles: true,
981
1164
  personalWorkspaceId
982
1165
  });
@@ -984,14 +1167,33 @@ function registerOnboard(program2) {
984
1167
  if (!json) printActions(target, actions);
985
1168
  }
986
1169
  if (json) {
987
- emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, timezone: tz, clients: result }, true);
1170
+ emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, timezone: tz, wire, clients: result }, true);
988
1171
  return;
989
1172
  }
990
1173
  process.stdout.write(
991
- dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone. Restart your AI client (or reload MCP) to pick up the new config.\n"
1174
+ dryRun ? "\n(dry run \u2014 nothing written)\n" : writeMcp ? `
1175
+ ${style.bold("Done.")} Restart your AI client (or reload MCP) to pick up the new config.
1176
+ ` : `
1177
+ ${style.bold("Done.")} Agent instructions written (no MCP config).
1178
+ `
992
1179
  );
993
1180
  });
994
1181
  }
1182
+ async function chooseWire(opts, yes) {
1183
+ if (opts.cliOnly) return "cli-only";
1184
+ if (canPrompt() && !yes) {
1185
+ return promptSelect(
1186
+ "How should I set up Sechroom in this project?",
1187
+ [
1188
+ { label: "Wire my AI client", value: "full", hint: "MCP server (.mcp.json) + agent instructions" },
1189
+ { label: "Agent instructions only", value: "agent-only", hint: "skip MCP config" },
1190
+ { label: "CLI only", value: "cli-only", hint: "don't write any AI-client files" }
1191
+ ],
1192
+ "full"
1193
+ );
1194
+ }
1195
+ return opts.mcp === false ? "agent-only" : "full";
1196
+ }
995
1197
 
996
1198
  // src/index.ts
997
1199
  function resolveVersion() {
@@ -1006,16 +1208,55 @@ function resolveVersion() {
1006
1208
  }
1007
1209
  var program = new Command();
1008
1210
  program.name("sechroom").description("Sechroom CLI \u2014 thin generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.").version(resolveVersion()).option("--base-url <url>", "API base URL (overrides config / SECHROOM_BASE_URL)").option("--tenant <tenant>", "Tenant id (required by the API; overrides config / SECHROOM_TENANT)").option("--json", "Emit compact JSON (for scripts and agents)", false);
1211
+ program.addHelpText(
1212
+ "after",
1213
+ `
1214
+ Examples:
1215
+ $ sechroom onboard guided first-run: configure, sign in, wire this project
1216
+ $ sechroom login sign in via browser (OAuth + PKCE)
1217
+ $ sechroom config set tenant ocd set your tenant (global)
1218
+ $ sechroom config set --local tenant cli-smoke pin tenant for this directory (.sechroom.json)
1219
+ $ sechroom config show resolved config + which source won
1220
+
1221
+ $ sechroom memory create --text "a note" --title "Note" --tag idea
1222
+ $ sechroom memory search "convention drift" --limit 5
1223
+ $ sechroom memory get mem_XXXX
1224
+ $ sechroom worklog append --text "shipped X; PR #123" --source claude-code-chris
1225
+ $ sechroom lookup mem_XXXX resolve any id -> kind / title / view URL
1226
+
1227
+ $ sechroom --json memory search "auth" compact JSON for scripts and agents
1228
+ $ SECHROOM_TOKEN=<bearer> sechroom --json memory get mem_XXXX headless
1229
+
1230
+ Config precedence (high -> low): --flag > env (SECHROOM_*) > ./.sechroom.json > global > default.
1231
+ Run 'sechroom <command> --help' for command-specific examples.`
1232
+ );
1009
1233
  program.hook("preAction", (_thisCmd, actionCmd) => {
1010
1234
  setQuiet(Boolean(actionCmd.optsWithGlobals().json));
1011
1235
  });
1012
- program.command("login").description("Sign in via browser (OAuth auth-code + PKCE, dynamic client registration)").action(async (_opts, cmd) => {
1236
+ program.command("login").description("Sign in via browser (OAuth auth-code + PKCE, dynamic client registration)").addHelpText(
1237
+ "after",
1238
+ `
1239
+ Examples:
1240
+ $ sechroom login sign in to the configured base URL + tenant
1241
+ $ sechroom login --base-url https://staging.app.sechroom.ai/api
1242
+ $ export SECHROOM_TOKEN=<bearer> headless: skip login entirely (CI / agents)`
1243
+ ).action(async (_opts, cmd) => {
1013
1244
  const g = cmd.optsWithGlobals();
1014
1245
  const persisted = readPersisted();
1015
1246
  const baseUrl = g.baseUrl ?? process.env.SECHROOM_BASE_URL ?? persisted.baseUrl ?? "https://app.sechroom.ai/api";
1016
1247
  await login({ baseUrl: baseUrl.replace(/\/$/, ""), tenant: g.tenant ?? "" });
1017
1248
  });
1018
1249
  var config = program.command("config").description("Manage persisted CLI config");
1250
+ config.addHelpText(
1251
+ "after",
1252
+ `
1253
+ Examples:
1254
+ $ sechroom config set baseUrl https://app.sechroom.ai/api prod (staging: https://staging.app.sechroom.ai/api)
1255
+ $ sechroom config set tenant ocd
1256
+ $ sechroom config set --local tenant cli-smoke this dir + subdirs (.sechroom.json)
1257
+ $ sechroom config set clientId dyn-XXXX global-only escape hatch (no DCR endpoint)
1258
+ $ sechroom config show --json`
1259
+ );
1019
1260
  config.command("set <key> <value>").description("Set baseUrl | tenant | clientId (clientId is global-only)").option("--local", "Write to the directory-local .sechroom.json (nearest up the tree, else cwd) instead of the global config").action((key, value, opts) => {
1020
1261
  if (opts.local) {
1021
1262
  if (!["baseUrl", "tenant"].includes(key)) {
@@ -1065,8 +1306,8 @@ registerLookup(program);
1065
1306
  registerInit(program);
1066
1307
  registerSetup(program);
1067
1308
  registerOnboard(program);
1068
- program.parseAsync().catch((err) => {
1069
- process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
1309
+ program.parseAsync().catch((err2) => {
1310
+ process.stderr.write(`error: ${err2 instanceof Error ? err2.message : String(err2)}
1070
1311
  `);
1071
1312
  process.exit(1);
1072
1313
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sechroom/cli",
3
- "version": "2026.6.6",
3
+ "version": "2026.6.8",
4
4
  "description": "Sechroom CLI — a thin, generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",