@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.
- package/README.md +1 -2
- package/dist/index.js +492 -10
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
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
|
-
|
|
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