@sechroom/cli 2026.7.13 → 2026.7.15
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 +406 -47
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -439,6 +439,9 @@ var quiet = false;
|
|
|
439
439
|
function setQuiet(q) {
|
|
440
440
|
quiet = q;
|
|
441
441
|
}
|
|
442
|
+
function isQuiet() {
|
|
443
|
+
return quiet;
|
|
444
|
+
}
|
|
442
445
|
function colorOn() {
|
|
443
446
|
return !quiet && !process.env.NO_COLOR && process.env.FORCE_COLOR !== "0" && Boolean(process.stdout.isTTY);
|
|
444
447
|
}
|
|
@@ -615,6 +618,22 @@ function emitAction(summary, data, json) {
|
|
|
615
618
|
process.stdout.write(`${ok("\u2713")} ${summary}
|
|
616
619
|
`);
|
|
617
620
|
}
|
|
621
|
+
var GOVERNANCE_QUEUED_PROBLEM_TYPE = "https://sechroom.dev/problems/governance-review-queued";
|
|
622
|
+
function isGovernanceQueued(body) {
|
|
623
|
+
return typeof body === "object" && body !== null && "type" in body && body.type === GOVERNANCE_QUEUED_PROBLEM_TYPE;
|
|
624
|
+
}
|
|
625
|
+
function formatQueuedForApproval(body, json) {
|
|
626
|
+
if (json) return JSON.stringify(body) + "\n";
|
|
627
|
+
const b = body ?? {};
|
|
628
|
+
const detail = typeof b.detail === "string" ? b.detail : void 0;
|
|
629
|
+
const requestId = typeof b.requestId === "string" ? b.requestId : void 0;
|
|
630
|
+
const message = detail ?? "This change requires operator approval before it takes effect.";
|
|
631
|
+
let out = `${warn("\u29D7")} Pending approval \u2014 ${message}
|
|
632
|
+
`;
|
|
633
|
+
if (requestId) out += ` ${style.dim(`request: ${requestId}`)}
|
|
634
|
+
`;
|
|
635
|
+
return out;
|
|
636
|
+
}
|
|
618
637
|
async function runApi(label, fn) {
|
|
619
638
|
const s = spinner(label);
|
|
620
639
|
let res;
|
|
@@ -624,6 +643,12 @@ async function runApi(label, fn) {
|
|
|
624
643
|
s.fail();
|
|
625
644
|
fail(err2);
|
|
626
645
|
}
|
|
646
|
+
const queuedBody = res.data ?? res.error;
|
|
647
|
+
if (res.response?.status === 202 && isGovernanceQueued(queuedBody)) {
|
|
648
|
+
s.stop();
|
|
649
|
+
process.stdout.write(formatQueuedForApproval(queuedBody, isQuiet()));
|
|
650
|
+
process.exit(0);
|
|
651
|
+
}
|
|
627
652
|
const httpFailed = res.response !== void 0 && !res.response.ok;
|
|
628
653
|
if (res.error !== void 0 && res.error !== null || httpFailed) {
|
|
629
654
|
s.fail();
|
|
@@ -1076,14 +1101,23 @@ function bodyOf(row) {
|
|
|
1076
1101
|
const m = row?.item ?? row;
|
|
1077
1102
|
return m?.text ?? m?.Text ?? "";
|
|
1078
1103
|
}
|
|
1104
|
+
function idOf(row) {
|
|
1105
|
+
const m = row?.item ?? row;
|
|
1106
|
+
return String(m?.id ?? m?.memoryId ?? "unknown");
|
|
1107
|
+
}
|
|
1079
1108
|
function entriesFromRows(rows, surface, source, roleTag, namePrefix) {
|
|
1080
1109
|
const out = /* @__PURE__ */ new Map();
|
|
1081
1110
|
for (const row of rows ?? []) {
|
|
1082
1111
|
const tags = tagsOf(row);
|
|
1083
1112
|
if (!tags.includes(roleTag)) continue;
|
|
1084
|
-
if (
|
|
1113
|
+
if (!tags.includes(`target:${surface}`)) continue;
|
|
1085
1114
|
const name = tagValue(tags, namePrefix);
|
|
1086
1115
|
if (!name) continue;
|
|
1116
|
+
if (out.has(name)) {
|
|
1117
|
+
throw new Error(
|
|
1118
|
+
`Ambiguous ${source} ${roleTag} '${name}' for target:${surface}: ${idOf(row)} duplicates another eligible component. Resolve the duplicate before installing.`
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1087
1121
|
out.set(name, { name, body: bodyOf(row), source });
|
|
1088
1122
|
}
|
|
1089
1123
|
return out;
|
|
@@ -1111,20 +1145,33 @@ function agentTargetFor(surface) {
|
|
|
1111
1145
|
return AGENT_TARGET[surface] ?? `${surface}-agent`;
|
|
1112
1146
|
}
|
|
1113
1147
|
async function fetchFeedRows(cfg, workspaceId) {
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1148
|
+
const client = await makeClient(cfg);
|
|
1149
|
+
const rows = [];
|
|
1150
|
+
let cursor;
|
|
1151
|
+
do {
|
|
1152
|
+
const { data, error, response } = await client.GET("/workspaces/{workspaceId}/memories/feed", {
|
|
1117
1153
|
params: {
|
|
1118
1154
|
path: { workspaceId },
|
|
1119
1155
|
// cascadeWorkspaces: skills land in an "Operator Skills" SUB-workspace;
|
|
1120
1156
|
// includeText: the feed omits bodies by default, we need them for SKILL.md.
|
|
1121
|
-
query: {
|
|
1157
|
+
query: {
|
|
1158
|
+
limit: 200,
|
|
1159
|
+
cascadeWorkspaces: true,
|
|
1160
|
+
includeText: true,
|
|
1161
|
+
...cursor ? { cursor } : {}
|
|
1162
|
+
}
|
|
1122
1163
|
}
|
|
1123
|
-
})
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1164
|
+
});
|
|
1165
|
+
if (error || !response.ok || !data) {
|
|
1166
|
+
throw new Error(
|
|
1167
|
+
`Could not read operator-skill catalogue from ${workspaceId}: HTTP ${response.status}.`
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
const feed = data;
|
|
1171
|
+
rows.push(...feed.results ?? feed.Results ?? []);
|
|
1172
|
+
cursor = feed.nextCursor ?? feed.NextCursor ?? void 0;
|
|
1173
|
+
} while (cursor);
|
|
1174
|
+
return rows;
|
|
1128
1175
|
}
|
|
1129
1176
|
async function fetchTemplateRows(cfg, personalWorkspaceId) {
|
|
1130
1177
|
const [systemRows, personalRows] = await Promise.all([
|
|
@@ -1144,7 +1191,10 @@ function resolveReferenceSet(rows, surface) {
|
|
|
1144
1191
|
}
|
|
1145
1192
|
|
|
1146
1193
|
// src/setup/materialise.ts
|
|
1147
|
-
var CLIENT_SURFACE =
|
|
1194
|
+
var CLIENT_SURFACE = {
|
|
1195
|
+
claude: "claude-code",
|
|
1196
|
+
codex: "gpt-codex"
|
|
1197
|
+
};
|
|
1148
1198
|
function writeSkills(dir, skills, surface) {
|
|
1149
1199
|
const written = [];
|
|
1150
1200
|
for (const s of skills) {
|
|
@@ -1177,11 +1227,71 @@ function writeReferencesIntoSkillDirs(dir, skills, refs) {
|
|
|
1177
1227
|
}
|
|
1178
1228
|
return refs.map((r) => r.name);
|
|
1179
1229
|
}
|
|
1180
|
-
var SKILL_SPEC = {
|
|
1181
|
-
|
|
1230
|
+
var SKILL_SPEC = {
|
|
1231
|
+
kind: "skill",
|
|
1232
|
+
dir: skillsDir,
|
|
1233
|
+
resolve: resolveSkillSet,
|
|
1234
|
+
write: writeSkills,
|
|
1235
|
+
supportsCodex: true
|
|
1236
|
+
};
|
|
1237
|
+
var AGENT_SPEC = {
|
|
1238
|
+
kind: "agent",
|
|
1239
|
+
dir: agentsDir,
|
|
1240
|
+
resolve: resolveAgentSet,
|
|
1241
|
+
write: writeAgents,
|
|
1242
|
+
supportsCodex: false
|
|
1243
|
+
};
|
|
1182
1244
|
function scopeOf(opts) {
|
|
1183
1245
|
return opts.local ? "project" : resolveScope(opts.scope);
|
|
1184
1246
|
}
|
|
1247
|
+
function parseClient(value) {
|
|
1248
|
+
if (value == null) return void 0;
|
|
1249
|
+
if (value === "claude" || value === "codex" || value === "all") return value;
|
|
1250
|
+
throw new Error(`--client must be 'claude', 'codex', or 'all' (got '${value}')`);
|
|
1251
|
+
}
|
|
1252
|
+
function clientSelection(spec, g, raw) {
|
|
1253
|
+
if (!spec.supportsCodex) return "claude";
|
|
1254
|
+
const explicit = parseClient(raw);
|
|
1255
|
+
if (explicit) return explicit;
|
|
1256
|
+
const claudeConfigured = Boolean(g.claudeConfigDir || process.env.CLAUDE_CONFIG_DIR);
|
|
1257
|
+
const codexConfigured = Boolean(g.codexHome || process.env.CODEX_HOME);
|
|
1258
|
+
if (claudeConfigured || codexConfigured) {
|
|
1259
|
+
if (claudeConfigured && codexConfigured) return "all";
|
|
1260
|
+
return codexConfigured ? "codex" : "claude";
|
|
1261
|
+
}
|
|
1262
|
+
const claudeDetected = resolveClaudeTargets({})[0]?.dir;
|
|
1263
|
+
const codexDetected = resolveCodexHomes({})[0];
|
|
1264
|
+
const hasClaude = Boolean(claudeDetected && existsSync3(claudeDetected));
|
|
1265
|
+
const hasCodex = Boolean(codexDetected && existsSync3(codexDetected));
|
|
1266
|
+
if (hasClaude && hasCodex) return "all";
|
|
1267
|
+
if (hasCodex) return "codex";
|
|
1268
|
+
return "claude";
|
|
1269
|
+
}
|
|
1270
|
+
function targetsFor(spec, g, scope, selection) {
|
|
1271
|
+
const clients = selection === "all" ? ["claude", "codex"] : [selection];
|
|
1272
|
+
if (scope === "project" && clients.includes("codex")) {
|
|
1273
|
+
throw new Error("Codex skills have no supported project scope; use --scope global or --client claude.");
|
|
1274
|
+
}
|
|
1275
|
+
const targets = [];
|
|
1276
|
+
for (const client of clients) {
|
|
1277
|
+
if (client === "claude") {
|
|
1278
|
+
targets.push(...resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd: process.cwd() }).map((target) => ({
|
|
1279
|
+
client,
|
|
1280
|
+
surface: CLIENT_SURFACE[client],
|
|
1281
|
+
dir: spec.dir(target.dir),
|
|
1282
|
+
label: target.label
|
|
1283
|
+
})));
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
targets.push(...resolveCodexHomes({ override: g.codexHome, scope }).map((home) => ({
|
|
1287
|
+
client,
|
|
1288
|
+
surface: CLIENT_SURFACE[client],
|
|
1289
|
+
dir: spec.dir(home),
|
|
1290
|
+
label: home
|
|
1291
|
+
})));
|
|
1292
|
+
}
|
|
1293
|
+
return targets;
|
|
1294
|
+
}
|
|
1185
1295
|
async function runInstall(spec, cmd, opts) {
|
|
1186
1296
|
const g = cmd.optsWithGlobals();
|
|
1187
1297
|
let scope;
|
|
@@ -1193,31 +1303,48 @@ async function runInstall(spec, cmd, opts) {
|
|
|
1193
1303
|
const cfg = resolveConfig(g);
|
|
1194
1304
|
const dryRun = Boolean(opts.dryRun);
|
|
1195
1305
|
const json = Boolean(g.json || opts.json);
|
|
1196
|
-
|
|
1306
|
+
let selection;
|
|
1307
|
+
let targets;
|
|
1308
|
+
try {
|
|
1309
|
+
selection = clientSelection(spec, g, opts.client);
|
|
1310
|
+
targets = targetsFor(spec, g, scope, selection);
|
|
1311
|
+
} catch (err2) {
|
|
1312
|
+
return fail(err2.message);
|
|
1313
|
+
}
|
|
1197
1314
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
1198
1315
|
const rows = await fetchTemplateRows(cfg, personalWorkspaceId);
|
|
1199
|
-
const items = spec.resolve(rows, CLIENT_SURFACE);
|
|
1200
|
-
const refs = spec.kind === "skill" ? resolveReferenceSet(rows, CLIENT_SURFACE) : [];
|
|
1201
1316
|
const results = targets.map((t) => {
|
|
1202
|
-
const
|
|
1203
|
-
const
|
|
1204
|
-
const
|
|
1205
|
-
|
|
1317
|
+
const items = spec.resolve(rows, t.surface);
|
|
1318
|
+
const refs = spec.kind === "skill" ? resolveReferenceSet(rows, t.surface) : [];
|
|
1319
|
+
const written = dryRun ? items.map((i) => i.name) : spec.write(t.dir, items, t.surface);
|
|
1320
|
+
const refsWritten = dryRun ? refs.map((r) => r.name) : writeReferencesIntoSkillDirs(t.dir, items, refs);
|
|
1321
|
+
return {
|
|
1322
|
+
client: t.client,
|
|
1323
|
+
surface: t.surface,
|
|
1324
|
+
dir: t.dir,
|
|
1325
|
+
label: t.label,
|
|
1326
|
+
available: items.length,
|
|
1327
|
+
references: refs.length,
|
|
1328
|
+
items: items.map(({ name, source }) => ({ name, source })),
|
|
1329
|
+
referenceItems: refs.map(({ name, source }) => ({ name, source })),
|
|
1330
|
+
written,
|
|
1331
|
+
refsWritten
|
|
1332
|
+
};
|
|
1206
1333
|
});
|
|
1207
|
-
if (json) return emit({ kind: spec.kind,
|
|
1208
|
-
if (
|
|
1334
|
+
if (json) return emit({ kind: spec.kind, client: selection, dryRun, targets: results }, true);
|
|
1335
|
+
if (results.every((r) => r.available === 0)) {
|
|
1209
1336
|
console.log(style.dim(`No ${spec.kind}s available to install \u2014 is the bundle installed for your account?`));
|
|
1210
1337
|
return;
|
|
1211
1338
|
}
|
|
1212
1339
|
for (const r of results) {
|
|
1213
1340
|
console.log(
|
|
1214
|
-
`${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.written.length} ${spec.kind}(s) ${style.dim("\u2192")} ${r.dir}`
|
|
1341
|
+
`${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.written.length} ${spec.kind}(s) for ${r.client} ${style.dim("\u2192")} ${r.dir}`
|
|
1215
1342
|
);
|
|
1216
1343
|
if (r.refsWritten.length)
|
|
1217
1344
|
console.log(
|
|
1218
1345
|
`${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.refsWritten.length} reference(s) into each skill ${style.dim("\u2192")} ${r.dir}/<skill>/references`
|
|
1219
1346
|
);
|
|
1220
|
-
if (dryRun) for (const i of items) console.log(` ${i.name} ${style.dim(`[${i.source}]`)}`);
|
|
1347
|
+
if (dryRun) for (const i of r.items) console.log(` ${i.name} ${style.dim(`[${i.source}]`)}`);
|
|
1221
1348
|
}
|
|
1222
1349
|
}
|
|
1223
1350
|
function runList(spec, cmd, opts) {
|
|
@@ -1229,16 +1356,22 @@ function runList(spec, cmd, opts) {
|
|
|
1229
1356
|
return fail(err2.message);
|
|
1230
1357
|
}
|
|
1231
1358
|
const json = Boolean(g.json || opts.json);
|
|
1232
|
-
|
|
1359
|
+
let selection;
|
|
1360
|
+
let targets;
|
|
1361
|
+
try {
|
|
1362
|
+
selection = clientSelection(spec, g, opts.client);
|
|
1363
|
+
targets = targetsFor(spec, g, scope, selection);
|
|
1364
|
+
} catch (err2) {
|
|
1365
|
+
return fail(err2.message);
|
|
1366
|
+
}
|
|
1233
1367
|
const out = targets.map((t) => {
|
|
1234
|
-
const
|
|
1235
|
-
const lock = readSkillsLock(dir);
|
|
1368
|
+
const lock = readSkillsLock(t.dir);
|
|
1236
1369
|
const entries = Object.entries(lock).flatMap(
|
|
1237
|
-
([slug, e]) => (e.skills ?? []).map((name) => ({ slug, name, present: existsSync3(join4(dir, name)) }))
|
|
1370
|
+
([slug, e]) => (e.skills ?? []).map((name) => ({ slug, name, present: existsSync3(join4(t.dir, name)) }))
|
|
1238
1371
|
);
|
|
1239
|
-
return { dir, label: t.label, entries };
|
|
1372
|
+
return { client: t.client, surface: t.surface, dir: t.dir, label: t.label, entries };
|
|
1240
1373
|
});
|
|
1241
|
-
if (json) return emit({ kind: spec.kind, targets: out }, true);
|
|
1374
|
+
if (json) return emit({ kind: spec.kind, client: selection, targets: out }, true);
|
|
1242
1375
|
let any = false;
|
|
1243
1376
|
for (const t of out) {
|
|
1244
1377
|
if (t.entries.length === 0) continue;
|
|
@@ -1261,33 +1394,39 @@ function runClean(spec, cmd, opts, slugArg) {
|
|
|
1261
1394
|
return fail(err2.message);
|
|
1262
1395
|
}
|
|
1263
1396
|
const json = Boolean(g.json || opts.json);
|
|
1264
|
-
|
|
1397
|
+
let selection;
|
|
1398
|
+
let targets;
|
|
1399
|
+
try {
|
|
1400
|
+
selection = clientSelection(spec, g, opts.client);
|
|
1401
|
+
targets = targetsFor(spec, g, scope, selection);
|
|
1402
|
+
} catch (err2) {
|
|
1403
|
+
return fail(err2.message);
|
|
1404
|
+
}
|
|
1265
1405
|
const cleaned = [];
|
|
1266
1406
|
const missing = [];
|
|
1267
1407
|
for (const t of targets) {
|
|
1268
|
-
const
|
|
1269
|
-
const lock = readSkillsLock(dir);
|
|
1408
|
+
const lock = readSkillsLock(t.dir);
|
|
1270
1409
|
const entry = lock[slug];
|
|
1271
1410
|
if (!entry) {
|
|
1272
|
-
missing.push(join4(dir, SKILLS_LOCK));
|
|
1411
|
+
missing.push(join4(t.dir, SKILLS_LOCK));
|
|
1273
1412
|
continue;
|
|
1274
1413
|
}
|
|
1275
1414
|
const removed = [];
|
|
1276
1415
|
for (const name of entry.skills) {
|
|
1277
|
-
const p = join4(dir, name);
|
|
1416
|
+
const p = join4(t.dir, name);
|
|
1278
1417
|
if (existsSync3(p)) {
|
|
1279
1418
|
rmSync2(p, { recursive: true, force: true });
|
|
1280
1419
|
removed.push(name);
|
|
1281
1420
|
}
|
|
1282
1421
|
}
|
|
1283
1422
|
delete lock[slug];
|
|
1284
|
-
writeSkillsLock(dir, lock);
|
|
1285
|
-
cleaned.push({ dir, removed });
|
|
1423
|
+
writeSkillsLock(t.dir, lock);
|
|
1424
|
+
cleaned.push({ client: t.client, surface: t.surface, dir: t.dir, removed });
|
|
1286
1425
|
}
|
|
1287
1426
|
if (cleaned.length === 0) {
|
|
1288
1427
|
return fail(`No materialised ${spec.kind}s recorded for '${slug}' in ${missing.join(", ")}.`);
|
|
1289
1428
|
}
|
|
1290
|
-
if (json) return emit({ kind: spec.kind, slug, cleaned, missing }, true);
|
|
1429
|
+
if (json) return emit({ kind: spec.kind, client: selection, slug, cleaned, missing }, true);
|
|
1291
1430
|
for (const c of cleaned) {
|
|
1292
1431
|
console.log(style.green(`Removed ${c.removed.length} ${spec.kind}(s) for ${slug} from ${c.dir}`));
|
|
1293
1432
|
}
|
|
@@ -1415,8 +1554,12 @@ var CLAUDE_HOOK_COMMANDS = {
|
|
|
1415
1554
|
};
|
|
1416
1555
|
var CODEX_HOOK_COMMANDS = {
|
|
1417
1556
|
SessionStart: "sechroom hook session-start",
|
|
1557
|
+
PreCompact: "sechroom hook pre-compact",
|
|
1418
1558
|
Stop: "sechroom hook session-end --debounce-minutes 10"
|
|
1419
1559
|
};
|
|
1560
|
+
function hookCommandsForSurface(surface) {
|
|
1561
|
+
return surface === "claude" ? CLAUDE_HOOK_COMMANDS : CODEX_HOOK_COMMANDS;
|
|
1562
|
+
}
|
|
1420
1563
|
function hasHookCommand(config2, event, command) {
|
|
1421
1564
|
const groups = config2.hooks?.[event] ?? [];
|
|
1422
1565
|
return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
|
|
@@ -1554,7 +1697,10 @@ function registerChannel(program2) {
|
|
|
1554
1697
|
"--tag <tag...>",
|
|
1555
1698
|
"Tag(s) the event must carry to match (repeatable). Default targets WLP task dispatches.",
|
|
1556
1699
|
["kind:task"]
|
|
1557
|
-
).option("--workspace <wsp...>", "Restrict to these workspace id(s)")
|
|
1700
|
+
).option("--workspace <wsp...>", "Restrict to these workspace id(s)").option(
|
|
1701
|
+
"--executor-instance <id>",
|
|
1702
|
+
"Join the exact executor-instance SignalR group"
|
|
1703
|
+
);
|
|
1558
1704
|
withFilterOpts(
|
|
1559
1705
|
channel.command("connect").description(
|
|
1560
1706
|
"Register a SignalR subscription and stream matched events to stdout"
|
|
@@ -1572,7 +1718,7 @@ function registerChannel(program2) {
|
|
|
1572
1718
|
(typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
|
|
1573
1719
|
)
|
|
1574
1720
|
);
|
|
1575
|
-
const conn = await openConnection(cfg, deliver);
|
|
1721
|
+
const conn = await openConnection(cfg, deliver, opts.executorInstance);
|
|
1576
1722
|
await reconcile(cfg, filter, deliver);
|
|
1577
1723
|
if (json) {
|
|
1578
1724
|
emit(
|
|
@@ -1621,7 +1767,7 @@ function registerChannel(program2) {
|
|
|
1621
1767
|
`))
|
|
1622
1768
|
);
|
|
1623
1769
|
});
|
|
1624
|
-
const conn = await openConnection(cfg, deliver);
|
|
1770
|
+
const conn = await openConnection(cfg, deliver, opts.executorInstance);
|
|
1625
1771
|
await reconcile(cfg, filter, deliver);
|
|
1626
1772
|
process.stderr.write(
|
|
1627
1773
|
style.dim(
|
|
@@ -1732,8 +1878,9 @@ async function ensureSubscription(cfg, name, filter) {
|
|
|
1732
1878
|
);
|
|
1733
1879
|
return await resp.json();
|
|
1734
1880
|
}
|
|
1735
|
-
async function openConnection(cfg, onEvent) {
|
|
1736
|
-
const
|
|
1881
|
+
async function openConnection(cfg, onEvent, executorInstanceId) {
|
|
1882
|
+
const query = executorInstanceId ? `?executorInstanceId=${encodeURIComponent(executorInstanceId)}` : "";
|
|
1883
|
+
const conn = new HubConnectionBuilder().withUrl(`${cfg.baseUrl}/notifications/${cfg.tenant}${query}`, {
|
|
1737
1884
|
transport: HttpTransportType.LongPolling,
|
|
1738
1885
|
accessTokenFactory: () => requireToken(cfg)
|
|
1739
1886
|
}).withAutomaticReconnect().build();
|
|
@@ -2436,6 +2583,12 @@ Fail-soft: no lane / no auth / no-or-partial intent file / API error -> exit 0,
|
|
|
2436
2583
|
const surfaceResults = installHookSurfaces(["codex"], { dryRun, claudeDir: "", codexHome })[0].results;
|
|
2437
2584
|
process.stdout.write(`${HOOK_SURFACE_LABEL.codex}:
|
|
2438
2585
|
`);
|
|
2586
|
+
if (dryRun) {
|
|
2587
|
+
for (const [event, command] of Object.entries(hookCommandsForSurface("codex"))) {
|
|
2588
|
+
process.stdout.write(` ${event}: ${command}
|
|
2589
|
+
`);
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2439
2592
|
for (const r of surfaceResults) {
|
|
2440
2593
|
results.push(r);
|
|
2441
2594
|
process.stdout.write(describe(r, dryRun) + "\n");
|
|
@@ -2987,6 +3140,205 @@ Examples:
|
|
|
2987
3140
|
});
|
|
2988
3141
|
}
|
|
2989
3142
|
|
|
3143
|
+
// src/commands/executor.ts
|
|
3144
|
+
function registerExecutor(program2) {
|
|
3145
|
+
const executor = program2.command("executor").description(
|
|
3146
|
+
"Register and operate a local Claude Code/Codex executor advertisement"
|
|
3147
|
+
);
|
|
3148
|
+
executor.command("submit-connector").description(
|
|
3149
|
+
"Submit a local-session connector definition for governed approval"
|
|
3150
|
+
).requiredOption("--slug <slug>", "Unique connector definition slug").requiredOption("--display-name <name>", "Human-readable connector name").requiredOption("--transport <kind>", "push | pull").option("--profile <profile...>", "Advertised runtime profiles", [
|
|
3151
|
+
"base",
|
|
3152
|
+
"dotnet-10"
|
|
3153
|
+
]).action(async (opts, cmd) => {
|
|
3154
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3155
|
+
const data = await api(cfg, "/connectors/definitions", {
|
|
3156
|
+
method: "POST",
|
|
3157
|
+
body: JSON.stringify({
|
|
3158
|
+
slug: opts.slug,
|
|
3159
|
+
displayName: opts.displayName,
|
|
3160
|
+
runtimeProfiles: opts.profile,
|
|
3161
|
+
connectorKind: "ExecutionRuntime",
|
|
3162
|
+
providerKind: "local-session",
|
|
3163
|
+
dispatchTransport: parseTransport(opts.transport)
|
|
3164
|
+
})
|
|
3165
|
+
});
|
|
3166
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3167
|
+
if (!cmd.optsWithGlobals().json) {
|
|
3168
|
+
process.stderr.write(
|
|
3169
|
+
style.dim(
|
|
3170
|
+
"approve this connector definition before registering executors\n"
|
|
3171
|
+
)
|
|
3172
|
+
);
|
|
3173
|
+
}
|
|
3174
|
+
});
|
|
3175
|
+
executor.command("register").description(
|
|
3176
|
+
"Create/reuse a SignalR binding and register this local executor instance"
|
|
3177
|
+
).requiredOption(
|
|
3178
|
+
"--instance-key <key>",
|
|
3179
|
+
"Stable key for this concrete session/lane"
|
|
3180
|
+
).requiredOption(
|
|
3181
|
+
"--connector <id>",
|
|
3182
|
+
"Approved local-session ConnectorDefinition id"
|
|
3183
|
+
).option("--runtime <kind>", "claude-code | codex", "claude-code").option(
|
|
3184
|
+
"--relay <id>",
|
|
3185
|
+
"Relay identity shared by sibling instances",
|
|
3186
|
+
"sechroom-cli-local"
|
|
3187
|
+
).option(
|
|
3188
|
+
"--subscription-name <name>",
|
|
3189
|
+
"SignalR delivery binding name",
|
|
3190
|
+
"executor-dispatch"
|
|
3191
|
+
).option(
|
|
3192
|
+
"--capability <key...>",
|
|
3193
|
+
"Capability operation keys claimed by this instance"
|
|
3194
|
+
).option("--tool-set-ref <ref>", "Optional governed tool-set reference").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (opts, cmd) => {
|
|
3195
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3196
|
+
if (!cfg.workspaceId)
|
|
3197
|
+
fail("executor register requires a configured workspaceId");
|
|
3198
|
+
const subscription = await api(
|
|
3199
|
+
cfg,
|
|
3200
|
+
"/me/delivery-subscriptions/signalr",
|
|
3201
|
+
{
|
|
3202
|
+
method: "POST",
|
|
3203
|
+
body: JSON.stringify({
|
|
3204
|
+
name: opts.subscriptionName,
|
|
3205
|
+
enabled: true,
|
|
3206
|
+
filter: { tags: ["kind:task"], workspaceScope: [cfg.workspaceId] }
|
|
3207
|
+
})
|
|
3208
|
+
}
|
|
3209
|
+
);
|
|
3210
|
+
const data = await api(
|
|
3211
|
+
cfg,
|
|
3212
|
+
"/me/executor-instances",
|
|
3213
|
+
{
|
|
3214
|
+
method: "POST",
|
|
3215
|
+
body: JSON.stringify({
|
|
3216
|
+
relayId: opts.relay,
|
|
3217
|
+
instanceKey: opts.instanceKey,
|
|
3218
|
+
runtimeKind: parseRuntimeKind(opts.runtime),
|
|
3219
|
+
activationMode: "Attached",
|
|
3220
|
+
deliverySubscriptionId: subscription.id,
|
|
3221
|
+
connectorId: opts.connector,
|
|
3222
|
+
claimedCapabilityKeys: opts.capability ?? [],
|
|
3223
|
+
toolSetRef: opts.toolSetRef ?? null,
|
|
3224
|
+
ttlSeconds: opts.ttl
|
|
3225
|
+
})
|
|
3226
|
+
}
|
|
3227
|
+
);
|
|
3228
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3229
|
+
if (!cmd.optsWithGlobals().json)
|
|
3230
|
+
process.stderr.write(
|
|
3231
|
+
style.dim(`refresh with: sechroom executor heartbeat ${data.id}
|
|
3232
|
+
`)
|
|
3233
|
+
);
|
|
3234
|
+
});
|
|
3235
|
+
executor.command("refresh <id>").description("Refresh one advertisement lease once").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (id, opts, cmd) => {
|
|
3236
|
+
const data = await refresh(
|
|
3237
|
+
resolveConfig(cmd.optsWithGlobals()),
|
|
3238
|
+
id,
|
|
3239
|
+
opts.ttl
|
|
3240
|
+
);
|
|
3241
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3242
|
+
});
|
|
3243
|
+
executor.command("heartbeat <id>").description("Keep an advertisement alive until interrupted").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).option("--interval <seconds>", "Refresh interval", parseInteger, 40).action(async (id, opts, cmd) => {
|
|
3244
|
+
if (opts.interval >= opts.ttl)
|
|
3245
|
+
fail("heartbeat interval must be shorter than the TTL");
|
|
3246
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3247
|
+
await refresh(cfg, id, opts.ttl);
|
|
3248
|
+
process.stderr.write(
|
|
3249
|
+
style.green("executor heartbeat active") + style.dim(` \u2014 ${id}
|
|
3250
|
+
`)
|
|
3251
|
+
);
|
|
3252
|
+
await holdHeartbeat(async () => {
|
|
3253
|
+
await refresh(cfg, id, opts.ttl);
|
|
3254
|
+
}, opts.interval * 1e3);
|
|
3255
|
+
});
|
|
3256
|
+
executor.command("offers <id>").description("List live dispatch offers addressed to this exact instance").action(async (id, _opts, cmd) => {
|
|
3257
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3258
|
+
const data = await api(
|
|
3259
|
+
cfg,
|
|
3260
|
+
`/me/executor-instances/${encodeURIComponent(id)}/dispatch-offers`
|
|
3261
|
+
);
|
|
3262
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3263
|
+
});
|
|
3264
|
+
executor.command("deregister <id>").description("Stop advertising this executor instance").action(async (id, _opts, cmd) => {
|
|
3265
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3266
|
+
const data = await api(
|
|
3267
|
+
cfg,
|
|
3268
|
+
`/me/executor-instances/${encodeURIComponent(id)}`,
|
|
3269
|
+
{
|
|
3270
|
+
method: "DELETE",
|
|
3271
|
+
body: JSON.stringify({})
|
|
3272
|
+
}
|
|
3273
|
+
);
|
|
3274
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
function parseRuntimeKind(value) {
|
|
3278
|
+
switch (value.trim().toLowerCase()) {
|
|
3279
|
+
case "claude":
|
|
3280
|
+
case "claude-code":
|
|
3281
|
+
return "ClaudeCode";
|
|
3282
|
+
case "codex":
|
|
3283
|
+
return "Codex";
|
|
3284
|
+
default:
|
|
3285
|
+
return fail("runtime must be claude-code or codex");
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
function parseTransport(value) {
|
|
3289
|
+
switch (value.trim().toLowerCase()) {
|
|
3290
|
+
case "push":
|
|
3291
|
+
return "Push";
|
|
3292
|
+
case "pull":
|
|
3293
|
+
return "Pull";
|
|
3294
|
+
default:
|
|
3295
|
+
return fail("transport must be push or pull");
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
async function refresh(cfg, id, ttlSeconds) {
|
|
3299
|
+
return api(
|
|
3300
|
+
cfg,
|
|
3301
|
+
`/me/executor-instances/${encodeURIComponent(id)}/refresh`,
|
|
3302
|
+
{
|
|
3303
|
+
method: "POST",
|
|
3304
|
+
body: JSON.stringify({ ttlSeconds })
|
|
3305
|
+
}
|
|
3306
|
+
);
|
|
3307
|
+
}
|
|
3308
|
+
async function api(cfg, path, init) {
|
|
3309
|
+
const token = await requireToken(cfg);
|
|
3310
|
+
const response = await fetch(`${cfg.baseUrl}${path}`, {
|
|
3311
|
+
...init,
|
|
3312
|
+
headers: {
|
|
3313
|
+
authorization: `Bearer ${token}`,
|
|
3314
|
+
tenant: cfg.tenant,
|
|
3315
|
+
"content-type": "application/json",
|
|
3316
|
+
"x-sechroom-surface": "cli"
|
|
3317
|
+
}
|
|
3318
|
+
});
|
|
3319
|
+
if (!response.ok)
|
|
3320
|
+
fail(
|
|
3321
|
+
`${init?.method ?? "GET"} ${path} failed (${response.status}): ${await response.text()}`
|
|
3322
|
+
);
|
|
3323
|
+
return response.json();
|
|
3324
|
+
}
|
|
3325
|
+
function parseInteger(value) {
|
|
3326
|
+
const parsed = Number.parseInt(value, 10);
|
|
3327
|
+
if (!Number.isFinite(parsed)) fail(`expected an integer, got '${value}'`);
|
|
3328
|
+
return parsed;
|
|
3329
|
+
}
|
|
3330
|
+
function holdHeartbeat(tick, intervalMs) {
|
|
3331
|
+
return new Promise((resolve3, reject) => {
|
|
3332
|
+
const timer = setInterval(() => void tick().catch(reject), intervalMs);
|
|
3333
|
+
const stop = () => {
|
|
3334
|
+
clearInterval(timer);
|
|
3335
|
+
resolve3();
|
|
3336
|
+
};
|
|
3337
|
+
process.once("SIGINT", stop);
|
|
3338
|
+
process.once("SIGTERM", stop);
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
|
|
2990
3342
|
// src/commands/filing.ts
|
|
2991
3343
|
function registerFiling(program2) {
|
|
2992
3344
|
const filing = program2.command("filing").description("Review and act on filing suggestions");
|
|
@@ -5444,7 +5796,9 @@ function registerSkills(program2) {
|
|
|
5444
5796
|
"after",
|
|
5445
5797
|
`
|
|
5446
5798
|
Examples:
|
|
5447
|
-
$ sechroom skills install
|
|
5799
|
+
$ sechroom skills install --client claude materialise target:claude-code skills to ~/.claude/skills
|
|
5800
|
+
$ sechroom skills install --client codex materialise target:gpt-codex skills to ~/.codex/skills
|
|
5801
|
+
$ sechroom skills install --client all keep both global clients in sync
|
|
5448
5802
|
$ sechroom skills install --scope project write them to ./.claude/skills instead
|
|
5449
5803
|
$ sechroom skills list what's materialised on disk
|
|
5450
5804
|
$ sechroom skills clean remove the materialised skill files
|
|
@@ -5453,11 +5807,15 @@ Examples:
|
|
|
5453
5807
|
$ sechroom skills package --from-source --workspace wsp_abc -o ./dist zip a draft from source
|
|
5454
5808
|
$ sechroom skills set-lane --code-lane claude-code-chris --design-lane claude-design-chris
|
|
5455
5809
|
|
|
5810
|
+
When --client is omitted, configured client flags/environment variables win;
|
|
5811
|
+
otherwise existing default client homes are detected. Both configured/detected
|
|
5812
|
+
clients select all; an unconfigured machine preserves the legacy Claude default.
|
|
5813
|
+
|
|
5456
5814
|
`
|
|
5457
5815
|
);
|
|
5458
|
-
skills.command("install").description("Materialise your installed skills to disk (the already-installed bundle \u2014 no server install)").option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--dry-run", "print what would be written; write nothing").option("--json", "machine output").action((opts, cmd) => runInstall(SKILL_SPEC, cmd, opts));
|
|
5459
|
-
skills.command("list").description("List the skills materialised on disk (per resolved config dir)").option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--json", "machine output").action((opts, cmd) => runList(SKILL_SPEC, cmd, opts));
|
|
5460
|
-
skills.command("clean [slug]").description(`Remove skill files materialised to disk (default ${DEFAULT_SKILLS_SLUG})`).option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--json", "machine output").action((slugArg, opts, cmd) => runClean(SKILL_SPEC, cmd, opts, slugArg));
|
|
5816
|
+
skills.command("install").description("Materialise your installed skills to disk (the already-installed bundle \u2014 no server install)").option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--client <client>", "claude, codex, or all (default: configured/detected clients)").option("--dry-run", "print what would be written; write nothing").option("--json", "machine output").action((opts, cmd) => runInstall(SKILL_SPEC, cmd, opts));
|
|
5817
|
+
skills.command("list").description("List the skills materialised on disk (per resolved config dir)").option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--client <client>", "claude, codex, or all (default: configured/detected clients)").option("--json", "machine output").action((opts, cmd) => runList(SKILL_SPEC, cmd, opts));
|
|
5818
|
+
skills.command("clean [slug]").description(`Remove skill files materialised to disk (default ${DEFAULT_SKILLS_SLUG})`).option("--scope <scope>", "global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global").option("--local", "alias for --scope project").option("--client <client>", "claude, codex, or all (default: configured/detected clients)").option("--json", "machine output").action((slugArg, opts, cmd) => runClean(SKILL_SPEC, cmd, opts, slugArg));
|
|
5461
5819
|
skills.command("preview").description("Render a workspace's draft bundle from source (no publish/install) and report the components").requiredOption("--workspace <id>", "workspace holding the draft bundle sources (wsp_\u2026)").option("--slug <slug>", "override the derived bundle slug").option("--title <title>", "override the derived bundle title").option("--version <version>", "override the derived bundle version").option("--default-install-parent <path>", "override the derived default install parent").option("--json", "machine output (the full RenderBundlePreviewResponse)").action(async (opts, cmd) => {
|
|
5462
5820
|
const json = Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json);
|
|
5463
5821
|
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
@@ -6399,6 +6757,7 @@ registerRelationships(program);
|
|
|
6399
6757
|
registerWorkspace(program);
|
|
6400
6758
|
registerProject(program);
|
|
6401
6759
|
registerDecomposition(program);
|
|
6760
|
+
registerExecutor(program);
|
|
6402
6761
|
registerClose(program);
|
|
6403
6762
|
registerFiling(program);
|
|
6404
6763
|
registerContinuity(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sechroom/cli",
|
|
3
|
-
"version": "2026.7.
|
|
3
|
+
"version": "2026.7.15",
|
|
4
4
|
"description": "Sechroom CLI — a thin, generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"gen": "openapi-typescript \"${SECHROOM_OPENAPI_URL:-https://app.sechroom.ai/api/openapi/v1.json}\" -o src/generated/api.d.ts",
|
|
44
44
|
"build": "tsup src/index.ts --format esm --target node20 --clean",
|
|
45
45
|
"dev": "tsx src/index.ts",
|
|
46
|
+
"test": "node --import tsx --test \"src/**/*.test.ts\"",
|
|
46
47
|
"check-types": "tsc --noEmit"
|
|
47
48
|
}
|
|
48
49
|
}
|