@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.
- package/README.md +38 -1
- package/dist/index.js +427 -46
- 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="<
|
|
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
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
266
|
-
|
|
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
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
319
|
-
|
|
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(
|
|
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