@sechroom/cli 2026.6.3 → 2026.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +268 -27
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -255,6 +255,37 @@ function spinner(text) {
255
255
  stop: clear
256
256
  };
257
257
  }
258
+ function canPrompt() {
259
+ return !quiet && Boolean(process.stdin.isTTY) && Boolean(process.stderr.isTTY);
260
+ }
261
+ async function promptYesNo(question) {
262
+ if (!canPrompt()) return false;
263
+ const { createInterface } = await import("readline");
264
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
265
+ try {
266
+ const answer = await new Promise((resolve) => {
267
+ rl.question(`${question} [y/N] `, resolve);
268
+ });
269
+ return /^y(es)?$/i.test(answer.trim());
270
+ } finally {
271
+ rl.close();
272
+ }
273
+ }
274
+ async function promptText(question, def) {
275
+ if (!canPrompt()) return def ?? "";
276
+ const { createInterface } = await import("readline");
277
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
278
+ try {
279
+ const suffix = def ? ` [${def}]` : "";
280
+ const answer = await new Promise((resolve) => {
281
+ rl.question(`${question}${suffix} `, resolve);
282
+ });
283
+ const trimmed = answer.trim();
284
+ return trimmed.length > 0 ? trimmed : def ?? "";
285
+ } finally {
286
+ rl.close();
287
+ }
288
+ }
258
289
  async function withSpinner(text, fn) {
259
290
  const s = spinner(text);
260
291
  try {
@@ -440,37 +471,68 @@ function parseTagArtifactId(id) {
440
471
  const tags = id.slice("tag:".length).split(",").map((t) => t.trim()).filter((t) => t.length > 0);
441
472
  return tags.length > 0 ? tags : null;
442
473
  }
443
- async function resolveInstructionBody(cfg, section) {
474
+ async function getPersonalWorkspaceId(cfg) {
475
+ const client = await makeClient(cfg);
476
+ const { data } = await client.GET("/me/personal-workspace", {});
477
+ return data?.workspaceId ?? null;
478
+ }
479
+ async function fetchMemoryFields(cfg, id) {
480
+ const client = await makeClient(cfg);
481
+ const { data } = await client.GET("/memories/{memoryId}", { params: { path: { memoryId: id } } });
482
+ const env = data;
483
+ return env?.item ?? env ?? null;
484
+ }
485
+ async function resolveInstruction(cfg, section, personalWorkspaceId) {
444
486
  const client = await makeClient(cfg);
445
487
  for (const artifact of section.artifacts) {
446
488
  const tags = parseTagArtifactId(artifact.id);
447
489
  if (!tags) continue;
448
490
  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
- }
491
+ body: { query: null, textQuery: null, semanticQuery: artifact.title ?? "role instruction template", hybrid: true, limit: 1, includeArchived: false, includeSystem: false, tags }
459
492
  });
460
493
  const hits = data ?? [];
461
494
  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 };
495
+ const templateId = hits[0].id;
496
+ const template = await fetchMemoryFields(cfg, templateId);
497
+ if (typeof template?.text !== "string" || template.text.length === 0) continue;
498
+ const templateTags = template.tags ?? tags;
499
+ if (personalWorkspaceId) {
500
+ const { data: ovr } = await client.POST("/memories/search", {
501
+ body: { query: null, textQuery: null, semanticQuery: "role override", hybrid: true, limit: 1, includeArchived: false, includeSystem: false, tags: ["sechroom:role:override", `sechroom:template-ref:${templateId}`], owner: { type: "Workspace", id: personalWorkspaceId } }
502
+ });
503
+ const ovrHits = ovr ?? [];
504
+ if (ovrHits.length > 0) {
505
+ const override = await fetchMemoryFields(cfg, ovrHits[0].id);
506
+ if (typeof override?.text === "string" && override.text.length > 0) {
507
+ return { title: override.title ?? template.title ?? artifact.title, body: override.text, source: "override", templateId, templateTags };
508
+ }
509
+ }
470
510
  }
511
+ return { title: template.title ?? artifact.title, body: template.text, source: "template", templateId, templateTags };
471
512
  }
472
513
  return null;
473
514
  }
515
+ async function createOverride(cfg, template, personalWorkspaceId) {
516
+ const client = await makeClient(cfg);
517
+ const overrideTags = template.templateTags.filter(
518
+ (t) => t !== "sechroom:role:template" && !t.startsWith("sechroom:bundle:") && !t.startsWith("sechroom:template-ref:")
519
+ );
520
+ overrideTags.push("sechroom:role:override", `sechroom:template-ref:${template.templateId}`);
521
+ const { error } = await client.POST("/memories", {
522
+ body: {
523
+ text: template.body,
524
+ type: "reference",
525
+ content: "{}",
526
+ confidence: 1,
527
+ source: "cli-agent-instructions-customize",
528
+ archetype: "Document",
529
+ title: template.title ?? null,
530
+ tags: overrideTags,
531
+ owner: { type: "Workspace", id: personalWorkspaceId }
532
+ }
533
+ });
534
+ if (error) throw new Error(`creating personal copy failed: ${JSON.stringify(error)}`);
535
+ }
474
536
 
475
537
  // src/setup/apply.ts
476
538
  var BLOCK_BEGIN = "<!-- @sechroom/cli:begin (managed \u2014 re-run `sechroom setup agent-files` to refresh) -->";
@@ -557,11 +619,12 @@ async function applyClient(cfg, setup, target, opts) {
557
619
  if (!section) {
558
620
  actions.push({ kind: "instruction", path: target.instruction.path, status: "skipped", note: `no instruction-file section on surface '${target.instruction.surfaceKey}'` });
559
621
  } else {
560
- const resolved = await resolveInstructionBody(cfg, section);
622
+ const resolved = await resolveInstruction(cfg, section, opts.personalWorkspaceId);
561
623
  if (!resolved) {
562
624
  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
625
  } else {
564
- actions.push(writeInstructionBlock(target.instruction.path, resolved.body, opts.dryRun));
626
+ const action = writeInstructionBlock(target.instruction.path, resolved.body, opts.dryRun);
627
+ actions.push(resolved.source === "override" ? { ...action, note: "your personal copy" } : action);
565
628
  }
566
629
  }
567
630
  }
@@ -569,8 +632,9 @@ async function applyClient(cfg, setup, target, opts) {
569
632
  }
570
633
 
571
634
  // src/setup/clients.ts
635
+ import { existsSync as existsSync3 } from "fs";
572
636
  import { homedir as homedir2 } from "os";
573
- import { join as join2 } from "path";
637
+ import { dirname as dirname2, join as join2 } from "path";
574
638
  function claudeDesktopConfigPath(home) {
575
639
  switch (process.platform) {
576
640
  case "darwin":
@@ -612,8 +676,49 @@ function clientTargets(cwd) {
612
676
  }
613
677
  var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor"];
614
678
  var DEFAULT_CLIENT_KEY = "claude-code";
679
+ function detectInstalledClients(cwd) {
680
+ const home = homedir2();
681
+ const detected = [];
682
+ if (existsSync3(join2(home, ".claude"))) detected.push("claude-code");
683
+ if (existsSync3(dirname2(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
684
+ if (existsSync3(join2(home, ".codex"))) detected.push("codex");
685
+ if (existsSync3(join2(home, ".cursor")) || existsSync3(join2(cwd, ".cursor"))) detected.push("cursor");
686
+ return detected;
687
+ }
615
688
 
616
689
  // src/commands/setup.ts
690
+ function copyChoice(opts) {
691
+ return opts.copy === true ? "yes" : opts.copy === false ? "no" : "ask";
692
+ }
693
+ async function maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, choice) {
694
+ if (!personalWorkspaceId || choice === "no") return;
695
+ const seen = /* @__PURE__ */ new Set();
696
+ for (const key of keys) {
697
+ const instr = targets[key]?.instruction;
698
+ if (!instr || seen.has(instr.surfaceKey)) continue;
699
+ seen.add(instr.surfaceKey);
700
+ const section = findSection(findSurface(setup, instr.surfaceKey), SectionType.InstructionFile);
701
+ if (!section) continue;
702
+ const resolved = await resolveInstruction(cfg, section, personalWorkspaceId);
703
+ if (!resolved || resolved.source === "override") continue;
704
+ let make = choice === "yes";
705
+ if (choice === "ask") {
706
+ process.stderr.write(
707
+ `
708
+ The ${instr.surfaceKey} agent instructions are the shared template.
709
+ Make a personal copy to tailor them for your workflow \u2014 your agent uses your
710
+ version, the shared template stays clean, and you can discard back anytime.
711
+ `
712
+ );
713
+ make = await promptYesNo("Make a personal copy to customise?");
714
+ }
715
+ if (make) {
716
+ await createOverride(cfg, resolved, personalWorkspaceId);
717
+ process.stderr.write(`\u2713 personal copy created for ${instr.surfaceKey} \u2014 edit it on the Agent setup page or via the API.
718
+ `);
719
+ }
720
+ }
721
+ }
617
722
  function resolveClientKeys(raw) {
618
723
  const targets = clientTargets(process.cwd());
619
724
  if (raw === "all") return [...ALL_CLIENT_KEYS];
@@ -636,19 +741,24 @@ ${client.label} (${client.key}):
636
741
  }
637
742
  }
638
743
  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) => {
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) => {
640
745
  const cfg = resolveConfig(cmd.optsWithGlobals());
641
746
  const setup = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
642
747
  const targets = clientTargets(process.cwd());
643
748
  const keys = resolveClientKeys(opts.client);
644
749
  const json = cmd.optsWithGlobals().json;
750
+ const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
751
+ if (!opts.dryRun && !opts.mcpOnly) {
752
+ await maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, copyChoice(opts));
753
+ }
645
754
  const result = [];
646
755
  for (const key of keys) {
647
756
  const target = targets[key];
648
757
  const actions = await applyClient(cfg, setup, target, {
649
758
  dryRun: Boolean(opts.dryRun),
650
759
  mcp: !opts.agentFilesOnly,
651
- agentFiles: !opts.mcpOnly
760
+ agentFiles: !opts.mcpOnly,
761
+ personalWorkspaceId
652
762
  });
653
763
  result.push({ client: key, actions });
654
764
  if (!json) printActions(target, actions);
@@ -675,8 +785,8 @@ function registerSetup(program2) {
675
785
  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
786
  await runSingle(client, cmd, { dryRun: Boolean(opts.dryRun), mcp: true, agentFiles: false });
677
787
  });
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 });
788
+ 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).option("--copy", "make a personal copy you can edit (default: prompt on a TTY, else skip)").action(async (client, opts, cmd) => {
789
+ await runSingle(client, cmd, { dryRun: Boolean(opts.dryRun), mcp: false, agentFiles: true, copy: opts.copy });
680
790
  });
681
791
  }
682
792
  async function runSingle(client, cmd, opts) {
@@ -685,7 +795,16 @@ async function runSingle(client, cmd, opts) {
685
795
  const target = targets[client];
686
796
  if (!target) fail(`unknown client '${client}'. Known: ${ALL_CLIENT_KEYS.join(", ")}.`);
687
797
  const setupData = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
688
- const actions = await applyClient(cfg, setupData, target, opts);
798
+ const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
799
+ if (opts.agentFiles && !opts.dryRun) {
800
+ await maybeOfferCopies(cfg, setupData, targets, [client], personalWorkspaceId, copyChoice(opts));
801
+ }
802
+ const actions = await applyClient(cfg, setupData, target, {
803
+ dryRun: opts.dryRun,
804
+ mcp: opts.mcp,
805
+ agentFiles: opts.agentFiles,
806
+ personalWorkspaceId
807
+ });
689
808
  const json = cmd.optsWithGlobals().json;
690
809
  if (json) {
691
810
  emit({ dryRun: opts.dryRun, client, actions }, true);
@@ -695,6 +814,127 @@ async function runSingle(client, cmd, opts) {
695
814
  }
696
815
  }
697
816
 
817
+ // src/commands/onboard.ts
818
+ var DEFAULT_BASE_URL2 = "https://app.sechroom.ai/api";
819
+ function systemTimezone() {
820
+ try {
821
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
822
+ } catch {
823
+ return "UTC";
824
+ }
825
+ }
826
+ async function ensureConfig(g, yes) {
827
+ 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) {
831
+ baseUrl = await promptText("Sechroom API base URL?", baseUrl);
832
+ tenant = await promptText("Tenant id?", tenant || void 0);
833
+ }
834
+ baseUrl = baseUrl.replace(/\/$/, "");
835
+ if (!tenant) {
836
+ fail(
837
+ "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
+ );
839
+ }
840
+ writePersisted({ baseUrl, tenant });
841
+ return { baseUrl, tenant, clientId: persisted.clientId };
842
+ }
843
+ async function ensureAuth(cfg, yes) {
844
+ if (process.env.SECHROOM_TOKEN) return;
845
+ const cached = readToken();
846
+ const usable = Boolean(cached?.accessToken) && (cached.expiresAt === void 0 || Date.now() < cached.expiresAt - 6e4 || Boolean(cached.refreshToken));
847
+ if (usable) return;
848
+ if (!canPrompt() || yes) {
849
+ fail("Not signed in. Run `sechroom login` first, or set SECHROOM_TOKEN for headless use.");
850
+ }
851
+ process.stderr.write("\nNot signed in \u2014 opening the browser to authenticate.\n");
852
+ await login(cfg);
853
+ }
854
+ async function ensureTimezone(cfg, opts) {
855
+ const client = await makeClient(cfg);
856
+ const { data, error } = await client.GET("/me/profile", {});
857
+ if (error) return { timezone: null, action: "skipped", note: "could not read profile" };
858
+ const current = data?.effectiveTimezone;
859
+ if (current && current.trim().length > 0) return { timezone: current, action: "already-set" };
860
+ const system = systemTimezone();
861
+ let tz = system;
862
+ if (canPrompt() && !opts.yes) {
863
+ tz = await promptText("Your timezone (IANA, e.g. Europe/London)?", system);
864
+ } else if (!opts.yes) {
865
+ return {
866
+ timezone: null,
867
+ action: "skipped",
868
+ note: "no timezone set \u2014 re-run interactively or pass --yes to adopt the system timezone"
869
+ };
870
+ }
871
+ if (!tz) return { timezone: null, action: "skipped", note: "no timezone provided" };
872
+ if (opts.dryRun) return { timezone: tz, action: "dry-run" };
873
+ const { error: putErr } = await client.PUT("/me/profile", {
874
+ body: { displayName: null, photoUrl: null, bio: null, timezone: tz }
875
+ });
876
+ if (putErr) return { timezone: tz, action: "skipped", note: `update failed: ${JSON.stringify(putErr)}` };
877
+ return { timezone: tz, action: "set" };
878
+ }
879
+ async function chooseClients(clientFlag, yes, cwd) {
880
+ if (clientFlag) return resolveClientKeys(clientFlag);
881
+ const detected = detectInstalledClients(cwd);
882
+ const fallback = (detected.length > 0 ? detected : [DEFAULT_CLIENT_KEY]).join(",");
883
+ if (!canPrompt() || yes) return resolveClientKeys(fallback);
884
+ process.stderr.write(
885
+ `
886
+ Available clients: ${ALL_CLIENT_KEYS.join(", ")}
887
+ ` + (detected.length > 0 ? `Detected on this machine: ${detected.join(", ")}
888
+ ` : "No clients auto-detected.\n")
889
+ );
890
+ const answer = await promptText("Which to wire? (comma-separated, or 'all')", fallback);
891
+ return resolveClientKeys(answer);
892
+ }
893
+ 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) => {
895
+ const g = cmd.optsWithGlobals();
896
+ const json = Boolean(g.json);
897
+ const yes = Boolean(opts.yes);
898
+ const dryRun = Boolean(opts.dryRun);
899
+ const cfg = await ensureConfig(g, yes);
900
+ await ensureAuth(cfg, yes);
901
+ const tz = await ensureTimezone(cfg, { yes, dryRun });
902
+ if (!json && tz.action !== "already-set") {
903
+ const line = tz.action === "set" ? `\u2713 timezone set to ${tz.timezone}
904
+ ` : tz.action === "dry-run" ? `(dry run \u2014 would set timezone to ${tz.timezone})
905
+ ` : `timezone not set \u2014 ${tz.note}
906
+ `;
907
+ process.stderr.write(line);
908
+ }
909
+ const keys = await chooseClients(opts.client, yes, process.cwd());
910
+ const setup = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
911
+ const targets = clientTargets(process.cwd());
912
+ const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
913
+ if (!dryRun) {
914
+ await maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, copyChoice(opts));
915
+ }
916
+ const result = [];
917
+ for (const key of keys) {
918
+ const target = targets[key];
919
+ const actions = await applyClient(cfg, setup, target, {
920
+ dryRun,
921
+ mcp: true,
922
+ agentFiles: true,
923
+ personalWorkspaceId
924
+ });
925
+ result.push({ client: key, actions });
926
+ if (!json) printActions(target, actions);
927
+ }
928
+ if (json) {
929
+ emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, timezone: tz, clients: result }, true);
930
+ return;
931
+ }
932
+ 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"
934
+ );
935
+ });
936
+ }
937
+
698
938
  // src/index.ts
699
939
  function resolveVersion() {
700
940
  try {
@@ -736,6 +976,7 @@ registerWorklog(program);
736
976
  registerLookup(program);
737
977
  registerInit(program);
738
978
  registerSetup(program);
979
+ registerOnboard(program);
739
980
  program.parseAsync().catch((err) => {
740
981
  process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
741
982
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sechroom/cli",
3
- "version": "2026.6.3",
3
+ "version": "2026.6.5",
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",