@sechroom/cli 2026.6.30-rc.3b1b16ff → 2026.6.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +429 -228
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21,9 +21,9 @@ var DEFAULT_ACCOUNT = "default";
|
|
|
21
21
|
var STATE_DIR_NAME = ".sechroom";
|
|
22
22
|
var BASELINE_CONFIG_NAME = ".sechroom.json";
|
|
23
23
|
var OVERRIDE_CONFIG_NAME = join(STATE_DIR_NAME, "config.json");
|
|
24
|
-
var BINDING_FIELDS = ["schemaVersion", "baseUrl", "tenant", "workspaceId", "defaultProjectId"];
|
|
24
|
+
var BINDING_FIELDS = ["schemaVersion", "baseUrl", "tenant", "workspaceId", "defaultProjectId", "workspaces"];
|
|
25
25
|
var DEFAULT_BASE_URL = "https://app.sechroom.ai/api";
|
|
26
|
-
var LOCAL_CONFIG_SCHEMA_VERSION =
|
|
26
|
+
var LOCAL_CONFIG_SCHEMA_VERSION = 3;
|
|
27
27
|
function ensureDir() {
|
|
28
28
|
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
29
29
|
}
|
|
@@ -161,12 +161,14 @@ function readLocalConfig() {
|
|
|
161
161
|
tenant: merged.tenant,
|
|
162
162
|
workspaceId: merged.workspaceId,
|
|
163
163
|
defaultProjectId: merged.defaultProjectId,
|
|
164
|
+
workspaces: merged.workspaces,
|
|
164
165
|
account: merged.account,
|
|
165
|
-
path: existsSync(baselinePath) ? baselinePath : overridePath
|
|
166
|
+
path: existsSync(baselinePath) ? baselinePath : overridePath,
|
|
167
|
+
home
|
|
166
168
|
};
|
|
167
169
|
}
|
|
168
|
-
function writeLocalConfig(patch) {
|
|
169
|
-
const home = findConfigHome() ?? process.cwd();
|
|
170
|
+
function writeLocalConfig(patch, opts) {
|
|
171
|
+
const home = (opts?.here ? process.cwd() : findConfigHome()) ?? process.cwd();
|
|
170
172
|
const baselinePath = join(home, BASELINE_CONFIG_NAME);
|
|
171
173
|
const overridePath = join(home, OVERRIDE_CONFIG_NAME);
|
|
172
174
|
const current = readJsonConfig(baselinePath) ?? {};
|
|
@@ -184,6 +186,30 @@ function committedBindingPath(dir) {
|
|
|
184
186
|
const p = join(dir, BASELINE_CONFIG_NAME);
|
|
185
187
|
return existsSync(p) ? p : void 0;
|
|
186
188
|
}
|
|
189
|
+
function selectWorkspaceBinding(local, explicitName) {
|
|
190
|
+
const bindings = local.workspaces ?? [];
|
|
191
|
+
if (explicitName) {
|
|
192
|
+
const hit = bindings.find((b) => b.name === explicitName);
|
|
193
|
+
if (!hit)
|
|
194
|
+
throw new Error(
|
|
195
|
+
`No workspace binding named "${explicitName}" in ${local.home ?? "this repo"}'s .sechroom.json` + (bindings.length > 0 ? ` (have: ${bindings.map((b) => b.name).join(", ")})` : "") + ". Add one with `sechroom workspace bind`."
|
|
196
|
+
);
|
|
197
|
+
return hit;
|
|
198
|
+
}
|
|
199
|
+
if (!local.home || bindings.length === 0) return void 0;
|
|
200
|
+
const rel = process.cwd().startsWith(local.home) ? process.cwd().slice(local.home.length).replace(/^[/\\]/, "") : "";
|
|
201
|
+
const norm = (p) => p.replace(/\/\*\*$/, "").replace(/[/\\]+$/, "");
|
|
202
|
+
let best;
|
|
203
|
+
for (const b of bindings) {
|
|
204
|
+
for (const raw of b.paths ?? []) {
|
|
205
|
+
const prefix = norm(raw);
|
|
206
|
+
if (prefix.length === 0) continue;
|
|
207
|
+
const matches = rel === prefix || rel.startsWith(prefix + "/");
|
|
208
|
+
if (matches && (!best || prefix.length > best.len)) best = { binding: b, len: prefix.length };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return best?.binding;
|
|
212
|
+
}
|
|
187
213
|
function resolveConfig(flags) {
|
|
188
214
|
const local = readLocalConfig();
|
|
189
215
|
const persisted = readPersisted();
|
|
@@ -194,8 +220,9 @@ function resolveConfig(flags) {
|
|
|
194
220
|
"No tenant set. The Sechroom API rejects untenanted requests (HTTP 400). Pass --tenant <id>, set SECHROOM_TENANT, run `sechroom config set tenant <id>`, or `sechroom config set --local tenant <id>` for this directory."
|
|
195
221
|
);
|
|
196
222
|
}
|
|
197
|
-
const
|
|
198
|
-
const
|
|
223
|
+
const binding = selectWorkspaceBinding(local, flags.binding ?? process.env.SECHROOM_BINDING);
|
|
224
|
+
const workspaceId = process.env.SECHROOM_WORKSPACE ?? binding?.workspaceId ?? local.workspaceId ?? persisted.workspaceId ?? void 0;
|
|
225
|
+
const defaultProjectId = (binding ? binding.defaultProjectId : void 0) ?? local.defaultProjectId ?? persisted.defaultProjectId ?? void 0;
|
|
199
226
|
const account = resolveAccountAlias(flags.account);
|
|
200
227
|
return { baseUrl: baseUrl.replace(/\/$/, ""), tenant, account, workspaceId, defaultProjectId, clientId: persisted.clientId };
|
|
201
228
|
}
|
|
@@ -212,10 +239,17 @@ function describeConfig(flags) {
|
|
|
212
239
|
return { value: void 0, source: "unset" };
|
|
213
240
|
};
|
|
214
241
|
const baseUrl = pick(flags.baseUrl, process.env.SECHROOM_BASE_URL, local.baseUrl, g.baseUrl, DEFAULT_BASE_URL);
|
|
242
|
+
let binding;
|
|
243
|
+
try {
|
|
244
|
+
binding = selectWorkspaceBinding(local, flags.binding ?? process.env.SECHROOM_BINDING);
|
|
245
|
+
} catch {
|
|
246
|
+
binding = void 0;
|
|
247
|
+
}
|
|
248
|
+
const workspaceId = process.env.SECHROOM_WORKSPACE ? { value: process.env.SECHROOM_WORKSPACE, source: "env" } : binding ? { value: binding.workspaceId, source: `binding "${binding.name}"${localTag !== "local" ? ` (${local.path})` : ""}` } : pick(void 0, void 0, local.workspaceId, g.workspaceId);
|
|
215
249
|
return {
|
|
216
250
|
baseUrl: { value: baseUrl.value, source: baseUrl.source },
|
|
217
251
|
tenant: pick(flags.tenant, process.env.SECHROOM_TENANT, local.tenant, g.tenant),
|
|
218
|
-
workspaceId
|
|
252
|
+
workspaceId,
|
|
219
253
|
localPath: local.path
|
|
220
254
|
};
|
|
221
255
|
}
|
|
@@ -1168,6 +1202,26 @@ Examples:
|
|
|
1168
1202
|
// src/commands/workspace.ts
|
|
1169
1203
|
function registerWorkspace(program2) {
|
|
1170
1204
|
const workspace = program2.command("workspace").description("Create, browse, and manage workspaces");
|
|
1205
|
+
workspace.command("bind <workspaceId>").description("Bind a workspace to this repo (.sechroom.json). With --name it becomes a NAMED v3 binding (multi-workspace); --paths scopes it to subtrees.").option("--name <name>", "binding name (enables multi-workspace; selectable via --binding / path match)").option("--paths <csv>", "comma-separated directory prefixes (relative to the repo root) this binding covers, e.g. frontend,docs").option("--default-project <id>", "informational default project for this binding").option("--here", "write the binding file at THIS directory even when a parent already carries one").action((workspaceId, opts, cmd) => {
|
|
1206
|
+
const json = cmd.optsWithGlobals().json;
|
|
1207
|
+
if (!opts.name) {
|
|
1208
|
+
const path2 = writeLocalConfig({ workspaceId, ...opts.defaultProject ? { defaultProjectId: opts.defaultProject } : {} }, { here: Boolean(opts.here) });
|
|
1209
|
+
if (json) return emit({ workspaceId, path: path2 }, true);
|
|
1210
|
+
console.log(style.green(`Workspace ${workspaceId} bound (${path2}).`));
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
const local = readLocalConfig();
|
|
1214
|
+
const bindings = (local.workspaces ?? []).filter((b) => b.name !== opts.name);
|
|
1215
|
+
bindings.push({
|
|
1216
|
+
name: opts.name,
|
|
1217
|
+
workspaceId,
|
|
1218
|
+
...opts.defaultProject ? { defaultProjectId: opts.defaultProject } : {},
|
|
1219
|
+
...opts.paths ? { paths: String(opts.paths).split(",").map((p) => p.trim()).filter(Boolean) } : {}
|
|
1220
|
+
});
|
|
1221
|
+
const path = writeLocalConfig({ workspaces: bindings }, { here: Boolean(opts.here) });
|
|
1222
|
+
if (json) return emit({ binding: opts.name, workspaceId, paths: opts.paths ?? null, path }, true);
|
|
1223
|
+
console.log(style.green(`Binding "${opts.name}" \u2192 ${workspaceId} saved (${path}).`));
|
|
1224
|
+
});
|
|
1171
1225
|
workspace.addHelpText(
|
|
1172
1226
|
"after",
|
|
1173
1227
|
`
|
|
@@ -1726,7 +1780,7 @@ import { delimiter, dirname as dirname4, join as join4 } from "path";
|
|
|
1726
1780
|
|
|
1727
1781
|
// src/sem.ts
|
|
1728
1782
|
import { basename as basename2, dirname as dirname2, join as join2 } from "path";
|
|
1729
|
-
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1783
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1730
1784
|
var SEM_FILE = join2(".sechroom", "lane.json");
|
|
1731
1785
|
var LEGACY_SEM_FILE = ".sem";
|
|
1732
1786
|
var STATE_DIR_NAME2 = ".sechroom";
|
|
@@ -1745,6 +1799,43 @@ function resolveSemPathForRead(start = process.cwd()) {
|
|
|
1745
1799
|
dir = parent;
|
|
1746
1800
|
}
|
|
1747
1801
|
}
|
|
1802
|
+
function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
|
|
1803
|
+
try {
|
|
1804
|
+
let dir = start;
|
|
1805
|
+
let gitPath;
|
|
1806
|
+
for (; ; ) {
|
|
1807
|
+
const candidate = join2(dir, ".git");
|
|
1808
|
+
if (existsSync2(candidate)) {
|
|
1809
|
+
gitPath = candidate;
|
|
1810
|
+
break;
|
|
1811
|
+
}
|
|
1812
|
+
const parent = dirname2(dir);
|
|
1813
|
+
if (parent === dir) break;
|
|
1814
|
+
dir = parent;
|
|
1815
|
+
}
|
|
1816
|
+
if (!gitPath || statSync(gitPath).isDirectory()) return lane;
|
|
1817
|
+
const gitFile = readFileSync2(gitPath, "utf8");
|
|
1818
|
+
const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
|
|
1819
|
+
if (!common) return lane;
|
|
1820
|
+
const worktreesDir = join2(common[1], "worktrees");
|
|
1821
|
+
const siblings = readdirSync(worktreesDir).filter((n) => {
|
|
1822
|
+
try {
|
|
1823
|
+
return statSync(join2(worktreesDir, n)).isDirectory();
|
|
1824
|
+
} catch {
|
|
1825
|
+
return false;
|
|
1826
|
+
}
|
|
1827
|
+
});
|
|
1828
|
+
return laneWithWorktreeSuffix(lane, gitFile, siblings);
|
|
1829
|
+
} catch {
|
|
1830
|
+
return lane;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
function laneWithWorktreeSuffix(lane, gitFile, siblings) {
|
|
1834
|
+
const m = gitFile.trim().match(/\/worktrees\/([^/\s]+)\s*$/);
|
|
1835
|
+
if (!m) return lane;
|
|
1836
|
+
const idx = [...siblings].sort().indexOf(m[1]);
|
|
1837
|
+
return idx < 0 ? lane : `${lane}-${idx + 2}`;
|
|
1838
|
+
}
|
|
1748
1839
|
function parseSem(text) {
|
|
1749
1840
|
const out = {};
|
|
1750
1841
|
for (const raw of text.split("\n")) {
|
|
@@ -2036,8 +2127,9 @@ function resolveLane(flagLane, cwd) {
|
|
|
2036
2127
|
const env = process.env.SECHROOM_LANE;
|
|
2037
2128
|
if (env) return env;
|
|
2038
2129
|
const start = cwd ?? process.cwd();
|
|
2039
|
-
const
|
|
2040
|
-
return
|
|
2130
|
+
const base = readSem(resolveSemPathForRead(start))?.values["code-lane"];
|
|
2131
|
+
if (!base) return void 0;
|
|
2132
|
+
return applyWorktreeLaneSuffix(base, start);
|
|
2041
2133
|
}
|
|
2042
2134
|
var INTENT_FILE = join4(".sechroom", "continuity.json");
|
|
2043
2135
|
function resolveIntentPath(start) {
|
|
@@ -2805,9 +2897,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
2805
2897
|
}
|
|
2806
2898
|
|
|
2807
2899
|
// src/setup/skills-offer.ts
|
|
2808
|
-
import { mkdirSync as
|
|
2809
|
-
import {
|
|
2810
|
-
import { join as join5 } from "path";
|
|
2900
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
2901
|
+
import { join as join6 } from "path";
|
|
2811
2902
|
|
|
2812
2903
|
// src/setup/lane-pin.ts
|
|
2813
2904
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -2889,57 +2980,157 @@ I can pin this checkout's lane so operator skills + the continuity hook resolve
|
|
|
2889
2980
|
writePin(code || void 0, design || void 0);
|
|
2890
2981
|
}
|
|
2891
2982
|
|
|
2892
|
-
// src/setup/
|
|
2893
|
-
var
|
|
2983
|
+
// src/setup/skill-resolution.ts
|
|
2984
|
+
var SYSTEM_WORKSPACE_ID = "wsp_system";
|
|
2985
|
+
var SKILL_ROLE_TAG = "sechroom:role:skill-template";
|
|
2986
|
+
var SKILL_NAME_PREFIX = "skill:";
|
|
2987
|
+
var AGENT_ROLE_TAG = "sechroom:role:agent-template";
|
|
2988
|
+
var AGENT_NAME_PREFIX = "agent:";
|
|
2989
|
+
function tagsOf(row) {
|
|
2990
|
+
const m = row?.item ?? row;
|
|
2991
|
+
return m?.tags ?? m?.Tags ?? [];
|
|
2992
|
+
}
|
|
2894
2993
|
function tagValue(tags, prefix) {
|
|
2895
2994
|
return tags.find((t) => t.startsWith(prefix))?.slice(prefix.length);
|
|
2896
2995
|
}
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2996
|
+
function bodyOf(row) {
|
|
2997
|
+
const m = row?.item ?? row;
|
|
2998
|
+
return m?.text ?? m?.Text ?? "";
|
|
2999
|
+
}
|
|
3000
|
+
function entriesFromRows(rows, surface, source, roleTag, namePrefix) {
|
|
3001
|
+
const out = /* @__PURE__ */ new Map();
|
|
3002
|
+
for (const row of rows ?? []) {
|
|
3003
|
+
const tags = tagsOf(row);
|
|
3004
|
+
if (!tags.includes(roleTag)) continue;
|
|
3005
|
+
if (tagValue(tags, "target:") !== surface) continue;
|
|
3006
|
+
const name = tagValue(tags, namePrefix);
|
|
3007
|
+
if (!name) continue;
|
|
3008
|
+
out.set(name, { name, body: bodyOf(row), source });
|
|
3009
|
+
}
|
|
3010
|
+
return out;
|
|
3011
|
+
}
|
|
3012
|
+
function resolveByRole(systemRows, personalRows, surface, roleTag, namePrefix) {
|
|
3013
|
+
const merged = entriesFromRows(systemRows, surface, "system", roleTag, namePrefix);
|
|
3014
|
+
for (const [name, item] of entriesFromRows(personalRows, surface, "personal", roleTag, namePrefix)) {
|
|
3015
|
+
merged.set(name, item);
|
|
3016
|
+
}
|
|
3017
|
+
return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
3018
|
+
}
|
|
3019
|
+
function resolveSkills(systemRows, personalRows, surface) {
|
|
3020
|
+
return resolveByRole(systemRows, personalRows, surface, SKILL_ROLE_TAG, SKILL_NAME_PREFIX);
|
|
3021
|
+
}
|
|
3022
|
+
function resolveAgents(systemRows, personalRows, surface) {
|
|
3023
|
+
return resolveByRole(systemRows, personalRows, surface, AGENT_ROLE_TAG, AGENT_NAME_PREFIX);
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
// src/setup/skills-lock.ts
|
|
3027
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
3028
|
+
import { homedir as homedir5 } from "os";
|
|
3029
|
+
import { join as join5 } from "path";
|
|
3030
|
+
var SKILLS_LOCK = ".sechroom-skills.json";
|
|
3031
|
+
var DEFAULT_SKILLS_SLUG = "operator-skills";
|
|
3032
|
+
function skillsDir(global) {
|
|
3033
|
+
return global ? join5(homedir5(), ".claude", "skills") : join5(process.cwd(), ".claude", "skills");
|
|
3034
|
+
}
|
|
3035
|
+
function agentsDir(global) {
|
|
3036
|
+
return global ? join5(homedir5(), ".claude", "agents") : join5(process.cwd(), ".claude", "agents");
|
|
3037
|
+
}
|
|
3038
|
+
function readSkillsLock(dir) {
|
|
3039
|
+
const lockPath = join5(dir, SKILLS_LOCK);
|
|
3040
|
+
if (!existsSync6(lockPath)) return {};
|
|
3041
|
+
try {
|
|
3042
|
+
return JSON.parse(readFileSync5(lockPath, "utf8"));
|
|
3043
|
+
} catch {
|
|
3044
|
+
return {};
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
function writeSkillsLock(dir, lock) {
|
|
3048
|
+
mkdirSync5(dir, { recursive: true });
|
|
3049
|
+
writeFileSync5(join5(dir, SKILLS_LOCK), JSON.stringify(lock, null, 2) + "\n");
|
|
3050
|
+
}
|
|
3051
|
+
function recordMaterialisedSkills(dir, slug, skills, meta = {}) {
|
|
3052
|
+
const lock = readSkillsLock(dir);
|
|
3053
|
+
lock[slug] = { surface: meta.surface, skills: [...skills].sort() };
|
|
3054
|
+
writeSkillsLock(dir, lock);
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
// src/setup/skills-offer.ts
|
|
3058
|
+
async function fetchFeedRows(cfg, workspaceId) {
|
|
2901
3059
|
try {
|
|
2902
3060
|
const client = await makeClient(cfg);
|
|
2903
3061
|
const feed = await client.GET("/workspaces/{workspaceId}/memories/feed", {
|
|
2904
3062
|
params: {
|
|
2905
|
-
path: { workspaceId
|
|
3063
|
+
path: { workspaceId },
|
|
3064
|
+
// cascadeWorkspaces: skills land in an "Operator Skills" SUB-workspace;
|
|
3065
|
+
// includeText: the feed omits bodies by default, we need them for SKILL.md.
|
|
2906
3066
|
query: { limit: 200, cascadeWorkspaces: true, includeText: true }
|
|
2907
3067
|
}
|
|
2908
3068
|
}).then((r) => r.data).catch(() => void 0);
|
|
2909
|
-
|
|
3069
|
+
return feed?.results ?? feed?.Results ?? [];
|
|
2910
3070
|
} catch {
|
|
2911
|
-
return;
|
|
3071
|
+
return [];
|
|
2912
3072
|
}
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
3073
|
+
}
|
|
3074
|
+
async function maybeOfferSkills(cfg, personalWorkspaceId, opts) {
|
|
3075
|
+
const surface = opts.surface ?? "claude-code";
|
|
3076
|
+
const [systemRows, personalRows] = await Promise.all([
|
|
3077
|
+
fetchFeedRows(cfg, SYSTEM_WORKSPACE_ID),
|
|
3078
|
+
personalWorkspaceId ? fetchFeedRows(cfg, personalWorkspaceId) : Promise.resolve([])
|
|
3079
|
+
]);
|
|
3080
|
+
const skills = resolveSkills(systemRows, personalRows, surface);
|
|
3081
|
+
const agents = resolveAgents(systemRows, personalRows, surface);
|
|
3082
|
+
if (skills.length === 0 && agents.length === 0) return;
|
|
3083
|
+
const sDir = skillsDir(true);
|
|
3084
|
+
const aDir = agentsDir(true);
|
|
3085
|
+
if (opts.dryRun) {
|
|
3086
|
+
const lines = (label, items) => items.length === 0 ? "" : `
|
|
3087
|
+
Would materialise ${style.bold(String(items.length))} ${label} for ${surface}:
|
|
3088
|
+
` + items.map((s) => ` ${s.name} ${style.dim(`[${s.source}]`)}`).join("\n") + "\n";
|
|
3089
|
+
process.stderr.write(lines("operator skill(s)", skills) + lines("agent(s)", agents));
|
|
3090
|
+
return;
|
|
2922
3091
|
}
|
|
2923
|
-
const
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
3092
|
+
const summary = [
|
|
3093
|
+
skills.length > 0 ? `${style.bold(String(skills.length))} skill(s)` : "",
|
|
3094
|
+
agents.length > 0 ? `${style.bold(String(agents.length))} agent(s)` : ""
|
|
3095
|
+
].filter(Boolean).join(" + ");
|
|
3096
|
+
process.stderr.write(`
|
|
3097
|
+
Found ${summary} available to you for ${surface}.
|
|
3098
|
+
`);
|
|
3099
|
+
if (skills.length > 0) process.stderr.write(` skills: ${skills.map((s) => s.name).join(", ")}
|
|
3100
|
+
`);
|
|
3101
|
+
if (agents.length > 0) process.stderr.write(` agents: ${agents.map((a) => a.name).join(", ")}
|
|
3102
|
+
`);
|
|
3103
|
+
const dest = [skills.length > 0 ? `${sDir}/` : "", agents.length > 0 ? `${aDir}/` : ""].filter(Boolean).join(" + ");
|
|
3104
|
+
const materialise = opts.yes ? true : canPrompt() ? await promptYesNo(`Write them to ${dest} so ${surface} can use them?`) : false;
|
|
2932
3105
|
if (!materialise) return;
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
const
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
3106
|
+
if (skills.length > 0) {
|
|
3107
|
+
const written = [];
|
|
3108
|
+
for (const s of skills) {
|
|
3109
|
+
mkdirSync6(join6(sDir, s.name), { recursive: true });
|
|
3110
|
+
writeFileSync6(join6(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
3111
|
+
written.push(s.name);
|
|
3112
|
+
}
|
|
3113
|
+
recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
3114
|
+
process.stderr.write(`${style.green("\u2713")} wrote ${written.length} skill(s) to ${sDir}
|
|
3115
|
+
`);
|
|
2939
3116
|
}
|
|
2940
|
-
|
|
3117
|
+
if (agents.length > 0) {
|
|
3118
|
+
mkdirSync6(aDir, { recursive: true });
|
|
3119
|
+
const written = [];
|
|
3120
|
+
for (const a of agents) {
|
|
3121
|
+
const file = `${a.name}.md`;
|
|
3122
|
+
writeFileSync6(join6(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
3123
|
+
written.push(file);
|
|
3124
|
+
}
|
|
3125
|
+
recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
3126
|
+
process.stderr.write(`${style.green("\u2713")} wrote ${written.length} agent(s) to ${aDir}
|
|
2941
3127
|
`);
|
|
2942
|
-
|
|
3128
|
+
}
|
|
3129
|
+
await ensureLanePin(cfg, {
|
|
3130
|
+
yes: opts.yes,
|
|
3131
|
+
dryRun: opts.dryRun,
|
|
3132
|
+
clients: [surface]
|
|
3133
|
+
});
|
|
2943
3134
|
}
|
|
2944
3135
|
|
|
2945
3136
|
// src/commands/setup.ts
|
|
@@ -3090,13 +3281,13 @@ async function runClients(clients, cmd, opts) {
|
|
|
3090
3281
|
}
|
|
3091
3282
|
|
|
3092
3283
|
// src/commands/onboard.ts
|
|
3093
|
-
import { existsSync as
|
|
3094
|
-
import { join as
|
|
3284
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3285
|
+
import { basename as basename3, join as join8 } from "path";
|
|
3095
3286
|
|
|
3096
3287
|
// src/commands/fanout.ts
|
|
3097
3288
|
import { spawnSync } from "child_process";
|
|
3098
|
-
import { existsSync as
|
|
3099
|
-
import { isAbsolute, join as
|
|
3289
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
3290
|
+
import { isAbsolute, join as join7, resolve } from "path";
|
|
3100
3291
|
var ICON = {
|
|
3101
3292
|
refresh: "\u21BB",
|
|
3102
3293
|
bind: "+",
|
|
@@ -3109,28 +3300,28 @@ function resolveChildDir(path, root) {
|
|
|
3109
3300
|
function discoverChildren(root) {
|
|
3110
3301
|
let names;
|
|
3111
3302
|
try {
|
|
3112
|
-
names =
|
|
3303
|
+
names = readdirSync2(root);
|
|
3113
3304
|
} catch {
|
|
3114
3305
|
return [];
|
|
3115
3306
|
}
|
|
3116
3307
|
const out = [];
|
|
3117
3308
|
for (const name of names.sort()) {
|
|
3118
3309
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
3119
|
-
const dir =
|
|
3310
|
+
const dir = join7(root, name);
|
|
3120
3311
|
try {
|
|
3121
|
-
if (!
|
|
3312
|
+
if (!statSync2(dir).isDirectory()) continue;
|
|
3122
3313
|
} catch {
|
|
3123
3314
|
continue;
|
|
3124
3315
|
}
|
|
3125
|
-
if (
|
|
3316
|
+
if (existsSync7(join7(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
3126
3317
|
}
|
|
3127
3318
|
return out;
|
|
3128
3319
|
}
|
|
3129
3320
|
function readManifest(path) {
|
|
3130
|
-
if (!
|
|
3321
|
+
if (!existsSync7(path)) return null;
|
|
3131
3322
|
let parsed;
|
|
3132
3323
|
try {
|
|
3133
|
-
parsed = JSON.parse(
|
|
3324
|
+
parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
3134
3325
|
} catch (err2) {
|
|
3135
3326
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
3136
3327
|
}
|
|
@@ -3268,7 +3459,39 @@ async function warnIfProjectStray(client, projectId, workspaceId, json) {
|
|
|
3268
3459
|
);
|
|
3269
3460
|
}
|
|
3270
3461
|
}
|
|
3271
|
-
async function
|
|
3462
|
+
async function fetchPersonalWorkspaceId(client) {
|
|
3463
|
+
try {
|
|
3464
|
+
const { data } = await client.GET("/me/personal-workspace", {});
|
|
3465
|
+
return data?.workspaceId ?? null;
|
|
3466
|
+
} catch {
|
|
3467
|
+
return null;
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
function nameTokens(s) {
|
|
3471
|
+
return s.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2);
|
|
3472
|
+
}
|
|
3473
|
+
function personalSubtreeIds(personalId, all) {
|
|
3474
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
3475
|
+
for (const w of all) {
|
|
3476
|
+
if (!w.parentId) continue;
|
|
3477
|
+
(childrenOf.get(w.parentId) ?? childrenOf.set(w.parentId, []).get(w.parentId)).push(w);
|
|
3478
|
+
}
|
|
3479
|
+
const ids = /* @__PURE__ */ new Set([personalId]);
|
|
3480
|
+
const queue = [personalId];
|
|
3481
|
+
while (queue.length > 0) {
|
|
3482
|
+
const id = queue.shift();
|
|
3483
|
+
for (const child of childrenOf.get(id) ?? []) {
|
|
3484
|
+
if (!ids.has(child.id)) {
|
|
3485
|
+
ids.add(child.id);
|
|
3486
|
+
queue.push(child.id);
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
return ids;
|
|
3491
|
+
}
|
|
3492
|
+
async function pickWorkspace(client, opts = {}) {
|
|
3493
|
+
const promptLabel = opts.promptLabel ?? "Bind this directory to a workspace:";
|
|
3494
|
+
const dirName = opts.dirName ?? basename3(process.cwd());
|
|
3272
3495
|
const all = await withSpinner("Listing your workspaces", () => fetchWorkspaces(client));
|
|
3273
3496
|
if (all.length === 0) {
|
|
3274
3497
|
process.stderr.write(`no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
|
|
@@ -3276,22 +3499,34 @@ async function pickWorkspace(client, promptLabel = "Bind this directory to a wor
|
|
|
3276
3499
|
return void 0;
|
|
3277
3500
|
}
|
|
3278
3501
|
const byId = new Map(all.map((w) => [w.id, w]));
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3502
|
+
const personalId = await fetchPersonalWorkspaceId(client);
|
|
3503
|
+
const excluded = personalId ? personalSubtreeIds(personalId, all) : /* @__PURE__ */ new Set();
|
|
3504
|
+
let candidates = all.filter((w) => !excluded.has(w.id));
|
|
3505
|
+
if (candidates.length === 0) candidates = all;
|
|
3506
|
+
const dirToks = new Set(nameTokens(dirName));
|
|
3507
|
+
const isMatch = (w) => nameTokens(w.name).some((t) => dirToks.has(t));
|
|
3508
|
+
const suggestions = candidates.filter(isMatch);
|
|
3509
|
+
let pool = candidates;
|
|
3510
|
+
if (candidates.length > 12 && suggestions.length === 0) {
|
|
3511
|
+
const q = (await promptText(`Filter ${candidates.length} workspaces (substring, Enter to list all)?`, "")).trim().toLowerCase();
|
|
3282
3512
|
if (q) {
|
|
3283
|
-
const hits =
|
|
3513
|
+
const hits = candidates.filter((w) => `${w.name} ${workspacePath(w, byId)}`.toLowerCase().includes(q));
|
|
3284
3514
|
if (hits.length > 0) pool = hits;
|
|
3285
3515
|
else process.stderr.write(`no match for "${q}" \u2014 listing all
|
|
3286
3516
|
`);
|
|
3287
3517
|
}
|
|
3288
3518
|
}
|
|
3289
3519
|
const SKIP = "__skip__";
|
|
3520
|
+
const byPath = (a, b) => workspacePath(a, byId).localeCompare(workspacePath(b, byId));
|
|
3521
|
+
const matched = pool.filter(isMatch).sort(byPath);
|
|
3522
|
+
const rest = pool.filter((w) => !isMatch(w)).sort(byPath);
|
|
3290
3523
|
const choices = [
|
|
3291
|
-
...
|
|
3524
|
+
...matched.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: style.dim(`matches "${dirName}"`) })),
|
|
3525
|
+
...rest.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: w.id })),
|
|
3292
3526
|
{ label: style.dim("skip \u2014 don't bind a workspace"), value: SKIP, hint: void 0 }
|
|
3293
3527
|
];
|
|
3294
|
-
const
|
|
3528
|
+
const defaultValue = matched.length === 1 ? matched[0].id : SKIP;
|
|
3529
|
+
const chosen = await promptSelect(promptLabel, choices, defaultValue);
|
|
3295
3530
|
if (chosen === SKIP) return void 0;
|
|
3296
3531
|
const picked = byId.get(chosen);
|
|
3297
3532
|
const collisions = all.filter((w) => w.id !== picked.id && namesCollide(w.name, picked.name));
|
|
@@ -3318,7 +3553,7 @@ async function resolveWorkspaceBinding(client, existing, opts) {
|
|
|
3318
3553
|
}
|
|
3319
3554
|
if (existing) return existing;
|
|
3320
3555
|
if (!canPrompt() || opts.yes) return void 0;
|
|
3321
|
-
return pickWorkspace(client);
|
|
3556
|
+
return pickWorkspace(client, { dirName: basename3(process.cwd()) });
|
|
3322
3557
|
}
|
|
3323
3558
|
async function ensureTenant(baseUrl, g, opts) {
|
|
3324
3559
|
const persisted = readPersisted();
|
|
@@ -3382,7 +3617,7 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
3382
3617
|
if (opts.persist !== false) {
|
|
3383
3618
|
const patch = { baseUrl, tenant, ...workspaceId ? { workspaceId } : {} };
|
|
3384
3619
|
if (storeLocal) {
|
|
3385
|
-
const path = writeLocalConfig(patch);
|
|
3620
|
+
const path = writeLocalConfig(patch, { here: Boolean(opts.here) });
|
|
3386
3621
|
if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved to ${path} (directory-local)
|
|
3387
3622
|
`);
|
|
3388
3623
|
} else {
|
|
@@ -3451,10 +3686,10 @@ async function chooseClients(clientFlag, yes, cwd) {
|
|
|
3451
3686
|
}
|
|
3452
3687
|
async function planRecurseChild(entry, root, client, opts) {
|
|
3453
3688
|
const dir = resolveChildDir(entry.path, root);
|
|
3454
|
-
if (!
|
|
3689
|
+
if (!existsSync8(dir)) {
|
|
3455
3690
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
3456
3691
|
}
|
|
3457
|
-
if (
|
|
3692
|
+
if (existsSync8(join8(dir, ".sechroom.json"))) {
|
|
3458
3693
|
return {
|
|
3459
3694
|
label: entry.path,
|
|
3460
3695
|
dir,
|
|
@@ -3481,7 +3716,10 @@ async function planRecurseChild(entry, root, client, opts) {
|
|
|
3481
3716
|
process.stderr.write(`
|
|
3482
3717
|
${style.bold(entry.path)} ${style.dim("is not bound yet.")}
|
|
3483
3718
|
`);
|
|
3484
|
-
const ws = await pickWorkspace(client,
|
|
3719
|
+
const ws = await pickWorkspace(client, {
|
|
3720
|
+
promptLabel: `Bind ${style.cyan(entry.path)} to a workspace:`,
|
|
3721
|
+
dirName: basename3(entry.path)
|
|
3722
|
+
});
|
|
3485
3723
|
if (!ws) {
|
|
3486
3724
|
return { label: entry.path, dir, disposition: "skip-unbound", argv: [], reason: "unbound \u2014 no workspace chosen (skipped)" };
|
|
3487
3725
|
}
|
|
@@ -3524,7 +3762,7 @@ This fan-out will pin the same lane in every repo:
|
|
|
3524
3762
|
async function runRecurse(cfg, g, opts) {
|
|
3525
3763
|
const { yes, dryRun, json } = opts;
|
|
3526
3764
|
const root = process.cwd();
|
|
3527
|
-
const manifestPath =
|
|
3765
|
+
const manifestPath = join8(root, ".sechroom", "repos.json");
|
|
3528
3766
|
const fromManifest = readManifest(manifestPath);
|
|
3529
3767
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
3530
3768
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
@@ -3552,7 +3790,7 @@ async function runRecurse(cfg, g, opts) {
|
|
|
3552
3790
|
summarizeFanout(results, { dryRun });
|
|
3553
3791
|
}
|
|
3554
3792
|
function registerOnboard(program2) {
|
|
3555
|
-
program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--recurse", "orchestration-root mode: onboard every child repo under this dir (auto-discovered, or from ./.sechroom/repos.json) \u2014 refreshes bound repos, prompts a workspace per new one", false).option("--lane <id>", "set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo").option("--design-lane <id>", "set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child").option("--client <list>", `comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`).option("--local", "save the binding (tenant + base URL + workspace) to a committed .sechroom.json in this repo instead of the global config", false).option("--workspace <id>", "bind this directory to a workspace (skips the interactive workspace pick)").option("--cli-only", "configure the CLI only \u2014 don't wire any AI client (no MCP config, no agent files)", false).option("--no-mcp", "skip the MCP server config (.mcp.json etc.); still write the agent instruction files").option("--copy", "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)").option("--dry-run", "walk through without writing files or changing the profile", false).option("--refresh", "re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)", false).option("--force", "rewrite every managed block, overwriting local edits inside the markers (content outside untouched)", false).option("--check", "report whether anything would change and exit (0 = all current, 1 = stale/drift/absent); writes nothing", false).option("-y, --yes", "non-interactive: accept defaults (system timezone, detected clients, global config, full wire)", false).addHelpText(
|
|
3793
|
+
program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--recurse", "orchestration-root mode: onboard every child repo under this dir (auto-discovered, or from ./.sechroom/repos.json) \u2014 refreshes bound repos, prompts a workspace per new one", false).option("--lane <id>", "set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo").option("--design-lane <id>", "set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child").option("--client <list>", `comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`).option("--local", "save the binding (tenant + base URL + workspace) to a committed .sechroom.json in this repo instead of the global config", false).option("--here", "with --local: write the binding at THIS directory even when a parent already carries one \u2014 binds a subtree (e.g. a monorepo's frontend/) to its own workspace", false).option("--workspace <id>", "bind this directory to a workspace (skips the interactive workspace pick)").option("--cli-only", "configure the CLI only \u2014 don't wire any AI client (no MCP config, no agent files)", false).option("--no-mcp", "skip the MCP server config (.mcp.json etc.); still write the agent instruction files").option("--copy", "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)").option("--dry-run", "walk through without writing files or changing the profile", false).option("--refresh", "re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)", false).option("--force", "rewrite every managed block, overwriting local edits inside the markers (content outside untouched)", false).option("--check", "report whether anything would change and exit (0 = all current, 1 = stale/drift/absent); writes nothing", false).option("-y, --yes", "non-interactive: accept defaults (system timezone, detected clients, global config, full wire)", false).addHelpText(
|
|
3556
3794
|
"after",
|
|
3557
3795
|
`
|
|
3558
3796
|
Examples:
|
|
@@ -3708,14 +3946,21 @@ async function chooseWire(opts, yes) {
|
|
|
3708
3946
|
return opts.mcp === false ? "agent-only" : "full";
|
|
3709
3947
|
}
|
|
3710
3948
|
var FALLBACK_AGENT_PROMPT = "Resume my sechroom continuity, summarise what I was last working on, then suggest the next step.";
|
|
3949
|
+
function printNextStepBlock(heading, lines) {
|
|
3950
|
+
const rule = style.dim("\u2500".repeat(52));
|
|
3951
|
+
process.stdout.write(
|
|
3952
|
+
`
|
|
3953
|
+
${rule}
|
|
3954
|
+
${style.bold(heading)}
|
|
3955
|
+
|
|
3956
|
+
` + lines.map((l) => ` ${l}`).join("\n") + `
|
|
3957
|
+
${rule}
|
|
3958
|
+
`
|
|
3959
|
+
);
|
|
3960
|
+
}
|
|
3711
3961
|
async function printStarterPrompt(mode, cfg) {
|
|
3712
3962
|
if (mode === "cli") {
|
|
3713
|
-
|
|
3714
|
-
`
|
|
3715
|
-
${style.bold("Next:")} pick up where you left off \u2014
|
|
3716
|
-
${style.cyan("sechroom continuity resume-me")}
|
|
3717
|
-
`
|
|
3718
|
-
);
|
|
3963
|
+
printNextStepBlock("Next \u2014 pick up where you left off:", [style.cyan("sechroom continuity resume-me")]);
|
|
3719
3964
|
return;
|
|
3720
3965
|
}
|
|
3721
3966
|
let primary = FALLBACK_AGENT_PROMPT;
|
|
@@ -3727,21 +3972,16 @@ ${style.bold("Next:")} pick up where you left off \u2014
|
|
|
3727
3972
|
} catch {
|
|
3728
3973
|
}
|
|
3729
3974
|
}
|
|
3730
|
-
|
|
3731
|
-
`
|
|
3732
|
-
${style.bold("Next:")} paste this into your AI agent to get going \u2014
|
|
3733
|
-
${style.cyan(`"${primary}"`)}
|
|
3734
|
-
`
|
|
3735
|
-
);
|
|
3975
|
+
printNextStepBlock("Next \u2014 paste this into your AI agent to get going:", [style.cyan(`"${primary}"`)]);
|
|
3736
3976
|
}
|
|
3737
3977
|
|
|
3738
3978
|
// src/commands/sweep.ts
|
|
3739
|
-
import { existsSync as
|
|
3740
|
-
import { dirname as dirname6, join as
|
|
3741
|
-
var DEFAULT_MANIFEST =
|
|
3979
|
+
import { existsSync as existsSync9 } from "fs";
|
|
3980
|
+
import { dirname as dirname6, join as join9, resolve as resolve2 } from "path";
|
|
3981
|
+
var DEFAULT_MANIFEST = join9(".sechroom", "repos.json");
|
|
3742
3982
|
function planEntry(entry, root) {
|
|
3743
3983
|
const dir = resolveChildDir(entry.path, root);
|
|
3744
|
-
if (!
|
|
3984
|
+
if (!existsSync9(dir)) {
|
|
3745
3985
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
3746
3986
|
}
|
|
3747
3987
|
if (committedBindingPath(dir)) {
|
|
@@ -3835,106 +4075,78 @@ Examples:
|
|
|
3835
4075
|
}
|
|
3836
4076
|
|
|
3837
4077
|
// src/commands/skills.ts
|
|
3838
|
-
import {
|
|
3839
|
-
import {
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
var
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
}
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
4078
|
+
import { join as join10 } from "path";
|
|
4079
|
+
import { existsSync as existsSync10, rmSync as rmSync2 } from "fs";
|
|
4080
|
+
|
|
4081
|
+
// src/commands/lane.ts
|
|
4082
|
+
var LANE_KEYS = ["code-lane", "design-lane"];
|
|
4083
|
+
function showLane(json) {
|
|
4084
|
+
const found = readSem();
|
|
4085
|
+
if (!found) {
|
|
4086
|
+
if (json) return emit({ path: null, values: {} }, true);
|
|
4087
|
+
return console.log(
|
|
4088
|
+
style.dim(`No ./.sechroom/lane.json pin in this checkout. Run 'sechroom lane set'.`)
|
|
4089
|
+
);
|
|
4090
|
+
}
|
|
4091
|
+
const resolved = { ...found.values };
|
|
4092
|
+
let suffixed = false;
|
|
4093
|
+
for (const k of LANE_KEYS) {
|
|
4094
|
+
const v = found.values[k];
|
|
4095
|
+
if (!v) continue;
|
|
4096
|
+
resolved[k] = applyWorktreeLaneSuffix(v);
|
|
4097
|
+
if (resolved[k] !== v) suffixed = true;
|
|
4098
|
+
}
|
|
4099
|
+
if (json) return emit({ path: found.path, values: resolved, worktreeSuffixApplied: suffixed }, true);
|
|
4100
|
+
console.log(style.dim(`from ${found.path}`));
|
|
4101
|
+
Object.entries(resolved).forEach(([k, v]) => console.log(" " + style.bold(k) + " = " + v));
|
|
4102
|
+
if (suffixed) console.log(style.dim(" (worktree -N suffix applied \u2014 non-primary git worktree)"));
|
|
4103
|
+
}
|
|
4104
|
+
function setLane(opts) {
|
|
4105
|
+
if (!opts.codeLane && !opts.designLane) fail("Provide --code-lane and/or --design-lane.");
|
|
4106
|
+
const target = localSemPath();
|
|
4107
|
+
const values = readLocalSemValues();
|
|
4108
|
+
if (opts.codeLane) values["code-lane"] = opts.codeLane;
|
|
4109
|
+
if (opts.designLane) values["design-lane"] = opts.designLane;
|
|
4110
|
+
writeSem(values, target);
|
|
4111
|
+
if (opts.json) return emit({ path: target, values }, true);
|
|
4112
|
+
console.log(style.green(`Wrote lane pin \u2192 ${target} ${style.dim("(git-ignored)")}`));
|
|
4113
|
+
Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
|
|
4114
|
+
}
|
|
4115
|
+
function registerLane(program2) {
|
|
4116
|
+
const lane = program2.command("lane").description("Show this checkout's continuity lane pin (worktree-aware -N suffix applied)").option("--json", "machine output").action((opts, cmd) => showLane(Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)));
|
|
4117
|
+
lane.command("set").description("Write this checkout's lane pin to ./.sechroom/lane.json").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action(
|
|
4118
|
+
(opts, cmd) => setLane({
|
|
4119
|
+
codeLane: opts.codeLane,
|
|
4120
|
+
designLane: opts.designLane,
|
|
4121
|
+
json: Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)
|
|
4122
|
+
})
|
|
4123
|
+
);
|
|
4124
|
+
lane.addHelpText(
|
|
4125
|
+
"after",
|
|
4126
|
+
`
|
|
4127
|
+
Examples:
|
|
4128
|
+
$ sechroom lane show the resolved lane(s)
|
|
4129
|
+
$ sechroom lane set --code-lane claude-code-chris --design-lane claude-design-chris
|
|
4130
|
+
|
|
4131
|
+
In a non-primary git worktree the ratified concurrent-session -N suffix is auto-applied (SBC-1094).
|
|
4132
|
+
(Aliases: 'sechroom skills lane' / 'skills set-lane' \u2014 kept for back-compat.)`
|
|
4133
|
+
);
|
|
3852
4134
|
}
|
|
4135
|
+
|
|
4136
|
+
// src/commands/skills.ts
|
|
3853
4137
|
function registerSkills(program2) {
|
|
3854
|
-
const skills = program2.command("skills").description("
|
|
4138
|
+
const skills = program2.command("skills").description("Manage operator skills (materialised by `onboard`)");
|
|
3855
4139
|
skills.addHelpText(
|
|
3856
4140
|
"after",
|
|
3857
4141
|
`
|
|
3858
4142
|
Examples:
|
|
3859
|
-
$ sechroom skills install --code-lane claude-code-chris --design-lane claude-design-chris
|
|
3860
|
-
$ sechroom skills install operator-skills --surface claude-code --local
|
|
3861
4143
|
$ sechroom skills list
|
|
3862
4144
|
$ sechroom skills set-lane --code-lane claude-code-chris --design-lane claude-design-chris
|
|
3863
4145
|
$ sechroom skills lane
|
|
3864
|
-
$ sechroom skills clean
|
|
4146
|
+
$ sechroom skills clean
|
|
4147
|
+
|
|
4148
|
+
To install/refresh skills, run 'sechroom onboard' (it offers to materialise them).`
|
|
3865
4149
|
);
|
|
3866
|
-
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) => {
|
|
3867
|
-
const slug = slugArg || DEFAULT_SLUG;
|
|
3868
|
-
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
3869
|
-
const pw = await runApi("resolving personal workspace", () => client.GET("/me/personal-workspace", {}));
|
|
3870
|
-
const personalWsId = pw?.id || pw?.workspaceId || pw?.personalWorkspaceId || pw?.item?.id;
|
|
3871
|
-
if (!personalWsId) fail("Could not resolve your personal workspace.");
|
|
3872
|
-
let version = opts.version;
|
|
3873
|
-
if (!version) {
|
|
3874
|
-
const cat = await runApi("reading the bundle catalogue", () => client.GET("/me/bundles", {}));
|
|
3875
|
-
const item = (cat?.bundles ?? cat?.Bundles ?? []).find((b) => (b.slug ?? b.Slug) === slug);
|
|
3876
|
-
if (!item) fail(`Bundle '${slug}' is not in your self-serve catalogue (must be UserInstallable + Published).`);
|
|
3877
|
-
version = item.latestVersion ?? item.LatestVersion;
|
|
3878
|
-
if (!version) fail(`Bundle '${slug}' has no installable (Published) version.`);
|
|
3879
|
-
}
|
|
3880
|
-
const installOptions = {};
|
|
3881
|
-
if (opts.codeLane) installOptions["identity.code-lane"] = opts.codeLane;
|
|
3882
|
-
if (opts.designLane) installOptions["identity.design-lane"] = opts.designLane;
|
|
3883
|
-
const res = await runApi(
|
|
3884
|
-
`installing ${slug}@${version}${opts.instance ? ` (${opts.instance})` : ""}`,
|
|
3885
|
-
() => client.POST("/me/bundles/{slug}/versions/{version}/install", {
|
|
3886
|
-
params: { path: { slug, version } },
|
|
3887
|
-
// instance: null/absent = the default instance (reinstall updates in
|
|
3888
|
-
// place); a name installs a separate instance.
|
|
3889
|
-
body: { installOptions, instance: opts.instance ?? null }
|
|
3890
|
-
})
|
|
3891
|
-
);
|
|
3892
|
-
const status = String(res?.status ?? res?.Status ?? "");
|
|
3893
|
-
if (status && status.toLowerCase() !== "completed") {
|
|
3894
|
-
fail(`Install did not complete (status=${status}; ${res?.failureReason ?? res?.FailureReason ?? ""}).`);
|
|
3895
|
-
}
|
|
3896
|
-
const feed = await runApi(
|
|
3897
|
-
"materialising skill files",
|
|
3898
|
-
() => client.GET("/workspaces/{workspaceId}/memories/feed", {
|
|
3899
|
-
// cascadeWorkspaces: skills land in an "Operator Skills" SUB-workspace of
|
|
3900
|
-
// the personal workspace, so we recurse from the personal-ws root.
|
|
3901
|
-
// includeText: the feed omits bodies by default; we need them for SKILL.md.
|
|
3902
|
-
params: {
|
|
3903
|
-
path: { workspaceId: personalWsId },
|
|
3904
|
-
query: { limit: 200, cascadeWorkspaces: true, includeText: true }
|
|
3905
|
-
}
|
|
3906
|
-
})
|
|
3907
|
-
);
|
|
3908
|
-
const rows = feed?.results ?? feed?.Results ?? [];
|
|
3909
|
-
const dir = skillsDir(!opts.local);
|
|
3910
|
-
const wantInstance = opts.instance || "default";
|
|
3911
|
-
const written = [];
|
|
3912
|
-
const bundleTagPrefix = `sechroom:bundle:${slug}@`;
|
|
3913
|
-
for (const r of rows) {
|
|
3914
|
-
const m = r.item ?? r;
|
|
3915
|
-
const tags = m.tags ?? m.Tags ?? [];
|
|
3916
|
-
if (!hasAny(tags, ROLE_TAGS)) continue;
|
|
3917
|
-
if (tagValue2(tags, "target:") !== opts.surface) continue;
|
|
3918
|
-
if (!tags.some((t) => t.startsWith(bundleTagPrefix))) continue;
|
|
3919
|
-
if ((tagValue2(tags, "sechroom:skill-instance:") ?? "default") !== wantInstance) continue;
|
|
3920
|
-
const name = tagValue2(tags, "skill:");
|
|
3921
|
-
if (!name) continue;
|
|
3922
|
-
const body = m.text ?? m.Text ?? "";
|
|
3923
|
-
mkdirSync6(join9(dir, name), { recursive: true });
|
|
3924
|
-
writeFileSync6(join9(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
|
|
3925
|
-
written.push(name);
|
|
3926
|
-
}
|
|
3927
|
-
mkdirSync6(dir, { recursive: true });
|
|
3928
|
-
const lockPath = join9(dir, LOCK);
|
|
3929
|
-
const lock = existsSync9(lockPath) ? JSON.parse(readFileSync6(lockPath, "utf8")) : {};
|
|
3930
|
-
lock[slug] = { surface: opts.surface, version, instance: wantInstance, skills: written.sort() };
|
|
3931
|
-
writeFileSync6(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
3932
|
-
if (opts.json) return emit({ slug, version, instance: wantInstance, surface: opts.surface, dir, installed: written }, true);
|
|
3933
|
-
const instanceNote = opts.instance ? ` (${opts.instance})` : "";
|
|
3934
|
-
console.log(style.green(`Installed ${slug}@${version}${instanceNote} \u2014 ${written.length} skill(s) \u2192 ${dir}`));
|
|
3935
|
-
written.forEach((n) => console.log(" " + style.dim("\u2022") + " " + n));
|
|
3936
|
-
if (written.length === 0) console.log(style.dim(` (no '${opts.surface}' skill bodies found; check --surface)`));
|
|
3937
|
-
});
|
|
3938
4150
|
skills.command("list").description("List your installed bundles (GET /me/bundle-installs)").option("--json", "machine output").action(async (opts, cmd) => {
|
|
3939
4151
|
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
3940
4152
|
const data = await runApi("reading your installs", () => client.GET("/me/bundle-installs", {}));
|
|
@@ -3947,49 +4159,33 @@ Examples:
|
|
|
3947
4159
|
console.log(` ${i.bundleSlug ?? i.BundleSlug}@${i.bundleVersion ?? i.BundleVersion ?? "?"}${tag}`);
|
|
3948
4160
|
});
|
|
3949
4161
|
});
|
|
3950
|
-
skills.command("clean [slug]").description(`Remove
|
|
3951
|
-
const slug = slugArg ||
|
|
4162
|
+
skills.command("clean [slug]").description(`Remove skill files materialised by onboard (default ${DEFAULT_SKILLS_SLUG})`).option("--local", "clean ./.claude/skills instead of ~/.claude/skills").option("--json", "machine output").action(async (slugArg, opts) => {
|
|
4163
|
+
const slug = slugArg || DEFAULT_SKILLS_SLUG;
|
|
3952
4164
|
const dir = skillsDir(!opts.local);
|
|
3953
|
-
const
|
|
3954
|
-
if (!existsSync9(lockPath)) fail(`No skills lockfile at ${lockPath}; nothing to clean.`);
|
|
3955
|
-
const lock = JSON.parse(readFileSync6(lockPath, "utf8"));
|
|
4165
|
+
const lock = readSkillsLock(dir);
|
|
3956
4166
|
const entry = lock[slug];
|
|
3957
|
-
if (!entry) fail(`No
|
|
4167
|
+
if (!entry) fail(`No materialised skills recorded for '${slug}' in ${join10(dir, SKILLS_LOCK)}.`);
|
|
3958
4168
|
const removed = [];
|
|
3959
4169
|
for (const name of entry.skills) {
|
|
3960
|
-
const skillPath =
|
|
3961
|
-
if (
|
|
4170
|
+
const skillPath = join10(dir, name);
|
|
4171
|
+
if (existsSync10(skillPath)) {
|
|
3962
4172
|
rmSync2(skillPath, { recursive: true, force: true });
|
|
3963
4173
|
removed.push(name);
|
|
3964
4174
|
}
|
|
3965
4175
|
}
|
|
3966
4176
|
delete lock[slug];
|
|
3967
|
-
|
|
4177
|
+
writeSkillsLock(dir, lock);
|
|
3968
4178
|
if (opts.json) return emit({ slug, removed, dir }, true);
|
|
3969
4179
|
console.log(style.green(`Removed ${removed.length} skill(s) for ${slug} from ${dir}`));
|
|
3970
4180
|
});
|
|
3971
|
-
skills.command("set-lane").description("
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
console.log(style.green(`Wrote lane pin \u2192 ${target} ${style.dim("(git-ignored)")}`));
|
|
3980
|
-
Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
|
|
3981
|
-
});
|
|
3982
|
-
skills.command("lane").description("Show the lane pin resolved from ./.sechroom/lane.json (nearest in this checkout; legacy ./.sem honoured)").option("--json", "machine output").action((opts, cmd) => {
|
|
3983
|
-
const json = cmd.optsWithGlobals().json;
|
|
3984
|
-
const found = readSem();
|
|
3985
|
-
if (!found) {
|
|
3986
|
-
if (json) return emit({ path: null, values: {} }, true);
|
|
3987
|
-
return console.log(style.dim(`No ./.sechroom/lane.json pin in this checkout. Run 'sechroom skills set-lane'.`));
|
|
3988
|
-
}
|
|
3989
|
-
if (json) return emit(found, true);
|
|
3990
|
-
console.log(style.dim(`from ${found.path}`));
|
|
3991
|
-
Object.entries(found.values).forEach(([k, v]) => console.log(" " + style.bold(k) + " = " + v));
|
|
3992
|
-
});
|
|
4181
|
+
skills.command("set-lane").description("Alias of `sechroom lane set` (kept for back-compat) \u2014 write this checkout's lane pin").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action(
|
|
4182
|
+
(opts, cmd) => setLane({
|
|
4183
|
+
codeLane: opts.codeLane,
|
|
4184
|
+
designLane: opts.designLane,
|
|
4185
|
+
json: Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)
|
|
4186
|
+
})
|
|
4187
|
+
);
|
|
4188
|
+
skills.command("lane").description("Alias of `sechroom lane` (kept for back-compat) \u2014 show this checkout's lane pin").option("--json", "machine output").action((opts, cmd) => showLane(Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)));
|
|
3993
4189
|
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) => {
|
|
3994
4190
|
if (!opts.defaultCodeLane && !opts.defaultDesignLane && !opts.handoverRecipient)
|
|
3995
4191
|
fail("Provide at least one of --default-code-lane / --default-design-lane / --handover-recipient.");
|
|
@@ -4053,22 +4249,24 @@ Examples:
|
|
|
4053
4249
|
}
|
|
4054
4250
|
|
|
4055
4251
|
// src/commands/reset.ts
|
|
4056
|
-
import { homedir as
|
|
4057
|
-
import { join as
|
|
4058
|
-
import { existsSync as
|
|
4059
|
-
var
|
|
4060
|
-
var localSkillsDir = () =>
|
|
4061
|
-
var globalSkillsDir = () =>
|
|
4252
|
+
import { homedir as homedir6 } from "os";
|
|
4253
|
+
import { join as join11 } from "path";
|
|
4254
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
|
|
4255
|
+
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
4256
|
+
var localSkillsDir = () => join11(process.cwd(), ".claude", "skills");
|
|
4257
|
+
var globalSkillsDir = () => join11(homedir6(), ".claude", "skills");
|
|
4258
|
+
var localAgentsDir = () => join11(process.cwd(), ".claude", "agents");
|
|
4259
|
+
var globalAgentsDir = () => join11(homedir6(), ".claude", "agents");
|
|
4062
4260
|
function removeMaterialisedSkills(dir) {
|
|
4063
4261
|
const removed = [];
|
|
4064
|
-
const lockPath =
|
|
4065
|
-
if (!
|
|
4262
|
+
const lockPath = join11(dir, SKILLS_LOCK2);
|
|
4263
|
+
if (!existsSync11(lockPath)) return removed;
|
|
4066
4264
|
try {
|
|
4067
4265
|
const lock = JSON.parse(readFileSync7(lockPath, "utf8"));
|
|
4068
4266
|
for (const entry of Object.values(lock)) {
|
|
4069
4267
|
for (const name of entry.skills ?? []) {
|
|
4070
|
-
const p =
|
|
4071
|
-
if (
|
|
4268
|
+
const p = join11(dir, name);
|
|
4269
|
+
if (existsSync11(p)) {
|
|
4072
4270
|
rmSync3(p, { recursive: true, force: true });
|
|
4073
4271
|
removed.push(p);
|
|
4074
4272
|
}
|
|
@@ -4113,28 +4311,30 @@ function registerReset(program2) {
|
|
|
4113
4311
|
}
|
|
4114
4312
|
}
|
|
4115
4313
|
const removed = [];
|
|
4116
|
-
const stateDir =
|
|
4117
|
-
if (
|
|
4314
|
+
const stateDir = join11(process.cwd(), ".sechroom");
|
|
4315
|
+
if (existsSync11(stateDir)) {
|
|
4118
4316
|
rmSync3(stateDir, { recursive: true, force: true });
|
|
4119
4317
|
removed.push(stateDir);
|
|
4120
4318
|
}
|
|
4121
|
-
const legacyCfg =
|
|
4122
|
-
if (
|
|
4319
|
+
const legacyCfg = join11(process.cwd(), ".sechroom.json");
|
|
4320
|
+
if (existsSync11(legacyCfg)) {
|
|
4123
4321
|
rmSync3(legacyCfg, { force: true });
|
|
4124
4322
|
removed.push(legacyCfg);
|
|
4125
4323
|
}
|
|
4126
|
-
const legacySem =
|
|
4127
|
-
if (
|
|
4324
|
+
const legacySem = join11(process.cwd(), ".sem");
|
|
4325
|
+
if (existsSync11(legacySem)) {
|
|
4128
4326
|
rmSync3(legacySem, { force: true });
|
|
4129
4327
|
removed.push(legacySem);
|
|
4130
4328
|
}
|
|
4131
4329
|
removed.push(...removeMaterialisedSkills(localSkillsDir()));
|
|
4330
|
+
removed.push(...removeMaterialisedSkills(localAgentsDir()));
|
|
4132
4331
|
if (global) {
|
|
4133
4332
|
const tok = clearToken();
|
|
4134
4333
|
if (tok) removed.push(tok);
|
|
4135
4334
|
const cfg = clearPersisted();
|
|
4136
4335
|
if (cfg) removed.push(cfg);
|
|
4137
4336
|
removed.push(...removeMaterialisedSkills(globalSkillsDir()));
|
|
4337
|
+
removed.push(...removeMaterialisedSkills(globalAgentsDir()));
|
|
4138
4338
|
}
|
|
4139
4339
|
if (json) return emit({ global, removed }, true);
|
|
4140
4340
|
if (removed.length === 0) {
|
|
@@ -4159,7 +4359,7 @@ function resolveVersion() {
|
|
|
4159
4359
|
}
|
|
4160
4360
|
}
|
|
4161
4361
|
var program = new Command();
|
|
4162
|
-
program.name("sechroom").description("Sechroom CLI \u2014 thin generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.").version(resolveVersion()).option("--base-url <url>", "API base URL (overrides config / SECHROOM_BASE_URL)").option("--tenant <tenant>", "Tenant id (required by the API; overrides config / SECHROOM_TENANT)").option("--json", "Emit compact JSON (for scripts and agents)", false);
|
|
4362
|
+
program.name("sechroom").description("Sechroom CLI \u2014 thin generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.").version(resolveVersion()).option("--base-url <url>", "API base URL (overrides config / SECHROOM_BASE_URL)").option("--tenant <tenant>", "Tenant id (required by the API; overrides config / SECHROOM_TENANT)").option("--binding <name>", "Named workspace binding from .sechroom.json `workspaces` (overrides path auto-selection / SECHROOM_BINDING)").option("--json", "Emit compact JSON (for scripts and agents)", false);
|
|
4163
4363
|
program.addHelpText(
|
|
4164
4364
|
"after",
|
|
4165
4365
|
`
|
|
@@ -4233,7 +4433,7 @@ config.command("set <key> <value>").description("Set baseUrl | tenant | workspac
|
|
|
4233
4433
|
});
|
|
4234
4434
|
config.command("show").description("Print resolved config + sources (flag > env > local > global > default)").action((_opts, cmd) => {
|
|
4235
4435
|
const g = cmd.optsWithGlobals();
|
|
4236
|
-
const d = describeConfig({ baseUrl: g.baseUrl, tenant: g.tenant });
|
|
4436
|
+
const d = describeConfig({ baseUrl: g.baseUrl, tenant: g.tenant, binding: g.binding });
|
|
4237
4437
|
if (g.json) {
|
|
4238
4438
|
process.stdout.write(
|
|
4239
4439
|
JSON.stringify({
|
|
@@ -4272,6 +4472,7 @@ registerSetup(program);
|
|
|
4272
4472
|
registerOnboard(program);
|
|
4273
4473
|
registerSweep(program);
|
|
4274
4474
|
registerSkills(program);
|
|
4475
|
+
registerLane(program);
|
|
4275
4476
|
registerReset(program);
|
|
4276
4477
|
program.parseAsync().catch((err2) => {
|
|
4277
4478
|
process.stderr.write(`error: ${err2 instanceof Error ? err2.message : String(err2)}
|
package/package.json
CHANGED