@sechroom/cli 2026.6.30 → 2026.6.31
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 +512 -100
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1773,8 +1773,13 @@ Examples:
|
|
|
1773
1773
|
});
|
|
1774
1774
|
}
|
|
1775
1775
|
|
|
1776
|
+
// src/commands/checkpoint.ts
|
|
1777
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1778
|
+
import { dirname as dirname5, join as join5 } from "path";
|
|
1779
|
+
|
|
1776
1780
|
// src/commands/hook.ts
|
|
1777
|
-
import {
|
|
1781
|
+
import { createHash as createHash2 } from "crypto";
|
|
1782
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1778
1783
|
import { homedir as homedir3 } from "os";
|
|
1779
1784
|
import { delimiter, dirname as dirname4, join as join4 } from "path";
|
|
1780
1785
|
|
|
@@ -1947,15 +1952,25 @@ var SectionType = {
|
|
|
1947
1952
|
* carried a workspaceId and that workspace has agent-setup-bundle memories. */
|
|
1948
1953
|
WorkspaceConventions: "workspace-conventions"
|
|
1949
1954
|
};
|
|
1950
|
-
async function fetchSetup(cfg) {
|
|
1955
|
+
async function fetchSetup(cfg, namespaceSlug) {
|
|
1951
1956
|
const client = await makeClient(cfg);
|
|
1957
|
+
const query = {};
|
|
1958
|
+
if (cfg.workspaceId) query.workspaceId = cfg.workspaceId;
|
|
1959
|
+
if (namespaceSlug) query.namespaceSlug = namespaceSlug;
|
|
1960
|
+
const hasQuery = query.workspaceId !== void 0 || query.namespaceSlug !== void 0;
|
|
1952
1961
|
const { data, error } = await client.GET(
|
|
1953
1962
|
"/operator-surface/setup",
|
|
1954
|
-
|
|
1963
|
+
hasQuery ? { params: { query } } : {}
|
|
1955
1964
|
);
|
|
1956
1965
|
if (error) throw new Error(`GET /operator-surface/setup failed: ${JSON.stringify(error)}`);
|
|
1957
1966
|
return data;
|
|
1958
1967
|
}
|
|
1968
|
+
async function listNamespaces(cfg) {
|
|
1969
|
+
const client = await makeClient(cfg);
|
|
1970
|
+
const { data } = await client.GET("/mcp-aggregator/namespaces", {});
|
|
1971
|
+
const rows = data ?? [];
|
|
1972
|
+
return rows.filter((r) => typeof r.slug === "string").map((r) => ({ slug: r.slug, displayName: r.displayName ?? r.slug }));
|
|
1973
|
+
}
|
|
1959
1974
|
function findSurface(setup, surfaceKey) {
|
|
1960
1975
|
return setup.surfaces.find((s) => s.surfaceKey === surfaceKey);
|
|
1961
1976
|
}
|
|
@@ -2026,12 +2041,14 @@ async function resolveWorkspaceConventions(cfg, section) {
|
|
|
2026
2041
|
if (parseTagArtifactId(artifact.id)) continue;
|
|
2027
2042
|
const mem = await fetchMemoryFields(cfg, artifact.id);
|
|
2028
2043
|
if (typeof mem?.text === "string" && mem.text.trim().length > 0) {
|
|
2029
|
-
|
|
2030
|
-
|
|
2044
|
+
const ref = `${artifact.id}@v${mem.version ?? 1}`;
|
|
2045
|
+
parts.push(`<!-- @sechroom/cli:section source=${ref} -->
|
|
2046
|
+
${mem.text.trim()}`);
|
|
2047
|
+
refs.push(ref);
|
|
2031
2048
|
}
|
|
2032
2049
|
}
|
|
2033
2050
|
if (parts.length === 0) return null;
|
|
2034
|
-
return { body: parts.join("\n\n
|
|
2051
|
+
return { body: parts.join("\n\n"), refs };
|
|
2035
2052
|
}
|
|
2036
2053
|
async function createOverride(cfg, template, personalWorkspaceId) {
|
|
2037
2054
|
const client = await makeClient(cfg);
|
|
@@ -2156,6 +2173,102 @@ function hasRequiredIntent(i) {
|
|
|
2156
2173
|
i.objective?.trim() && i.state?.trim() && i.lastAction?.trim() && i.nextAction?.trim() && i.resumeInstruction?.trim()
|
|
2157
2174
|
);
|
|
2158
2175
|
}
|
|
2176
|
+
async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScope, opts) {
|
|
2177
|
+
const lane = resolveLane(laneFlag, cwd);
|
|
2178
|
+
if (!lane) return false;
|
|
2179
|
+
const intent = readIntent(cwd);
|
|
2180
|
+
if (!intent || !hasRequiredIntent(intent)) return false;
|
|
2181
|
+
if (opts?.skipIfUnchanged && unchangedSinceLastPush(cwd, intent)) return false;
|
|
2182
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2183
|
+
const client = await makeClient(cfg);
|
|
2184
|
+
await client.POST("/continuity/snapshots", {
|
|
2185
|
+
body: {
|
|
2186
|
+
laneId: lane,
|
|
2187
|
+
scope: scopeFlag ?? intent.scope ?? defaultScope,
|
|
2188
|
+
currentObjective: intent.objective,
|
|
2189
|
+
currentState: intent.state,
|
|
2190
|
+
lastMeaningfulAction: intent.lastAction,
|
|
2191
|
+
nextIntendedAction: intent.nextAction,
|
|
2192
|
+
resumeInstruction: intent.resumeInstruction,
|
|
2193
|
+
activeConstraints: intent.constraints ?? null,
|
|
2194
|
+
openQuestions: intent.questions ?? null,
|
|
2195
|
+
surfaceMarkers: intent.surfaceMarkers ?? null,
|
|
2196
|
+
relevantArtifactIds: intent.artifacts ?? null,
|
|
2197
|
+
confidence: intent.confidence ?? null,
|
|
2198
|
+
// Frequent triggers (compaction, session-end) land within the FR-051 4h
|
|
2199
|
+
// window; Acknowledge lets the checkpoint persist on the lane.
|
|
2200
|
+
concurrentSessionPolicy: "Acknowledge"
|
|
2201
|
+
}
|
|
2202
|
+
});
|
|
2203
|
+
recordPush(cwd, intent);
|
|
2204
|
+
return true;
|
|
2205
|
+
}
|
|
2206
|
+
function ledgerPath(start) {
|
|
2207
|
+
const intent = resolveIntentPath(start);
|
|
2208
|
+
const dir = intent ? dirname4(intent) : join4(start, ".sechroom");
|
|
2209
|
+
return join4(dir, ".checkpoint-state.json");
|
|
2210
|
+
}
|
|
2211
|
+
function readLedger(start) {
|
|
2212
|
+
try {
|
|
2213
|
+
const p = ledgerPath(start);
|
|
2214
|
+
if (!existsSync4(p)) return {};
|
|
2215
|
+
return JSON.parse(readFileSync3(p, "utf8"));
|
|
2216
|
+
} catch {
|
|
2217
|
+
return {};
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
function intentHash(i) {
|
|
2221
|
+
const canonical = JSON.stringify({
|
|
2222
|
+
objective: i.objective ?? "",
|
|
2223
|
+
state: i.state ?? "",
|
|
2224
|
+
lastAction: i.lastAction ?? "",
|
|
2225
|
+
nextAction: i.nextAction ?? "",
|
|
2226
|
+
resumeInstruction: i.resumeInstruction ?? "",
|
|
2227
|
+
scope: i.scope ?? "",
|
|
2228
|
+
constraints: i.constraints ?? [],
|
|
2229
|
+
questions: i.questions ?? [],
|
|
2230
|
+
surfaceMarkers: i.surfaceMarkers ?? [],
|
|
2231
|
+
artifacts: i.artifacts ?? [],
|
|
2232
|
+
confidence: i.confidence ?? null
|
|
2233
|
+
});
|
|
2234
|
+
return createHash2("sha256").update(canonical, "utf8").digest("hex");
|
|
2235
|
+
}
|
|
2236
|
+
function recentlyCheckpointed(start, minutes) {
|
|
2237
|
+
const { lastEpochMs } = readLedger(start);
|
|
2238
|
+
return typeof lastEpochMs === "number" && Date.now() - lastEpochMs < minutes * 6e4;
|
|
2239
|
+
}
|
|
2240
|
+
function unchangedSinceLastPush(start, intent) {
|
|
2241
|
+
const ledger = readLedger(start);
|
|
2242
|
+
if (ledger.lastHash == null) return false;
|
|
2243
|
+
const path = resolveIntentPath(start);
|
|
2244
|
+
if (path && ledger.lastMtimeMs != null) {
|
|
2245
|
+
try {
|
|
2246
|
+
if (statSync2(path).mtimeMs <= ledger.lastMtimeMs) return true;
|
|
2247
|
+
} catch {
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
return intentHash(intent) === ledger.lastHash;
|
|
2251
|
+
}
|
|
2252
|
+
function recordPush(start, intent) {
|
|
2253
|
+
try {
|
|
2254
|
+
const p = ledgerPath(start);
|
|
2255
|
+
const path = resolveIntentPath(start);
|
|
2256
|
+
let mtimeMs;
|
|
2257
|
+
try {
|
|
2258
|
+
if (path) mtimeMs = statSync2(path).mtimeMs;
|
|
2259
|
+
} catch {
|
|
2260
|
+
mtimeMs = void 0;
|
|
2261
|
+
}
|
|
2262
|
+
mkdirSync3(dirname4(p), { recursive: true });
|
|
2263
|
+
const ledger = {
|
|
2264
|
+
lastEpochMs: Date.now(),
|
|
2265
|
+
lastMtimeMs: mtimeMs,
|
|
2266
|
+
lastHash: intentHash(intent)
|
|
2267
|
+
};
|
|
2268
|
+
writeFileSync3(p, JSON.stringify(ledger) + "\n");
|
|
2269
|
+
} catch {
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2159
2272
|
function formatContext(bundle, lane) {
|
|
2160
2273
|
const s = bundle?.latestSnapshot;
|
|
2161
2274
|
if (!s) return null;
|
|
@@ -2192,20 +2305,23 @@ function emitSessionStart(additionalContext) {
|
|
|
2192
2305
|
}) + "\n"
|
|
2193
2306
|
);
|
|
2194
2307
|
}
|
|
2195
|
-
var
|
|
2308
|
+
var CLAUDE_HOOK_COMMANDS = {
|
|
2196
2309
|
SessionStart: "sechroom hook session-start",
|
|
2197
|
-
PreCompact: "sechroom hook pre-compact"
|
|
2310
|
+
PreCompact: "sechroom hook pre-compact",
|
|
2311
|
+
SessionEnd: "sechroom hook session-end"
|
|
2312
|
+
};
|
|
2313
|
+
var CODEX_HOOK_COMMANDS = {
|
|
2314
|
+
SessionStart: "sechroom hook session-start",
|
|
2315
|
+
Stop: "sechroom hook session-end --debounce-minutes 10"
|
|
2198
2316
|
};
|
|
2199
|
-
var HOOK_EVENTS = ["SessionStart", "PreCompact"];
|
|
2200
2317
|
function hasHookCommand(config2, event, command) {
|
|
2201
2318
|
const groups = config2.hooks?.[event] ?? [];
|
|
2202
2319
|
return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
|
|
2203
2320
|
}
|
|
2204
|
-
function mergeHooks(config2) {
|
|
2321
|
+
function mergeHooks(config2, commands) {
|
|
2205
2322
|
config2.hooks ??= {};
|
|
2206
2323
|
let added = 0;
|
|
2207
|
-
for (const event of
|
|
2208
|
-
const command = HOOK_COMMANDS[event];
|
|
2324
|
+
for (const [event, command] of Object.entries(commands)) {
|
|
2209
2325
|
if (hasHookCommand(config2, event, command)) continue;
|
|
2210
2326
|
const groups = config2.hooks[event] ??= [];
|
|
2211
2327
|
groups.push({ hooks: [{ type: "command", command }] });
|
|
@@ -2219,10 +2335,10 @@ function readJsonConfig2(path) {
|
|
|
2219
2335
|
if (!raw.trim()) return {};
|
|
2220
2336
|
return JSON.parse(raw);
|
|
2221
2337
|
}
|
|
2222
|
-
function installHooksJson(path, dryRun) {
|
|
2338
|
+
function installHooksJson(path, commands, dryRun) {
|
|
2223
2339
|
const existed = existsSync4(path) && readFileSync3(path, "utf8").trim().length > 0;
|
|
2224
2340
|
const config2 = readJsonConfig2(path);
|
|
2225
|
-
const added = mergeHooks(config2);
|
|
2341
|
+
const added = mergeHooks(config2, commands);
|
|
2226
2342
|
if (added === 0 && existed) return { path, status: "current" };
|
|
2227
2343
|
if (!dryRun) {
|
|
2228
2344
|
mkdirSync3(dirname4(path), { recursive: true });
|
|
@@ -2283,9 +2399,9 @@ function installHookSurfaces(surfaces, opts) {
|
|
|
2283
2399
|
for (const surface of surfaces) {
|
|
2284
2400
|
if (surface === "claude") {
|
|
2285
2401
|
const path = opts.local ? join4(opts.cwd, ".claude", "settings.json") : join4(opts.home, ".claude", "settings.json");
|
|
2286
|
-
out.push({ surface, results: [installHooksJson(path, opts.dryRun)] });
|
|
2402
|
+
out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
|
|
2287
2403
|
} else {
|
|
2288
|
-
const hooksJson = installHooksJson(join4(opts.home, ".codex", "hooks.json"), opts.dryRun);
|
|
2404
|
+
const hooksJson = installHooksJson(join4(opts.home, ".codex", "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
|
|
2289
2405
|
const featureFlag = installCodexFeatureFlag(join4(opts.home, ".codex", "config.toml"), opts.dryRun);
|
|
2290
2406
|
out.push({ surface, results: [hooksJson, featureFlag] });
|
|
2291
2407
|
}
|
|
@@ -2365,31 +2481,23 @@ Fail-soft: no lane / no auth / no-or-partial intent file / API error -> exit 0,
|
|
|
2365
2481
|
const raw = await readStdin();
|
|
2366
2482
|
const input = parseHookInput(raw);
|
|
2367
2483
|
const cwd = input.cwd ?? process.cwd();
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
surfaceMarkers: intent.surfaceMarkers ?? null,
|
|
2386
|
-
relevantArtifactIds: intent.artifacts ?? null,
|
|
2387
|
-
confidence: intent.confidence ?? null,
|
|
2388
|
-
// Compaction is infrequent, so the FR-051 clobber guard doesn't bite;
|
|
2389
|
-
// Acknowledge lets a within-window checkpoint land on the lane.
|
|
2390
|
-
concurrentSessionPolicy: "Acknowledge"
|
|
2391
|
-
}
|
|
2392
|
-
});
|
|
2484
|
+
await saveSnapshotFromIntent(cmd, cwd, opts.lane, opts.scope, "compaction", { skipIfUnchanged: true });
|
|
2485
|
+
return process.exit(0);
|
|
2486
|
+
} catch {
|
|
2487
|
+
return process.exit(0);
|
|
2488
|
+
}
|
|
2489
|
+
});
|
|
2490
|
+
hook.command("session-end").description("Save a continuity snapshot from the intent file on a SessionEnd (Claude) / Stop (Codex) hook").option("--lane <laneId>", "Override the resolved lane (else SECHROOM_LANE, else ./.sem code-lane)").option("--scope <scope>", "Snapshot scope (else the intent file's `scope`, else 'session-end')").option("--surface <surface>", "Target surface: claude | codex (lifecycle-only on both)", "claude").option(
|
|
2491
|
+
"--debounce-minutes <n>",
|
|
2492
|
+
"skip if a hook checkpoint ran within this many minutes \u2014 for high-frequency triggers like Codex Stop (Claude SessionEnd passes none)"
|
|
2493
|
+
).action(async (opts, cmd) => {
|
|
2494
|
+
try {
|
|
2495
|
+
const raw = await readStdin();
|
|
2496
|
+
const input = parseHookInput(raw);
|
|
2497
|
+
const cwd = input.cwd ?? process.cwd();
|
|
2498
|
+
const debounce = opts.debounceMinutes != null ? Number(opts.debounceMinutes) : 0;
|
|
2499
|
+
if (debounce > 0 && recentlyCheckpointed(cwd, debounce)) return process.exit(0);
|
|
2500
|
+
await saveSnapshotFromIntent(cmd, cwd, opts.lane, opts.scope, "session-end", { skipIfUnchanged: true });
|
|
2393
2501
|
return process.exit(0);
|
|
2394
2502
|
} catch {
|
|
2395
2503
|
return process.exit(0);
|
|
@@ -2434,6 +2542,101 @@ Fail-soft: no lane / no auth / no-or-partial intent file / API error -> exit 0,
|
|
|
2434
2542
|
});
|
|
2435
2543
|
}
|
|
2436
2544
|
|
|
2545
|
+
// src/commands/checkpoint.ts
|
|
2546
|
+
function registerCheckpoint(program2) {
|
|
2547
|
+
program2.command("checkpoint").description(
|
|
2548
|
+
"Checkpoint working state: create a continuity snapshot (server-validated) AND sync ./.sechroom/continuity.json in one step"
|
|
2549
|
+
).option("--lane <laneId>", "Lane id (else SECHROOM_LANE, else ./.sem code-lane)").option("--scope <scope>", "Snapshot scope (else the file's scope, else 'session')").option("--objective <text>", "Current objective").option("--state <text>", "Current state").option("--last-action <text>", "Last meaningful action").option("--next-action <text>", "Next intended action").option("--resume-instruction <text>", "Resume instruction").option("--constraint <text...>", "Active constraints (repeatable)").option("--question <text...>", "Open questions (repeatable)").option("--surface-marker <text...>", "Surface markers (repeatable)").option("--artifact <id...>", "Relevant artifact ids (repeatable)").option("--confidence <n>", "Confidence 0..1").option("--dry-run", "validate + print the snapshot payload without creating it or writing the file", false).addHelpText(
|
|
2550
|
+
"after",
|
|
2551
|
+
`
|
|
2552
|
+
File-first: reads ./.sechroom/continuity.json (kept current as you work) as the base; any flag
|
|
2553
|
+
overrides that field. The snapshot is created FIRST (server-validated), then the local file is
|
|
2554
|
+
written/normalized with the returned snapshotId. Lane: --lane > SECHROOM_LANE > ./.sem code-lane.
|
|
2555
|
+
|
|
2556
|
+
Examples:
|
|
2557
|
+
$ sechroom checkpoint snapshot from ./.sechroom/continuity.json, then sync it
|
|
2558
|
+
$ sechroom checkpoint --next-action "..." override one field, keep the rest from the file
|
|
2559
|
+
$ sechroom checkpoint --lane claude-code-chris --objective "..." --state "..." \\
|
|
2560
|
+
--last-action "..." --next-action "..." --resume-instruction "..."`
|
|
2561
|
+
).action(async (opts, cmd) => {
|
|
2562
|
+
const cwd = process.cwd();
|
|
2563
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2564
|
+
const json = Boolean(cmd.optsWithGlobals().json);
|
|
2565
|
+
const base = readIntent(cwd) ?? {};
|
|
2566
|
+
const merged = {
|
|
2567
|
+
objective: opts.objective ?? base.objective,
|
|
2568
|
+
state: opts.state ?? base.state,
|
|
2569
|
+
lastAction: opts.lastAction ?? base.lastAction,
|
|
2570
|
+
nextAction: opts.nextAction ?? base.nextAction,
|
|
2571
|
+
resumeInstruction: opts.resumeInstruction ?? base.resumeInstruction,
|
|
2572
|
+
scope: opts.scope ?? base.scope,
|
|
2573
|
+
constraints: opts.constraint ?? base.constraints,
|
|
2574
|
+
questions: opts.question ?? base.questions,
|
|
2575
|
+
surfaceMarkers: opts.surfaceMarker ?? base.surfaceMarkers,
|
|
2576
|
+
artifacts: opts.artifact ?? base.artifacts,
|
|
2577
|
+
confidence: opts.confidence != null ? Number(opts.confidence) : base.confidence
|
|
2578
|
+
};
|
|
2579
|
+
const lane = resolveLane(opts.lane, cwd);
|
|
2580
|
+
if (!lane) {
|
|
2581
|
+
fail(
|
|
2582
|
+
"no lane resolved \u2014 pass --lane, set SECHROOM_LANE, or pin one in ./.sem (code-lane). See `sechroom lane`."
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
const required = [
|
|
2586
|
+
["objective", "--objective"],
|
|
2587
|
+
["state", "--state"],
|
|
2588
|
+
["lastAction", "--last-action"],
|
|
2589
|
+
["nextAction", "--next-action"],
|
|
2590
|
+
["resumeInstruction", "--resume-instruction"]
|
|
2591
|
+
];
|
|
2592
|
+
const missing = required.filter(([k]) => !String(merged[k] ?? "").trim()).map(([, flag]) => flag);
|
|
2593
|
+
if (missing.length > 0) {
|
|
2594
|
+
fail(
|
|
2595
|
+
`missing required field(s): ${missing.join(", ")} \u2014 supply via flag or in ./.sechroom/continuity.json`
|
|
2596
|
+
);
|
|
2597
|
+
}
|
|
2598
|
+
const scope = merged.scope ?? "session";
|
|
2599
|
+
const body = {
|
|
2600
|
+
laneId: lane,
|
|
2601
|
+
scope,
|
|
2602
|
+
currentObjective: merged.objective,
|
|
2603
|
+
currentState: merged.state,
|
|
2604
|
+
lastMeaningfulAction: merged.lastAction,
|
|
2605
|
+
nextIntendedAction: merged.nextAction,
|
|
2606
|
+
resumeInstruction: merged.resumeInstruction,
|
|
2607
|
+
activeConstraints: merged.constraints ?? null,
|
|
2608
|
+
openQuestions: merged.questions ?? null,
|
|
2609
|
+
surfaceMarkers: merged.surfaceMarkers ?? null,
|
|
2610
|
+
relevantArtifactIds: merged.artifacts ?? null,
|
|
2611
|
+
confidence: merged.confidence ?? null,
|
|
2612
|
+
// Explicit checkpoints are often within the FR-051 4h window; Acknowledge
|
|
2613
|
+
// lets one land on the lane (matches `hook pre-compact`).
|
|
2614
|
+
concurrentSessionPolicy: "Acknowledge"
|
|
2615
|
+
};
|
|
2616
|
+
if (opts.dryRun) {
|
|
2617
|
+
emit({ dryRun: true, lane, scope, wouldCreate: body }, json);
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
const data = await runApi("Creating snapshot", async () => {
|
|
2621
|
+
const client = await makeClient(cfg);
|
|
2622
|
+
return client.POST("/continuity/snapshots", { body });
|
|
2623
|
+
});
|
|
2624
|
+
const path = resolveIntentPath(cwd) ?? join5(cwd, INTENT_FILE);
|
|
2625
|
+
const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
|
|
2626
|
+
mkdirSync4(dirname5(path), { recursive: true });
|
|
2627
|
+
writeFileSync4(path, JSON.stringify(fileBody, null, 2) + "\n");
|
|
2628
|
+
recordPush(cwd, merged);
|
|
2629
|
+
if (json) {
|
|
2630
|
+
emit({ snapshotId: data.snapshotId, lane, scope, file: path }, true);
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
process.stdout.write(
|
|
2634
|
+
`${style.bold("\u2713")} checkpoint ${style.bold(data.snapshotId)} ${style.dim(`(lane ${lane}, scope ${scope})`)} \u2014 synced ${path}
|
|
2635
|
+
`
|
|
2636
|
+
);
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2437
2640
|
// src/commands/account.ts
|
|
2438
2641
|
function registerId(program2) {
|
|
2439
2642
|
const id = program2.command("id").description("Allocate human-authored id sequences (FR-*, D-*)");
|
|
@@ -2650,16 +2853,16 @@ Examples:
|
|
|
2650
2853
|
}
|
|
2651
2854
|
|
|
2652
2855
|
// src/setup/apply.ts
|
|
2653
|
-
import { createHash as
|
|
2654
|
-
import { mkdirSync as
|
|
2655
|
-
import { dirname as
|
|
2856
|
+
import { createHash as createHash3 } from "crypto";
|
|
2857
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5, existsSync as existsSync5 } from "fs";
|
|
2858
|
+
import { dirname as dirname6 } from "path";
|
|
2656
2859
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
2657
2860
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
2658
2861
|
function normalizeBody(s) {
|
|
2659
2862
|
return s.replace(/\r\n/g, "\n").trim();
|
|
2660
2863
|
}
|
|
2661
2864
|
function bodySha256(body) {
|
|
2662
|
-
return
|
|
2865
|
+
return createHash3("sha256").update(normalizeBody(body), "utf8").digest("hex");
|
|
2663
2866
|
}
|
|
2664
2867
|
function renderBlock(write) {
|
|
2665
2868
|
const body = normalizeBody(write.body);
|
|
@@ -2705,7 +2908,7 @@ function parseManagedBlock(content, block) {
|
|
|
2705
2908
|
return null;
|
|
2706
2909
|
}
|
|
2707
2910
|
function ensureDir2(path) {
|
|
2708
|
-
|
|
2911
|
+
mkdirSync5(dirname6(path), { recursive: true });
|
|
2709
2912
|
}
|
|
2710
2913
|
function readOr(path, fallback) {
|
|
2711
2914
|
try {
|
|
@@ -2728,7 +2931,7 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
2728
2931
|
current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
|
|
2729
2932
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
2730
2933
|
ensureDir2(path);
|
|
2731
|
-
|
|
2934
|
+
writeFileSync5(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
2732
2935
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
2733
2936
|
}
|
|
2734
2937
|
function mergeCodexToml(path, snippet, dryRun) {
|
|
@@ -2739,7 +2942,7 @@ function mergeCodexToml(path, snippet, dryRun) {
|
|
|
2739
2942
|
const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
|
|
2740
2943
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
2741
2944
|
ensureDir2(path);
|
|
2742
|
-
|
|
2945
|
+
writeFileSync5(path, next, { mode: 384 });
|
|
2743
2946
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
2744
2947
|
}
|
|
2745
2948
|
function writeInstructionBlock(path, write, dryRun) {
|
|
@@ -2747,7 +2950,7 @@ function writeInstructionBlock(path, write, dryRun) {
|
|
|
2747
2950
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
2748
2951
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
2749
2952
|
ensureDir2(path);
|
|
2750
|
-
|
|
2953
|
+
writeFileSync5(path, next);
|
|
2751
2954
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
2752
2955
|
}
|
|
2753
2956
|
function computeBlockFile(current, write) {
|
|
@@ -2788,7 +2991,7 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
2788
2991
|
const next = computeBlockFile(current, write);
|
|
2789
2992
|
if (!dryRun) {
|
|
2790
2993
|
ensureDir2(proposedPath);
|
|
2791
|
-
|
|
2994
|
+
writeFileSync5(proposedPath, next);
|
|
2792
2995
|
}
|
|
2793
2996
|
return {
|
|
2794
2997
|
kind: "instruction",
|
|
@@ -2897,8 +3100,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
2897
3100
|
}
|
|
2898
3101
|
|
|
2899
3102
|
// src/setup/skills-offer.ts
|
|
2900
|
-
import { mkdirSync as
|
|
2901
|
-
import { join as
|
|
3103
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
3104
|
+
import { join as join7 } from "path";
|
|
2902
3105
|
|
|
2903
3106
|
// src/setup/lane-pin.ts
|
|
2904
3107
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -3024,19 +3227,19 @@ function resolveAgents(systemRows, personalRows, surface) {
|
|
|
3024
3227
|
}
|
|
3025
3228
|
|
|
3026
3229
|
// src/setup/skills-lock.ts
|
|
3027
|
-
import { existsSync as existsSync6, mkdirSync as
|
|
3230
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
3028
3231
|
import { homedir as homedir5 } from "os";
|
|
3029
|
-
import { join as
|
|
3232
|
+
import { join as join6 } from "path";
|
|
3030
3233
|
var SKILLS_LOCK = ".sechroom-skills.json";
|
|
3031
3234
|
var DEFAULT_SKILLS_SLUG = "operator-skills";
|
|
3032
3235
|
function skillsDir(global) {
|
|
3033
|
-
return global ?
|
|
3236
|
+
return global ? join6(homedir5(), ".claude", "skills") : join6(process.cwd(), ".claude", "skills");
|
|
3034
3237
|
}
|
|
3035
3238
|
function agentsDir(global) {
|
|
3036
|
-
return global ?
|
|
3239
|
+
return global ? join6(homedir5(), ".claude", "agents") : join6(process.cwd(), ".claude", "agents");
|
|
3037
3240
|
}
|
|
3038
3241
|
function readSkillsLock(dir) {
|
|
3039
|
-
const lockPath =
|
|
3242
|
+
const lockPath = join6(dir, SKILLS_LOCK);
|
|
3040
3243
|
if (!existsSync6(lockPath)) return {};
|
|
3041
3244
|
try {
|
|
3042
3245
|
return JSON.parse(readFileSync5(lockPath, "utf8"));
|
|
@@ -3045,8 +3248,8 @@ function readSkillsLock(dir) {
|
|
|
3045
3248
|
}
|
|
3046
3249
|
}
|
|
3047
3250
|
function writeSkillsLock(dir, lock) {
|
|
3048
|
-
|
|
3049
|
-
|
|
3251
|
+
mkdirSync6(dir, { recursive: true });
|
|
3252
|
+
writeFileSync6(join6(dir, SKILLS_LOCK), JSON.stringify(lock, null, 2) + "\n");
|
|
3050
3253
|
}
|
|
3051
3254
|
function recordMaterialisedSkills(dir, slug, skills, meta = {}) {
|
|
3052
3255
|
const lock = readSkillsLock(dir);
|
|
@@ -3106,8 +3309,8 @@ Found ${summary} available to you for ${surface}.
|
|
|
3106
3309
|
if (skills.length > 0) {
|
|
3107
3310
|
const written = [];
|
|
3108
3311
|
for (const s of skills) {
|
|
3109
|
-
|
|
3110
|
-
|
|
3312
|
+
mkdirSync7(join7(sDir, s.name), { recursive: true });
|
|
3313
|
+
writeFileSync7(join7(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
3111
3314
|
written.push(s.name);
|
|
3112
3315
|
}
|
|
3113
3316
|
recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -3115,11 +3318,11 @@ Found ${summary} available to you for ${surface}.
|
|
|
3115
3318
|
`);
|
|
3116
3319
|
}
|
|
3117
3320
|
if (agents.length > 0) {
|
|
3118
|
-
|
|
3321
|
+
mkdirSync7(aDir, { recursive: true });
|
|
3119
3322
|
const written = [];
|
|
3120
3323
|
for (const a of agents) {
|
|
3121
3324
|
const file = `${a.name}.md`;
|
|
3122
|
-
|
|
3325
|
+
writeFileSync7(join7(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
3123
3326
|
written.push(file);
|
|
3124
3327
|
}
|
|
3125
3328
|
recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -3187,8 +3390,65 @@ ${client.label} (${client.key}):
|
|
|
3187
3390
|
`);
|
|
3188
3391
|
}
|
|
3189
3392
|
}
|
|
3393
|
+
function resolveEvalMode(opts) {
|
|
3394
|
+
return opts.check ? "check" : opts.force ? "force" : "apply";
|
|
3395
|
+
}
|
|
3396
|
+
function summarizeEval(result, mode, json, dryRun) {
|
|
3397
|
+
const counts = { current: 0, stale: 0, drift: 0, absent: 0 };
|
|
3398
|
+
for (const { actions } of result) for (const a of actions) if (a.eval) counts[a.eval]++;
|
|
3399
|
+
const wouldChange = counts.stale + counts.drift + counts.absent;
|
|
3400
|
+
if (mode === "check") {
|
|
3401
|
+
if (!json) {
|
|
3402
|
+
if (wouldChange === 0) {
|
|
3403
|
+
process.stdout.write("\u2713 all instruction blocks are up to date.\n");
|
|
3404
|
+
} else {
|
|
3405
|
+
const bits = [];
|
|
3406
|
+
if (counts.stale) bits.push(`${counts.stale} out of date`);
|
|
3407
|
+
if (counts.drift) bits.push(`${counts.drift} with local edits`);
|
|
3408
|
+
if (counts.absent) bits.push(`${counts.absent} not yet written`);
|
|
3409
|
+
process.stderr.write(
|
|
3410
|
+
`\u26A0 ${wouldChange} instruction block(s) would change: ${bits.join(", ")}. Re-run with ${style.cyan("--refresh")}.
|
|
3411
|
+
`
|
|
3412
|
+
);
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
process.exit(wouldChange === 0 ? 0 : 1);
|
|
3416
|
+
}
|
|
3417
|
+
if (json) return;
|
|
3418
|
+
if (!dryRun && counts.stale) {
|
|
3419
|
+
process.stderr.write(`\u21BB refreshed ${counts.stale} section(s) the server had moved
|
|
3420
|
+
`);
|
|
3421
|
+
}
|
|
3422
|
+
if (!dryRun && counts.drift) {
|
|
3423
|
+
process.stderr.write(
|
|
3424
|
+
mode === "force" ? `\u26A0 overwrote ${counts.drift} section(s) that had local edits (--force)
|
|
3425
|
+
` : `\u26A0 ${counts.drift} section(s) have local edits \u2014 wrote a .proposed file alongside (original untouched). Review + merge, or re-run with ${style.cyan("--force")}.
|
|
3426
|
+
`
|
|
3427
|
+
);
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
var GLOBAL_NAMESPACE = "__global__";
|
|
3431
|
+
async function resolveNamespaceChoice(cfg, flag) {
|
|
3432
|
+
if (flag) return flag;
|
|
3433
|
+
if (!canPrompt()) return null;
|
|
3434
|
+
const namespaces = await listNamespaces(cfg);
|
|
3435
|
+
if (namespaces.length === 0) return null;
|
|
3436
|
+
const picked = await promptSelect(
|
|
3437
|
+
"Which namespace should this connection use?",
|
|
3438
|
+
[
|
|
3439
|
+
{ label: "Global (whole tenant)", value: GLOBAL_NAMESPACE },
|
|
3440
|
+
...namespaces.map((n) => ({
|
|
3441
|
+
label: n.displayName,
|
|
3442
|
+
value: n.slug,
|
|
3443
|
+
hint: n.slug
|
|
3444
|
+
}))
|
|
3445
|
+
],
|
|
3446
|
+
GLOBAL_NAMESPACE
|
|
3447
|
+
);
|
|
3448
|
+
return picked === GLOBAL_NAMESPACE ? null : picked;
|
|
3449
|
+
}
|
|
3190
3450
|
function registerInit(program2) {
|
|
3191
|
-
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(
|
|
3451
|
+
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)").option("--namespace <slug>", "MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)").option("--refresh", "refresh out-of-date agent-file blocks in place (local edits preserved to .proposed)", false).option("--force", "rewrite agent-file managed blocks, overwriting local edits inside the markers", false).option("--check", "report whether agent files would change and exit (0 = current, 1 = stale/drift/absent); writes nothing", false).addHelpText(
|
|
3192
3452
|
"after",
|
|
3193
3453
|
`
|
|
3194
3454
|
Examples:
|
|
@@ -3199,12 +3459,18 @@ Examples:
|
|
|
3199
3459
|
$ sechroom init --dry-run --json preview the writes, change nothing`
|
|
3200
3460
|
).action(async (opts, cmd) => {
|
|
3201
3461
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3202
|
-
const
|
|
3462
|
+
const mode = resolveEvalMode(opts);
|
|
3463
|
+
const check = mode === "check";
|
|
3464
|
+
const namespaceSlug = await resolveNamespaceChoice(cfg, opts.namespace);
|
|
3465
|
+
const setup = await withSpinner(
|
|
3466
|
+
"Fetching setup descriptors",
|
|
3467
|
+
() => fetchSetup(cfg, namespaceSlug ?? void 0)
|
|
3468
|
+
);
|
|
3203
3469
|
const targets = clientTargets(process.cwd());
|
|
3204
3470
|
const keys = resolveClientKeys(opts.client);
|
|
3205
3471
|
const json = cmd.optsWithGlobals().json;
|
|
3206
3472
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
3207
|
-
if (!opts.dryRun && !opts.mcpOnly) {
|
|
3473
|
+
if (!opts.dryRun && !opts.mcpOnly && !check) {
|
|
3208
3474
|
await maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, copyChoice(opts));
|
|
3209
3475
|
}
|
|
3210
3476
|
const result = [];
|
|
@@ -3214,11 +3480,13 @@ Examples:
|
|
|
3214
3480
|
dryRun: Boolean(opts.dryRun),
|
|
3215
3481
|
mcp: !opts.agentFilesOnly,
|
|
3216
3482
|
agentFiles: !opts.mcpOnly,
|
|
3217
|
-
personalWorkspaceId
|
|
3483
|
+
personalWorkspaceId,
|
|
3484
|
+
mode
|
|
3218
3485
|
});
|
|
3219
3486
|
result.push({ client: key, actions });
|
|
3220
|
-
if (!json) printActions(target, actions);
|
|
3487
|
+
if (!json && !check) printActions(target, actions);
|
|
3221
3488
|
}
|
|
3489
|
+
summarizeEval(result, mode, Boolean(json), Boolean(opts.dryRun));
|
|
3222
3490
|
if (!json && !opts.dryRun && !opts.mcpOnly) {
|
|
3223
3491
|
await maybeOfferSkills(cfg, personalWorkspaceId, { yes: false, dryRun: Boolean(opts.dryRun), surface: "claude-code" });
|
|
3224
3492
|
}
|
|
@@ -3244,20 +3512,93 @@ Next \u2014 verify: ${verify.description}
|
|
|
3244
3512
|
}
|
|
3245
3513
|
function registerSetup(program2) {
|
|
3246
3514
|
const setup = program2.command("setup").description("Granular onboarding steps (init runs these together)");
|
|
3247
|
-
setup.command("mcp <clients...>").description(`Write only the MCP config for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`).option("--dry-run", "print what would be written without writing", false).addHelpText("after", "\nExamples:\n $ sechroom setup mcp codex\n $ sechroom setup mcp claude-code codex\n $ sechroom setup mcp all").action(async (clients, opts, cmd) => {
|
|
3248
|
-
await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: true, agentFiles: false });
|
|
3515
|
+
setup.command("mcp <clients...>").description(`Write only the MCP config for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`).option("--dry-run", "print what would be written without writing", false).option("--namespace <slug>", "MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)").addHelpText("after", "\nExamples:\n $ sechroom setup mcp codex\n $ sechroom setup mcp claude-code codex\n $ sechroom setup mcp all").action(async (clients, opts, cmd) => {
|
|
3516
|
+
await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: true, agentFiles: false, namespace: opts.namespace });
|
|
3249
3517
|
});
|
|
3250
|
-
setup.command("agent-files <clients...>").description(`Write only the agent instruction file(s) for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`).option("--dry-run", "print what would be written without writing", false).option("--copy", "make a personal copy you can edit (default: prompt on a TTY, else skip)").addHelpText("after", "\nExamples:\n $ sechroom setup agent-files claude-code CLAUDE.md\n $ sechroom setup agent-files claude-code codex CLAUDE.md + AGENTS.md in one run\n $ sechroom setup agent-files all").action(async (clients, opts, cmd) => {
|
|
3251
|
-
await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: false, agentFiles: true, copy: opts.copy });
|
|
3518
|
+
setup.command("agent-files <clients...>").description(`Write only the agent instruction file(s) for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`).option("--dry-run", "print what would be written without writing", false).option("--copy", "make a personal copy you can edit (default: prompt on a TTY, else skip)").option("--refresh", "refresh out-of-date blocks in place (local edits preserved to .proposed)", false).option("--force", "rewrite managed blocks, overwriting local edits inside the markers", false).option("--check", "report whether anything would change and exit (0 = current, 1 = stale/drift/absent); writes nothing", false).addHelpText("after", "\nExamples:\n $ sechroom setup agent-files claude-code CLAUDE.md\n $ sechroom setup agent-files claude-code codex CLAUDE.md + AGENTS.md in one run\n $ sechroom setup agent-files all --check CI gate: nonzero exit if out of date\n $ sechroom setup agent-files claude-code --force overwrite local edits in the managed block").action(async (clients, opts, cmd) => {
|
|
3519
|
+
await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: false, agentFiles: true, copy: opts.copy, mode: resolveEvalMode(opts) });
|
|
3520
|
+
});
|
|
3521
|
+
setup.command("new-convention <title...>").description("Scaffold a workspace-conventions section: author a correctly-tagged memo (header as first body line) + regen the agent files").option("--kind <kind>", "reference | standard (orders the section; reference first)", "reference").option("--workspace <id>", "workspace to author in (default: the bound workspace)").option("--body <markdown>", "section body (default: a TODO scaffold to edit later)").option("--no-regen", "skip the agent-files regen after authoring").option("--dry-run", "print what would be authored; write nothing", false).addHelpText(
|
|
3522
|
+
"after",
|
|
3523
|
+
`
|
|
3524
|
+
The memo carries the two conventions a workspace-conventions section needs (FR-sechroom-236):
|
|
3525
|
+
the \`agent-setup-bundle\` tag + the \`# Header\` as the FIRST body line. It's authored in the
|
|
3526
|
+
BOUND workspace (so the regen, which sources conventions from there, picks it up). Edit it later
|
|
3527
|
+
in the app or via \`sechroom memory edit-text\`.
|
|
3528
|
+
|
|
3529
|
+
Examples:
|
|
3530
|
+
$ sechroom setup new-convention "Deploy runbook"
|
|
3531
|
+
$ sechroom setup new-convention "Backend testing" --kind standard --body "- run dotnet test ..."
|
|
3532
|
+
$ sechroom setup new-convention "Draft section" --no-regen author only, regen later`
|
|
3533
|
+
).action(async (titleParts, opts, cmd) => {
|
|
3534
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3535
|
+
const json = Boolean(cmd.optsWithGlobals().json);
|
|
3536
|
+
const title = titleParts.join(" ").trim();
|
|
3537
|
+
if (!title) fail('a section title is required, e.g. `sechroom setup new-convention "Deploy runbook"`.');
|
|
3538
|
+
const workspaceId = opts.workspace ?? cfg.workspaceId;
|
|
3539
|
+
if (!workspaceId)
|
|
3540
|
+
fail("no workspace \u2014 pass --workspace <id> or bind one (`sechroom config set --local workspaceId <id>`).");
|
|
3541
|
+
const kind = String(opts.kind).toLowerCase() === "standard" ? "standard" : "reference";
|
|
3542
|
+
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._";
|
|
3543
|
+
const text = `# ${title}
|
|
3544
|
+
|
|
3545
|
+
${body}
|
|
3546
|
+
`;
|
|
3547
|
+
const tags = ["agent-setup-bundle", "scope:sechroom", `kind:${kind}`, "archetype:document"];
|
|
3548
|
+
if (opts.dryRun) {
|
|
3549
|
+
emit({ dryRun: true, workspaceId, title, kind, tags, text }, json);
|
|
3550
|
+
return;
|
|
3551
|
+
}
|
|
3552
|
+
const data = await runApi("Authoring convention memo", async () => {
|
|
3553
|
+
const client = await makeClient(cfg);
|
|
3554
|
+
return client.POST("/memories", {
|
|
3555
|
+
body: {
|
|
3556
|
+
text,
|
|
3557
|
+
type: kind,
|
|
3558
|
+
content: "{}",
|
|
3559
|
+
confidence: 1,
|
|
3560
|
+
source: "cli-new-convention",
|
|
3561
|
+
archetype: "Document",
|
|
3562
|
+
title,
|
|
3563
|
+
tags,
|
|
3564
|
+
owner: { type: "Workspace", id: workspaceId }
|
|
3565
|
+
}
|
|
3566
|
+
});
|
|
3567
|
+
});
|
|
3568
|
+
if (!json) {
|
|
3569
|
+
const view = resolveViewUrl(cfg.baseUrl, data.url);
|
|
3570
|
+
process.stdout.write(
|
|
3571
|
+
`\u2713 authored convention ${style.bold(data.id)} ${style.dim(`"${title}"`)}${view ? ` ${style.dim("\u2192")} ${view}` : ""}
|
|
3572
|
+
`
|
|
3573
|
+
);
|
|
3574
|
+
}
|
|
3575
|
+
if (opts.regen === false) {
|
|
3576
|
+
if (json) emit({ id: data.id, workspaceId, regen: false }, true);
|
|
3577
|
+
else process.stdout.write("Skipped regen (--no-regen). Run `sechroom setup agent-files all` to apply.\n");
|
|
3578
|
+
return;
|
|
3579
|
+
}
|
|
3580
|
+
await runClients(["claude-code", "codex"], cmd, {
|
|
3581
|
+
dryRun: false,
|
|
3582
|
+
mcp: false,
|
|
3583
|
+
agentFiles: true,
|
|
3584
|
+
copy: false,
|
|
3585
|
+
mode: "apply"
|
|
3586
|
+
});
|
|
3252
3587
|
});
|
|
3253
3588
|
}
|
|
3254
3589
|
async function runClients(clients, cmd, opts) {
|
|
3255
3590
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3591
|
+
const mode = opts.mode ?? "apply";
|
|
3592
|
+
const check = mode === "check";
|
|
3256
3593
|
const targets = clientTargets(process.cwd());
|
|
3257
3594
|
const keys = resolveClientKeys(clients.join(","));
|
|
3258
|
-
const
|
|
3595
|
+
const namespaceSlug = opts.mcp ? await resolveNamespaceChoice(cfg, opts.namespace) : null;
|
|
3596
|
+
const setupData = await withSpinner(
|
|
3597
|
+
"Fetching setup descriptors",
|
|
3598
|
+
() => fetchSetup(cfg, namespaceSlug ?? void 0)
|
|
3599
|
+
);
|
|
3259
3600
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
3260
|
-
if (opts.agentFiles && !opts.dryRun) {
|
|
3601
|
+
if (opts.agentFiles && !opts.dryRun && !check) {
|
|
3261
3602
|
await maybeOfferCopies(cfg, setupData, targets, keys, personalWorkspaceId, copyChoice(opts));
|
|
3262
3603
|
}
|
|
3263
3604
|
const json = cmd.optsWithGlobals().json;
|
|
@@ -3268,11 +3609,13 @@ async function runClients(clients, cmd, opts) {
|
|
|
3268
3609
|
dryRun: opts.dryRun,
|
|
3269
3610
|
mcp: opts.mcp,
|
|
3270
3611
|
agentFiles: opts.agentFiles,
|
|
3271
|
-
personalWorkspaceId
|
|
3612
|
+
personalWorkspaceId,
|
|
3613
|
+
mode
|
|
3272
3614
|
});
|
|
3273
3615
|
result.push({ client: key, actions });
|
|
3274
|
-
if (!json) printActions(target, actions);
|
|
3616
|
+
if (!json && !check) printActions(target, actions);
|
|
3275
3617
|
}
|
|
3618
|
+
summarizeEval(result, mode, Boolean(json), opts.dryRun);
|
|
3276
3619
|
if (json) {
|
|
3277
3620
|
emit({ dryRun: opts.dryRun, clients: result }, true);
|
|
3278
3621
|
return;
|
|
@@ -3280,14 +3623,81 @@ async function runClients(clients, cmd, opts) {
|
|
|
3280
3623
|
process.stdout.write(opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n");
|
|
3281
3624
|
}
|
|
3282
3625
|
|
|
3626
|
+
// src/commands/namespace.ts
|
|
3627
|
+
function registerNamespace(program2) {
|
|
3628
|
+
const namespace = program2.command("namespace").description("Browse, inspect, and wire up MCP namespaces");
|
|
3629
|
+
namespace.addHelpText(
|
|
3630
|
+
"after",
|
|
3631
|
+
`
|
|
3632
|
+
Examples:
|
|
3633
|
+
$ sechroom namespace list
|
|
3634
|
+
$ sechroom namespace show eng
|
|
3635
|
+
$ sechroom namespace use eng wire Claude Code to the 'eng' namespace
|
|
3636
|
+
$ sechroom namespace use eng --client all`
|
|
3637
|
+
);
|
|
3638
|
+
namespace.command("list").description("List the namespaces you can reach (GET /mcp-aggregator/namespaces)").action(async (_opts, cmd) => {
|
|
3639
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3640
|
+
const data = await runApi("Listing namespaces", async () => {
|
|
3641
|
+
const client = await makeClient(cfg);
|
|
3642
|
+
return client.GET("/mcp-aggregator/namespaces", {});
|
|
3643
|
+
});
|
|
3644
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
3645
|
+
});
|
|
3646
|
+
namespace.command("show <slug>").description("Show a namespace's details (GET /mcp-aggregator/namespaces/{slug}). For the tool list it exposes, point an OpenAPI client at /t/{tenant}/namespaces/{slug}/api/openapi.json.").action(async (slug, _opts, cmd) => {
|
|
3647
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3648
|
+
const data = await runApi("Fetching namespace", async () => {
|
|
3649
|
+
const client = await makeClient(cfg);
|
|
3650
|
+
return client.GET("/mcp-aggregator/namespaces/{slug}", {
|
|
3651
|
+
params: { path: { slug } }
|
|
3652
|
+
});
|
|
3653
|
+
});
|
|
3654
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
3655
|
+
});
|
|
3656
|
+
namespace.command("use <slug>").description("Wire an AI client's MCP config to this namespace's URL").option(
|
|
3657
|
+
"--client <list>",
|
|
3658
|
+
`comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all'`,
|
|
3659
|
+
DEFAULT_CLIENT_KEY
|
|
3660
|
+
).option("--dry-run", "print what would be written without writing", false).action(async (slug, opts, cmd) => {
|
|
3661
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3662
|
+
const setup = await withSpinner(
|
|
3663
|
+
"Fetching setup descriptors",
|
|
3664
|
+
() => fetchSetup(cfg, slug)
|
|
3665
|
+
);
|
|
3666
|
+
const targets = clientTargets(process.cwd());
|
|
3667
|
+
const keys = resolveClientKeys(opts.client);
|
|
3668
|
+
const json = cmd.optsWithGlobals().json;
|
|
3669
|
+
const result = [];
|
|
3670
|
+
for (const key of keys) {
|
|
3671
|
+
const target = targets[key];
|
|
3672
|
+
const actions = await applyClient(cfg, setup, target, {
|
|
3673
|
+
dryRun: Boolean(opts.dryRun),
|
|
3674
|
+
mcp: true,
|
|
3675
|
+
agentFiles: false,
|
|
3676
|
+
personalWorkspaceId: null
|
|
3677
|
+
});
|
|
3678
|
+
result.push({ client: key, actions });
|
|
3679
|
+
if (!json) printActions(target, actions);
|
|
3680
|
+
}
|
|
3681
|
+
if (json) {
|
|
3682
|
+
emit({ namespace: slug, dryRun: Boolean(opts.dryRun), clients: result }, true);
|
|
3683
|
+
return;
|
|
3684
|
+
}
|
|
3685
|
+
process.stdout.write(
|
|
3686
|
+
opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : `
|
|
3687
|
+
Wired to namespace '${slug}'. Restart your AI client (or reload MCP) to pick it up.
|
|
3688
|
+
`
|
|
3689
|
+
);
|
|
3690
|
+
});
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3283
3693
|
// src/commands/onboard.ts
|
|
3284
3694
|
import { existsSync as existsSync8 } from "fs";
|
|
3285
|
-
import { basename as basename3, join as
|
|
3695
|
+
import { basename as basename3, join as join9 } from "path";
|
|
3286
3696
|
|
|
3287
3697
|
// src/commands/fanout.ts
|
|
3288
3698
|
import { spawnSync } from "child_process";
|
|
3289
|
-
import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as
|
|
3290
|
-
import { isAbsolute, join as
|
|
3699
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
3700
|
+
import { isAbsolute, join as join8, resolve } from "path";
|
|
3291
3701
|
var ICON = {
|
|
3292
3702
|
refresh: "\u21BB",
|
|
3293
3703
|
bind: "+",
|
|
@@ -3307,13 +3717,13 @@ function discoverChildren(root) {
|
|
|
3307
3717
|
const out = [];
|
|
3308
3718
|
for (const name of names.sort()) {
|
|
3309
3719
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
3310
|
-
const dir =
|
|
3720
|
+
const dir = join8(root, name);
|
|
3311
3721
|
try {
|
|
3312
|
-
if (!
|
|
3722
|
+
if (!statSync3(dir).isDirectory()) continue;
|
|
3313
3723
|
} catch {
|
|
3314
3724
|
continue;
|
|
3315
3725
|
}
|
|
3316
|
-
if (existsSync7(
|
|
3726
|
+
if (existsSync7(join8(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
3317
3727
|
}
|
|
3318
3728
|
return out;
|
|
3319
3729
|
}
|
|
@@ -3689,7 +4099,7 @@ async function planRecurseChild(entry, root, client, opts) {
|
|
|
3689
4099
|
if (!existsSync8(dir)) {
|
|
3690
4100
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
3691
4101
|
}
|
|
3692
|
-
if (existsSync8(
|
|
4102
|
+
if (existsSync8(join9(dir, ".sechroom.json"))) {
|
|
3693
4103
|
return {
|
|
3694
4104
|
label: entry.path,
|
|
3695
4105
|
dir,
|
|
@@ -3762,7 +4172,7 @@ This fan-out will pin the same lane in every repo:
|
|
|
3762
4172
|
async function runRecurse(cfg, g, opts) {
|
|
3763
4173
|
const { yes, dryRun, json } = opts;
|
|
3764
4174
|
const root = process.cwd();
|
|
3765
|
-
const manifestPath =
|
|
4175
|
+
const manifestPath = join9(root, ".sechroom", "repos.json");
|
|
3766
4176
|
const fromManifest = readManifest(manifestPath);
|
|
3767
4177
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
3768
4178
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
@@ -3977,8 +4387,8 @@ async function printStarterPrompt(mode, cfg) {
|
|
|
3977
4387
|
|
|
3978
4388
|
// src/commands/sweep.ts
|
|
3979
4389
|
import { existsSync as existsSync9 } from "fs";
|
|
3980
|
-
import { dirname as
|
|
3981
|
-
var DEFAULT_MANIFEST =
|
|
4390
|
+
import { dirname as dirname7, join as join10, resolve as resolve2 } from "path";
|
|
4391
|
+
var DEFAULT_MANIFEST = join10(".sechroom", "repos.json");
|
|
3982
4392
|
function planEntry(entry, root) {
|
|
3983
4393
|
const dir = resolveChildDir(entry.path, root);
|
|
3984
4394
|
if (!existsSync9(dir)) {
|
|
@@ -4057,7 +4467,7 @@ Examples:
|
|
|
4057
4467
|
`);
|
|
4058
4468
|
return;
|
|
4059
4469
|
}
|
|
4060
|
-
const root =
|
|
4470
|
+
const root = dirname7(dirname7(manifestPath));
|
|
4061
4471
|
const plans = repos.map((entry) => planEntry(entry, root));
|
|
4062
4472
|
if (!json) {
|
|
4063
4473
|
process.stderr.write(
|
|
@@ -4075,7 +4485,7 @@ Examples:
|
|
|
4075
4485
|
}
|
|
4076
4486
|
|
|
4077
4487
|
// src/commands/skills.ts
|
|
4078
|
-
import { join as
|
|
4488
|
+
import { join as join11 } from "path";
|
|
4079
4489
|
import { existsSync as existsSync10, rmSync as rmSync2 } from "fs";
|
|
4080
4490
|
|
|
4081
4491
|
// src/commands/lane.ts
|
|
@@ -4164,10 +4574,10 @@ To install/refresh skills, run 'sechroom onboard' (it offers to materialise them
|
|
|
4164
4574
|
const dir = skillsDir(!opts.local);
|
|
4165
4575
|
const lock = readSkillsLock(dir);
|
|
4166
4576
|
const entry = lock[slug];
|
|
4167
|
-
if (!entry) fail(`No materialised skills recorded for '${slug}' in ${
|
|
4577
|
+
if (!entry) fail(`No materialised skills recorded for '${slug}' in ${join11(dir, SKILLS_LOCK)}.`);
|
|
4168
4578
|
const removed = [];
|
|
4169
4579
|
for (const name of entry.skills) {
|
|
4170
|
-
const skillPath =
|
|
4580
|
+
const skillPath = join11(dir, name);
|
|
4171
4581
|
if (existsSync10(skillPath)) {
|
|
4172
4582
|
rmSync2(skillPath, { recursive: true, force: true });
|
|
4173
4583
|
removed.push(name);
|
|
@@ -4250,22 +4660,22 @@ To install/refresh skills, run 'sechroom onboard' (it offers to materialise them
|
|
|
4250
4660
|
|
|
4251
4661
|
// src/commands/reset.ts
|
|
4252
4662
|
import { homedir as homedir6 } from "os";
|
|
4253
|
-
import { join as
|
|
4663
|
+
import { join as join12 } from "path";
|
|
4254
4664
|
import { existsSync as existsSync11, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
|
|
4255
4665
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
4256
|
-
var localSkillsDir = () =>
|
|
4257
|
-
var globalSkillsDir = () =>
|
|
4258
|
-
var localAgentsDir = () =>
|
|
4259
|
-
var globalAgentsDir = () =>
|
|
4666
|
+
var localSkillsDir = () => join12(process.cwd(), ".claude", "skills");
|
|
4667
|
+
var globalSkillsDir = () => join12(homedir6(), ".claude", "skills");
|
|
4668
|
+
var localAgentsDir = () => join12(process.cwd(), ".claude", "agents");
|
|
4669
|
+
var globalAgentsDir = () => join12(homedir6(), ".claude", "agents");
|
|
4260
4670
|
function removeMaterialisedSkills(dir) {
|
|
4261
4671
|
const removed = [];
|
|
4262
|
-
const lockPath =
|
|
4672
|
+
const lockPath = join12(dir, SKILLS_LOCK2);
|
|
4263
4673
|
if (!existsSync11(lockPath)) return removed;
|
|
4264
4674
|
try {
|
|
4265
4675
|
const lock = JSON.parse(readFileSync7(lockPath, "utf8"));
|
|
4266
4676
|
for (const entry of Object.values(lock)) {
|
|
4267
4677
|
for (const name of entry.skills ?? []) {
|
|
4268
|
-
const p =
|
|
4678
|
+
const p = join12(dir, name);
|
|
4269
4679
|
if (existsSync11(p)) {
|
|
4270
4680
|
rmSync3(p, { recursive: true, force: true });
|
|
4271
4681
|
removed.push(p);
|
|
@@ -4311,17 +4721,17 @@ function registerReset(program2) {
|
|
|
4311
4721
|
}
|
|
4312
4722
|
}
|
|
4313
4723
|
const removed = [];
|
|
4314
|
-
const stateDir =
|
|
4724
|
+
const stateDir = join12(process.cwd(), ".sechroom");
|
|
4315
4725
|
if (existsSync11(stateDir)) {
|
|
4316
4726
|
rmSync3(stateDir, { recursive: true, force: true });
|
|
4317
4727
|
removed.push(stateDir);
|
|
4318
4728
|
}
|
|
4319
|
-
const legacyCfg =
|
|
4729
|
+
const legacyCfg = join12(process.cwd(), ".sechroom.json");
|
|
4320
4730
|
if (existsSync11(legacyCfg)) {
|
|
4321
4731
|
rmSync3(legacyCfg, { force: true });
|
|
4322
4732
|
removed.push(legacyCfg);
|
|
4323
4733
|
}
|
|
4324
|
-
const legacySem =
|
|
4734
|
+
const legacySem = join12(process.cwd(), ".sem");
|
|
4325
4735
|
if (existsSync11(legacySem)) {
|
|
4326
4736
|
rmSync3(legacySem, { force: true });
|
|
4327
4737
|
removed.push(legacySem);
|
|
@@ -4463,12 +4873,14 @@ registerWorkspace(program);
|
|
|
4463
4873
|
registerProject(program);
|
|
4464
4874
|
registerFiling(program);
|
|
4465
4875
|
registerContinuity(program);
|
|
4876
|
+
registerCheckpoint(program);
|
|
4466
4877
|
registerHook(program);
|
|
4467
4878
|
registerId(program);
|
|
4468
4879
|
registerAccount(program);
|
|
4469
4880
|
registerChat(program);
|
|
4470
4881
|
registerInit(program);
|
|
4471
4882
|
registerSetup(program);
|
|
4883
|
+
registerNamespace(program);
|
|
4472
4884
|
registerOnboard(program);
|
|
4473
4885
|
registerSweep(program);
|
|
4474
4886
|
registerSkills(program);
|
package/package.json
CHANGED