@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.
- package/dist/index.js +80 -115
- 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(
|
|
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]} ${
|
|
472
|
+
process.stderr.write(`\r${FRAMES[i]} ${text2}`);
|
|
472
473
|
};
|
|
473
|
-
process.stderr.write(`\r${FRAMES[0]} ${
|
|
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 ??
|
|
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 ??
|
|
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
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
choices.
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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,
|
|
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(
|
|
1639
|
+
function parseLaneJson(text2) {
|
|
1670
1640
|
try {
|
|
1671
|
-
const parsed = JSON.parse(
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
4062
|
-
const rest =
|
|
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.
|
|
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",
|