@sechroom/cli 2026.6.2 → 2026.6.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 (3) hide show
  1. package/README.md +38 -1
  2. package/dist/index.js +427 -46
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -103,11 +103,43 @@ sechroom --json memory get mem_XXXX # agent-friendly
103
103
  Headless:
104
104
 
105
105
  ```bash
106
- export SECHROOM_TOKEN="<jwt from /auth/dev/token>"
106
+ export SECHROOM_TOKEN="<bearer: a PAT or dev-token JWT>"
107
107
  export SECHROOM_TENANT=ocd
108
108
  sechroom --json memory search "rate limiting"
109
109
  ```
110
110
 
111
+ ## Onboarding (`init` / `setup`)
112
+
113
+ `sechroom init` wires a project for sechroom by rendering the server's
114
+ operator-surface setup descriptors (`GET /operator-surface/setup`, tenant-scoped
115
+ — the aggregator URL is baked in) into local AI-client config + agent instruction
116
+ files. **Idempotent — merges/appends, never clobbers** (JSON `mcpServers` merge;
117
+ Codex TOML table replace; instruction files use a managed marker block).
118
+
119
+ ```bash
120
+ sechroom init # Claude Code (default): .mcp.json + CLAUDE.md
121
+ sechroom init --client all # claude-code, claude-desktop, codex, cursor
122
+ sechroom init --client codex,cursor # a subset
123
+ sechroom init --dry-run --json # preview the writes, no changes
124
+
125
+ # granular pieces init orchestrates:
126
+ sechroom setup mcp claude-desktop # just the MCP config for one client
127
+ sechroom setup agent-files codex # just the AGENTS.md instruction file
128
+ ```
129
+
130
+ Per client → where it writes:
131
+
132
+ | client | MCP config | instruction file |
133
+ |---|---|---|
134
+ | `claude-code` | `./.mcp.json` | `./CLAUDE.md` |
135
+ | `claude-desktop` | `claude_desktop_config.json` (OS path) | `~/.claude/CLAUDE.md` |
136
+ | `codex` | `~/.codex/config.toml` | `./AGENTS.md` |
137
+ | `cursor` | `./.cursor/mcp.json` | `./AGENTS.md` |
138
+
139
+ The instruction-file step pulls the role template the SEM Starter bundle ships
140
+ (via the descriptor's tag-query artifact). If that bundle isn't installed in the
141
+ tenant, the step **skips gracefully** with a note (MCP config still gets written).
142
+
111
143
  ## Layout
112
144
 
113
145
  ```
@@ -121,6 +153,11 @@ src/
121
153
  memory.ts create / get / search
122
154
  worklog.ts append
123
155
  lookup.ts resolve any id (mem_…/unprefixed/sechroom:<id>) -> kind/title/url
156
+ setup.ts init + setup mcp/agent-files
157
+ setup/
158
+ operator-surface.ts fetch GET /operator-surface/setup + resolve template artifacts
159
+ clients.ts client→(surface, local paths, format) registry
160
+ apply.ts writers: mcp json merge / codex toml / instruction marker block
124
161
  ```
125
162
 
126
163
  ## Remaining before ship
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import { readFileSync as readFileSync3 } from "fs";
4
5
  import { Command } from "commander";
5
6
 
6
7
  // src/auth.ts
@@ -211,6 +212,62 @@ async function requireToken(cfg) {
211
212
 
212
213
  // src/client.ts
213
214
  import createClient from "openapi-fetch";
215
+
216
+ // src/ui.ts
217
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
218
+ var quiet = false;
219
+ function setQuiet(q) {
220
+ quiet = q;
221
+ }
222
+ function active() {
223
+ return !quiet && Boolean(process.stderr.isTTY);
224
+ }
225
+ function spinner(text) {
226
+ if (!active()) {
227
+ return { succeed() {
228
+ }, fail() {
229
+ }, stop() {
230
+ } };
231
+ }
232
+ let i = 0;
233
+ const render = () => {
234
+ i = (i + 1) % FRAMES.length;
235
+ process.stderr.write(`\r${FRAMES[i]} ${text}`);
236
+ };
237
+ process.stderr.write(`\r${FRAMES[0]} ${text}`);
238
+ const timer = setInterval(render, 80);
239
+ timer.unref?.();
240
+ const clear = () => {
241
+ clearInterval(timer);
242
+ process.stderr.write("\r\x1B[K");
243
+ };
244
+ return {
245
+ succeed(t) {
246
+ clear();
247
+ process.stderr.write(`\u2713 ${t ?? text}
248
+ `);
249
+ },
250
+ fail(t) {
251
+ clear();
252
+ process.stderr.write(`\u2717 ${t ?? text}
253
+ `);
254
+ },
255
+ stop: clear
256
+ };
257
+ }
258
+ async function withSpinner(text, fn) {
259
+ const s = spinner(text);
260
+ try {
261
+ const result = await fn();
262
+ s.succeed();
263
+ return result;
264
+ } catch (err) {
265
+ s.fail();
266
+ throw err;
267
+ }
268
+ }
269
+
270
+ // src/client.ts
214
271
  async function makeClient(cfg) {
215
272
  const token = await requireToken(cfg);
216
273
  const client = createClient({ baseUrl: cfg.baseUrl });
@@ -230,6 +287,22 @@ function emit(data, json) {
230
287
  process.stdout.write(JSON.stringify(data, null, 2) + "\n");
231
288
  }
232
289
  }
290
+ async function runApi(label, fn) {
291
+ const s = spinner(label);
292
+ let res;
293
+ try {
294
+ res = await fn();
295
+ } catch (err) {
296
+ s.fail();
297
+ fail(err);
298
+ }
299
+ if (res.error !== void 0 && res.error !== null) {
300
+ s.fail();
301
+ fail(res.error);
302
+ }
303
+ s.succeed();
304
+ return res.data;
305
+ }
233
306
  function fail(error) {
234
307
  const msg = typeof error === "object" && error !== null && "title" in error ? String(error.title) : typeof error === "object" ? JSON.stringify(error) : String(error);
235
308
  process.stderr.write(`error: ${msg}
@@ -242,50 +315,51 @@ function registerMemory(program2) {
242
315
  const memory = program2.command("memory").description("Create, read, and search memories");
243
316
  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) => {
244
317
  const cfg = resolveConfig(cmd.optsWithGlobals());
245
- const client = await makeClient(cfg);
246
318
  const unfiled = String(opts.ownerType).toLowerCase() === "unfiled";
247
- const { data, error } = await client.POST("/memories", {
248
- body: {
249
- text: opts.text,
250
- type: opts.type,
251
- content: "{}",
252
- confidence: Number(opts.confidence),
253
- source: opts.source,
254
- archetype: "Document",
255
- title: opts.title ?? null,
256
- tags: opts.tag ?? null,
257
- owner: unfiled ? null : { type: opts.ownerType, id: String(opts.ownerId ?? "") }
258
- }
319
+ const data = await runApi("Creating memory", async () => {
320
+ const client = await makeClient(cfg);
321
+ return client.POST("/memories", {
322
+ body: {
323
+ text: opts.text,
324
+ type: opts.type,
325
+ content: "{}",
326
+ confidence: Number(opts.confidence),
327
+ source: opts.source,
328
+ archetype: "Document",
329
+ title: opts.title ?? null,
330
+ tags: opts.tag ?? null,
331
+ owner: unfiled ? null : { type: opts.ownerType, id: String(opts.ownerId ?? "") }
332
+ }
333
+ });
259
334
  });
260
- if (error) fail(error);
261
335
  emit(data, cmd.optsWithGlobals().json);
262
336
  });
263
337
  memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").action(async (memoryId, _opts, cmd) => {
264
338
  const cfg = resolveConfig(cmd.optsWithGlobals());
265
- const client = await makeClient(cfg);
266
- const { data, error } = await client.GET("/memories/{memoryId}", {
267
- params: { path: { memoryId } }
339
+ const data = await runApi("Fetching memory", async () => {
340
+ const client = await makeClient(cfg);
341
+ return client.GET("/memories/{memoryId}", { params: { path: { memoryId } } });
268
342
  });
269
- if (error) fail(error);
270
343
  emit(data, cmd.optsWithGlobals().json);
271
344
  });
272
345
  memory.command("search <query>").description("Hybrid search (POST /memories/search; SemanticQuery -> vector+FTS RRF)").option("--limit <n>", "Max results", "10").option("--tag <tag...>", "Require all listed tags").option("--workspace <workspaceId>", "Scope to a workspace (cascades to its projects)").option("--include-archived", "Include archived memories", false).action(async (query, opts, cmd) => {
273
346
  const cfg = resolveConfig(cmd.optsWithGlobals());
274
- const client = await makeClient(cfg);
275
- const { data, error } = await client.POST("/memories/search", {
276
- body: {
277
- query: null,
278
- textQuery: null,
279
- semanticQuery: query,
280
- hybrid: true,
281
- limit: Number(opts.limit),
282
- includeArchived: Boolean(opts.includeArchived),
283
- includeSystem: false,
284
- ...opts.tag ? { tags: opts.tag } : {},
285
- ...opts.workspace ? { workspaceId: opts.workspace } : {}
286
- }
347
+ const data = await runApi("Searching", async () => {
348
+ const client = await makeClient(cfg);
349
+ return client.POST("/memories/search", {
350
+ body: {
351
+ query: null,
352
+ textQuery: null,
353
+ semanticQuery: query,
354
+ hybrid: true,
355
+ limit: Number(opts.limit),
356
+ includeArchived: Boolean(opts.includeArchived),
357
+ includeSystem: false,
358
+ ...opts.tag ? { tags: opts.tag } : {},
359
+ ...opts.workspace ? { workspaceId: opts.workspace } : {}
360
+ }
361
+ });
287
362
  });
288
- if (error) fail(error);
289
363
  emit(data, cmd.optsWithGlobals().json);
290
364
  });
291
365
  }
@@ -295,16 +369,17 @@ function registerWorklog(program2) {
295
369
  const worklog = program2.command("worklog").description("Append to the daily work log");
296
370
  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) => {
297
371
  const cfg = resolveConfig(cmd.optsWithGlobals());
298
- const client = await makeClient(cfg);
299
- const { data, error } = await client.POST("/operator-surface/work-log/append", {
300
- body: {
301
- bullet: opts.text,
302
- laneId: opts.source ?? null,
303
- workspaceId: opts.workspace ?? null,
304
- title: opts.title ?? null
305
- }
372
+ const data = await runApi("Appending work-log entry", async () => {
373
+ const client = await makeClient(cfg);
374
+ return client.POST("/operator-surface/work-log/append", {
375
+ body: {
376
+ bullet: opts.text,
377
+ laneId: opts.source ?? null,
378
+ workspaceId: opts.workspace ?? null,
379
+ title: opts.title ?? null
380
+ }
381
+ });
306
382
  });
307
- if (error) fail(error);
308
383
  emit(data, cmd.optsWithGlobals().json);
309
384
  });
310
385
  }
@@ -315,11 +390,10 @@ function registerLookup(program2) {
315
390
  "Resolve a sechroom id to its kind, title, and view URL (mem_\u2026/wsp_\u2026/prj_\u2026, unprefixed, or sechroom:<id>)"
316
391
  ).action(async (id, _opts, cmd) => {
317
392
  const cfg = resolveConfig(cmd.optsWithGlobals());
318
- const client = await makeClient(cfg);
319
- const { data, error } = await client.GET("/lookup/{id}", {
320
- params: { path: { id } }
393
+ const data = await runApi(`Resolving ${id}`, async () => {
394
+ const client = await makeClient(cfg);
395
+ return client.GET("/lookup/{id}", { params: { path: { id } } });
321
396
  });
322
- if (error) fail(error);
323
397
  if (data == null) {
324
398
  process.stderr.write(`not found: ${id}
325
399
  `);
@@ -329,9 +403,314 @@ function registerLookup(program2) {
329
403
  });
330
404
  }
331
405
 
406
+ // src/setup/apply.ts
407
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
408
+ import { dirname } from "path";
409
+
410
+ // src/setup/operator-surface.ts
411
+ var SectionType = {
412
+ McpConfig: "mcp-config",
413
+ McpConfigToml: "mcp-config-toml",
414
+ InstructionFile: "instruction-file",
415
+ ProjectConfig: "project-config",
416
+ Verify: "verify"
417
+ };
418
+ async function fetchSetup(cfg) {
419
+ const client = await makeClient(cfg);
420
+ const { data, error } = await client.GET("/operator-surface/setup", {});
421
+ if (error) throw new Error(`GET /operator-surface/setup failed: ${JSON.stringify(error)}`);
422
+ return data;
423
+ }
424
+ function findSurface(setup, surfaceKey) {
425
+ return setup.surfaces.find((s) => s.surfaceKey === surfaceKey);
426
+ }
427
+ function findSection(surface, sectionType) {
428
+ return surface?.sections.find((s) => s.sectionType === sectionType);
429
+ }
430
+ function sectionSnippet(section) {
431
+ if (!section) return null;
432
+ for (const step of section.steps) {
433
+ if (step.copyValue) return step.copyValue;
434
+ if (step.codeSnippet) return step.codeSnippet;
435
+ }
436
+ return null;
437
+ }
438
+ function parseTagArtifactId(id) {
439
+ if (!id.startsWith("tag:")) return null;
440
+ const tags = id.slice("tag:".length).split(",").map((t) => t.trim()).filter((t) => t.length > 0);
441
+ return tags.length > 0 ? tags : null;
442
+ }
443
+ async function resolveInstructionBody(cfg, section) {
444
+ const client = await makeClient(cfg);
445
+ for (const artifact of section.artifacts) {
446
+ const tags = parseTagArtifactId(artifact.id);
447
+ if (!tags) continue;
448
+ const { data } = await client.POST("/memories/search", {
449
+ body: {
450
+ query: null,
451
+ textQuery: null,
452
+ semanticQuery: artifact.title ?? "role instruction template",
453
+ hybrid: true,
454
+ limit: 1,
455
+ includeArchived: false,
456
+ includeSystem: false,
457
+ tags
458
+ }
459
+ });
460
+ const hits = data ?? [];
461
+ if (hits.length === 0) continue;
462
+ const memId = hits[0].id;
463
+ const { data: memEnvelope } = await client.GET("/memories/{memoryId}", {
464
+ params: { path: { memoryId: memId } }
465
+ });
466
+ const env = memEnvelope;
467
+ const body = env?.item?.text ?? env?.text;
468
+ if (typeof body === "string" && body.length > 0) {
469
+ return { title: env?.item?.title ?? env?.title ?? artifact.title, body };
470
+ }
471
+ }
472
+ return null;
473
+ }
474
+
475
+ // src/setup/apply.ts
476
+ var BLOCK_BEGIN = "<!-- @sechroom/cli:begin (managed \u2014 re-run `sechroom setup agent-files` to refresh) -->";
477
+ var BLOCK_END = "<!-- @sechroom/cli:end -->";
478
+ function ensureDir2(path) {
479
+ mkdirSync2(dirname(path), { recursive: true });
480
+ }
481
+ function readOr(path, fallback) {
482
+ try {
483
+ return readFileSync2(path, "utf8");
484
+ } catch {
485
+ return fallback;
486
+ }
487
+ }
488
+ function mergeMcpJson(path, snippet, dryRun) {
489
+ const incoming = JSON.parse(snippet);
490
+ const existed = existsSync2(path);
491
+ let current = {};
492
+ if (existed) {
493
+ try {
494
+ current = JSON.parse(readFileSync2(path, "utf8"));
495
+ } catch {
496
+ return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
497
+ }
498
+ }
499
+ current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
500
+ if (dryRun) return { kind: "mcp", path, status: "dry-run" };
501
+ ensureDir2(path);
502
+ writeFileSync2(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
503
+ return { kind: "mcp", path, status: existed ? "merged" : "created" };
504
+ }
505
+ function mergeCodexToml(path, snippet, dryRun) {
506
+ const existed = existsSync2(path);
507
+ let body = readOr(path, "");
508
+ body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
509
+ const trimmed = body.trim();
510
+ const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
511
+ if (dryRun) return { kind: "mcp", path, status: "dry-run" };
512
+ ensureDir2(path);
513
+ writeFileSync2(path, next, { mode: 384 });
514
+ return { kind: "mcp", path, status: existed ? "merged" : "created" };
515
+ }
516
+ function writeInstructionBlock(path, body, dryRun) {
517
+ const block = `${BLOCK_BEGIN}
518
+ ${body.trim()}
519
+ ${BLOCK_END}
520
+ `;
521
+ const existed = existsSync2(path);
522
+ const current = readOr(path, "");
523
+ let next;
524
+ const re = new RegExp(`${escapeRe(BLOCK_BEGIN)}[\\s\\S]*?${escapeRe(BLOCK_END)}\\n?`);
525
+ if (re.test(current)) {
526
+ next = current.replace(re, block);
527
+ } else {
528
+ next = current.trim().length > 0 ? `${current.trimEnd()}
529
+
530
+ ${block}` : block;
531
+ }
532
+ if (dryRun) return { kind: "instruction", path, status: "dry-run" };
533
+ ensureDir2(path);
534
+ writeFileSync2(path, next);
535
+ return { kind: "instruction", path, status: existed ? "merged" : "created" };
536
+ }
537
+ function escapeRe(s) {
538
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
539
+ }
540
+ async function applyClient(cfg, setup, target, opts) {
541
+ const actions = [];
542
+ if (opts.mcp && target.mcp) {
543
+ const surface = findSurface(setup, target.mcp.surfaceKey);
544
+ const section = findSection(surface, target.mcp.sectionType);
545
+ const snippet = sectionSnippet(section);
546
+ if (!snippet) {
547
+ actions.push({ kind: "mcp", path: target.mcp.path, status: "skipped", note: `no ${target.mcp.sectionType} section on surface '${target.mcp.surfaceKey}'` });
548
+ } else {
549
+ actions.push(
550
+ target.mcp.format === "toml" ? mergeCodexToml(target.mcp.path, snippet, opts.dryRun) : mergeMcpJson(target.mcp.path, snippet, opts.dryRun)
551
+ );
552
+ }
553
+ }
554
+ if (opts.agentFiles && target.instruction) {
555
+ const surface = findSurface(setup, target.instruction.surfaceKey);
556
+ const section = findSection(surface, SectionType.InstructionFile);
557
+ if (!section) {
558
+ actions.push({ kind: "instruction", path: target.instruction.path, status: "skipped", note: `no instruction-file section on surface '${target.instruction.surfaceKey}'` });
559
+ } else {
560
+ const resolved = await resolveInstructionBody(cfg, section);
561
+ if (!resolved) {
562
+ 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`" });
563
+ } else {
564
+ actions.push(writeInstructionBlock(target.instruction.path, resolved.body, opts.dryRun));
565
+ }
566
+ }
567
+ }
568
+ return actions;
569
+ }
570
+
571
+ // src/setup/clients.ts
572
+ import { homedir as homedir2 } from "os";
573
+ import { join as join2 } from "path";
574
+ function claudeDesktopConfigPath(home) {
575
+ switch (process.platform) {
576
+ case "darwin":
577
+ return join2(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
578
+ case "win32":
579
+ return join2(process.env.APPDATA ?? join2(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
580
+ default:
581
+ return join2(home, ".config", "Claude", "claude_desktop_config.json");
582
+ }
583
+ }
584
+ function clientTargets(cwd) {
585
+ const home = homedir2();
586
+ return {
587
+ "claude-code": {
588
+ key: "claude-code",
589
+ label: "Claude Code",
590
+ mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join2(cwd, ".mcp.json"), format: "json" },
591
+ instruction: { surfaceKey: "claude-code", path: join2(cwd, "CLAUDE.md") }
592
+ },
593
+ "claude-desktop": {
594
+ key: "claude-desktop",
595
+ label: "Claude Desktop",
596
+ mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
597
+ instruction: { surfaceKey: "claude-desktop", path: join2(home, ".claude", "CLAUDE.md") }
598
+ },
599
+ codex: {
600
+ key: "codex",
601
+ label: "Codex CLI",
602
+ mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join2(home, ".codex", "config.toml"), format: "toml" },
603
+ instruction: { surfaceKey: "chatgpt", path: join2(cwd, "AGENTS.md") }
604
+ },
605
+ cursor: {
606
+ key: "cursor",
607
+ label: "Cursor",
608
+ mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join2(cwd, ".cursor", "mcp.json"), format: "json" },
609
+ instruction: { surfaceKey: "chatgpt", path: join2(cwd, "AGENTS.md") }
610
+ }
611
+ };
612
+ }
613
+ var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor"];
614
+ var DEFAULT_CLIENT_KEY = "claude-code";
615
+
616
+ // src/commands/setup.ts
617
+ function resolveClientKeys(raw) {
618
+ const targets = clientTargets(process.cwd());
619
+ if (raw === "all") return [...ALL_CLIENT_KEYS];
620
+ const keys = raw.split(",").map((k) => k.trim()).filter(Boolean);
621
+ for (const k of keys) {
622
+ if (!targets[k]) {
623
+ fail(`unknown client '${k}'. Known: ${ALL_CLIENT_KEYS.join(", ")}, or 'all'.`);
624
+ }
625
+ }
626
+ return keys;
627
+ }
628
+ function printActions(client, actions) {
629
+ process.stdout.write(`
630
+ ${client.label} (${client.key}):
631
+ `);
632
+ for (const a of actions) {
633
+ const tag = a.status === "skipped" ? "skip" : a.status;
634
+ process.stdout.write(` [${tag}] ${a.kind}: ${a.path}${a.note ? ` \u2014 ${a.note}` : ""}
635
+ `);
636
+ }
637
+ }
638
+ function registerInit(program2) {
639
+ 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).action(async (opts, cmd) => {
640
+ const cfg = resolveConfig(cmd.optsWithGlobals());
641
+ const setup = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
642
+ const targets = clientTargets(process.cwd());
643
+ const keys = resolveClientKeys(opts.client);
644
+ const json = cmd.optsWithGlobals().json;
645
+ const result = [];
646
+ for (const key of keys) {
647
+ const target = targets[key];
648
+ const actions = await applyClient(cfg, setup, target, {
649
+ dryRun: Boolean(opts.dryRun),
650
+ mcp: !opts.agentFilesOnly,
651
+ agentFiles: !opts.mcpOnly
652
+ });
653
+ result.push({ client: key, actions });
654
+ if (!json) printActions(target, actions);
655
+ }
656
+ if (json) {
657
+ emit({ dryRun: Boolean(opts.dryRun), clients: result }, true);
658
+ return;
659
+ }
660
+ const first = targets[keys[0]];
661
+ const surface = findSurface(setup, first.mcp?.surfaceKey ?? first.instruction?.surfaceKey ?? "");
662
+ const verify = findSection(surface, SectionType.Verify);
663
+ if (verify?.description) {
664
+ process.stdout.write(`
665
+ Next \u2014 verify: ${verify.description}
666
+ `);
667
+ }
668
+ process.stdout.write(
669
+ opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone. Restart your AI client (or reload MCP) to pick up the new config.\n"
670
+ );
671
+ });
672
+ }
673
+ function registerSetup(program2) {
674
+ const setup = program2.command("setup").description("Granular onboarding steps (init runs these together)");
675
+ setup.command("mcp <client>").description(`Write only the MCP config for a client (${ALL_CLIENT_KEYS.join(", ")})`).option("--dry-run", "print what would be written without writing", false).action(async (client, opts, cmd) => {
676
+ await runSingle(client, cmd, { dryRun: Boolean(opts.dryRun), mcp: true, agentFiles: false });
677
+ });
678
+ setup.command("agent-files <client>").description(`Write only the agent instruction file for a client (${ALL_CLIENT_KEYS.join(", ")})`).option("--dry-run", "print what would be written without writing", false).action(async (client, opts, cmd) => {
679
+ await runSingle(client, cmd, { dryRun: Boolean(opts.dryRun), mcp: false, agentFiles: true });
680
+ });
681
+ }
682
+ async function runSingle(client, cmd, opts) {
683
+ const cfg = resolveConfig(cmd.optsWithGlobals());
684
+ const targets = clientTargets(process.cwd());
685
+ const target = targets[client];
686
+ if (!target) fail(`unknown client '${client}'. Known: ${ALL_CLIENT_KEYS.join(", ")}.`);
687
+ const setupData = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
688
+ const actions = await applyClient(cfg, setupData, target, opts);
689
+ const json = cmd.optsWithGlobals().json;
690
+ if (json) {
691
+ emit({ dryRun: opts.dryRun, client, actions }, true);
692
+ } else {
693
+ printActions(target, actions);
694
+ process.stdout.write(opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n");
695
+ }
696
+ }
697
+
332
698
  // src/index.ts
699
+ function resolveVersion() {
700
+ try {
701
+ const pkg = JSON.parse(
702
+ readFileSync3(new URL("../package.json", import.meta.url), "utf8")
703
+ );
704
+ return pkg.version ?? "0.0.0";
705
+ } catch {
706
+ return "0.0.0";
707
+ }
708
+ }
333
709
  var program = new Command();
334
- program.name("sechroom").description("Sechroom CLI \u2014 thin generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.").version("0.0.0").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);
710
+ 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);
711
+ program.hook("preAction", (_thisCmd, actionCmd) => {
712
+ setQuiet(Boolean(actionCmd.optsWithGlobals().json));
713
+ });
335
714
  program.command("login").description("Sign in via browser (OAuth auth-code + PKCE, dynamic client registration)").action(async (_opts, cmd) => {
336
715
  const g = cmd.optsWithGlobals();
337
716
  const persisted = readPersisted();
@@ -355,6 +734,8 @@ config.command("show").description("Print persisted config").action(() => {
355
734
  registerMemory(program);
356
735
  registerWorklog(program);
357
736
  registerLookup(program);
737
+ registerInit(program);
738
+ registerSetup(program);
358
739
  program.parseAsync().catch((err) => {
359
740
  process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
360
741
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sechroom/cli",
3
- "version": "2026.6.2",
3
+ "version": "2026.6.3",
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",