@sechroom/cli 2026.6.5 → 2026.6.7

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 +46 -0
  2. package/dist/index.js +345 -58
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -100,6 +100,30 @@ 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
+
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.
111
+
112
+ ```bash
113
+ sechroom config set --local tenant cli-smoke # this dir + subdirs
114
+ sechroom config set --local baseUrl https://staging.app.sechroom.ai/api
115
+ sechroom config show # resolved values + which source won
116
+ ```
117
+
118
+ **Smoke testing.** There is a dedicated **`cli-smoke`** tenant on **both staging and prod** for exercising the CLI without touching real tenants — point at staging and use it:
119
+
120
+ ```bash
121
+ sechroom config set --local baseUrl https://staging.app.sechroom.ai/api
122
+ sechroom config set --local tenant cli-smoke
123
+ sechroom login
124
+ sechroom worklog append --text "cli smoke" --source claude-code-chris
125
+ ```
126
+
103
127
  Headless:
104
128
 
105
129
  ```bash
@@ -120,6 +144,7 @@ Codex TOML table replace; instruction files use a managed marker block).
120
144
  sechroom init # Claude Code (default): .mcp.json + CLAUDE.md
121
145
  sechroom init --client all # claude-code, claude-desktop, codex, cursor
122
146
  sechroom init --client codex,cursor # a subset
147
+ sechroom init --mcp-only # just the MCP config (skip agent files)
123
148
  sechroom init --dry-run --json # preview the writes, no changes
124
149
 
125
150
  # granular pieces init orchestrates:
@@ -127,6 +152,27 @@ sechroom setup mcp claude-desktop # just the MCP config for one client
127
152
  sechroom setup agent-files codex # just the AGENTS.md instruction file
128
153
  ```
129
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
+
130
176
  Per client → where it writes:
131
177
 
132
178
  | client | MCP config | instruction file |
package/dist/index.js CHANGED
@@ -11,11 +11,12 @@ import open from "open";
11
11
 
12
12
  // src/config.ts
13
13
  import { homedir } from "os";
14
- import { join } from "path";
14
+ import { join, dirname } from "path";
15
15
  import { mkdirSync, readFileSync, writeFileSync, existsSync } from "fs";
16
16
  var CONFIG_DIR = join(homedir(), ".config", "sechroom");
17
17
  var CONFIG_FILE = join(CONFIG_DIR, "config.json");
18
18
  var TOKEN_FILE = join(CONFIG_DIR, "token.json");
19
+ var LOCAL_CONFIG_NAME = ".sechroom.json";
19
20
  var DEFAULT_BASE_URL = "https://app.sechroom.ai/api";
20
21
  function ensureDir() {
21
22
  if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
@@ -32,6 +33,13 @@ function writePersisted(patch) {
32
33
  const next = { ...readPersisted(), ...patch };
33
34
  writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), { mode: 384 });
34
35
  }
36
+ function readDcrClientId(baseUrl) {
37
+ return readPersisted().clientIds?.[baseUrl];
38
+ }
39
+ function writeDcrClientId(baseUrl, clientId) {
40
+ const p = readPersisted();
41
+ writePersisted({ clientIds: { ...p.clientIds ?? {}, [baseUrl]: clientId } });
42
+ }
35
43
  function readToken() {
36
44
  const envTok = process.env.SECHROOM_TOKEN;
37
45
  if (envTok) return { accessToken: envTok };
@@ -45,17 +53,67 @@ function writeToken(tok) {
45
53
  ensureDir();
46
54
  writeFileSync(TOKEN_FILE, JSON.stringify(tok, null, 2), { mode: 384 });
47
55
  }
56
+ function findLocalConfigPath(start = process.cwd()) {
57
+ let dir = start;
58
+ for (; ; ) {
59
+ const candidate = join(dir, LOCAL_CONFIG_NAME);
60
+ if (existsSync(candidate)) return candidate;
61
+ const parent = dirname(dir);
62
+ if (parent === dir) return void 0;
63
+ dir = parent;
64
+ }
65
+ }
66
+ function readLocalConfig() {
67
+ const path = findLocalConfigPath();
68
+ if (!path) return {};
69
+ try {
70
+ const c = JSON.parse(readFileSync(path, "utf8"));
71
+ return { baseUrl: c.baseUrl, tenant: c.tenant, path };
72
+ } catch {
73
+ return {};
74
+ }
75
+ }
76
+ function writeLocalConfig(patch) {
77
+ const path = findLocalConfigPath() ?? join(process.cwd(), LOCAL_CONFIG_NAME);
78
+ let current = {};
79
+ try {
80
+ current = JSON.parse(readFileSync(path, "utf8"));
81
+ } catch {
82
+ }
83
+ writeFileSync(path, JSON.stringify({ ...current, ...patch }, null, 2), { mode: 384 });
84
+ return path;
85
+ }
48
86
  function resolveConfig(flags) {
87
+ const local = readLocalConfig();
49
88
  const persisted = readPersisted();
50
- const baseUrl = flags.baseUrl ?? process.env.SECHROOM_BASE_URL ?? persisted.baseUrl ?? DEFAULT_BASE_URL;
51
- const tenant = flags.tenant ?? process.env.SECHROOM_TENANT ?? persisted.tenant ?? "";
89
+ const baseUrl = flags.baseUrl ?? process.env.SECHROOM_BASE_URL ?? local.baseUrl ?? persisted.baseUrl ?? DEFAULT_BASE_URL;
90
+ const tenant = flags.tenant ?? process.env.SECHROOM_TENANT ?? local.tenant ?? persisted.tenant ?? "";
52
91
  if (!tenant) {
53
92
  throw new Error(
54
- "No tenant set. The Sechroom API rejects untenanted requests (HTTP 400). Pass --tenant <id>, set SECHROOM_TENANT, or run `sechroom config set tenant <id>`."
93
+ "No tenant set. The Sechroom API rejects untenanted requests (HTTP 400). Pass --tenant <id>, set SECHROOM_TENANT, run `sechroom config set tenant <id>`, or `sechroom config set --local tenant <id>` for this directory."
55
94
  );
56
95
  }
57
96
  return { baseUrl: baseUrl.replace(/\/$/, ""), tenant, clientId: persisted.clientId };
58
97
  }
98
+ function describeConfig(flags) {
99
+ const local = readLocalConfig();
100
+ const g = readPersisted();
101
+ const localTag = local.path ? `local (${local.path})` : "local";
102
+ const pick = (flag, env, l, gl, def) => {
103
+ if (flag) return { value: flag, source: "flag" };
104
+ if (env) return { value: env, source: "env" };
105
+ if (l) return { value: l, source: localTag };
106
+ if (gl) return { value: gl, source: "global" };
107
+ if (def) return { value: def, source: "default" };
108
+ return { value: void 0, source: "unset" };
109
+ };
110
+ const baseUrl = pick(flags.baseUrl, process.env.SECHROOM_BASE_URL, local.baseUrl, g.baseUrl, DEFAULT_BASE_URL);
111
+ return {
112
+ baseUrl: { value: baseUrl.value, source: baseUrl.source },
113
+ tenant: pick(flags.tenant, process.env.SECHROOM_TENANT, local.tenant, g.tenant),
114
+ localPath: local.path
115
+ };
116
+ }
59
117
 
60
118
  // src/auth.ts
61
119
  var SCOPES = "openid email profile";
@@ -72,26 +130,26 @@ async function discover(baseUrl) {
72
130
  if (!res.ok) throw new Error(`AS discovery failed (${res.status}) at ${baseUrl}`);
73
131
  return await res.json();
74
132
  }
75
- async function ensureClientId(meta) {
76
- const cached = readPersisted().clientId;
77
- if (cached) return cached;
78
- if (!meta.registration_endpoint) {
79
- throw new Error(
80
- "Server advertises no registration_endpoint and no client_id is cached. Pre-register a client and run `sechroom config set clientId <id>`."
81
- );
133
+ async function ensureClientId(meta, baseUrl) {
134
+ if (meta.registration_endpoint) {
135
+ const res = await fetch(meta.registration_endpoint, {
136
+ method: "POST",
137
+ headers: { "content-type": "application/json" },
138
+ body: JSON.stringify({
139
+ client_name: "sechroom-cli",
140
+ redirect_uris: CANDIDATE_PORTS.map(redirectUriFor)
141
+ })
142
+ });
143
+ if (!res.ok) throw new Error(`Dynamic client registration failed (${res.status}): ${await res.text()}`);
144
+ const reg = await res.json();
145
+ writeDcrClientId(baseUrl, reg.client_id);
146
+ return reg.client_id;
82
147
  }
83
- const res = await fetch(meta.registration_endpoint, {
84
- method: "POST",
85
- headers: { "content-type": "application/json" },
86
- body: JSON.stringify({
87
- client_name: "sechroom-cli",
88
- redirect_uris: CANDIDATE_PORTS.map(redirectUriFor)
89
- })
90
- });
91
- if (!res.ok) throw new Error(`Dynamic client registration failed (${res.status}): ${await res.text()}`);
92
- const reg = await res.json();
93
- writePersisted({ clientId: reg.client_id });
94
- return reg.client_id;
148
+ const fallback = readPersisted().clientId ?? readDcrClientId(baseUrl);
149
+ if (fallback) return fallback;
150
+ throw new Error(
151
+ "Server advertises no registration_endpoint and no client_id is configured. Pre-register a client and run `sechroom config set clientId <id>`."
152
+ );
95
153
  }
96
154
  function startLoopback(state) {
97
155
  return new Promise((resolveOuter, rejectOuter) => {
@@ -115,13 +173,13 @@ function startLoopback(state) {
115
173
  }
116
174
  const got = url.searchParams.get("code");
117
175
  const gotState = url.searchParams.get("state");
118
- const err = url.searchParams.get("error");
119
- 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" });
120
178
  res.end(
121
- 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>"
122
180
  );
123
181
  server.close();
124
- if (err) return rejectCode(new Error(`Authorization error: ${err}`));
182
+ if (err2) return rejectCode(new Error(`Authorization error: ${err2}`));
125
183
  if (!got) return rejectCode(new Error("No code in callback."));
126
184
  if (gotState !== state) return rejectCode(new Error("State mismatch \u2014 possible CSRF."));
127
185
  resolveCode(got);
@@ -162,7 +220,7 @@ function persistTokenResponse(json) {
162
220
  }
163
221
  async function login(cfg) {
164
222
  const meta = await discover(cfg.baseUrl);
165
- const clientId = await ensureClientId(meta);
223
+ const clientId = await ensureClientId(meta, cfg.baseUrl);
166
224
  const state = b64url(randomBytes(16));
167
225
  const verifier = b64url(randomBytes(32));
168
226
  const challenge = b64url(createHash("sha256").update(verifier).digest());
@@ -219,6 +277,22 @@ var quiet = false;
219
277
  function setQuiet(q) {
220
278
  quiet = q;
221
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);
222
296
  function active() {
223
297
  return !quiet && Boolean(process.stderr.isTTY);
224
298
  }
@@ -244,12 +318,12 @@ function spinner(text) {
244
318
  return {
245
319
  succeed(t) {
246
320
  clear();
247
- process.stderr.write(`\u2713 ${t ?? text}
321
+ process.stderr.write(`${ok("\u2713")} ${t ?? text}
248
322
  `);
249
323
  },
250
324
  fail(t) {
251
325
  clear();
252
- process.stderr.write(`\u2717 ${t ?? text}
326
+ process.stderr.write(`${err("\u2717")} ${t ?? text}
253
327
  `);
254
328
  },
255
329
  stop: clear
@@ -286,15 +360,48 @@ async function promptText(question, def) {
286
360
  rl.close();
287
361
  }
288
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
+ }
289
396
  async function withSpinner(text, fn) {
290
397
  const s = spinner(text);
291
398
  try {
292
399
  const result = await fn();
293
400
  s.succeed();
294
401
  return result;
295
- } catch (err) {
402
+ } catch (err2) {
296
403
  s.fail();
297
- throw err;
404
+ throw err2;
298
405
  }
299
406
  }
300
407
 
@@ -318,14 +425,25 @@ function emit(data, json) {
318
425
  process.stdout.write(JSON.stringify(data, null, 2) + "\n");
319
426
  }
320
427
  }
428
+ function publicUrl(url) {
429
+ return url.replace(/^https?:\/\/localhost:5012/, "https://sechroom.yi.ocd.codes");
430
+ }
431
+ function emitAction(summary, data, json) {
432
+ if (json) {
433
+ process.stdout.write(JSON.stringify(data) + "\n");
434
+ return;
435
+ }
436
+ process.stdout.write(`${ok("\u2713")} ${summary}
437
+ `);
438
+ }
321
439
  async function runApi(label, fn) {
322
440
  const s = spinner(label);
323
441
  let res;
324
442
  try {
325
443
  res = await fn();
326
- } catch (err) {
444
+ } catch (err2) {
327
445
  s.fail();
328
- fail(err);
446
+ fail(err2);
329
447
  }
330
448
  if (res.error !== void 0 && res.error !== null) {
331
449
  s.fail();
@@ -344,6 +462,16 @@ function fail(error) {
344
462
  // src/commands/memory.ts
345
463
  function registerMemory(program2) {
346
464
  const memory = program2.command("memory").description("Create, read, and search memories");
465
+ memory.addHelpText(
466
+ "after",
467
+ `
468
+ Examples:
469
+ $ sechroom memory create --text "first note" --type reference --tag idea --tag cli
470
+ $ sechroom memory create --text "filed note" --owner-type Workspace --owner-id wsp_XXXX
471
+ $ sechroom memory search "rate limiting" --limit 5 --tag kind:decision
472
+ $ sechroom memory search "auth flow" --workspace wsp_XXXX --json
473
+ $ sechroom memory get mem_XXXX --json`
474
+ );
347
475
  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) => {
348
476
  const cfg = resolveConfig(cmd.optsWithGlobals());
349
477
  const unfiled = String(opts.ownerType).toLowerCase() === "unfiled";
@@ -363,7 +491,9 @@ function registerMemory(program2) {
363
491
  }
364
492
  });
365
493
  });
366
- emit(data, cmd.optsWithGlobals().json);
494
+ const titlePart = opts.title ? ` ${style.dim(`"${opts.title}"`)}` : "";
495
+ const urlPart = data.url ? ` ${style.dim("\u2192")} ${publicUrl(data.url)}` : "";
496
+ emitAction(`created memory ${style.bold(data.id)}${titlePart}${urlPart}`, data, cmd.optsWithGlobals().json);
367
497
  });
368
498
  memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").action(async (memoryId, _opts, cmd) => {
369
499
  const cfg = resolveConfig(cmd.optsWithGlobals());
@@ -398,6 +528,13 @@ function registerMemory(program2) {
398
528
  // src/commands/worklog.ts
399
529
  function registerWorklog(program2) {
400
530
  const worklog = program2.command("worklog").description("Append to the daily work log");
531
+ worklog.addHelpText(
532
+ "after",
533
+ `
534
+ Examples:
535
+ $ sechroom worklog append --text "shipped CLI help + onboarding scope; PR #1430"
536
+ $ sechroom worklog append --text "smoke passed" --source claude-code-chris --title "CLI smoke"`
537
+ );
401
538
  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) => {
402
539
  const cfg = resolveConfig(cmd.optsWithGlobals());
403
540
  const data = await runApi("Appending work-log entry", async () => {
@@ -411,7 +548,7 @@ function registerWorklog(program2) {
411
548
  }
412
549
  });
413
550
  });
414
- emit(data, cmd.optsWithGlobals().json);
551
+ emitAction(`appended work-log entry ${style.bold(data.memoryId)}`, data, cmd.optsWithGlobals().json);
415
552
  });
416
553
  }
417
554
 
@@ -419,6 +556,14 @@ function registerWorklog(program2) {
419
556
  function registerLookup(program2) {
420
557
  program2.command("lookup <id>").description(
421
558
  "Resolve a sechroom id to its kind, title, and view URL (mem_\u2026/wsp_\u2026/prj_\u2026, unprefixed, or sechroom:<id>)"
559
+ ).addHelpText(
560
+ "after",
561
+ `
562
+ Examples:
563
+ $ sechroom lookup mem_XXXX a memory -> kind / title / view URL
564
+ $ sechroom lookup wsp_XXXX a workspace
565
+ $ sechroom lookup sechroom:mem_XXXX namespaced / portable form also resolves
566
+ $ sechroom lookup mem_XXXX --json`
422
567
  ).action(async (id, _opts, cmd) => {
423
568
  const cfg = resolveConfig(cmd.optsWithGlobals());
424
569
  const data = await runApi(`Resolving ${id}`, async () => {
@@ -436,7 +581,7 @@ function registerLookup(program2) {
436
581
 
437
582
  // src/setup/apply.ts
438
583
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
439
- import { dirname } from "path";
584
+ import { dirname as dirname2 } from "path";
440
585
 
441
586
  // src/setup/operator-surface.ts
442
587
  var SectionType = {
@@ -538,7 +683,7 @@ async function createOverride(cfg, template, personalWorkspaceId) {
538
683
  var BLOCK_BEGIN = "<!-- @sechroom/cli:begin (managed \u2014 re-run `sechroom setup agent-files` to refresh) -->";
539
684
  var BLOCK_END = "<!-- @sechroom/cli:end -->";
540
685
  function ensureDir2(path) {
541
- mkdirSync2(dirname(path), { recursive: true });
686
+ mkdirSync2(dirname2(path), { recursive: true });
542
687
  }
543
688
  function readOr(path, fallback) {
544
689
  try {
@@ -634,7 +779,7 @@ async function applyClient(cfg, setup, target, opts) {
634
779
  // src/setup/clients.ts
635
780
  import { existsSync as existsSync3 } from "fs";
636
781
  import { homedir as homedir2 } from "os";
637
- import { dirname as dirname2, join as join2 } from "path";
782
+ import { dirname as dirname3, join as join2 } from "path";
638
783
  function claudeDesktopConfigPath(home) {
639
784
  switch (process.platform) {
640
785
  case "darwin":
@@ -680,7 +825,7 @@ function detectInstalledClients(cwd) {
680
825
  const home = homedir2();
681
826
  const detected = [];
682
827
  if (existsSync3(join2(home, ".claude"))) detected.push("claude-code");
683
- if (existsSync3(dirname2(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
828
+ if (existsSync3(dirname3(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
684
829
  if (existsSync3(join2(home, ".codex"))) detected.push("codex");
685
830
  if (existsSync3(join2(home, ".cursor")) || existsSync3(join2(cwd, ".cursor"))) detected.push("cursor");
686
831
  return detected;
@@ -741,7 +886,16 @@ ${client.label} (${client.key}):
741
886
  }
742
887
  }
743
888
  function registerInit(program2) {
744
- 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) => {
889
+ 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(
890
+ "after",
891
+ `
892
+ Examples:
893
+ $ sechroom init Claude Code (default): ./.mcp.json + ./CLAUDE.md
894
+ $ sechroom init --client all claude-code, claude-desktop, codex, cursor
895
+ $ sechroom init --client codex,cursor
896
+ $ sechroom init --mcp-only just the MCP config (skip agent files)
897
+ $ sechroom init --dry-run --json preview the writes, change nothing`
898
+ ).action(async (opts, cmd) => {
745
899
  const cfg = resolveConfig(cmd.optsWithGlobals());
746
900
  const setup = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
747
901
  const targets = clientTargets(process.cwd());
@@ -823,11 +977,12 @@ function systemTimezone() {
823
977
  return "UTC";
824
978
  }
825
979
  }
826
- async function ensureConfig(g, yes) {
980
+ async function ensureConfig(g, opts) {
827
981
  const persisted = readPersisted();
828
- let baseUrl = g.baseUrl ?? process.env.SECHROOM_BASE_URL ?? persisted.baseUrl ?? DEFAULT_BASE_URL2;
829
- let tenant = g.tenant ?? process.env.SECHROOM_TENANT ?? persisted.tenant ?? "";
830
- if (canPrompt() && !yes) {
982
+ const local = readLocalConfig();
983
+ let baseUrl = g.baseUrl ?? process.env.SECHROOM_BASE_URL ?? local.baseUrl ?? persisted.baseUrl ?? DEFAULT_BASE_URL2;
984
+ let tenant = g.tenant ?? process.env.SECHROOM_TENANT ?? local.tenant ?? persisted.tenant ?? "";
985
+ if (canPrompt() && !opts.yes) {
831
986
  baseUrl = await promptText("Sechroom API base URL?", baseUrl);
832
987
  tenant = await promptText("Tenant id?", tenant || void 0);
833
988
  }
@@ -837,7 +992,26 @@ async function ensureConfig(g, yes) {
837
992
  "No tenant set. Pass --tenant <id>, set SECHROOM_TENANT, or run `sechroom config set tenant <id>` \u2014 the API rejects untenanted requests (HTTP 400)."
838
993
  );
839
994
  }
840
- writePersisted({ baseUrl, tenant });
995
+ let storeLocal = Boolean(opts.local);
996
+ if (!opts.local && canPrompt() && !opts.yes) {
997
+ storeLocal = await promptSelect(
998
+ "Where should this tenant + base URL be saved?",
999
+ [
1000
+ { label: "Globally", value: "global", hint: "all projects on this machine" },
1001
+ { label: "This directory", value: "local", hint: ".sechroom.json \u2014 project + subdirs" }
1002
+ ],
1003
+ local.path ? "local" : "global"
1004
+ ) === "local";
1005
+ }
1006
+ if (storeLocal) {
1007
+ const path = writeLocalConfig({ baseUrl, tenant });
1008
+ if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved to ${path} (directory-local)
1009
+ `);
1010
+ } else {
1011
+ writePersisted({ baseUrl, tenant });
1012
+ if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved globally (~/.config/sechroom/config.json)
1013
+ `);
1014
+ }
841
1015
  return { baseUrl, tenant, clientId: persisted.clientId };
842
1016
  }
843
1017
  async function ensureAuth(cfg, yes) {
@@ -891,21 +1065,45 @@ Available clients: ${ALL_CLIENT_KEYS.join(", ")}
891
1065
  return resolveClientKeys(answer);
892
1066
  }
893
1067
  function registerOnboard(program2) {
894
- 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) => {
1068
+ 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(
1069
+ "after",
1070
+ `
1071
+ Examples:
1072
+ $ sechroom onboard guided, interactive (asks where to save config + how to wire)
1073
+ $ sechroom onboard --cli-only just the CLI \u2014 no .mcp.json, no agent files
1074
+ $ sechroom onboard --no-mcp agent instructions only, skip MCP config
1075
+ $ sechroom onboard --local save tenant + base URL to ./.sechroom.json
1076
+ $ sechroom onboard --yes non-interactive: defaults + global config + full wire
1077
+ $ sechroom onboard --client all --dry-run preview wiring every client, write nothing`
1078
+ ).action(async (opts, cmd) => {
895
1079
  const g = cmd.optsWithGlobals();
896
1080
  const json = Boolean(g.json);
897
1081
  const yes = Boolean(opts.yes);
898
1082
  const dryRun = Boolean(opts.dryRun);
899
- const cfg = await ensureConfig(g, yes);
1083
+ const cfg = await ensureConfig(g, { yes, json, local: Boolean(opts.local) });
900
1084
  await ensureAuth(cfg, yes);
901
1085
  const tz = await ensureTimezone(cfg, { yes, dryRun });
902
1086
  if (!json && tz.action !== "already-set") {
903
- const line = tz.action === "set" ? `\u2713 timezone set to ${tz.timezone}
1087
+ const line = tz.action === "set" ? `${ok("\u2713")} timezone set to ${tz.timezone}
904
1088
  ` : tz.action === "dry-run" ? `(dry run \u2014 would set timezone to ${tz.timezone})
905
1089
  ` : `timezone not set \u2014 ${tz.note}
906
1090
  `;
907
1091
  process.stderr.write(line);
908
1092
  }
1093
+ const wire = await chooseWire(opts, yes);
1094
+ if (wire === "cli-only") {
1095
+ if (json) {
1096
+ emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, timezone: tz, wire, clients: [] }, true);
1097
+ return;
1098
+ }
1099
+ process.stdout.write(
1100
+ `
1101
+ ${style.bold("Done.")} The CLI is configured for ${style.cyan(cfg.tenant)} \u2014 no AI-client files written.
1102
+ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom --help")}
1103
+ `
1104
+ );
1105
+ return;
1106
+ }
909
1107
  const keys = await chooseClients(opts.client, yes, process.cwd());
910
1108
  const setup = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
911
1109
  const targets = clientTargets(process.cwd());
@@ -913,12 +1111,13 @@ function registerOnboard(program2) {
913
1111
  if (!dryRun) {
914
1112
  await maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, copyChoice(opts));
915
1113
  }
1114
+ const writeMcp = wire === "full";
916
1115
  const result = [];
917
1116
  for (const key of keys) {
918
1117
  const target = targets[key];
919
1118
  const actions = await applyClient(cfg, setup, target, {
920
1119
  dryRun,
921
- mcp: true,
1120
+ mcp: writeMcp,
922
1121
  agentFiles: true,
923
1122
  personalWorkspaceId
924
1123
  });
@@ -926,14 +1125,33 @@ function registerOnboard(program2) {
926
1125
  if (!json) printActions(target, actions);
927
1126
  }
928
1127
  if (json) {
929
- emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, timezone: tz, clients: result }, true);
1128
+ emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, timezone: tz, wire, clients: result }, true);
930
1129
  return;
931
1130
  }
932
1131
  process.stdout.write(
933
- dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone. Restart your AI client (or reload MCP) to pick up the new config.\n"
1132
+ dryRun ? "\n(dry run \u2014 nothing written)\n" : writeMcp ? `
1133
+ ${style.bold("Done.")} Restart your AI client (or reload MCP) to pick up the new config.
1134
+ ` : `
1135
+ ${style.bold("Done.")} Agent instructions written (no MCP config).
1136
+ `
934
1137
  );
935
1138
  });
936
1139
  }
1140
+ async function chooseWire(opts, yes) {
1141
+ if (opts.cliOnly) return "cli-only";
1142
+ if (canPrompt() && !yes) {
1143
+ return promptSelect(
1144
+ "How should I set up Sechroom in this project?",
1145
+ [
1146
+ { label: "Wire my AI client", value: "full", hint: "MCP server (.mcp.json) + agent instructions" },
1147
+ { label: "Agent instructions only", value: "agent-only", hint: "skip MCP config" },
1148
+ { label: "CLI only", value: "cli-only", hint: "don't write any AI-client files" }
1149
+ ],
1150
+ "full"
1151
+ );
1152
+ }
1153
+ return opts.mcp === false ? "agent-only" : "full";
1154
+ }
937
1155
 
938
1156
  // src/index.ts
939
1157
  function resolveVersion() {
@@ -948,17 +1166,67 @@ function resolveVersion() {
948
1166
  }
949
1167
  var program = new Command();
950
1168
  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);
1169
+ program.addHelpText(
1170
+ "after",
1171
+ `
1172
+ Examples:
1173
+ $ sechroom onboard guided first-run: configure, sign in, wire this project
1174
+ $ sechroom login sign in via browser (OAuth + PKCE)
1175
+ $ sechroom config set tenant ocd set your tenant (global)
1176
+ $ sechroom config set --local tenant cli-smoke pin tenant for this directory (.sechroom.json)
1177
+ $ sechroom config show resolved config + which source won
1178
+
1179
+ $ sechroom memory create --text "a note" --title "Note" --tag idea
1180
+ $ sechroom memory search "convention drift" --limit 5
1181
+ $ sechroom memory get mem_XXXX
1182
+ $ sechroom worklog append --text "shipped X; PR #123" --source claude-code-chris
1183
+ $ sechroom lookup mem_XXXX resolve any id -> kind / title / view URL
1184
+
1185
+ $ sechroom --json memory search "auth" compact JSON for scripts and agents
1186
+ $ SECHROOM_TOKEN=<bearer> sechroom --json memory get mem_XXXX headless
1187
+
1188
+ Config precedence (high -> low): --flag > env (SECHROOM_*) > ./.sechroom.json > global > default.
1189
+ Run 'sechroom <command> --help' for command-specific examples.`
1190
+ );
951
1191
  program.hook("preAction", (_thisCmd, actionCmd) => {
952
1192
  setQuiet(Boolean(actionCmd.optsWithGlobals().json));
953
1193
  });
954
- program.command("login").description("Sign in via browser (OAuth auth-code + PKCE, dynamic client registration)").action(async (_opts, cmd) => {
1194
+ program.command("login").description("Sign in via browser (OAuth auth-code + PKCE, dynamic client registration)").addHelpText(
1195
+ "after",
1196
+ `
1197
+ Examples:
1198
+ $ sechroom login sign in to the configured base URL + tenant
1199
+ $ sechroom login --base-url https://staging.app.sechroom.ai/api
1200
+ $ export SECHROOM_TOKEN=<bearer> headless: skip login entirely (CI / agents)`
1201
+ ).action(async (_opts, cmd) => {
955
1202
  const g = cmd.optsWithGlobals();
956
1203
  const persisted = readPersisted();
957
1204
  const baseUrl = g.baseUrl ?? process.env.SECHROOM_BASE_URL ?? persisted.baseUrl ?? "https://app.sechroom.ai/api";
958
1205
  await login({ baseUrl: baseUrl.replace(/\/$/, ""), tenant: g.tenant ?? "" });
959
1206
  });
960
1207
  var config = program.command("config").description("Manage persisted CLI config");
961
- config.command("set <key> <value>").description("Set baseUrl | tenant | clientId").action((key, value) => {
1208
+ config.addHelpText(
1209
+ "after",
1210
+ `
1211
+ Examples:
1212
+ $ sechroom config set baseUrl https://app.sechroom.ai/api prod (staging: https://staging.app.sechroom.ai/api)
1213
+ $ sechroom config set tenant ocd
1214
+ $ sechroom config set --local tenant cli-smoke this dir + subdirs (.sechroom.json)
1215
+ $ sechroom config set clientId dyn-XXXX global-only escape hatch (no DCR endpoint)
1216
+ $ sechroom config show --json`
1217
+ );
1218
+ 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) => {
1219
+ if (opts.local) {
1220
+ if (!["baseUrl", "tenant"].includes(key)) {
1221
+ process.stderr.write(`--local supports only: baseUrl | tenant (clientId is global)
1222
+ `);
1223
+ process.exit(1);
1224
+ }
1225
+ const path = writeLocalConfig({ [key]: value });
1226
+ process.stdout.write(`set ${key} (local: ${path})
1227
+ `);
1228
+ return;
1229
+ }
962
1230
  if (!["baseUrl", "tenant", "clientId"].includes(key)) {
963
1231
  process.stderr.write(`unknown key: ${key} (expected baseUrl | tenant | clientId)
964
1232
  `);
@@ -968,8 +1236,27 @@ config.command("set <key> <value>").description("Set baseUrl | tenant | clientId
968
1236
  process.stdout.write(`set ${key}
969
1237
  `);
970
1238
  });
971
- config.command("show").description("Print persisted config").action(() => {
972
- process.stdout.write(JSON.stringify(readPersisted(), null, 2) + "\n");
1239
+ config.command("show").description("Print resolved config + sources (flag > env > local > global > default)").action((_opts, cmd) => {
1240
+ const g = cmd.optsWithGlobals();
1241
+ const d = describeConfig({ baseUrl: g.baseUrl, tenant: g.tenant });
1242
+ if (g.json) {
1243
+ process.stdout.write(
1244
+ JSON.stringify({
1245
+ resolved: { baseUrl: d.baseUrl, tenant: d.tenant },
1246
+ global: readPersisted(),
1247
+ local: readLocalConfig()
1248
+ }) + "\n"
1249
+ );
1250
+ return;
1251
+ }
1252
+ process.stdout.write(
1253
+ `baseUrl: ${d.baseUrl.value} [${d.baseUrl.source}]
1254
+ tenant: ${d.tenant.value ?? "(unset)"} [${d.tenant.source}]
1255
+
1256
+ global: ${JSON.stringify(readPersisted())}
1257
+ local: ${d.localPath ?? "(none)"} ${JSON.stringify(readLocalConfig())}
1258
+ `
1259
+ );
973
1260
  });
974
1261
  registerMemory(program);
975
1262
  registerWorklog(program);
@@ -977,8 +1264,8 @@ registerLookup(program);
977
1264
  registerInit(program);
978
1265
  registerSetup(program);
979
1266
  registerOnboard(program);
980
- program.parseAsync().catch((err) => {
981
- process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
1267
+ program.parseAsync().catch((err2) => {
1268
+ process.stderr.write(`error: ${err2 instanceof Error ? err2.message : String(err2)}
982
1269
  `);
983
1270
  process.exit(1);
984
1271
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sechroom/cli",
3
- "version": "2026.6.5",
3
+ "version": "2026.6.7",
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",