@sechroom/cli 2026.6.9 → 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.
Files changed (3) hide show
  1. package/README.md +1 -2
  2. package/dist/index.js +492 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -146,12 +146,11 @@ The CLI mirrors the sechroom MCP tool surface — every command is a thin wrappe
146
146
  | `continuity` | snapshot-create / -get · snapshots · resume-me / resume-lane · changed-since · load-set · grant / revoke-grant |
147
147
  | `id` | next / peek (FR-_/D-_ sequence allocation) |
148
148
  | `account` | profile / set-profile · feed · reviews / review-get / review-accept · lookup-batch |
149
- | `chat` | messages · replies · stop-tracking (Slack / Discord, via `--surface`) |
149
+ | `chat` | send · messages · replies · stop-tracking (Slack / Discord, via `--surface`) |
150
150
  | `worklog` · `lookup` | append · resolve any id |
151
151
 
152
152
  Notes on deliberate gaps (API-rooted, not CLI):
153
153
  - **No `memory delete`** — the API exposes no hard DELETE; `memory archive` is the soft-delete path.
154
- - **No `chat send`** — the unified `/chat/*` surface has no POST send endpoint yet, so the Slack/Discord *send* tools can't be wrapped. Reading works today; `send` lands once the route is added to the spec.
155
154
  - **`memory revert`** needs `--text` + `--content` — the revert endpoint doesn't reconstruct a version's body from its number; pull them from `memory versions` / `memory get` first.
156
155
 
157
156
  ## Onboarding (`init` / `setup`)
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
  });
@@ -468,6 +479,12 @@ function emit(data, json) {
468
479
  function publicUrl(url) {
469
480
  return url.replace(/^https?:\/\/localhost:5012/, "https://sechroom.yi.ocd.codes");
470
481
  }
482
+ function resolveViewUrl(baseUrl, url) {
483
+ if (!url) return void 0;
484
+ if (/^https?:\/\//i.test(url)) return publicUrl(url);
485
+ const origin = baseUrl.replace(/\/api\/?$/i, "").replace(/\/+$/, "");
486
+ return publicUrl(`${origin}${url.startsWith("/") ? url : `/${url}`}`);
487
+ }
471
488
  function emitAction(summary, data, json) {
472
489
  if (json) {
473
490
  process.stdout.write(JSON.stringify(data) + "\n");
@@ -538,7 +555,8 @@ Examples:
538
555
  });
539
556
  });
540
557
  const titlePart = opts.title ? ` ${style.dim(`"${opts.title}"`)}` : "";
541
- const urlPart = data.url ? ` ${style.dim("\u2192")} ${publicUrl(data.url)}` : "";
558
+ const view = resolveViewUrl(cfg.baseUrl, data.url);
559
+ const urlPart = view ? ` ${style.dim("\u2192")} ${view}` : "";
542
560
  emitAction(`created memory ${style.bold(data.id)}${titlePart}${urlPart}`, data, cmd.optsWithGlobals().json);
543
561
  });
544
562
  memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").action(async (memoryId, _opts, cmd) => {
@@ -847,7 +865,8 @@ Examples:
847
865
  });
848
866
  });
849
867
  const inversePart = data.inverseId ? ` ${style.dim(`(inverse ${data.inverseId})`)}` : "";
850
- const urlPart = data.url ? ` ${style.dim("\u2192")} ${publicUrl(data.url)}` : "";
868
+ const view = resolveViewUrl(cfg.baseUrl, data.url);
869
+ const urlPart = view ? ` ${style.dim("\u2192")} ${view}` : "";
851
870
  emitAction(
852
871
  `created relationship ${style.bold(data.id)} ${style.dim(`${fromMemoryId} \u2192 ${toMemoryId}`)}${inversePart}${urlPart}`,
853
872
  data,
@@ -997,7 +1016,8 @@ Examples:
997
1016
  }
998
1017
  });
999
1018
  });
1000
- const urlPart = data.url ? ` ${style.dim("\u2192")} ${publicUrl(data.url)}` : "";
1019
+ const view = resolveViewUrl(cfg.baseUrl, data.url);
1020
+ const urlPart = view ? ` ${style.dim("\u2192")} ${view}` : "";
1001
1021
  emitAction(
1002
1022
  `created workspace ${style.bold(data.id)} ${style.dim(`"${opts.name}"`)}${urlPart}`,
1003
1023
  data,
@@ -1140,7 +1160,8 @@ Examples:
1140
1160
  }
1141
1161
  });
1142
1162
  });
1143
- const urlPart = data.url ? ` ${style.dim("\u2192")} ${publicUrl(data.url)}` : "";
1163
+ const view = resolveViewUrl(cfg.baseUrl, data.url);
1164
+ const urlPart = view ? ` ${style.dim("\u2192")} ${view}` : "";
1144
1165
  emitAction(`created project ${style.bold(data.id)}${urlPart}`, data, cmd.optsWithGlobals().json);
1145
1166
  });
1146
1167
  project.command("list").description("List projects (GET /projects)").option("--workspace <workspaceId>", "Scope to a workspace").option("--status <status>", "Draft | Active | OnHold | Completed | Cancelled").option("--include-archived", "Include archived projects", false).action(async (opts, cmd) => {
@@ -1658,17 +1679,52 @@ Examples:
1658
1679
 
1659
1680
  // src/commands/chat.ts
1660
1681
  function registerChat(program2) {
1661
- const chat = program2.command("chat").description("Read Slack / Discord channel messages + thread replies").option("--surface <surface>", "slack | discord", "slack");
1682
+ const chat = program2.command("chat").description("Send and read Slack / Discord channel messages").option("--surface <surface>", "slack | discord", "slack");
1662
1683
  chat.addHelpText(
1663
1684
  "after",
1664
1685
  `
1665
1686
  Examples:
1687
+ $ sechroom chat send C0123456789 "deploy is green" --surface slack
1688
+ $ sechroom chat send 987654321098765432 "deploy is green" --surface discord --guild 123456789012345678
1689
+ $ sechroom chat send C0123456789 "lgtm" --surface slack --as user --parent 1718049600.123456
1666
1690
  $ sechroom chat messages --surface slack
1667
- $ sechroom chat messages --surface discord --json
1668
1691
  $ sechroom chat replies 1718049600.123456 --surface slack
1669
- $ sechroom chat replies 1234567890123456789 --surface discord --json
1670
1692
  $ sechroom chat stop-tracking 1718049600.123456 --surface slack`
1671
1693
  );
1694
+ chat.command("send <channelId> <text>").description("Send a message to a channel (POST /chat/channel-messages/{surface})").option("--guild <guildId>", "Discord guild snowflake \u2014 required for --surface discord").option("--memory <memoryId>", "Attach a sechroom memory id").option("--no-track", "Don't capture replies to this message").option("--parent <parentMessage>", "Thread under a parent (Slack thread_ts / Discord message id)").option("--source <source>", "Source / lane stamp (renders an attribution footer)", "cli").option("--as <as>", "Slack only: 'bot' (default) or 'user' (your linked Slack identity)", "bot").action(async (channelId, text, opts, cmd) => {
1695
+ const { surface, ...globals } = cmd.optsWithGlobals();
1696
+ const json = Boolean(cmd.optsWithGlobals().json);
1697
+ const cfg = resolveConfig(globals);
1698
+ const data = await runApi("Sending message", async () => {
1699
+ const client = await makeClient(cfg);
1700
+ return client.POST("/chat/channel-messages/{surface}", {
1701
+ params: { path: { surface: String(surface) } },
1702
+ body: {
1703
+ channelId,
1704
+ text,
1705
+ guildId: opts.guild ?? null,
1706
+ attachedMemoryId: opts.memory ?? null,
1707
+ trackReplies: opts.track,
1708
+ parentMessage: opts.parent ?? null,
1709
+ source: opts.source,
1710
+ as: opts.as
1711
+ }
1712
+ });
1713
+ });
1714
+ if (!data.ok) {
1715
+ if (json) {
1716
+ emit(data, true);
1717
+ } else {
1718
+ process.stderr.write(
1719
+ `${err("\u2717")} send failed: ${data.upstreamError ?? "error"}${data.errorDescription ? ` \u2014 ${data.errorDescription}` : ""}
1720
+ `
1721
+ );
1722
+ }
1723
+ process.exit(1);
1724
+ }
1725
+ const idPart = data.persistedId ? ` ${style.dim(`(${data.persistedId})`)}` : "";
1726
+ emitAction(`sent to ${surface} ${style.bold(channelId)}${idPart}`, data, json);
1727
+ });
1672
1728
  chat.command("messages").description("List recent channel messages (GET /chat/channel-messages/{surface})").action(async (_opts, cmd) => {
1673
1729
  const { surface, ...globals } = cmd.optsWithGlobals();
1674
1730
  const cfg = resolveConfig(globals);
@@ -1955,6 +2011,129 @@ function detectInstalledClients(cwd) {
1955
2011
  return detected;
1956
2012
  }
1957
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
+
1958
2137
  // src/commands/setup.ts
1959
2138
  function copyChoice(opts) {
1960
2139
  return opts.copy === true ? "yes" : opts.copy === false ? "no" : "ask";
@@ -2041,6 +2220,9 @@ Examples:
2041
2220
  result.push({ client: key, actions });
2042
2221
  if (!json) printActions(target, actions);
2043
2222
  }
2223
+ if (!json && !opts.dryRun && !opts.mcpOnly) {
2224
+ await maybeOfferSkills(cfg, personalWorkspaceId, { yes: false, dryRun: Boolean(opts.dryRun), surface: "claude-code" });
2225
+ }
2044
2226
  if (json) {
2045
2227
  emit({ dryRun: Boolean(opts.dryRun), clients: result }, true);
2046
2228
  return;
@@ -2250,6 +2432,9 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
2250
2432
  result.push({ client: key, actions });
2251
2433
  if (!json) printActions(target, actions);
2252
2434
  }
2435
+ if (!json && !dryRun) {
2436
+ await maybeOfferSkills(cfg, personalWorkspaceId, { yes, dryRun, surface: "claude-code" });
2437
+ }
2253
2438
  if (json) {
2254
2439
  emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, timezone: tz, wire, clients: result }, true);
2255
2440
  return;
@@ -2279,11 +2464,306 @@ async function chooseWire(opts, yes) {
2279
2464
  return opts.mcp === false ? "agent-only" : "full";
2280
2465
  }
2281
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
+
2282
2762
  // src/index.ts
2283
2763
  function resolveVersion() {
2284
2764
  try {
2285
2765
  const pkg = JSON.parse(
2286
- readFileSync3(new URL("../package.json", import.meta.url), "utf8")
2766
+ readFileSync6(new URL("../package.json", import.meta.url), "utf8")
2287
2767
  );
2288
2768
  return pkg.version ?? "0.0.0";
2289
2769
  } catch {
@@ -2398,6 +2878,8 @@ registerChat(program);
2398
2878
  registerInit(program);
2399
2879
  registerSetup(program);
2400
2880
  registerOnboard(program);
2881
+ registerSkills(program);
2882
+ registerReset(program);
2401
2883
  program.parseAsync().catch((err2) => {
2402
2884
  process.stderr.write(`error: ${err2 instanceof Error ? err2.message : String(err2)}
2403
2885
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sechroom/cli",
3
- "version": "2026.6.9",
3
+ "version": "2026.6.11",
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",