@sechroom/cli 2026.6.6 → 2026.6.8
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/README.md +29 -0
- package/dist/index.js +277 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,6 +100,13 @@ 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
|
+
|
|
103
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.
|
|
104
111
|
|
|
105
112
|
```bash
|
|
@@ -137,6 +144,7 @@ Codex TOML table replace; instruction files use a managed marker block).
|
|
|
137
144
|
sechroom init # Claude Code (default): .mcp.json + CLAUDE.md
|
|
138
145
|
sechroom init --client all # claude-code, claude-desktop, codex, cursor
|
|
139
146
|
sechroom init --client codex,cursor # a subset
|
|
147
|
+
sechroom init --mcp-only # just the MCP config (skip agent files)
|
|
140
148
|
sechroom init --dry-run --json # preview the writes, no changes
|
|
141
149
|
|
|
142
150
|
# granular pieces init orchestrates:
|
|
@@ -144,6 +152,27 @@ sechroom setup mcp claude-desktop # just the MCP config for one client
|
|
|
144
152
|
sechroom setup agent-files codex # just the AGENTS.md instruction file
|
|
145
153
|
```
|
|
146
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
|
+
|
|
147
176
|
Per client → where it writes:
|
|
148
177
|
|
|
149
178
|
| client | MCP config | instruction file |
|
package/dist/index.js
CHANGED
|
@@ -173,13 +173,13 @@ function startLoopback(state) {
|
|
|
173
173
|
}
|
|
174
174
|
const got = url.searchParams.get("code");
|
|
175
175
|
const gotState = url.searchParams.get("state");
|
|
176
|
-
const
|
|
177
|
-
res.writeHead(
|
|
176
|
+
const err2 = url.searchParams.get("error");
|
|
177
|
+
res.writeHead(err2 ? 400 : 200, { "content-type": "text/html" });
|
|
178
178
|
res.end(
|
|
179
|
-
|
|
179
|
+
err2 ? `<h3>Authorization failed: ${err2}</h3>` : "<h3>Signed in to Sechroom.</h3><p>Return to the terminal.</p>"
|
|
180
180
|
);
|
|
181
181
|
server.close();
|
|
182
|
-
if (
|
|
182
|
+
if (err2) return rejectCode(new Error(`Authorization error: ${err2}`));
|
|
183
183
|
if (!got) return rejectCode(new Error("No code in callback."));
|
|
184
184
|
if (gotState !== state) return rejectCode(new Error("State mismatch \u2014 possible CSRF."));
|
|
185
185
|
resolveCode(got);
|
|
@@ -277,6 +277,22 @@ var quiet = false;
|
|
|
277
277
|
function setQuiet(q) {
|
|
278
278
|
quiet = q;
|
|
279
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);
|
|
280
296
|
function active() {
|
|
281
297
|
return !quiet && Boolean(process.stderr.isTTY);
|
|
282
298
|
}
|
|
@@ -302,12 +318,12 @@ function spinner(text) {
|
|
|
302
318
|
return {
|
|
303
319
|
succeed(t) {
|
|
304
320
|
clear();
|
|
305
|
-
process.stderr.write(
|
|
321
|
+
process.stderr.write(`${ok("\u2713")} ${t ?? text}
|
|
306
322
|
`);
|
|
307
323
|
},
|
|
308
324
|
fail(t) {
|
|
309
325
|
clear();
|
|
310
|
-
process.stderr.write(
|
|
326
|
+
process.stderr.write(`${err("\u2717")} ${t ?? text}
|
|
311
327
|
`);
|
|
312
328
|
},
|
|
313
329
|
stop: clear
|
|
@@ -344,15 +360,88 @@ async function promptText(question, def) {
|
|
|
344
360
|
rl.close();
|
|
345
361
|
}
|
|
346
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
|
+
}
|
|
396
|
+
async function promptMultiSelect(question, choices, preselected = []) {
|
|
397
|
+
if (choices.length === 0) return [];
|
|
398
|
+
const pre = (v) => preselected.includes(v);
|
|
399
|
+
const preValues = () => choices.filter((c) => pre(c.value)).map((c) => c.value);
|
|
400
|
+
if (!canPrompt()) return preValues();
|
|
401
|
+
const { createInterface } = await import("readline");
|
|
402
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
403
|
+
try {
|
|
404
|
+
process.stderr.write(
|
|
405
|
+
`${style.bold(question)} ${style.dim("(numbers or 'all', comma-separated; Enter keeps \u25C9)")}
|
|
406
|
+
`
|
|
407
|
+
);
|
|
408
|
+
choices.forEach((c, i) => {
|
|
409
|
+
const box = pre(c.value) ? style.cyan("\u25C9") : "\u25CB";
|
|
410
|
+
const hint = c.hint ? ` ${style.dim(`\u2014 ${c.hint}`)}` : "";
|
|
411
|
+
process.stderr.write(` ${box} ${style.bold(String(i + 1))}. ${c.label}${hint}
|
|
412
|
+
`);
|
|
413
|
+
});
|
|
414
|
+
const answer = await new Promise((resolve) => {
|
|
415
|
+
rl.question(`Select ${style.dim("[Enter = \u25C9]")} `, resolve);
|
|
416
|
+
});
|
|
417
|
+
const trimmed = answer.trim().toLowerCase();
|
|
418
|
+
if (!trimmed) return preValues();
|
|
419
|
+
if (trimmed === "all") return choices.map((c) => c.value);
|
|
420
|
+
const picks = [];
|
|
421
|
+
for (const tok of trimmed.split(",").map((t) => t.trim()).filter(Boolean)) {
|
|
422
|
+
const n = Number(tok);
|
|
423
|
+
if (Number.isInteger(n) && n >= 1 && n <= choices.length) {
|
|
424
|
+
picks.push(choices[n - 1].value);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const byLabel = choices.find((c) => c.label.toLowerCase().startsWith(tok));
|
|
428
|
+
if (byLabel) picks.push(byLabel.value);
|
|
429
|
+
}
|
|
430
|
+
const uniq = [...new Set(picks)];
|
|
431
|
+
return uniq.length > 0 ? uniq : preValues();
|
|
432
|
+
} finally {
|
|
433
|
+
rl.close();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
347
436
|
async function withSpinner(text, fn) {
|
|
348
437
|
const s = spinner(text);
|
|
349
438
|
try {
|
|
350
439
|
const result = await fn();
|
|
351
440
|
s.succeed();
|
|
352
441
|
return result;
|
|
353
|
-
} catch (
|
|
442
|
+
} catch (err2) {
|
|
354
443
|
s.fail();
|
|
355
|
-
throw
|
|
444
|
+
throw err2;
|
|
356
445
|
}
|
|
357
446
|
}
|
|
358
447
|
|
|
@@ -376,14 +465,25 @@ function emit(data, json) {
|
|
|
376
465
|
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
377
466
|
}
|
|
378
467
|
}
|
|
468
|
+
function publicUrl(url) {
|
|
469
|
+
return url.replace(/^https?:\/\/localhost:5012/, "https://sechroom.yi.ocd.codes");
|
|
470
|
+
}
|
|
471
|
+
function emitAction(summary, data, json) {
|
|
472
|
+
if (json) {
|
|
473
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
process.stdout.write(`${ok("\u2713")} ${summary}
|
|
477
|
+
`);
|
|
478
|
+
}
|
|
379
479
|
async function runApi(label, fn) {
|
|
380
480
|
const s = spinner(label);
|
|
381
481
|
let res;
|
|
382
482
|
try {
|
|
383
483
|
res = await fn();
|
|
384
|
-
} catch (
|
|
484
|
+
} catch (err2) {
|
|
385
485
|
s.fail();
|
|
386
|
-
fail(
|
|
486
|
+
fail(err2);
|
|
387
487
|
}
|
|
388
488
|
if (res.error !== void 0 && res.error !== null) {
|
|
389
489
|
s.fail();
|
|
@@ -402,6 +502,16 @@ function fail(error) {
|
|
|
402
502
|
// src/commands/memory.ts
|
|
403
503
|
function registerMemory(program2) {
|
|
404
504
|
const memory = program2.command("memory").description("Create, read, and search memories");
|
|
505
|
+
memory.addHelpText(
|
|
506
|
+
"after",
|
|
507
|
+
`
|
|
508
|
+
Examples:
|
|
509
|
+
$ sechroom memory create --text "first note" --type reference --tag idea --tag cli
|
|
510
|
+
$ sechroom memory create --text "filed note" --owner-type Workspace --owner-id wsp_XXXX
|
|
511
|
+
$ sechroom memory search "rate limiting" --limit 5 --tag kind:decision
|
|
512
|
+
$ sechroom memory search "auth flow" --workspace wsp_XXXX --json
|
|
513
|
+
$ sechroom memory get mem_XXXX --json`
|
|
514
|
+
);
|
|
405
515
|
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) => {
|
|
406
516
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
407
517
|
const unfiled = String(opts.ownerType).toLowerCase() === "unfiled";
|
|
@@ -421,7 +531,9 @@ function registerMemory(program2) {
|
|
|
421
531
|
}
|
|
422
532
|
});
|
|
423
533
|
});
|
|
424
|
-
|
|
534
|
+
const titlePart = opts.title ? ` ${style.dim(`"${opts.title}"`)}` : "";
|
|
535
|
+
const urlPart = data.url ? ` ${style.dim("\u2192")} ${publicUrl(data.url)}` : "";
|
|
536
|
+
emitAction(`created memory ${style.bold(data.id)}${titlePart}${urlPart}`, data, cmd.optsWithGlobals().json);
|
|
425
537
|
});
|
|
426
538
|
memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").action(async (memoryId, _opts, cmd) => {
|
|
427
539
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
@@ -456,6 +568,13 @@ function registerMemory(program2) {
|
|
|
456
568
|
// src/commands/worklog.ts
|
|
457
569
|
function registerWorklog(program2) {
|
|
458
570
|
const worklog = program2.command("worklog").description("Append to the daily work log");
|
|
571
|
+
worklog.addHelpText(
|
|
572
|
+
"after",
|
|
573
|
+
`
|
|
574
|
+
Examples:
|
|
575
|
+
$ sechroom worklog append --text "shipped CLI help + onboarding scope; PR #1430"
|
|
576
|
+
$ sechroom worklog append --text "smoke passed" --source claude-code-chris --title "CLI smoke"`
|
|
577
|
+
);
|
|
459
578
|
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) => {
|
|
460
579
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
461
580
|
const data = await runApi("Appending work-log entry", async () => {
|
|
@@ -469,7 +588,7 @@ function registerWorklog(program2) {
|
|
|
469
588
|
}
|
|
470
589
|
});
|
|
471
590
|
});
|
|
472
|
-
|
|
591
|
+
emitAction(`appended work-log entry ${style.bold(data.memoryId)}`, data, cmd.optsWithGlobals().json);
|
|
473
592
|
});
|
|
474
593
|
}
|
|
475
594
|
|
|
@@ -477,6 +596,14 @@ function registerWorklog(program2) {
|
|
|
477
596
|
function registerLookup(program2) {
|
|
478
597
|
program2.command("lookup <id>").description(
|
|
479
598
|
"Resolve a sechroom id to its kind, title, and view URL (mem_\u2026/wsp_\u2026/prj_\u2026, unprefixed, or sechroom:<id>)"
|
|
599
|
+
).addHelpText(
|
|
600
|
+
"after",
|
|
601
|
+
`
|
|
602
|
+
Examples:
|
|
603
|
+
$ sechroom lookup mem_XXXX a memory -> kind / title / view URL
|
|
604
|
+
$ sechroom lookup wsp_XXXX a workspace
|
|
605
|
+
$ sechroom lookup sechroom:mem_XXXX namespaced / portable form also resolves
|
|
606
|
+
$ sechroom lookup mem_XXXX --json`
|
|
480
607
|
).action(async (id, _opts, cmd) => {
|
|
481
608
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
482
609
|
const data = await runApi(`Resolving ${id}`, async () => {
|
|
@@ -799,7 +926,16 @@ ${client.label} (${client.key}):
|
|
|
799
926
|
}
|
|
800
927
|
}
|
|
801
928
|
function registerInit(program2) {
|
|
802
|
-
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)").
|
|
929
|
+
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(
|
|
930
|
+
"after",
|
|
931
|
+
`
|
|
932
|
+
Examples:
|
|
933
|
+
$ sechroom init Claude Code (default): ./.mcp.json + ./CLAUDE.md
|
|
934
|
+
$ sechroom init --client all claude-code, claude-desktop, codex, cursor
|
|
935
|
+
$ sechroom init --client codex,cursor
|
|
936
|
+
$ sechroom init --mcp-only just the MCP config (skip agent files)
|
|
937
|
+
$ sechroom init --dry-run --json preview the writes, change nothing`
|
|
938
|
+
).action(async (opts, cmd) => {
|
|
803
939
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
804
940
|
const setup = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
|
|
805
941
|
const targets = clientTargets(process.cwd());
|
|
@@ -881,11 +1017,12 @@ function systemTimezone() {
|
|
|
881
1017
|
return "UTC";
|
|
882
1018
|
}
|
|
883
1019
|
}
|
|
884
|
-
async function ensureConfig(g,
|
|
1020
|
+
async function ensureConfig(g, opts) {
|
|
885
1021
|
const persisted = readPersisted();
|
|
886
|
-
|
|
887
|
-
let
|
|
888
|
-
|
|
1022
|
+
const local = readLocalConfig();
|
|
1023
|
+
let baseUrl = g.baseUrl ?? process.env.SECHROOM_BASE_URL ?? local.baseUrl ?? persisted.baseUrl ?? DEFAULT_BASE_URL2;
|
|
1024
|
+
let tenant = g.tenant ?? process.env.SECHROOM_TENANT ?? local.tenant ?? persisted.tenant ?? "";
|
|
1025
|
+
if (canPrompt() && !opts.yes) {
|
|
889
1026
|
baseUrl = await promptText("Sechroom API base URL?", baseUrl);
|
|
890
1027
|
tenant = await promptText("Tenant id?", tenant || void 0);
|
|
891
1028
|
}
|
|
@@ -895,7 +1032,26 @@ async function ensureConfig(g, yes) {
|
|
|
895
1032
|
"No tenant set. Pass --tenant <id>, set SECHROOM_TENANT, or run `sechroom config set tenant <id>` \u2014 the API rejects untenanted requests (HTTP 400)."
|
|
896
1033
|
);
|
|
897
1034
|
}
|
|
898
|
-
|
|
1035
|
+
let storeLocal = Boolean(opts.local);
|
|
1036
|
+
if (!opts.local && canPrompt() && !opts.yes) {
|
|
1037
|
+
storeLocal = await promptSelect(
|
|
1038
|
+
"Where should this tenant + base URL be saved?",
|
|
1039
|
+
[
|
|
1040
|
+
{ label: "Globally", value: "global", hint: "all projects on this machine" },
|
|
1041
|
+
{ label: "This directory", value: "local", hint: ".sechroom.json \u2014 project + subdirs" }
|
|
1042
|
+
],
|
|
1043
|
+
local.path ? "local" : "global"
|
|
1044
|
+
) === "local";
|
|
1045
|
+
}
|
|
1046
|
+
if (storeLocal) {
|
|
1047
|
+
const path = writeLocalConfig({ baseUrl, tenant });
|
|
1048
|
+
if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved to ${path} (directory-local)
|
|
1049
|
+
`);
|
|
1050
|
+
} else {
|
|
1051
|
+
writePersisted({ baseUrl, tenant });
|
|
1052
|
+
if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved globally (~/.config/sechroom/config.json)
|
|
1053
|
+
`);
|
|
1054
|
+
}
|
|
899
1055
|
return { baseUrl, tenant, clientId: persisted.clientId };
|
|
900
1056
|
}
|
|
901
1057
|
async function ensureAuth(cfg, yes) {
|
|
@@ -937,33 +1093,59 @@ async function ensureTimezone(cfg, opts) {
|
|
|
937
1093
|
async function chooseClients(clientFlag, yes, cwd) {
|
|
938
1094
|
if (clientFlag) return resolveClientKeys(clientFlag);
|
|
939
1095
|
const detected = detectInstalledClients(cwd);
|
|
940
|
-
const
|
|
941
|
-
if (!canPrompt() || yes) return
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1096
|
+
const preselected = detected.length > 0 ? detected : [DEFAULT_CLIENT_KEY];
|
|
1097
|
+
if (!canPrompt() || yes) return preselected;
|
|
1098
|
+
const picks = await promptMultiSelect(
|
|
1099
|
+
"Which AI clients should I wire?",
|
|
1100
|
+
ALL_CLIENT_KEYS.map((k) => ({
|
|
1101
|
+
label: k,
|
|
1102
|
+
value: k,
|
|
1103
|
+
hint: detected.includes(k) ? "detected" : void 0
|
|
1104
|
+
})),
|
|
1105
|
+
preselected
|
|
947
1106
|
);
|
|
948
|
-
|
|
949
|
-
return resolveClientKeys(answer);
|
|
1107
|
+
return picks.length > 0 ? picks : preselected;
|
|
950
1108
|
}
|
|
951
1109
|
function registerOnboard(program2) {
|
|
952
|
-
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).
|
|
1110
|
+
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(
|
|
1111
|
+
"after",
|
|
1112
|
+
`
|
|
1113
|
+
Examples:
|
|
1114
|
+
$ sechroom onboard guided, interactive (asks where to save config + how to wire)
|
|
1115
|
+
$ sechroom onboard --cli-only just the CLI \u2014 no .mcp.json, no agent files
|
|
1116
|
+
$ sechroom onboard --no-mcp agent instructions only, skip MCP config
|
|
1117
|
+
$ sechroom onboard --local save tenant + base URL to ./.sechroom.json
|
|
1118
|
+
$ sechroom onboard --yes non-interactive: defaults + global config + full wire
|
|
1119
|
+
$ sechroom onboard --client all --dry-run preview wiring every client, write nothing`
|
|
1120
|
+
).action(async (opts, cmd) => {
|
|
953
1121
|
const g = cmd.optsWithGlobals();
|
|
954
1122
|
const json = Boolean(g.json);
|
|
955
1123
|
const yes = Boolean(opts.yes);
|
|
956
1124
|
const dryRun = Boolean(opts.dryRun);
|
|
957
|
-
const cfg = await ensureConfig(g, yes);
|
|
1125
|
+
const cfg = await ensureConfig(g, { yes, json, local: Boolean(opts.local) });
|
|
958
1126
|
await ensureAuth(cfg, yes);
|
|
959
1127
|
const tz = await ensureTimezone(cfg, { yes, dryRun });
|
|
960
1128
|
if (!json && tz.action !== "already-set") {
|
|
961
|
-
const line = tz.action === "set" ?
|
|
1129
|
+
const line = tz.action === "set" ? `${ok("\u2713")} timezone set to ${tz.timezone}
|
|
962
1130
|
` : tz.action === "dry-run" ? `(dry run \u2014 would set timezone to ${tz.timezone})
|
|
963
1131
|
` : `timezone not set \u2014 ${tz.note}
|
|
964
1132
|
`;
|
|
965
1133
|
process.stderr.write(line);
|
|
966
1134
|
}
|
|
1135
|
+
const wire = await chooseWire(opts, yes);
|
|
1136
|
+
if (wire === "cli-only") {
|
|
1137
|
+
if (json) {
|
|
1138
|
+
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, timezone: tz, wire, clients: [] }, true);
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
process.stdout.write(
|
|
1142
|
+
`
|
|
1143
|
+
${style.bold("Done.")} The CLI is configured for ${style.cyan(cfg.tenant)} \u2014 no AI-client files written.
|
|
1144
|
+
Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom --help")}
|
|
1145
|
+
`
|
|
1146
|
+
);
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
967
1149
|
const keys = await chooseClients(opts.client, yes, process.cwd());
|
|
968
1150
|
const setup = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
|
|
969
1151
|
const targets = clientTargets(process.cwd());
|
|
@@ -971,12 +1153,13 @@ function registerOnboard(program2) {
|
|
|
971
1153
|
if (!dryRun) {
|
|
972
1154
|
await maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, copyChoice(opts));
|
|
973
1155
|
}
|
|
1156
|
+
const writeMcp = wire === "full";
|
|
974
1157
|
const result = [];
|
|
975
1158
|
for (const key of keys) {
|
|
976
1159
|
const target = targets[key];
|
|
977
1160
|
const actions = await applyClient(cfg, setup, target, {
|
|
978
1161
|
dryRun,
|
|
979
|
-
mcp:
|
|
1162
|
+
mcp: writeMcp,
|
|
980
1163
|
agentFiles: true,
|
|
981
1164
|
personalWorkspaceId
|
|
982
1165
|
});
|
|
@@ -984,14 +1167,33 @@ function registerOnboard(program2) {
|
|
|
984
1167
|
if (!json) printActions(target, actions);
|
|
985
1168
|
}
|
|
986
1169
|
if (json) {
|
|
987
|
-
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, timezone: tz, clients: result }, true);
|
|
1170
|
+
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, timezone: tz, wire, clients: result }, true);
|
|
988
1171
|
return;
|
|
989
1172
|
}
|
|
990
1173
|
process.stdout.write(
|
|
991
|
-
dryRun ? "\n(dry run \u2014 nothing written)\n" :
|
|
1174
|
+
dryRun ? "\n(dry run \u2014 nothing written)\n" : writeMcp ? `
|
|
1175
|
+
${style.bold("Done.")} Restart your AI client (or reload MCP) to pick up the new config.
|
|
1176
|
+
` : `
|
|
1177
|
+
${style.bold("Done.")} Agent instructions written (no MCP config).
|
|
1178
|
+
`
|
|
992
1179
|
);
|
|
993
1180
|
});
|
|
994
1181
|
}
|
|
1182
|
+
async function chooseWire(opts, yes) {
|
|
1183
|
+
if (opts.cliOnly) return "cli-only";
|
|
1184
|
+
if (canPrompt() && !yes) {
|
|
1185
|
+
return promptSelect(
|
|
1186
|
+
"How should I set up Sechroom in this project?",
|
|
1187
|
+
[
|
|
1188
|
+
{ label: "Wire my AI client", value: "full", hint: "MCP server (.mcp.json) + agent instructions" },
|
|
1189
|
+
{ label: "Agent instructions only", value: "agent-only", hint: "skip MCP config" },
|
|
1190
|
+
{ label: "CLI only", value: "cli-only", hint: "don't write any AI-client files" }
|
|
1191
|
+
],
|
|
1192
|
+
"full"
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
return opts.mcp === false ? "agent-only" : "full";
|
|
1196
|
+
}
|
|
995
1197
|
|
|
996
1198
|
// src/index.ts
|
|
997
1199
|
function resolveVersion() {
|
|
@@ -1006,16 +1208,55 @@ function resolveVersion() {
|
|
|
1006
1208
|
}
|
|
1007
1209
|
var program = new Command();
|
|
1008
1210
|
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);
|
|
1211
|
+
program.addHelpText(
|
|
1212
|
+
"after",
|
|
1213
|
+
`
|
|
1214
|
+
Examples:
|
|
1215
|
+
$ sechroom onboard guided first-run: configure, sign in, wire this project
|
|
1216
|
+
$ sechroom login sign in via browser (OAuth + PKCE)
|
|
1217
|
+
$ sechroom config set tenant ocd set your tenant (global)
|
|
1218
|
+
$ sechroom config set --local tenant cli-smoke pin tenant for this directory (.sechroom.json)
|
|
1219
|
+
$ sechroom config show resolved config + which source won
|
|
1220
|
+
|
|
1221
|
+
$ sechroom memory create --text "a note" --title "Note" --tag idea
|
|
1222
|
+
$ sechroom memory search "convention drift" --limit 5
|
|
1223
|
+
$ sechroom memory get mem_XXXX
|
|
1224
|
+
$ sechroom worklog append --text "shipped X; PR #123" --source claude-code-chris
|
|
1225
|
+
$ sechroom lookup mem_XXXX resolve any id -> kind / title / view URL
|
|
1226
|
+
|
|
1227
|
+
$ sechroom --json memory search "auth" compact JSON for scripts and agents
|
|
1228
|
+
$ SECHROOM_TOKEN=<bearer> sechroom --json memory get mem_XXXX headless
|
|
1229
|
+
|
|
1230
|
+
Config precedence (high -> low): --flag > env (SECHROOM_*) > ./.sechroom.json > global > default.
|
|
1231
|
+
Run 'sechroom <command> --help' for command-specific examples.`
|
|
1232
|
+
);
|
|
1009
1233
|
program.hook("preAction", (_thisCmd, actionCmd) => {
|
|
1010
1234
|
setQuiet(Boolean(actionCmd.optsWithGlobals().json));
|
|
1011
1235
|
});
|
|
1012
|
-
program.command("login").description("Sign in via browser (OAuth auth-code + PKCE, dynamic client registration)").
|
|
1236
|
+
program.command("login").description("Sign in via browser (OAuth auth-code + PKCE, dynamic client registration)").addHelpText(
|
|
1237
|
+
"after",
|
|
1238
|
+
`
|
|
1239
|
+
Examples:
|
|
1240
|
+
$ sechroom login sign in to the configured base URL + tenant
|
|
1241
|
+
$ sechroom login --base-url https://staging.app.sechroom.ai/api
|
|
1242
|
+
$ export SECHROOM_TOKEN=<bearer> headless: skip login entirely (CI / agents)`
|
|
1243
|
+
).action(async (_opts, cmd) => {
|
|
1013
1244
|
const g = cmd.optsWithGlobals();
|
|
1014
1245
|
const persisted = readPersisted();
|
|
1015
1246
|
const baseUrl = g.baseUrl ?? process.env.SECHROOM_BASE_URL ?? persisted.baseUrl ?? "https://app.sechroom.ai/api";
|
|
1016
1247
|
await login({ baseUrl: baseUrl.replace(/\/$/, ""), tenant: g.tenant ?? "" });
|
|
1017
1248
|
});
|
|
1018
1249
|
var config = program.command("config").description("Manage persisted CLI config");
|
|
1250
|
+
config.addHelpText(
|
|
1251
|
+
"after",
|
|
1252
|
+
`
|
|
1253
|
+
Examples:
|
|
1254
|
+
$ sechroom config set baseUrl https://app.sechroom.ai/api prod (staging: https://staging.app.sechroom.ai/api)
|
|
1255
|
+
$ sechroom config set tenant ocd
|
|
1256
|
+
$ sechroom config set --local tenant cli-smoke this dir + subdirs (.sechroom.json)
|
|
1257
|
+
$ sechroom config set clientId dyn-XXXX global-only escape hatch (no DCR endpoint)
|
|
1258
|
+
$ sechroom config show --json`
|
|
1259
|
+
);
|
|
1019
1260
|
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) => {
|
|
1020
1261
|
if (opts.local) {
|
|
1021
1262
|
if (!["baseUrl", "tenant"].includes(key)) {
|
|
@@ -1065,8 +1306,8 @@ registerLookup(program);
|
|
|
1065
1306
|
registerInit(program);
|
|
1066
1307
|
registerSetup(program);
|
|
1067
1308
|
registerOnboard(program);
|
|
1068
|
-
program.parseAsync().catch((
|
|
1069
|
-
process.stderr.write(`error: ${
|
|
1309
|
+
program.parseAsync().catch((err2) => {
|
|
1310
|
+
process.stderr.write(`error: ${err2 instanceof Error ? err2.message : String(err2)}
|
|
1070
1311
|
`);
|
|
1071
1312
|
process.exit(1);
|
|
1072
1313
|
});
|
package/package.json
CHANGED