@sechroom/cli 2026.6.10 → 2026.6.12

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.
Files changed (2) hide show
  1. package/dist/index.js +443 -6
  2. 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 readFileSync3 } from "fs";
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
  });
@@ -491,9 +502,10 @@ async function runApi(label, fn) {
491
502
  s.fail();
492
503
  fail(err2);
493
504
  }
494
- if (res.error !== void 0 && res.error !== null) {
505
+ const httpFailed = res.response !== void 0 && !res.response.ok;
506
+ if (res.error !== void 0 && res.error !== null || httpFailed) {
495
507
  s.fail();
496
- fail(res.error);
508
+ fail(res.error ?? (res.response ? `HTTP ${res.response.status} ${res.response.statusText}`.trim() : "request failed"));
497
509
  }
498
510
  s.succeed();
499
511
  return res.data;
@@ -2000,6 +2012,129 @@ function detectInstalledClients(cwd) {
2000
2012
  return detected;
2001
2013
  }
2002
2014
 
2015
+ // src/setup/skills-offer.ts
2016
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
2017
+ import { homedir as homedir3 } from "os";
2018
+ import { join as join4 } from "path";
2019
+
2020
+ // src/sem.ts
2021
+ import { dirname as dirname4, join as join3 } from "path";
2022
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
2023
+ var SEM_FILE = ".sem";
2024
+ function localSemPath(cwd = process.cwd()) {
2025
+ return join3(cwd, SEM_FILE);
2026
+ }
2027
+ function resolveSemPathForRead(start = process.cwd()) {
2028
+ let dir = start;
2029
+ while (true) {
2030
+ const candidate = join3(dir, SEM_FILE);
2031
+ if (existsSync4(candidate)) return candidate;
2032
+ const parent = dirname4(dir);
2033
+ if (parent === dir) return void 0;
2034
+ dir = parent;
2035
+ }
2036
+ }
2037
+ function parseSem(text) {
2038
+ const out = {};
2039
+ for (const raw of text.split("\n")) {
2040
+ const line = raw.trim();
2041
+ if (!line || line.startsWith("#")) continue;
2042
+ const eq = line.indexOf("=");
2043
+ if (eq === -1) continue;
2044
+ const key = line.slice(0, eq).trim();
2045
+ const value = line.slice(eq + 1).trim();
2046
+ if (key) out[key] = value;
2047
+ }
2048
+ return out;
2049
+ }
2050
+ function serializeSem(values) {
2051
+ const header = "# sechroom lane pin (per-location fallback) \u2014 resolved at runtime by operator skills.\n";
2052
+ const body = Object.entries(values).map(([k, v]) => `${k} = ${v}`).join("\n");
2053
+ return header + body + "\n";
2054
+ }
2055
+ function readSem(path) {
2056
+ const p = path ?? resolveSemPathForRead();
2057
+ if (!p || !existsSync4(p)) return void 0;
2058
+ return { path: p, values: parseSem(readFileSync3(p, "utf8")) };
2059
+ }
2060
+
2061
+ // src/setup/skills-offer.ts
2062
+ var ROLE_TAG = "sechroom:role:skill-template";
2063
+ function tagValue(tags, prefix) {
2064
+ return tags.find((t) => t.startsWith(prefix))?.slice(prefix.length);
2065
+ }
2066
+ async function maybeOfferSkills(cfg, personalWorkspaceId, opts) {
2067
+ if (!personalWorkspaceId || opts.dryRun) return;
2068
+ const surface = opts.surface ?? "claude-code";
2069
+ let rows = [];
2070
+ try {
2071
+ const client = await makeClient(cfg);
2072
+ const feed = await client.GET("/workspaces/{workspaceId}/memories/feed", {
2073
+ params: {
2074
+ path: { workspaceId: personalWorkspaceId },
2075
+ query: { limit: 200, cascadeWorkspaces: true, includeText: true }
2076
+ }
2077
+ }).then((r) => r.data).catch(() => void 0);
2078
+ rows = feed?.results ?? feed?.Results ?? [];
2079
+ } catch {
2080
+ return;
2081
+ }
2082
+ const skills = rows.map((r) => r.item ?? r).filter((m) => {
2083
+ const tags = m.tags ?? m.Tags ?? [];
2084
+ return tags.includes(ROLE_TAG) && tagValue(tags, "target:") === surface;
2085
+ });
2086
+ if (skills.length === 0) return;
2087
+ const byName = /* @__PURE__ */ new Map();
2088
+ for (const m of skills) {
2089
+ const name = tagValue(m.tags ?? m.Tags ?? [], "skill:");
2090
+ if (name) byName.set(name, m);
2091
+ }
2092
+ const names = [...byName.keys()].sort();
2093
+ if (names.length === 0) return;
2094
+ process.stderr.write(
2095
+ `
2096
+ Found ${style.bold(String(names.length))} operator skill(s) installed in your workspace: ${names.join(", ")}.
2097
+ `
2098
+ );
2099
+ const dir = join4(homedir3(), ".claude", "skills");
2100
+ const materialise = opts.yes ? true : canPrompt() ? await promptYesNo(`Write them to ${dir}/ so ${surface} can use them?`) : false;
2101
+ if (!materialise) return;
2102
+ const written = [];
2103
+ for (const [name, m] of byName) {
2104
+ const body = m.text ?? m.Text ?? "";
2105
+ mkdirSync3(join4(dir, name), { recursive: true });
2106
+ writeFileSync3(join4(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
2107
+ written.push(name);
2108
+ }
2109
+ process.stderr.write(`${style.green("\u2713")} wrote ${written.length} skill(s) to ${dir}
2110
+ `);
2111
+ if (readSem()) return;
2112
+ const setLane = opts.yes ? false : canPrompt() ? await promptYesNo("Set your lane now so the skills can resolve their identity slots?") : false;
2113
+ if (!setLane) {
2114
+ process.stderr.write(
2115
+ ` ${style.dim("run")} ${style.cyan("sechroom skills set-lane --code-lane \u2026 --design-lane \u2026")} ${style.dim("when ready.")}
2116
+ `
2117
+ );
2118
+ return;
2119
+ }
2120
+ let wf;
2121
+ try {
2122
+ const client = await makeClient(cfg);
2123
+ wf = await client.GET("/me/workflow-preferences", {}).then((r) => r.data).catch(() => void 0);
2124
+ } catch {
2125
+ }
2126
+ const code = await promptText("Code-lane id (e.g. claude-code-you)?", wf?.defaultCodeLane);
2127
+ const design = await promptText("Design-lane id (e.g. claude-design-you)?", wf?.defaultDesignLane);
2128
+ const values = {};
2129
+ if (code) values["code-lane"] = code;
2130
+ if (design) values["design-lane"] = design;
2131
+ if (Object.keys(values).length === 0) return;
2132
+ const target = localSemPath();
2133
+ writeFileSync3(target, serializeSem(values));
2134
+ process.stderr.write(`${style.green("\u2713")} lane pin written \u2192 ${target}
2135
+ `);
2136
+ }
2137
+
2003
2138
  // src/commands/setup.ts
2004
2139
  function copyChoice(opts) {
2005
2140
  return opts.copy === true ? "yes" : opts.copy === false ? "no" : "ask";
@@ -2086,6 +2221,9 @@ Examples:
2086
2221
  result.push({ client: key, actions });
2087
2222
  if (!json) printActions(target, actions);
2088
2223
  }
2224
+ if (!json && !opts.dryRun && !opts.mcpOnly) {
2225
+ await maybeOfferSkills(cfg, personalWorkspaceId, { yes: false, dryRun: Boolean(opts.dryRun), surface: "claude-code" });
2226
+ }
2089
2227
  if (json) {
2090
2228
  emit({ dryRun: Boolean(opts.dryRun), clients: result }, true);
2091
2229
  return;
@@ -2152,7 +2290,6 @@ async function ensureConfig(g, opts) {
2152
2290
  let baseUrl = g.baseUrl ?? process.env.SECHROOM_BASE_URL ?? local.baseUrl ?? persisted.baseUrl ?? DEFAULT_BASE_URL2;
2153
2291
  let tenant = g.tenant ?? process.env.SECHROOM_TENANT ?? local.tenant ?? persisted.tenant ?? "";
2154
2292
  if (canPrompt() && !opts.yes) {
2155
- baseUrl = await promptText("Sechroom API base URL?", baseUrl);
2156
2293
  tenant = await promptText("Tenant id?", tenant || void 0);
2157
2294
  }
2158
2295
  baseUrl = baseUrl.replace(/\/$/, "");
@@ -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
- readFileSync3(new URL("../package.json", import.meta.url), "utf8")
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sechroom/cli",
3
- "version": "2026.6.10",
3
+ "version": "2026.6.12",
4
4
  "description": "Sechroom CLI — a thin, generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",