@sechroom/cli 2026.6.10 → 2026.6.11
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 +440 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/auth.ts
|
|
@@ -12,7 +12,7 @@ import open from "open";
|
|
|
12
12
|
// src/config.ts
|
|
13
13
|
import { homedir } from "os";
|
|
14
14
|
import { join, dirname } from "path";
|
|
15
|
-
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "fs";
|
|
15
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from "fs";
|
|
16
16
|
var CONFIG_DIR = join(homedir(), ".config", "sechroom");
|
|
17
17
|
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
18
18
|
var TOKEN_FILE = join(CONFIG_DIR, "token.json");
|
|
@@ -53,6 +53,16 @@ function writeToken(tok) {
|
|
|
53
53
|
ensureDir();
|
|
54
54
|
writeFileSync(TOKEN_FILE, JSON.stringify(tok, null, 2), { mode: 384 });
|
|
55
55
|
}
|
|
56
|
+
function clearToken() {
|
|
57
|
+
if (!existsSync(TOKEN_FILE)) return void 0;
|
|
58
|
+
rmSync(TOKEN_FILE);
|
|
59
|
+
return TOKEN_FILE;
|
|
60
|
+
}
|
|
61
|
+
function clearPersisted() {
|
|
62
|
+
if (!existsSync(CONFIG_FILE)) return void 0;
|
|
63
|
+
rmSync(CONFIG_FILE);
|
|
64
|
+
return CONFIG_FILE;
|
|
65
|
+
}
|
|
56
66
|
function findLocalConfigPath(start = process.cwd()) {
|
|
57
67
|
let dir = start;
|
|
58
68
|
for (; ; ) {
|
|
@@ -453,6 +463,7 @@ async function makeClient(cfg) {
|
|
|
453
463
|
onRequest({ request }) {
|
|
454
464
|
request.headers.set("authorization", `Bearer ${token}`);
|
|
455
465
|
request.headers.set("tenant", cfg.tenant);
|
|
466
|
+
request.headers.set("x-sechroom-surface", "cli");
|
|
456
467
|
return request;
|
|
457
468
|
}
|
|
458
469
|
});
|
|
@@ -2000,6 +2011,129 @@ function detectInstalledClients(cwd) {
|
|
|
2000
2011
|
return detected;
|
|
2001
2012
|
}
|
|
2002
2013
|
|
|
2014
|
+
// src/setup/skills-offer.ts
|
|
2015
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
2016
|
+
import { homedir as homedir3 } from "os";
|
|
2017
|
+
import { join as join4 } from "path";
|
|
2018
|
+
|
|
2019
|
+
// src/sem.ts
|
|
2020
|
+
import { dirname as dirname4, join as join3 } from "path";
|
|
2021
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
2022
|
+
var SEM_FILE = ".sem";
|
|
2023
|
+
function localSemPath(cwd = process.cwd()) {
|
|
2024
|
+
return join3(cwd, SEM_FILE);
|
|
2025
|
+
}
|
|
2026
|
+
function resolveSemPathForRead(start = process.cwd()) {
|
|
2027
|
+
let dir = start;
|
|
2028
|
+
while (true) {
|
|
2029
|
+
const candidate = join3(dir, SEM_FILE);
|
|
2030
|
+
if (existsSync4(candidate)) return candidate;
|
|
2031
|
+
const parent = dirname4(dir);
|
|
2032
|
+
if (parent === dir) return void 0;
|
|
2033
|
+
dir = parent;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
function parseSem(text) {
|
|
2037
|
+
const out = {};
|
|
2038
|
+
for (const raw of text.split("\n")) {
|
|
2039
|
+
const line = raw.trim();
|
|
2040
|
+
if (!line || line.startsWith("#")) continue;
|
|
2041
|
+
const eq = line.indexOf("=");
|
|
2042
|
+
if (eq === -1) continue;
|
|
2043
|
+
const key = line.slice(0, eq).trim();
|
|
2044
|
+
const value = line.slice(eq + 1).trim();
|
|
2045
|
+
if (key) out[key] = value;
|
|
2046
|
+
}
|
|
2047
|
+
return out;
|
|
2048
|
+
}
|
|
2049
|
+
function serializeSem(values) {
|
|
2050
|
+
const header = "# sechroom lane pin (per-location fallback) \u2014 resolved at runtime by operator skills.\n";
|
|
2051
|
+
const body = Object.entries(values).map(([k, v]) => `${k} = ${v}`).join("\n");
|
|
2052
|
+
return header + body + "\n";
|
|
2053
|
+
}
|
|
2054
|
+
function readSem(path) {
|
|
2055
|
+
const p = path ?? resolveSemPathForRead();
|
|
2056
|
+
if (!p || !existsSync4(p)) return void 0;
|
|
2057
|
+
return { path: p, values: parseSem(readFileSync3(p, "utf8")) };
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// src/setup/skills-offer.ts
|
|
2061
|
+
var ROLE_TAG = "sechroom:role:skill-template";
|
|
2062
|
+
function tagValue(tags, prefix) {
|
|
2063
|
+
return tags.find((t) => t.startsWith(prefix))?.slice(prefix.length);
|
|
2064
|
+
}
|
|
2065
|
+
async function maybeOfferSkills(cfg, personalWorkspaceId, opts) {
|
|
2066
|
+
if (!personalWorkspaceId || opts.dryRun) return;
|
|
2067
|
+
const surface = opts.surface ?? "claude-code";
|
|
2068
|
+
let rows = [];
|
|
2069
|
+
try {
|
|
2070
|
+
const client = await makeClient(cfg);
|
|
2071
|
+
const feed = await client.GET("/workspaces/{workspaceId}/memories/feed", {
|
|
2072
|
+
params: {
|
|
2073
|
+
path: { workspaceId: personalWorkspaceId },
|
|
2074
|
+
query: { limit: 200, cascadeWorkspaces: true, includeText: true }
|
|
2075
|
+
}
|
|
2076
|
+
}).then((r) => r.data).catch(() => void 0);
|
|
2077
|
+
rows = feed?.results ?? feed?.Results ?? [];
|
|
2078
|
+
} catch {
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
const skills = rows.map((r) => r.item ?? r).filter((m) => {
|
|
2082
|
+
const tags = m.tags ?? m.Tags ?? [];
|
|
2083
|
+
return tags.includes(ROLE_TAG) && tagValue(tags, "target:") === surface;
|
|
2084
|
+
});
|
|
2085
|
+
if (skills.length === 0) return;
|
|
2086
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2087
|
+
for (const m of skills) {
|
|
2088
|
+
const name = tagValue(m.tags ?? m.Tags ?? [], "skill:");
|
|
2089
|
+
if (name) byName.set(name, m);
|
|
2090
|
+
}
|
|
2091
|
+
const names = [...byName.keys()].sort();
|
|
2092
|
+
if (names.length === 0) return;
|
|
2093
|
+
process.stderr.write(
|
|
2094
|
+
`
|
|
2095
|
+
Found ${style.bold(String(names.length))} operator skill(s) installed in your workspace: ${names.join(", ")}.
|
|
2096
|
+
`
|
|
2097
|
+
);
|
|
2098
|
+
const dir = join4(homedir3(), ".claude", "skills");
|
|
2099
|
+
const materialise = opts.yes ? true : canPrompt() ? await promptYesNo(`Write them to ${dir}/ so ${surface} can use them?`) : false;
|
|
2100
|
+
if (!materialise) return;
|
|
2101
|
+
const written = [];
|
|
2102
|
+
for (const [name, m] of byName) {
|
|
2103
|
+
const body = m.text ?? m.Text ?? "";
|
|
2104
|
+
mkdirSync3(join4(dir, name), { recursive: true });
|
|
2105
|
+
writeFileSync3(join4(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
|
|
2106
|
+
written.push(name);
|
|
2107
|
+
}
|
|
2108
|
+
process.stderr.write(`${style.green("\u2713")} wrote ${written.length} skill(s) to ${dir}
|
|
2109
|
+
`);
|
|
2110
|
+
if (readSem()) return;
|
|
2111
|
+
const setLane = opts.yes ? false : canPrompt() ? await promptYesNo("Set your lane now so the skills can resolve their identity slots?") : false;
|
|
2112
|
+
if (!setLane) {
|
|
2113
|
+
process.stderr.write(
|
|
2114
|
+
` ${style.dim("run")} ${style.cyan("sechroom skills set-lane --code-lane \u2026 --design-lane \u2026")} ${style.dim("when ready.")}
|
|
2115
|
+
`
|
|
2116
|
+
);
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
let wf;
|
|
2120
|
+
try {
|
|
2121
|
+
const client = await makeClient(cfg);
|
|
2122
|
+
wf = await client.GET("/me/workflow-preferences", {}).then((r) => r.data).catch(() => void 0);
|
|
2123
|
+
} catch {
|
|
2124
|
+
}
|
|
2125
|
+
const code = await promptText("Code-lane id (e.g. claude-code-you)?", wf?.defaultCodeLane);
|
|
2126
|
+
const design = await promptText("Design-lane id (e.g. claude-design-you)?", wf?.defaultDesignLane);
|
|
2127
|
+
const values = {};
|
|
2128
|
+
if (code) values["code-lane"] = code;
|
|
2129
|
+
if (design) values["design-lane"] = design;
|
|
2130
|
+
if (Object.keys(values).length === 0) return;
|
|
2131
|
+
const target = localSemPath();
|
|
2132
|
+
writeFileSync3(target, serializeSem(values));
|
|
2133
|
+
process.stderr.write(`${style.green("\u2713")} lane pin written \u2192 ${target}
|
|
2134
|
+
`);
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2003
2137
|
// src/commands/setup.ts
|
|
2004
2138
|
function copyChoice(opts) {
|
|
2005
2139
|
return opts.copy === true ? "yes" : opts.copy === false ? "no" : "ask";
|
|
@@ -2086,6 +2220,9 @@ Examples:
|
|
|
2086
2220
|
result.push({ client: key, actions });
|
|
2087
2221
|
if (!json) printActions(target, actions);
|
|
2088
2222
|
}
|
|
2223
|
+
if (!json && !opts.dryRun && !opts.mcpOnly) {
|
|
2224
|
+
await maybeOfferSkills(cfg, personalWorkspaceId, { yes: false, dryRun: Boolean(opts.dryRun), surface: "claude-code" });
|
|
2225
|
+
}
|
|
2089
2226
|
if (json) {
|
|
2090
2227
|
emit({ dryRun: Boolean(opts.dryRun), clients: result }, true);
|
|
2091
2228
|
return;
|
|
@@ -2295,6 +2432,9 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
2295
2432
|
result.push({ client: key, actions });
|
|
2296
2433
|
if (!json) printActions(target, actions);
|
|
2297
2434
|
}
|
|
2435
|
+
if (!json && !dryRun) {
|
|
2436
|
+
await maybeOfferSkills(cfg, personalWorkspaceId, { yes, dryRun, surface: "claude-code" });
|
|
2437
|
+
}
|
|
2298
2438
|
if (json) {
|
|
2299
2439
|
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, timezone: tz, wire, clients: result }, true);
|
|
2300
2440
|
return;
|
|
@@ -2324,11 +2464,306 @@ async function chooseWire(opts, yes) {
|
|
|
2324
2464
|
return opts.mcp === false ? "agent-only" : "full";
|
|
2325
2465
|
}
|
|
2326
2466
|
|
|
2467
|
+
// src/commands/skills.ts
|
|
2468
|
+
import { homedir as homedir4 } from "os";
|
|
2469
|
+
import { dirname as dirname5, join as join5 } from "path";
|
|
2470
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, rmSync as rmSync2, existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
2471
|
+
var DEFAULT_SLUG = "operator-skills";
|
|
2472
|
+
var ROLE_TAGS = ["sechroom:role:skill-template", "role:skill-template"];
|
|
2473
|
+
var LOCK = ".sechroom-skills.json";
|
|
2474
|
+
function skillsDir(global) {
|
|
2475
|
+
return global ? join5(homedir4(), ".claude", "skills") : join5(process.cwd(), ".claude", "skills");
|
|
2476
|
+
}
|
|
2477
|
+
function tagValue2(tags, prefix) {
|
|
2478
|
+
return (tags ?? []).find((t) => t.startsWith(prefix))?.slice(prefix.length);
|
|
2479
|
+
}
|
|
2480
|
+
function hasAny(tags, candidates) {
|
|
2481
|
+
return (tags ?? []).some((t) => candidates.includes(t));
|
|
2482
|
+
}
|
|
2483
|
+
function registerSkills(program2) {
|
|
2484
|
+
const skills = program2.command("skills").description("Install + manage operator skills from a bundle");
|
|
2485
|
+
skills.addHelpText(
|
|
2486
|
+
"after",
|
|
2487
|
+
`
|
|
2488
|
+
Examples:
|
|
2489
|
+
$ sechroom skills install --code-lane claude-code-chris --design-lane claude-design-chris
|
|
2490
|
+
$ sechroom skills install operator-skills --surface claude-code --local
|
|
2491
|
+
$ sechroom skills list
|
|
2492
|
+
$ sechroom skills set-lane --code-lane claude-code-chris --design-lane claude-design-chris
|
|
2493
|
+
$ sechroom skills lane
|
|
2494
|
+
$ sechroom skills clean`
|
|
2495
|
+
);
|
|
2496
|
+
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) => {
|
|
2497
|
+
const slug = slugArg || DEFAULT_SLUG;
|
|
2498
|
+
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
2499
|
+
const pw = await runApi("resolving personal workspace", () => client.GET("/me/personal-workspace", {}));
|
|
2500
|
+
const personalWsId = pw?.id || pw?.workspaceId || pw?.personalWorkspaceId || pw?.item?.id;
|
|
2501
|
+
if (!personalWsId) fail("Could not resolve your personal workspace.");
|
|
2502
|
+
let version = opts.version;
|
|
2503
|
+
if (!version) {
|
|
2504
|
+
const cat = await runApi("reading the bundle catalogue", () => client.GET("/me/bundles", {}));
|
|
2505
|
+
const item = (cat?.bundles ?? cat?.Bundles ?? []).find((b) => (b.slug ?? b.Slug) === slug);
|
|
2506
|
+
if (!item) fail(`Bundle '${slug}' is not in your self-serve catalogue (must be UserInstallable + Published).`);
|
|
2507
|
+
version = item.latestVersion ?? item.LatestVersion;
|
|
2508
|
+
if (!version) fail(`Bundle '${slug}' has no installable (Published) version.`);
|
|
2509
|
+
}
|
|
2510
|
+
const installOptions = {};
|
|
2511
|
+
if (opts.codeLane) installOptions["identity.code-lane"] = opts.codeLane;
|
|
2512
|
+
if (opts.designLane) installOptions["identity.design-lane"] = opts.designLane;
|
|
2513
|
+
const res = await runApi(
|
|
2514
|
+
`installing ${slug}@${version}${opts.instance ? ` (${opts.instance})` : ""}`,
|
|
2515
|
+
() => client.POST("/me/bundles/{slug}/versions/{version}/install", {
|
|
2516
|
+
params: { path: { slug, version } },
|
|
2517
|
+
// instance: null/absent = the default instance (reinstall updates in
|
|
2518
|
+
// place); a name installs a separate instance.
|
|
2519
|
+
body: { installOptions, instance: opts.instance ?? null }
|
|
2520
|
+
})
|
|
2521
|
+
);
|
|
2522
|
+
const status = String(res?.status ?? res?.Status ?? "");
|
|
2523
|
+
if (status && status.toLowerCase() !== "completed") {
|
|
2524
|
+
fail(`Install did not complete (status=${status}; ${res?.failureReason ?? res?.FailureReason ?? ""}).`);
|
|
2525
|
+
}
|
|
2526
|
+
const feed = await runApi(
|
|
2527
|
+
"materialising skill files",
|
|
2528
|
+
() => client.GET("/workspaces/{workspaceId}/memories/feed", {
|
|
2529
|
+
// cascadeWorkspaces: skills land in an "Operator Skills" SUB-workspace of
|
|
2530
|
+
// the personal workspace, so we recurse from the personal-ws root.
|
|
2531
|
+
// includeText: the feed omits bodies by default; we need them for SKILL.md.
|
|
2532
|
+
params: {
|
|
2533
|
+
path: { workspaceId: personalWsId },
|
|
2534
|
+
query: { limit: 200, cascadeWorkspaces: true, includeText: true }
|
|
2535
|
+
}
|
|
2536
|
+
})
|
|
2537
|
+
);
|
|
2538
|
+
const rows = feed?.results ?? feed?.Results ?? [];
|
|
2539
|
+
const dir = skillsDir(!opts.local);
|
|
2540
|
+
const wantInstance = opts.instance || "default";
|
|
2541
|
+
const written = [];
|
|
2542
|
+
const bundleTagPrefix = `sechroom:bundle:${slug}@`;
|
|
2543
|
+
for (const r of rows) {
|
|
2544
|
+
const m = r.item ?? r;
|
|
2545
|
+
const tags = m.tags ?? m.Tags ?? [];
|
|
2546
|
+
if (!hasAny(tags, ROLE_TAGS)) continue;
|
|
2547
|
+
if (tagValue2(tags, "target:") !== opts.surface) continue;
|
|
2548
|
+
if (!tags.some((t) => t.startsWith(bundleTagPrefix))) continue;
|
|
2549
|
+
if ((tagValue2(tags, "sechroom:skill-instance:") ?? "default") !== wantInstance) continue;
|
|
2550
|
+
const name = tagValue2(tags, "skill:");
|
|
2551
|
+
if (!name) continue;
|
|
2552
|
+
const body = m.text ?? m.Text ?? "";
|
|
2553
|
+
mkdirSync4(join5(dir, name), { recursive: true });
|
|
2554
|
+
writeFileSync4(join5(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
|
|
2555
|
+
written.push(name);
|
|
2556
|
+
}
|
|
2557
|
+
mkdirSync4(dir, { recursive: true });
|
|
2558
|
+
const lockPath = join5(dir, LOCK);
|
|
2559
|
+
const lock = existsSync5(lockPath) ? JSON.parse(readFileSync4(lockPath, "utf8")) : {};
|
|
2560
|
+
lock[slug] = { surface: opts.surface, version, instance: wantInstance, skills: written.sort() };
|
|
2561
|
+
writeFileSync4(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
2562
|
+
if (opts.json) return emit({ slug, version, instance: wantInstance, surface: opts.surface, dir, installed: written }, true);
|
|
2563
|
+
const instanceNote = opts.instance ? ` (${opts.instance})` : "";
|
|
2564
|
+
console.log(style.green(`Installed ${slug}@${version}${instanceNote} \u2014 ${written.length} skill(s) \u2192 ${dir}`));
|
|
2565
|
+
written.forEach((n) => console.log(" " + style.dim("\u2022") + " " + n));
|
|
2566
|
+
if (written.length === 0) console.log(style.dim(` (no '${opts.surface}' skill bodies found; check --surface)`));
|
|
2567
|
+
});
|
|
2568
|
+
skills.command("list").description("List your installed bundles (GET /me/bundle-installs)").option("--json", "machine output").action(async (opts, cmd) => {
|
|
2569
|
+
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
2570
|
+
const data = await runApi("reading your installs", () => client.GET("/me/bundle-installs", {}));
|
|
2571
|
+
if (opts.json) return emit(data, true);
|
|
2572
|
+
const installs = data?.installs ?? data?.Installs ?? [];
|
|
2573
|
+
if (installs.length === 0) return console.log(style.dim("No bundles installed."));
|
|
2574
|
+
installs.forEach((i) => {
|
|
2575
|
+
const inst = i.instance ?? i.Instance ?? "";
|
|
2576
|
+
const tag = inst ? style.dim(` [${inst}]`) : "";
|
|
2577
|
+
console.log(` ${i.bundleSlug ?? i.BundleSlug}@${i.bundleVersion ?? i.BundleVersion ?? "?"}${tag}`);
|
|
2578
|
+
});
|
|
2579
|
+
});
|
|
2580
|
+
skills.command("clean [slug]").description(`Remove materialised skill files written by install (default ${DEFAULT_SLUG})`).option("--local", "clean ./.claude/skills instead of ~/.claude/skills").option("--json", "machine output").action(async (slugArg, opts) => {
|
|
2581
|
+
const slug = slugArg || DEFAULT_SLUG;
|
|
2582
|
+
const dir = skillsDir(!opts.local);
|
|
2583
|
+
const lockPath = join5(dir, LOCK);
|
|
2584
|
+
if (!existsSync5(lockPath)) fail(`No skills lockfile at ${lockPath}; nothing to clean.`);
|
|
2585
|
+
const lock = JSON.parse(readFileSync4(lockPath, "utf8"));
|
|
2586
|
+
const entry = lock[slug];
|
|
2587
|
+
if (!entry) fail(`No installed record for '${slug}' in ${lockPath}.`);
|
|
2588
|
+
const removed = [];
|
|
2589
|
+
for (const name of entry.skills) {
|
|
2590
|
+
const skillPath = join5(dir, name);
|
|
2591
|
+
if (existsSync5(skillPath)) {
|
|
2592
|
+
rmSync2(skillPath, { recursive: true, force: true });
|
|
2593
|
+
removed.push(name);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
delete lock[slug];
|
|
2597
|
+
writeFileSync4(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
2598
|
+
if (opts.json) return emit({ slug, removed, dir }, true);
|
|
2599
|
+
console.log(style.green(`Removed ${removed.length} skill(s) for ${slug} from ${dir}`));
|
|
2600
|
+
});
|
|
2601
|
+
skills.command("set-lane").description("Write this checkout's lane pin to a local ./.sem file (read at runtime by skills)").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((opts, cmd) => {
|
|
2602
|
+
if (!opts.codeLane && !opts.designLane) fail("Provide --code-lane and/or --design-lane.");
|
|
2603
|
+
const target = localSemPath();
|
|
2604
|
+
const values = readSem(target)?.values ?? {};
|
|
2605
|
+
if (opts.codeLane) values["code-lane"] = opts.codeLane;
|
|
2606
|
+
if (opts.designLane) values["design-lane"] = opts.designLane;
|
|
2607
|
+
mkdirSync4(dirname5(target), { recursive: true });
|
|
2608
|
+
writeFileSync4(target, serializeSem(values));
|
|
2609
|
+
if (cmd.optsWithGlobals().json) return emit({ path: target, values }, true);
|
|
2610
|
+
console.log(style.green(`Wrote lane pin \u2192 ${target}`));
|
|
2611
|
+
Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
|
|
2612
|
+
});
|
|
2613
|
+
skills.command("lane").description("Show the lane pin resolved from ./.sem (nearest in this checkout)").option("--json", "machine output").action((opts, cmd) => {
|
|
2614
|
+
const json = cmd.optsWithGlobals().json;
|
|
2615
|
+
const found = readSem();
|
|
2616
|
+
if (!found) {
|
|
2617
|
+
if (json) return emit({ path: null, values: {} }, true);
|
|
2618
|
+
return console.log(style.dim(`No ./.sem pin in this checkout. Run 'sechroom skills set-lane'.`));
|
|
2619
|
+
}
|
|
2620
|
+
if (json) return emit(found, true);
|
|
2621
|
+
console.log(style.dim(`from ${found.path}`));
|
|
2622
|
+
Object.entries(found.values).forEach(([k, v]) => console.log(" " + style.bold(k) + " = " + v));
|
|
2623
|
+
});
|
|
2624
|
+
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) => {
|
|
2625
|
+
if (!opts.defaultCodeLane && !opts.defaultDesignLane && !opts.handoverRecipient)
|
|
2626
|
+
fail("Provide at least one of --default-code-lane / --default-design-lane / --handover-recipient.");
|
|
2627
|
+
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
2628
|
+
const cur = await runApi(
|
|
2629
|
+
"reading workflow preferences",
|
|
2630
|
+
() => client.GET("/me/workflow-preferences", {})
|
|
2631
|
+
);
|
|
2632
|
+
const body = {
|
|
2633
|
+
defaultCodeLane: opts.defaultCodeLane ?? cur?.defaultCodeLane ?? null,
|
|
2634
|
+
defaultDesignLane: opts.defaultDesignLane ?? cur?.defaultDesignLane ?? null,
|
|
2635
|
+
handoverRecipient: opts.handoverRecipient ?? cur?.handoverRecipient ?? null
|
|
2636
|
+
};
|
|
2637
|
+
const res = await runApi(
|
|
2638
|
+
"saving workflow preferences",
|
|
2639
|
+
() => client.POST("/me/workflow-preferences", { body })
|
|
2640
|
+
);
|
|
2641
|
+
if (cmd.optsWithGlobals().json) return emit(res, true);
|
|
2642
|
+
console.log(style.green("Saved your workflow preferences"));
|
|
2643
|
+
console.log(" " + style.dim("default-code-lane") + " = " + (body.defaultCodeLane ?? "(unset)"));
|
|
2644
|
+
console.log(" " + style.dim("default-design-lane") + " = " + (body.defaultDesignLane ?? "(unset)"));
|
|
2645
|
+
console.log(" " + style.dim("handover-recipient") + " = " + (body.handoverRecipient ?? "(unset)"));
|
|
2646
|
+
});
|
|
2647
|
+
skills.command("workflow").description("Show your per-operator workflow defaults (GET /me/workflow-preferences)").option("--json", "machine output").action(async (opts, cmd) => {
|
|
2648
|
+
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
2649
|
+
const data = await runApi(
|
|
2650
|
+
"reading workflow preferences",
|
|
2651
|
+
() => client.GET("/me/workflow-preferences", {})
|
|
2652
|
+
);
|
|
2653
|
+
if (cmd.optsWithGlobals().json) return emit(data, true);
|
|
2654
|
+
console.log(" " + style.bold("default-code-lane") + " = " + (data?.defaultCodeLane ?? style.dim("(unset)")));
|
|
2655
|
+
console.log(" " + style.bold("default-design-lane") + " = " + (data?.defaultDesignLane ?? style.dim("(unset)")));
|
|
2656
|
+
console.log(" " + style.bold("handover-recipient") + " = " + (data?.handoverRecipient ?? style.dim("(unset)")));
|
|
2657
|
+
});
|
|
2658
|
+
skills.command("resolve").description("Resolve the effective ${identity.*} slot values (per-location .sem + per-operator workflow prefs)").option("--json", "machine output (a flat slot->value map + per-slot source)").action(async (opts, cmd) => {
|
|
2659
|
+
const local = readSem()?.values ?? {};
|
|
2660
|
+
let operator = {};
|
|
2661
|
+
try {
|
|
2662
|
+
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
2663
|
+
operator = await runApi(
|
|
2664
|
+
"reading workflow preferences",
|
|
2665
|
+
() => client.GET("/me/workflow-preferences", {})
|
|
2666
|
+
) ?? {};
|
|
2667
|
+
} catch {
|
|
2668
|
+
}
|
|
2669
|
+
const pick = (loc, op) => loc != null && loc !== "" ? { value: loc, source: "per-location" } : op != null && op !== "" ? { value: op, source: "per-operator" } : { value: null, source: "unset" };
|
|
2670
|
+
const slots = {
|
|
2671
|
+
"identity.code-lane": pick(local["code-lane"], operator?.defaultCodeLane),
|
|
2672
|
+
"identity.design-lane": pick(local["design-lane"], operator?.defaultDesignLane),
|
|
2673
|
+
"identity.handover-recipient": pick(void 0, operator?.handoverRecipient)
|
|
2674
|
+
};
|
|
2675
|
+
if (cmd.optsWithGlobals().json) {
|
|
2676
|
+
const values = Object.fromEntries(Object.entries(slots).map(([k, v]) => [k, v.value]));
|
|
2677
|
+
return emit({ values, sources: Object.fromEntries(Object.entries(slots).map(([k, v]) => [k, v.source])) }, true);
|
|
2678
|
+
}
|
|
2679
|
+
for (const [slot, { value, source }] of Object.entries(slots)) {
|
|
2680
|
+
const v = value == null ? style.dim("(unset)") : value;
|
|
2681
|
+
console.log(" " + style.bold(slot) + " = " + v + " " + style.dim(`[${source}]`));
|
|
2682
|
+
}
|
|
2683
|
+
});
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
// src/commands/reset.ts
|
|
2687
|
+
import { homedir as homedir5 } from "os";
|
|
2688
|
+
import { join as join6 } from "path";
|
|
2689
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, rmSync as rmSync3 } from "fs";
|
|
2690
|
+
var SKILLS_LOCK = ".sechroom-skills.json";
|
|
2691
|
+
var localSkillsDir = () => join6(process.cwd(), ".claude", "skills");
|
|
2692
|
+
var globalSkillsDir = () => join6(homedir5(), ".claude", "skills");
|
|
2693
|
+
function removeMaterialisedSkills(dir) {
|
|
2694
|
+
const removed = [];
|
|
2695
|
+
const lockPath = join6(dir, SKILLS_LOCK);
|
|
2696
|
+
if (!existsSync6(lockPath)) return removed;
|
|
2697
|
+
try {
|
|
2698
|
+
const lock = JSON.parse(readFileSync5(lockPath, "utf8"));
|
|
2699
|
+
for (const entry of Object.values(lock)) {
|
|
2700
|
+
for (const name of entry.skills ?? []) {
|
|
2701
|
+
const p = join6(dir, name);
|
|
2702
|
+
if (existsSync6(p)) {
|
|
2703
|
+
rmSync3(p, { recursive: true, force: true });
|
|
2704
|
+
removed.push(p);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
} catch {
|
|
2709
|
+
}
|
|
2710
|
+
rmSync3(lockPath, { force: true });
|
|
2711
|
+
removed.push(lockPath);
|
|
2712
|
+
return removed;
|
|
2713
|
+
}
|
|
2714
|
+
function registerReset(program2) {
|
|
2715
|
+
program2.command("logout").description("Sign out \u2014 remove the cached (global) auth token").action((_opts, cmd) => {
|
|
2716
|
+
const removed = clearToken();
|
|
2717
|
+
if (cmd.optsWithGlobals().json) return emit({ removed: removed ? [removed] : [] }, true);
|
|
2718
|
+
console.log(
|
|
2719
|
+
removed ? style.green("Signed out \u2014 auth token removed.") : style.dim("Already signed out (no token).")
|
|
2720
|
+
);
|
|
2721
|
+
});
|
|
2722
|
+
program2.command("reset").description("Reset LOCAL CLI state for this directory (./.sechroom.json, ./.sem, ./.claude/skills); --global also wipes the machine-wide token + config + ~/.claude/skills").option("--global", "also remove the global auth token, config, and ~/.claude/skills").option("-y, --yes", "don't prompt for confirmation").option("--json", "machine output").action(async (opts, cmd) => {
|
|
2723
|
+
const json = cmd.optsWithGlobals().json;
|
|
2724
|
+
const global = Boolean(opts.global);
|
|
2725
|
+
if (!opts.yes && canPrompt()) {
|
|
2726
|
+
const scope = global ? "this directory's local state AND your global auth token + config + ~/.claude/skills" : "this directory's local state (./.sechroom.json, ./.sem, ./.claude/skills)";
|
|
2727
|
+
if (!await promptYesNo(`Remove ${scope}?`)) {
|
|
2728
|
+
if (!json) console.log(style.dim("Cancelled."));
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
const removed = [];
|
|
2733
|
+
const localCfg = join6(process.cwd(), ".sechroom.json");
|
|
2734
|
+
if (existsSync6(localCfg)) {
|
|
2735
|
+
rmSync3(localCfg, { force: true });
|
|
2736
|
+
removed.push(localCfg);
|
|
2737
|
+
}
|
|
2738
|
+
const sem = localSemPath();
|
|
2739
|
+
if (existsSync6(sem)) {
|
|
2740
|
+
rmSync3(sem, { force: true });
|
|
2741
|
+
removed.push(sem);
|
|
2742
|
+
}
|
|
2743
|
+
removed.push(...removeMaterialisedSkills(localSkillsDir()));
|
|
2744
|
+
if (global) {
|
|
2745
|
+
const tok = clearToken();
|
|
2746
|
+
if (tok) removed.push(tok);
|
|
2747
|
+
const cfg = clearPersisted();
|
|
2748
|
+
if (cfg) removed.push(cfg);
|
|
2749
|
+
removed.push(...removeMaterialisedSkills(globalSkillsDir()));
|
|
2750
|
+
}
|
|
2751
|
+
if (json) return emit({ global, removed }, true);
|
|
2752
|
+
if (removed.length === 0) {
|
|
2753
|
+
console.log(style.dim(global ? "Nothing to remove \u2014 already clean." : "No local CLI state in this directory."));
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
console.log(style.green(`Reset complete \u2014 removed ${removed.length} item(s):`));
|
|
2757
|
+
removed.forEach((p) => console.log(" " + style.dim("\u2022") + " " + p));
|
|
2758
|
+
if (global) console.log(style.dim("Run 'sechroom onboard' to set up again."));
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2327
2762
|
// src/index.ts
|
|
2328
2763
|
function resolveVersion() {
|
|
2329
2764
|
try {
|
|
2330
2765
|
const pkg = JSON.parse(
|
|
2331
|
-
|
|
2766
|
+
readFileSync6(new URL("../package.json", import.meta.url), "utf8")
|
|
2332
2767
|
);
|
|
2333
2768
|
return pkg.version ?? "0.0.0";
|
|
2334
2769
|
} catch {
|
|
@@ -2443,6 +2878,8 @@ registerChat(program);
|
|
|
2443
2878
|
registerInit(program);
|
|
2444
2879
|
registerSetup(program);
|
|
2445
2880
|
registerOnboard(program);
|
|
2881
|
+
registerSkills(program);
|
|
2882
|
+
registerReset(program);
|
|
2446
2883
|
program.parseAsync().catch((err2) => {
|
|
2447
2884
|
process.stderr.write(`error: ${err2 instanceof Error ? err2.message : String(err2)}
|
|
2448
2885
|
`);
|
package/package.json
CHANGED