@sechroom/cli 2026.7.10 → 2026.7.11-rc.5e6a0694
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 +1375 -566
- package/package.json +4 -2
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,8 +657,32 @@ async function runApi(label, fn) {
|
|
|
632
657
|
s.succeed();
|
|
633
658
|
return res.data;
|
|
634
659
|
}
|
|
660
|
+
function formatFailureMessage(error) {
|
|
661
|
+
let msg;
|
|
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) {
|
|
668
|
+
const problem = error;
|
|
669
|
+
msg = String(problem.title);
|
|
670
|
+
if (problem.errors && typeof problem.errors === "object") {
|
|
671
|
+
const detail = Object.entries(problem.errors).flatMap(
|
|
672
|
+
([field, msgs]) => (Array.isArray(msgs) ? msgs : [msgs]).map((m) => ` ${field}: ${String(m)}`)
|
|
673
|
+
);
|
|
674
|
+
if (detail.length > 0) msg += `
|
|
675
|
+
${detail.join("\n")}`;
|
|
676
|
+
}
|
|
677
|
+
} else if (typeof error === "object" && error !== null) {
|
|
678
|
+
msg = JSON.stringify(error);
|
|
679
|
+
} else {
|
|
680
|
+
msg = String(error);
|
|
681
|
+
}
|
|
682
|
+
return msg;
|
|
683
|
+
}
|
|
635
684
|
function fail(error) {
|
|
636
|
-
const msg =
|
|
685
|
+
const msg = formatFailureMessage(error);
|
|
637
686
|
process.stderr.write(`error: ${msg}
|
|
638
687
|
`);
|
|
639
688
|
process.exit(1);
|
|
@@ -1061,14 +1110,23 @@ function bodyOf(row) {
|
|
|
1061
1110
|
const m = row?.item ?? row;
|
|
1062
1111
|
return m?.text ?? m?.Text ?? "";
|
|
1063
1112
|
}
|
|
1113
|
+
function idOf(row) {
|
|
1114
|
+
const m = row?.item ?? row;
|
|
1115
|
+
return String(m?.id ?? m?.memoryId ?? "unknown");
|
|
1116
|
+
}
|
|
1064
1117
|
function entriesFromRows(rows, surface, source, roleTag, namePrefix) {
|
|
1065
1118
|
const out = /* @__PURE__ */ new Map();
|
|
1066
1119
|
for (const row of rows ?? []) {
|
|
1067
1120
|
const tags = tagsOf(row);
|
|
1068
1121
|
if (!tags.includes(roleTag)) continue;
|
|
1069
|
-
if (
|
|
1122
|
+
if (!tags.includes(`target:${surface}`)) continue;
|
|
1070
1123
|
const name = tagValue(tags, namePrefix);
|
|
1071
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
|
+
}
|
|
1072
1130
|
out.set(name, { name, body: bodyOf(row), source });
|
|
1073
1131
|
}
|
|
1074
1132
|
return out;
|
|
@@ -1091,25 +1149,41 @@ function resolveReferences(systemRows, personalRows, surface) {
|
|
|
1091
1149
|
}
|
|
1092
1150
|
|
|
1093
1151
|
// src/setup/skill-resolution-io.ts
|
|
1094
|
-
var AGENT_TARGET = {
|
|
1152
|
+
var AGENT_TARGET = {
|
|
1153
|
+
"claude-code": "claude-agent",
|
|
1154
|
+
"gpt-codex": "gpt-codex-agent"
|
|
1155
|
+
};
|
|
1095
1156
|
function agentTargetFor(surface) {
|
|
1096
1157
|
return AGENT_TARGET[surface] ?? `${surface}-agent`;
|
|
1097
1158
|
}
|
|
1098
1159
|
async function fetchFeedRows(cfg, workspaceId) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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", {
|
|
1102
1165
|
params: {
|
|
1103
1166
|
path: { workspaceId },
|
|
1104
1167
|
// cascadeWorkspaces: skills land in an "Operator Skills" SUB-workspace;
|
|
1105
1168
|
// includeText: the feed omits bodies by default, we need them for SKILL.md.
|
|
1106
|
-
query: {
|
|
1169
|
+
query: {
|
|
1170
|
+
limit: 200,
|
|
1171
|
+
cascadeWorkspaces: true,
|
|
1172
|
+
includeText: true,
|
|
1173
|
+
...cursor ? { cursor } : {}
|
|
1174
|
+
}
|
|
1107
1175
|
}
|
|
1108
|
-
})
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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;
|
|
1113
1187
|
}
|
|
1114
1188
|
async function fetchTemplateRows(cfg, personalWorkspaceId) {
|
|
1115
1189
|
const [systemRows, personalRows] = await Promise.all([
|
|
@@ -1129,7 +1203,10 @@ function resolveReferenceSet(rows, surface) {
|
|
|
1129
1203
|
}
|
|
1130
1204
|
|
|
1131
1205
|
// src/setup/materialise.ts
|
|
1132
|
-
var CLIENT_SURFACE =
|
|
1206
|
+
var CLIENT_SURFACE = {
|
|
1207
|
+
claude: "claude-code",
|
|
1208
|
+
codex: "gpt-codex"
|
|
1209
|
+
};
|
|
1133
1210
|
function writeSkills(dir, skills, surface) {
|
|
1134
1211
|
const written = [];
|
|
1135
1212
|
for (const s of skills) {
|
|
@@ -1140,12 +1217,58 @@ function writeSkills(dir, skills, surface) {
|
|
|
1140
1217
|
if (written.length) recordMaterialisedSkills(dir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
1141
1218
|
return written;
|
|
1142
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
|
+
}
|
|
1143
1264
|
function writeAgents(dir, agents, surface) {
|
|
1144
1265
|
if (agents.length) mkdirSync3(dir, { recursive: true });
|
|
1145
1266
|
const written = [];
|
|
1146
1267
|
for (const a of agents) {
|
|
1147
|
-
const
|
|
1148
|
-
|
|
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);
|
|
1149
1272
|
written.push(file);
|
|
1150
1273
|
}
|
|
1151
1274
|
if (written.length) recordMaterialisedSkills(dir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -1162,11 +1285,71 @@ function writeReferencesIntoSkillDirs(dir, skills, refs) {
|
|
|
1162
1285
|
}
|
|
1163
1286
|
return refs.map((r) => r.name);
|
|
1164
1287
|
}
|
|
1165
|
-
var SKILL_SPEC = {
|
|
1166
|
-
|
|
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
|
+
};
|
|
1167
1302
|
function scopeOf(opts) {
|
|
1168
1303
|
return opts.local ? "project" : resolveScope(opts.scope);
|
|
1169
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
|
+
}
|
|
1170
1353
|
async function runInstall(spec, cmd, opts) {
|
|
1171
1354
|
const g = cmd.optsWithGlobals();
|
|
1172
1355
|
let scope;
|
|
@@ -1178,31 +1361,52 @@ async function runInstall(spec, cmd, opts) {
|
|
|
1178
1361
|
const cfg = resolveConfig(g);
|
|
1179
1362
|
const dryRun = Boolean(opts.dryRun);
|
|
1180
1363
|
const json = Boolean(g.json || opts.json);
|
|
1181
|
-
|
|
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
|
+
}
|
|
1182
1372
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
1183
1373
|
const rows = await fetchTemplateRows(cfg, personalWorkspaceId);
|
|
1184
|
-
const items = spec.resolve(rows, CLIENT_SURFACE);
|
|
1185
|
-
const refs = spec.kind === "skill" ? resolveReferenceSet(rows, CLIENT_SURFACE) : [];
|
|
1186
1374
|
const results = targets.map((t) => {
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
+
};
|
|
1191
1395
|
});
|
|
1192
|
-
if (json) return emit({ kind: spec.kind,
|
|
1193
|
-
if (
|
|
1396
|
+
if (json) return emit({ kind: spec.kind, client: selection, dryRun, targets: results }, true);
|
|
1397
|
+
if (results.every((r) => r.available === 0)) {
|
|
1194
1398
|
console.log(style.dim(`No ${spec.kind}s available to install \u2014 is the bundle installed for your account?`));
|
|
1195
1399
|
return;
|
|
1196
1400
|
}
|
|
1197
1401
|
for (const r of results) {
|
|
1198
1402
|
console.log(
|
|
1199
|
-
`${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}`
|
|
1200
1404
|
);
|
|
1201
1405
|
if (r.refsWritten.length)
|
|
1202
1406
|
console.log(
|
|
1203
1407
|
`${dryRun ? "" : style.green("\u2713 ")}${dryRun ? "would write" : "wrote"} ${r.refsWritten.length} reference(s) into each skill ${style.dim("\u2192")} ${r.dir}/<skill>/references`
|
|
1204
1408
|
);
|
|
1205
|
-
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}]`)}`);
|
|
1206
1410
|
}
|
|
1207
1411
|
}
|
|
1208
1412
|
function runList(spec, cmd, opts) {
|
|
@@ -1214,16 +1418,22 @@ function runList(spec, cmd, opts) {
|
|
|
1214
1418
|
return fail(err2.message);
|
|
1215
1419
|
}
|
|
1216
1420
|
const json = Boolean(g.json || opts.json);
|
|
1217
|
-
|
|
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
|
+
}
|
|
1218
1429
|
const out = targets.map((t) => {
|
|
1219
|
-
const
|
|
1220
|
-
const lock = readSkillsLock(dir);
|
|
1430
|
+
const lock = readSkillsLock(t.dir);
|
|
1221
1431
|
const entries = Object.entries(lock).flatMap(
|
|
1222
|
-
([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)) }))
|
|
1223
1433
|
);
|
|
1224
|
-
return { dir, label: t.label, entries };
|
|
1434
|
+
return { client: t.client, surface: t.surface, dir: t.dir, label: t.label, entries };
|
|
1225
1435
|
});
|
|
1226
|
-
if (json) return emit({ kind: spec.kind, targets: out }, true);
|
|
1436
|
+
if (json) return emit({ kind: spec.kind, client: selection, targets: out }, true);
|
|
1227
1437
|
let any = false;
|
|
1228
1438
|
for (const t of out) {
|
|
1229
1439
|
if (t.entries.length === 0) continue;
|
|
@@ -1246,33 +1456,39 @@ function runClean(spec, cmd, opts, slugArg) {
|
|
|
1246
1456
|
return fail(err2.message);
|
|
1247
1457
|
}
|
|
1248
1458
|
const json = Boolean(g.json || opts.json);
|
|
1249
|
-
|
|
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
|
+
}
|
|
1250
1467
|
const cleaned = [];
|
|
1251
1468
|
const missing = [];
|
|
1252
1469
|
for (const t of targets) {
|
|
1253
|
-
const
|
|
1254
|
-
const lock = readSkillsLock(dir);
|
|
1470
|
+
const lock = readSkillsLock(t.dir);
|
|
1255
1471
|
const entry = lock[slug];
|
|
1256
1472
|
if (!entry) {
|
|
1257
|
-
missing.push(join4(dir, SKILLS_LOCK));
|
|
1473
|
+
missing.push(join4(t.dir, SKILLS_LOCK));
|
|
1258
1474
|
continue;
|
|
1259
1475
|
}
|
|
1260
1476
|
const removed = [];
|
|
1261
1477
|
for (const name of entry.skills) {
|
|
1262
|
-
const p = join4(dir, name);
|
|
1478
|
+
const p = join4(t.dir, name);
|
|
1263
1479
|
if (existsSync3(p)) {
|
|
1264
1480
|
rmSync2(p, { recursive: true, force: true });
|
|
1265
1481
|
removed.push(name);
|
|
1266
1482
|
}
|
|
1267
1483
|
}
|
|
1268
1484
|
delete lock[slug];
|
|
1269
|
-
writeSkillsLock(dir, lock);
|
|
1270
|
-
cleaned.push({ dir, removed });
|
|
1485
|
+
writeSkillsLock(t.dir, lock);
|
|
1486
|
+
cleaned.push({ client: t.client, surface: t.surface, dir: t.dir, removed });
|
|
1271
1487
|
}
|
|
1272
1488
|
if (cleaned.length === 0) {
|
|
1273
1489
|
return fail(`No materialised ${spec.kind}s recorded for '${slug}' in ${missing.join(", ")}.`);
|
|
1274
1490
|
}
|
|
1275
|
-
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);
|
|
1276
1492
|
for (const c of cleaned) {
|
|
1277
1493
|
console.log(style.green(`Removed ${c.removed.length} ${spec.kind}(s) for ${slug} from ${c.dir}`));
|
|
1278
1494
|
}
|
|
@@ -1286,22 +1502,24 @@ function registerAgents(program2) {
|
|
|
1286
1502
|
`
|
|
1287
1503
|
Examples:
|
|
1288
1504
|
$ sechroom agents install materialise your installed agents to ~/.claude/agents
|
|
1505
|
+
$ sechroom agents install --client codex materialise native TOML agents to ~/.codex/agents
|
|
1289
1506
|
$ sechroom agents install --scope project write them to ./.claude/agents instead
|
|
1290
1507
|
$ sechroom agents install --claude-config-dir ~/.claude-work target another instance
|
|
1291
1508
|
$ sechroom agents list what's materialised on disk
|
|
1292
1509
|
$ sechroom agents clean remove the materialised agent files
|
|
1293
1510
|
|
|
1294
|
-
Agents are resolved from the agent target (target:claude-agent
|
|
1295
|
-
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).`
|
|
1296
1514
|
);
|
|
1297
|
-
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));
|
|
1298
|
-
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));
|
|
1299
|
-
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));
|
|
1300
1518
|
}
|
|
1301
1519
|
|
|
1302
1520
|
// src/commands/channel.ts
|
|
1303
|
-
import { existsSync as
|
|
1304
|
-
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";
|
|
1305
1523
|
import {
|
|
1306
1524
|
HttpTransportType,
|
|
1307
1525
|
HubConnectionBuilder
|
|
@@ -1309,52 +1527,218 @@ import {
|
|
|
1309
1527
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1310
1528
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1311
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
|
+
|
|
1312
1696
|
// src/commands/hook-install.ts
|
|
1313
|
-
import { existsSync as
|
|
1314
|
-
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";
|
|
1315
1699
|
|
|
1316
1700
|
// src/setup/clients.ts
|
|
1317
|
-
import { existsSync as
|
|
1701
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1318
1702
|
import { homedir as homedir3 } from "os";
|
|
1319
|
-
import { dirname as
|
|
1703
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
1320
1704
|
function claudeDesktopConfigPath(home) {
|
|
1321
1705
|
switch (process.platform) {
|
|
1322
1706
|
case "darwin":
|
|
1323
|
-
return
|
|
1707
|
+
return join6(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
1324
1708
|
case "win32":
|
|
1325
|
-
return
|
|
1709
|
+
return join6(process.env.APPDATA ?? join6(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
1326
1710
|
default:
|
|
1327
|
-
return
|
|
1711
|
+
return join6(home, ".config", "Claude", "claude_desktop_config.json");
|
|
1328
1712
|
}
|
|
1329
1713
|
}
|
|
1330
1714
|
function clientTargets(cwd, opts = {}) {
|
|
1331
1715
|
const home = homedir3();
|
|
1332
|
-
const claudeDir = opts.claudeDir ??
|
|
1333
|
-
const codexHome = opts.codexHome ??
|
|
1716
|
+
const claudeDir = opts.claudeDir ?? join6(home, ".claude");
|
|
1717
|
+
const codexHome = opts.codexHome ?? join6(home, ".codex");
|
|
1334
1718
|
return {
|
|
1335
1719
|
"claude-code": {
|
|
1336
1720
|
key: "claude-code",
|
|
1337
1721
|
label: "Claude Code",
|
|
1338
|
-
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path:
|
|
1339
|
-
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") }
|
|
1340
1724
|
},
|
|
1341
1725
|
"claude-desktop": {
|
|
1342
1726
|
key: "claude-desktop",
|
|
1343
1727
|
label: "Claude Desktop",
|
|
1344
1728
|
mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
|
|
1345
|
-
instruction: { surfaceKey: "claude-desktop", path:
|
|
1729
|
+
instruction: { surfaceKey: "claude-desktop", path: join6(claudeDir, "CLAUDE.md") }
|
|
1346
1730
|
},
|
|
1347
1731
|
codex: {
|
|
1348
1732
|
key: "codex",
|
|
1349
1733
|
label: "Codex CLI",
|
|
1350
|
-
mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path:
|
|
1351
|
-
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") }
|
|
1352
1736
|
},
|
|
1353
1737
|
cursor: {
|
|
1354
1738
|
key: "cursor",
|
|
1355
1739
|
label: "Cursor",
|
|
1356
|
-
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path:
|
|
1357
|
-
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") }
|
|
1358
1742
|
},
|
|
1359
1743
|
antigravity: {
|
|
1360
1744
|
key: "antigravity",
|
|
@@ -1365,8 +1749,8 @@ function clientTargets(cwd, opts = {}) {
|
|
|
1365
1749
|
// `type` — comes from the `antigravity` server surface, so we don't
|
|
1366
1750
|
// hardcode it here. Instructions go in the project `AGENTS.md`
|
|
1367
1751
|
// (cross-tool, shared with Codex/Cursor).
|
|
1368
|
-
mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path:
|
|
1369
|
-
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") }
|
|
1370
1754
|
}
|
|
1371
1755
|
};
|
|
1372
1756
|
}
|
|
@@ -1375,11 +1759,11 @@ var DEFAULT_CLIENT_KEY = "claude-code";
|
|
|
1375
1759
|
function detectInstalledClients(cwd) {
|
|
1376
1760
|
const home = homedir3();
|
|
1377
1761
|
const detected = [];
|
|
1378
|
-
if (resolveClaudeTargets({}).some((t) =>
|
|
1379
|
-
if (
|
|
1380
|
-
if (resolveCodexHomes({}).some((d) =>
|
|
1381
|
-
if (
|
|
1382
|
-
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");
|
|
1383
1767
|
return detected;
|
|
1384
1768
|
}
|
|
1385
1769
|
|
|
@@ -1388,15 +1772,24 @@ var CLAUDE_HOOK_COMMANDS = {
|
|
|
1388
1772
|
SessionStart: "sechroom hook session-start",
|
|
1389
1773
|
PreCompact: "sechroom hook pre-compact",
|
|
1390
1774
|
SessionEnd: "sechroom hook session-end",
|
|
1391
|
-
// WLP telemetry tap (D-WLP-10) — per-turn executor self-report.
|
|
1392
|
-
//
|
|
1393
|
-
//
|
|
1394
|
-
|
|
1775
|
+
// WLP telemetry tap (D-WLP-10 + FR-352 Tier 1) — per-turn executor self-report. The one
|
|
1776
|
+
// `telemetry hook` verb dispatches on hook_event_name: Stop/SubagentStop → parsed (token/context) +
|
|
1777
|
+
// terminal (turn end), Notification/PermissionDenied → approval. No-op (exit 0) unless this checkout
|
|
1778
|
+
// is bound via `sechroom telemetry bind`, so it's safe to wire for every Claude install; an event a
|
|
1779
|
+
// given Claude Code version doesn't know is inert (never fires). Claude-only.
|
|
1780
|
+
Stop: "sechroom telemetry hook",
|
|
1781
|
+
SubagentStop: "sechroom telemetry hook",
|
|
1782
|
+
Notification: "sechroom telemetry hook",
|
|
1783
|
+
PermissionDenied: "sechroom telemetry hook"
|
|
1395
1784
|
};
|
|
1396
1785
|
var CODEX_HOOK_COMMANDS = {
|
|
1397
1786
|
SessionStart: "sechroom hook session-start",
|
|
1787
|
+
PreCompact: "sechroom hook pre-compact",
|
|
1398
1788
|
Stop: "sechroom hook session-end --debounce-minutes 10"
|
|
1399
1789
|
};
|
|
1790
|
+
function hookCommandsForSurface(surface) {
|
|
1791
|
+
return surface === "claude" ? CLAUDE_HOOK_COMMANDS : CODEX_HOOK_COMMANDS;
|
|
1792
|
+
}
|
|
1400
1793
|
function hasHookCommand(config2, event, command) {
|
|
1401
1794
|
const groups = config2.hooks?.[event] ?? [];
|
|
1402
1795
|
return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
|
|
@@ -1413,24 +1806,30 @@ function mergeHooks(config2, commands) {
|
|
|
1413
1806
|
return added;
|
|
1414
1807
|
}
|
|
1415
1808
|
function readJsonConfig2(path) {
|
|
1416
|
-
if (!
|
|
1417
|
-
const raw =
|
|
1809
|
+
if (!existsSync6(path)) return {};
|
|
1810
|
+
const raw = readFileSync4(path, "utf8");
|
|
1418
1811
|
if (!raw.trim()) return {};
|
|
1419
1812
|
return JSON.parse(raw);
|
|
1420
1813
|
}
|
|
1421
1814
|
function installHooksJson(path, commands, dryRun) {
|
|
1422
|
-
const existed =
|
|
1815
|
+
const existed = existsSync6(path) && readFileSync4(path, "utf8").trim().length > 0;
|
|
1423
1816
|
const config2 = readJsonConfig2(path);
|
|
1424
1817
|
const added = mergeHooks(config2, commands);
|
|
1425
1818
|
if (added === 0 && existed) return { path, status: "current" };
|
|
1426
1819
|
if (!dryRun) {
|
|
1427
|
-
|
|
1428
|
-
|
|
1820
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
1821
|
+
writeFileSync5(path, JSON.stringify(config2, null, 2) + "\n");
|
|
1429
1822
|
}
|
|
1430
1823
|
return { path, status: existed ? "merged" : "created" };
|
|
1431
1824
|
}
|
|
1432
1825
|
function installClaudeCommands(claudeDir, commands, dryRun) {
|
|
1433
|
-
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
|
+
];
|
|
1434
1833
|
}
|
|
1435
1834
|
function ensureCodexFeaturesHooks(content) {
|
|
1436
1835
|
const lines = content.split("\n");
|
|
@@ -1453,13 +1852,13 @@ function ensureCodexFeaturesHooks(content) {
|
|
|
1453
1852
|
return { next: lines.join("\n"), changed: true };
|
|
1454
1853
|
}
|
|
1455
1854
|
function installCodexFeatureFlag(path, dryRun) {
|
|
1456
|
-
const existed =
|
|
1457
|
-
const content = existed ?
|
|
1855
|
+
const existed = existsSync6(path);
|
|
1856
|
+
const content = existed ? readFileSync4(path, "utf8") : "";
|
|
1458
1857
|
const { next, changed } = ensureCodexFeaturesHooks(content);
|
|
1459
1858
|
if (!changed) return { path, status: "current" };
|
|
1460
1859
|
if (!dryRun) {
|
|
1461
|
-
|
|
1462
|
-
|
|
1860
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
1861
|
+
writeFileSync5(path, next);
|
|
1463
1862
|
}
|
|
1464
1863
|
return { path, status: existed ? "merged" : "created" };
|
|
1465
1864
|
}
|
|
@@ -1484,11 +1883,11 @@ function installHookSurfaces(surfaces, opts) {
|
|
|
1484
1883
|
const out = [];
|
|
1485
1884
|
for (const surface of surfaces) {
|
|
1486
1885
|
if (surface === "claude") {
|
|
1487
|
-
const path =
|
|
1886
|
+
const path = join7(opts.claudeDir, "settings.json");
|
|
1488
1887
|
out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
|
|
1489
1888
|
} else {
|
|
1490
|
-
const hooksJson = installHooksJson(
|
|
1491
|
-
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);
|
|
1492
1891
|
out.push({ surface, results: [hooksJson, featureFlag] });
|
|
1493
1892
|
}
|
|
1494
1893
|
}
|
|
@@ -1508,7 +1907,7 @@ function isSechroomOnPath() {
|
|
|
1508
1907
|
for (const dir of pathEnv.split(delimiter)) {
|
|
1509
1908
|
if (!dir) continue;
|
|
1510
1909
|
for (const name of names) {
|
|
1511
|
-
if (
|
|
1910
|
+
if (existsSync6(join7(dir, name))) return true;
|
|
1512
1911
|
}
|
|
1513
1912
|
}
|
|
1514
1913
|
return false;
|
|
@@ -1521,6 +1920,446 @@ function warnIfSechroomNotOnPath(write = (s) => void process.stderr.write(s)) {
|
|
|
1521
1920
|
return true;
|
|
1522
1921
|
}
|
|
1523
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
|
+
|
|
1524
2363
|
// src/commands/channel.ts
|
|
1525
2364
|
function registerChannel(program2) {
|
|
1526
2365
|
const channel = program2.command("channel").description(
|
|
@@ -1528,13 +2367,17 @@ function registerChannel(program2) {
|
|
|
1528
2367
|
);
|
|
1529
2368
|
const withFilterOpts = (c) => c.option(
|
|
1530
2369
|
"--name <name>",
|
|
1531
|
-
"
|
|
1532
|
-
"wlp-dispatch"
|
|
2370
|
+
"Deprecated: ignored; the installed executor selects its delivery subscription"
|
|
1533
2371
|
).option(
|
|
1534
2372
|
"--tag <tag...>",
|
|
1535
|
-
"
|
|
1536
|
-
|
|
1537
|
-
|
|
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
|
+
);
|
|
1538
2381
|
withFilterOpts(
|
|
1539
2382
|
channel.command("connect").description(
|
|
1540
2383
|
"Register a SignalR subscription and stream matched events to stdout"
|
|
@@ -1542,37 +2385,54 @@ function registerChannel(program2) {
|
|
|
1542
2385
|
).action(async (opts, cmd) => {
|
|
1543
2386
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
1544
2387
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1545
|
-
|
|
1546
|
-
const
|
|
1547
|
-
const
|
|
1548
|
-
const deliver =
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
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
|
|
1554
2410
|
);
|
|
1555
|
-
const conn = await openConnection(cfg, deliver);
|
|
1556
|
-
await reconcile(cfg, filter, deliver);
|
|
1557
2411
|
if (json) {
|
|
1558
2412
|
emit(
|
|
1559
2413
|
{
|
|
1560
2414
|
connected: true,
|
|
1561
2415
|
tenant: cfg.tenant,
|
|
1562
|
-
|
|
1563
|
-
|
|
2416
|
+
executorInstanceId: instance.id,
|
|
2417
|
+
instanceKey: located.state.instanceKey,
|
|
2418
|
+
laneId: located.state.laneId
|
|
1564
2419
|
},
|
|
1565
2420
|
true
|
|
1566
2421
|
);
|
|
1567
2422
|
} else {
|
|
1568
2423
|
process.stderr.write(
|
|
1569
2424
|
style.green("channel connected") + style.dim(
|
|
1570
|
-
` \u2014 tenant ${cfg.tenant},
|
|
2425
|
+
` \u2014 tenant ${cfg.tenant}, executor ${located.state.instanceKey}
|
|
1571
2426
|
`
|
|
1572
2427
|
) + style.dim("streaming matched events to stdout; Ctrl-C to stop.\n")
|
|
1573
2428
|
);
|
|
1574
2429
|
}
|
|
1575
|
-
|
|
2430
|
+
try {
|
|
2431
|
+
await holdOpen(conn);
|
|
2432
|
+
} finally {
|
|
2433
|
+
stopReconciliation();
|
|
2434
|
+
stopHeartbeat();
|
|
2435
|
+
}
|
|
1576
2436
|
});
|
|
1577
2437
|
withFilterOpts(
|
|
1578
2438
|
channel.command("mcp").description(
|
|
@@ -1580,18 +2440,18 @@ function registerChannel(program2) {
|
|
|
1580
2440
|
)
|
|
1581
2441
|
).action(async (opts, cmd) => {
|
|
1582
2442
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1583
|
-
|
|
2443
|
+
warnLegacyChannelOptions(opts);
|
|
2444
|
+
const located = requireExecutorState();
|
|
2445
|
+
const instance = await ensureExecutorInstance(cfg, located);
|
|
1584
2446
|
const mcp = new Server(
|
|
1585
2447
|
{ name: "sechroom", version: "0.1.0" },
|
|
1586
2448
|
{
|
|
1587
2449
|
capabilities: { experimental: { "claude/channel": {} } },
|
|
1588
|
-
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.'
|
|
1589
2451
|
}
|
|
1590
2452
|
);
|
|
1591
2453
|
await mcp.connect(new StdioServerTransport());
|
|
1592
|
-
|
|
1593
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1594
|
-
const deliver = makeDeliver(filter, seen, (payload) => {
|
|
2454
|
+
const deliver = (payload) => {
|
|
1595
2455
|
const { content, meta } = summarizeEvent(payload);
|
|
1596
2456
|
void mcp.notification({
|
|
1597
2457
|
method: "notifications/claude/channel",
|
|
@@ -1600,37 +2460,53 @@ function registerChannel(program2) {
|
|
|
1600
2460
|
(e) => process.stderr.write(err(`channel push failed: ${String(e)}
|
|
1601
2461
|
`))
|
|
1602
2462
|
);
|
|
1603
|
-
}
|
|
1604
|
-
const
|
|
1605
|
-
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
|
+
);
|
|
1606
2481
|
process.stderr.write(
|
|
1607
2482
|
style.dim(
|
|
1608
|
-
`sechroom channel (mcp) \u2014 tenant ${cfg.tenant},
|
|
2483
|
+
`sechroom channel (mcp) \u2014 tenant ${cfg.tenant}, executor ${located.state.instanceKey}
|
|
1609
2484
|
`
|
|
1610
2485
|
)
|
|
1611
2486
|
);
|
|
1612
|
-
|
|
2487
|
+
try {
|
|
2488
|
+
await holdOpen(conn);
|
|
2489
|
+
} finally {
|
|
2490
|
+
stopReconciliation();
|
|
2491
|
+
stopHeartbeat();
|
|
2492
|
+
}
|
|
1613
2493
|
});
|
|
1614
2494
|
channel.command("install").description(
|
|
1615
2495
|
"Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
|
|
1616
2496
|
).option(
|
|
1617
2497
|
"--workspace <wsp...>",
|
|
1618
|
-
"
|
|
2498
|
+
"Deprecated: accepted only to migrate an existing managed entry"
|
|
1619
2499
|
).option(
|
|
1620
2500
|
"--tag <tag...>",
|
|
1621
|
-
"
|
|
1622
|
-
["kind:task"]
|
|
2501
|
+
"Deprecated: accepted only to migrate an existing managed entry"
|
|
1623
2502
|
).option(
|
|
1624
2503
|
"--name <name>",
|
|
1625
2504
|
"MCP server + subscription name (idempotent per name)",
|
|
1626
2505
|
"sechroom-channel"
|
|
1627
2506
|
).option("--dry-run", "Print what would change; write nothing").action((opts) => {
|
|
1628
|
-
const path =
|
|
2507
|
+
const path = join9(process.cwd(), ".mcp.json");
|
|
1629
2508
|
const dryRun = Boolean(opts.dryRun);
|
|
1630
|
-
const args = ["channel", "mcp"
|
|
1631
|
-
for (const w of opts.workspace ?? [])
|
|
1632
|
-
args.push("--workspace", w);
|
|
1633
|
-
for (const t of opts.tag ?? []) args.push("--tag", t);
|
|
2509
|
+
const args = ["channel", "mcp"];
|
|
1634
2510
|
const entry = { command: "sechroom", args };
|
|
1635
2511
|
const config2 = readMcpConfig(path);
|
|
1636
2512
|
config2.mcpServers ??= {};
|
|
@@ -1638,8 +2514,8 @@ function registerChannel(program2) {
|
|
|
1638
2514
|
const status = JSON.stringify(existing) === JSON.stringify(entry) ? "current" : existing ? "updated" : "created";
|
|
1639
2515
|
if (status !== "current" && !dryRun) {
|
|
1640
2516
|
config2.mcpServers[opts.name] = entry;
|
|
1641
|
-
|
|
1642
|
-
|
|
2517
|
+
mkdirSync7(dirname6(path), { recursive: true });
|
|
2518
|
+
writeFileSync7(path, JSON.stringify(config2, null, 2) + "\n");
|
|
1643
2519
|
}
|
|
1644
2520
|
const verb = status === "current" ? "already configured" : dryRun ? `would ${status === "created" ? "create" : "update"}` : status;
|
|
1645
2521
|
process.stdout.write(`${style.green("channel")} ${path} (${verb})
|
|
@@ -1659,23 +2535,124 @@ Load it (Channels research preview) by launching your agent with:
|
|
|
1659
2535
|
);
|
|
1660
2536
|
}
|
|
1661
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
|
+
);
|
|
1662
2544
|
});
|
|
1663
2545
|
channel.addHelpText(
|
|
1664
2546
|
"after",
|
|
1665
2547
|
`
|
|
1666
2548
|
Examples:
|
|
1667
|
-
$ sechroom
|
|
1668
|
-
$ sechroom channel connect
|
|
1669
|
-
$ 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
|
|
1670
2551
|
|
|
1671
2552
|
# Wire it as a Claude Code channel MCP server (research preview, v2.1.80+):
|
|
1672
|
-
$ sechroom channel install
|
|
2553
|
+
$ sechroom channel install migrate/install the exact-instance channel
|
|
1673
2554
|
# then: claude --dangerously-load-development-channels server:sechroom-channel`
|
|
1674
2555
|
);
|
|
1675
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
|
+
}
|
|
1676
2653
|
function readMcpConfig(path) {
|
|
1677
|
-
if (!
|
|
1678
|
-
const raw =
|
|
2654
|
+
if (!existsSync8(path)) return {};
|
|
2655
|
+
const raw = readFileSync6(path, "utf8");
|
|
1679
2656
|
if (!raw.trim()) return {};
|
|
1680
2657
|
try {
|
|
1681
2658
|
return JSON.parse(raw);
|
|
@@ -1685,35 +2662,9 @@ function readMcpConfig(path) {
|
|
|
1685
2662
|
);
|
|
1686
2663
|
}
|
|
1687
2664
|
}
|
|
1688
|
-
function
|
|
1689
|
-
const
|
|
1690
|
-
const
|
|
1691
|
-
if (tags.length === 0 && workspaceScope.length === 0)
|
|
1692
|
-
fail(
|
|
1693
|
-
"A channel subscription needs at least one --tag or --workspace (an empty filter receives nothing)."
|
|
1694
|
-
);
|
|
1695
|
-
return { tags, workspaceScope };
|
|
1696
|
-
}
|
|
1697
|
-
async function ensureSubscription(cfg, name, filter) {
|
|
1698
|
-
const token = await requireToken(cfg);
|
|
1699
|
-
const resp = await fetch(`${cfg.baseUrl}/me/delivery-subscriptions/signalr`, {
|
|
1700
|
-
method: "POST",
|
|
1701
|
-
headers: {
|
|
1702
|
-
authorization: `Bearer ${token}`,
|
|
1703
|
-
tenant: cfg.tenant,
|
|
1704
|
-
"content-type": "application/json",
|
|
1705
|
-
"x-sechroom-surface": "cli"
|
|
1706
|
-
},
|
|
1707
|
-
body: JSON.stringify({ name, enabled: true, filter })
|
|
1708
|
-
});
|
|
1709
|
-
if (!resp.ok)
|
|
1710
|
-
fail(
|
|
1711
|
-
`Could not register the SignalR subscription (HTTP ${resp.status}): ${await resp.text()}`
|
|
1712
|
-
);
|
|
1713
|
-
return await resp.json();
|
|
1714
|
-
}
|
|
1715
|
-
async function openConnection(cfg, onEvent) {
|
|
1716
|
-
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}`, {
|
|
1717
2668
|
transport: HttpTransportType.LongPolling,
|
|
1718
2669
|
accessTokenFactory: () => requireToken(cfg)
|
|
1719
2670
|
}).withAutomaticReconnect().build();
|
|
@@ -1747,7 +2698,8 @@ function parseEvent(payload) {
|
|
|
1747
2698
|
}
|
|
1748
2699
|
}
|
|
1749
2700
|
const obj = data ?? {};
|
|
1750
|
-
const
|
|
2701
|
+
const envelope = obj.data ?? obj;
|
|
2702
|
+
const inner = envelope.offer ?? envelope;
|
|
1751
2703
|
const rawTags = inner.tags ?? inner.Tags;
|
|
1752
2704
|
return {
|
|
1753
2705
|
eventType: str(inner.eventType ?? inner.EventType ?? obj.type) || "substrate.event",
|
|
@@ -1756,113 +2708,6 @@ function parseEvent(payload) {
|
|
|
1756
2708
|
tags: Array.isArray(rawTags) ? rawTags.filter((t) => typeof t === "string") : void 0
|
|
1757
2709
|
};
|
|
1758
2710
|
}
|
|
1759
|
-
function shouldDeliver(payload, filter) {
|
|
1760
|
-
const { workspaceId, tags } = parseEvent(payload);
|
|
1761
|
-
if (filter.workspaceScope.length > 0 && (!workspaceId || !filter.workspaceScope.includes(workspaceId)))
|
|
1762
|
-
return false;
|
|
1763
|
-
if (filter.tags.length > 0) {
|
|
1764
|
-
if (!tags) return false;
|
|
1765
|
-
return facetedTagMatch(tags, filter.tags);
|
|
1766
|
-
}
|
|
1767
|
-
return true;
|
|
1768
|
-
}
|
|
1769
|
-
function makeDeliver(filter, seen, forward) {
|
|
1770
|
-
return (payload) => {
|
|
1771
|
-
if (!shouldDeliver(payload, filter)) return;
|
|
1772
|
-
const { memoryId } = parseEvent(payload);
|
|
1773
|
-
if (memoryId) {
|
|
1774
|
-
if (seen.has(memoryId)) return;
|
|
1775
|
-
seen.add(memoryId);
|
|
1776
|
-
}
|
|
1777
|
-
forward(payload);
|
|
1778
|
-
};
|
|
1779
|
-
}
|
|
1780
|
-
async function reconcile(cfg, filter, deliver) {
|
|
1781
|
-
if (filter.workspaceScope.length === 0) {
|
|
1782
|
-
process.stderr.write(
|
|
1783
|
-
style.dim(
|
|
1784
|
-
"channel: no --workspace to reconcile against; live feed only (a dropped dispatch won't be recovered).\n"
|
|
1785
|
-
)
|
|
1786
|
-
);
|
|
1787
|
-
return;
|
|
1788
|
-
}
|
|
1789
|
-
if (filter.tags.length === 0) return;
|
|
1790
|
-
let token;
|
|
1791
|
-
try {
|
|
1792
|
-
token = await requireToken(cfg);
|
|
1793
|
-
} catch {
|
|
1794
|
-
return;
|
|
1795
|
-
}
|
|
1796
|
-
const qs = `filterTags=${encodeURIComponent(filter.tags.join(","))}&limit=100`;
|
|
1797
|
-
let recovered = 0;
|
|
1798
|
-
for (const ws of filter.workspaceScope) {
|
|
1799
|
-
try {
|
|
1800
|
-
const resp = await fetch(
|
|
1801
|
-
`${cfg.baseUrl}/workspaces/${encodeURIComponent(ws)}/memories/feed?${qs}`,
|
|
1802
|
-
{
|
|
1803
|
-
headers: {
|
|
1804
|
-
authorization: `Bearer ${token}`,
|
|
1805
|
-
tenant: cfg.tenant,
|
|
1806
|
-
"x-sechroom-surface": "cli"
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
);
|
|
1810
|
-
if (!resp.ok) {
|
|
1811
|
-
process.stderr.write(
|
|
1812
|
-
err(
|
|
1813
|
-
`channel: reconcile query for ${ws} failed (HTTP ${resp.status})
|
|
1814
|
-
`
|
|
1815
|
-
)
|
|
1816
|
-
);
|
|
1817
|
-
continue;
|
|
1818
|
-
}
|
|
1819
|
-
const data = await resp.json();
|
|
1820
|
-
for (const m of data.results ?? []) {
|
|
1821
|
-
if (!m.id) continue;
|
|
1822
|
-
deliver({
|
|
1823
|
-
eventType: "reconcile",
|
|
1824
|
-
memoryId: m.id,
|
|
1825
|
-
workspaceId: ws,
|
|
1826
|
-
tags: m.tags ?? []
|
|
1827
|
-
});
|
|
1828
|
-
recovered++;
|
|
1829
|
-
}
|
|
1830
|
-
} catch (e) {
|
|
1831
|
-
process.stderr.write(
|
|
1832
|
-
err(`channel: reconcile error for ${ws}: ${String(e)}
|
|
1833
|
-
`)
|
|
1834
|
-
);
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
if (recovered > 0)
|
|
1838
|
-
process.stderr.write(
|
|
1839
|
-
style.dim(
|
|
1840
|
-
`channel: reconciled ${recovered} already-queued event(s) on connect.
|
|
1841
|
-
`
|
|
1842
|
-
)
|
|
1843
|
-
);
|
|
1844
|
-
}
|
|
1845
|
-
function facetedTagMatch(eventTags, filterTags) {
|
|
1846
|
-
const have = new Set(eventTags);
|
|
1847
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1848
|
-
for (const f of filterTags) {
|
|
1849
|
-
const ns = f.endsWith(":*") ? f.slice(0, -2) : namespaceOf(f);
|
|
1850
|
-
const group = groups.get(ns) ?? [];
|
|
1851
|
-
group.push(f);
|
|
1852
|
-
groups.set(ns, group);
|
|
1853
|
-
}
|
|
1854
|
-
for (const [ns, group] of groups) {
|
|
1855
|
-
const ok2 = group.some(
|
|
1856
|
-
(f) => f.endsWith(":*") ? eventTags.some((t) => namespaceOf(t) === ns) : have.has(f)
|
|
1857
|
-
);
|
|
1858
|
-
if (!ok2) return false;
|
|
1859
|
-
}
|
|
1860
|
-
return true;
|
|
1861
|
-
}
|
|
1862
|
-
function namespaceOf(tag) {
|
|
1863
|
-
const i = tag.indexOf(":");
|
|
1864
|
-
return i >= 0 ? tag.slice(0, i) : tag;
|
|
1865
|
-
}
|
|
1866
2711
|
function summarizeEvent(payload) {
|
|
1867
2712
|
const { eventType, memoryId, workspaceId } = parseEvent(payload);
|
|
1868
2713
|
const content = memoryId ? `${eventType}: ${memoryId}${workspaceId ? ` (workspace ${workspaceId})` : ""}` : typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
@@ -1870,6 +2715,10 @@ function summarizeEvent(payload) {
|
|
|
1870
2715
|
if (eventType) meta.event_type = eventType;
|
|
1871
2716
|
if (memoryId) meta.memory_id = memoryId;
|
|
1872
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;
|
|
1873
2722
|
return { content, meta };
|
|
1874
2723
|
}
|
|
1875
2724
|
function str(v) {
|
|
@@ -1959,177 +2808,13 @@ Examples:
|
|
|
1959
2808
|
}
|
|
1960
2809
|
|
|
1961
2810
|
// src/commands/checkpoint.ts
|
|
1962
|
-
import { mkdirSync as
|
|
1963
|
-
import { dirname as
|
|
2811
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
2812
|
+
import { dirname as dirname8, join as join11 } from "path";
|
|
1964
2813
|
|
|
1965
2814
|
// src/commands/hook.ts
|
|
1966
2815
|
import { createHash as createHash2 } from "crypto";
|
|
1967
|
-
import { existsSync as
|
|
1968
|
-
import { dirname as
|
|
1969
|
-
|
|
1970
|
-
// src/sem.ts
|
|
1971
|
-
import { dirname as dirname5, join as join8 } from "path";
|
|
1972
|
-
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync6 } from "fs";
|
|
1973
|
-
var SEM_FILE = join8(".sechroom", "lane.json");
|
|
1974
|
-
var STATE_DIR_NAME2 = ".sechroom";
|
|
1975
|
-
function localSemPath(cwd = process.cwd()) {
|
|
1976
|
-
return join8(cwd, SEM_FILE);
|
|
1977
|
-
}
|
|
1978
|
-
function resolveSemPathForRead(start = process.cwd()) {
|
|
1979
|
-
let dir = start;
|
|
1980
|
-
while (true) {
|
|
1981
|
-
const candidate = join8(dir, SEM_FILE);
|
|
1982
|
-
if (existsSync7(candidate)) return candidate;
|
|
1983
|
-
const parent = dirname5(dir);
|
|
1984
|
-
if (parent === dir) return void 0;
|
|
1985
|
-
dir = parent;
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1988
|
-
function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
|
|
1989
|
-
try {
|
|
1990
|
-
let dir = start;
|
|
1991
|
-
let gitPath;
|
|
1992
|
-
for (; ; ) {
|
|
1993
|
-
const candidate = join8(dir, ".git");
|
|
1994
|
-
if (existsSync7(candidate)) {
|
|
1995
|
-
gitPath = candidate;
|
|
1996
|
-
break;
|
|
1997
|
-
}
|
|
1998
|
-
const parent = dirname5(dir);
|
|
1999
|
-
if (parent === dir) break;
|
|
2000
|
-
dir = parent;
|
|
2001
|
-
}
|
|
2002
|
-
if (!gitPath || statSync(gitPath).isDirectory()) return lane;
|
|
2003
|
-
const gitFile = readFileSync5(gitPath, "utf8");
|
|
2004
|
-
const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
|
|
2005
|
-
if (!common) return lane;
|
|
2006
|
-
const worktreesDir = join8(common[1], "worktrees");
|
|
2007
|
-
const siblings = readdirSync(worktreesDir).filter((n) => {
|
|
2008
|
-
try {
|
|
2009
|
-
return statSync(join8(worktreesDir, n)).isDirectory();
|
|
2010
|
-
} catch {
|
|
2011
|
-
return false;
|
|
2012
|
-
}
|
|
2013
|
-
});
|
|
2014
|
-
return laneWithWorktreeSuffix(lane, gitFile, siblings);
|
|
2015
|
-
} catch {
|
|
2016
|
-
return lane;
|
|
2017
|
-
}
|
|
2018
|
-
}
|
|
2019
|
-
function laneWithWorktreeSuffix(lane, gitFile, siblings) {
|
|
2020
|
-
const m = gitFile.trim().match(/\/worktrees\/([^/\s]+)\s*$/);
|
|
2021
|
-
if (!m) return lane;
|
|
2022
|
-
const idx = [...siblings].sort().indexOf(m[1]);
|
|
2023
|
-
return idx < 0 ? lane : `${lane}-${idx + 2}`;
|
|
2024
|
-
}
|
|
2025
|
-
function serializeSem(values) {
|
|
2026
|
-
return JSON.stringify(values, null, 2) + "\n";
|
|
2027
|
-
}
|
|
2028
|
-
function readSem(path) {
|
|
2029
|
-
const p = path ?? resolveSemPathForRead();
|
|
2030
|
-
if (!p || !existsSync7(p)) return void 0;
|
|
2031
|
-
return { path: p, values: parseLaneJson(readFileSync5(p, "utf8")) };
|
|
2032
|
-
}
|
|
2033
|
-
function readLocalSemValues(cwd = process.cwd()) {
|
|
2034
|
-
const next = join8(cwd, SEM_FILE);
|
|
2035
|
-
if (existsSync7(next)) return readSem(next)?.values ?? {};
|
|
2036
|
-
return {};
|
|
2037
|
-
}
|
|
2038
|
-
function parseLaneJson(text2) {
|
|
2039
|
-
try {
|
|
2040
|
-
const parsed = JSON.parse(text2);
|
|
2041
|
-
const out = {};
|
|
2042
|
-
for (const [k, v] of Object.entries(parsed)) {
|
|
2043
|
-
if (typeof v === "string") out[k] = v;
|
|
2044
|
-
}
|
|
2045
|
-
return out;
|
|
2046
|
-
} catch {
|
|
2047
|
-
return {};
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
var STATE_DIR_IGNORE = `${STATE_DIR_NAME2}/`;
|
|
2051
|
-
function writeSem(values, path = localSemPath()) {
|
|
2052
|
-
mkdirSync6(dirname5(path), { recursive: true });
|
|
2053
|
-
writeFileSync6(path, serializeSem(values));
|
|
2054
|
-
ensureSemIgnored(path);
|
|
2055
|
-
ensureContinuityScaffold(path);
|
|
2056
|
-
return path;
|
|
2057
|
-
}
|
|
2058
|
-
function ensureStateDirIgnored(cwd = process.cwd()) {
|
|
2059
|
-
ensureSemIgnored(localSemPath(cwd));
|
|
2060
|
-
}
|
|
2061
|
-
var CONTINUITY_FILE_NAME = "continuity.json";
|
|
2062
|
-
var CONTINUITY_SCAFFOLD = JSON.stringify(
|
|
2063
|
-
{
|
|
2064
|
-
_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.",
|
|
2065
|
-
objective: "",
|
|
2066
|
-
state: "",
|
|
2067
|
-
lastAction: "",
|
|
2068
|
-
nextAction: "",
|
|
2069
|
-
resumeInstruction: "",
|
|
2070
|
-
constraints: [],
|
|
2071
|
-
questions: [],
|
|
2072
|
-
artifacts: [],
|
|
2073
|
-
confidence: null
|
|
2074
|
-
},
|
|
2075
|
-
null,
|
|
2076
|
-
2
|
|
2077
|
-
) + "\n";
|
|
2078
|
-
function ensureContinuityScaffold(semPath) {
|
|
2079
|
-
try {
|
|
2080
|
-
const target = join8(dirname5(semPath), CONTINUITY_FILE_NAME);
|
|
2081
|
-
if (existsSync7(target)) return;
|
|
2082
|
-
writeFileSync6(target, CONTINUITY_SCAFFOLD);
|
|
2083
|
-
} catch {
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
function ignoresSem(content) {
|
|
2087
|
-
return content.split("\n").some((line) => {
|
|
2088
|
-
const t = line.trim();
|
|
2089
|
-
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}`;
|
|
2090
|
-
});
|
|
2091
|
-
}
|
|
2092
|
-
function inGitRepo(startDir) {
|
|
2093
|
-
let dir = startDir;
|
|
2094
|
-
for (; ; ) {
|
|
2095
|
-
if (existsSync7(join8(dir, ".git"))) return true;
|
|
2096
|
-
const parent = dirname5(dir);
|
|
2097
|
-
if (parent === dir) return false;
|
|
2098
|
-
dir = parent;
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
function resolveGitignoreTarget(startDir) {
|
|
2102
|
-
let dir = startDir;
|
|
2103
|
-
for (; ; ) {
|
|
2104
|
-
const gi = join8(dir, ".gitignore");
|
|
2105
|
-
if (existsSync7(gi)) return { path: gi, exists: true };
|
|
2106
|
-
const parent = dirname5(dir);
|
|
2107
|
-
if (existsSync7(join8(dir, ".git")) || parent === dir) {
|
|
2108
|
-
return { path: join8(startDir, ".gitignore"), exists: false };
|
|
2109
|
-
}
|
|
2110
|
-
dir = parent;
|
|
2111
|
-
}
|
|
2112
|
-
}
|
|
2113
|
-
function ensureSemIgnored(semPath) {
|
|
2114
|
-
try {
|
|
2115
|
-
const checkoutDir = dirname5(dirname5(semPath));
|
|
2116
|
-
if (!inGitRepo(checkoutDir)) return;
|
|
2117
|
-
const target = resolveGitignoreTarget(checkoutDir);
|
|
2118
|
-
if (target.exists) {
|
|
2119
|
-
const content = readFileSync5(target.path, "utf8");
|
|
2120
|
-
if (ignoresSem(content)) return;
|
|
2121
|
-
const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
2122
|
-
appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
|
|
2123
|
-
`);
|
|
2124
|
-
} else {
|
|
2125
|
-
writeFileSync6(target.path, `${STATE_DIR_IGNORE}
|
|
2126
|
-
`);
|
|
2127
|
-
}
|
|
2128
|
-
} catch {
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
|
|
2132
|
-
// 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";
|
|
2133
2818
|
async function readStdin() {
|
|
2134
2819
|
if (process.stdin.isTTY) return "";
|
|
2135
2820
|
const chunks = [];
|
|
@@ -2153,13 +2838,13 @@ function resolveLane(flagLane, cwd) {
|
|
|
2153
2838
|
if (!base) return void 0;
|
|
2154
2839
|
return applyWorktreeLaneSuffix(base, start);
|
|
2155
2840
|
}
|
|
2156
|
-
var INTENT_FILE =
|
|
2841
|
+
var INTENT_FILE = join10(".sechroom", "continuity.json");
|
|
2157
2842
|
function resolveIntentPath(start) {
|
|
2158
2843
|
let dir = start;
|
|
2159
2844
|
for (; ; ) {
|
|
2160
|
-
const candidate =
|
|
2161
|
-
if (
|
|
2162
|
-
const parent =
|
|
2845
|
+
const candidate = join10(dir, INTENT_FILE);
|
|
2846
|
+
if (existsSync9(candidate)) return candidate;
|
|
2847
|
+
const parent = dirname7(dir);
|
|
2163
2848
|
if (parent === dir) return void 0;
|
|
2164
2849
|
dir = parent;
|
|
2165
2850
|
}
|
|
@@ -2168,7 +2853,7 @@ function readIntent(start) {
|
|
|
2168
2853
|
const path = resolveIntentPath(start);
|
|
2169
2854
|
if (!path) return void 0;
|
|
2170
2855
|
try {
|
|
2171
|
-
return JSON.parse(
|
|
2856
|
+
return JSON.parse(readFileSync7(path, "utf8"));
|
|
2172
2857
|
} catch {
|
|
2173
2858
|
return void 0;
|
|
2174
2859
|
}
|
|
@@ -2210,14 +2895,14 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
|
|
|
2210
2895
|
}
|
|
2211
2896
|
function ledgerPath(start) {
|
|
2212
2897
|
const intent = resolveIntentPath(start);
|
|
2213
|
-
const dir = intent ?
|
|
2214
|
-
return
|
|
2898
|
+
const dir = intent ? dirname7(intent) : join10(start, ".sechroom");
|
|
2899
|
+
return join10(dir, ".checkpoint-state.json");
|
|
2215
2900
|
}
|
|
2216
2901
|
function readLedger(start) {
|
|
2217
2902
|
try {
|
|
2218
2903
|
const p = ledgerPath(start);
|
|
2219
|
-
if (!
|
|
2220
|
-
return JSON.parse(
|
|
2904
|
+
if (!existsSync9(p)) return {};
|
|
2905
|
+
return JSON.parse(readFileSync7(p, "utf8"));
|
|
2221
2906
|
} catch {
|
|
2222
2907
|
return {};
|
|
2223
2908
|
}
|
|
@@ -2264,13 +2949,13 @@ function recordPush(start, intent) {
|
|
|
2264
2949
|
} catch {
|
|
2265
2950
|
mtimeMs = void 0;
|
|
2266
2951
|
}
|
|
2267
|
-
|
|
2952
|
+
mkdirSync8(dirname7(p), { recursive: true });
|
|
2268
2953
|
const ledger = {
|
|
2269
2954
|
lastEpochMs: Date.now(),
|
|
2270
2955
|
lastMtimeMs: mtimeMs,
|
|
2271
2956
|
lastHash: intentHash(intent)
|
|
2272
2957
|
};
|
|
2273
|
-
|
|
2958
|
+
writeFileSync8(p, JSON.stringify(ledger) + "\n");
|
|
2274
2959
|
} catch {
|
|
2275
2960
|
}
|
|
2276
2961
|
}
|
|
@@ -2416,6 +3101,12 @@ Fail-soft: no lane / no auth / no-or-partial intent file / API error -> exit 0,
|
|
|
2416
3101
|
const surfaceResults = installHookSurfaces(["codex"], { dryRun, claudeDir: "", codexHome })[0].results;
|
|
2417
3102
|
process.stdout.write(`${HOOK_SURFACE_LABEL.codex}:
|
|
2418
3103
|
`);
|
|
3104
|
+
if (dryRun) {
|
|
3105
|
+
for (const [event, command] of Object.entries(hookCommandsForSurface("codex"))) {
|
|
3106
|
+
process.stdout.write(` ${event}: ${command}
|
|
3107
|
+
`);
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
2419
3110
|
for (const r of surfaceResults) {
|
|
2420
3111
|
results.push(r);
|
|
2421
3112
|
process.stdout.write(describe(r, dryRun) + "\n");
|
|
@@ -2517,10 +3208,10 @@ Examples:
|
|
|
2517
3208
|
const client = await makeClient(cfg);
|
|
2518
3209
|
return client.POST("/continuity/snapshots", { body });
|
|
2519
3210
|
});
|
|
2520
|
-
const path = resolveIntentPath(cwd) ??
|
|
3211
|
+
const path = resolveIntentPath(cwd) ?? join11(cwd, INTENT_FILE);
|
|
2521
3212
|
const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
|
|
2522
|
-
|
|
2523
|
-
|
|
3213
|
+
mkdirSync9(dirname8(path), { recursive: true });
|
|
3214
|
+
writeFileSync9(path, JSON.stringify(fileBody, null, 2) + "\n");
|
|
2524
3215
|
recordPush(cwd, merged);
|
|
2525
3216
|
if (json) {
|
|
2526
3217
|
emit({ snapshotId: data.snapshotId, lane, scope, file: path }, true);
|
|
@@ -2534,7 +3225,7 @@ Examples:
|
|
|
2534
3225
|
}
|
|
2535
3226
|
|
|
2536
3227
|
// src/commands/close.ts
|
|
2537
|
-
import { readFileSync as
|
|
3228
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
2538
3229
|
var VERDICTS = ["pass", "soft-fail", "plan-invalid", "blocked"];
|
|
2539
3230
|
function registerClose(program2) {
|
|
2540
3231
|
program2.command("close").description(
|
|
@@ -2551,6 +3242,9 @@ function registerClose(program2) {
|
|
|
2551
3242
|
).option(
|
|
2552
3243
|
"--no-status-flip",
|
|
2553
3244
|
"Skip the status flip on a BARE task. (Managed runs never flip here \u2014 the run engine reflects the verdict onto the task on resume.)"
|
|
3245
|
+
).option(
|
|
3246
|
+
"--to-version <n>",
|
|
3247
|
+
"Pin the Reference edge at this task version \u2014 the DISPATCHED version the executor worked against (D-continuity-2). Default: the task's current version (status flips are metadata edits that don't bump the version, so current == dispatched in the normal case; pass this when the task's content changed between dispatch and close)."
|
|
2554
3248
|
).option("--source <source>", "Source / lane stamp", "cli").addHelpText(
|
|
2555
3249
|
"after",
|
|
2556
3250
|
`
|
|
@@ -2572,7 +3266,7 @@ Examples:
|
|
|
2572
3266
|
);
|
|
2573
3267
|
let bodyText;
|
|
2574
3268
|
try {
|
|
2575
|
-
bodyText = opts.file ?
|
|
3269
|
+
bodyText = opts.file ? readFileSync8(opts.file, "utf8") : readFileSync8(0, "utf8");
|
|
2576
3270
|
} catch {
|
|
2577
3271
|
fail(
|
|
2578
3272
|
opts.file ? `could not read --file ${opts.file}` : "no closeout body \u2014 pass --file <path> or pipe it on stdin"
|
|
@@ -2584,6 +3278,7 @@ Examples:
|
|
|
2584
3278
|
);
|
|
2585
3279
|
const client = await makeClient(cfg);
|
|
2586
3280
|
let matchTags;
|
|
3281
|
+
let dispatchedVersion;
|
|
2587
3282
|
if (opts.decomposition) {
|
|
2588
3283
|
const run = await runApi(
|
|
2589
3284
|
"Reading run contract",
|
|
@@ -2601,6 +3296,8 @@ Examples:
|
|
|
2601
3296
|
`run ${opts.decomposition} is awaiting task ${run.awaitingTaskId}, not ${opts.task} \u2014 refusing to mis-key the resume.`
|
|
2602
3297
|
);
|
|
2603
3298
|
matchTags = await_.matchTags;
|
|
3299
|
+
if (typeof await_.dispatchedTaskVersion === "number")
|
|
3300
|
+
dispatchedVersion = await_.dispatchedTaskVersion;
|
|
2604
3301
|
} else {
|
|
2605
3302
|
matchTags = [`wlp-task:${opts.task}`];
|
|
2606
3303
|
}
|
|
@@ -2627,11 +3324,34 @@ Examples:
|
|
|
2627
3324
|
}
|
|
2628
3325
|
})
|
|
2629
3326
|
);
|
|
3327
|
+
let toVersion;
|
|
3328
|
+
if (opts.toVersion !== void 0) {
|
|
3329
|
+
toVersion = Number(opts.toVersion);
|
|
3330
|
+
if (!Number.isInteger(toVersion) || toVersion < 1)
|
|
3331
|
+
fail(
|
|
3332
|
+
`--to-version must be an integer >= 1 \u2014 got '${opts.toVersion}'`
|
|
3333
|
+
);
|
|
3334
|
+
} else if (dispatchedVersion !== void 0) {
|
|
3335
|
+
toVersion = dispatchedVersion;
|
|
3336
|
+
} else {
|
|
3337
|
+
const task = await runApi(
|
|
3338
|
+
"Reading task version",
|
|
3339
|
+
async () => client.GET("/memories/{memoryId}", {
|
|
3340
|
+
params: { path: { memoryId: opts.task } }
|
|
3341
|
+
})
|
|
3342
|
+
);
|
|
3343
|
+
const v = task?.item?.currentVersion ?? task?.currentVersion;
|
|
3344
|
+
if (typeof v !== "number" || v < 1)
|
|
3345
|
+
fail(
|
|
3346
|
+
`could not read task ${opts.task} version to pin the Reference edge \u2014 pass --to-version <n>`
|
|
3347
|
+
);
|
|
3348
|
+
toVersion = v;
|
|
3349
|
+
}
|
|
2630
3350
|
const edge = await runApi(
|
|
2631
3351
|
"Wiring Reference edge",
|
|
2632
3352
|
async () => client.POST("/memories/{memoryId}/relationships", {
|
|
2633
3353
|
params: { path: { memoryId: closeout.id } },
|
|
2634
|
-
body: { toMemoryId: opts.task, type: "Reference" }
|
|
3354
|
+
body: { toMemoryId: opts.task, type: "Reference", toVersion }
|
|
2635
3355
|
})
|
|
2636
3356
|
);
|
|
2637
3357
|
let taskStatus;
|
|
@@ -2847,16 +3567,17 @@ function registerDecomposition(program2) {
|
|
|
2847
3567
|
Examples:
|
|
2848
3568
|
$ sechroom decomposition decompose mem_XXXX
|
|
2849
3569
|
$ sechroom decomposition execute sug_XXXX
|
|
3570
|
+
$ sechroom decomposition publish-run sug_XXXX
|
|
2850
3571
|
$ sechroom decomposition accept sug_XXXX
|
|
2851
3572
|
$ sechroom decomposition reject sug_XXXX --reason "wrong shape"`
|
|
2852
3573
|
);
|
|
2853
3574
|
decomposition.command("decompose <briefId>").description(
|
|
2854
|
-
"Decompose a
|
|
3575
|
+
"Decompose a work brief into a candidate Task graph (POST /work-briefs/{id}/decompose)"
|
|
2855
3576
|
).action(async (briefId, _opts, cmd) => {
|
|
2856
3577
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2857
3578
|
const data = await runApi("Queueing decomposition", async () => {
|
|
2858
3579
|
const client = await makeClient(cfg);
|
|
2859
|
-
return client.POST("/
|
|
3580
|
+
return client.POST("/work-briefs/{id}/decompose", {
|
|
2860
3581
|
params: { path: { id: briefId } },
|
|
2861
3582
|
body: { id: briefId }
|
|
2862
3583
|
});
|
|
@@ -2884,6 +3605,23 @@ Examples:
|
|
|
2884
3605
|
cmd.optsWithGlobals().json
|
|
2885
3606
|
);
|
|
2886
3607
|
});
|
|
3608
|
+
decomposition.command("publish-run <decompositionId>").description(
|
|
3609
|
+
"Publish an accepted decomposition's context pack on demand (POST /decompositions/{id}/publish-run)"
|
|
3610
|
+
).action(async (decompositionId, _opts, cmd) => {
|
|
3611
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3612
|
+
const data = await runApi("Publishing context pack", async () => {
|
|
3613
|
+
const client = await makeClient(cfg);
|
|
3614
|
+
return client.POST("/decompositions/{id}/publish-run", {
|
|
3615
|
+
params: { path: { id: decompositionId } },
|
|
3616
|
+
body: {}
|
|
3617
|
+
});
|
|
3618
|
+
});
|
|
3619
|
+
emitAction(
|
|
3620
|
+
`published ${style.bold(decompositionId)} \u2192 run ${style.bold(data.runRecordId)} (${data.outcome})`,
|
|
3621
|
+
data,
|
|
3622
|
+
cmd.optsWithGlobals().json
|
|
3623
|
+
);
|
|
3624
|
+
});
|
|
2887
3625
|
decomposition.command("accept <decompositionId>").description(
|
|
2888
3626
|
"Accept a Pending decomposition \u2014 promote + ratify its Tasks (POST /decompositions/{id}/accept)"
|
|
2889
3627
|
).action(async (decompositionId, _opts, cmd) => {
|
|
@@ -3431,8 +4169,8 @@ Examples:
|
|
|
3431
4169
|
|
|
3432
4170
|
// src/setup/apply.ts
|
|
3433
4171
|
import { createHash as createHash3 } from "crypto";
|
|
3434
|
-
import { mkdirSync as
|
|
3435
|
-
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";
|
|
3436
4174
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
3437
4175
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
3438
4176
|
function normalizeBody(s) {
|
|
@@ -3485,22 +4223,22 @@ function parseManagedBlock(content, block) {
|
|
|
3485
4223
|
return null;
|
|
3486
4224
|
}
|
|
3487
4225
|
function ensureDir2(path) {
|
|
3488
|
-
|
|
4226
|
+
mkdirSync10(dirname9(path), { recursive: true });
|
|
3489
4227
|
}
|
|
3490
4228
|
function readOr(path, fallback) {
|
|
3491
4229
|
try {
|
|
3492
|
-
return
|
|
4230
|
+
return readFileSync9(path, "utf8");
|
|
3493
4231
|
} catch {
|
|
3494
4232
|
return fallback;
|
|
3495
4233
|
}
|
|
3496
4234
|
}
|
|
3497
4235
|
function mergeMcpJson(path, snippet, dryRun) {
|
|
3498
4236
|
const incoming = JSON.parse(snippet);
|
|
3499
|
-
const existed =
|
|
4237
|
+
const existed = existsSync10(path);
|
|
3500
4238
|
let current = {};
|
|
3501
4239
|
if (existed) {
|
|
3502
4240
|
try {
|
|
3503
|
-
current = JSON.parse(
|
|
4241
|
+
current = JSON.parse(readFileSync9(path, "utf8"));
|
|
3504
4242
|
} catch {
|
|
3505
4243
|
return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
|
|
3506
4244
|
}
|
|
@@ -3508,26 +4246,26 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
3508
4246
|
current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
|
|
3509
4247
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3510
4248
|
ensureDir2(path);
|
|
3511
|
-
|
|
4249
|
+
writeFileSync10(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
3512
4250
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3513
4251
|
}
|
|
3514
4252
|
function mergeCodexToml(path, snippet, dryRun) {
|
|
3515
|
-
const existed =
|
|
4253
|
+
const existed = existsSync10(path);
|
|
3516
4254
|
let body = readOr(path, "");
|
|
3517
4255
|
body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
|
|
3518
4256
|
const trimmed = body.trim();
|
|
3519
4257
|
const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
|
|
3520
4258
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3521
4259
|
ensureDir2(path);
|
|
3522
|
-
|
|
4260
|
+
writeFileSync10(path, next, { mode: 384 });
|
|
3523
4261
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3524
4262
|
}
|
|
3525
4263
|
function writeInstructionBlock(path, write, dryRun) {
|
|
3526
|
-
const existed =
|
|
4264
|
+
const existed = existsSync10(path);
|
|
3527
4265
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
3528
4266
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
3529
4267
|
ensureDir2(path);
|
|
3530
|
-
|
|
4268
|
+
writeFileSync10(path, next);
|
|
3531
4269
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
3532
4270
|
}
|
|
3533
4271
|
function computeBlockFile(current, write) {
|
|
@@ -3568,7 +4306,7 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
3568
4306
|
const next = computeBlockFile(current, write);
|
|
3569
4307
|
if (!dryRun) {
|
|
3570
4308
|
ensureDir2(proposedPath);
|
|
3571
|
-
|
|
4309
|
+
writeFileSync10(proposedPath, next);
|
|
3572
4310
|
}
|
|
3573
4311
|
return {
|
|
3574
4312
|
kind: "instruction",
|
|
@@ -3698,8 +4436,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
3698
4436
|
}
|
|
3699
4437
|
|
|
3700
4438
|
// src/setup/skills-offer.ts
|
|
3701
|
-
import { mkdirSync as
|
|
3702
|
-
import { join as
|
|
4439
|
+
import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
|
|
4440
|
+
import { join as join12 } from "path";
|
|
3703
4441
|
|
|
3704
4442
|
// src/setup/lane-pin.ts
|
|
3705
4443
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -3815,8 +4553,8 @@ Found ${summary} available to you for ${surface}.
|
|
|
3815
4553
|
if (skills.length > 0) {
|
|
3816
4554
|
const written = [];
|
|
3817
4555
|
for (const s of skills) {
|
|
3818
|
-
|
|
3819
|
-
|
|
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");
|
|
3820
4558
|
written.push(s.name);
|
|
3821
4559
|
}
|
|
3822
4560
|
recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -3824,11 +4562,11 @@ Found ${summary} available to you for ${surface}.
|
|
|
3824
4562
|
`);
|
|
3825
4563
|
}
|
|
3826
4564
|
if (agents.length > 0) {
|
|
3827
|
-
|
|
4565
|
+
mkdirSync11(aDir, { recursive: true });
|
|
3828
4566
|
const written = [];
|
|
3829
4567
|
for (const a of agents) {
|
|
3830
4568
|
const file = `${a.name}.md`;
|
|
3831
|
-
|
|
4569
|
+
writeFileSync11(join12(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
3832
4570
|
written.push(file);
|
|
3833
4571
|
}
|
|
3834
4572
|
recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -4211,13 +4949,13 @@ Wired to namespace '${slug}'. Restart your AI client (or reload MCP) to pick it
|
|
|
4211
4949
|
}
|
|
4212
4950
|
|
|
4213
4951
|
// src/commands/onboard.ts
|
|
4214
|
-
import { existsSync as
|
|
4215
|
-
import { basename as basename2, join as
|
|
4952
|
+
import { existsSync as existsSync12 } from "fs";
|
|
4953
|
+
import { basename as basename2, join as join14 } from "path";
|
|
4216
4954
|
|
|
4217
4955
|
// src/commands/fanout.ts
|
|
4218
4956
|
import { spawnSync } from "child_process";
|
|
4219
|
-
import { existsSync as
|
|
4220
|
-
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";
|
|
4221
4959
|
var ICON = {
|
|
4222
4960
|
refresh: "\u21BB",
|
|
4223
4961
|
bind: "+",
|
|
@@ -4237,21 +4975,21 @@ function discoverChildren(root) {
|
|
|
4237
4975
|
const out = [];
|
|
4238
4976
|
for (const name of names.sort()) {
|
|
4239
4977
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
4240
|
-
const dir =
|
|
4978
|
+
const dir = join13(root, name);
|
|
4241
4979
|
try {
|
|
4242
4980
|
if (!statSync3(dir).isDirectory()) continue;
|
|
4243
4981
|
} catch {
|
|
4244
4982
|
continue;
|
|
4245
4983
|
}
|
|
4246
|
-
if (
|
|
4984
|
+
if (existsSync11(join13(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
4247
4985
|
}
|
|
4248
4986
|
return out;
|
|
4249
4987
|
}
|
|
4250
4988
|
function readManifest(path) {
|
|
4251
|
-
if (!
|
|
4989
|
+
if (!existsSync11(path)) return null;
|
|
4252
4990
|
let parsed;
|
|
4253
4991
|
try {
|
|
4254
|
-
parsed = JSON.parse(
|
|
4992
|
+
parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
4255
4993
|
} catch (err2) {
|
|
4256
4994
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
4257
4995
|
}
|
|
@@ -4617,10 +5355,10 @@ async function chooseScope(scopeFlag, yes) {
|
|
|
4617
5355
|
}
|
|
4618
5356
|
async function planRecurseChild(entry, root, client, opts) {
|
|
4619
5357
|
const dir = resolveChildDir(entry.path, root);
|
|
4620
|
-
if (!
|
|
5358
|
+
if (!existsSync12(dir)) {
|
|
4621
5359
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
4622
5360
|
}
|
|
4623
|
-
if (
|
|
5361
|
+
if (existsSync12(join14(dir, ".sechroom.json"))) {
|
|
4624
5362
|
return {
|
|
4625
5363
|
label: entry.path,
|
|
4626
5364
|
dir,
|
|
@@ -4693,7 +5431,7 @@ This fan-out will pin the same lane in every repo:
|
|
|
4693
5431
|
async function runRecurse(cfg, g, opts) {
|
|
4694
5432
|
const { yes, dryRun, json } = opts;
|
|
4695
5433
|
const root = process.cwd();
|
|
4696
|
-
const manifestPath =
|
|
5434
|
+
const manifestPath = join14(root, ".sechroom", "repos.json");
|
|
4697
5435
|
const fromManifest = readManifest(manifestPath);
|
|
4698
5436
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
4699
5437
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
@@ -5065,24 +5803,45 @@ Examples:
|
|
|
5065
5803
|
$ sechroom relationship suggestions --status Pending --memory mem_XXXX
|
|
5066
5804
|
$ sechroom relationship suggestion accept rsg_XXXX`
|
|
5067
5805
|
);
|
|
5068
|
-
relationship.command("create <fromMemoryId> <toMemoryId>").description("Create a relationship (POST /memories/{memoryId}/relationships)").option("--type <type>", "Relationship type (Reference, Related, Parent, Child, Follows, \u2026)", "Reference").
|
|
5806
|
+
relationship.command("create <fromMemoryId> <toMemoryId>").description("Create a relationship (POST /memories/{memoryId}/relationships)").option("--type <type>", "Relationship type (Reference, Related, Parent, Child, Follows, \u2026)", "Reference").option(
|
|
5807
|
+
"--to-version <number>",
|
|
5808
|
+
"Version of the target memory to pin the edge to (defaults to the target's current version)"
|
|
5809
|
+
).action(async (fromMemoryId, toMemoryId, opts, cmd) => {
|
|
5069
5810
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
5070
|
-
const
|
|
5071
|
-
|
|
5072
|
-
|
|
5811
|
+
const client = await makeClient(cfg);
|
|
5812
|
+
let toVersion;
|
|
5813
|
+
if (opts.toVersion !== void 0) {
|
|
5814
|
+
toVersion = Number(opts.toVersion);
|
|
5815
|
+
if (!Number.isInteger(toVersion) || toVersion < 1) {
|
|
5816
|
+
fail(`--to-version must be a positive integer (got '${opts.toVersion}').`);
|
|
5817
|
+
}
|
|
5818
|
+
} else {
|
|
5819
|
+
const target = await runApi(
|
|
5820
|
+
"Resolving target version",
|
|
5821
|
+
async () => client.GET("/memories/{memoryId}", { params: { path: { memoryId: toMemoryId } } })
|
|
5822
|
+
);
|
|
5823
|
+
if (typeof target.item?.currentVersion !== "number") {
|
|
5824
|
+
fail(`Could not resolve the current version of ${toMemoryId}; pass --to-version explicitly.`);
|
|
5825
|
+
}
|
|
5826
|
+
toVersion = target.item.currentVersion;
|
|
5827
|
+
}
|
|
5828
|
+
const data = await runApi(
|
|
5829
|
+
"Creating relationship",
|
|
5830
|
+
async () => client.POST("/memories/{memoryId}/relationships", {
|
|
5073
5831
|
params: { path: { memoryId: fromMemoryId } },
|
|
5074
5832
|
body: {
|
|
5075
5833
|
toMemoryId,
|
|
5076
|
-
type: opts.type
|
|
5834
|
+
type: opts.type,
|
|
5835
|
+
toVersion
|
|
5077
5836
|
}
|
|
5078
|
-
})
|
|
5079
|
-
|
|
5837
|
+
})
|
|
5838
|
+
);
|
|
5080
5839
|
const inversePart = data.inverseId ? ` ${style.dim(`(inverse ${data.inverseId})`)}` : "";
|
|
5081
5840
|
const view = resolveViewUrl(cfg.baseUrl, data.url);
|
|
5082
5841
|
const urlPart = view ? ` ${style.dim("\u2192")} ${view}` : "";
|
|
5083
5842
|
emitAction(
|
|
5084
|
-
`created relationship ${style.bold(data.id)} ${style.dim(`${fromMemoryId} \u2192 ${toMemoryId}`)}${inversePart}${urlPart}`,
|
|
5085
|
-
data,
|
|
5843
|
+
`created relationship ${style.bold(data.id)} ${style.dim(`${fromMemoryId} \u2192 ${toMemoryId} @v${toVersion}`)}${inversePart}${urlPart}`,
|
|
5844
|
+
{ ...data, toVersion },
|
|
5086
5845
|
cmd.optsWithGlobals().json
|
|
5087
5846
|
);
|
|
5088
5847
|
});
|
|
@@ -5205,23 +5964,23 @@ Examples:
|
|
|
5205
5964
|
|
|
5206
5965
|
// src/commands/reset.ts
|
|
5207
5966
|
import { homedir as homedir4 } from "os";
|
|
5208
|
-
import { join as
|
|
5209
|
-
import { existsSync as
|
|
5967
|
+
import { join as join15 } from "path";
|
|
5968
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11, rmSync as rmSync3 } from "fs";
|
|
5210
5969
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
5211
|
-
var localSkillsDir = () =>
|
|
5212
|
-
var globalSkillsDir = () =>
|
|
5213
|
-
var localAgentsDir = () =>
|
|
5214
|
-
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");
|
|
5215
5974
|
function removeMaterialisedSkills(dir) {
|
|
5216
5975
|
const removed = [];
|
|
5217
|
-
const lockPath =
|
|
5218
|
-
if (!
|
|
5976
|
+
const lockPath = join15(dir, SKILLS_LOCK2);
|
|
5977
|
+
if (!existsSync13(lockPath)) return removed;
|
|
5219
5978
|
try {
|
|
5220
|
-
const lock = JSON.parse(
|
|
5979
|
+
const lock = JSON.parse(readFileSync11(lockPath, "utf8"));
|
|
5221
5980
|
for (const entry of Object.values(lock)) {
|
|
5222
5981
|
for (const name of entry.skills ?? []) {
|
|
5223
|
-
const p =
|
|
5224
|
-
if (
|
|
5982
|
+
const p = join15(dir, name);
|
|
5983
|
+
if (existsSync13(p)) {
|
|
5225
5984
|
rmSync3(p, { recursive: true, force: true });
|
|
5226
5985
|
removed.push(p);
|
|
5227
5986
|
}
|
|
@@ -5266,18 +6025,18 @@ function registerReset(program2) {
|
|
|
5266
6025
|
}
|
|
5267
6026
|
}
|
|
5268
6027
|
const removed = [];
|
|
5269
|
-
const stateDir =
|
|
5270
|
-
if (
|
|
6028
|
+
const stateDir = join15(process.cwd(), ".sechroom");
|
|
6029
|
+
if (existsSync13(stateDir)) {
|
|
5271
6030
|
rmSync3(stateDir, { recursive: true, force: true });
|
|
5272
6031
|
removed.push(stateDir);
|
|
5273
6032
|
}
|
|
5274
|
-
const legacyCfg =
|
|
5275
|
-
if (
|
|
6033
|
+
const legacyCfg = join15(process.cwd(), ".sechroom.json");
|
|
6034
|
+
if (existsSync13(legacyCfg)) {
|
|
5276
6035
|
rmSync3(legacyCfg, { force: true });
|
|
5277
6036
|
removed.push(legacyCfg);
|
|
5278
6037
|
}
|
|
5279
|
-
const legacySem =
|
|
5280
|
-
if (
|
|
6038
|
+
const legacySem = join15(process.cwd(), ".sem");
|
|
6039
|
+
if (existsSync13(legacySem)) {
|
|
5281
6040
|
rmSync3(legacySem, { force: true });
|
|
5282
6041
|
removed.push(legacySem);
|
|
5283
6042
|
}
|
|
@@ -5303,8 +6062,8 @@ function registerReset(program2) {
|
|
|
5303
6062
|
}
|
|
5304
6063
|
|
|
5305
6064
|
// src/commands/skills.ts
|
|
5306
|
-
import { existsSync as
|
|
5307
|
-
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";
|
|
5308
6067
|
function filenameFromDisposition(header) {
|
|
5309
6068
|
if (!header) return void 0;
|
|
5310
6069
|
const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
|
|
@@ -5312,11 +6071,11 @@ function filenameFromDisposition(header) {
|
|
|
5312
6071
|
}
|
|
5313
6072
|
function resolveOutputPath(output, serverFilename) {
|
|
5314
6073
|
const filename = serverFilename || "skills.zip";
|
|
5315
|
-
if (!output) return
|
|
5316
|
-
const looksLikeDir = output.endsWith("/") ||
|
|
6074
|
+
if (!output) return join16(process.cwd(), filename);
|
|
6075
|
+
const looksLikeDir = output.endsWith("/") || existsSync14(output) && statSync4(output).isDirectory();
|
|
5317
6076
|
if (looksLikeDir) {
|
|
5318
|
-
|
|
5319
|
-
return
|
|
6077
|
+
mkdirSync12(output, { recursive: true });
|
|
6078
|
+
return join16(output, filename);
|
|
5320
6079
|
}
|
|
5321
6080
|
return output;
|
|
5322
6081
|
}
|
|
@@ -5347,7 +6106,7 @@ async function downloadZip(label, call, output) {
|
|
|
5347
6106
|
const buf = Buffer.from(res.data);
|
|
5348
6107
|
const filename = filenameFromDisposition(res.response.headers.get("content-disposition")) ?? "skills.zip";
|
|
5349
6108
|
const path = resolveOutputPath(output, filename);
|
|
5350
|
-
|
|
6109
|
+
writeFileSync12(path, buf);
|
|
5351
6110
|
return { path, bytes: buf.length, filename };
|
|
5352
6111
|
}
|
|
5353
6112
|
function registerSkills(program2) {
|
|
@@ -5356,7 +6115,9 @@ function registerSkills(program2) {
|
|
|
5356
6115
|
"after",
|
|
5357
6116
|
`
|
|
5358
6117
|
Examples:
|
|
5359
|
-
$ 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
|
|
5360
6121
|
$ sechroom skills install --scope project write them to ./.claude/skills instead
|
|
5361
6122
|
$ sechroom skills list what's materialised on disk
|
|
5362
6123
|
$ sechroom skills clean remove the materialised skill files
|
|
@@ -5365,11 +6126,15 @@ Examples:
|
|
|
5365
6126
|
$ sechroom skills package --from-source --workspace wsp_abc -o ./dist zip a draft from source
|
|
5366
6127
|
$ sechroom skills set-lane --code-lane claude-code-chris --design-lane claude-design-chris
|
|
5367
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
|
+
|
|
5368
6133
|
`
|
|
5369
6134
|
);
|
|
5370
|
-
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));
|
|
5371
|
-
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));
|
|
5372
|
-
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));
|
|
5373
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) => {
|
|
5374
6139
|
const json = Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json);
|
|
5375
6140
|
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
@@ -5515,12 +6280,12 @@ Examples:
|
|
|
5515
6280
|
}
|
|
5516
6281
|
|
|
5517
6282
|
// src/commands/sweep.ts
|
|
5518
|
-
import { existsSync as
|
|
5519
|
-
import { dirname as
|
|
5520
|
-
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");
|
|
5521
6286
|
function planEntry(entry, root) {
|
|
5522
6287
|
const dir = resolveChildDir(entry.path, root);
|
|
5523
|
-
if (!
|
|
6288
|
+
if (!existsSync15(dir)) {
|
|
5524
6289
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
5525
6290
|
}
|
|
5526
6291
|
if (committedBindingPath(dir)) {
|
|
@@ -5596,7 +6361,7 @@ Examples:
|
|
|
5596
6361
|
`);
|
|
5597
6362
|
return;
|
|
5598
6363
|
}
|
|
5599
|
-
const root =
|
|
6364
|
+
const root = dirname10(dirname10(manifestPath));
|
|
5600
6365
|
const plans = repos.map((entry) => planEntry(entry, root));
|
|
5601
6366
|
if (!json) {
|
|
5602
6367
|
process.stderr.write(
|
|
@@ -5615,13 +6380,13 @@ Examples:
|
|
|
5615
6380
|
|
|
5616
6381
|
// src/commands/telemetry.ts
|
|
5617
6382
|
import {
|
|
5618
|
-
existsSync as
|
|
5619
|
-
mkdirSync as
|
|
5620
|
-
readFileSync as
|
|
6383
|
+
existsSync as existsSync16,
|
|
6384
|
+
mkdirSync as mkdirSync13,
|
|
6385
|
+
readFileSync as readFileSync12,
|
|
5621
6386
|
rmSync as rmSync4,
|
|
5622
|
-
writeFileSync as
|
|
6387
|
+
writeFileSync as writeFileSync13
|
|
5623
6388
|
} from "fs";
|
|
5624
|
-
import { dirname as
|
|
6389
|
+
import { dirname as dirname11, join as join18 } from "path";
|
|
5625
6390
|
function registerTelemetry(program2) {
|
|
5626
6391
|
const telemetry = program2.command("telemetry").description(
|
|
5627
6392
|
"Emit WLP run telemetry (an executor leg's progress events) into a decomposition run"
|
|
@@ -5691,14 +6456,14 @@ function registerTelemetry(program2) {
|
|
|
5691
6456
|
"Decomposition id this session executes"
|
|
5692
6457
|
).requiredOption("--task <id>", "Task id this session executes").action((opts, cmd) => {
|
|
5693
6458
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
5694
|
-
const dir =
|
|
5695
|
-
|
|
5696
|
-
const path =
|
|
6459
|
+
const dir = join18(process.cwd(), ".sechroom");
|
|
6460
|
+
mkdirSync13(dir, { recursive: true });
|
|
6461
|
+
const path = join18(dir, BINDING_FILE);
|
|
5697
6462
|
const binding = {
|
|
5698
6463
|
decompositionId: opts.decomposition,
|
|
5699
6464
|
taskId: opts.task
|
|
5700
6465
|
};
|
|
5701
|
-
|
|
6466
|
+
writeFileSync13(path, JSON.stringify(binding, null, 2) + "\n");
|
|
5702
6467
|
ensureStateDirIgnored(process.cwd());
|
|
5703
6468
|
if (json) {
|
|
5704
6469
|
emit({ bound: true, ...binding, path }, true);
|
|
@@ -5713,8 +6478,8 @@ function registerTelemetry(program2) {
|
|
|
5713
6478
|
});
|
|
5714
6479
|
telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
|
|
5715
6480
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
5716
|
-
const path =
|
|
5717
|
-
const existed =
|
|
6481
|
+
const path = join18(process.cwd(), ".sechroom", BINDING_FILE);
|
|
6482
|
+
const existed = existsSync16(path);
|
|
5718
6483
|
if (existed) rmSync4(path);
|
|
5719
6484
|
if (json) emit({ unbound: existed, path }, true);
|
|
5720
6485
|
else
|
|
@@ -5723,7 +6488,7 @@ function registerTelemetry(program2) {
|
|
|
5723
6488
|
);
|
|
5724
6489
|
});
|
|
5725
6490
|
telemetry.command("hook").description(
|
|
5726
|
-
"Per-turn telemetry self-report for
|
|
6491
|
+
"Per-turn telemetry self-report for Claude Code hooks \u2014 Stop/SubagentStop \u2192 parsed + terminal, Notification/PermissionDenied \u2192 approval (reads stdin; no-op unless bound). Fail-soft."
|
|
5727
6492
|
).action(async (_opts, cmd) => {
|
|
5728
6493
|
try {
|
|
5729
6494
|
const raw = await readStdin2();
|
|
@@ -5732,21 +6497,10 @@ function registerTelemetry(program2) {
|
|
|
5732
6497
|
const binding = findBinding(cwd);
|
|
5733
6498
|
if (!binding) return process.exit(0);
|
|
5734
6499
|
const usage = input.transcript_path ? parseTranscript(input.transcript_path) : null;
|
|
5735
|
-
|
|
6500
|
+
const events = buildHookEvents(input, usage, binding.taskId);
|
|
6501
|
+
if (events.length === 0) return process.exit(0);
|
|
5736
6502
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
5737
|
-
await postTelemetry(cfg, binding.decompositionId,
|
|
5738
|
-
{
|
|
5739
|
-
taskId: binding.taskId,
|
|
5740
|
-
kind: "Parsed",
|
|
5741
|
-
tokensIn: usage.tokensIn,
|
|
5742
|
-
tokensOut: usage.tokensOut,
|
|
5743
|
-
contextUsed: usage.contextUsed,
|
|
5744
|
-
contextWindow: usage.contextWindow,
|
|
5745
|
-
text: null,
|
|
5746
|
-
approvalState: null,
|
|
5747
|
-
verdict: null
|
|
5748
|
-
}
|
|
5749
|
-
]);
|
|
6503
|
+
await postTelemetry(cfg, binding.decompositionId, events);
|
|
5750
6504
|
return process.exit(0);
|
|
5751
6505
|
} catch {
|
|
5752
6506
|
return process.exit(0);
|
|
@@ -5774,7 +6528,12 @@ function registerTelemetry(program2) {
|
|
|
5774
6528
|
scope,
|
|
5775
6529
|
cwd
|
|
5776
6530
|
});
|
|
5777
|
-
const commands = {
|
|
6531
|
+
const commands = {
|
|
6532
|
+
Stop: "sechroom telemetry hook",
|
|
6533
|
+
SubagentStop: "sechroom telemetry hook",
|
|
6534
|
+
Notification: "sechroom telemetry hook",
|
|
6535
|
+
PermissionDenied: "sechroom telemetry hook"
|
|
6536
|
+
};
|
|
5778
6537
|
try {
|
|
5779
6538
|
const multi = targets.length > 1;
|
|
5780
6539
|
const results = targets.map((t) => {
|
|
@@ -5828,11 +6587,11 @@ async function postTelemetry(cfg, decompositionId, events) {
|
|
|
5828
6587
|
function findBinding(start) {
|
|
5829
6588
|
let dir = start;
|
|
5830
6589
|
for (; ; ) {
|
|
5831
|
-
const path =
|
|
5832
|
-
if (
|
|
6590
|
+
const path = join18(dir, ".sechroom", BINDING_FILE);
|
|
6591
|
+
if (existsSync16(path)) {
|
|
5833
6592
|
try {
|
|
5834
6593
|
const b = JSON.parse(
|
|
5835
|
-
|
|
6594
|
+
readFileSync12(path, "utf8")
|
|
5836
6595
|
);
|
|
5837
6596
|
if (b.decompositionId && b.taskId)
|
|
5838
6597
|
return { decompositionId: b.decompositionId, taskId: b.taskId };
|
|
@@ -5840,18 +6599,18 @@ function findBinding(start) {
|
|
|
5840
6599
|
}
|
|
5841
6600
|
return null;
|
|
5842
6601
|
}
|
|
5843
|
-
const parent =
|
|
6602
|
+
const parent = dirname11(dir);
|
|
5844
6603
|
if (parent === dir) return null;
|
|
5845
6604
|
dir = parent;
|
|
5846
6605
|
}
|
|
5847
6606
|
}
|
|
5848
6607
|
function parseTranscript(path) {
|
|
5849
|
-
if (!
|
|
6608
|
+
if (!existsSync16(path)) return null;
|
|
5850
6609
|
let tokensIn = 0;
|
|
5851
6610
|
let tokensOut = 0;
|
|
5852
6611
|
let contextUsed = 0;
|
|
5853
6612
|
let model = "";
|
|
5854
|
-
for (const line of
|
|
6613
|
+
for (const line of readFileSync12(path, "utf8").split("\n")) {
|
|
5855
6614
|
if (!line.trim()) continue;
|
|
5856
6615
|
let obj;
|
|
5857
6616
|
try {
|
|
@@ -5875,6 +6634,55 @@ function windowFor(model, contextUsed = 0) {
|
|
|
5875
6634
|
if (m.includes("[1m]") || m.includes("-1m")) return 1e6;
|
|
5876
6635
|
return contextUsed > 2e5 ? 1e6 : 2e5;
|
|
5877
6636
|
}
|
|
6637
|
+
function buildHookEvents(input, usage, taskId) {
|
|
6638
|
+
const events = [];
|
|
6639
|
+
const base = (kind, over) => ({
|
|
6640
|
+
taskId,
|
|
6641
|
+
kind,
|
|
6642
|
+
tokensIn: null,
|
|
6643
|
+
tokensOut: null,
|
|
6644
|
+
contextUsed: null,
|
|
6645
|
+
contextWindow: null,
|
|
6646
|
+
text: null,
|
|
6647
|
+
approvalState: null,
|
|
6648
|
+
verdict: null,
|
|
6649
|
+
...over
|
|
6650
|
+
});
|
|
6651
|
+
if (usage) {
|
|
6652
|
+
events.push(
|
|
6653
|
+
base("Parsed", {
|
|
6654
|
+
tokensIn: usage.tokensIn,
|
|
6655
|
+
tokensOut: usage.tokensOut,
|
|
6656
|
+
contextUsed: usage.contextUsed,
|
|
6657
|
+
contextWindow: usage.contextWindow
|
|
6658
|
+
})
|
|
6659
|
+
);
|
|
6660
|
+
}
|
|
6661
|
+
switch (input.hook_event_name) {
|
|
6662
|
+
case "PermissionDenied":
|
|
6663
|
+
events.push(
|
|
6664
|
+
base("Approval", {
|
|
6665
|
+
approvalState: "denied",
|
|
6666
|
+
text: input.tool_name ?? input.message ?? null
|
|
6667
|
+
})
|
|
6668
|
+
);
|
|
6669
|
+
break;
|
|
6670
|
+
case "Notification":
|
|
6671
|
+
if (isPermissionNotification(input))
|
|
6672
|
+
events.push(base("Approval", { text: input.message ?? null }));
|
|
6673
|
+
break;
|
|
6674
|
+
case "Stop":
|
|
6675
|
+
case "SubagentStop":
|
|
6676
|
+
events.push(base("Terminal", { text: input.last_assistant_message ?? null }));
|
|
6677
|
+
break;
|
|
6678
|
+
}
|
|
6679
|
+
return events;
|
|
6680
|
+
}
|
|
6681
|
+
function isPermissionNotification(input) {
|
|
6682
|
+
const t = (input.notification_type ?? input.type ?? "").toLowerCase();
|
|
6683
|
+
if (t) return t.includes("permission");
|
|
6684
|
+
return (input.message ?? "").toLowerCase().includes("permission");
|
|
6685
|
+
}
|
|
5878
6686
|
async function readStdin2() {
|
|
5879
6687
|
if (process.stdin.isTTY) return "";
|
|
5880
6688
|
const chunks = [];
|
|
@@ -6109,7 +6917,7 @@ Examples:
|
|
|
6109
6917
|
function resolveVersion() {
|
|
6110
6918
|
try {
|
|
6111
6919
|
const pkg = JSON.parse(
|
|
6112
|
-
|
|
6920
|
+
readFileSync13(new URL("../package.json", import.meta.url), "utf8")
|
|
6113
6921
|
);
|
|
6114
6922
|
return pkg.version ?? "0.0.0";
|
|
6115
6923
|
} catch {
|
|
@@ -6268,6 +7076,7 @@ registerRelationships(program);
|
|
|
6268
7076
|
registerWorkspace(program);
|
|
6269
7077
|
registerProject(program);
|
|
6270
7078
|
registerDecomposition(program);
|
|
7079
|
+
registerExecutor(program);
|
|
6271
7080
|
registerClose(program);
|
|
6272
7081
|
registerFiling(program);
|
|
6273
7082
|
registerContinuity(program);
|