@sechroom/cli 2026.6.32-rc.925b06e7 → 2026.6.32
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 +846 -287
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1773,14 +1773,19 @@ 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
|
|
|
1781
1786
|
// src/sem.ts
|
|
1782
1787
|
import { basename as basename2, dirname as dirname2, join as join2 } from "path";
|
|
1783
|
-
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1788
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1784
1789
|
var SEM_FILE = join2(".sechroom", "lane.json");
|
|
1785
1790
|
var LEGACY_SEM_FILE = ".sem";
|
|
1786
1791
|
var STATE_DIR_NAME2 = ".sechroom";
|
|
@@ -1799,6 +1804,43 @@ function resolveSemPathForRead(start = process.cwd()) {
|
|
|
1799
1804
|
dir = parent;
|
|
1800
1805
|
}
|
|
1801
1806
|
}
|
|
1807
|
+
function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
|
|
1808
|
+
try {
|
|
1809
|
+
let dir = start;
|
|
1810
|
+
let gitPath;
|
|
1811
|
+
for (; ; ) {
|
|
1812
|
+
const candidate = join2(dir, ".git");
|
|
1813
|
+
if (existsSync2(candidate)) {
|
|
1814
|
+
gitPath = candidate;
|
|
1815
|
+
break;
|
|
1816
|
+
}
|
|
1817
|
+
const parent = dirname2(dir);
|
|
1818
|
+
if (parent === dir) break;
|
|
1819
|
+
dir = parent;
|
|
1820
|
+
}
|
|
1821
|
+
if (!gitPath || statSync(gitPath).isDirectory()) return lane;
|
|
1822
|
+
const gitFile = readFileSync2(gitPath, "utf8");
|
|
1823
|
+
const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
|
|
1824
|
+
if (!common) return lane;
|
|
1825
|
+
const worktreesDir = join2(common[1], "worktrees");
|
|
1826
|
+
const siblings = readdirSync(worktreesDir).filter((n) => {
|
|
1827
|
+
try {
|
|
1828
|
+
return statSync(join2(worktreesDir, n)).isDirectory();
|
|
1829
|
+
} catch {
|
|
1830
|
+
return false;
|
|
1831
|
+
}
|
|
1832
|
+
});
|
|
1833
|
+
return laneWithWorktreeSuffix(lane, gitFile, siblings);
|
|
1834
|
+
} catch {
|
|
1835
|
+
return lane;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
function laneWithWorktreeSuffix(lane, gitFile, siblings) {
|
|
1839
|
+
const m = gitFile.trim().match(/\/worktrees\/([^/\s]+)\s*$/);
|
|
1840
|
+
if (!m) return lane;
|
|
1841
|
+
const idx = [...siblings].sort().indexOf(m[1]);
|
|
1842
|
+
return idx < 0 ? lane : `${lane}-${idx + 2}`;
|
|
1843
|
+
}
|
|
1802
1844
|
function parseSem(text) {
|
|
1803
1845
|
const out = {};
|
|
1804
1846
|
for (const raw of text.split("\n")) {
|
|
@@ -1910,15 +1952,25 @@ var SectionType = {
|
|
|
1910
1952
|
* carried a workspaceId and that workspace has agent-setup-bundle memories. */
|
|
1911
1953
|
WorkspaceConventions: "workspace-conventions"
|
|
1912
1954
|
};
|
|
1913
|
-
async function fetchSetup(cfg) {
|
|
1955
|
+
async function fetchSetup(cfg, namespaceSlug) {
|
|
1914
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;
|
|
1915
1961
|
const { data, error } = await client.GET(
|
|
1916
1962
|
"/operator-surface/setup",
|
|
1917
|
-
|
|
1963
|
+
hasQuery ? { params: { query } } : {}
|
|
1918
1964
|
);
|
|
1919
1965
|
if (error) throw new Error(`GET /operator-surface/setup failed: ${JSON.stringify(error)}`);
|
|
1920
1966
|
return data;
|
|
1921
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
|
+
}
|
|
1922
1974
|
function findSurface(setup, surfaceKey) {
|
|
1923
1975
|
return setup.surfaces.find((s) => s.surfaceKey === surfaceKey);
|
|
1924
1976
|
}
|
|
@@ -1989,12 +2041,14 @@ async function resolveWorkspaceConventions(cfg, section) {
|
|
|
1989
2041
|
if (parseTagArtifactId(artifact.id)) continue;
|
|
1990
2042
|
const mem = await fetchMemoryFields(cfg, artifact.id);
|
|
1991
2043
|
if (typeof mem?.text === "string" && mem.text.trim().length > 0) {
|
|
1992
|
-
|
|
1993
|
-
|
|
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);
|
|
1994
2048
|
}
|
|
1995
2049
|
}
|
|
1996
2050
|
if (parts.length === 0) return null;
|
|
1997
|
-
return { body: parts.join("\n\n
|
|
2051
|
+
return { body: parts.join("\n\n"), refs };
|
|
1998
2052
|
}
|
|
1999
2053
|
async function createOverride(cfg, template, personalWorkspaceId) {
|
|
2000
2054
|
const client = await makeClient(cfg);
|
|
@@ -2090,8 +2144,9 @@ function resolveLane(flagLane, cwd) {
|
|
|
2090
2144
|
const env = process.env.SECHROOM_LANE;
|
|
2091
2145
|
if (env) return env;
|
|
2092
2146
|
const start = cwd ?? process.cwd();
|
|
2093
|
-
const
|
|
2094
|
-
return
|
|
2147
|
+
const base = readSem(resolveSemPathForRead(start))?.values["code-lane"];
|
|
2148
|
+
if (!base) return void 0;
|
|
2149
|
+
return applyWorktreeLaneSuffix(base, start);
|
|
2095
2150
|
}
|
|
2096
2151
|
var INTENT_FILE = join4(".sechroom", "continuity.json");
|
|
2097
2152
|
function resolveIntentPath(start) {
|
|
@@ -2118,6 +2173,102 @@ function hasRequiredIntent(i) {
|
|
|
2118
2173
|
i.objective?.trim() && i.state?.trim() && i.lastAction?.trim() && i.nextAction?.trim() && i.resumeInstruction?.trim()
|
|
2119
2174
|
);
|
|
2120
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
|
+
}
|
|
2121
2272
|
function formatContext(bundle, lane) {
|
|
2122
2273
|
const s = bundle?.latestSnapshot;
|
|
2123
2274
|
if (!s) return null;
|
|
@@ -2154,20 +2305,23 @@ function emitSessionStart(additionalContext) {
|
|
|
2154
2305
|
}) + "\n"
|
|
2155
2306
|
);
|
|
2156
2307
|
}
|
|
2157
|
-
var
|
|
2308
|
+
var CLAUDE_HOOK_COMMANDS = {
|
|
2309
|
+
SessionStart: "sechroom hook session-start",
|
|
2310
|
+
PreCompact: "sechroom hook pre-compact",
|
|
2311
|
+
SessionEnd: "sechroom hook session-end"
|
|
2312
|
+
};
|
|
2313
|
+
var CODEX_HOOK_COMMANDS = {
|
|
2158
2314
|
SessionStart: "sechroom hook session-start",
|
|
2159
|
-
|
|
2315
|
+
Stop: "sechroom hook session-end --debounce-minutes 10"
|
|
2160
2316
|
};
|
|
2161
|
-
var HOOK_EVENTS = ["SessionStart", "PreCompact"];
|
|
2162
2317
|
function hasHookCommand(config2, event, command) {
|
|
2163
2318
|
const groups = config2.hooks?.[event] ?? [];
|
|
2164
2319
|
return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
|
|
2165
2320
|
}
|
|
2166
|
-
function mergeHooks(config2) {
|
|
2321
|
+
function mergeHooks(config2, commands) {
|
|
2167
2322
|
config2.hooks ??= {};
|
|
2168
2323
|
let added = 0;
|
|
2169
|
-
for (const event of
|
|
2170
|
-
const command = HOOK_COMMANDS[event];
|
|
2324
|
+
for (const [event, command] of Object.entries(commands)) {
|
|
2171
2325
|
if (hasHookCommand(config2, event, command)) continue;
|
|
2172
2326
|
const groups = config2.hooks[event] ??= [];
|
|
2173
2327
|
groups.push({ hooks: [{ type: "command", command }] });
|
|
@@ -2181,10 +2335,10 @@ function readJsonConfig2(path) {
|
|
|
2181
2335
|
if (!raw.trim()) return {};
|
|
2182
2336
|
return JSON.parse(raw);
|
|
2183
2337
|
}
|
|
2184
|
-
function installHooksJson(path, dryRun) {
|
|
2338
|
+
function installHooksJson(path, commands, dryRun) {
|
|
2185
2339
|
const existed = existsSync4(path) && readFileSync3(path, "utf8").trim().length > 0;
|
|
2186
2340
|
const config2 = readJsonConfig2(path);
|
|
2187
|
-
const added = mergeHooks(config2);
|
|
2341
|
+
const added = mergeHooks(config2, commands);
|
|
2188
2342
|
if (added === 0 && existed) return { path, status: "current" };
|
|
2189
2343
|
if (!dryRun) {
|
|
2190
2344
|
mkdirSync3(dirname4(path), { recursive: true });
|
|
@@ -2245,9 +2399,9 @@ function installHookSurfaces(surfaces, opts) {
|
|
|
2245
2399
|
for (const surface of surfaces) {
|
|
2246
2400
|
if (surface === "claude") {
|
|
2247
2401
|
const path = opts.local ? join4(opts.cwd, ".claude", "settings.json") : join4(opts.home, ".claude", "settings.json");
|
|
2248
|
-
out.push({ surface, results: [installHooksJson(path, opts.dryRun)] });
|
|
2402
|
+
out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
|
|
2249
2403
|
} else {
|
|
2250
|
-
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);
|
|
2251
2405
|
const featureFlag = installCodexFeatureFlag(join4(opts.home, ".codex", "config.toml"), opts.dryRun);
|
|
2252
2406
|
out.push({ surface, results: [hooksJson, featureFlag] });
|
|
2253
2407
|
}
|
|
@@ -2327,31 +2481,23 @@ Fail-soft: no lane / no auth / no-or-partial intent file / API error -> exit 0,
|
|
|
2327
2481
|
const raw = await readStdin();
|
|
2328
2482
|
const input = parseHookInput(raw);
|
|
2329
2483
|
const cwd = input.cwd ?? process.cwd();
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
surfaceMarkers: intent.surfaceMarkers ?? null,
|
|
2348
|
-
relevantArtifactIds: intent.artifacts ?? null,
|
|
2349
|
-
confidence: intent.confidence ?? null,
|
|
2350
|
-
// Compaction is infrequent, so the FR-051 clobber guard doesn't bite;
|
|
2351
|
-
// Acknowledge lets a within-window checkpoint land on the lane.
|
|
2352
|
-
concurrentSessionPolicy: "Acknowledge"
|
|
2353
|
-
}
|
|
2354
|
-
});
|
|
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 });
|
|
2355
2501
|
return process.exit(0);
|
|
2356
2502
|
} catch {
|
|
2357
2503
|
return process.exit(0);
|
|
@@ -2396,6 +2542,101 @@ Fail-soft: no lane / no auth / no-or-partial intent file / API error -> exit 0,
|
|
|
2396
2542
|
});
|
|
2397
2543
|
}
|
|
2398
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
|
+
|
|
2399
2640
|
// src/commands/account.ts
|
|
2400
2641
|
function registerId(program2) {
|
|
2401
2642
|
const id = program2.command("id").description("Allocate human-authored id sequences (FR-*, D-*)");
|
|
@@ -2612,16 +2853,16 @@ Examples:
|
|
|
2612
2853
|
}
|
|
2613
2854
|
|
|
2614
2855
|
// src/setup/apply.ts
|
|
2615
|
-
import { createHash as
|
|
2616
|
-
import { mkdirSync as
|
|
2617
|
-
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";
|
|
2618
2859
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
2619
2860
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
2620
2861
|
function normalizeBody(s) {
|
|
2621
2862
|
return s.replace(/\r\n/g, "\n").trim();
|
|
2622
2863
|
}
|
|
2623
2864
|
function bodySha256(body) {
|
|
2624
|
-
return
|
|
2865
|
+
return createHash3("sha256").update(normalizeBody(body), "utf8").digest("hex");
|
|
2625
2866
|
}
|
|
2626
2867
|
function renderBlock(write) {
|
|
2627
2868
|
const body = normalizeBody(write.body);
|
|
@@ -2667,7 +2908,7 @@ function parseManagedBlock(content, block) {
|
|
|
2667
2908
|
return null;
|
|
2668
2909
|
}
|
|
2669
2910
|
function ensureDir2(path) {
|
|
2670
|
-
|
|
2911
|
+
mkdirSync5(dirname6(path), { recursive: true });
|
|
2671
2912
|
}
|
|
2672
2913
|
function readOr(path, fallback) {
|
|
2673
2914
|
try {
|
|
@@ -2690,7 +2931,7 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
2690
2931
|
current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
|
|
2691
2932
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
2692
2933
|
ensureDir2(path);
|
|
2693
|
-
|
|
2934
|
+
writeFileSync5(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
2694
2935
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
2695
2936
|
}
|
|
2696
2937
|
function mergeCodexToml(path, snippet, dryRun) {
|
|
@@ -2701,7 +2942,7 @@ function mergeCodexToml(path, snippet, dryRun) {
|
|
|
2701
2942
|
const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
|
|
2702
2943
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
2703
2944
|
ensureDir2(path);
|
|
2704
|
-
|
|
2945
|
+
writeFileSync5(path, next, { mode: 384 });
|
|
2705
2946
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
2706
2947
|
}
|
|
2707
2948
|
function writeInstructionBlock(path, write, dryRun) {
|
|
@@ -2709,7 +2950,7 @@ function writeInstructionBlock(path, write, dryRun) {
|
|
|
2709
2950
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
2710
2951
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
2711
2952
|
ensureDir2(path);
|
|
2712
|
-
|
|
2953
|
+
writeFileSync5(path, next);
|
|
2713
2954
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
2714
2955
|
}
|
|
2715
2956
|
function computeBlockFile(current, write) {
|
|
@@ -2750,7 +2991,7 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
2750
2991
|
const next = computeBlockFile(current, write);
|
|
2751
2992
|
if (!dryRun) {
|
|
2752
2993
|
ensureDir2(proposedPath);
|
|
2753
|
-
|
|
2994
|
+
writeFileSync5(proposedPath, next);
|
|
2754
2995
|
}
|
|
2755
2996
|
return {
|
|
2756
2997
|
kind: "instruction",
|
|
@@ -2859,9 +3100,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
2859
3100
|
}
|
|
2860
3101
|
|
|
2861
3102
|
// src/setup/skills-offer.ts
|
|
2862
|
-
import { mkdirSync as
|
|
2863
|
-
import {
|
|
2864
|
-
import { join as join5 } from "path";
|
|
3103
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
3104
|
+
import { join as join7 } from "path";
|
|
2865
3105
|
|
|
2866
3106
|
// src/setup/lane-pin.ts
|
|
2867
3107
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -2943,57 +3183,157 @@ I can pin this checkout's lane so operator skills + the continuity hook resolve
|
|
|
2943
3183
|
writePin(code || void 0, design || void 0);
|
|
2944
3184
|
}
|
|
2945
3185
|
|
|
2946
|
-
// src/setup/
|
|
2947
|
-
var
|
|
3186
|
+
// src/setup/skill-resolution.ts
|
|
3187
|
+
var SYSTEM_WORKSPACE_ID = "wsp_system";
|
|
3188
|
+
var SKILL_ROLE_TAG = "sechroom:role:skill-template";
|
|
3189
|
+
var SKILL_NAME_PREFIX = "skill:";
|
|
3190
|
+
var AGENT_ROLE_TAG = "sechroom:role:agent-template";
|
|
3191
|
+
var AGENT_NAME_PREFIX = "agent:";
|
|
3192
|
+
function tagsOf(row) {
|
|
3193
|
+
const m = row?.item ?? row;
|
|
3194
|
+
return m?.tags ?? m?.Tags ?? [];
|
|
3195
|
+
}
|
|
2948
3196
|
function tagValue(tags, prefix) {
|
|
2949
3197
|
return tags.find((t) => t.startsWith(prefix))?.slice(prefix.length);
|
|
2950
3198
|
}
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
3199
|
+
function bodyOf(row) {
|
|
3200
|
+
const m = row?.item ?? row;
|
|
3201
|
+
return m?.text ?? m?.Text ?? "";
|
|
3202
|
+
}
|
|
3203
|
+
function entriesFromRows(rows, surface, source, roleTag, namePrefix) {
|
|
3204
|
+
const out = /* @__PURE__ */ new Map();
|
|
3205
|
+
for (const row of rows ?? []) {
|
|
3206
|
+
const tags = tagsOf(row);
|
|
3207
|
+
if (!tags.includes(roleTag)) continue;
|
|
3208
|
+
if (tagValue(tags, "target:") !== surface) continue;
|
|
3209
|
+
const name = tagValue(tags, namePrefix);
|
|
3210
|
+
if (!name) continue;
|
|
3211
|
+
out.set(name, { name, body: bodyOf(row), source });
|
|
3212
|
+
}
|
|
3213
|
+
return out;
|
|
3214
|
+
}
|
|
3215
|
+
function resolveByRole(systemRows, personalRows, surface, roleTag, namePrefix) {
|
|
3216
|
+
const merged = entriesFromRows(systemRows, surface, "system", roleTag, namePrefix);
|
|
3217
|
+
for (const [name, item] of entriesFromRows(personalRows, surface, "personal", roleTag, namePrefix)) {
|
|
3218
|
+
merged.set(name, item);
|
|
3219
|
+
}
|
|
3220
|
+
return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
3221
|
+
}
|
|
3222
|
+
function resolveSkills(systemRows, personalRows, surface) {
|
|
3223
|
+
return resolveByRole(systemRows, personalRows, surface, SKILL_ROLE_TAG, SKILL_NAME_PREFIX);
|
|
3224
|
+
}
|
|
3225
|
+
function resolveAgents(systemRows, personalRows, surface) {
|
|
3226
|
+
return resolveByRole(systemRows, personalRows, surface, AGENT_ROLE_TAG, AGENT_NAME_PREFIX);
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
// src/setup/skills-lock.ts
|
|
3230
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
3231
|
+
import { homedir as homedir5 } from "os";
|
|
3232
|
+
import { join as join6 } from "path";
|
|
3233
|
+
var SKILLS_LOCK = ".sechroom-skills.json";
|
|
3234
|
+
var DEFAULT_SKILLS_SLUG = "operator-skills";
|
|
3235
|
+
function skillsDir(global) {
|
|
3236
|
+
return global ? join6(homedir5(), ".claude", "skills") : join6(process.cwd(), ".claude", "skills");
|
|
3237
|
+
}
|
|
3238
|
+
function agentsDir(global) {
|
|
3239
|
+
return global ? join6(homedir5(), ".claude", "agents") : join6(process.cwd(), ".claude", "agents");
|
|
3240
|
+
}
|
|
3241
|
+
function readSkillsLock(dir) {
|
|
3242
|
+
const lockPath = join6(dir, SKILLS_LOCK);
|
|
3243
|
+
if (!existsSync6(lockPath)) return {};
|
|
3244
|
+
try {
|
|
3245
|
+
return JSON.parse(readFileSync5(lockPath, "utf8"));
|
|
3246
|
+
} catch {
|
|
3247
|
+
return {};
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
function writeSkillsLock(dir, lock) {
|
|
3251
|
+
mkdirSync6(dir, { recursive: true });
|
|
3252
|
+
writeFileSync6(join6(dir, SKILLS_LOCK), JSON.stringify(lock, null, 2) + "\n");
|
|
3253
|
+
}
|
|
3254
|
+
function recordMaterialisedSkills(dir, slug, skills, meta = {}) {
|
|
3255
|
+
const lock = readSkillsLock(dir);
|
|
3256
|
+
lock[slug] = { surface: meta.surface, skills: [...skills].sort() };
|
|
3257
|
+
writeSkillsLock(dir, lock);
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
// src/setup/skills-offer.ts
|
|
3261
|
+
async function fetchFeedRows(cfg, workspaceId) {
|
|
2955
3262
|
try {
|
|
2956
3263
|
const client = await makeClient(cfg);
|
|
2957
3264
|
const feed = await client.GET("/workspaces/{workspaceId}/memories/feed", {
|
|
2958
3265
|
params: {
|
|
2959
|
-
path: { workspaceId
|
|
3266
|
+
path: { workspaceId },
|
|
3267
|
+
// cascadeWorkspaces: skills land in an "Operator Skills" SUB-workspace;
|
|
3268
|
+
// includeText: the feed omits bodies by default, we need them for SKILL.md.
|
|
2960
3269
|
query: { limit: 200, cascadeWorkspaces: true, includeText: true }
|
|
2961
3270
|
}
|
|
2962
3271
|
}).then((r) => r.data).catch(() => void 0);
|
|
2963
|
-
|
|
3272
|
+
return feed?.results ?? feed?.Results ?? [];
|
|
2964
3273
|
} catch {
|
|
2965
|
-
return;
|
|
3274
|
+
return [];
|
|
2966
3275
|
}
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
3276
|
+
}
|
|
3277
|
+
async function maybeOfferSkills(cfg, personalWorkspaceId, opts) {
|
|
3278
|
+
const surface = opts.surface ?? "claude-code";
|
|
3279
|
+
const [systemRows, personalRows] = await Promise.all([
|
|
3280
|
+
fetchFeedRows(cfg, SYSTEM_WORKSPACE_ID),
|
|
3281
|
+
personalWorkspaceId ? fetchFeedRows(cfg, personalWorkspaceId) : Promise.resolve([])
|
|
3282
|
+
]);
|
|
3283
|
+
const skills = resolveSkills(systemRows, personalRows, surface);
|
|
3284
|
+
const agents = resolveAgents(systemRows, personalRows, surface);
|
|
3285
|
+
if (skills.length === 0 && agents.length === 0) return;
|
|
3286
|
+
const sDir = skillsDir(true);
|
|
3287
|
+
const aDir = agentsDir(true);
|
|
3288
|
+
if (opts.dryRun) {
|
|
3289
|
+
const lines = (label, items) => items.length === 0 ? "" : `
|
|
3290
|
+
Would materialise ${style.bold(String(items.length))} ${label} for ${surface}:
|
|
3291
|
+
` + items.map((s) => ` ${s.name} ${style.dim(`[${s.source}]`)}`).join("\n") + "\n";
|
|
3292
|
+
process.stderr.write(lines("operator skill(s)", skills) + lines("agent(s)", agents));
|
|
3293
|
+
return;
|
|
2976
3294
|
}
|
|
2977
|
-
const
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
3295
|
+
const summary = [
|
|
3296
|
+
skills.length > 0 ? `${style.bold(String(skills.length))} skill(s)` : "",
|
|
3297
|
+
agents.length > 0 ? `${style.bold(String(agents.length))} agent(s)` : ""
|
|
3298
|
+
].filter(Boolean).join(" + ");
|
|
3299
|
+
process.stderr.write(`
|
|
3300
|
+
Found ${summary} available to you for ${surface}.
|
|
3301
|
+
`);
|
|
3302
|
+
if (skills.length > 0) process.stderr.write(` skills: ${skills.map((s) => s.name).join(", ")}
|
|
3303
|
+
`);
|
|
3304
|
+
if (agents.length > 0) process.stderr.write(` agents: ${agents.map((a) => a.name).join(", ")}
|
|
3305
|
+
`);
|
|
3306
|
+
const dest = [skills.length > 0 ? `${sDir}/` : "", agents.length > 0 ? `${aDir}/` : ""].filter(Boolean).join(" + ");
|
|
3307
|
+
const materialise = opts.yes ? true : canPrompt() ? await promptYesNo(`Write them to ${dest} so ${surface} can use them?`) : false;
|
|
2986
3308
|
if (!materialise) return;
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
const
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
3309
|
+
if (skills.length > 0) {
|
|
3310
|
+
const written = [];
|
|
3311
|
+
for (const s of skills) {
|
|
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");
|
|
3314
|
+
written.push(s.name);
|
|
3315
|
+
}
|
|
3316
|
+
recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
3317
|
+
process.stderr.write(`${style.green("\u2713")} wrote ${written.length} skill(s) to ${sDir}
|
|
3318
|
+
`);
|
|
2993
3319
|
}
|
|
2994
|
-
|
|
3320
|
+
if (agents.length > 0) {
|
|
3321
|
+
mkdirSync7(aDir, { recursive: true });
|
|
3322
|
+
const written = [];
|
|
3323
|
+
for (const a of agents) {
|
|
3324
|
+
const file = `${a.name}.md`;
|
|
3325
|
+
writeFileSync7(join7(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
3326
|
+
written.push(file);
|
|
3327
|
+
}
|
|
3328
|
+
recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
3329
|
+
process.stderr.write(`${style.green("\u2713")} wrote ${written.length} agent(s) to ${aDir}
|
|
2995
3330
|
`);
|
|
2996
|
-
|
|
3331
|
+
}
|
|
3332
|
+
await ensureLanePin(cfg, {
|
|
3333
|
+
yes: opts.yes,
|
|
3334
|
+
dryRun: opts.dryRun,
|
|
3335
|
+
clients: [surface]
|
|
3336
|
+
});
|
|
2997
3337
|
}
|
|
2998
3338
|
|
|
2999
3339
|
// src/commands/setup.ts
|
|
@@ -3031,14 +3371,14 @@ version, the shared template stays clean, and you can discard back anytime.
|
|
|
3031
3371
|
}
|
|
3032
3372
|
function resolveClientKeys(raw) {
|
|
3033
3373
|
const targets = clientTargets(process.cwd());
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
for (const k of
|
|
3374
|
+
const tokens = (Array.isArray(raw) ? raw : [raw]).flatMap((t) => t.split(",")).map((k) => k.trim()).filter(Boolean);
|
|
3375
|
+
if (tokens.includes("all")) return [...ALL_CLIENT_KEYS];
|
|
3376
|
+
for (const k of tokens) {
|
|
3037
3377
|
if (!targets[k]) {
|
|
3038
3378
|
fail(`unknown client '${k}'. Known: ${ALL_CLIENT_KEYS.join(", ")}, or 'all'.`);
|
|
3039
3379
|
}
|
|
3040
3380
|
}
|
|
3041
|
-
return
|
|
3381
|
+
return [...new Set(tokens)];
|
|
3042
3382
|
}
|
|
3043
3383
|
function printActions(client, actions) {
|
|
3044
3384
|
process.stdout.write(`
|
|
@@ -3050,24 +3390,87 @@ ${client.label} (${client.key}):
|
|
|
3050
3390
|
`);
|
|
3051
3391
|
}
|
|
3052
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
|
+
}
|
|
3053
3450
|
function registerInit(program2) {
|
|
3054
|
-
program2.command("init").description("Wire this project for sechroom: write MCP config + agent instruction files from the server's setup descriptors").option("--client <list
|
|
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...>", `clients to wire \u2014 space- or comma-separated (${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(
|
|
3055
3452
|
"after",
|
|
3056
3453
|
`
|
|
3057
3454
|
Examples:
|
|
3058
3455
|
$ sechroom init Claude Code (default): ./.mcp.json + ./CLAUDE.md
|
|
3059
3456
|
$ sechroom init --client all claude-code, claude-desktop, codex, cursor
|
|
3060
|
-
$ sechroom init --client codex
|
|
3457
|
+
$ sechroom init --client codex cursor space-separated (comma also works)
|
|
3061
3458
|
$ sechroom init --mcp-only just the MCP config (skip agent files)
|
|
3062
3459
|
$ sechroom init --dry-run --json preview the writes, change nothing`
|
|
3063
3460
|
).action(async (opts, cmd) => {
|
|
3064
3461
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3065
|
-
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
|
+
);
|
|
3066
3469
|
const targets = clientTargets(process.cwd());
|
|
3067
3470
|
const keys = resolveClientKeys(opts.client);
|
|
3068
3471
|
const json = cmd.optsWithGlobals().json;
|
|
3069
3472
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
3070
|
-
if (!opts.dryRun && !opts.mcpOnly) {
|
|
3473
|
+
if (!opts.dryRun && !opts.mcpOnly && !check) {
|
|
3071
3474
|
await maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, copyChoice(opts));
|
|
3072
3475
|
}
|
|
3073
3476
|
const result = [];
|
|
@@ -3077,11 +3480,13 @@ Examples:
|
|
|
3077
3480
|
dryRun: Boolean(opts.dryRun),
|
|
3078
3481
|
mcp: !opts.agentFilesOnly,
|
|
3079
3482
|
agentFiles: !opts.mcpOnly,
|
|
3080
|
-
personalWorkspaceId
|
|
3483
|
+
personalWorkspaceId,
|
|
3484
|
+
mode
|
|
3081
3485
|
});
|
|
3082
3486
|
result.push({ client: key, actions });
|
|
3083
|
-
if (!json) printActions(target, actions);
|
|
3487
|
+
if (!json && !check) printActions(target, actions);
|
|
3084
3488
|
}
|
|
3489
|
+
summarizeEval(result, mode, Boolean(json), Boolean(opts.dryRun));
|
|
3085
3490
|
if (!json && !opts.dryRun && !opts.mcpOnly) {
|
|
3086
3491
|
await maybeOfferSkills(cfg, personalWorkspaceId, { yes: false, dryRun: Boolean(opts.dryRun), surface: "claude-code" });
|
|
3087
3492
|
}
|
|
@@ -3107,20 +3512,93 @@ Next \u2014 verify: ${verify.description}
|
|
|
3107
3512
|
}
|
|
3108
3513
|
function registerSetup(program2) {
|
|
3109
3514
|
const setup = program2.command("setup").description("Granular onboarding steps (init runs these together)");
|
|
3110
|
-
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) => {
|
|
3111
|
-
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 });
|
|
3112
3517
|
});
|
|
3113
|
-
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) => {
|
|
3114
|
-
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
|
+
});
|
|
3115
3587
|
});
|
|
3116
3588
|
}
|
|
3117
3589
|
async function runClients(clients, cmd, opts) {
|
|
3118
3590
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3591
|
+
const mode = opts.mode ?? "apply";
|
|
3592
|
+
const check = mode === "check";
|
|
3119
3593
|
const targets = clientTargets(process.cwd());
|
|
3120
3594
|
const keys = resolveClientKeys(clients.join(","));
|
|
3121
|
-
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
|
+
);
|
|
3122
3600
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
3123
|
-
if (opts.agentFiles && !opts.dryRun) {
|
|
3601
|
+
if (opts.agentFiles && !opts.dryRun && !check) {
|
|
3124
3602
|
await maybeOfferCopies(cfg, setupData, targets, keys, personalWorkspaceId, copyChoice(opts));
|
|
3125
3603
|
}
|
|
3126
3604
|
const json = cmd.optsWithGlobals().json;
|
|
@@ -3131,11 +3609,13 @@ async function runClients(clients, cmd, opts) {
|
|
|
3131
3609
|
dryRun: opts.dryRun,
|
|
3132
3610
|
mcp: opts.mcp,
|
|
3133
3611
|
agentFiles: opts.agentFiles,
|
|
3134
|
-
personalWorkspaceId
|
|
3612
|
+
personalWorkspaceId,
|
|
3613
|
+
mode
|
|
3135
3614
|
});
|
|
3136
3615
|
result.push({ client: key, actions });
|
|
3137
|
-
if (!json) printActions(target, actions);
|
|
3616
|
+
if (!json && !check) printActions(target, actions);
|
|
3138
3617
|
}
|
|
3618
|
+
summarizeEval(result, mode, Boolean(json), opts.dryRun);
|
|
3139
3619
|
if (json) {
|
|
3140
3620
|
emit({ dryRun: opts.dryRun, clients: result }, true);
|
|
3141
3621
|
return;
|
|
@@ -3143,14 +3623,81 @@ async function runClients(clients, cmd, opts) {
|
|
|
3143
3623
|
process.stdout.write(opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n");
|
|
3144
3624
|
}
|
|
3145
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
|
+
|
|
3146
3693
|
// src/commands/onboard.ts
|
|
3147
|
-
import { existsSync as
|
|
3148
|
-
import { join as
|
|
3694
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3695
|
+
import { basename as basename3, join as join9 } from "path";
|
|
3149
3696
|
|
|
3150
3697
|
// src/commands/fanout.ts
|
|
3151
3698
|
import { spawnSync } from "child_process";
|
|
3152
|
-
import { existsSync as
|
|
3153
|
-
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";
|
|
3154
3701
|
var ICON = {
|
|
3155
3702
|
refresh: "\u21BB",
|
|
3156
3703
|
bind: "+",
|
|
@@ -3163,28 +3710,28 @@ function resolveChildDir(path, root) {
|
|
|
3163
3710
|
function discoverChildren(root) {
|
|
3164
3711
|
let names;
|
|
3165
3712
|
try {
|
|
3166
|
-
names =
|
|
3713
|
+
names = readdirSync2(root);
|
|
3167
3714
|
} catch {
|
|
3168
3715
|
return [];
|
|
3169
3716
|
}
|
|
3170
3717
|
const out = [];
|
|
3171
3718
|
for (const name of names.sort()) {
|
|
3172
3719
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
3173
|
-
const dir =
|
|
3720
|
+
const dir = join8(root, name);
|
|
3174
3721
|
try {
|
|
3175
|
-
if (!
|
|
3722
|
+
if (!statSync3(dir).isDirectory()) continue;
|
|
3176
3723
|
} catch {
|
|
3177
3724
|
continue;
|
|
3178
3725
|
}
|
|
3179
|
-
if (
|
|
3726
|
+
if (existsSync7(join8(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
3180
3727
|
}
|
|
3181
3728
|
return out;
|
|
3182
3729
|
}
|
|
3183
3730
|
function readManifest(path) {
|
|
3184
|
-
if (!
|
|
3731
|
+
if (!existsSync7(path)) return null;
|
|
3185
3732
|
let parsed;
|
|
3186
3733
|
try {
|
|
3187
|
-
parsed = JSON.parse(
|
|
3734
|
+
parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
3188
3735
|
} catch (err2) {
|
|
3189
3736
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
3190
3737
|
}
|
|
@@ -3322,7 +3869,39 @@ async function warnIfProjectStray(client, projectId, workspaceId, json) {
|
|
|
3322
3869
|
);
|
|
3323
3870
|
}
|
|
3324
3871
|
}
|
|
3325
|
-
async function
|
|
3872
|
+
async function fetchPersonalWorkspaceId(client) {
|
|
3873
|
+
try {
|
|
3874
|
+
const { data } = await client.GET("/me/personal-workspace", {});
|
|
3875
|
+
return data?.workspaceId ?? null;
|
|
3876
|
+
} catch {
|
|
3877
|
+
return null;
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
function nameTokens(s) {
|
|
3881
|
+
return s.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2);
|
|
3882
|
+
}
|
|
3883
|
+
function personalSubtreeIds(personalId, all) {
|
|
3884
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
3885
|
+
for (const w of all) {
|
|
3886
|
+
if (!w.parentId) continue;
|
|
3887
|
+
(childrenOf.get(w.parentId) ?? childrenOf.set(w.parentId, []).get(w.parentId)).push(w);
|
|
3888
|
+
}
|
|
3889
|
+
const ids = /* @__PURE__ */ new Set([personalId]);
|
|
3890
|
+
const queue = [personalId];
|
|
3891
|
+
while (queue.length > 0) {
|
|
3892
|
+
const id = queue.shift();
|
|
3893
|
+
for (const child of childrenOf.get(id) ?? []) {
|
|
3894
|
+
if (!ids.has(child.id)) {
|
|
3895
|
+
ids.add(child.id);
|
|
3896
|
+
queue.push(child.id);
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
return ids;
|
|
3901
|
+
}
|
|
3902
|
+
async function pickWorkspace(client, opts = {}) {
|
|
3903
|
+
const promptLabel = opts.promptLabel ?? "Bind this directory to a workspace:";
|
|
3904
|
+
const dirName = opts.dirName ?? basename3(process.cwd());
|
|
3326
3905
|
const all = await withSpinner("Listing your workspaces", () => fetchWorkspaces(client));
|
|
3327
3906
|
if (all.length === 0) {
|
|
3328
3907
|
process.stderr.write(`no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
|
|
@@ -3330,22 +3909,34 @@ async function pickWorkspace(client, promptLabel = "Bind this directory to a wor
|
|
|
3330
3909
|
return void 0;
|
|
3331
3910
|
}
|
|
3332
3911
|
const byId = new Map(all.map((w) => [w.id, w]));
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3912
|
+
const personalId = await fetchPersonalWorkspaceId(client);
|
|
3913
|
+
const excluded = personalId ? personalSubtreeIds(personalId, all) : /* @__PURE__ */ new Set();
|
|
3914
|
+
let candidates = all.filter((w) => !excluded.has(w.id));
|
|
3915
|
+
if (candidates.length === 0) candidates = all;
|
|
3916
|
+
const dirToks = new Set(nameTokens(dirName));
|
|
3917
|
+
const isMatch = (w) => nameTokens(w.name).some((t) => dirToks.has(t));
|
|
3918
|
+
const suggestions = candidates.filter(isMatch);
|
|
3919
|
+
let pool = candidates;
|
|
3920
|
+
if (candidates.length > 12 && suggestions.length === 0) {
|
|
3921
|
+
const q = (await promptText(`Filter ${candidates.length} workspaces (substring, Enter to list all)?`, "")).trim().toLowerCase();
|
|
3336
3922
|
if (q) {
|
|
3337
|
-
const hits =
|
|
3923
|
+
const hits = candidates.filter((w) => `${w.name} ${workspacePath(w, byId)}`.toLowerCase().includes(q));
|
|
3338
3924
|
if (hits.length > 0) pool = hits;
|
|
3339
3925
|
else process.stderr.write(`no match for "${q}" \u2014 listing all
|
|
3340
3926
|
`);
|
|
3341
3927
|
}
|
|
3342
3928
|
}
|
|
3343
3929
|
const SKIP = "__skip__";
|
|
3930
|
+
const byPath = (a, b) => workspacePath(a, byId).localeCompare(workspacePath(b, byId));
|
|
3931
|
+
const matched = pool.filter(isMatch).sort(byPath);
|
|
3932
|
+
const rest = pool.filter((w) => !isMatch(w)).sort(byPath);
|
|
3344
3933
|
const choices = [
|
|
3345
|
-
...
|
|
3934
|
+
...matched.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: style.dim(`matches "${dirName}"`) })),
|
|
3935
|
+
...rest.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: w.id })),
|
|
3346
3936
|
{ label: style.dim("skip \u2014 don't bind a workspace"), value: SKIP, hint: void 0 }
|
|
3347
3937
|
];
|
|
3348
|
-
const
|
|
3938
|
+
const defaultValue = matched.length === 1 ? matched[0].id : SKIP;
|
|
3939
|
+
const chosen = await promptSelect(promptLabel, choices, defaultValue);
|
|
3349
3940
|
if (chosen === SKIP) return void 0;
|
|
3350
3941
|
const picked = byId.get(chosen);
|
|
3351
3942
|
const collisions = all.filter((w) => w.id !== picked.id && namesCollide(w.name, picked.name));
|
|
@@ -3372,7 +3963,7 @@ async function resolveWorkspaceBinding(client, existing, opts) {
|
|
|
3372
3963
|
}
|
|
3373
3964
|
if (existing) return existing;
|
|
3374
3965
|
if (!canPrompt() || opts.yes) return void 0;
|
|
3375
|
-
return pickWorkspace(client);
|
|
3966
|
+
return pickWorkspace(client, { dirName: basename3(process.cwd()) });
|
|
3376
3967
|
}
|
|
3377
3968
|
async function ensureTenant(baseUrl, g, opts) {
|
|
3378
3969
|
const persisted = readPersisted();
|
|
@@ -3488,7 +4079,7 @@ async function ensureTimezone(cfg, opts) {
|
|
|
3488
4079
|
return { timezone: tz, action: "set" };
|
|
3489
4080
|
}
|
|
3490
4081
|
async function chooseClients(clientFlag, yes, cwd) {
|
|
3491
|
-
if (clientFlag) return resolveClientKeys(clientFlag);
|
|
4082
|
+
if (clientFlag && clientFlag.length > 0) return resolveClientKeys(clientFlag);
|
|
3492
4083
|
const detected = detectInstalledClients(cwd);
|
|
3493
4084
|
const preselected = detected.length > 0 ? detected : [DEFAULT_CLIENT_KEY];
|
|
3494
4085
|
if (!canPrompt() || yes) return preselected;
|
|
@@ -3505,10 +4096,10 @@ async function chooseClients(clientFlag, yes, cwd) {
|
|
|
3505
4096
|
}
|
|
3506
4097
|
async function planRecurseChild(entry, root, client, opts) {
|
|
3507
4098
|
const dir = resolveChildDir(entry.path, root);
|
|
3508
|
-
if (!
|
|
4099
|
+
if (!existsSync8(dir)) {
|
|
3509
4100
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
3510
4101
|
}
|
|
3511
|
-
if (
|
|
4102
|
+
if (existsSync8(join9(dir, ".sechroom.json"))) {
|
|
3512
4103
|
return {
|
|
3513
4104
|
label: entry.path,
|
|
3514
4105
|
dir,
|
|
@@ -3535,7 +4126,10 @@ async function planRecurseChild(entry, root, client, opts) {
|
|
|
3535
4126
|
process.stderr.write(`
|
|
3536
4127
|
${style.bold(entry.path)} ${style.dim("is not bound yet.")}
|
|
3537
4128
|
`);
|
|
3538
|
-
const ws = await pickWorkspace(client,
|
|
4129
|
+
const ws = await pickWorkspace(client, {
|
|
4130
|
+
promptLabel: `Bind ${style.cyan(entry.path)} to a workspace:`,
|
|
4131
|
+
dirName: basename3(entry.path)
|
|
4132
|
+
});
|
|
3539
4133
|
if (!ws) {
|
|
3540
4134
|
return { label: entry.path, dir, disposition: "skip-unbound", argv: [], reason: "unbound \u2014 no workspace chosen (skipped)" };
|
|
3541
4135
|
}
|
|
@@ -3578,7 +4172,7 @@ This fan-out will pin the same lane in every repo:
|
|
|
3578
4172
|
async function runRecurse(cfg, g, opts) {
|
|
3579
4173
|
const { yes, dryRun, json } = opts;
|
|
3580
4174
|
const root = process.cwd();
|
|
3581
|
-
const manifestPath =
|
|
4175
|
+
const manifestPath = join9(root, ".sechroom", "repos.json");
|
|
3582
4176
|
const fromManifest = readManifest(manifestPath);
|
|
3583
4177
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
3584
4178
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
@@ -3606,7 +4200,7 @@ async function runRecurse(cfg, g, opts) {
|
|
|
3606
4200
|
summarizeFanout(results, { dryRun });
|
|
3607
4201
|
}
|
|
3608
4202
|
function registerOnboard(program2) {
|
|
3609
|
-
program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--recurse", "orchestration-root mode: onboard every child repo under this dir (auto-discovered, or from ./.sechroom/repos.json) \u2014 refreshes bound repos, prompts a workspace per new one", false).option("--lane <id>", "set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo").option("--design-lane <id>", "set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child").option("--client <list
|
|
4203
|
+
program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--recurse", "orchestration-root mode: onboard every child repo under this dir (auto-discovered, or from ./.sechroom/repos.json) \u2014 refreshes bound repos, prompts a workspace per new one", false).option("--lane <id>", "set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo").option("--design-lane <id>", "set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child").option("--client <list...>", `clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`).option("--local", "save the binding (tenant + base URL + workspace) to a committed .sechroom.json in this repo instead of the global config", false).option("--here", "with --local: write the binding at THIS directory even when a parent already carries one \u2014 binds a subtree (e.g. a monorepo's frontend/) to its own workspace", false).option("--workspace <id>", "bind this directory to a workspace (skips the interactive workspace pick)").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("--refresh", "re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)", false).option("--force", "rewrite every managed block, overwriting local edits inside the markers (content outside untouched)", false).option("--check", "report whether anything would change and exit (0 = all current, 1 = stale/drift/absent); writes nothing", false).option("-y, --yes", "non-interactive: accept defaults (system timezone, detected clients, global config, full wire)", false).addHelpText(
|
|
3610
4204
|
"after",
|
|
3611
4205
|
`
|
|
3612
4206
|
Examples:
|
|
@@ -3762,14 +4356,21 @@ async function chooseWire(opts, yes) {
|
|
|
3762
4356
|
return opts.mcp === false ? "agent-only" : "full";
|
|
3763
4357
|
}
|
|
3764
4358
|
var FALLBACK_AGENT_PROMPT = "Resume my sechroom continuity, summarise what I was last working on, then suggest the next step.";
|
|
4359
|
+
function printNextStepBlock(heading, lines) {
|
|
4360
|
+
const rule = style.dim("\u2500".repeat(52));
|
|
4361
|
+
process.stdout.write(
|
|
4362
|
+
`
|
|
4363
|
+
${rule}
|
|
4364
|
+
${style.bold(heading)}
|
|
4365
|
+
|
|
4366
|
+
` + lines.map((l) => ` ${l}`).join("\n") + `
|
|
4367
|
+
${rule}
|
|
4368
|
+
`
|
|
4369
|
+
);
|
|
4370
|
+
}
|
|
3765
4371
|
async function printStarterPrompt(mode, cfg) {
|
|
3766
4372
|
if (mode === "cli") {
|
|
3767
|
-
|
|
3768
|
-
`
|
|
3769
|
-
${style.bold("Next:")} pick up where you left off \u2014
|
|
3770
|
-
${style.cyan("sechroom continuity resume-me")}
|
|
3771
|
-
`
|
|
3772
|
-
);
|
|
4373
|
+
printNextStepBlock("Next \u2014 pick up where you left off:", [style.cyan("sechroom continuity resume-me")]);
|
|
3773
4374
|
return;
|
|
3774
4375
|
}
|
|
3775
4376
|
let primary = FALLBACK_AGENT_PROMPT;
|
|
@@ -3781,21 +4382,16 @@ ${style.bold("Next:")} pick up where you left off \u2014
|
|
|
3781
4382
|
} catch {
|
|
3782
4383
|
}
|
|
3783
4384
|
}
|
|
3784
|
-
|
|
3785
|
-
`
|
|
3786
|
-
${style.bold("Next:")} paste this into your AI agent to get going \u2014
|
|
3787
|
-
${style.cyan(`"${primary}"`)}
|
|
3788
|
-
`
|
|
3789
|
-
);
|
|
4385
|
+
printNextStepBlock("Next \u2014 paste this into your AI agent to get going:", [style.cyan(`"${primary}"`)]);
|
|
3790
4386
|
}
|
|
3791
4387
|
|
|
3792
4388
|
// src/commands/sweep.ts
|
|
3793
|
-
import { existsSync as
|
|
3794
|
-
import { dirname as
|
|
3795
|
-
var DEFAULT_MANIFEST =
|
|
4389
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4390
|
+
import { dirname as dirname7, join as join10, resolve as resolve2 } from "path";
|
|
4391
|
+
var DEFAULT_MANIFEST = join10(".sechroom", "repos.json");
|
|
3796
4392
|
function planEntry(entry, root) {
|
|
3797
4393
|
const dir = resolveChildDir(entry.path, root);
|
|
3798
|
-
if (!
|
|
4394
|
+
if (!existsSync9(dir)) {
|
|
3799
4395
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
3800
4396
|
}
|
|
3801
4397
|
if (committedBindingPath(dir)) {
|
|
@@ -3871,7 +4467,7 @@ Examples:
|
|
|
3871
4467
|
`);
|
|
3872
4468
|
return;
|
|
3873
4469
|
}
|
|
3874
|
-
const root =
|
|
4470
|
+
const root = dirname7(dirname7(manifestPath));
|
|
3875
4471
|
const plans = repos.map((entry) => planEntry(entry, root));
|
|
3876
4472
|
if (!json) {
|
|
3877
4473
|
process.stderr.write(
|
|
@@ -3889,106 +4485,78 @@ Examples:
|
|
|
3889
4485
|
}
|
|
3890
4486
|
|
|
3891
4487
|
// src/commands/skills.ts
|
|
3892
|
-
import {
|
|
3893
|
-
import {
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
var
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
}
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
4488
|
+
import { join as join11 } from "path";
|
|
4489
|
+
import { existsSync as existsSync10, rmSync as rmSync2 } from "fs";
|
|
4490
|
+
|
|
4491
|
+
// src/commands/lane.ts
|
|
4492
|
+
var LANE_KEYS = ["code-lane", "design-lane"];
|
|
4493
|
+
function showLane(json) {
|
|
4494
|
+
const found = readSem();
|
|
4495
|
+
if (!found) {
|
|
4496
|
+
if (json) return emit({ path: null, values: {} }, true);
|
|
4497
|
+
return console.log(
|
|
4498
|
+
style.dim(`No ./.sechroom/lane.json pin in this checkout. Run 'sechroom lane set'.`)
|
|
4499
|
+
);
|
|
4500
|
+
}
|
|
4501
|
+
const resolved = { ...found.values };
|
|
4502
|
+
let suffixed = false;
|
|
4503
|
+
for (const k of LANE_KEYS) {
|
|
4504
|
+
const v = found.values[k];
|
|
4505
|
+
if (!v) continue;
|
|
4506
|
+
resolved[k] = applyWorktreeLaneSuffix(v);
|
|
4507
|
+
if (resolved[k] !== v) suffixed = true;
|
|
4508
|
+
}
|
|
4509
|
+
if (json) return emit({ path: found.path, values: resolved, worktreeSuffixApplied: suffixed }, true);
|
|
4510
|
+
console.log(style.dim(`from ${found.path}`));
|
|
4511
|
+
Object.entries(resolved).forEach(([k, v]) => console.log(" " + style.bold(k) + " = " + v));
|
|
4512
|
+
if (suffixed) console.log(style.dim(" (worktree -N suffix applied \u2014 non-primary git worktree)"));
|
|
4513
|
+
}
|
|
4514
|
+
function setLane(opts) {
|
|
4515
|
+
if (!opts.codeLane && !opts.designLane) fail("Provide --code-lane and/or --design-lane.");
|
|
4516
|
+
const target = localSemPath();
|
|
4517
|
+
const values = readLocalSemValues();
|
|
4518
|
+
if (opts.codeLane) values["code-lane"] = opts.codeLane;
|
|
4519
|
+
if (opts.designLane) values["design-lane"] = opts.designLane;
|
|
4520
|
+
writeSem(values, target);
|
|
4521
|
+
if (opts.json) return emit({ path: target, values }, true);
|
|
4522
|
+
console.log(style.green(`Wrote lane pin \u2192 ${target} ${style.dim("(git-ignored)")}`));
|
|
4523
|
+
Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
|
|
4524
|
+
}
|
|
4525
|
+
function registerLane(program2) {
|
|
4526
|
+
const lane = program2.command("lane").description("Show this checkout's continuity lane pin (worktree-aware -N suffix applied)").option("--json", "machine output").action((opts, cmd) => showLane(Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)));
|
|
4527
|
+
lane.command("set").description("Write this checkout's lane pin to ./.sechroom/lane.json").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action(
|
|
4528
|
+
(opts, cmd) => setLane({
|
|
4529
|
+
codeLane: opts.codeLane,
|
|
4530
|
+
designLane: opts.designLane,
|
|
4531
|
+
json: Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)
|
|
4532
|
+
})
|
|
4533
|
+
);
|
|
4534
|
+
lane.addHelpText(
|
|
4535
|
+
"after",
|
|
4536
|
+
`
|
|
4537
|
+
Examples:
|
|
4538
|
+
$ sechroom lane show the resolved lane(s)
|
|
4539
|
+
$ sechroom lane set --code-lane claude-code-chris --design-lane claude-design-chris
|
|
4540
|
+
|
|
4541
|
+
In a non-primary git worktree the ratified concurrent-session -N suffix is auto-applied (SBC-1094).
|
|
4542
|
+
(Aliases: 'sechroom skills lane' / 'skills set-lane' \u2014 kept for back-compat.)`
|
|
4543
|
+
);
|
|
3906
4544
|
}
|
|
4545
|
+
|
|
4546
|
+
// src/commands/skills.ts
|
|
3907
4547
|
function registerSkills(program2) {
|
|
3908
|
-
const skills = program2.command("skills").description("
|
|
4548
|
+
const skills = program2.command("skills").description("Manage operator skills (materialised by `onboard`)");
|
|
3909
4549
|
skills.addHelpText(
|
|
3910
4550
|
"after",
|
|
3911
4551
|
`
|
|
3912
4552
|
Examples:
|
|
3913
|
-
$ sechroom skills install --code-lane claude-code-chris --design-lane claude-design-chris
|
|
3914
|
-
$ sechroom skills install operator-skills --surface claude-code --local
|
|
3915
4553
|
$ sechroom skills list
|
|
3916
4554
|
$ sechroom skills set-lane --code-lane claude-code-chris --design-lane claude-design-chris
|
|
3917
4555
|
$ sechroom skills lane
|
|
3918
|
-
$ sechroom skills clean
|
|
4556
|
+
$ sechroom skills clean
|
|
4557
|
+
|
|
4558
|
+
To install/refresh skills, run 'sechroom onboard' (it offers to materialise them).`
|
|
3919
4559
|
);
|
|
3920
|
-
skills.command("install [slug]").description(`Install a skills bundle (default ${DEFAULT_SLUG}) into your personal workspace + write SKILL.md files`).option("--version <v>", "bundle version (default: latest published in the catalogue)").option("--instance <name>", "install as a named, separate instance (install the same bundle more than once)").option("--code-lane <id>", "identity.code-lane binding (e.g. claude-code-chris)").option("--design-lane <id>", "identity.design-lane binding (e.g. claude-design-chris)").option("--surface <s>", "skill target surface to materialise", "claude-code").option("--local", "write to ./.claude/skills instead of ~/.claude/skills").option("--json", "machine output").action(async (slugArg, opts, cmd) => {
|
|
3921
|
-
const slug = slugArg || DEFAULT_SLUG;
|
|
3922
|
-
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
3923
|
-
const pw = await runApi("resolving personal workspace", () => client.GET("/me/personal-workspace", {}));
|
|
3924
|
-
const personalWsId = pw?.id || pw?.workspaceId || pw?.personalWorkspaceId || pw?.item?.id;
|
|
3925
|
-
if (!personalWsId) fail("Could not resolve your personal workspace.");
|
|
3926
|
-
let version = opts.version;
|
|
3927
|
-
if (!version) {
|
|
3928
|
-
const cat = await runApi("reading the bundle catalogue", () => client.GET("/me/bundles", {}));
|
|
3929
|
-
const item = (cat?.bundles ?? cat?.Bundles ?? []).find((b) => (b.slug ?? b.Slug) === slug);
|
|
3930
|
-
if (!item) fail(`Bundle '${slug}' is not in your self-serve catalogue (must be UserInstallable + Published).`);
|
|
3931
|
-
version = item.latestVersion ?? item.LatestVersion;
|
|
3932
|
-
if (!version) fail(`Bundle '${slug}' has no installable (Published) version.`);
|
|
3933
|
-
}
|
|
3934
|
-
const installOptions = {};
|
|
3935
|
-
if (opts.codeLane) installOptions["identity.code-lane"] = opts.codeLane;
|
|
3936
|
-
if (opts.designLane) installOptions["identity.design-lane"] = opts.designLane;
|
|
3937
|
-
const res = await runApi(
|
|
3938
|
-
`installing ${slug}@${version}${opts.instance ? ` (${opts.instance})` : ""}`,
|
|
3939
|
-
() => client.POST("/me/bundles/{slug}/versions/{version}/install", {
|
|
3940
|
-
params: { path: { slug, version } },
|
|
3941
|
-
// instance: null/absent = the default instance (reinstall updates in
|
|
3942
|
-
// place); a name installs a separate instance.
|
|
3943
|
-
body: { installOptions, instance: opts.instance ?? null }
|
|
3944
|
-
})
|
|
3945
|
-
);
|
|
3946
|
-
const status = String(res?.status ?? res?.Status ?? "");
|
|
3947
|
-
if (status && status.toLowerCase() !== "completed") {
|
|
3948
|
-
fail(`Install did not complete (status=${status}; ${res?.failureReason ?? res?.FailureReason ?? ""}).`);
|
|
3949
|
-
}
|
|
3950
|
-
const feed = await runApi(
|
|
3951
|
-
"materialising skill files",
|
|
3952
|
-
() => client.GET("/workspaces/{workspaceId}/memories/feed", {
|
|
3953
|
-
// cascadeWorkspaces: skills land in an "Operator Skills" SUB-workspace of
|
|
3954
|
-
// the personal workspace, so we recurse from the personal-ws root.
|
|
3955
|
-
// includeText: the feed omits bodies by default; we need them for SKILL.md.
|
|
3956
|
-
params: {
|
|
3957
|
-
path: { workspaceId: personalWsId },
|
|
3958
|
-
query: { limit: 200, cascadeWorkspaces: true, includeText: true }
|
|
3959
|
-
}
|
|
3960
|
-
})
|
|
3961
|
-
);
|
|
3962
|
-
const rows = feed?.results ?? feed?.Results ?? [];
|
|
3963
|
-
const dir = skillsDir(!opts.local);
|
|
3964
|
-
const wantInstance = opts.instance || "default";
|
|
3965
|
-
const written = [];
|
|
3966
|
-
const bundleTagPrefix = `sechroom:bundle:${slug}@`;
|
|
3967
|
-
for (const r of rows) {
|
|
3968
|
-
const m = r.item ?? r;
|
|
3969
|
-
const tags = m.tags ?? m.Tags ?? [];
|
|
3970
|
-
if (!hasAny(tags, ROLE_TAGS)) continue;
|
|
3971
|
-
if (tagValue2(tags, "target:") !== opts.surface) continue;
|
|
3972
|
-
if (!tags.some((t) => t.startsWith(bundleTagPrefix))) continue;
|
|
3973
|
-
if ((tagValue2(tags, "sechroom:skill-instance:") ?? "default") !== wantInstance) continue;
|
|
3974
|
-
const name = tagValue2(tags, "skill:");
|
|
3975
|
-
if (!name) continue;
|
|
3976
|
-
const body = m.text ?? m.Text ?? "";
|
|
3977
|
-
mkdirSync6(join9(dir, name), { recursive: true });
|
|
3978
|
-
writeFileSync6(join9(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
|
|
3979
|
-
written.push(name);
|
|
3980
|
-
}
|
|
3981
|
-
mkdirSync6(dir, { recursive: true });
|
|
3982
|
-
const lockPath = join9(dir, LOCK);
|
|
3983
|
-
const lock = existsSync9(lockPath) ? JSON.parse(readFileSync6(lockPath, "utf8")) : {};
|
|
3984
|
-
lock[slug] = { surface: opts.surface, version, instance: wantInstance, skills: written.sort() };
|
|
3985
|
-
writeFileSync6(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
3986
|
-
if (opts.json) return emit({ slug, version, instance: wantInstance, surface: opts.surface, dir, installed: written }, true);
|
|
3987
|
-
const instanceNote = opts.instance ? ` (${opts.instance})` : "";
|
|
3988
|
-
console.log(style.green(`Installed ${slug}@${version}${instanceNote} \u2014 ${written.length} skill(s) \u2192 ${dir}`));
|
|
3989
|
-
written.forEach((n) => console.log(" " + style.dim("\u2022") + " " + n));
|
|
3990
|
-
if (written.length === 0) console.log(style.dim(` (no '${opts.surface}' skill bodies found; check --surface)`));
|
|
3991
|
-
});
|
|
3992
4560
|
skills.command("list").description("List your installed bundles (GET /me/bundle-installs)").option("--json", "machine output").action(async (opts, cmd) => {
|
|
3993
4561
|
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
3994
4562
|
const data = await runApi("reading your installs", () => client.GET("/me/bundle-installs", {}));
|
|
@@ -4001,49 +4569,33 @@ Examples:
|
|
|
4001
4569
|
console.log(` ${i.bundleSlug ?? i.BundleSlug}@${i.bundleVersion ?? i.BundleVersion ?? "?"}${tag}`);
|
|
4002
4570
|
});
|
|
4003
4571
|
});
|
|
4004
|
-
skills.command("clean [slug]").description(`Remove
|
|
4005
|
-
const slug = slugArg ||
|
|
4572
|
+
skills.command("clean [slug]").description(`Remove skill files materialised by onboard (default ${DEFAULT_SKILLS_SLUG})`).option("--local", "clean ./.claude/skills instead of ~/.claude/skills").option("--json", "machine output").action(async (slugArg, opts) => {
|
|
4573
|
+
const slug = slugArg || DEFAULT_SKILLS_SLUG;
|
|
4006
4574
|
const dir = skillsDir(!opts.local);
|
|
4007
|
-
const
|
|
4008
|
-
if (!existsSync9(lockPath)) fail(`No skills lockfile at ${lockPath}; nothing to clean.`);
|
|
4009
|
-
const lock = JSON.parse(readFileSync6(lockPath, "utf8"));
|
|
4575
|
+
const lock = readSkillsLock(dir);
|
|
4010
4576
|
const entry = lock[slug];
|
|
4011
|
-
if (!entry) fail(`No
|
|
4577
|
+
if (!entry) fail(`No materialised skills recorded for '${slug}' in ${join11(dir, SKILLS_LOCK)}.`);
|
|
4012
4578
|
const removed = [];
|
|
4013
4579
|
for (const name of entry.skills) {
|
|
4014
|
-
const skillPath =
|
|
4015
|
-
if (
|
|
4580
|
+
const skillPath = join11(dir, name);
|
|
4581
|
+
if (existsSync10(skillPath)) {
|
|
4016
4582
|
rmSync2(skillPath, { recursive: true, force: true });
|
|
4017
4583
|
removed.push(name);
|
|
4018
4584
|
}
|
|
4019
4585
|
}
|
|
4020
4586
|
delete lock[slug];
|
|
4021
|
-
|
|
4587
|
+
writeSkillsLock(dir, lock);
|
|
4022
4588
|
if (opts.json) return emit({ slug, removed, dir }, true);
|
|
4023
4589
|
console.log(style.green(`Removed ${removed.length} skill(s) for ${slug} from ${dir}`));
|
|
4024
4590
|
});
|
|
4025
|
-
skills.command("set-lane").description("
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
console.log(style.green(`Wrote lane pin \u2192 ${target} ${style.dim("(git-ignored)")}`));
|
|
4034
|
-
Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
|
|
4035
|
-
});
|
|
4036
|
-
skills.command("lane").description("Show the lane pin resolved from ./.sechroom/lane.json (nearest in this checkout; legacy ./.sem honoured)").option("--json", "machine output").action((opts, cmd) => {
|
|
4037
|
-
const json = cmd.optsWithGlobals().json;
|
|
4038
|
-
const found = readSem();
|
|
4039
|
-
if (!found) {
|
|
4040
|
-
if (json) return emit({ path: null, values: {} }, true);
|
|
4041
|
-
return console.log(style.dim(`No ./.sechroom/lane.json pin in this checkout. Run 'sechroom skills set-lane'.`));
|
|
4042
|
-
}
|
|
4043
|
-
if (json) return emit(found, true);
|
|
4044
|
-
console.log(style.dim(`from ${found.path}`));
|
|
4045
|
-
Object.entries(found.values).forEach(([k, v]) => console.log(" " + style.bold(k) + " = " + v));
|
|
4046
|
-
});
|
|
4591
|
+
skills.command("set-lane").description("Alias of `sechroom lane set` (kept for back-compat) \u2014 write this checkout's lane pin").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action(
|
|
4592
|
+
(opts, cmd) => setLane({
|
|
4593
|
+
codeLane: opts.codeLane,
|
|
4594
|
+
designLane: opts.designLane,
|
|
4595
|
+
json: Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)
|
|
4596
|
+
})
|
|
4597
|
+
);
|
|
4598
|
+
skills.command("lane").description("Alias of `sechroom lane` (kept for back-compat) \u2014 show this checkout's lane pin").option("--json", "machine output").action((opts, cmd) => showLane(Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)));
|
|
4047
4599
|
skills.command("set-workflow").description("Set your per-operator workflow defaults (server-side; follows you across tenants)").option("--default-code-lane <id>", "personal default code lane (e.g. claude-code-chris)").option("--default-design-lane <id>", "personal default design lane (e.g. claude-design-chris)").option("--handover-recipient <id>", "your daily-handover counterparty (e.g. andy)").option("--json", "machine output").action(async (opts, cmd) => {
|
|
4048
4600
|
if (!opts.defaultCodeLane && !opts.defaultDesignLane && !opts.handoverRecipient)
|
|
4049
4601
|
fail("Provide at least one of --default-code-lane / --default-design-lane / --handover-recipient.");
|
|
@@ -4107,22 +4659,24 @@ Examples:
|
|
|
4107
4659
|
}
|
|
4108
4660
|
|
|
4109
4661
|
// src/commands/reset.ts
|
|
4110
|
-
import { homedir as
|
|
4111
|
-
import { join as
|
|
4112
|
-
import { existsSync as
|
|
4113
|
-
var
|
|
4114
|
-
var localSkillsDir = () =>
|
|
4115
|
-
var globalSkillsDir = () =>
|
|
4662
|
+
import { homedir as homedir6 } from "os";
|
|
4663
|
+
import { join as join12 } from "path";
|
|
4664
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
|
|
4665
|
+
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
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");
|
|
4116
4670
|
function removeMaterialisedSkills(dir) {
|
|
4117
4671
|
const removed = [];
|
|
4118
|
-
const lockPath =
|
|
4119
|
-
if (!
|
|
4672
|
+
const lockPath = join12(dir, SKILLS_LOCK2);
|
|
4673
|
+
if (!existsSync11(lockPath)) return removed;
|
|
4120
4674
|
try {
|
|
4121
4675
|
const lock = JSON.parse(readFileSync7(lockPath, "utf8"));
|
|
4122
4676
|
for (const entry of Object.values(lock)) {
|
|
4123
4677
|
for (const name of entry.skills ?? []) {
|
|
4124
|
-
const p =
|
|
4125
|
-
if (
|
|
4678
|
+
const p = join12(dir, name);
|
|
4679
|
+
if (existsSync11(p)) {
|
|
4126
4680
|
rmSync3(p, { recursive: true, force: true });
|
|
4127
4681
|
removed.push(p);
|
|
4128
4682
|
}
|
|
@@ -4167,28 +4721,30 @@ function registerReset(program2) {
|
|
|
4167
4721
|
}
|
|
4168
4722
|
}
|
|
4169
4723
|
const removed = [];
|
|
4170
|
-
const stateDir =
|
|
4171
|
-
if (
|
|
4724
|
+
const stateDir = join12(process.cwd(), ".sechroom");
|
|
4725
|
+
if (existsSync11(stateDir)) {
|
|
4172
4726
|
rmSync3(stateDir, { recursive: true, force: true });
|
|
4173
4727
|
removed.push(stateDir);
|
|
4174
4728
|
}
|
|
4175
|
-
const legacyCfg =
|
|
4176
|
-
if (
|
|
4729
|
+
const legacyCfg = join12(process.cwd(), ".sechroom.json");
|
|
4730
|
+
if (existsSync11(legacyCfg)) {
|
|
4177
4731
|
rmSync3(legacyCfg, { force: true });
|
|
4178
4732
|
removed.push(legacyCfg);
|
|
4179
4733
|
}
|
|
4180
|
-
const legacySem =
|
|
4181
|
-
if (
|
|
4734
|
+
const legacySem = join12(process.cwd(), ".sem");
|
|
4735
|
+
if (existsSync11(legacySem)) {
|
|
4182
4736
|
rmSync3(legacySem, { force: true });
|
|
4183
4737
|
removed.push(legacySem);
|
|
4184
4738
|
}
|
|
4185
4739
|
removed.push(...removeMaterialisedSkills(localSkillsDir()));
|
|
4740
|
+
removed.push(...removeMaterialisedSkills(localAgentsDir()));
|
|
4186
4741
|
if (global) {
|
|
4187
4742
|
const tok = clearToken();
|
|
4188
4743
|
if (tok) removed.push(tok);
|
|
4189
4744
|
const cfg = clearPersisted();
|
|
4190
4745
|
if (cfg) removed.push(cfg);
|
|
4191
4746
|
removed.push(...removeMaterialisedSkills(globalSkillsDir()));
|
|
4747
|
+
removed.push(...removeMaterialisedSkills(globalAgentsDir()));
|
|
4192
4748
|
}
|
|
4193
4749
|
if (json) return emit({ global, removed }, true);
|
|
4194
4750
|
if (removed.length === 0) {
|
|
@@ -4317,15 +4873,18 @@ registerWorkspace(program);
|
|
|
4317
4873
|
registerProject(program);
|
|
4318
4874
|
registerFiling(program);
|
|
4319
4875
|
registerContinuity(program);
|
|
4876
|
+
registerCheckpoint(program);
|
|
4320
4877
|
registerHook(program);
|
|
4321
4878
|
registerId(program);
|
|
4322
4879
|
registerAccount(program);
|
|
4323
4880
|
registerChat(program);
|
|
4324
4881
|
registerInit(program);
|
|
4325
4882
|
registerSetup(program);
|
|
4883
|
+
registerNamespace(program);
|
|
4326
4884
|
registerOnboard(program);
|
|
4327
4885
|
registerSweep(program);
|
|
4328
4886
|
registerSkills(program);
|
|
4887
|
+
registerLane(program);
|
|
4329
4888
|
registerReset(program);
|
|
4330
4889
|
program.parseAsync().catch((err2) => {
|
|
4331
4890
|
process.stderr.write(`error: ${err2 instanceof Error ? err2.message : String(err2)}
|