@sechroom/cli 2026.7.13 → 2026.7.14-rc.08244cc9
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 +43 -19
- package/dist/index.js +1213 -535
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/auth.ts
|
|
@@ -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();
|
|
@@ -632,9 +657,14 @@ async function runApi(label, fn) {
|
|
|
632
657
|
s.succeed();
|
|
633
658
|
return res.data;
|
|
634
659
|
}
|
|
635
|
-
function
|
|
660
|
+
function formatFailureMessage(error) {
|
|
636
661
|
let msg;
|
|
637
|
-
if (
|
|
662
|
+
if (error instanceof Error) {
|
|
663
|
+
msg = error.message || error.name;
|
|
664
|
+
if (error.cause instanceof Error && error.cause.message) {
|
|
665
|
+
msg += `: ${error.cause.message}`;
|
|
666
|
+
}
|
|
667
|
+
} else if (typeof error === "object" && error !== null && "title" in error) {
|
|
638
668
|
const problem = error;
|
|
639
669
|
msg = String(problem.title);
|
|
640
670
|
if (problem.errors && typeof problem.errors === "object") {
|
|
@@ -649,6 +679,10 @@ ${detail.join("\n")}`;
|
|
|
649
679
|
} else {
|
|
650
680
|
msg = String(error);
|
|
651
681
|
}
|
|
682
|
+
return msg;
|
|
683
|
+
}
|
|
684
|
+
function fail(error) {
|
|
685
|
+
const msg = formatFailureMessage(error);
|
|
652
686
|
process.stderr.write(`error: ${msg}
|
|
653
687
|
`);
|
|
654
688
|
process.exit(1);
|
|
@@ -1076,14 +1110,23 @@ function bodyOf(row) {
|
|
|
1076
1110
|
const m = row?.item ?? row;
|
|
1077
1111
|
return m?.text ?? m?.Text ?? "";
|
|
1078
1112
|
}
|
|
1113
|
+
function idOf(row) {
|
|
1114
|
+
const m = row?.item ?? row;
|
|
1115
|
+
return String(m?.id ?? m?.memoryId ?? "unknown");
|
|
1116
|
+
}
|
|
1079
1117
|
function entriesFromRows(rows, surface, source, roleTag, namePrefix) {
|
|
1080
1118
|
const out = /* @__PURE__ */ new Map();
|
|
1081
1119
|
for (const row of rows ?? []) {
|
|
1082
1120
|
const tags = tagsOf(row);
|
|
1083
1121
|
if (!tags.includes(roleTag)) continue;
|
|
1084
|
-
if (
|
|
1122
|
+
if (!tags.includes(`target:${surface}`)) continue;
|
|
1085
1123
|
const name = tagValue(tags, namePrefix);
|
|
1086
1124
|
if (!name) continue;
|
|
1125
|
+
if (out.has(name)) {
|
|
1126
|
+
throw new Error(
|
|
1127
|
+
`Ambiguous ${source} ${roleTag} '${name}' for target:${surface}: ${idOf(row)} duplicates another eligible component. Resolve the duplicate before installing.`
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1087
1130
|
out.set(name, { name, body: bodyOf(row), source });
|
|
1088
1131
|
}
|
|
1089
1132
|
return out;
|
|
@@ -1106,25 +1149,41 @@ function resolveReferences(systemRows, personalRows, surface) {
|
|
|
1106
1149
|
}
|
|
1107
1150
|
|
|
1108
1151
|
// src/setup/skill-resolution-io.ts
|
|
1109
|
-
var AGENT_TARGET = {
|
|
1152
|
+
var AGENT_TARGET = {
|
|
1153
|
+
"claude-code": "claude-agent",
|
|
1154
|
+
"gpt-codex": "gpt-codex-agent"
|
|
1155
|
+
};
|
|
1110
1156
|
function agentTargetFor(surface) {
|
|
1111
1157
|
return AGENT_TARGET[surface] ?? `${surface}-agent`;
|
|
1112
1158
|
}
|
|
1113
1159
|
async function fetchFeedRows(cfg, workspaceId) {
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1160
|
+
const client = await makeClient(cfg);
|
|
1161
|
+
const rows = [];
|
|
1162
|
+
let cursor;
|
|
1163
|
+
do {
|
|
1164
|
+
const { data, error, response } = await client.GET("/workspaces/{workspaceId}/memories/feed", {
|
|
1117
1165
|
params: {
|
|
1118
1166
|
path: { workspaceId },
|
|
1119
1167
|
// cascadeWorkspaces: skills land in an "Operator Skills" SUB-workspace;
|
|
1120
1168
|
// includeText: the feed omits bodies by default, we need them for SKILL.md.
|
|
1121
|
-
query: {
|
|
1169
|
+
query: {
|
|
1170
|
+
limit: 200,
|
|
1171
|
+
cascadeWorkspaces: true,
|
|
1172
|
+
includeText: true,
|
|
1173
|
+
...cursor ? { cursor } : {}
|
|
1174
|
+
}
|
|
1122
1175
|
}
|
|
1123
|
-
})
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1176
|
+
});
|
|
1177
|
+
if (error || !response.ok || !data) {
|
|
1178
|
+
throw new Error(
|
|
1179
|
+
`Could not read operator-skill catalogue from ${workspaceId}: HTTP ${response.status}.`
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
const feed = data;
|
|
1183
|
+
rows.push(...feed.results ?? feed.Results ?? []);
|
|
1184
|
+
cursor = feed.nextCursor ?? feed.NextCursor ?? void 0;
|
|
1185
|
+
} while (cursor);
|
|
1186
|
+
return rows;
|
|
1128
1187
|
}
|
|
1129
1188
|
async function fetchTemplateRows(cfg, personalWorkspaceId) {
|
|
1130
1189
|
const [systemRows, personalRows] = await Promise.all([
|
|
@@ -1144,7 +1203,10 @@ function resolveReferenceSet(rows, surface) {
|
|
|
1144
1203
|
}
|
|
1145
1204
|
|
|
1146
1205
|
// src/setup/materialise.ts
|
|
1147
|
-
var CLIENT_SURFACE =
|
|
1206
|
+
var CLIENT_SURFACE = {
|
|
1207
|
+
claude: "claude-code",
|
|
1208
|
+
codex: "gpt-codex"
|
|
1209
|
+
};
|
|
1148
1210
|
function writeSkills(dir, skills, surface) {
|
|
1149
1211
|
const written = [];
|
|
1150
1212
|
for (const s of skills) {
|
|
@@ -1155,12 +1217,58 @@ function writeSkills(dir, skills, surface) {
|
|
|
1155
1217
|
if (written.length) recordMaterialisedSkills(dir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
1156
1218
|
return written;
|
|
1157
1219
|
}
|
|
1220
|
+
function splitAgentFrontmatter(body) {
|
|
1221
|
+
body = body.replaceAll("\r\n", "\n");
|
|
1222
|
+
if (!body.startsWith("---\n")) return { instructions: body };
|
|
1223
|
+
const end = body.indexOf("\n---\n", 4);
|
|
1224
|
+
if (end < 0) return { instructions: body };
|
|
1225
|
+
const frontmatter = body.slice(4, end);
|
|
1226
|
+
const field = (key) => {
|
|
1227
|
+
const line = frontmatter.split("\n").find((candidate) => candidate.startsWith(`${key}:`));
|
|
1228
|
+
return line?.slice(key.length + 1).trim().replace(/^(["'])(.*)\1$/, "$2");
|
|
1229
|
+
};
|
|
1230
|
+
return { name: field("name"), description: field("description"), instructions: body.slice(end + 5).trimStart() };
|
|
1231
|
+
}
|
|
1232
|
+
function validateCodexSkills(skills) {
|
|
1233
|
+
for (const skill of skills) {
|
|
1234
|
+
const parsed = splitAgentFrontmatter(skill.body);
|
|
1235
|
+
if (parsed.name !== skill.name || !parsed.description) {
|
|
1236
|
+
throw new Error(
|
|
1237
|
+
`Codex skill '${skill.name}' must begin with YAML frontmatter containing matching name and a description.`
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
var WORKER_NAMES = ["substrate-drafter", "substrate-miner", "substrate-verifier"];
|
|
1243
|
+
function validateCodexWorkerDependencies(skills, agents) {
|
|
1244
|
+
const available = new Set(agents.map((agent) => agent.name));
|
|
1245
|
+
const missing = WORKER_NAMES.filter(
|
|
1246
|
+
(worker) => skills.some((skill) => skill.body.includes(worker)) && !available.has(worker)
|
|
1247
|
+
);
|
|
1248
|
+
if (missing.length) {
|
|
1249
|
+
throw new Error(`Codex skills reference missing agent template(s): ${missing.join(", ")}.`);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
function codexAgentToml(agent) {
|
|
1253
|
+
const parsed = splitAgentFrontmatter(agent.body);
|
|
1254
|
+
if (!parsed.description) {
|
|
1255
|
+
throw new Error(`Codex agent '${agent.name}' requires a description in its leading YAML frontmatter.`);
|
|
1256
|
+
}
|
|
1257
|
+
return [
|
|
1258
|
+
`name = ${JSON.stringify(parsed.name || agent.name)}`,
|
|
1259
|
+
`description = ${JSON.stringify(parsed.description)}`,
|
|
1260
|
+
`developer_instructions = ${JSON.stringify(parsed.instructions.trimEnd())}`,
|
|
1261
|
+
""
|
|
1262
|
+
].join("\n");
|
|
1263
|
+
}
|
|
1158
1264
|
function writeAgents(dir, agents, surface) {
|
|
1159
1265
|
if (agents.length) mkdirSync3(dir, { recursive: true });
|
|
1160
1266
|
const written = [];
|
|
1161
1267
|
for (const a of agents) {
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1268
|
+
const codex = surface === CLIENT_SURFACE.codex;
|
|
1269
|
+
const file = `${a.name}.${codex ? "toml" : "md"}`;
|
|
1270
|
+
const body = codex ? codexAgentToml(a) : a.body.endsWith("\n") ? a.body : a.body + "\n";
|
|
1271
|
+
writeFileSync3(join4(dir, file), body);
|
|
1164
1272
|
written.push(file);
|
|
1165
1273
|
}
|
|
1166
1274
|
if (written.length) recordMaterialisedSkills(dir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -1177,11 +1285,71 @@ function writeReferencesIntoSkillDirs(dir, skills, refs) {
|
|
|
1177
1285
|
}
|
|
1178
1286
|
return refs.map((r) => r.name);
|
|
1179
1287
|
}
|
|
1180
|
-
var SKILL_SPEC = {
|
|
1181
|
-
|
|
1288
|
+
var SKILL_SPEC = {
|
|
1289
|
+
kind: "skill",
|
|
1290
|
+
dir: skillsDir,
|
|
1291
|
+
resolve: resolveSkillSet,
|
|
1292
|
+
write: writeSkills,
|
|
1293
|
+
supportsCodex: true
|
|
1294
|
+
};
|
|
1295
|
+
var AGENT_SPEC = {
|
|
1296
|
+
kind: "agent",
|
|
1297
|
+
dir: agentsDir,
|
|
1298
|
+
resolve: resolveAgentSet,
|
|
1299
|
+
write: writeAgents,
|
|
1300
|
+
supportsCodex: true
|
|
1301
|
+
};
|
|
1182
1302
|
function scopeOf(opts) {
|
|
1183
1303
|
return opts.local ? "project" : resolveScope(opts.scope);
|
|
1184
1304
|
}
|
|
1305
|
+
function parseClient(value) {
|
|
1306
|
+
if (value == null) return void 0;
|
|
1307
|
+
if (value === "claude" || value === "codex" || value === "all") return value;
|
|
1308
|
+
throw new Error(`--client must be 'claude', 'codex', or 'all' (got '${value}')`);
|
|
1309
|
+
}
|
|
1310
|
+
function clientSelection(spec, g, raw) {
|
|
1311
|
+
if (!spec.supportsCodex) return "claude";
|
|
1312
|
+
const explicit = parseClient(raw);
|
|
1313
|
+
if (explicit) return explicit;
|
|
1314
|
+
const claudeConfigured = Boolean(g.claudeConfigDir || process.env.CLAUDE_CONFIG_DIR);
|
|
1315
|
+
const codexConfigured = Boolean(g.codexHome || process.env.CODEX_HOME);
|
|
1316
|
+
if (claudeConfigured || codexConfigured) {
|
|
1317
|
+
if (claudeConfigured && codexConfigured) return "all";
|
|
1318
|
+
return codexConfigured ? "codex" : "claude";
|
|
1319
|
+
}
|
|
1320
|
+
const claudeDetected = resolveClaudeTargets({})[0]?.dir;
|
|
1321
|
+
const codexDetected = resolveCodexHomes({})[0];
|
|
1322
|
+
const hasClaude = Boolean(claudeDetected && existsSync3(claudeDetected));
|
|
1323
|
+
const hasCodex = Boolean(codexDetected && existsSync3(codexDetected));
|
|
1324
|
+
if (hasClaude && hasCodex) return "all";
|
|
1325
|
+
if (hasCodex) return "codex";
|
|
1326
|
+
return "claude";
|
|
1327
|
+
}
|
|
1328
|
+
function targetsFor(spec, g, scope, selection) {
|
|
1329
|
+
const clients = selection === "all" ? ["claude", "codex"] : [selection];
|
|
1330
|
+
if (scope === "project" && clients.includes("codex")) {
|
|
1331
|
+
throw new Error("Codex skills have no supported project scope; use --scope global or --client claude.");
|
|
1332
|
+
}
|
|
1333
|
+
const targets = [];
|
|
1334
|
+
for (const client of clients) {
|
|
1335
|
+
if (client === "claude") {
|
|
1336
|
+
targets.push(...resolveClaudeTargets({ override: g.claudeConfigDir, scope, cwd: process.cwd() }).map((target) => ({
|
|
1337
|
+
client,
|
|
1338
|
+
surface: CLIENT_SURFACE[client],
|
|
1339
|
+
dir: spec.dir(target.dir),
|
|
1340
|
+
label: target.label
|
|
1341
|
+
})));
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
targets.push(...resolveCodexHomes({ override: g.codexHome, scope }).map((home) => ({
|
|
1345
|
+
client,
|
|
1346
|
+
surface: CLIENT_SURFACE[client],
|
|
1347
|
+
dir: spec.dir(home),
|
|
1348
|
+
label: home
|
|
1349
|
+
})));
|
|
1350
|
+
}
|
|
1351
|
+
return targets;
|
|
1352
|
+
}
|
|
1185
1353
|
async function runInstall(spec, cmd, opts) {
|
|
1186
1354
|
const g = cmd.optsWithGlobals();
|
|
1187
1355
|
let scope;
|
|
@@ -1193,31 +1361,52 @@ async function runInstall(spec, cmd, opts) {
|
|
|
1193
1361
|
const cfg = resolveConfig(g);
|
|
1194
1362
|
const dryRun = Boolean(opts.dryRun);
|
|
1195
1363
|
const json = Boolean(g.json || opts.json);
|
|
1196
|
-
|
|
1364
|
+
let selection;
|
|
1365
|
+
let targets;
|
|
1366
|
+
try {
|
|
1367
|
+
selection = clientSelection(spec, g, opts.client);
|
|
1368
|
+
targets = targetsFor(spec, g, scope, selection);
|
|
1369
|
+
} catch (err2) {
|
|
1370
|
+
return fail(err2.message);
|
|
1371
|
+
}
|
|
1197
1372
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
1198
1373
|
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
1374
|
const results = targets.map((t) => {
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1375
|
+
const items = spec.resolve(rows, t.surface);
|
|
1376
|
+
if (t.client === "codex" && spec.kind === "skill") {
|
|
1377
|
+
validateCodexSkills(items);
|
|
1378
|
+
validateCodexWorkerDependencies(items, resolveAgentSet(rows, t.surface));
|
|
1379
|
+
}
|
|
1380
|
+
const refs = spec.kind === "skill" ? resolveReferenceSet(rows, t.surface) : [];
|
|
1381
|
+
const written = dryRun ? items.map((i) => i.name) : spec.write(t.dir, items, t.surface);
|
|
1382
|
+
const refsWritten = dryRun ? refs.map((r) => r.name) : writeReferencesIntoSkillDirs(t.dir, items, refs);
|
|
1383
|
+
return {
|
|
1384
|
+
client: t.client,
|
|
1385
|
+
surface: t.surface,
|
|
1386
|
+
dir: t.dir,
|
|
1387
|
+
label: t.label,
|
|
1388
|
+
available: items.length,
|
|
1389
|
+
references: refs.length,
|
|
1390
|
+
items: items.map(({ name, source }) => ({ name, source })),
|
|
1391
|
+
referenceItems: refs.map(({ name, source }) => ({ name, source })),
|
|
1392
|
+
written,
|
|
1393
|
+
refsWritten
|
|
1394
|
+
};
|
|
1206
1395
|
});
|
|
1207
|
-
if (json) return emit({ kind: spec.kind,
|
|
1208
|
-
if (
|
|
1396
|
+
if (json) return emit({ kind: spec.kind, client: selection, dryRun, targets: results }, true);
|
|
1397
|
+
if (results.every((r) => r.available === 0)) {
|
|
1209
1398
|
console.log(style.dim(`No ${spec.kind}s available to install \u2014 is the bundle installed for your account?`));
|
|
1210
1399
|
return;
|
|
1211
1400
|
}
|
|
1212
1401
|
for (const r of results) {
|
|
1213
1402
|
console.log(
|
|
1214
|
-
`${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.written.length} ${spec.kind}(s) ${style.dim("\u2192")} ${r.dir}`
|
|
1403
|
+
`${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.written.length} ${spec.kind}(s) for ${r.client} ${style.dim("\u2192")} ${r.dir}`
|
|
1215
1404
|
);
|
|
1216
1405
|
if (r.refsWritten.length)
|
|
1217
1406
|
console.log(
|
|
1218
1407
|
`${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.refsWritten.length} reference(s) into each skill ${style.dim("\u2192")} ${r.dir}/<skill>/references`
|
|
1219
1408
|
);
|
|
1220
|
-
if (dryRun) for (const i of items) console.log(` ${i.name} ${style.dim(`[${i.source}]`)}`);
|
|
1409
|
+
if (dryRun) for (const i of r.items) console.log(` ${i.name} ${style.dim(`[${i.source}]`)}`);
|
|
1221
1410
|
}
|
|
1222
1411
|
}
|
|
1223
1412
|
function runList(spec, cmd, opts) {
|
|
@@ -1229,16 +1418,22 @@ function runList(spec, cmd, opts) {
|
|
|
1229
1418
|
return fail(err2.message);
|
|
1230
1419
|
}
|
|
1231
1420
|
const json = Boolean(g.json || opts.json);
|
|
1232
|
-
|
|
1421
|
+
let selection;
|
|
1422
|
+
let targets;
|
|
1423
|
+
try {
|
|
1424
|
+
selection = clientSelection(spec, g, opts.client);
|
|
1425
|
+
targets = targetsFor(spec, g, scope, selection);
|
|
1426
|
+
} catch (err2) {
|
|
1427
|
+
return fail(err2.message);
|
|
1428
|
+
}
|
|
1233
1429
|
const out = targets.map((t) => {
|
|
1234
|
-
const
|
|
1235
|
-
const lock = readSkillsLock(dir);
|
|
1430
|
+
const lock = readSkillsLock(t.dir);
|
|
1236
1431
|
const entries = Object.entries(lock).flatMap(
|
|
1237
|
-
([slug, e]) => (e.skills ?? []).map((name) => ({ slug, name, present: existsSync3(join4(dir, name)) }))
|
|
1432
|
+
([slug, e]) => (e.skills ?? []).map((name) => ({ slug, name, present: existsSync3(join4(t.dir, name)) }))
|
|
1238
1433
|
);
|
|
1239
|
-
return { dir, label: t.label, entries };
|
|
1434
|
+
return { client: t.client, surface: t.surface, dir: t.dir, label: t.label, entries };
|
|
1240
1435
|
});
|
|
1241
|
-
if (json) return emit({ kind: spec.kind, targets: out }, true);
|
|
1436
|
+
if (json) return emit({ kind: spec.kind, client: selection, targets: out }, true);
|
|
1242
1437
|
let any = false;
|
|
1243
1438
|
for (const t of out) {
|
|
1244
1439
|
if (t.entries.length === 0) continue;
|
|
@@ -1261,33 +1456,39 @@ function runClean(spec, cmd, opts, slugArg) {
|
|
|
1261
1456
|
return fail(err2.message);
|
|
1262
1457
|
}
|
|
1263
1458
|
const json = Boolean(g.json || opts.json);
|
|
1264
|
-
|
|
1459
|
+
let selection;
|
|
1460
|
+
let targets;
|
|
1461
|
+
try {
|
|
1462
|
+
selection = clientSelection(spec, g, opts.client);
|
|
1463
|
+
targets = targetsFor(spec, g, scope, selection);
|
|
1464
|
+
} catch (err2) {
|
|
1465
|
+
return fail(err2.message);
|
|
1466
|
+
}
|
|
1265
1467
|
const cleaned = [];
|
|
1266
1468
|
const missing = [];
|
|
1267
1469
|
for (const t of targets) {
|
|
1268
|
-
const
|
|
1269
|
-
const lock = readSkillsLock(dir);
|
|
1470
|
+
const lock = readSkillsLock(t.dir);
|
|
1270
1471
|
const entry = lock[slug];
|
|
1271
1472
|
if (!entry) {
|
|
1272
|
-
missing.push(join4(dir, SKILLS_LOCK));
|
|
1473
|
+
missing.push(join4(t.dir, SKILLS_LOCK));
|
|
1273
1474
|
continue;
|
|
1274
1475
|
}
|
|
1275
1476
|
const removed = [];
|
|
1276
1477
|
for (const name of entry.skills) {
|
|
1277
|
-
const p = join4(dir, name);
|
|
1478
|
+
const p = join4(t.dir, name);
|
|
1278
1479
|
if (existsSync3(p)) {
|
|
1279
1480
|
rmSync2(p, { recursive: true, force: true });
|
|
1280
1481
|
removed.push(name);
|
|
1281
1482
|
}
|
|
1282
1483
|
}
|
|
1283
1484
|
delete lock[slug];
|
|
1284
|
-
writeSkillsLock(dir, lock);
|
|
1285
|
-
cleaned.push({ dir, removed });
|
|
1485
|
+
writeSkillsLock(t.dir, lock);
|
|
1486
|
+
cleaned.push({ client: t.client, surface: t.surface, dir: t.dir, removed });
|
|
1286
1487
|
}
|
|
1287
1488
|
if (cleaned.length === 0) {
|
|
1288
1489
|
return fail(`No materialised ${spec.kind}s recorded for '${slug}' in ${missing.join(", ")}.`);
|
|
1289
1490
|
}
|
|
1290
|
-
if (json) return emit({ kind: spec.kind, slug, cleaned, missing }, true);
|
|
1491
|
+
if (json) return emit({ kind: spec.kind, client: selection, slug, cleaned, missing }, true);
|
|
1291
1492
|
for (const c of cleaned) {
|
|
1292
1493
|
console.log(style.green(`Removed ${c.removed.length} ${spec.kind}(s) for ${slug} from ${c.dir}`));
|
|
1293
1494
|
}
|
|
@@ -1301,22 +1502,24 @@ function registerAgents(program2) {
|
|
|
1301
1502
|
`
|
|
1302
1503
|
Examples:
|
|
1303
1504
|
$ sechroom agents install materialise your installed agents to ~/.claude/agents
|
|
1505
|
+
$ sechroom agents install --client codex materialise native TOML agents to ~/.codex/agents
|
|
1304
1506
|
$ sechroom agents install --scope project write them to ./.claude/agents instead
|
|
1305
1507
|
$ sechroom agents install --claude-config-dir ~/.claude-work target another instance
|
|
1306
1508
|
$ sechroom agents list what's materialised on disk
|
|
1307
1509
|
$ sechroom agents clean remove the materialised agent files
|
|
1308
1510
|
|
|
1309
|
-
Agents are resolved from the agent target (target:claude-agent
|
|
1310
|
-
workers your loop skills call
|
|
1511
|
+
Agents are resolved from the client's agent target (target:claude-agent or
|
|
1512
|
+
target:gpt-codex-agent), the dispatchable workers your loop skills call
|
|
1513
|
+
(e.g. find-prior-art \u2192 substrate-miner).`
|
|
1311
1514
|
);
|
|
1312
|
-
agents.command("install").description("Materialise your installed subagents 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(AGENT_SPEC, cmd, opts));
|
|
1313
|
-
agents.command("list").description("List the subagents 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(AGENT_SPEC, cmd, opts));
|
|
1314
|
-
agents.command("clean [slug]").description(`Remove subagent 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(AGENT_SPEC, cmd, opts, slugArg));
|
|
1515
|
+
agents.command("install").description("Materialise your installed subagents 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("--client <client>", "claude, codex, or all").option("--json", "machine output").action((opts, cmd) => runInstall(AGENT_SPEC, cmd, opts));
|
|
1516
|
+
agents.command("list").description("List the subagents 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").option("--client <client>", "claude, codex, or all").action((opts, cmd) => runList(AGENT_SPEC, cmd, opts));
|
|
1517
|
+
agents.command("clean [slug]").description(`Remove subagent 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").option("--client <client>", "claude, codex, or all").action((slugArg, opts, cmd) => runClean(AGENT_SPEC, cmd, opts, slugArg));
|
|
1315
1518
|
}
|
|
1316
1519
|
|
|
1317
1520
|
// src/commands/channel.ts
|
|
1318
|
-
import { existsSync as
|
|
1319
|
-
import { dirname as
|
|
1521
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
1522
|
+
import { dirname as dirname6, join as join9 } from "path";
|
|
1320
1523
|
import {
|
|
1321
1524
|
HttpTransportType,
|
|
1322
1525
|
HubConnectionBuilder
|
|
@@ -1324,52 +1527,218 @@ import {
|
|
|
1324
1527
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1325
1528
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1326
1529
|
|
|
1530
|
+
// src/commands/executor.ts
|
|
1531
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
1532
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
1533
|
+
|
|
1534
|
+
// src/sem.ts
|
|
1535
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
1536
|
+
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync4 } from "fs";
|
|
1537
|
+
var SEM_FILE = join5(".sechroom", "lane.json");
|
|
1538
|
+
var STATE_DIR_NAME2 = ".sechroom";
|
|
1539
|
+
function localSemPath(cwd = process.cwd()) {
|
|
1540
|
+
return join5(cwd, SEM_FILE);
|
|
1541
|
+
}
|
|
1542
|
+
function resolveSemPathForRead(start = process.cwd()) {
|
|
1543
|
+
let dir = start;
|
|
1544
|
+
while (true) {
|
|
1545
|
+
const candidate = join5(dir, SEM_FILE);
|
|
1546
|
+
if (existsSync4(candidate)) return candidate;
|
|
1547
|
+
const parent = dirname2(dir);
|
|
1548
|
+
if (parent === dir) return void 0;
|
|
1549
|
+
dir = parent;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
|
|
1553
|
+
try {
|
|
1554
|
+
let dir = start;
|
|
1555
|
+
let gitPath;
|
|
1556
|
+
for (; ; ) {
|
|
1557
|
+
const candidate = join5(dir, ".git");
|
|
1558
|
+
if (existsSync4(candidate)) {
|
|
1559
|
+
gitPath = candidate;
|
|
1560
|
+
break;
|
|
1561
|
+
}
|
|
1562
|
+
const parent = dirname2(dir);
|
|
1563
|
+
if (parent === dir) break;
|
|
1564
|
+
dir = parent;
|
|
1565
|
+
}
|
|
1566
|
+
if (!gitPath || statSync(gitPath).isDirectory()) return lane;
|
|
1567
|
+
const gitFile = readFileSync3(gitPath, "utf8");
|
|
1568
|
+
const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
|
|
1569
|
+
if (!common) return lane;
|
|
1570
|
+
const worktreesDir = join5(common[1], "worktrees");
|
|
1571
|
+
const siblings = readdirSync(worktreesDir).filter((n) => {
|
|
1572
|
+
try {
|
|
1573
|
+
return statSync(join5(worktreesDir, n)).isDirectory();
|
|
1574
|
+
} catch {
|
|
1575
|
+
return false;
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
return laneWithWorktreeSuffix(lane, gitFile, siblings);
|
|
1579
|
+
} catch {
|
|
1580
|
+
return lane;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
function laneWithWorktreeSuffix(lane, gitFile, siblings) {
|
|
1584
|
+
const m = gitFile.trim().match(/\/worktrees\/([^/\s]+)\s*$/);
|
|
1585
|
+
if (!m) return lane;
|
|
1586
|
+
const idx = [...siblings].sort().indexOf(m[1]);
|
|
1587
|
+
return idx < 0 ? lane : `${lane}-${idx + 2}`;
|
|
1588
|
+
}
|
|
1589
|
+
function serializeSem(values) {
|
|
1590
|
+
return JSON.stringify(values, null, 2) + "\n";
|
|
1591
|
+
}
|
|
1592
|
+
function readSem(path) {
|
|
1593
|
+
const p = path ?? resolveSemPathForRead();
|
|
1594
|
+
if (!p || !existsSync4(p)) return void 0;
|
|
1595
|
+
return { path: p, values: parseLaneJson(readFileSync3(p, "utf8")) };
|
|
1596
|
+
}
|
|
1597
|
+
function readLocalSemValues(cwd = process.cwd()) {
|
|
1598
|
+
const next = join5(cwd, SEM_FILE);
|
|
1599
|
+
if (existsSync4(next)) return readSem(next)?.values ?? {};
|
|
1600
|
+
return {};
|
|
1601
|
+
}
|
|
1602
|
+
function parseLaneJson(text2) {
|
|
1603
|
+
try {
|
|
1604
|
+
const parsed = JSON.parse(text2);
|
|
1605
|
+
const out = {};
|
|
1606
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
1607
|
+
if (typeof v === "string") out[k] = v;
|
|
1608
|
+
}
|
|
1609
|
+
return out;
|
|
1610
|
+
} catch {
|
|
1611
|
+
return {};
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
var STATE_DIR_IGNORE = `${STATE_DIR_NAME2}/`;
|
|
1615
|
+
function writeSem(values, path = localSemPath()) {
|
|
1616
|
+
mkdirSync4(dirname2(path), { recursive: true });
|
|
1617
|
+
writeFileSync4(path, serializeSem(values));
|
|
1618
|
+
ensureSemIgnored(path);
|
|
1619
|
+
ensureContinuityScaffold(path);
|
|
1620
|
+
return path;
|
|
1621
|
+
}
|
|
1622
|
+
function ensureStateDirIgnored(cwd = process.cwd()) {
|
|
1623
|
+
ensureSemIgnored(localSemPath(cwd));
|
|
1624
|
+
}
|
|
1625
|
+
var CONTINUITY_FILE_NAME = "continuity.json";
|
|
1626
|
+
var CONTINUITY_SCAFFOLD = JSON.stringify(
|
|
1627
|
+
{
|
|
1628
|
+
_readme: "Agent-maintained continuity intent. Keep these current during the session; `sechroom checkpoint` and the PreCompact hook snapshot from here. The five required fields (objective, state, lastAction, nextAction, resumeInstruction) must all be non-empty for a snapshot to be created.",
|
|
1629
|
+
objective: "",
|
|
1630
|
+
state: "",
|
|
1631
|
+
lastAction: "",
|
|
1632
|
+
nextAction: "",
|
|
1633
|
+
resumeInstruction: "",
|
|
1634
|
+
constraints: [],
|
|
1635
|
+
questions: [],
|
|
1636
|
+
artifacts: [],
|
|
1637
|
+
confidence: null
|
|
1638
|
+
},
|
|
1639
|
+
null,
|
|
1640
|
+
2
|
|
1641
|
+
) + "\n";
|
|
1642
|
+
function ensureContinuityScaffold(semPath) {
|
|
1643
|
+
try {
|
|
1644
|
+
const target = join5(dirname2(semPath), CONTINUITY_FILE_NAME);
|
|
1645
|
+
if (existsSync4(target)) return;
|
|
1646
|
+
writeFileSync4(target, CONTINUITY_SCAFFOLD);
|
|
1647
|
+
} catch {
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
function ignoresSem(content) {
|
|
1651
|
+
return content.split("\n").some((line) => {
|
|
1652
|
+
const t = line.trim();
|
|
1653
|
+
return t === STATE_DIR_NAME2 || t === STATE_DIR_IGNORE || t === `/${STATE_DIR_NAME2}` || t === `/${STATE_DIR_IGNORE}` || t === `**/${STATE_DIR_NAME2}` || t === `**/${STATE_DIR_IGNORE}`;
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
function inGitRepo(startDir) {
|
|
1657
|
+
let dir = startDir;
|
|
1658
|
+
for (; ; ) {
|
|
1659
|
+
if (existsSync4(join5(dir, ".git"))) return true;
|
|
1660
|
+
const parent = dirname2(dir);
|
|
1661
|
+
if (parent === dir) return false;
|
|
1662
|
+
dir = parent;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
function resolveGitignoreTarget(startDir) {
|
|
1666
|
+
let dir = startDir;
|
|
1667
|
+
for (; ; ) {
|
|
1668
|
+
const gi = join5(dir, ".gitignore");
|
|
1669
|
+
if (existsSync4(gi)) return { path: gi, exists: true };
|
|
1670
|
+
const parent = dirname2(dir);
|
|
1671
|
+
if (existsSync4(join5(dir, ".git")) || parent === dir) {
|
|
1672
|
+
return { path: join5(startDir, ".gitignore"), exists: false };
|
|
1673
|
+
}
|
|
1674
|
+
dir = parent;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
function ensureSemIgnored(semPath) {
|
|
1678
|
+
try {
|
|
1679
|
+
const checkoutDir = dirname2(dirname2(semPath));
|
|
1680
|
+
if (!inGitRepo(checkoutDir)) return;
|
|
1681
|
+
const target = resolveGitignoreTarget(checkoutDir);
|
|
1682
|
+
if (target.exists) {
|
|
1683
|
+
const content = readFileSync3(target.path, "utf8");
|
|
1684
|
+
if (ignoresSem(content)) return;
|
|
1685
|
+
const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
1686
|
+
appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
|
|
1687
|
+
`);
|
|
1688
|
+
} else {
|
|
1689
|
+
writeFileSync4(target.path, `${STATE_DIR_IGNORE}
|
|
1690
|
+
`);
|
|
1691
|
+
}
|
|
1692
|
+
} catch {
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1327
1696
|
// src/commands/hook-install.ts
|
|
1328
|
-
import { existsSync as
|
|
1329
|
-
import { delimiter, dirname as
|
|
1697
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
1698
|
+
import { delimiter, dirname as dirname4, join as join7 } from "path";
|
|
1330
1699
|
|
|
1331
1700
|
// src/setup/clients.ts
|
|
1332
|
-
import { existsSync as
|
|
1701
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1333
1702
|
import { homedir as homedir3 } from "os";
|
|
1334
|
-
import { dirname as
|
|
1703
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
1335
1704
|
function claudeDesktopConfigPath(home) {
|
|
1336
1705
|
switch (process.platform) {
|
|
1337
1706
|
case "darwin":
|
|
1338
|
-
return
|
|
1707
|
+
return join6(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
1339
1708
|
case "win32":
|
|
1340
|
-
return
|
|
1709
|
+
return join6(process.env.APPDATA ?? join6(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
1341
1710
|
default:
|
|
1342
|
-
return
|
|
1711
|
+
return join6(home, ".config", "Claude", "claude_desktop_config.json");
|
|
1343
1712
|
}
|
|
1344
1713
|
}
|
|
1345
1714
|
function clientTargets(cwd, opts = {}) {
|
|
1346
1715
|
const home = homedir3();
|
|
1347
|
-
const claudeDir = opts.claudeDir ??
|
|
1348
|
-
const codexHome = opts.codexHome ??
|
|
1716
|
+
const claudeDir = opts.claudeDir ?? join6(home, ".claude");
|
|
1717
|
+
const codexHome = opts.codexHome ?? join6(home, ".codex");
|
|
1349
1718
|
return {
|
|
1350
1719
|
"claude-code": {
|
|
1351
1720
|
key: "claude-code",
|
|
1352
1721
|
label: "Claude Code",
|
|
1353
|
-
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path:
|
|
1354
|
-
instruction: { surfaceKey: "claude-code", path:
|
|
1722
|
+
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join6(cwd, ".mcp.json"), format: "json" },
|
|
1723
|
+
instruction: { surfaceKey: "claude-code", path: join6(cwd, "CLAUDE.md") }
|
|
1355
1724
|
},
|
|
1356
1725
|
"claude-desktop": {
|
|
1357
1726
|
key: "claude-desktop",
|
|
1358
1727
|
label: "Claude Desktop",
|
|
1359
1728
|
mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
|
|
1360
|
-
instruction: { surfaceKey: "claude-desktop", path:
|
|
1729
|
+
instruction: { surfaceKey: "claude-desktop", path: join6(claudeDir, "CLAUDE.md") }
|
|
1361
1730
|
},
|
|
1362
1731
|
codex: {
|
|
1363
1732
|
key: "codex",
|
|
1364
1733
|
label: "Codex CLI",
|
|
1365
|
-
mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path:
|
|
1366
|
-
instruction: { surfaceKey: "chatgpt", path:
|
|
1734
|
+
mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join6(codexHome, "config.toml"), format: "toml" },
|
|
1735
|
+
instruction: { surfaceKey: "chatgpt", path: join6(cwd, "AGENTS.md") }
|
|
1367
1736
|
},
|
|
1368
1737
|
cursor: {
|
|
1369
1738
|
key: "cursor",
|
|
1370
1739
|
label: "Cursor",
|
|
1371
|
-
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path:
|
|
1372
|
-
instruction: { surfaceKey: "chatgpt", path:
|
|
1740
|
+
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join6(cwd, ".cursor", "mcp.json"), format: "json" },
|
|
1741
|
+
instruction: { surfaceKey: "chatgpt", path: join6(cwd, "AGENTS.md") }
|
|
1373
1742
|
},
|
|
1374
1743
|
antigravity: {
|
|
1375
1744
|
key: "antigravity",
|
|
@@ -1380,8 +1749,8 @@ function clientTargets(cwd, opts = {}) {
|
|
|
1380
1749
|
// `type` — comes from the `antigravity` server surface, so we don't
|
|
1381
1750
|
// hardcode it here. Instructions go in the project `AGENTS.md`
|
|
1382
1751
|
// (cross-tool, shared with Codex/Cursor).
|
|
1383
|
-
mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path:
|
|
1384
|
-
instruction: { surfaceKey: "antigravity", path:
|
|
1752
|
+
mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path: join6(home, ".gemini", "config", "mcp_config.json"), format: "json" },
|
|
1753
|
+
instruction: { surfaceKey: "antigravity", path: join6(cwd, "AGENTS.md") }
|
|
1385
1754
|
}
|
|
1386
1755
|
};
|
|
1387
1756
|
}
|
|
@@ -1390,11 +1759,11 @@ var DEFAULT_CLIENT_KEY = "claude-code";
|
|
|
1390
1759
|
function detectInstalledClients(cwd) {
|
|
1391
1760
|
const home = homedir3();
|
|
1392
1761
|
const detected = [];
|
|
1393
|
-
if (resolveClaudeTargets({}).some((t) =>
|
|
1394
|
-
if (
|
|
1395
|
-
if (resolveCodexHomes({}).some((d) =>
|
|
1396
|
-
if (
|
|
1397
|
-
if (
|
|
1762
|
+
if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir))) detected.push("claude-code");
|
|
1763
|
+
if (existsSync5(dirname3(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
|
|
1764
|
+
if (resolveCodexHomes({}).some((d) => existsSync5(d))) detected.push("codex");
|
|
1765
|
+
if (existsSync5(join6(home, ".cursor")) || existsSync5(join6(cwd, ".cursor"))) detected.push("cursor");
|
|
1766
|
+
if (existsSync5(join6(home, ".gemini"))) detected.push("antigravity");
|
|
1398
1767
|
return detected;
|
|
1399
1768
|
}
|
|
1400
1769
|
|
|
@@ -1415,8 +1784,12 @@ var CLAUDE_HOOK_COMMANDS = {
|
|
|
1415
1784
|
};
|
|
1416
1785
|
var CODEX_HOOK_COMMANDS = {
|
|
1417
1786
|
SessionStart: "sechroom hook session-start",
|
|
1787
|
+
PreCompact: "sechroom hook pre-compact",
|
|
1418
1788
|
Stop: "sechroom hook session-end --debounce-minutes 10"
|
|
1419
1789
|
};
|
|
1790
|
+
function hookCommandsForSurface(surface) {
|
|
1791
|
+
return surface === "claude" ? CLAUDE_HOOK_COMMANDS : CODEX_HOOK_COMMANDS;
|
|
1792
|
+
}
|
|
1420
1793
|
function hasHookCommand(config2, event, command) {
|
|
1421
1794
|
const groups = config2.hooks?.[event] ?? [];
|
|
1422
1795
|
return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
|
|
@@ -1433,24 +1806,30 @@ function mergeHooks(config2, commands) {
|
|
|
1433
1806
|
return added;
|
|
1434
1807
|
}
|
|
1435
1808
|
function readJsonConfig2(path) {
|
|
1436
|
-
if (!
|
|
1437
|
-
const raw =
|
|
1809
|
+
if (!existsSync6(path)) return {};
|
|
1810
|
+
const raw = readFileSync4(path, "utf8");
|
|
1438
1811
|
if (!raw.trim()) return {};
|
|
1439
1812
|
return JSON.parse(raw);
|
|
1440
1813
|
}
|
|
1441
1814
|
function installHooksJson(path, commands, dryRun) {
|
|
1442
|
-
const existed =
|
|
1815
|
+
const existed = existsSync6(path) && readFileSync4(path, "utf8").trim().length > 0;
|
|
1443
1816
|
const config2 = readJsonConfig2(path);
|
|
1444
1817
|
const added = mergeHooks(config2, commands);
|
|
1445
1818
|
if (added === 0 && existed) return { path, status: "current" };
|
|
1446
1819
|
if (!dryRun) {
|
|
1447
|
-
|
|
1448
|
-
|
|
1820
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
1821
|
+
writeFileSync5(path, JSON.stringify(config2, null, 2) + "\n");
|
|
1449
1822
|
}
|
|
1450
1823
|
return { path, status: existed ? "merged" : "created" };
|
|
1451
1824
|
}
|
|
1452
1825
|
function installClaudeCommands(claudeDir, commands, dryRun) {
|
|
1453
|
-
return installHooksJson(
|
|
1826
|
+
return installHooksJson(join7(claudeDir, "settings.json"), commands, dryRun);
|
|
1827
|
+
}
|
|
1828
|
+
function installCodexCommands(codexHome, commands, dryRun) {
|
|
1829
|
+
return [
|
|
1830
|
+
installHooksJson(join7(codexHome, "hooks.json"), commands, dryRun),
|
|
1831
|
+
installCodexFeatureFlag(join7(codexHome, "config.toml"), dryRun)
|
|
1832
|
+
];
|
|
1454
1833
|
}
|
|
1455
1834
|
function ensureCodexFeaturesHooks(content) {
|
|
1456
1835
|
const lines = content.split("\n");
|
|
@@ -1473,13 +1852,13 @@ function ensureCodexFeaturesHooks(content) {
|
|
|
1473
1852
|
return { next: lines.join("\n"), changed: true };
|
|
1474
1853
|
}
|
|
1475
1854
|
function installCodexFeatureFlag(path, dryRun) {
|
|
1476
|
-
const existed =
|
|
1477
|
-
const content = existed ?
|
|
1855
|
+
const existed = existsSync6(path);
|
|
1856
|
+
const content = existed ? readFileSync4(path, "utf8") : "";
|
|
1478
1857
|
const { next, changed } = ensureCodexFeaturesHooks(content);
|
|
1479
1858
|
if (!changed) return { path, status: "current" };
|
|
1480
1859
|
if (!dryRun) {
|
|
1481
|
-
|
|
1482
|
-
|
|
1860
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
1861
|
+
writeFileSync5(path, next);
|
|
1483
1862
|
}
|
|
1484
1863
|
return { path, status: existed ? "merged" : "created" };
|
|
1485
1864
|
}
|
|
@@ -1504,11 +1883,11 @@ function installHookSurfaces(surfaces, opts) {
|
|
|
1504
1883
|
const out = [];
|
|
1505
1884
|
for (const surface of surfaces) {
|
|
1506
1885
|
if (surface === "claude") {
|
|
1507
|
-
const path =
|
|
1886
|
+
const path = join7(opts.claudeDir, "settings.json");
|
|
1508
1887
|
out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
|
|
1509
1888
|
} else {
|
|
1510
|
-
const hooksJson = installHooksJson(
|
|
1511
|
-
const featureFlag = installCodexFeatureFlag(
|
|
1889
|
+
const hooksJson = installHooksJson(join7(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
|
|
1890
|
+
const featureFlag = installCodexFeatureFlag(join7(opts.codexHome, "config.toml"), opts.dryRun);
|
|
1512
1891
|
out.push({ surface, results: [hooksJson, featureFlag] });
|
|
1513
1892
|
}
|
|
1514
1893
|
}
|
|
@@ -1528,7 +1907,7 @@ function isSechroomOnPath() {
|
|
|
1528
1907
|
for (const dir of pathEnv.split(delimiter)) {
|
|
1529
1908
|
if (!dir) continue;
|
|
1530
1909
|
for (const name of names) {
|
|
1531
|
-
if (
|
|
1910
|
+
if (existsSync6(join7(dir, name))) return true;
|
|
1532
1911
|
}
|
|
1533
1912
|
}
|
|
1534
1913
|
return false;
|
|
@@ -1541,6 +1920,446 @@ function warnIfSechroomNotOnPath(write = (s) => void process.stderr.write(s)) {
|
|
|
1541
1920
|
return true;
|
|
1542
1921
|
}
|
|
1543
1922
|
|
|
1923
|
+
// src/commands/executor.ts
|
|
1924
|
+
function executorSubscriptionInput(name) {
|
|
1925
|
+
return {
|
|
1926
|
+
name,
|
|
1927
|
+
enabled: true,
|
|
1928
|
+
filter: { tags: ["kind:task"], workspaceScope: [] }
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
function executorRegistrationInput(state, deliverySubscriptionId) {
|
|
1932
|
+
return {
|
|
1933
|
+
relayId: state.relayId,
|
|
1934
|
+
instanceKey: state.instanceKey,
|
|
1935
|
+
laneId: state.laneId ?? state.instanceKey,
|
|
1936
|
+
runtimeKind: parseRuntimeKind(state.runtime),
|
|
1937
|
+
activationMode: "Attached",
|
|
1938
|
+
deliverySubscriptionId,
|
|
1939
|
+
connectorId: state.connectorId,
|
|
1940
|
+
claimedCapabilityKeys: state.capabilityKeys,
|
|
1941
|
+
toolSetRef: null,
|
|
1942
|
+
ttlSeconds: state.ttlSeconds
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
var EXECUTOR_STATE = "executor.json";
|
|
1946
|
+
var EXECUTOR_PULSE_COMMAND = "sechroom executor hook-pulse";
|
|
1947
|
+
var EXECUTOR_STOP_COMMAND = "sechroom executor hook-stop";
|
|
1948
|
+
var CLAUDE_EXECUTOR_HOOKS = {
|
|
1949
|
+
SessionStart: EXECUTOR_PULSE_COMMAND,
|
|
1950
|
+
UserPromptSubmit: EXECUTOR_PULSE_COMMAND,
|
|
1951
|
+
PreToolUse: EXECUTOR_PULSE_COMMAND,
|
|
1952
|
+
Stop: EXECUTOR_PULSE_COMMAND,
|
|
1953
|
+
SessionEnd: EXECUTOR_STOP_COMMAND
|
|
1954
|
+
};
|
|
1955
|
+
var CODEX_EXECUTOR_HOOKS = {
|
|
1956
|
+
SessionStart: EXECUTOR_PULSE_COMMAND,
|
|
1957
|
+
UserPromptSubmit: EXECUTOR_PULSE_COMMAND,
|
|
1958
|
+
PreToolUse: EXECUTOR_PULSE_COMMAND,
|
|
1959
|
+
Stop: EXECUTOR_PULSE_COMMAND
|
|
1960
|
+
};
|
|
1961
|
+
function registerExecutor(program2) {
|
|
1962
|
+
const executor = program2.command("executor").description(
|
|
1963
|
+
"Register and operate a local Claude Code/Codex executor advertisement"
|
|
1964
|
+
);
|
|
1965
|
+
executor.command("install").description(
|
|
1966
|
+
"Configure this checkout's harness to advertise itself as a WLP executor"
|
|
1967
|
+
).option("--connector <id>", "Approved local-session ConnectorDefinition id").option(
|
|
1968
|
+
"--instance-key <key>",
|
|
1969
|
+
"Stable executor identity (defaults to .sechroom/lane.json code-lane)"
|
|
1970
|
+
).option(
|
|
1971
|
+
"--lane-id <lane>",
|
|
1972
|
+
"Canonical affinity lane (defaults to .sechroom/lane.json code-lane)"
|
|
1973
|
+
).option("--runtime <kind>", "claude-code | codex").option("--surface <surface>", "claude | codex").option(
|
|
1974
|
+
"--capability <key...>",
|
|
1975
|
+
"Capability operation keys claimed by this instance"
|
|
1976
|
+
).option(
|
|
1977
|
+
"--relay <id>",
|
|
1978
|
+
"Relay identity shared by sibling instances",
|
|
1979
|
+
"sechroom-cli-local"
|
|
1980
|
+
).option(
|
|
1981
|
+
"--subscription-name <name>",
|
|
1982
|
+
"SignalR delivery binding name",
|
|
1983
|
+
"executor-dispatch"
|
|
1984
|
+
).option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 600).option(
|
|
1985
|
+
"--refresh-after <seconds>",
|
|
1986
|
+
"Minimum age before a hook refreshes",
|
|
1987
|
+
parseInteger,
|
|
1988
|
+
40
|
|
1989
|
+
).option(
|
|
1990
|
+
"-y, --yes",
|
|
1991
|
+
"Non-interactive: accept detected surface and lane defaults",
|
|
1992
|
+
false
|
|
1993
|
+
).option("--dry-run", "Show hook files without writing", false).action(async (opts, cmd) => {
|
|
1994
|
+
const globals = cmd.optsWithGlobals();
|
|
1995
|
+
const lane = readSem()?.values["code-lane"];
|
|
1996
|
+
const detected = detectHookSurfaces(process.cwd());
|
|
1997
|
+
let surface = opts.surface;
|
|
1998
|
+
let instanceKey = opts.instanceKey;
|
|
1999
|
+
let runtime = opts.runtime;
|
|
2000
|
+
let laneId = opts.laneId;
|
|
2001
|
+
let connector = opts.connector;
|
|
2002
|
+
let capabilities = opts.capability;
|
|
2003
|
+
const surfaceDefault = detected.length === 1 ? detected[0] : lane?.includes("codex") ? "codex" : "claude";
|
|
2004
|
+
if (!opts.yes && canPrompt()) {
|
|
2005
|
+
surface = await promptText(
|
|
2006
|
+
"Harness surface (claude or codex)?",
|
|
2007
|
+
surface ?? surfaceDefault
|
|
2008
|
+
);
|
|
2009
|
+
instanceKey = await promptText(
|
|
2010
|
+
"Executor instance key?",
|
|
2011
|
+
instanceKey ?? lane ?? ""
|
|
2012
|
+
);
|
|
2013
|
+
laneId = await promptText(
|
|
2014
|
+
"Executor affinity lane?",
|
|
2015
|
+
laneId ?? lane ?? ""
|
|
2016
|
+
);
|
|
2017
|
+
runtime = await promptText(
|
|
2018
|
+
"Runtime (claude-code or codex)?",
|
|
2019
|
+
runtime ?? (surface === "codex" ? "codex" : "claude-code")
|
|
2020
|
+
);
|
|
2021
|
+
connector = await promptText(
|
|
2022
|
+
"Approved local-session connector id?",
|
|
2023
|
+
connector ?? ""
|
|
2024
|
+
);
|
|
2025
|
+
const capabilityText = await promptText(
|
|
2026
|
+
"Capability keys (comma-separated; blank for none)?",
|
|
2027
|
+
capabilities?.join(",") ?? ""
|
|
2028
|
+
);
|
|
2029
|
+
capabilities = capabilityText.split(",").map((x) => x.trim()).filter(Boolean);
|
|
2030
|
+
}
|
|
2031
|
+
surface ??= surfaceDefault;
|
|
2032
|
+
instanceKey ??= lane;
|
|
2033
|
+
laneId ??= lane;
|
|
2034
|
+
runtime ??= surface === "codex" ? "codex" : "claude-code";
|
|
2035
|
+
if (!connector)
|
|
2036
|
+
fail(
|
|
2037
|
+
"executor install requires --connector (or an interactive connector id)"
|
|
2038
|
+
);
|
|
2039
|
+
if (!instanceKey)
|
|
2040
|
+
fail(
|
|
2041
|
+
"no instance key resolved; pass --instance-key or pin .sechroom/lane.json code-lane"
|
|
2042
|
+
);
|
|
2043
|
+
if (!opts.yes && !canPrompt())
|
|
2044
|
+
fail("non-interactive executor install requires --yes");
|
|
2045
|
+
parseRuntimeKind(runtime);
|
|
2046
|
+
if (!["claude", "codex"].includes(surface))
|
|
2047
|
+
fail("surface must be claude or codex");
|
|
2048
|
+
if (opts.refreshAfter >= opts.ttl)
|
|
2049
|
+
fail("refresh-after must be shorter than the TTL");
|
|
2050
|
+
const sem = readSem();
|
|
2051
|
+
const checkout = sem ? dirname5(dirname5(sem.path)) : process.cwd();
|
|
2052
|
+
const statePath = join8(checkout, ".sechroom", EXECUTOR_STATE);
|
|
2053
|
+
const state = {
|
|
2054
|
+
schemaVersion: 1,
|
|
2055
|
+
instanceKey,
|
|
2056
|
+
laneId,
|
|
2057
|
+
runtime: runtime.toLowerCase() === "codex" ? "codex" : "claude-code",
|
|
2058
|
+
connectorId: connector,
|
|
2059
|
+
capabilityKeys: capabilities ?? [],
|
|
2060
|
+
relayId: opts.relay,
|
|
2061
|
+
subscriptionName: opts.subscriptionName,
|
|
2062
|
+
ttlSeconds: opts.ttl,
|
|
2063
|
+
refreshAfterSeconds: opts.refreshAfter
|
|
2064
|
+
};
|
|
2065
|
+
if (!opts.dryRun) {
|
|
2066
|
+
mkdirSync6(dirname5(statePath), { recursive: true });
|
|
2067
|
+
writeFileSync6(statePath, JSON.stringify(state, null, 2) + "\n");
|
|
2068
|
+
ensureStateDirIgnored(checkout);
|
|
2069
|
+
}
|
|
2070
|
+
const configuredClaudeDirs = globals.claudeConfigDir || process.env.CLAUDE_CONFIG_DIR ? resolveClaudeTargets({ override: globals.claudeConfigDir }).map(
|
|
2071
|
+
(target) => target.dir
|
|
2072
|
+
) : [join8(checkout, ".claude")];
|
|
2073
|
+
const configuredCodexHomes = globals.codexHome || process.env.CODEX_HOME ? resolveCodexHomes({ override: globals.codexHome }) : [join8(checkout, ".codex")];
|
|
2074
|
+
const hookTargets = surface === "claude" ? configuredClaudeDirs : configuredCodexHomes;
|
|
2075
|
+
for (const target of hookTargets) {
|
|
2076
|
+
const results = surface === "claude" ? [
|
|
2077
|
+
installClaudeCommands(
|
|
2078
|
+
target,
|
|
2079
|
+
CLAUDE_EXECUTOR_HOOKS,
|
|
2080
|
+
opts.dryRun
|
|
2081
|
+
)
|
|
2082
|
+
] : installCodexCommands(target, CODEX_EXECUTOR_HOOKS, opts.dryRun);
|
|
2083
|
+
for (const result of results)
|
|
2084
|
+
process.stderr.write(describe(result, opts.dryRun) + "\n");
|
|
2085
|
+
}
|
|
2086
|
+
warnIfSechroomNotOnPath();
|
|
2087
|
+
process.stderr.write(
|
|
2088
|
+
style.green("executor harness configured") + style.dim(` \u2014 ${instanceKey}
|
|
2089
|
+
`)
|
|
2090
|
+
);
|
|
2091
|
+
});
|
|
2092
|
+
executor.command("hook-pulse").description("Hook adapter: register or refresh this checkout's executor").action(async (_opts, cmd) => {
|
|
2093
|
+
await drainStdin();
|
|
2094
|
+
const located = readExecutorState();
|
|
2095
|
+
if (!located) return;
|
|
2096
|
+
const { state, path } = located;
|
|
2097
|
+
const age = state.lastRefreshAt ? Date.now() - Date.parse(state.lastRefreshAt) : Number.POSITIVE_INFINITY;
|
|
2098
|
+
if (state.instanceId && age < state.refreshAfterSeconds * 1e3) return;
|
|
2099
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2100
|
+
try {
|
|
2101
|
+
await ensureExecutorInstance(cfg, { state, path });
|
|
2102
|
+
} catch {
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
executor.command("hook-stop").description("Hook adapter: deregister this checkout's executor").action(async (_opts, cmd) => {
|
|
2106
|
+
await drainStdin();
|
|
2107
|
+
const located = readExecutorState();
|
|
2108
|
+
if (!located?.state.instanceId) return;
|
|
2109
|
+
try {
|
|
2110
|
+
await api(
|
|
2111
|
+
resolveConfig(cmd.optsWithGlobals()),
|
|
2112
|
+
`/me/executor-instances/${encodeURIComponent(located.state.instanceId)}`,
|
|
2113
|
+
{ method: "DELETE", body: JSON.stringify({}) }
|
|
2114
|
+
);
|
|
2115
|
+
delete located.state.instanceId;
|
|
2116
|
+
delete located.state.lastRefreshAt;
|
|
2117
|
+
writeFileSync6(
|
|
2118
|
+
located.path,
|
|
2119
|
+
JSON.stringify(located.state, null, 2) + "\n"
|
|
2120
|
+
);
|
|
2121
|
+
} catch {
|
|
2122
|
+
}
|
|
2123
|
+
});
|
|
2124
|
+
executor.command("submit-connector").description(
|
|
2125
|
+
"Submit a local-session connector definition for governed approval"
|
|
2126
|
+
).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", [
|
|
2127
|
+
"base",
|
|
2128
|
+
"dotnet-10"
|
|
2129
|
+
]).action(async (opts, cmd) => {
|
|
2130
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2131
|
+
const data = await api(cfg, "/connectors/definitions", {
|
|
2132
|
+
method: "POST",
|
|
2133
|
+
body: JSON.stringify({
|
|
2134
|
+
slug: opts.slug,
|
|
2135
|
+
displayName: opts.displayName,
|
|
2136
|
+
runtimeProfiles: opts.profile,
|
|
2137
|
+
connectorKind: "ExecutionRuntime",
|
|
2138
|
+
providerKind: "local-session",
|
|
2139
|
+
dispatchTransport: parseTransport(opts.transport)
|
|
2140
|
+
})
|
|
2141
|
+
});
|
|
2142
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
2143
|
+
if (!cmd.optsWithGlobals().json) {
|
|
2144
|
+
process.stderr.write(
|
|
2145
|
+
style.dim(
|
|
2146
|
+
"approve this connector definition before registering executors\n"
|
|
2147
|
+
)
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
});
|
|
2151
|
+
executor.command("register").description(
|
|
2152
|
+
"Create/reuse a SignalR binding and register this local executor instance"
|
|
2153
|
+
).requiredOption(
|
|
2154
|
+
"--instance-key <key>",
|
|
2155
|
+
"Stable key for this concrete session/lane"
|
|
2156
|
+
).requiredOption(
|
|
2157
|
+
"--connector <id>",
|
|
2158
|
+
"Approved local-session ConnectorDefinition id"
|
|
2159
|
+
).option("--runtime <kind>", "claude-code | codex", "claude-code").option(
|
|
2160
|
+
"--lane-id <lane>",
|
|
2161
|
+
"Canonical affinity lane (defaults to instance key)"
|
|
2162
|
+
).option(
|
|
2163
|
+
"--relay <id>",
|
|
2164
|
+
"Relay identity shared by sibling instances",
|
|
2165
|
+
"sechroom-cli-local"
|
|
2166
|
+
).option(
|
|
2167
|
+
"--subscription-name <name>",
|
|
2168
|
+
"SignalR delivery binding name",
|
|
2169
|
+
"executor-dispatch"
|
|
2170
|
+
).option(
|
|
2171
|
+
"--capability <key...>",
|
|
2172
|
+
"Capability operation keys claimed by this instance"
|
|
2173
|
+
).option("--tool-set-ref <ref>", "Optional governed tool-set reference").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (opts, cmd) => {
|
|
2174
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2175
|
+
const subscription = await api(
|
|
2176
|
+
cfg,
|
|
2177
|
+
"/me/delivery-subscriptions/signalr",
|
|
2178
|
+
{
|
|
2179
|
+
method: "POST",
|
|
2180
|
+
body: JSON.stringify({
|
|
2181
|
+
name: opts.subscriptionName,
|
|
2182
|
+
enabled: true,
|
|
2183
|
+
// Exact executor fanout ignores this generic filter; the fixed tag only
|
|
2184
|
+
// satisfies the legacy SignalR subscription shape.
|
|
2185
|
+
filter: { tags: ["kind:task"], workspaceScope: [] }
|
|
2186
|
+
})
|
|
2187
|
+
}
|
|
2188
|
+
);
|
|
2189
|
+
const data = await api(
|
|
2190
|
+
cfg,
|
|
2191
|
+
"/me/executor-instances",
|
|
2192
|
+
{
|
|
2193
|
+
method: "POST",
|
|
2194
|
+
body: JSON.stringify({
|
|
2195
|
+
relayId: opts.relay,
|
|
2196
|
+
instanceKey: opts.instanceKey,
|
|
2197
|
+
laneId: opts.laneId ?? opts.instanceKey,
|
|
2198
|
+
runtimeKind: parseRuntimeKind(opts.runtime),
|
|
2199
|
+
activationMode: "Attached",
|
|
2200
|
+
deliverySubscriptionId: subscription.id,
|
|
2201
|
+
connectorId: opts.connector,
|
|
2202
|
+
claimedCapabilityKeys: opts.capability ?? [],
|
|
2203
|
+
toolSetRef: opts.toolSetRef ?? null,
|
|
2204
|
+
ttlSeconds: opts.ttl
|
|
2205
|
+
})
|
|
2206
|
+
}
|
|
2207
|
+
);
|
|
2208
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
2209
|
+
if (!cmd.optsWithGlobals().json)
|
|
2210
|
+
process.stderr.write(
|
|
2211
|
+
style.dim(`refresh with: sechroom executor heartbeat ${data.id}
|
|
2212
|
+
`)
|
|
2213
|
+
);
|
|
2214
|
+
});
|
|
2215
|
+
executor.command("refresh <id>").description("Refresh one advertisement lease once").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (id, opts, cmd) => {
|
|
2216
|
+
const data = await refreshExecutorInstance(
|
|
2217
|
+
resolveConfig(cmd.optsWithGlobals()),
|
|
2218
|
+
id,
|
|
2219
|
+
opts.ttl
|
|
2220
|
+
);
|
|
2221
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
2222
|
+
});
|
|
2223
|
+
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) => {
|
|
2224
|
+
if (opts.interval >= opts.ttl)
|
|
2225
|
+
fail("heartbeat interval must be shorter than the TTL");
|
|
2226
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2227
|
+
await refreshExecutorInstance(cfg, id, opts.ttl);
|
|
2228
|
+
process.stderr.write(
|
|
2229
|
+
style.green("executor heartbeat active") + style.dim(` \u2014 ${id}
|
|
2230
|
+
`)
|
|
2231
|
+
);
|
|
2232
|
+
await holdHeartbeat(async () => {
|
|
2233
|
+
await refreshExecutorInstance(cfg, id, opts.ttl);
|
|
2234
|
+
}, opts.interval * 1e3);
|
|
2235
|
+
});
|
|
2236
|
+
executor.command("offers <id>").description("List live dispatch offers addressed to this exact instance").action(async (id, _opts, cmd) => {
|
|
2237
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2238
|
+
const data = await api(
|
|
2239
|
+
cfg,
|
|
2240
|
+
`/me/executor-instances/${encodeURIComponent(id)}/dispatch-offers`
|
|
2241
|
+
);
|
|
2242
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
2243
|
+
});
|
|
2244
|
+
executor.command("deregister <id>").description("Stop advertising this executor instance").action(async (id, _opts, cmd) => {
|
|
2245
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2246
|
+
const data = await api(
|
|
2247
|
+
cfg,
|
|
2248
|
+
`/me/executor-instances/${encodeURIComponent(id)}`,
|
|
2249
|
+
{
|
|
2250
|
+
method: "DELETE",
|
|
2251
|
+
body: JSON.stringify({})
|
|
2252
|
+
}
|
|
2253
|
+
);
|
|
2254
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
function parseRuntimeKind(value) {
|
|
2258
|
+
switch (value.trim().toLowerCase()) {
|
|
2259
|
+
case "claude":
|
|
2260
|
+
case "claude-code":
|
|
2261
|
+
return "ClaudeCode";
|
|
2262
|
+
case "codex":
|
|
2263
|
+
return "Codex";
|
|
2264
|
+
default:
|
|
2265
|
+
return fail("runtime must be claude-code or codex");
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
function parseTransport(value) {
|
|
2269
|
+
switch (value.trim().toLowerCase()) {
|
|
2270
|
+
case "push":
|
|
2271
|
+
return "Push";
|
|
2272
|
+
case "pull":
|
|
2273
|
+
return "Pull";
|
|
2274
|
+
default:
|
|
2275
|
+
return fail("transport must be push or pull");
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
async function refreshExecutorInstance(cfg, id, ttlSeconds) {
|
|
2279
|
+
return api(
|
|
2280
|
+
cfg,
|
|
2281
|
+
`/me/executor-instances/${encodeURIComponent(id)}/refresh`,
|
|
2282
|
+
{
|
|
2283
|
+
method: "POST",
|
|
2284
|
+
body: JSON.stringify({ ttlSeconds })
|
|
2285
|
+
}
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
async function registerInstance(cfg, state) {
|
|
2289
|
+
const subscription = await api(
|
|
2290
|
+
cfg,
|
|
2291
|
+
"/me/delivery-subscriptions/signalr",
|
|
2292
|
+
{
|
|
2293
|
+
method: "POST",
|
|
2294
|
+
body: JSON.stringify(executorSubscriptionInput(state.subscriptionName))
|
|
2295
|
+
}
|
|
2296
|
+
);
|
|
2297
|
+
return api(cfg, "/me/executor-instances", {
|
|
2298
|
+
method: "POST",
|
|
2299
|
+
body: JSON.stringify(executorRegistrationInput(state, subscription.id))
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
async function ensureExecutorInstance(cfg, located) {
|
|
2303
|
+
const { state, path } = located;
|
|
2304
|
+
state.laneId ??= state.instanceKey;
|
|
2305
|
+
const data = await registerInstance(cfg, state);
|
|
2306
|
+
state.instanceId = data.id;
|
|
2307
|
+
state.lastRefreshAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2308
|
+
writeFileSync6(path, JSON.stringify(state, null, 2) + "\n");
|
|
2309
|
+
return data;
|
|
2310
|
+
}
|
|
2311
|
+
function readExecutorState(start = process.cwd()) {
|
|
2312
|
+
const semPath = resolveSemPathForRead(start);
|
|
2313
|
+
const sem = semPath ? readSem(semPath) : void 0;
|
|
2314
|
+
const path = join8(
|
|
2315
|
+
sem ? dirname5(sem.path) : join8(start, ".sechroom"),
|
|
2316
|
+
EXECUTOR_STATE
|
|
2317
|
+
);
|
|
2318
|
+
if (!existsSync7(path)) return void 0;
|
|
2319
|
+
return {
|
|
2320
|
+
state: JSON.parse(readFileSync5(path, "utf8")),
|
|
2321
|
+
path
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
async function drainStdin() {
|
|
2325
|
+
if (process.stdin.isTTY) return;
|
|
2326
|
+
for await (const _chunk of process.stdin) {
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
async function api(cfg, path, init) {
|
|
2330
|
+
const token = await requireToken(cfg);
|
|
2331
|
+
const response = await fetch(`${cfg.baseUrl}${path}`, {
|
|
2332
|
+
...init,
|
|
2333
|
+
headers: {
|
|
2334
|
+
authorization: `Bearer ${token}`,
|
|
2335
|
+
tenant: cfg.tenant,
|
|
2336
|
+
"content-type": "application/json",
|
|
2337
|
+
"x-sechroom-surface": "cli"
|
|
2338
|
+
}
|
|
2339
|
+
});
|
|
2340
|
+
if (!response.ok)
|
|
2341
|
+
fail(
|
|
2342
|
+
`${init?.method ?? "GET"} ${path} failed (${response.status}): ${await response.text()}`
|
|
2343
|
+
);
|
|
2344
|
+
return response.json();
|
|
2345
|
+
}
|
|
2346
|
+
function parseInteger(value) {
|
|
2347
|
+
const parsed = Number.parseInt(value, 10);
|
|
2348
|
+
if (!Number.isFinite(parsed)) fail(`expected an integer, got '${value}'`);
|
|
2349
|
+
return parsed;
|
|
2350
|
+
}
|
|
2351
|
+
function holdHeartbeat(tick, intervalMs) {
|
|
2352
|
+
return new Promise((resolve3, reject) => {
|
|
2353
|
+
const timer = setInterval(() => void tick().catch(reject), intervalMs);
|
|
2354
|
+
const stop = () => {
|
|
2355
|
+
clearInterval(timer);
|
|
2356
|
+
resolve3();
|
|
2357
|
+
};
|
|
2358
|
+
process.once("SIGINT", stop);
|
|
2359
|
+
process.once("SIGTERM", stop);
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
|
|
1544
2363
|
// src/commands/channel.ts
|
|
1545
2364
|
function registerChannel(program2) {
|
|
1546
2365
|
const channel = program2.command("channel").description(
|
|
@@ -1548,13 +2367,17 @@ function registerChannel(program2) {
|
|
|
1548
2367
|
);
|
|
1549
2368
|
const withFilterOpts = (c) => c.option(
|
|
1550
2369
|
"--name <name>",
|
|
1551
|
-
"
|
|
1552
|
-
"wlp-dispatch"
|
|
2370
|
+
"Deprecated: ignored; the installed executor selects its delivery subscription"
|
|
1553
2371
|
).option(
|
|
1554
2372
|
"--tag <tag...>",
|
|
1555
|
-
"
|
|
1556
|
-
|
|
1557
|
-
|
|
2373
|
+
"Deprecated: executor eligibility comes from the installed capability advertisement"
|
|
2374
|
+
).option(
|
|
2375
|
+
"--workspace <wsp...>",
|
|
2376
|
+
"Deprecated: workspace authority is resolved by the server"
|
|
2377
|
+
).option(
|
|
2378
|
+
"--executor-instance <id>",
|
|
2379
|
+
"Deprecated: the instance is read from .sechroom/executor.json"
|
|
2380
|
+
);
|
|
1558
2381
|
withFilterOpts(
|
|
1559
2382
|
channel.command("connect").description(
|
|
1560
2383
|
"Register a SignalR subscription and stream matched events to stdout"
|
|
@@ -1562,37 +2385,54 @@ function registerChannel(program2) {
|
|
|
1562
2385
|
).action(async (opts, cmd) => {
|
|
1563
2386
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
1564
2387
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1565
|
-
|
|
1566
|
-
const
|
|
1567
|
-
const
|
|
1568
|
-
const deliver =
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
2388
|
+
warnLegacyChannelOptions(opts);
|
|
2389
|
+
const located = requireExecutorState();
|
|
2390
|
+
const instance = await ensureExecutorInstance(cfg, located);
|
|
2391
|
+
const deliver = (payload) => process.stdout.write(
|
|
2392
|
+
(typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
|
|
2393
|
+
);
|
|
2394
|
+
const drain = createClaimDrain(cfg, instance.id, deliver);
|
|
2395
|
+
const conn = await openConnection(
|
|
2396
|
+
cfg,
|
|
2397
|
+
() => {
|
|
2398
|
+
void drain().catch(
|
|
2399
|
+
(e) => process.stderr.write(err(`channel claim failed: ${String(e)}
|
|
2400
|
+
`))
|
|
2401
|
+
);
|
|
2402
|
+
},
|
|
2403
|
+
instance.id
|
|
2404
|
+
);
|
|
2405
|
+
await drain();
|
|
2406
|
+
const stopReconciliation = startOfferReconciliation(drain);
|
|
2407
|
+
const stopHeartbeat = startExecutorHeartbeat(
|
|
2408
|
+
() => refreshExecutorInstance(cfg, instance.id, located.state.ttlSeconds),
|
|
2409
|
+
located.state.refreshAfterSeconds * 1e3
|
|
1574
2410
|
);
|
|
1575
|
-
const conn = await openConnection(cfg, deliver);
|
|
1576
|
-
await reconcile(cfg, filter, deliver);
|
|
1577
2411
|
if (json) {
|
|
1578
2412
|
emit(
|
|
1579
2413
|
{
|
|
1580
2414
|
connected: true,
|
|
1581
2415
|
tenant: cfg.tenant,
|
|
1582
|
-
|
|
1583
|
-
|
|
2416
|
+
executorInstanceId: instance.id,
|
|
2417
|
+
instanceKey: located.state.instanceKey,
|
|
2418
|
+
laneId: located.state.laneId
|
|
1584
2419
|
},
|
|
1585
2420
|
true
|
|
1586
2421
|
);
|
|
1587
2422
|
} else {
|
|
1588
2423
|
process.stderr.write(
|
|
1589
2424
|
style.green("channel connected") + style.dim(
|
|
1590
|
-
` \u2014 tenant ${cfg.tenant},
|
|
2425
|
+
` \u2014 tenant ${cfg.tenant}, executor ${located.state.instanceKey}
|
|
1591
2426
|
`
|
|
1592
2427
|
) + style.dim("streaming matched events to stdout; Ctrl-C to stop.\n")
|
|
1593
2428
|
);
|
|
1594
2429
|
}
|
|
1595
|
-
|
|
2430
|
+
try {
|
|
2431
|
+
await holdOpen(conn);
|
|
2432
|
+
} finally {
|
|
2433
|
+
stopReconciliation();
|
|
2434
|
+
stopHeartbeat();
|
|
2435
|
+
}
|
|
1596
2436
|
});
|
|
1597
2437
|
withFilterOpts(
|
|
1598
2438
|
channel.command("mcp").description(
|
|
@@ -1600,18 +2440,18 @@ function registerChannel(program2) {
|
|
|
1600
2440
|
)
|
|
1601
2441
|
).action(async (opts, cmd) => {
|
|
1602
2442
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1603
|
-
|
|
2443
|
+
warnLegacyChannelOptions(opts);
|
|
2444
|
+
const located = requireExecutorState();
|
|
2445
|
+
const instance = await ensureExecutorInstance(cfg, located);
|
|
1604
2446
|
const mcp = new Server(
|
|
1605
2447
|
{ name: "sechroom", version: "0.1.0" },
|
|
1606
2448
|
{
|
|
1607
2449
|
capabilities: { experimental: { "claude/channel": {} } },
|
|
1608
|
-
instructions: 'Matched Sechroom substrate events arrive as <channel source="sechroom"> tags. A WLP dispatch
|
|
2450
|
+
instructions: 'Matched Sechroom substrate events arrive as <channel source="sechroom"> tags. A WLP dispatch delivered here has already been atomically claimed for this executor. Load the memory id from the event and retain the lease and claim token for holder-bound completion.'
|
|
1609
2451
|
}
|
|
1610
2452
|
);
|
|
1611
2453
|
await mcp.connect(new StdioServerTransport());
|
|
1612
|
-
|
|
1613
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1614
|
-
const deliver = makeDeliver(filter, seen, (payload) => {
|
|
2454
|
+
const deliver = (payload) => {
|
|
1615
2455
|
const { content, meta } = summarizeEvent(payload);
|
|
1616
2456
|
void mcp.notification({
|
|
1617
2457
|
method: "notifications/claude/channel",
|
|
@@ -1620,37 +2460,53 @@ function registerChannel(program2) {
|
|
|
1620
2460
|
(e) => process.stderr.write(err(`channel push failed: ${String(e)}
|
|
1621
2461
|
`))
|
|
1622
2462
|
);
|
|
1623
|
-
}
|
|
1624
|
-
const
|
|
1625
|
-
await
|
|
2463
|
+
};
|
|
2464
|
+
const drain = createClaimDrain(cfg, instance.id, deliver);
|
|
2465
|
+
const conn = await openConnection(
|
|
2466
|
+
cfg,
|
|
2467
|
+
() => {
|
|
2468
|
+
void drain().catch(
|
|
2469
|
+
(e) => process.stderr.write(err(`channel claim failed: ${String(e)}
|
|
2470
|
+
`))
|
|
2471
|
+
);
|
|
2472
|
+
},
|
|
2473
|
+
instance.id
|
|
2474
|
+
);
|
|
2475
|
+
await drain();
|
|
2476
|
+
const stopReconciliation = startOfferReconciliation(drain);
|
|
2477
|
+
const stopHeartbeat = startExecutorHeartbeat(
|
|
2478
|
+
() => refreshExecutorInstance(cfg, instance.id, located.state.ttlSeconds),
|
|
2479
|
+
located.state.refreshAfterSeconds * 1e3
|
|
2480
|
+
);
|
|
1626
2481
|
process.stderr.write(
|
|
1627
2482
|
style.dim(
|
|
1628
|
-
`sechroom channel (mcp) \u2014 tenant ${cfg.tenant},
|
|
2483
|
+
`sechroom channel (mcp) \u2014 tenant ${cfg.tenant}, executor ${located.state.instanceKey}
|
|
1629
2484
|
`
|
|
1630
2485
|
)
|
|
1631
2486
|
);
|
|
1632
|
-
|
|
2487
|
+
try {
|
|
2488
|
+
await holdOpen(conn);
|
|
2489
|
+
} finally {
|
|
2490
|
+
stopReconciliation();
|
|
2491
|
+
stopHeartbeat();
|
|
2492
|
+
}
|
|
1633
2493
|
});
|
|
1634
2494
|
channel.command("install").description(
|
|
1635
2495
|
"Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
|
|
1636
2496
|
).option(
|
|
1637
2497
|
"--workspace <wsp...>",
|
|
1638
|
-
"
|
|
2498
|
+
"Deprecated: accepted only to migrate an existing managed entry"
|
|
1639
2499
|
).option(
|
|
1640
2500
|
"--tag <tag...>",
|
|
1641
|
-
"
|
|
1642
|
-
["kind:task"]
|
|
2501
|
+
"Deprecated: accepted only to migrate an existing managed entry"
|
|
1643
2502
|
).option(
|
|
1644
2503
|
"--name <name>",
|
|
1645
2504
|
"MCP server + subscription name (idempotent per name)",
|
|
1646
2505
|
"sechroom-channel"
|
|
1647
2506
|
).option("--dry-run", "Print what would change; write nothing").action((opts) => {
|
|
1648
|
-
const path =
|
|
2507
|
+
const path = join9(process.cwd(), ".mcp.json");
|
|
1649
2508
|
const dryRun = Boolean(opts.dryRun);
|
|
1650
|
-
const args = ["channel", "mcp"
|
|
1651
|
-
for (const w of opts.workspace ?? [])
|
|
1652
|
-
args.push("--workspace", w);
|
|
1653
|
-
for (const t of opts.tag ?? []) args.push("--tag", t);
|
|
2509
|
+
const args = ["channel", "mcp"];
|
|
1654
2510
|
const entry = { command: "sechroom", args };
|
|
1655
2511
|
const config2 = readMcpConfig(path);
|
|
1656
2512
|
config2.mcpServers ??= {};
|
|
@@ -1658,8 +2514,8 @@ function registerChannel(program2) {
|
|
|
1658
2514
|
const status = JSON.stringify(existing) === JSON.stringify(entry) ? "current" : existing ? "updated" : "created";
|
|
1659
2515
|
if (status !== "current" && !dryRun) {
|
|
1660
2516
|
config2.mcpServers[opts.name] = entry;
|
|
1661
|
-
|
|
1662
|
-
|
|
2517
|
+
mkdirSync7(dirname6(path), { recursive: true });
|
|
2518
|
+
writeFileSync7(path, JSON.stringify(config2, null, 2) + "\n");
|
|
1663
2519
|
}
|
|
1664
2520
|
const verb = status === "current" ? "already configured" : dryRun ? `would ${status === "created" ? "create" : "update"}` : status;
|
|
1665
2521
|
process.stdout.write(`${style.green("channel")} ${path} (${verb})
|
|
@@ -1679,23 +2535,124 @@ Load it (Channels research preview) by launching your agent with:
|
|
|
1679
2535
|
);
|
|
1680
2536
|
}
|
|
1681
2537
|
warnIfSechroomNotOnPath();
|
|
2538
|
+
if ((opts.workspace?.length ?? 0) > 0 || (opts.tag?.length ?? 0) > 0)
|
|
2539
|
+
process.stderr.write(
|
|
2540
|
+
style.dim(
|
|
2541
|
+
"channel: --workspace/--tag are retired; the managed entry now uses the installed executor advertisement.\n"
|
|
2542
|
+
)
|
|
2543
|
+
);
|
|
1682
2544
|
});
|
|
1683
2545
|
channel.addHelpText(
|
|
1684
2546
|
"after",
|
|
1685
2547
|
`
|
|
1686
2548
|
Examples:
|
|
1687
|
-
$ sechroom
|
|
1688
|
-
$ sechroom channel connect
|
|
1689
|
-
$ sechroom channel connect --workspace wsp_X --json | jq .
|
|
2549
|
+
$ sechroom executor install configure capability + lane advertisement
|
|
2550
|
+
$ sechroom channel connect claim WLP dispatches and stream them to stdout
|
|
1690
2551
|
|
|
1691
2552
|
# Wire it as a Claude Code channel MCP server (research preview, v2.1.80+):
|
|
1692
|
-
$ sechroom channel install
|
|
2553
|
+
$ sechroom channel install migrate/install the exact-instance channel
|
|
1693
2554
|
# then: claude --dangerously-load-development-channels server:sechroom-channel`
|
|
1694
2555
|
);
|
|
1695
2556
|
}
|
|
2557
|
+
function requireExecutorState() {
|
|
2558
|
+
const located = readExecutorState();
|
|
2559
|
+
if (!located)
|
|
2560
|
+
return fail(
|
|
2561
|
+
"channel requires an installed executor advertisement; run `sechroom executor install` first."
|
|
2562
|
+
);
|
|
2563
|
+
return located;
|
|
2564
|
+
}
|
|
2565
|
+
function warnLegacyChannelOptions(opts) {
|
|
2566
|
+
if (!opts.name && (opts.workspace?.length ?? 0) === 0 && (opts.tag?.length ?? 0) === 0 && !opts.executorInstance)
|
|
2567
|
+
return;
|
|
2568
|
+
process.stderr.write(
|
|
2569
|
+
style.dim(
|
|
2570
|
+
"channel: --name, --workspace, --tag, and --executor-instance are retired; delivery, eligibility, and identity come from the installed executor advertisement.\n"
|
|
2571
|
+
)
|
|
2572
|
+
);
|
|
2573
|
+
}
|
|
2574
|
+
function createClaimDrain(cfg, executorInstanceId, deliver, dependencies = {}) {
|
|
2575
|
+
let active2;
|
|
2576
|
+
const state = {};
|
|
2577
|
+
return () => {
|
|
2578
|
+
active2 ??= drainClaims(cfg, executorInstanceId, deliver, {
|
|
2579
|
+
...dependencies,
|
|
2580
|
+
state
|
|
2581
|
+
}).finally(() => {
|
|
2582
|
+
active2 = void 0;
|
|
2583
|
+
});
|
|
2584
|
+
return active2;
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
2587
|
+
function startOfferReconciliation(drain, intervalMilliseconds = 5e3, dependencies = {}) {
|
|
2588
|
+
const schedule = dependencies.setInterval ?? setInterval;
|
|
2589
|
+
const cancel = dependencies.clearInterval ?? clearInterval;
|
|
2590
|
+
const onError = dependencies.onError ?? ((error) => process.stderr.write(err(`channel claim failed: ${String(error)}
|
|
2591
|
+
`)));
|
|
2592
|
+
const timer = schedule(() => {
|
|
2593
|
+
void drain().catch(onError);
|
|
2594
|
+
}, intervalMilliseconds);
|
|
2595
|
+
return () => cancel(timer);
|
|
2596
|
+
}
|
|
2597
|
+
function startExecutorHeartbeat(refresh, intervalMilliseconds, dependencies = {}) {
|
|
2598
|
+
const schedule = dependencies.setInterval ?? setInterval;
|
|
2599
|
+
const cancel = dependencies.clearInterval ?? clearInterval;
|
|
2600
|
+
const onError = dependencies.onError ?? ((error) => process.stderr.write(err(`channel heartbeat failed: ${String(error)}
|
|
2601
|
+
`)));
|
|
2602
|
+
const timer = schedule(() => {
|
|
2603
|
+
void refresh().catch(onError);
|
|
2604
|
+
}, intervalMilliseconds);
|
|
2605
|
+
return () => cancel(timer);
|
|
2606
|
+
}
|
|
2607
|
+
async function drainClaims(cfg, executorInstanceId, deliver, dependencies = {}) {
|
|
2608
|
+
const request = dependencies.request ?? api;
|
|
2609
|
+
const sleep = dependencies.sleep ?? ((milliseconds) => new Promise((resolve3) => setTimeout(resolve3, milliseconds)));
|
|
2610
|
+
const idempotencyKey = dependencies.idempotencyKey ?? ((offer) => `channel:${offer.generationId}`);
|
|
2611
|
+
const state = dependencies.state ?? {};
|
|
2612
|
+
for (; ; ) {
|
|
2613
|
+
if (state.pendingIdempotencyKey) {
|
|
2614
|
+
const replay = await request(
|
|
2615
|
+
cfg,
|
|
2616
|
+
`/me/executor-instances/${encodeURIComponent(executorInstanceId)}/dispatch-offers/claim-next`,
|
|
2617
|
+
{
|
|
2618
|
+
method: "POST",
|
|
2619
|
+
body: JSON.stringify({ idempotencyKey: state.pendingIdempotencyKey })
|
|
2620
|
+
}
|
|
2621
|
+
);
|
|
2622
|
+
state.pendingIdempotencyKey = void 0;
|
|
2623
|
+
if (replay.outcome === "Claimed" || replay.outcome === "AlreadyHeld") {
|
|
2624
|
+
deliver(replay);
|
|
2625
|
+
continue;
|
|
2626
|
+
}
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
const offers = await request(
|
|
2630
|
+
cfg,
|
|
2631
|
+
`/me/executor-instances/${encodeURIComponent(executorInstanceId)}/dispatch-offers`
|
|
2632
|
+
);
|
|
2633
|
+
const offer = offers[0];
|
|
2634
|
+
if (!offer) return;
|
|
2635
|
+
if (offer.suggestedClaimDelayMs > 0)
|
|
2636
|
+
await sleep(offer.suggestedClaimDelayMs);
|
|
2637
|
+
state.pendingIdempotencyKey = idempotencyKey(offer);
|
|
2638
|
+
const claim = await request(
|
|
2639
|
+
cfg,
|
|
2640
|
+
`/me/executor-instances/${encodeURIComponent(executorInstanceId)}/dispatch-offers/claim-next`,
|
|
2641
|
+
{
|
|
2642
|
+
method: "POST",
|
|
2643
|
+
body: JSON.stringify({ idempotencyKey: state.pendingIdempotencyKey })
|
|
2644
|
+
}
|
|
2645
|
+
);
|
|
2646
|
+
state.pendingIdempotencyKey = void 0;
|
|
2647
|
+
if (claim.outcome === "NoOffer") return;
|
|
2648
|
+
if (claim.outcome === "Claimed" || claim.outcome === "AlreadyHeld")
|
|
2649
|
+
deliver(claim);
|
|
2650
|
+
else return;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
1696
2653
|
function readMcpConfig(path) {
|
|
1697
|
-
if (!
|
|
1698
|
-
const raw =
|
|
2654
|
+
if (!existsSync8(path)) return {};
|
|
2655
|
+
const raw = readFileSync6(path, "utf8");
|
|
1699
2656
|
if (!raw.trim()) return {};
|
|
1700
2657
|
try {
|
|
1701
2658
|
return JSON.parse(raw);
|
|
@@ -1705,35 +2662,9 @@ function readMcpConfig(path) {
|
|
|
1705
2662
|
);
|
|
1706
2663
|
}
|
|
1707
2664
|
}
|
|
1708
|
-
function
|
|
1709
|
-
const
|
|
1710
|
-
const
|
|
1711
|
-
if (tags.length === 0 && workspaceScope.length === 0)
|
|
1712
|
-
fail(
|
|
1713
|
-
"A channel subscription needs at least one --tag or --workspace (an empty filter receives nothing)."
|
|
1714
|
-
);
|
|
1715
|
-
return { tags, workspaceScope };
|
|
1716
|
-
}
|
|
1717
|
-
async function ensureSubscription(cfg, name, filter) {
|
|
1718
|
-
const token = await requireToken(cfg);
|
|
1719
|
-
const resp = await fetch(`${cfg.baseUrl}/me/delivery-subscriptions/signalr`, {
|
|
1720
|
-
method: "POST",
|
|
1721
|
-
headers: {
|
|
1722
|
-
authorization: `Bearer ${token}`,
|
|
1723
|
-
tenant: cfg.tenant,
|
|
1724
|
-
"content-type": "application/json",
|
|
1725
|
-
"x-sechroom-surface": "cli"
|
|
1726
|
-
},
|
|
1727
|
-
body: JSON.stringify({ name, enabled: true, filter })
|
|
1728
|
-
});
|
|
1729
|
-
if (!resp.ok)
|
|
1730
|
-
fail(
|
|
1731
|
-
`Could not register the SignalR subscription (HTTP ${resp.status}): ${await resp.text()}`
|
|
1732
|
-
);
|
|
1733
|
-
return await resp.json();
|
|
1734
|
-
}
|
|
1735
|
-
async function openConnection(cfg, onEvent) {
|
|
1736
|
-
const conn = new HubConnectionBuilder().withUrl(`${cfg.baseUrl}/notifications/${cfg.tenant}`, {
|
|
2665
|
+
async function openConnection(cfg, onEvent, executorInstanceId) {
|
|
2666
|
+
const query = executorInstanceId ? `?executorInstanceId=${encodeURIComponent(executorInstanceId)}` : "";
|
|
2667
|
+
const conn = new HubConnectionBuilder().withUrl(`${cfg.baseUrl}/notifications/${cfg.tenant}${query}`, {
|
|
1737
2668
|
transport: HttpTransportType.LongPolling,
|
|
1738
2669
|
accessTokenFactory: () => requireToken(cfg)
|
|
1739
2670
|
}).withAutomaticReconnect().build();
|
|
@@ -1767,7 +2698,8 @@ function parseEvent(payload) {
|
|
|
1767
2698
|
}
|
|
1768
2699
|
}
|
|
1769
2700
|
const obj = data ?? {};
|
|
1770
|
-
const
|
|
2701
|
+
const envelope = obj.data ?? obj;
|
|
2702
|
+
const inner = envelope.offer ?? envelope;
|
|
1771
2703
|
const rawTags = inner.tags ?? inner.Tags;
|
|
1772
2704
|
return {
|
|
1773
2705
|
eventType: str(inner.eventType ?? inner.EventType ?? obj.type) || "substrate.event",
|
|
@@ -1776,113 +2708,6 @@ function parseEvent(payload) {
|
|
|
1776
2708
|
tags: Array.isArray(rawTags) ? rawTags.filter((t) => typeof t === "string") : void 0
|
|
1777
2709
|
};
|
|
1778
2710
|
}
|
|
1779
|
-
function shouldDeliver(payload, filter) {
|
|
1780
|
-
const { workspaceId, tags } = parseEvent(payload);
|
|
1781
|
-
if (filter.workspaceScope.length > 0 && (!workspaceId || !filter.workspaceScope.includes(workspaceId)))
|
|
1782
|
-
return false;
|
|
1783
|
-
if (filter.tags.length > 0) {
|
|
1784
|
-
if (!tags) return false;
|
|
1785
|
-
return facetedTagMatch(tags, filter.tags);
|
|
1786
|
-
}
|
|
1787
|
-
return true;
|
|
1788
|
-
}
|
|
1789
|
-
function makeDeliver(filter, seen, forward) {
|
|
1790
|
-
return (payload) => {
|
|
1791
|
-
if (!shouldDeliver(payload, filter)) return;
|
|
1792
|
-
const { memoryId } = parseEvent(payload);
|
|
1793
|
-
if (memoryId) {
|
|
1794
|
-
if (seen.has(memoryId)) return;
|
|
1795
|
-
seen.add(memoryId);
|
|
1796
|
-
}
|
|
1797
|
-
forward(payload);
|
|
1798
|
-
};
|
|
1799
|
-
}
|
|
1800
|
-
async function reconcile(cfg, filter, deliver) {
|
|
1801
|
-
if (filter.workspaceScope.length === 0) {
|
|
1802
|
-
process.stderr.write(
|
|
1803
|
-
style.dim(
|
|
1804
|
-
"channel: no --workspace to reconcile against; live feed only (a dropped dispatch won't be recovered).\n"
|
|
1805
|
-
)
|
|
1806
|
-
);
|
|
1807
|
-
return;
|
|
1808
|
-
}
|
|
1809
|
-
if (filter.tags.length === 0) return;
|
|
1810
|
-
let token;
|
|
1811
|
-
try {
|
|
1812
|
-
token = await requireToken(cfg);
|
|
1813
|
-
} catch {
|
|
1814
|
-
return;
|
|
1815
|
-
}
|
|
1816
|
-
const qs = `filterTags=${encodeURIComponent(filter.tags.join(","))}&limit=100`;
|
|
1817
|
-
let recovered = 0;
|
|
1818
|
-
for (const ws of filter.workspaceScope) {
|
|
1819
|
-
try {
|
|
1820
|
-
const resp = await fetch(
|
|
1821
|
-
`${cfg.baseUrl}/workspaces/${encodeURIComponent(ws)}/memories/feed?${qs}`,
|
|
1822
|
-
{
|
|
1823
|
-
headers: {
|
|
1824
|
-
authorization: `Bearer ${token}`,
|
|
1825
|
-
tenant: cfg.tenant,
|
|
1826
|
-
"x-sechroom-surface": "cli"
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
);
|
|
1830
|
-
if (!resp.ok) {
|
|
1831
|
-
process.stderr.write(
|
|
1832
|
-
err(
|
|
1833
|
-
`channel: reconcile query for ${ws} failed (HTTP ${resp.status})
|
|
1834
|
-
`
|
|
1835
|
-
)
|
|
1836
|
-
);
|
|
1837
|
-
continue;
|
|
1838
|
-
}
|
|
1839
|
-
const data = await resp.json();
|
|
1840
|
-
for (const m of data.results ?? []) {
|
|
1841
|
-
if (!m.id) continue;
|
|
1842
|
-
deliver({
|
|
1843
|
-
eventType: "reconcile",
|
|
1844
|
-
memoryId: m.id,
|
|
1845
|
-
workspaceId: ws,
|
|
1846
|
-
tags: m.tags ?? []
|
|
1847
|
-
});
|
|
1848
|
-
recovered++;
|
|
1849
|
-
}
|
|
1850
|
-
} catch (e) {
|
|
1851
|
-
process.stderr.write(
|
|
1852
|
-
err(`channel: reconcile error for ${ws}: ${String(e)}
|
|
1853
|
-
`)
|
|
1854
|
-
);
|
|
1855
|
-
}
|
|
1856
|
-
}
|
|
1857
|
-
if (recovered > 0)
|
|
1858
|
-
process.stderr.write(
|
|
1859
|
-
style.dim(
|
|
1860
|
-
`channel: reconciled ${recovered} already-queued event(s) on connect.
|
|
1861
|
-
`
|
|
1862
|
-
)
|
|
1863
|
-
);
|
|
1864
|
-
}
|
|
1865
|
-
function facetedTagMatch(eventTags, filterTags) {
|
|
1866
|
-
const have = new Set(eventTags);
|
|
1867
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1868
|
-
for (const f of filterTags) {
|
|
1869
|
-
const ns = f.endsWith(":*") ? f.slice(0, -2) : namespaceOf(f);
|
|
1870
|
-
const group = groups.get(ns) ?? [];
|
|
1871
|
-
group.push(f);
|
|
1872
|
-
groups.set(ns, group);
|
|
1873
|
-
}
|
|
1874
|
-
for (const [ns, group] of groups) {
|
|
1875
|
-
const ok2 = group.some(
|
|
1876
|
-
(f) => f.endsWith(":*") ? eventTags.some((t) => namespaceOf(t) === ns) : have.has(f)
|
|
1877
|
-
);
|
|
1878
|
-
if (!ok2) return false;
|
|
1879
|
-
}
|
|
1880
|
-
return true;
|
|
1881
|
-
}
|
|
1882
|
-
function namespaceOf(tag) {
|
|
1883
|
-
const i = tag.indexOf(":");
|
|
1884
|
-
return i >= 0 ? tag.slice(0, i) : tag;
|
|
1885
|
-
}
|
|
1886
2711
|
function summarizeEvent(payload) {
|
|
1887
2712
|
const { eventType, memoryId, workspaceId } = parseEvent(payload);
|
|
1888
2713
|
const content = memoryId ? `${eventType}: ${memoryId}${workspaceId ? ` (workspace ${workspaceId})` : ""}` : typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
@@ -1890,6 +2715,10 @@ function summarizeEvent(payload) {
|
|
|
1890
2715
|
if (eventType) meta.event_type = eventType;
|
|
1891
2716
|
if (memoryId) meta.memory_id = memoryId;
|
|
1892
2717
|
if (workspaceId) meta.workspace_id = workspaceId;
|
|
2718
|
+
const claim = payload ?? {};
|
|
2719
|
+
if (claim.outcome) meta.claim_outcome = claim.outcome;
|
|
2720
|
+
if (claim.lease?.id) meta.lease_id = claim.lease.id;
|
|
2721
|
+
if (claim.claimToken) meta.claim_token = claim.claimToken;
|
|
1893
2722
|
return { content, meta };
|
|
1894
2723
|
}
|
|
1895
2724
|
function str(v) {
|
|
@@ -1979,177 +2808,13 @@ Examples:
|
|
|
1979
2808
|
}
|
|
1980
2809
|
|
|
1981
2810
|
// src/commands/checkpoint.ts
|
|
1982
|
-
import { mkdirSync as
|
|
1983
|
-
import { dirname as
|
|
2811
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
2812
|
+
import { dirname as dirname8, join as join11 } from "path";
|
|
1984
2813
|
|
|
1985
2814
|
// src/commands/hook.ts
|
|
1986
2815
|
import { createHash as createHash2 } from "crypto";
|
|
1987
|
-
import { existsSync as
|
|
1988
|
-
import { dirname as
|
|
1989
|
-
|
|
1990
|
-
// src/sem.ts
|
|
1991
|
-
import { dirname as dirname5, join as join8 } from "path";
|
|
1992
|
-
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync6 } from "fs";
|
|
1993
|
-
var SEM_FILE = join8(".sechroom", "lane.json");
|
|
1994
|
-
var STATE_DIR_NAME2 = ".sechroom";
|
|
1995
|
-
function localSemPath(cwd = process.cwd()) {
|
|
1996
|
-
return join8(cwd, SEM_FILE);
|
|
1997
|
-
}
|
|
1998
|
-
function resolveSemPathForRead(start = process.cwd()) {
|
|
1999
|
-
let dir = start;
|
|
2000
|
-
while (true) {
|
|
2001
|
-
const candidate = join8(dir, SEM_FILE);
|
|
2002
|
-
if (existsSync7(candidate)) return candidate;
|
|
2003
|
-
const parent = dirname5(dir);
|
|
2004
|
-
if (parent === dir) return void 0;
|
|
2005
|
-
dir = parent;
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
|
|
2009
|
-
try {
|
|
2010
|
-
let dir = start;
|
|
2011
|
-
let gitPath;
|
|
2012
|
-
for (; ; ) {
|
|
2013
|
-
const candidate = join8(dir, ".git");
|
|
2014
|
-
if (existsSync7(candidate)) {
|
|
2015
|
-
gitPath = candidate;
|
|
2016
|
-
break;
|
|
2017
|
-
}
|
|
2018
|
-
const parent = dirname5(dir);
|
|
2019
|
-
if (parent === dir) break;
|
|
2020
|
-
dir = parent;
|
|
2021
|
-
}
|
|
2022
|
-
if (!gitPath || statSync(gitPath).isDirectory()) return lane;
|
|
2023
|
-
const gitFile = readFileSync5(gitPath, "utf8");
|
|
2024
|
-
const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
|
|
2025
|
-
if (!common) return lane;
|
|
2026
|
-
const worktreesDir = join8(common[1], "worktrees");
|
|
2027
|
-
const siblings = readdirSync(worktreesDir).filter((n) => {
|
|
2028
|
-
try {
|
|
2029
|
-
return statSync(join8(worktreesDir, n)).isDirectory();
|
|
2030
|
-
} catch {
|
|
2031
|
-
return false;
|
|
2032
|
-
}
|
|
2033
|
-
});
|
|
2034
|
-
return laneWithWorktreeSuffix(lane, gitFile, siblings);
|
|
2035
|
-
} catch {
|
|
2036
|
-
return lane;
|
|
2037
|
-
}
|
|
2038
|
-
}
|
|
2039
|
-
function laneWithWorktreeSuffix(lane, gitFile, siblings) {
|
|
2040
|
-
const m = gitFile.trim().match(/\/worktrees\/([^/\s]+)\s*$/);
|
|
2041
|
-
if (!m) return lane;
|
|
2042
|
-
const idx = [...siblings].sort().indexOf(m[1]);
|
|
2043
|
-
return idx < 0 ? lane : `${lane}-${idx + 2}`;
|
|
2044
|
-
}
|
|
2045
|
-
function serializeSem(values) {
|
|
2046
|
-
return JSON.stringify(values, null, 2) + "\n";
|
|
2047
|
-
}
|
|
2048
|
-
function readSem(path) {
|
|
2049
|
-
const p = path ?? resolveSemPathForRead();
|
|
2050
|
-
if (!p || !existsSync7(p)) return void 0;
|
|
2051
|
-
return { path: p, values: parseLaneJson(readFileSync5(p, "utf8")) };
|
|
2052
|
-
}
|
|
2053
|
-
function readLocalSemValues(cwd = process.cwd()) {
|
|
2054
|
-
const next = join8(cwd, SEM_FILE);
|
|
2055
|
-
if (existsSync7(next)) return readSem(next)?.values ?? {};
|
|
2056
|
-
return {};
|
|
2057
|
-
}
|
|
2058
|
-
function parseLaneJson(text2) {
|
|
2059
|
-
try {
|
|
2060
|
-
const parsed = JSON.parse(text2);
|
|
2061
|
-
const out = {};
|
|
2062
|
-
for (const [k, v] of Object.entries(parsed)) {
|
|
2063
|
-
if (typeof v === "string") out[k] = v;
|
|
2064
|
-
}
|
|
2065
|
-
return out;
|
|
2066
|
-
} catch {
|
|
2067
|
-
return {};
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
var STATE_DIR_IGNORE = `${STATE_DIR_NAME2}/`;
|
|
2071
|
-
function writeSem(values, path = localSemPath()) {
|
|
2072
|
-
mkdirSync6(dirname5(path), { recursive: true });
|
|
2073
|
-
writeFileSync6(path, serializeSem(values));
|
|
2074
|
-
ensureSemIgnored(path);
|
|
2075
|
-
ensureContinuityScaffold(path);
|
|
2076
|
-
return path;
|
|
2077
|
-
}
|
|
2078
|
-
function ensureStateDirIgnored(cwd = process.cwd()) {
|
|
2079
|
-
ensureSemIgnored(localSemPath(cwd));
|
|
2080
|
-
}
|
|
2081
|
-
var CONTINUITY_FILE_NAME = "continuity.json";
|
|
2082
|
-
var CONTINUITY_SCAFFOLD = JSON.stringify(
|
|
2083
|
-
{
|
|
2084
|
-
_readme: "Agent-maintained continuity intent. Keep these current during the session; `sechroom checkpoint` and the PreCompact hook snapshot from here. The five required fields (objective, state, lastAction, nextAction, resumeInstruction) must all be non-empty for a snapshot to be created.",
|
|
2085
|
-
objective: "",
|
|
2086
|
-
state: "",
|
|
2087
|
-
lastAction: "",
|
|
2088
|
-
nextAction: "",
|
|
2089
|
-
resumeInstruction: "",
|
|
2090
|
-
constraints: [],
|
|
2091
|
-
questions: [],
|
|
2092
|
-
artifacts: [],
|
|
2093
|
-
confidence: null
|
|
2094
|
-
},
|
|
2095
|
-
null,
|
|
2096
|
-
2
|
|
2097
|
-
) + "\n";
|
|
2098
|
-
function ensureContinuityScaffold(semPath) {
|
|
2099
|
-
try {
|
|
2100
|
-
const target = join8(dirname5(semPath), CONTINUITY_FILE_NAME);
|
|
2101
|
-
if (existsSync7(target)) return;
|
|
2102
|
-
writeFileSync6(target, CONTINUITY_SCAFFOLD);
|
|
2103
|
-
} catch {
|
|
2104
|
-
}
|
|
2105
|
-
}
|
|
2106
|
-
function ignoresSem(content) {
|
|
2107
|
-
return content.split("\n").some((line) => {
|
|
2108
|
-
const t = line.trim();
|
|
2109
|
-
return t === STATE_DIR_NAME2 || t === STATE_DIR_IGNORE || t === `/${STATE_DIR_NAME2}` || t === `/${STATE_DIR_IGNORE}` || t === `**/${STATE_DIR_NAME2}` || t === `**/${STATE_DIR_IGNORE}`;
|
|
2110
|
-
});
|
|
2111
|
-
}
|
|
2112
|
-
function inGitRepo(startDir) {
|
|
2113
|
-
let dir = startDir;
|
|
2114
|
-
for (; ; ) {
|
|
2115
|
-
if (existsSync7(join8(dir, ".git"))) return true;
|
|
2116
|
-
const parent = dirname5(dir);
|
|
2117
|
-
if (parent === dir) return false;
|
|
2118
|
-
dir = parent;
|
|
2119
|
-
}
|
|
2120
|
-
}
|
|
2121
|
-
function resolveGitignoreTarget(startDir) {
|
|
2122
|
-
let dir = startDir;
|
|
2123
|
-
for (; ; ) {
|
|
2124
|
-
const gi = join8(dir, ".gitignore");
|
|
2125
|
-
if (existsSync7(gi)) return { path: gi, exists: true };
|
|
2126
|
-
const parent = dirname5(dir);
|
|
2127
|
-
if (existsSync7(join8(dir, ".git")) || parent === dir) {
|
|
2128
|
-
return { path: join8(startDir, ".gitignore"), exists: false };
|
|
2129
|
-
}
|
|
2130
|
-
dir = parent;
|
|
2131
|
-
}
|
|
2132
|
-
}
|
|
2133
|
-
function ensureSemIgnored(semPath) {
|
|
2134
|
-
try {
|
|
2135
|
-
const checkoutDir = dirname5(dirname5(semPath));
|
|
2136
|
-
if (!inGitRepo(checkoutDir)) return;
|
|
2137
|
-
const target = resolveGitignoreTarget(checkoutDir);
|
|
2138
|
-
if (target.exists) {
|
|
2139
|
-
const content = readFileSync5(target.path, "utf8");
|
|
2140
|
-
if (ignoresSem(content)) return;
|
|
2141
|
-
const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
2142
|
-
appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
|
|
2143
|
-
`);
|
|
2144
|
-
} else {
|
|
2145
|
-
writeFileSync6(target.path, `${STATE_DIR_IGNORE}
|
|
2146
|
-
`);
|
|
2147
|
-
}
|
|
2148
|
-
} catch {
|
|
2149
|
-
}
|
|
2150
|
-
}
|
|
2151
|
-
|
|
2152
|
-
// src/commands/hook.ts
|
|
2816
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync7, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
|
|
2817
|
+
import { dirname as dirname7, join as join10 } from "path";
|
|
2153
2818
|
async function readStdin() {
|
|
2154
2819
|
if (process.stdin.isTTY) return "";
|
|
2155
2820
|
const chunks = [];
|
|
@@ -2173,13 +2838,13 @@ function resolveLane(flagLane, cwd) {
|
|
|
2173
2838
|
if (!base) return void 0;
|
|
2174
2839
|
return applyWorktreeLaneSuffix(base, start);
|
|
2175
2840
|
}
|
|
2176
|
-
var INTENT_FILE =
|
|
2841
|
+
var INTENT_FILE = join10(".sechroom", "continuity.json");
|
|
2177
2842
|
function resolveIntentPath(start) {
|
|
2178
2843
|
let dir = start;
|
|
2179
2844
|
for (; ; ) {
|
|
2180
|
-
const candidate =
|
|
2181
|
-
if (
|
|
2182
|
-
const parent =
|
|
2845
|
+
const candidate = join10(dir, INTENT_FILE);
|
|
2846
|
+
if (existsSync9(candidate)) return candidate;
|
|
2847
|
+
const parent = dirname7(dir);
|
|
2183
2848
|
if (parent === dir) return void 0;
|
|
2184
2849
|
dir = parent;
|
|
2185
2850
|
}
|
|
@@ -2188,7 +2853,7 @@ function readIntent(start) {
|
|
|
2188
2853
|
const path = resolveIntentPath(start);
|
|
2189
2854
|
if (!path) return void 0;
|
|
2190
2855
|
try {
|
|
2191
|
-
return JSON.parse(
|
|
2856
|
+
return JSON.parse(readFileSync7(path, "utf8"));
|
|
2192
2857
|
} catch {
|
|
2193
2858
|
return void 0;
|
|
2194
2859
|
}
|
|
@@ -2230,14 +2895,14 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
|
|
|
2230
2895
|
}
|
|
2231
2896
|
function ledgerPath(start) {
|
|
2232
2897
|
const intent = resolveIntentPath(start);
|
|
2233
|
-
const dir = intent ?
|
|
2234
|
-
return
|
|
2898
|
+
const dir = intent ? dirname7(intent) : join10(start, ".sechroom");
|
|
2899
|
+
return join10(dir, ".checkpoint-state.json");
|
|
2235
2900
|
}
|
|
2236
2901
|
function readLedger(start) {
|
|
2237
2902
|
try {
|
|
2238
2903
|
const p = ledgerPath(start);
|
|
2239
|
-
if (!
|
|
2240
|
-
return JSON.parse(
|
|
2904
|
+
if (!existsSync9(p)) return {};
|
|
2905
|
+
return JSON.parse(readFileSync7(p, "utf8"));
|
|
2241
2906
|
} catch {
|
|
2242
2907
|
return {};
|
|
2243
2908
|
}
|
|
@@ -2284,13 +2949,13 @@ function recordPush(start, intent) {
|
|
|
2284
2949
|
} catch {
|
|
2285
2950
|
mtimeMs = void 0;
|
|
2286
2951
|
}
|
|
2287
|
-
|
|
2952
|
+
mkdirSync8(dirname7(p), { recursive: true });
|
|
2288
2953
|
const ledger = {
|
|
2289
2954
|
lastEpochMs: Date.now(),
|
|
2290
2955
|
lastMtimeMs: mtimeMs,
|
|
2291
2956
|
lastHash: intentHash(intent)
|
|
2292
2957
|
};
|
|
2293
|
-
|
|
2958
|
+
writeFileSync8(p, JSON.stringify(ledger) + "\n");
|
|
2294
2959
|
} catch {
|
|
2295
2960
|
}
|
|
2296
2961
|
}
|
|
@@ -2436,6 +3101,12 @@ Fail-soft: no lane / no auth / no-or-partial intent file / API error -> exit 0,
|
|
|
2436
3101
|
const surfaceResults = installHookSurfaces(["codex"], { dryRun, claudeDir: "", codexHome })[0].results;
|
|
2437
3102
|
process.stdout.write(`${HOOK_SURFACE_LABEL.codex}:
|
|
2438
3103
|
`);
|
|
3104
|
+
if (dryRun) {
|
|
3105
|
+
for (const [event, command] of Object.entries(hookCommandsForSurface("codex"))) {
|
|
3106
|
+
process.stdout.write(` ${event}: ${command}
|
|
3107
|
+
`);
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
2439
3110
|
for (const r of surfaceResults) {
|
|
2440
3111
|
results.push(r);
|
|
2441
3112
|
process.stdout.write(describe(r, dryRun) + "\n");
|
|
@@ -2537,10 +3208,10 @@ Examples:
|
|
|
2537
3208
|
const client = await makeClient(cfg);
|
|
2538
3209
|
return client.POST("/continuity/snapshots", { body });
|
|
2539
3210
|
});
|
|
2540
|
-
const path = resolveIntentPath(cwd) ??
|
|
3211
|
+
const path = resolveIntentPath(cwd) ?? join11(cwd, INTENT_FILE);
|
|
2541
3212
|
const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
|
|
2542
|
-
|
|
2543
|
-
|
|
3213
|
+
mkdirSync9(dirname8(path), { recursive: true });
|
|
3214
|
+
writeFileSync9(path, JSON.stringify(fileBody, null, 2) + "\n");
|
|
2544
3215
|
recordPush(cwd, merged);
|
|
2545
3216
|
if (json) {
|
|
2546
3217
|
emit({ snapshotId: data.snapshotId, lane, scope, file: path }, true);
|
|
@@ -2554,7 +3225,7 @@ Examples:
|
|
|
2554
3225
|
}
|
|
2555
3226
|
|
|
2556
3227
|
// src/commands/close.ts
|
|
2557
|
-
import { readFileSync as
|
|
3228
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
2558
3229
|
var VERDICTS = ["pass", "soft-fail", "plan-invalid", "blocked"];
|
|
2559
3230
|
function registerClose(program2) {
|
|
2560
3231
|
program2.command("close").description(
|
|
@@ -2595,7 +3266,7 @@ Examples:
|
|
|
2595
3266
|
);
|
|
2596
3267
|
let bodyText;
|
|
2597
3268
|
try {
|
|
2598
|
-
bodyText = opts.file ?
|
|
3269
|
+
bodyText = opts.file ? readFileSync8(opts.file, "utf8") : readFileSync8(0, "utf8");
|
|
2599
3270
|
} catch {
|
|
2600
3271
|
fail(
|
|
2601
3272
|
opts.file ? `could not read --file ${opts.file}` : "no closeout body \u2014 pass --file <path> or pipe it on stdin"
|
|
@@ -3498,8 +4169,8 @@ Examples:
|
|
|
3498
4169
|
|
|
3499
4170
|
// src/setup/apply.ts
|
|
3500
4171
|
import { createHash as createHash3 } from "crypto";
|
|
3501
|
-
import { mkdirSync as
|
|
3502
|
-
import { dirname as
|
|
4172
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync10, existsSync as existsSync10 } from "fs";
|
|
4173
|
+
import { dirname as dirname9 } from "path";
|
|
3503
4174
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
3504
4175
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
3505
4176
|
function normalizeBody(s) {
|
|
@@ -3552,22 +4223,22 @@ function parseManagedBlock(content, block) {
|
|
|
3552
4223
|
return null;
|
|
3553
4224
|
}
|
|
3554
4225
|
function ensureDir2(path) {
|
|
3555
|
-
|
|
4226
|
+
mkdirSync10(dirname9(path), { recursive: true });
|
|
3556
4227
|
}
|
|
3557
4228
|
function readOr(path, fallback) {
|
|
3558
4229
|
try {
|
|
3559
|
-
return
|
|
4230
|
+
return readFileSync9(path, "utf8");
|
|
3560
4231
|
} catch {
|
|
3561
4232
|
return fallback;
|
|
3562
4233
|
}
|
|
3563
4234
|
}
|
|
3564
4235
|
function mergeMcpJson(path, snippet, dryRun) {
|
|
3565
4236
|
const incoming = JSON.parse(snippet);
|
|
3566
|
-
const existed =
|
|
4237
|
+
const existed = existsSync10(path);
|
|
3567
4238
|
let current = {};
|
|
3568
4239
|
if (existed) {
|
|
3569
4240
|
try {
|
|
3570
|
-
current = JSON.parse(
|
|
4241
|
+
current = JSON.parse(readFileSync9(path, "utf8"));
|
|
3571
4242
|
} catch {
|
|
3572
4243
|
return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
|
|
3573
4244
|
}
|
|
@@ -3575,26 +4246,26 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
3575
4246
|
current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
|
|
3576
4247
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3577
4248
|
ensureDir2(path);
|
|
3578
|
-
|
|
4249
|
+
writeFileSync10(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
3579
4250
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3580
4251
|
}
|
|
3581
4252
|
function mergeCodexToml(path, snippet, dryRun) {
|
|
3582
|
-
const existed =
|
|
4253
|
+
const existed = existsSync10(path);
|
|
3583
4254
|
let body = readOr(path, "");
|
|
3584
4255
|
body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
|
|
3585
4256
|
const trimmed = body.trim();
|
|
3586
4257
|
const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
|
|
3587
4258
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3588
4259
|
ensureDir2(path);
|
|
3589
|
-
|
|
4260
|
+
writeFileSync10(path, next, { mode: 384 });
|
|
3590
4261
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3591
4262
|
}
|
|
3592
4263
|
function writeInstructionBlock(path, write, dryRun) {
|
|
3593
|
-
const existed =
|
|
4264
|
+
const existed = existsSync10(path);
|
|
3594
4265
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
3595
4266
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
3596
4267
|
ensureDir2(path);
|
|
3597
|
-
|
|
4268
|
+
writeFileSync10(path, next);
|
|
3598
4269
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
3599
4270
|
}
|
|
3600
4271
|
function computeBlockFile(current, write) {
|
|
@@ -3635,7 +4306,7 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
3635
4306
|
const next = computeBlockFile(current, write);
|
|
3636
4307
|
if (!dryRun) {
|
|
3637
4308
|
ensureDir2(proposedPath);
|
|
3638
|
-
|
|
4309
|
+
writeFileSync10(proposedPath, next);
|
|
3639
4310
|
}
|
|
3640
4311
|
return {
|
|
3641
4312
|
kind: "instruction",
|
|
@@ -3765,8 +4436,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
3765
4436
|
}
|
|
3766
4437
|
|
|
3767
4438
|
// src/setup/skills-offer.ts
|
|
3768
|
-
import { mkdirSync as
|
|
3769
|
-
import { join as
|
|
4439
|
+
import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
|
|
4440
|
+
import { join as join12 } from "path";
|
|
3770
4441
|
|
|
3771
4442
|
// src/setup/lane-pin.ts
|
|
3772
4443
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -3882,8 +4553,8 @@ Found ${summary} available to you for ${surface}.
|
|
|
3882
4553
|
if (skills.length > 0) {
|
|
3883
4554
|
const written = [];
|
|
3884
4555
|
for (const s of skills) {
|
|
3885
|
-
|
|
3886
|
-
|
|
4556
|
+
mkdirSync11(join12(sDir, s.name), { recursive: true });
|
|
4557
|
+
writeFileSync11(join12(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
3887
4558
|
written.push(s.name);
|
|
3888
4559
|
}
|
|
3889
4560
|
recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -3891,11 +4562,11 @@ Found ${summary} available to you for ${surface}.
|
|
|
3891
4562
|
`);
|
|
3892
4563
|
}
|
|
3893
4564
|
if (agents.length > 0) {
|
|
3894
|
-
|
|
4565
|
+
mkdirSync11(aDir, { recursive: true });
|
|
3895
4566
|
const written = [];
|
|
3896
4567
|
for (const a of agents) {
|
|
3897
4568
|
const file = `${a.name}.md`;
|
|
3898
|
-
|
|
4569
|
+
writeFileSync11(join12(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
3899
4570
|
written.push(file);
|
|
3900
4571
|
}
|
|
3901
4572
|
recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -4278,13 +4949,13 @@ Wired to namespace '${slug}'. Restart your AI client (or reload MCP) to pick it
|
|
|
4278
4949
|
}
|
|
4279
4950
|
|
|
4280
4951
|
// src/commands/onboard.ts
|
|
4281
|
-
import { existsSync as
|
|
4282
|
-
import { basename as basename2, join as
|
|
4952
|
+
import { existsSync as existsSync12 } from "fs";
|
|
4953
|
+
import { basename as basename2, join as join14 } from "path";
|
|
4283
4954
|
|
|
4284
4955
|
// src/commands/fanout.ts
|
|
4285
4956
|
import { spawnSync } from "child_process";
|
|
4286
|
-
import { existsSync as
|
|
4287
|
-
import { isAbsolute, join as
|
|
4957
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
4958
|
+
import { isAbsolute, join as join13, resolve } from "path";
|
|
4288
4959
|
var ICON = {
|
|
4289
4960
|
refresh: "\u21BB",
|
|
4290
4961
|
bind: "+",
|
|
@@ -4304,21 +4975,21 @@ function discoverChildren(root) {
|
|
|
4304
4975
|
const out = [];
|
|
4305
4976
|
for (const name of names.sort()) {
|
|
4306
4977
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
4307
|
-
const dir =
|
|
4978
|
+
const dir = join13(root, name);
|
|
4308
4979
|
try {
|
|
4309
4980
|
if (!statSync3(dir).isDirectory()) continue;
|
|
4310
4981
|
} catch {
|
|
4311
4982
|
continue;
|
|
4312
4983
|
}
|
|
4313
|
-
if (
|
|
4984
|
+
if (existsSync11(join13(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
4314
4985
|
}
|
|
4315
4986
|
return out;
|
|
4316
4987
|
}
|
|
4317
4988
|
function readManifest(path) {
|
|
4318
|
-
if (!
|
|
4989
|
+
if (!existsSync11(path)) return null;
|
|
4319
4990
|
let parsed;
|
|
4320
4991
|
try {
|
|
4321
|
-
parsed = JSON.parse(
|
|
4992
|
+
parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
4322
4993
|
} catch (err2) {
|
|
4323
4994
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
4324
4995
|
}
|
|
@@ -4684,10 +5355,10 @@ async function chooseScope(scopeFlag, yes) {
|
|
|
4684
5355
|
}
|
|
4685
5356
|
async function planRecurseChild(entry, root, client, opts) {
|
|
4686
5357
|
const dir = resolveChildDir(entry.path, root);
|
|
4687
|
-
if (!
|
|
5358
|
+
if (!existsSync12(dir)) {
|
|
4688
5359
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
4689
5360
|
}
|
|
4690
|
-
if (
|
|
5361
|
+
if (existsSync12(join14(dir, ".sechroom.json"))) {
|
|
4691
5362
|
return {
|
|
4692
5363
|
label: entry.path,
|
|
4693
5364
|
dir,
|
|
@@ -4760,7 +5431,7 @@ This fan-out will pin the same lane in every repo:
|
|
|
4760
5431
|
async function runRecurse(cfg, g, opts) {
|
|
4761
5432
|
const { yes, dryRun, json } = opts;
|
|
4762
5433
|
const root = process.cwd();
|
|
4763
|
-
const manifestPath =
|
|
5434
|
+
const manifestPath = join14(root, ".sechroom", "repos.json");
|
|
4764
5435
|
const fromManifest = readManifest(manifestPath);
|
|
4765
5436
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
4766
5437
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
@@ -5293,23 +5964,23 @@ Examples:
|
|
|
5293
5964
|
|
|
5294
5965
|
// src/commands/reset.ts
|
|
5295
5966
|
import { homedir as homedir4 } from "os";
|
|
5296
|
-
import { join as
|
|
5297
|
-
import { existsSync as
|
|
5967
|
+
import { join as join15 } from "path";
|
|
5968
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11, rmSync as rmSync3 } from "fs";
|
|
5298
5969
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
5299
|
-
var localSkillsDir = () =>
|
|
5300
|
-
var globalSkillsDir = () =>
|
|
5301
|
-
var localAgentsDir = () =>
|
|
5302
|
-
var globalAgentsDir = () =>
|
|
5970
|
+
var localSkillsDir = () => join15(process.cwd(), ".claude", "skills");
|
|
5971
|
+
var globalSkillsDir = () => join15(homedir4(), ".claude", "skills");
|
|
5972
|
+
var localAgentsDir = () => join15(process.cwd(), ".claude", "agents");
|
|
5973
|
+
var globalAgentsDir = () => join15(homedir4(), ".claude", "agents");
|
|
5303
5974
|
function removeMaterialisedSkills(dir) {
|
|
5304
5975
|
const removed = [];
|
|
5305
|
-
const lockPath =
|
|
5306
|
-
if (!
|
|
5976
|
+
const lockPath = join15(dir, SKILLS_LOCK2);
|
|
5977
|
+
if (!existsSync13(lockPath)) return removed;
|
|
5307
5978
|
try {
|
|
5308
|
-
const lock = JSON.parse(
|
|
5979
|
+
const lock = JSON.parse(readFileSync11(lockPath, "utf8"));
|
|
5309
5980
|
for (const entry of Object.values(lock)) {
|
|
5310
5981
|
for (const name of entry.skills ?? []) {
|
|
5311
|
-
const p =
|
|
5312
|
-
if (
|
|
5982
|
+
const p = join15(dir, name);
|
|
5983
|
+
if (existsSync13(p)) {
|
|
5313
5984
|
rmSync3(p, { recursive: true, force: true });
|
|
5314
5985
|
removed.push(p);
|
|
5315
5986
|
}
|
|
@@ -5354,18 +6025,18 @@ function registerReset(program2) {
|
|
|
5354
6025
|
}
|
|
5355
6026
|
}
|
|
5356
6027
|
const removed = [];
|
|
5357
|
-
const stateDir =
|
|
5358
|
-
if (
|
|
6028
|
+
const stateDir = join15(process.cwd(), ".sechroom");
|
|
6029
|
+
if (existsSync13(stateDir)) {
|
|
5359
6030
|
rmSync3(stateDir, { recursive: true, force: true });
|
|
5360
6031
|
removed.push(stateDir);
|
|
5361
6032
|
}
|
|
5362
|
-
const legacyCfg =
|
|
5363
|
-
if (
|
|
6033
|
+
const legacyCfg = join15(process.cwd(), ".sechroom.json");
|
|
6034
|
+
if (existsSync13(legacyCfg)) {
|
|
5364
6035
|
rmSync3(legacyCfg, { force: true });
|
|
5365
6036
|
removed.push(legacyCfg);
|
|
5366
6037
|
}
|
|
5367
|
-
const legacySem =
|
|
5368
|
-
if (
|
|
6038
|
+
const legacySem = join15(process.cwd(), ".sem");
|
|
6039
|
+
if (existsSync13(legacySem)) {
|
|
5369
6040
|
rmSync3(legacySem, { force: true });
|
|
5370
6041
|
removed.push(legacySem);
|
|
5371
6042
|
}
|
|
@@ -5391,8 +6062,8 @@ function registerReset(program2) {
|
|
|
5391
6062
|
}
|
|
5392
6063
|
|
|
5393
6064
|
// src/commands/skills.ts
|
|
5394
|
-
import { existsSync as
|
|
5395
|
-
import { join as
|
|
6065
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync12, statSync as statSync4, writeFileSync as writeFileSync12 } from "fs";
|
|
6066
|
+
import { join as join16 } from "path";
|
|
5396
6067
|
function filenameFromDisposition(header) {
|
|
5397
6068
|
if (!header) return void 0;
|
|
5398
6069
|
const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
|
|
@@ -5400,11 +6071,11 @@ function filenameFromDisposition(header) {
|
|
|
5400
6071
|
}
|
|
5401
6072
|
function resolveOutputPath(output, serverFilename) {
|
|
5402
6073
|
const filename = serverFilename || "skills.zip";
|
|
5403
|
-
if (!output) return
|
|
5404
|
-
const looksLikeDir = output.endsWith("/") ||
|
|
6074
|
+
if (!output) return join16(process.cwd(), filename);
|
|
6075
|
+
const looksLikeDir = output.endsWith("/") || existsSync14(output) && statSync4(output).isDirectory();
|
|
5405
6076
|
if (looksLikeDir) {
|
|
5406
|
-
|
|
5407
|
-
return
|
|
6077
|
+
mkdirSync12(output, { recursive: true });
|
|
6078
|
+
return join16(output, filename);
|
|
5408
6079
|
}
|
|
5409
6080
|
return output;
|
|
5410
6081
|
}
|
|
@@ -5435,7 +6106,7 @@ async function downloadZip(label, call, output) {
|
|
|
5435
6106
|
const buf = Buffer.from(res.data);
|
|
5436
6107
|
const filename = filenameFromDisposition(res.response.headers.get("content-disposition")) ?? "skills.zip";
|
|
5437
6108
|
const path = resolveOutputPath(output, filename);
|
|
5438
|
-
|
|
6109
|
+
writeFileSync12(path, buf);
|
|
5439
6110
|
return { path, bytes: buf.length, filename };
|
|
5440
6111
|
}
|
|
5441
6112
|
function registerSkills(program2) {
|
|
@@ -5444,7 +6115,9 @@ function registerSkills(program2) {
|
|
|
5444
6115
|
"after",
|
|
5445
6116
|
`
|
|
5446
6117
|
Examples:
|
|
5447
|
-
$ sechroom skills install
|
|
6118
|
+
$ sechroom skills install --client claude materialise target:claude-code skills to ~/.claude/skills
|
|
6119
|
+
$ sechroom skills install --client codex materialise target:gpt-codex skills to ~/.codex/skills
|
|
6120
|
+
$ sechroom skills install --client all keep both global clients in sync
|
|
5448
6121
|
$ sechroom skills install --scope project write them to ./.claude/skills instead
|
|
5449
6122
|
$ sechroom skills list what's materialised on disk
|
|
5450
6123
|
$ sechroom skills clean remove the materialised skill files
|
|
@@ -5453,11 +6126,15 @@ Examples:
|
|
|
5453
6126
|
$ sechroom skills package --from-source --workspace wsp_abc -o ./dist zip a draft from source
|
|
5454
6127
|
$ sechroom skills set-lane --code-lane claude-code-chris --design-lane claude-design-chris
|
|
5455
6128
|
|
|
6129
|
+
When --client is omitted, configured client flags/environment variables win;
|
|
6130
|
+
otherwise existing default client homes are detected. Both configured/detected
|
|
6131
|
+
clients select all; an unconfigured machine preserves the legacy Claude default.
|
|
6132
|
+
|
|
5456
6133
|
`
|
|
5457
6134
|
);
|
|
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));
|
|
6135
|
+
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));
|
|
6136
|
+
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));
|
|
6137
|
+
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
6138
|
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
6139
|
const json = Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json);
|
|
5463
6140
|
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
@@ -5603,12 +6280,12 @@ Examples:
|
|
|
5603
6280
|
}
|
|
5604
6281
|
|
|
5605
6282
|
// src/commands/sweep.ts
|
|
5606
|
-
import { existsSync as
|
|
5607
|
-
import { dirname as
|
|
5608
|
-
var DEFAULT_MANIFEST =
|
|
6283
|
+
import { existsSync as existsSync15 } from "fs";
|
|
6284
|
+
import { dirname as dirname10, join as join17, resolve as resolve2 } from "path";
|
|
6285
|
+
var DEFAULT_MANIFEST = join17(".sechroom", "repos.json");
|
|
5609
6286
|
function planEntry(entry, root) {
|
|
5610
6287
|
const dir = resolveChildDir(entry.path, root);
|
|
5611
|
-
if (!
|
|
6288
|
+
if (!existsSync15(dir)) {
|
|
5612
6289
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
5613
6290
|
}
|
|
5614
6291
|
if (committedBindingPath(dir)) {
|
|
@@ -5684,7 +6361,7 @@ Examples:
|
|
|
5684
6361
|
`);
|
|
5685
6362
|
return;
|
|
5686
6363
|
}
|
|
5687
|
-
const root =
|
|
6364
|
+
const root = dirname10(dirname10(manifestPath));
|
|
5688
6365
|
const plans = repos.map((entry) => planEntry(entry, root));
|
|
5689
6366
|
if (!json) {
|
|
5690
6367
|
process.stderr.write(
|
|
@@ -5703,13 +6380,13 @@ Examples:
|
|
|
5703
6380
|
|
|
5704
6381
|
// src/commands/telemetry.ts
|
|
5705
6382
|
import {
|
|
5706
|
-
existsSync as
|
|
5707
|
-
mkdirSync as
|
|
5708
|
-
readFileSync as
|
|
6383
|
+
existsSync as existsSync16,
|
|
6384
|
+
mkdirSync as mkdirSync13,
|
|
6385
|
+
readFileSync as readFileSync12,
|
|
5709
6386
|
rmSync as rmSync4,
|
|
5710
|
-
writeFileSync as
|
|
6387
|
+
writeFileSync as writeFileSync13
|
|
5711
6388
|
} from "fs";
|
|
5712
|
-
import { dirname as
|
|
6389
|
+
import { dirname as dirname11, join as join18 } from "path";
|
|
5713
6390
|
function registerTelemetry(program2) {
|
|
5714
6391
|
const telemetry = program2.command("telemetry").description(
|
|
5715
6392
|
"Emit WLP run telemetry (an executor leg's progress events) into a decomposition run"
|
|
@@ -5779,14 +6456,14 @@ function registerTelemetry(program2) {
|
|
|
5779
6456
|
"Decomposition id this session executes"
|
|
5780
6457
|
).requiredOption("--task <id>", "Task id this session executes").action((opts, cmd) => {
|
|
5781
6458
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
5782
|
-
const dir =
|
|
5783
|
-
|
|
5784
|
-
const path =
|
|
6459
|
+
const dir = join18(process.cwd(), ".sechroom");
|
|
6460
|
+
mkdirSync13(dir, { recursive: true });
|
|
6461
|
+
const path = join18(dir, BINDING_FILE);
|
|
5785
6462
|
const binding = {
|
|
5786
6463
|
decompositionId: opts.decomposition,
|
|
5787
6464
|
taskId: opts.task
|
|
5788
6465
|
};
|
|
5789
|
-
|
|
6466
|
+
writeFileSync13(path, JSON.stringify(binding, null, 2) + "\n");
|
|
5790
6467
|
ensureStateDirIgnored(process.cwd());
|
|
5791
6468
|
if (json) {
|
|
5792
6469
|
emit({ bound: true, ...binding, path }, true);
|
|
@@ -5801,8 +6478,8 @@ function registerTelemetry(program2) {
|
|
|
5801
6478
|
});
|
|
5802
6479
|
telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
|
|
5803
6480
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
5804
|
-
const path =
|
|
5805
|
-
const existed =
|
|
6481
|
+
const path = join18(process.cwd(), ".sechroom", BINDING_FILE);
|
|
6482
|
+
const existed = existsSync16(path);
|
|
5806
6483
|
if (existed) rmSync4(path);
|
|
5807
6484
|
if (json) emit({ unbound: existed, path }, true);
|
|
5808
6485
|
else
|
|
@@ -5910,11 +6587,11 @@ async function postTelemetry(cfg, decompositionId, events) {
|
|
|
5910
6587
|
function findBinding(start) {
|
|
5911
6588
|
let dir = start;
|
|
5912
6589
|
for (; ; ) {
|
|
5913
|
-
const path =
|
|
5914
|
-
if (
|
|
6590
|
+
const path = join18(dir, ".sechroom", BINDING_FILE);
|
|
6591
|
+
if (existsSync16(path)) {
|
|
5915
6592
|
try {
|
|
5916
6593
|
const b = JSON.parse(
|
|
5917
|
-
|
|
6594
|
+
readFileSync12(path, "utf8")
|
|
5918
6595
|
);
|
|
5919
6596
|
if (b.decompositionId && b.taskId)
|
|
5920
6597
|
return { decompositionId: b.decompositionId, taskId: b.taskId };
|
|
@@ -5922,18 +6599,18 @@ function findBinding(start) {
|
|
|
5922
6599
|
}
|
|
5923
6600
|
return null;
|
|
5924
6601
|
}
|
|
5925
|
-
const parent =
|
|
6602
|
+
const parent = dirname11(dir);
|
|
5926
6603
|
if (parent === dir) return null;
|
|
5927
6604
|
dir = parent;
|
|
5928
6605
|
}
|
|
5929
6606
|
}
|
|
5930
6607
|
function parseTranscript(path) {
|
|
5931
|
-
if (!
|
|
6608
|
+
if (!existsSync16(path)) return null;
|
|
5932
6609
|
let tokensIn = 0;
|
|
5933
6610
|
let tokensOut = 0;
|
|
5934
6611
|
let contextUsed = 0;
|
|
5935
6612
|
let model = "";
|
|
5936
|
-
for (const line of
|
|
6613
|
+
for (const line of readFileSync12(path, "utf8").split("\n")) {
|
|
5937
6614
|
if (!line.trim()) continue;
|
|
5938
6615
|
let obj;
|
|
5939
6616
|
try {
|
|
@@ -6240,7 +6917,7 @@ Examples:
|
|
|
6240
6917
|
function resolveVersion() {
|
|
6241
6918
|
try {
|
|
6242
6919
|
const pkg = JSON.parse(
|
|
6243
|
-
|
|
6920
|
+
readFileSync13(new URL("../package.json", import.meta.url), "utf8")
|
|
6244
6921
|
);
|
|
6245
6922
|
return pkg.version ?? "0.0.0";
|
|
6246
6923
|
} catch {
|
|
@@ -6399,6 +7076,7 @@ registerRelationships(program);
|
|
|
6399
7076
|
registerWorkspace(program);
|
|
6400
7077
|
registerProject(program);
|
|
6401
7078
|
registerDecomposition(program);
|
|
7079
|
+
registerExecutor(program);
|
|
6402
7080
|
registerClose(program);
|
|
6403
7081
|
registerFiling(program);
|
|
6404
7082
|
registerContinuity(program);
|