@sechroom/cli 2026.6.239-rc.6418da97 → 2026.7.2

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 +80 -115
  2. package/package.json +4 -3
package/dist/index.js CHANGED
@@ -433,6 +433,7 @@ async function requireToken(cfg) {
433
433
  import createClient from "openapi-fetch";
434
434
 
435
435
  // src/ui.ts
436
+ import * as clack from "@clack/prompts";
436
437
  var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
437
438
  var quiet = false;
438
439
  function setQuiet(q) {
@@ -458,7 +459,7 @@ var err = (s) => style.red(s);
458
459
  function active() {
459
460
  return !quiet && Boolean(process.stderr.isTTY);
460
461
  }
461
- function spinner(text) {
462
+ function spinner(text2) {
462
463
  if (!active()) {
463
464
  return { succeed() {
464
465
  }, fail() {
@@ -468,9 +469,9 @@ function spinner(text) {
468
469
  let i = 0;
469
470
  const render = () => {
470
471
  i = (i + 1) % FRAMES.length;
471
- process.stderr.write(`\r${FRAMES[i]} ${text}`);
472
+ process.stderr.write(`\r${FRAMES[i]} ${text2}`);
472
473
  };
473
- process.stderr.write(`\r${FRAMES[0]} ${text}`);
474
+ process.stderr.write(`\r${FRAMES[0]} ${text2}`);
474
475
  const timer = setInterval(render, 80);
475
476
  timer.unref?.();
476
477
  const clear = () => {
@@ -480,12 +481,12 @@ function spinner(text) {
480
481
  return {
481
482
  succeed(t) {
482
483
  clear();
483
- process.stderr.write(`${ok("\u2713")} ${t ?? text}
484
+ process.stderr.write(`${ok("\u2713")} ${t ?? text2}
484
485
  `);
485
486
  },
486
487
  fail(t) {
487
488
  clear();
488
- process.stderr.write(`${err("\u2717")} ${t ?? text}
489
+ process.stderr.write(`${err("\u2717")} ${t ?? text2}
489
490
  `);
490
491
  },
491
492
  stop: clear
@@ -496,31 +497,24 @@ function canPrompt() {
496
497
  }
497
498
  async function promptYesNo(question) {
498
499
  if (!canPrompt()) return false;
499
- const { createInterface } = await import("readline");
500
- const rl = createInterface({ input: process.stdin, output: process.stderr });
501
- try {
502
- const answer = await new Promise((resolve3) => {
503
- rl.question(`${question} [y/N] `, resolve3);
504
- });
505
- return /^y(es)?$/i.test(answer.trim());
506
- } finally {
507
- rl.close();
508
- }
500
+ const r = await clack.confirm({
501
+ message: question,
502
+ initialValue: false,
503
+ output: process.stderr
504
+ });
505
+ return clack.isCancel(r) ? false : r;
509
506
  }
510
507
  async function promptText(question, def) {
511
508
  if (!canPrompt()) return def ?? "";
512
- const { createInterface } = await import("readline");
513
- const rl = createInterface({ input: process.stdin, output: process.stderr });
514
- try {
515
- const suffix = def ? ` [${def}]` : "";
516
- const answer = await new Promise((resolve3) => {
517
- rl.question(`${question}${suffix} `, resolve3);
518
- });
519
- const trimmed = answer.trim();
520
- return trimmed.length > 0 ? trimmed : def ?? "";
521
- } finally {
522
- rl.close();
523
- }
509
+ const r = await clack.text({
510
+ message: question,
511
+ defaultValue: def,
512
+ placeholder: def,
513
+ output: process.stderr
514
+ });
515
+ if (clack.isCancel(r)) return def ?? "";
516
+ const trimmed = (r ?? "").trim();
517
+ return trimmed.length > 0 ? trimmed : def ?? "";
524
518
  }
525
519
  async function promptSelect(question, choices, def) {
526
520
  if (choices.length === 0) throw new Error("promptSelect: no choices");
@@ -529,74 +523,50 @@ async function promptSelect(question, choices, def) {
529
523
  def !== void 0 ? choices.findIndex((c) => c.value === def) : 0
530
524
  );
531
525
  if (!canPrompt()) return choices[defIdx].value;
532
- const { createInterface } = await import("readline");
533
- const rl = createInterface({ input: process.stdin, output: process.stderr });
534
- try {
535
- process.stderr.write(`${style.bold(question)}
536
- `);
537
- choices.forEach((c, i) => {
538
- const marker = i === defIdx ? style.cyan("\u203A") : " ";
539
- const hint = c.hint ? ` ${style.dim(`\u2014 ${c.hint}`)}` : "";
540
- process.stderr.write(` ${marker} ${style.bold(String(i + 1))}. ${c.label}${hint}
541
- `);
542
- });
543
- const answer = await new Promise((resolve3) => {
544
- rl.question(`Choose ${style.dim(`[${defIdx + 1}]`)} `, resolve3);
545
- });
546
- const trimmed = answer.trim();
547
- if (!trimmed) return choices[defIdx].value;
548
- const n = Number(trimmed);
549
- if (Number.isInteger(n) && n >= 1 && n <= choices.length) return choices[n - 1].value;
550
- const byLabel = choices.find(
551
- (c) => c.label.toLowerCase().startsWith(trimmed.toLowerCase())
552
- );
553
- return byLabel ? byLabel.value : choices[defIdx].value;
554
- } finally {
555
- rl.close();
556
- }
526
+ const r = await clack.select({
527
+ message: question,
528
+ // Cast: clack's Option<T> is a conditional type (primitive vs not) that TS
529
+ // can't verify against a generic T; the runtime shape is correct.
530
+ options: choices.map((c) => ({ value: c.value, label: c.label, hint: c.hint })),
531
+ initialValue: choices[defIdx].value,
532
+ output: process.stderr
533
+ });
534
+ return clack.isCancel(r) ? choices[defIdx].value : r;
557
535
  }
558
536
  async function promptMultiSelect(question, choices, preselected = []) {
559
537
  if (choices.length === 0) return [];
560
- const pre = (v) => preselected.includes(v);
561
- const preValues = () => choices.filter((c) => pre(c.value)).map((c) => c.value);
562
- if (!canPrompt()) return preValues();
563
- const { createInterface } = await import("readline");
564
- const rl = createInterface({ input: process.stdin, output: process.stderr });
565
- try {
566
- process.stderr.write(
567
- `${style.bold(question)} ${style.dim("(numbers or 'all', comma-separated; Enter keeps \u25C9)")}
568
- `
569
- );
570
- choices.forEach((c, i) => {
571
- const box = pre(c.value) ? style.cyan("\u25C9") : "\u25CB";
572
- const hint = c.hint ? ` ${style.dim(`\u2014 ${c.hint}`)}` : "";
573
- process.stderr.write(` ${box} ${style.bold(String(i + 1))}. ${c.label}${hint}
574
- `);
575
- });
576
- const answer = await new Promise((resolve3) => {
577
- rl.question(`Select ${style.dim("[Enter = \u25C9]")} `, resolve3);
578
- });
579
- const trimmed = answer.trim().toLowerCase();
580
- if (!trimmed) return preValues();
581
- if (trimmed === "all") return choices.map((c) => c.value);
582
- const picks = [];
583
- for (const tok of trimmed.split(",").map((t) => t.trim()).filter(Boolean)) {
584
- const n = Number(tok);
585
- if (Number.isInteger(n) && n >= 1 && n <= choices.length) {
586
- picks.push(choices[n - 1].value);
587
- continue;
588
- }
589
- const byLabel = choices.find((c) => c.label.toLowerCase().startsWith(tok));
590
- if (byLabel) picks.push(byLabel.value);
591
- }
592
- const uniq = [...new Set(picks)];
593
- return uniq.length > 0 ? uniq : preValues();
594
- } finally {
595
- rl.close();
596
- }
597
- }
598
- async function withSpinner(text, fn) {
599
- const s = spinner(text);
538
+ const preValues = choices.filter((c) => preselected.includes(c.value)).map((c) => c.value);
539
+ if (!canPrompt()) return preValues;
540
+ const r = await clack.multiselect({
541
+ message: question,
542
+ // Cast: clack's Option<T> is a conditional type (primitive vs not) that TS
543
+ // can't verify against a generic T; the runtime shape is correct.
544
+ options: choices.map((c) => ({ value: c.value, label: c.label, hint: c.hint })),
545
+ initialValues: preValues,
546
+ required: false,
547
+ output: process.stderr
548
+ });
549
+ return clack.isCancel(r) ? preValues : r;
550
+ }
551
+ async function promptAutocomplete(question, choices, def) {
552
+ if (choices.length === 0) throw new Error("promptAutocomplete: no choices");
553
+ const defIdx = Math.max(
554
+ 0,
555
+ def !== void 0 ? choices.findIndex((c) => c.value === def) : 0
556
+ );
557
+ if (!canPrompt()) return choices[defIdx].value;
558
+ const r = await clack.autocomplete({
559
+ message: question,
560
+ // Cast: clack's Option<T> is a conditional type (primitive vs not) that TS
561
+ // can't verify against a generic T; the runtime shape is correct.
562
+ options: choices.map((c) => ({ value: c.value, label: c.label, hint: c.hint })),
563
+ placeholder: "Type to filter\u2026",
564
+ output: process.stderr
565
+ });
566
+ return clack.isCancel(r) ? choices[defIdx].value : r;
567
+ }
568
+ async function withSpinner(text2, fn) {
569
+ const s = spinner(text2);
600
570
  try {
601
571
  const result = await fn();
602
572
  s.succeed();
@@ -1521,7 +1491,7 @@ Examples:
1521
1491
  $ sechroom chat replies 1718049600.123456 --surface slack
1522
1492
  $ sechroom chat stop-tracking 1718049600.123456 --surface slack`
1523
1493
  );
1524
- chat.command("send <channelId> <text>").description("Send a message to a channel (POST /chat/channel-messages/{surface})").option("--guild <guildId>", "Discord guild snowflake \u2014 required for --surface discord").option("--memory <memoryId>", "Attach a sechroom memory id").option("--no-track", "Don't capture replies to this message").option("--parent <parentMessage>", "Thread under a parent (Slack thread_ts / Discord message id)").option("--source <source>", "Source / lane stamp (renders an attribution footer)", "cli").option("--as <as>", "Slack only: 'bot' (default) or 'user' (your linked Slack identity)", "bot").action(async (channelId, text, opts, cmd) => {
1494
+ chat.command("send <channelId> <text>").description("Send a message to a channel (POST /chat/channel-messages/{surface})").option("--guild <guildId>", "Discord guild snowflake \u2014 required for --surface discord").option("--memory <memoryId>", "Attach a sechroom memory id").option("--no-track", "Don't capture replies to this message").option("--parent <parentMessage>", "Thread under a parent (Slack thread_ts / Discord message id)").option("--source <source>", "Source / lane stamp (renders an attribution footer)", "cli").option("--as <as>", "Slack only: 'bot' (default) or 'user' (your linked Slack identity)", "bot").action(async (channelId, text2, opts, cmd) => {
1525
1495
  const { surface, ...globals } = cmd.optsWithGlobals();
1526
1496
  const json = Boolean(cmd.optsWithGlobals().json);
1527
1497
  const cfg = resolveConfig(globals);
@@ -1531,7 +1501,7 @@ Examples:
1531
1501
  params: { path: { surface: String(surface) } },
1532
1502
  body: {
1533
1503
  channelId,
1534
- text,
1504
+ text: text2,
1535
1505
  guildId: opts.guild ?? null,
1536
1506
  attachedMemoryId: opts.memory ?? null,
1537
1507
  trackReplies: opts.track,
@@ -1666,9 +1636,9 @@ function readLocalSemValues(cwd = process.cwd()) {
1666
1636
  if (existsSync4(next)) return readSem(next)?.values ?? {};
1667
1637
  return {};
1668
1638
  }
1669
- function parseLaneJson(text) {
1639
+ function parseLaneJson(text2) {
1670
1640
  try {
1671
- const parsed = JSON.parse(text);
1641
+ const parsed = JSON.parse(text2);
1672
1642
  const out = {};
1673
1643
  for (const [k, v] of Object.entries(parsed)) {
1674
1644
  if (typeof v === "string") out[k] = v;
@@ -3224,7 +3194,10 @@ async function applyClient(cfg, setup, target, opts) {
3224
3194
  if (!section) {
3225
3195
  actions.push({ kind: "instruction", path: target.instruction.path, status: "skipped", note: `no instruction-file section on surface '${target.instruction.surfaceKey}'` });
3226
3196
  } else {
3227
- const resolved = await resolveInstruction(cfg, section, opts.personalWorkspaceId);
3197
+ const resolved = await withSpinner(
3198
+ `Resolving ${target.label} agent instructions`,
3199
+ () => resolveInstruction(cfg, section, opts.personalWorkspaceId)
3200
+ );
3228
3201
  if (!resolved) {
3229
3202
  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`" });
3230
3203
  } else {
@@ -3239,7 +3212,10 @@ async function applyClient(cfg, setup, target, opts) {
3239
3212
  }
3240
3213
  const conventionsSection = findSection(surface, SectionType.WorkspaceConventions);
3241
3214
  if (conventionsSection) {
3242
- const conventions = await resolveWorkspaceConventions(cfg, conventionsSection);
3215
+ const conventions = await withSpinner(
3216
+ `Composing ${target.label} workspace conventions`,
3217
+ () => resolveWorkspaceConventions(cfg, conventionsSection)
3218
+ );
3243
3219
  if (conventions) {
3244
3220
  const action = applyBlock(
3245
3221
  target.instruction.path,
@@ -3667,20 +3643,20 @@ Examples:
3667
3643
  fail("no workspace \u2014 pass --workspace <id> or bind one (`sechroom config set --local workspaceId <id>`).");
3668
3644
  const kind = String(opts.kind).toLowerCase() === "standard" ? "standard" : "reference";
3669
3645
  const body = typeof opts.body === "string" && opts.body.trim().length > 0 ? opts.body.trim() : "_TODO: write this section, then edit the memo and re-run the regen._";
3670
- const text = `# ${title}
3646
+ const text2 = `# ${title}
3671
3647
 
3672
3648
  ${body}
3673
3649
  `;
3674
3650
  const tags = ["agent-setup-bundle", "scope:sechroom", `kind:${kind}`, "archetype:document"];
3675
3651
  if (opts.dryRun) {
3676
- emit({ dryRun: true, workspaceId, title, kind, tags, text }, json);
3652
+ emit({ dryRun: true, workspaceId, title, kind, tags, text: text2 }, json);
3677
3653
  return;
3678
3654
  }
3679
3655
  const data = await runApi("Authoring convention memo", async () => {
3680
3656
  const client = await makeClient(cfg);
3681
3657
  return client.POST("/memories", {
3682
3658
  body: {
3683
- text,
3659
+ text: text2,
3684
3660
  type: kind,
3685
3661
  content: "{}",
3686
3662
  confidence: 1,
@@ -4045,28 +4021,17 @@ async function pickWorkspace(client, opts = {}) {
4045
4021
  if (candidates.length === 0) candidates = all;
4046
4022
  const dirToks = new Set(nameTokens(dirName));
4047
4023
  const isMatch = (w) => nameTokens(w.name).some((t) => dirToks.has(t));
4048
- const suggestions = candidates.filter(isMatch);
4049
- let pool = candidates;
4050
- if (candidates.length > 12 && suggestions.length === 0) {
4051
- const q = (await promptText(`Filter ${candidates.length} workspaces (substring, Enter to list all)?`, "")).trim().toLowerCase();
4052
- if (q) {
4053
- const hits = candidates.filter((w) => `${w.name} ${workspacePath(w, byId)}`.toLowerCase().includes(q));
4054
- if (hits.length > 0) pool = hits;
4055
- else process.stderr.write(`no match for "${q}" \u2014 listing all
4056
- `);
4057
- }
4058
- }
4059
4024
  const SKIP = "__skip__";
4060
4025
  const byPath = (a, b) => workspacePath(a, byId).localeCompare(workspacePath(b, byId));
4061
- const matched = pool.filter(isMatch).sort(byPath);
4062
- const rest = pool.filter((w) => !isMatch(w)).sort(byPath);
4026
+ const matched = candidates.filter(isMatch).sort(byPath);
4027
+ const rest = candidates.filter((w) => !isMatch(w)).sort(byPath);
4063
4028
  const choices = [
4064
4029
  ...matched.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: style.dim(`matches "${dirName}"`) })),
4065
4030
  ...rest.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: w.id })),
4066
4031
  { label: style.dim("skip \u2014 don't bind a workspace"), value: SKIP, hint: void 0 }
4067
4032
  ];
4068
4033
  const defaultValue = matched.length === 1 ? matched[0].id : SKIP;
4069
- const chosen = await promptSelect(promptLabel, choices, defaultValue);
4034
+ const chosen = candidates.length > 12 ? await promptAutocomplete(promptLabel, choices, defaultValue) : await promptSelect(promptLabel, choices, defaultValue);
4070
4035
  if (chosen === SKIP) return void 0;
4071
4036
  const picked = byId.get(chosen);
4072
4037
  const collisions = all.filter((w) => w.id !== picked.id && namesCollide(w.name, picked.name));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sechroom/cli",
3
- "version": "2026.6.239-rc.6418da97",
3
+ "version": "2026.7.2",
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",
@@ -24,11 +24,12 @@
24
24
  "registry": "https://registry.npmjs.org/"
25
25
  },
26
26
  "dependencies": {
27
+ "@clack/prompts": "^1.6.0",
27
28
  "@microsoft/signalr": "^10.0.0",
29
+ "@modelcontextprotocol/sdk": "^1.12.0",
28
30
  "commander": "^12.1.0",
29
31
  "open": "^10.1.0",
30
- "openapi-fetch": "^0.13.0",
31
- "@modelcontextprotocol/sdk": "^1.12.0"
32
+ "openapi-fetch": "^0.13.0"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@types/node": "^20.14.0",