@sechroom/cli 2026.7.7-rc.56782977 → 2026.7.8-rc.4b6e8ecc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +502 -93
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/auth.ts
|
|
@@ -657,9 +657,14 @@ async function runApi(label, fn) {
|
|
|
657
657
|
s.succeed();
|
|
658
658
|
return res.data;
|
|
659
659
|
}
|
|
660
|
-
function
|
|
660
|
+
function formatFailureMessage(error) {
|
|
661
661
|
let msg;
|
|
662
|
-
if (
|
|
662
|
+
if (error instanceof Error) {
|
|
663
|
+
msg = error.message || error.name;
|
|
664
|
+
if (error.cause instanceof Error && error.cause.message) {
|
|
665
|
+
msg += `: ${error.cause.message}`;
|
|
666
|
+
}
|
|
667
|
+
} else if (typeof error === "object" && error !== null && "title" in error) {
|
|
663
668
|
const problem = error;
|
|
664
669
|
msg = String(problem.title);
|
|
665
670
|
if (problem.errors && typeof problem.errors === "object") {
|
|
@@ -674,6 +679,10 @@ ${detail.join("\n")}`;
|
|
|
674
679
|
} else {
|
|
675
680
|
msg = String(error);
|
|
676
681
|
}
|
|
682
|
+
return msg;
|
|
683
|
+
}
|
|
684
|
+
function fail(error) {
|
|
685
|
+
const msg = formatFailureMessage(error);
|
|
677
686
|
process.stderr.write(`error: ${msg}
|
|
678
687
|
`);
|
|
679
688
|
process.exit(1);
|
|
@@ -1140,7 +1149,10 @@ function resolveReferences(systemRows, personalRows, surface) {
|
|
|
1140
1149
|
}
|
|
1141
1150
|
|
|
1142
1151
|
// src/setup/skill-resolution-io.ts
|
|
1143
|
-
var AGENT_TARGET = {
|
|
1152
|
+
var AGENT_TARGET = {
|
|
1153
|
+
"claude-code": "claude-agent",
|
|
1154
|
+
"gpt-codex": "gpt-codex-agent"
|
|
1155
|
+
};
|
|
1144
1156
|
function agentTargetFor(surface) {
|
|
1145
1157
|
return AGENT_TARGET[surface] ?? `${surface}-agent`;
|
|
1146
1158
|
}
|
|
@@ -1205,12 +1217,58 @@ function writeSkills(dir, skills, surface) {
|
|
|
1205
1217
|
if (written.length) recordMaterialisedSkills(dir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
1206
1218
|
return written;
|
|
1207
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
|
+
}
|
|
1208
1264
|
function writeAgents(dir, agents, surface) {
|
|
1209
1265
|
if (agents.length) mkdirSync3(dir, { recursive: true });
|
|
1210
1266
|
const written = [];
|
|
1211
1267
|
for (const a of agents) {
|
|
1212
|
-
const
|
|
1213
|
-
|
|
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);
|
|
1214
1272
|
written.push(file);
|
|
1215
1273
|
}
|
|
1216
1274
|
if (written.length) recordMaterialisedSkills(dir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -1239,7 +1297,7 @@ var AGENT_SPEC = {
|
|
|
1239
1297
|
dir: agentsDir,
|
|
1240
1298
|
resolve: resolveAgentSet,
|
|
1241
1299
|
write: writeAgents,
|
|
1242
|
-
supportsCodex:
|
|
1300
|
+
supportsCodex: true
|
|
1243
1301
|
};
|
|
1244
1302
|
function scopeOf(opts) {
|
|
1245
1303
|
return opts.local ? "project" : resolveScope(opts.scope);
|
|
@@ -1315,6 +1373,10 @@ async function runInstall(spec, cmd, opts) {
|
|
|
1315
1373
|
const rows = await fetchTemplateRows(cfg, personalWorkspaceId);
|
|
1316
1374
|
const results = targets.map((t) => {
|
|
1317
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
|
+
}
|
|
1318
1380
|
const refs = spec.kind === "skill" ? resolveReferenceSet(rows, t.surface) : [];
|
|
1319
1381
|
const written = dryRun ? items.map((i) => i.name) : spec.write(t.dir, items, t.surface);
|
|
1320
1382
|
const refsWritten = dryRun ? refs.map((r) => r.name) : writeReferencesIntoSkillDirs(t.dir, items, refs);
|
|
@@ -1440,17 +1502,19 @@ function registerAgents(program2) {
|
|
|
1440
1502
|
`
|
|
1441
1503
|
Examples:
|
|
1442
1504
|
$ sechroom agents install materialise your installed agents to ~/.claude/agents
|
|
1505
|
+
$ sechroom agents install --client codex materialise native TOML agents to ~/.codex/agents
|
|
1443
1506
|
$ sechroom agents install --scope project write them to ./.claude/agents instead
|
|
1444
1507
|
$ sechroom agents install --claude-config-dir ~/.claude-work target another instance
|
|
1445
1508
|
$ sechroom agents list what's materialised on disk
|
|
1446
1509
|
$ sechroom agents clean remove the materialised agent files
|
|
1447
1510
|
|
|
1448
|
-
Agents are resolved from the agent target (target:claude-agent
|
|
1449
|
-
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).`
|
|
1450
1514
|
);
|
|
1451
|
-
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));
|
|
1452
|
-
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));
|
|
1453
|
-
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));
|
|
1454
1518
|
}
|
|
1455
1519
|
|
|
1456
1520
|
// src/commands/channel.ts
|
|
@@ -1595,6 +1659,12 @@ function installHooksJson(path, commands, dryRun) {
|
|
|
1595
1659
|
function installClaudeCommands(claudeDir, commands, dryRun) {
|
|
1596
1660
|
return installHooksJson(join6(claudeDir, "settings.json"), commands, dryRun);
|
|
1597
1661
|
}
|
|
1662
|
+
function installCodexCommands(codexHome, commands, dryRun) {
|
|
1663
|
+
return [
|
|
1664
|
+
installHooksJson(join6(codexHome, "hooks.json"), commands, dryRun),
|
|
1665
|
+
installCodexFeatureFlag(join6(codexHome, "config.toml"), dryRun)
|
|
1666
|
+
];
|
|
1667
|
+
}
|
|
1598
1668
|
function ensureCodexFeaturesHooks(content) {
|
|
1599
1669
|
const lines = content.split("\n");
|
|
1600
1670
|
const headerIdx = lines.findIndex((l) => l.trim() === "[features]");
|
|
@@ -1697,7 +1767,10 @@ function registerChannel(program2) {
|
|
|
1697
1767
|
"--tag <tag...>",
|
|
1698
1768
|
"Tag(s) the event must carry to match (repeatable). Default targets WLP task dispatches.",
|
|
1699
1769
|
["kind:task"]
|
|
1700
|
-
).option("--workspace <wsp...>", "Restrict to these workspace id(s)")
|
|
1770
|
+
).option("--workspace <wsp...>", "Restrict to these workspace id(s)").option(
|
|
1771
|
+
"--executor-instance <id>",
|
|
1772
|
+
"Join the exact executor-instance SignalR group"
|
|
1773
|
+
);
|
|
1701
1774
|
withFilterOpts(
|
|
1702
1775
|
channel.command("connect").description(
|
|
1703
1776
|
"Register a SignalR subscription and stream matched events to stdout"
|
|
@@ -1715,7 +1788,7 @@ function registerChannel(program2) {
|
|
|
1715
1788
|
(typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
|
|
1716
1789
|
)
|
|
1717
1790
|
);
|
|
1718
|
-
const conn = await openConnection(cfg, deliver);
|
|
1791
|
+
const conn = await openConnection(cfg, deliver, opts.executorInstance);
|
|
1719
1792
|
await reconcile(cfg, filter, deliver);
|
|
1720
1793
|
if (json) {
|
|
1721
1794
|
emit(
|
|
@@ -1764,7 +1837,7 @@ function registerChannel(program2) {
|
|
|
1764
1837
|
`))
|
|
1765
1838
|
);
|
|
1766
1839
|
});
|
|
1767
|
-
const conn = await openConnection(cfg, deliver);
|
|
1840
|
+
const conn = await openConnection(cfg, deliver, opts.executorInstance);
|
|
1768
1841
|
await reconcile(cfg, filter, deliver);
|
|
1769
1842
|
process.stderr.write(
|
|
1770
1843
|
style.dim(
|
|
@@ -1875,8 +1948,9 @@ async function ensureSubscription(cfg, name, filter) {
|
|
|
1875
1948
|
);
|
|
1876
1949
|
return await resp.json();
|
|
1877
1950
|
}
|
|
1878
|
-
async function openConnection(cfg, onEvent) {
|
|
1879
|
-
const
|
|
1951
|
+
async function openConnection(cfg, onEvent, executorInstanceId) {
|
|
1952
|
+
const query = executorInstanceId ? `?executorInstanceId=${encodeURIComponent(executorInstanceId)}` : "";
|
|
1953
|
+
const conn = new HubConnectionBuilder().withUrl(`${cfg.baseUrl}/notifications/${cfg.tenant}${query}`, {
|
|
1880
1954
|
transport: HttpTransportType.LongPolling,
|
|
1881
1955
|
accessTokenFactory: () => requireToken(cfg)
|
|
1882
1956
|
}).withAutomaticReconnect().build();
|
|
@@ -3136,6 +3210,340 @@ Examples:
|
|
|
3136
3210
|
});
|
|
3137
3211
|
}
|
|
3138
3212
|
|
|
3213
|
+
// src/commands/executor.ts
|
|
3214
|
+
import { dirname as dirname8, join as join11 } from "path";
|
|
3215
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
|
|
3216
|
+
var EXECUTOR_STATE = "executor.json";
|
|
3217
|
+
var EXECUTOR_PULSE_COMMAND = "sechroom executor hook-pulse";
|
|
3218
|
+
var EXECUTOR_STOP_COMMAND = "sechroom executor hook-stop";
|
|
3219
|
+
var CLAUDE_EXECUTOR_HOOKS = {
|
|
3220
|
+
SessionStart: EXECUTOR_PULSE_COMMAND,
|
|
3221
|
+
UserPromptSubmit: EXECUTOR_PULSE_COMMAND,
|
|
3222
|
+
PreToolUse: EXECUTOR_PULSE_COMMAND,
|
|
3223
|
+
Stop: EXECUTOR_PULSE_COMMAND,
|
|
3224
|
+
SessionEnd: EXECUTOR_STOP_COMMAND
|
|
3225
|
+
};
|
|
3226
|
+
var CODEX_EXECUTOR_HOOKS = {
|
|
3227
|
+
SessionStart: EXECUTOR_PULSE_COMMAND,
|
|
3228
|
+
UserPromptSubmit: EXECUTOR_PULSE_COMMAND,
|
|
3229
|
+
PreToolUse: EXECUTOR_PULSE_COMMAND,
|
|
3230
|
+
Stop: EXECUTOR_PULSE_COMMAND
|
|
3231
|
+
};
|
|
3232
|
+
function registerExecutor(program2) {
|
|
3233
|
+
const executor = program2.command("executor").description(
|
|
3234
|
+
"Register and operate a local Claude Code/Codex executor advertisement"
|
|
3235
|
+
);
|
|
3236
|
+
executor.command("install").description("Configure this checkout's harness to advertise itself as a WLP executor").option("--connector <id>", "Approved local-session ConnectorDefinition id").option("--instance-key <key>", "Stable executor identity (defaults to .sechroom/lane.json code-lane)").option("--runtime <kind>", "claude-code | codex").option("--surface <surface>", "claude | codex").option("--capability <key...>", "Capability operation keys claimed by this instance").option("--relay <id>", "Relay identity shared by sibling instances", "sechroom-cli-local").option("--subscription-name <name>", "SignalR delivery binding name", "executor-dispatch").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 600).option("--refresh-after <seconds>", "Minimum age before a hook refreshes", parseInteger, 40).option("-y, --yes", "Non-interactive: accept detected surface and lane defaults", false).option("--dry-run", "Show hook files without writing", false).action(async (opts, cmd) => {
|
|
3237
|
+
const globals = cmd.optsWithGlobals();
|
|
3238
|
+
const lane = readSem()?.values["code-lane"];
|
|
3239
|
+
const detected = detectHookSurfaces(process.cwd());
|
|
3240
|
+
let surface = opts.surface;
|
|
3241
|
+
let instanceKey = opts.instanceKey;
|
|
3242
|
+
let runtime = opts.runtime;
|
|
3243
|
+
let connector = opts.connector;
|
|
3244
|
+
let capabilities = opts.capability;
|
|
3245
|
+
const surfaceDefault = detected.length === 1 ? detected[0] : lane?.includes("codex") ? "codex" : "claude";
|
|
3246
|
+
if (!opts.yes && canPrompt()) {
|
|
3247
|
+
surface = await promptText("Harness surface (claude or codex)?", surface ?? surfaceDefault);
|
|
3248
|
+
instanceKey = await promptText("Executor instance key?", instanceKey ?? lane ?? "");
|
|
3249
|
+
runtime = await promptText("Runtime (claude-code or codex)?", runtime ?? (surface === "codex" ? "codex" : "claude-code"));
|
|
3250
|
+
connector = await promptText("Approved local-session connector id?", connector ?? "");
|
|
3251
|
+
const capabilityText = await promptText("Capability keys (comma-separated; blank for none)?", capabilities?.join(",") ?? "");
|
|
3252
|
+
capabilities = capabilityText.split(",").map((x) => x.trim()).filter(Boolean);
|
|
3253
|
+
}
|
|
3254
|
+
surface ??= surfaceDefault;
|
|
3255
|
+
instanceKey ??= lane;
|
|
3256
|
+
runtime ??= surface === "codex" ? "codex" : "claude-code";
|
|
3257
|
+
if (!connector) fail("executor install requires --connector (or an interactive connector id)");
|
|
3258
|
+
if (!instanceKey) fail("no instance key resolved; pass --instance-key or pin .sechroom/lane.json code-lane");
|
|
3259
|
+
if (!opts.yes && !canPrompt()) fail("non-interactive executor install requires --yes");
|
|
3260
|
+
parseRuntimeKind(runtime);
|
|
3261
|
+
if (!["claude", "codex"].includes(surface)) fail("surface must be claude or codex");
|
|
3262
|
+
if (opts.refreshAfter >= opts.ttl) fail("refresh-after must be shorter than the TTL");
|
|
3263
|
+
const sem = readSem();
|
|
3264
|
+
const checkout = sem ? dirname8(dirname8(sem.path)) : process.cwd();
|
|
3265
|
+
const statePath = join11(checkout, ".sechroom", EXECUTOR_STATE);
|
|
3266
|
+
const state = {
|
|
3267
|
+
schemaVersion: 1,
|
|
3268
|
+
instanceKey,
|
|
3269
|
+
runtime: runtime.toLowerCase() === "codex" ? "codex" : "claude-code",
|
|
3270
|
+
connectorId: connector,
|
|
3271
|
+
capabilityKeys: capabilities ?? [],
|
|
3272
|
+
relayId: opts.relay,
|
|
3273
|
+
subscriptionName: opts.subscriptionName,
|
|
3274
|
+
ttlSeconds: opts.ttl,
|
|
3275
|
+
refreshAfterSeconds: opts.refreshAfter
|
|
3276
|
+
};
|
|
3277
|
+
if (!opts.dryRun) {
|
|
3278
|
+
mkdirSync9(dirname8(statePath), { recursive: true });
|
|
3279
|
+
writeFileSync9(statePath, JSON.stringify(state, null, 2) + "\n");
|
|
3280
|
+
ensureStateDirIgnored(checkout);
|
|
3281
|
+
}
|
|
3282
|
+
const configuredClaudeDirs = globals.claudeConfigDir || process.env.CLAUDE_CONFIG_DIR ? resolveClaudeTargets({ override: globals.claudeConfigDir }).map((target) => target.dir) : [join11(checkout, ".claude")];
|
|
3283
|
+
const configuredCodexHomes = globals.codexHome || process.env.CODEX_HOME ? resolveCodexHomes({ override: globals.codexHome }) : [join11(checkout, ".codex")];
|
|
3284
|
+
const hookTargets = surface === "claude" ? configuredClaudeDirs : configuredCodexHomes;
|
|
3285
|
+
for (const target of hookTargets) {
|
|
3286
|
+
const results = surface === "claude" ? [installClaudeCommands(target, CLAUDE_EXECUTOR_HOOKS, opts.dryRun)] : installCodexCommands(target, CODEX_EXECUTOR_HOOKS, opts.dryRun);
|
|
3287
|
+
for (const result of results) process.stderr.write(describe(result, opts.dryRun) + "\n");
|
|
3288
|
+
}
|
|
3289
|
+
warnIfSechroomNotOnPath();
|
|
3290
|
+
process.stderr.write(style.green("executor harness configured") + style.dim(` \u2014 ${instanceKey}
|
|
3291
|
+
`));
|
|
3292
|
+
});
|
|
3293
|
+
executor.command("hook-pulse").description("Hook adapter: register or refresh this checkout's executor").action(async (_opts, cmd) => {
|
|
3294
|
+
await drainStdin();
|
|
3295
|
+
const located = readExecutorState();
|
|
3296
|
+
if (!located) return;
|
|
3297
|
+
const { state, path } = located;
|
|
3298
|
+
const age = state.lastRefreshAt ? Date.now() - Date.parse(state.lastRefreshAt) : Number.POSITIVE_INFINITY;
|
|
3299
|
+
if (state.instanceId && age < state.refreshAfterSeconds * 1e3) return;
|
|
3300
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3301
|
+
try {
|
|
3302
|
+
let data;
|
|
3303
|
+
if (state.instanceId) {
|
|
3304
|
+
try {
|
|
3305
|
+
data = await refresh(cfg, state.instanceId, state.ttlSeconds);
|
|
3306
|
+
} catch {
|
|
3307
|
+
data = await registerInstance(cfg, state);
|
|
3308
|
+
}
|
|
3309
|
+
} else {
|
|
3310
|
+
data = await registerInstance(cfg, state);
|
|
3311
|
+
}
|
|
3312
|
+
state.instanceId = data.id;
|
|
3313
|
+
state.lastRefreshAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3314
|
+
writeFileSync9(path, JSON.stringify(state, null, 2) + "\n");
|
|
3315
|
+
} catch {
|
|
3316
|
+
}
|
|
3317
|
+
});
|
|
3318
|
+
executor.command("hook-stop").description("Hook adapter: deregister this checkout's executor").action(async (_opts, cmd) => {
|
|
3319
|
+
await drainStdin();
|
|
3320
|
+
const located = readExecutorState();
|
|
3321
|
+
if (!located?.state.instanceId) return;
|
|
3322
|
+
try {
|
|
3323
|
+
await api(resolveConfig(cmd.optsWithGlobals()), `/me/executor-instances/${encodeURIComponent(located.state.instanceId)}`, { method: "DELETE", body: JSON.stringify({}) });
|
|
3324
|
+
delete located.state.instanceId;
|
|
3325
|
+
delete located.state.lastRefreshAt;
|
|
3326
|
+
writeFileSync9(located.path, JSON.stringify(located.state, null, 2) + "\n");
|
|
3327
|
+
} catch {
|
|
3328
|
+
}
|
|
3329
|
+
});
|
|
3330
|
+
executor.command("submit-connector").description(
|
|
3331
|
+
"Submit a local-session connector definition for governed approval"
|
|
3332
|
+
).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", [
|
|
3333
|
+
"base",
|
|
3334
|
+
"dotnet-10"
|
|
3335
|
+
]).action(async (opts, cmd) => {
|
|
3336
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3337
|
+
const data = await api(cfg, "/connectors/definitions", {
|
|
3338
|
+
method: "POST",
|
|
3339
|
+
body: JSON.stringify({
|
|
3340
|
+
slug: opts.slug,
|
|
3341
|
+
displayName: opts.displayName,
|
|
3342
|
+
runtimeProfiles: opts.profile,
|
|
3343
|
+
connectorKind: "ExecutionRuntime",
|
|
3344
|
+
providerKind: "local-session",
|
|
3345
|
+
dispatchTransport: parseTransport(opts.transport)
|
|
3346
|
+
})
|
|
3347
|
+
});
|
|
3348
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3349
|
+
if (!cmd.optsWithGlobals().json) {
|
|
3350
|
+
process.stderr.write(
|
|
3351
|
+
style.dim(
|
|
3352
|
+
"approve this connector definition before registering executors\n"
|
|
3353
|
+
)
|
|
3354
|
+
);
|
|
3355
|
+
}
|
|
3356
|
+
});
|
|
3357
|
+
executor.command("register").description(
|
|
3358
|
+
"Create/reuse a SignalR binding and register this local executor instance"
|
|
3359
|
+
).requiredOption(
|
|
3360
|
+
"--instance-key <key>",
|
|
3361
|
+
"Stable key for this concrete session/lane"
|
|
3362
|
+
).requiredOption(
|
|
3363
|
+
"--connector <id>",
|
|
3364
|
+
"Approved local-session ConnectorDefinition id"
|
|
3365
|
+
).option("--runtime <kind>", "claude-code | codex", "claude-code").option(
|
|
3366
|
+
"--relay <id>",
|
|
3367
|
+
"Relay identity shared by sibling instances",
|
|
3368
|
+
"sechroom-cli-local"
|
|
3369
|
+
).option(
|
|
3370
|
+
"--subscription-name <name>",
|
|
3371
|
+
"SignalR delivery binding name",
|
|
3372
|
+
"executor-dispatch"
|
|
3373
|
+
).option(
|
|
3374
|
+
"--capability <key...>",
|
|
3375
|
+
"Capability operation keys claimed by this instance"
|
|
3376
|
+
).option("--tool-set-ref <ref>", "Optional governed tool-set reference").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (opts, cmd) => {
|
|
3377
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3378
|
+
if (!cfg.workspaceId)
|
|
3379
|
+
fail("executor register requires a configured workspaceId");
|
|
3380
|
+
const subscription = await api(
|
|
3381
|
+
cfg,
|
|
3382
|
+
"/me/delivery-subscriptions/signalr",
|
|
3383
|
+
{
|
|
3384
|
+
method: "POST",
|
|
3385
|
+
body: JSON.stringify({
|
|
3386
|
+
name: opts.subscriptionName,
|
|
3387
|
+
enabled: true,
|
|
3388
|
+
filter: { tags: ["kind:task"], workspaceScope: [cfg.workspaceId] }
|
|
3389
|
+
})
|
|
3390
|
+
}
|
|
3391
|
+
);
|
|
3392
|
+
const data = await api(
|
|
3393
|
+
cfg,
|
|
3394
|
+
"/me/executor-instances",
|
|
3395
|
+
{
|
|
3396
|
+
method: "POST",
|
|
3397
|
+
body: JSON.stringify({
|
|
3398
|
+
relayId: opts.relay,
|
|
3399
|
+
instanceKey: opts.instanceKey,
|
|
3400
|
+
runtimeKind: parseRuntimeKind(opts.runtime),
|
|
3401
|
+
activationMode: "Attached",
|
|
3402
|
+
deliverySubscriptionId: subscription.id,
|
|
3403
|
+
connectorId: opts.connector,
|
|
3404
|
+
claimedCapabilityKeys: opts.capability ?? [],
|
|
3405
|
+
toolSetRef: opts.toolSetRef ?? null,
|
|
3406
|
+
ttlSeconds: opts.ttl
|
|
3407
|
+
})
|
|
3408
|
+
}
|
|
3409
|
+
);
|
|
3410
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3411
|
+
if (!cmd.optsWithGlobals().json)
|
|
3412
|
+
process.stderr.write(
|
|
3413
|
+
style.dim(`refresh with: sechroom executor heartbeat ${data.id}
|
|
3414
|
+
`)
|
|
3415
|
+
);
|
|
3416
|
+
});
|
|
3417
|
+
executor.command("refresh <id>").description("Refresh one advertisement lease once").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (id, opts, cmd) => {
|
|
3418
|
+
const data = await refresh(
|
|
3419
|
+
resolveConfig(cmd.optsWithGlobals()),
|
|
3420
|
+
id,
|
|
3421
|
+
opts.ttl
|
|
3422
|
+
);
|
|
3423
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3424
|
+
});
|
|
3425
|
+
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) => {
|
|
3426
|
+
if (opts.interval >= opts.ttl)
|
|
3427
|
+
fail("heartbeat interval must be shorter than the TTL");
|
|
3428
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3429
|
+
await refresh(cfg, id, opts.ttl);
|
|
3430
|
+
process.stderr.write(
|
|
3431
|
+
style.green("executor heartbeat active") + style.dim(` \u2014 ${id}
|
|
3432
|
+
`)
|
|
3433
|
+
);
|
|
3434
|
+
await holdHeartbeat(async () => {
|
|
3435
|
+
await refresh(cfg, id, opts.ttl);
|
|
3436
|
+
}, opts.interval * 1e3);
|
|
3437
|
+
});
|
|
3438
|
+
executor.command("offers <id>").description("List live dispatch offers addressed to this exact instance").action(async (id, _opts, cmd) => {
|
|
3439
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3440
|
+
const data = await api(
|
|
3441
|
+
cfg,
|
|
3442
|
+
`/me/executor-instances/${encodeURIComponent(id)}/dispatch-offers`
|
|
3443
|
+
);
|
|
3444
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3445
|
+
});
|
|
3446
|
+
executor.command("deregister <id>").description("Stop advertising this executor instance").action(async (id, _opts, cmd) => {
|
|
3447
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3448
|
+
const data = await api(
|
|
3449
|
+
cfg,
|
|
3450
|
+
`/me/executor-instances/${encodeURIComponent(id)}`,
|
|
3451
|
+
{
|
|
3452
|
+
method: "DELETE",
|
|
3453
|
+
body: JSON.stringify({})
|
|
3454
|
+
}
|
|
3455
|
+
);
|
|
3456
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3457
|
+
});
|
|
3458
|
+
}
|
|
3459
|
+
function parseRuntimeKind(value) {
|
|
3460
|
+
switch (value.trim().toLowerCase()) {
|
|
3461
|
+
case "claude":
|
|
3462
|
+
case "claude-code":
|
|
3463
|
+
return "ClaudeCode";
|
|
3464
|
+
case "codex":
|
|
3465
|
+
return "Codex";
|
|
3466
|
+
default:
|
|
3467
|
+
return fail("runtime must be claude-code or codex");
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
function parseTransport(value) {
|
|
3471
|
+
switch (value.trim().toLowerCase()) {
|
|
3472
|
+
case "push":
|
|
3473
|
+
return "Push";
|
|
3474
|
+
case "pull":
|
|
3475
|
+
return "Pull";
|
|
3476
|
+
default:
|
|
3477
|
+
return fail("transport must be push or pull");
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
async function refresh(cfg, id, ttlSeconds) {
|
|
3481
|
+
return api(
|
|
3482
|
+
cfg,
|
|
3483
|
+
`/me/executor-instances/${encodeURIComponent(id)}/refresh`,
|
|
3484
|
+
{
|
|
3485
|
+
method: "POST",
|
|
3486
|
+
body: JSON.stringify({ ttlSeconds })
|
|
3487
|
+
}
|
|
3488
|
+
);
|
|
3489
|
+
}
|
|
3490
|
+
async function registerInstance(cfg, state) {
|
|
3491
|
+
if (!cfg.workspaceId) fail("executor registration requires a configured workspaceId");
|
|
3492
|
+
const subscription = await api(cfg, "/me/delivery-subscriptions/signalr", {
|
|
3493
|
+
method: "POST",
|
|
3494
|
+
body: JSON.stringify({ name: state.subscriptionName, enabled: true, filter: { tags: ["kind:task"], workspaceScope: [cfg.workspaceId] } })
|
|
3495
|
+
});
|
|
3496
|
+
return api(cfg, "/me/executor-instances", {
|
|
3497
|
+
method: "POST",
|
|
3498
|
+
body: JSON.stringify({ relayId: state.relayId, instanceKey: state.instanceKey, runtimeKind: parseRuntimeKind(state.runtime), activationMode: "Attached", deliverySubscriptionId: subscription.id, connectorId: state.connectorId, claimedCapabilityKeys: state.capabilityKeys, toolSetRef: null, ttlSeconds: state.ttlSeconds })
|
|
3499
|
+
});
|
|
3500
|
+
}
|
|
3501
|
+
function readExecutorState(start = process.cwd()) {
|
|
3502
|
+
const semPath = resolveSemPathForRead(start);
|
|
3503
|
+
const sem = semPath ? readSem(semPath) : void 0;
|
|
3504
|
+
const path = join11(sem ? dirname8(sem.path) : join11(start, ".sechroom"), EXECUTOR_STATE);
|
|
3505
|
+
if (!existsSync9(path)) return void 0;
|
|
3506
|
+
return { state: JSON.parse(readFileSync8(path, "utf8")), path };
|
|
3507
|
+
}
|
|
3508
|
+
async function drainStdin() {
|
|
3509
|
+
if (process.stdin.isTTY) return;
|
|
3510
|
+
for await (const _chunk of process.stdin) {
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
async function api(cfg, path, init) {
|
|
3514
|
+
const token = await requireToken(cfg);
|
|
3515
|
+
const response = await fetch(`${cfg.baseUrl}${path}`, {
|
|
3516
|
+
...init,
|
|
3517
|
+
headers: {
|
|
3518
|
+
authorization: `Bearer ${token}`,
|
|
3519
|
+
tenant: cfg.tenant,
|
|
3520
|
+
"content-type": "application/json",
|
|
3521
|
+
"x-sechroom-surface": "cli"
|
|
3522
|
+
}
|
|
3523
|
+
});
|
|
3524
|
+
if (!response.ok)
|
|
3525
|
+
fail(
|
|
3526
|
+
`${init?.method ?? "GET"} ${path} failed (${response.status}): ${await response.text()}`
|
|
3527
|
+
);
|
|
3528
|
+
return response.json();
|
|
3529
|
+
}
|
|
3530
|
+
function parseInteger(value) {
|
|
3531
|
+
const parsed = Number.parseInt(value, 10);
|
|
3532
|
+
if (!Number.isFinite(parsed)) fail(`expected an integer, got '${value}'`);
|
|
3533
|
+
return parsed;
|
|
3534
|
+
}
|
|
3535
|
+
function holdHeartbeat(tick, intervalMs) {
|
|
3536
|
+
return new Promise((resolve3, reject) => {
|
|
3537
|
+
const timer = setInterval(() => void tick().catch(reject), intervalMs);
|
|
3538
|
+
const stop = () => {
|
|
3539
|
+
clearInterval(timer);
|
|
3540
|
+
resolve3();
|
|
3541
|
+
};
|
|
3542
|
+
process.once("SIGINT", stop);
|
|
3543
|
+
process.once("SIGTERM", stop);
|
|
3544
|
+
});
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3139
3547
|
// src/commands/filing.ts
|
|
3140
3548
|
function registerFiling(program2) {
|
|
3141
3549
|
const filing = program2.command("filing").description("Review and act on filing suggestions");
|
|
@@ -3647,8 +4055,8 @@ Examples:
|
|
|
3647
4055
|
|
|
3648
4056
|
// src/setup/apply.ts
|
|
3649
4057
|
import { createHash as createHash3 } from "crypto";
|
|
3650
|
-
import { mkdirSync as
|
|
3651
|
-
import { dirname as
|
|
4058
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync10, existsSync as existsSync10 } from "fs";
|
|
4059
|
+
import { dirname as dirname9 } from "path";
|
|
3652
4060
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
3653
4061
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
3654
4062
|
function normalizeBody(s) {
|
|
@@ -3701,22 +4109,22 @@ function parseManagedBlock(content, block) {
|
|
|
3701
4109
|
return null;
|
|
3702
4110
|
}
|
|
3703
4111
|
function ensureDir2(path) {
|
|
3704
|
-
|
|
4112
|
+
mkdirSync10(dirname9(path), { recursive: true });
|
|
3705
4113
|
}
|
|
3706
4114
|
function readOr(path, fallback) {
|
|
3707
4115
|
try {
|
|
3708
|
-
return
|
|
4116
|
+
return readFileSync9(path, "utf8");
|
|
3709
4117
|
} catch {
|
|
3710
4118
|
return fallback;
|
|
3711
4119
|
}
|
|
3712
4120
|
}
|
|
3713
4121
|
function mergeMcpJson(path, snippet, dryRun) {
|
|
3714
4122
|
const incoming = JSON.parse(snippet);
|
|
3715
|
-
const existed =
|
|
4123
|
+
const existed = existsSync10(path);
|
|
3716
4124
|
let current = {};
|
|
3717
4125
|
if (existed) {
|
|
3718
4126
|
try {
|
|
3719
|
-
current = JSON.parse(
|
|
4127
|
+
current = JSON.parse(readFileSync9(path, "utf8"));
|
|
3720
4128
|
} catch {
|
|
3721
4129
|
return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
|
|
3722
4130
|
}
|
|
@@ -3724,26 +4132,26 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
3724
4132
|
current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
|
|
3725
4133
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3726
4134
|
ensureDir2(path);
|
|
3727
|
-
|
|
4135
|
+
writeFileSync10(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
3728
4136
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3729
4137
|
}
|
|
3730
4138
|
function mergeCodexToml(path, snippet, dryRun) {
|
|
3731
|
-
const existed =
|
|
4139
|
+
const existed = existsSync10(path);
|
|
3732
4140
|
let body = readOr(path, "");
|
|
3733
4141
|
body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
|
|
3734
4142
|
const trimmed = body.trim();
|
|
3735
4143
|
const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
|
|
3736
4144
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3737
4145
|
ensureDir2(path);
|
|
3738
|
-
|
|
4146
|
+
writeFileSync10(path, next, { mode: 384 });
|
|
3739
4147
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3740
4148
|
}
|
|
3741
4149
|
function writeInstructionBlock(path, write, dryRun) {
|
|
3742
|
-
const existed =
|
|
4150
|
+
const existed = existsSync10(path);
|
|
3743
4151
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
3744
4152
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
3745
4153
|
ensureDir2(path);
|
|
3746
|
-
|
|
4154
|
+
writeFileSync10(path, next);
|
|
3747
4155
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
3748
4156
|
}
|
|
3749
4157
|
function computeBlockFile(current, write) {
|
|
@@ -3784,7 +4192,7 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
3784
4192
|
const next = computeBlockFile(current, write);
|
|
3785
4193
|
if (!dryRun) {
|
|
3786
4194
|
ensureDir2(proposedPath);
|
|
3787
|
-
|
|
4195
|
+
writeFileSync10(proposedPath, next);
|
|
3788
4196
|
}
|
|
3789
4197
|
return {
|
|
3790
4198
|
kind: "instruction",
|
|
@@ -3914,8 +4322,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
3914
4322
|
}
|
|
3915
4323
|
|
|
3916
4324
|
// src/setup/skills-offer.ts
|
|
3917
|
-
import { mkdirSync as
|
|
3918
|
-
import { join as
|
|
4325
|
+
import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
|
|
4326
|
+
import { join as join12 } from "path";
|
|
3919
4327
|
|
|
3920
4328
|
// src/setup/lane-pin.ts
|
|
3921
4329
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -4031,8 +4439,8 @@ Found ${summary} available to you for ${surface}.
|
|
|
4031
4439
|
if (skills.length > 0) {
|
|
4032
4440
|
const written = [];
|
|
4033
4441
|
for (const s of skills) {
|
|
4034
|
-
|
|
4035
|
-
|
|
4442
|
+
mkdirSync11(join12(sDir, s.name), { recursive: true });
|
|
4443
|
+
writeFileSync11(join12(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
4036
4444
|
written.push(s.name);
|
|
4037
4445
|
}
|
|
4038
4446
|
recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -4040,11 +4448,11 @@ Found ${summary} available to you for ${surface}.
|
|
|
4040
4448
|
`);
|
|
4041
4449
|
}
|
|
4042
4450
|
if (agents.length > 0) {
|
|
4043
|
-
|
|
4451
|
+
mkdirSync11(aDir, { recursive: true });
|
|
4044
4452
|
const written = [];
|
|
4045
4453
|
for (const a of agents) {
|
|
4046
4454
|
const file = `${a.name}.md`;
|
|
4047
|
-
|
|
4455
|
+
writeFileSync11(join12(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
4048
4456
|
written.push(file);
|
|
4049
4457
|
}
|
|
4050
4458
|
recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -4427,13 +4835,13 @@ Wired to namespace '${slug}'. Restart your AI client (or reload MCP) to pick it
|
|
|
4427
4835
|
}
|
|
4428
4836
|
|
|
4429
4837
|
// src/commands/onboard.ts
|
|
4430
|
-
import { existsSync as
|
|
4431
|
-
import { basename as basename2, join as
|
|
4838
|
+
import { existsSync as existsSync12 } from "fs";
|
|
4839
|
+
import { basename as basename2, join as join14 } from "path";
|
|
4432
4840
|
|
|
4433
4841
|
// src/commands/fanout.ts
|
|
4434
4842
|
import { spawnSync } from "child_process";
|
|
4435
|
-
import { existsSync as
|
|
4436
|
-
import { isAbsolute, join as
|
|
4843
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
4844
|
+
import { isAbsolute, join as join13, resolve } from "path";
|
|
4437
4845
|
var ICON = {
|
|
4438
4846
|
refresh: "\u21BB",
|
|
4439
4847
|
bind: "+",
|
|
@@ -4453,21 +4861,21 @@ function discoverChildren(root) {
|
|
|
4453
4861
|
const out = [];
|
|
4454
4862
|
for (const name of names.sort()) {
|
|
4455
4863
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
4456
|
-
const dir =
|
|
4864
|
+
const dir = join13(root, name);
|
|
4457
4865
|
try {
|
|
4458
4866
|
if (!statSync3(dir).isDirectory()) continue;
|
|
4459
4867
|
} catch {
|
|
4460
4868
|
continue;
|
|
4461
4869
|
}
|
|
4462
|
-
if (
|
|
4870
|
+
if (existsSync11(join13(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
4463
4871
|
}
|
|
4464
4872
|
return out;
|
|
4465
4873
|
}
|
|
4466
4874
|
function readManifest(path) {
|
|
4467
|
-
if (!
|
|
4875
|
+
if (!existsSync11(path)) return null;
|
|
4468
4876
|
let parsed;
|
|
4469
4877
|
try {
|
|
4470
|
-
parsed = JSON.parse(
|
|
4878
|
+
parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
4471
4879
|
} catch (err2) {
|
|
4472
4880
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
4473
4881
|
}
|
|
@@ -4833,10 +5241,10 @@ async function chooseScope(scopeFlag, yes) {
|
|
|
4833
5241
|
}
|
|
4834
5242
|
async function planRecurseChild(entry, root, client, opts) {
|
|
4835
5243
|
const dir = resolveChildDir(entry.path, root);
|
|
4836
|
-
if (!
|
|
5244
|
+
if (!existsSync12(dir)) {
|
|
4837
5245
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
4838
5246
|
}
|
|
4839
|
-
if (
|
|
5247
|
+
if (existsSync12(join14(dir, ".sechroom.json"))) {
|
|
4840
5248
|
return {
|
|
4841
5249
|
label: entry.path,
|
|
4842
5250
|
dir,
|
|
@@ -4909,7 +5317,7 @@ This fan-out will pin the same lane in every repo:
|
|
|
4909
5317
|
async function runRecurse(cfg, g, opts) {
|
|
4910
5318
|
const { yes, dryRun, json } = opts;
|
|
4911
5319
|
const root = process.cwd();
|
|
4912
|
-
const manifestPath =
|
|
5320
|
+
const manifestPath = join14(root, ".sechroom", "repos.json");
|
|
4913
5321
|
const fromManifest = readManifest(manifestPath);
|
|
4914
5322
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
4915
5323
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
@@ -5442,23 +5850,23 @@ Examples:
|
|
|
5442
5850
|
|
|
5443
5851
|
// src/commands/reset.ts
|
|
5444
5852
|
import { homedir as homedir4 } from "os";
|
|
5445
|
-
import { join as
|
|
5446
|
-
import { existsSync as
|
|
5853
|
+
import { join as join15 } from "path";
|
|
5854
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11, rmSync as rmSync3 } from "fs";
|
|
5447
5855
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
5448
|
-
var localSkillsDir = () =>
|
|
5449
|
-
var globalSkillsDir = () =>
|
|
5450
|
-
var localAgentsDir = () =>
|
|
5451
|
-
var globalAgentsDir = () =>
|
|
5856
|
+
var localSkillsDir = () => join15(process.cwd(), ".claude", "skills");
|
|
5857
|
+
var globalSkillsDir = () => join15(homedir4(), ".claude", "skills");
|
|
5858
|
+
var localAgentsDir = () => join15(process.cwd(), ".claude", "agents");
|
|
5859
|
+
var globalAgentsDir = () => join15(homedir4(), ".claude", "agents");
|
|
5452
5860
|
function removeMaterialisedSkills(dir) {
|
|
5453
5861
|
const removed = [];
|
|
5454
|
-
const lockPath =
|
|
5455
|
-
if (!
|
|
5862
|
+
const lockPath = join15(dir, SKILLS_LOCK2);
|
|
5863
|
+
if (!existsSync13(lockPath)) return removed;
|
|
5456
5864
|
try {
|
|
5457
|
-
const lock = JSON.parse(
|
|
5865
|
+
const lock = JSON.parse(readFileSync11(lockPath, "utf8"));
|
|
5458
5866
|
for (const entry of Object.values(lock)) {
|
|
5459
5867
|
for (const name of entry.skills ?? []) {
|
|
5460
|
-
const p =
|
|
5461
|
-
if (
|
|
5868
|
+
const p = join15(dir, name);
|
|
5869
|
+
if (existsSync13(p)) {
|
|
5462
5870
|
rmSync3(p, { recursive: true, force: true });
|
|
5463
5871
|
removed.push(p);
|
|
5464
5872
|
}
|
|
@@ -5503,18 +5911,18 @@ function registerReset(program2) {
|
|
|
5503
5911
|
}
|
|
5504
5912
|
}
|
|
5505
5913
|
const removed = [];
|
|
5506
|
-
const stateDir =
|
|
5507
|
-
if (
|
|
5914
|
+
const stateDir = join15(process.cwd(), ".sechroom");
|
|
5915
|
+
if (existsSync13(stateDir)) {
|
|
5508
5916
|
rmSync3(stateDir, { recursive: true, force: true });
|
|
5509
5917
|
removed.push(stateDir);
|
|
5510
5918
|
}
|
|
5511
|
-
const legacyCfg =
|
|
5512
|
-
if (
|
|
5919
|
+
const legacyCfg = join15(process.cwd(), ".sechroom.json");
|
|
5920
|
+
if (existsSync13(legacyCfg)) {
|
|
5513
5921
|
rmSync3(legacyCfg, { force: true });
|
|
5514
5922
|
removed.push(legacyCfg);
|
|
5515
5923
|
}
|
|
5516
|
-
const legacySem =
|
|
5517
|
-
if (
|
|
5924
|
+
const legacySem = join15(process.cwd(), ".sem");
|
|
5925
|
+
if (existsSync13(legacySem)) {
|
|
5518
5926
|
rmSync3(legacySem, { force: true });
|
|
5519
5927
|
removed.push(legacySem);
|
|
5520
5928
|
}
|
|
@@ -5540,8 +5948,8 @@ function registerReset(program2) {
|
|
|
5540
5948
|
}
|
|
5541
5949
|
|
|
5542
5950
|
// src/commands/skills.ts
|
|
5543
|
-
import { existsSync as
|
|
5544
|
-
import { join as
|
|
5951
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync12, statSync as statSync4, writeFileSync as writeFileSync12 } from "fs";
|
|
5952
|
+
import { join as join16 } from "path";
|
|
5545
5953
|
function filenameFromDisposition(header) {
|
|
5546
5954
|
if (!header) return void 0;
|
|
5547
5955
|
const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
|
|
@@ -5549,11 +5957,11 @@ function filenameFromDisposition(header) {
|
|
|
5549
5957
|
}
|
|
5550
5958
|
function resolveOutputPath(output, serverFilename) {
|
|
5551
5959
|
const filename = serverFilename || "skills.zip";
|
|
5552
|
-
if (!output) return
|
|
5553
|
-
const looksLikeDir = output.endsWith("/") ||
|
|
5960
|
+
if (!output) return join16(process.cwd(), filename);
|
|
5961
|
+
const looksLikeDir = output.endsWith("/") || existsSync14(output) && statSync4(output).isDirectory();
|
|
5554
5962
|
if (looksLikeDir) {
|
|
5555
|
-
|
|
5556
|
-
return
|
|
5963
|
+
mkdirSync12(output, { recursive: true });
|
|
5964
|
+
return join16(output, filename);
|
|
5557
5965
|
}
|
|
5558
5966
|
return output;
|
|
5559
5967
|
}
|
|
@@ -5584,7 +5992,7 @@ async function downloadZip(label, call, output) {
|
|
|
5584
5992
|
const buf = Buffer.from(res.data);
|
|
5585
5993
|
const filename = filenameFromDisposition(res.response.headers.get("content-disposition")) ?? "skills.zip";
|
|
5586
5994
|
const path = resolveOutputPath(output, filename);
|
|
5587
|
-
|
|
5995
|
+
writeFileSync12(path, buf);
|
|
5588
5996
|
return { path, bytes: buf.length, filename };
|
|
5589
5997
|
}
|
|
5590
5998
|
function registerSkills(program2) {
|
|
@@ -5758,12 +6166,12 @@ clients select all; an unconfigured machine preserves the legacy Claude default.
|
|
|
5758
6166
|
}
|
|
5759
6167
|
|
|
5760
6168
|
// src/commands/sweep.ts
|
|
5761
|
-
import { existsSync as
|
|
5762
|
-
import { dirname as
|
|
5763
|
-
var DEFAULT_MANIFEST =
|
|
6169
|
+
import { existsSync as existsSync15 } from "fs";
|
|
6170
|
+
import { dirname as dirname10, join as join17, resolve as resolve2 } from "path";
|
|
6171
|
+
var DEFAULT_MANIFEST = join17(".sechroom", "repos.json");
|
|
5764
6172
|
function planEntry(entry, root) {
|
|
5765
6173
|
const dir = resolveChildDir(entry.path, root);
|
|
5766
|
-
if (!
|
|
6174
|
+
if (!existsSync15(dir)) {
|
|
5767
6175
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
5768
6176
|
}
|
|
5769
6177
|
if (committedBindingPath(dir)) {
|
|
@@ -5839,7 +6247,7 @@ Examples:
|
|
|
5839
6247
|
`);
|
|
5840
6248
|
return;
|
|
5841
6249
|
}
|
|
5842
|
-
const root =
|
|
6250
|
+
const root = dirname10(dirname10(manifestPath));
|
|
5843
6251
|
const plans = repos.map((entry) => planEntry(entry, root));
|
|
5844
6252
|
if (!json) {
|
|
5845
6253
|
process.stderr.write(
|
|
@@ -5858,13 +6266,13 @@ Examples:
|
|
|
5858
6266
|
|
|
5859
6267
|
// src/commands/telemetry.ts
|
|
5860
6268
|
import {
|
|
5861
|
-
existsSync as
|
|
5862
|
-
mkdirSync as
|
|
5863
|
-
readFileSync as
|
|
6269
|
+
existsSync as existsSync16,
|
|
6270
|
+
mkdirSync as mkdirSync13,
|
|
6271
|
+
readFileSync as readFileSync12,
|
|
5864
6272
|
rmSync as rmSync4,
|
|
5865
|
-
writeFileSync as
|
|
6273
|
+
writeFileSync as writeFileSync13
|
|
5866
6274
|
} from "fs";
|
|
5867
|
-
import { dirname as
|
|
6275
|
+
import { dirname as dirname11, join as join18 } from "path";
|
|
5868
6276
|
function registerTelemetry(program2) {
|
|
5869
6277
|
const telemetry = program2.command("telemetry").description(
|
|
5870
6278
|
"Emit WLP run telemetry (an executor leg's progress events) into a decomposition run"
|
|
@@ -5934,14 +6342,14 @@ function registerTelemetry(program2) {
|
|
|
5934
6342
|
"Decomposition id this session executes"
|
|
5935
6343
|
).requiredOption("--task <id>", "Task id this session executes").action((opts, cmd) => {
|
|
5936
6344
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
5937
|
-
const dir =
|
|
5938
|
-
|
|
5939
|
-
const path =
|
|
6345
|
+
const dir = join18(process.cwd(), ".sechroom");
|
|
6346
|
+
mkdirSync13(dir, { recursive: true });
|
|
6347
|
+
const path = join18(dir, BINDING_FILE);
|
|
5940
6348
|
const binding = {
|
|
5941
6349
|
decompositionId: opts.decomposition,
|
|
5942
6350
|
taskId: opts.task
|
|
5943
6351
|
};
|
|
5944
|
-
|
|
6352
|
+
writeFileSync13(path, JSON.stringify(binding, null, 2) + "\n");
|
|
5945
6353
|
ensureStateDirIgnored(process.cwd());
|
|
5946
6354
|
if (json) {
|
|
5947
6355
|
emit({ bound: true, ...binding, path }, true);
|
|
@@ -5956,8 +6364,8 @@ function registerTelemetry(program2) {
|
|
|
5956
6364
|
});
|
|
5957
6365
|
telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
|
|
5958
6366
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
5959
|
-
const path =
|
|
5960
|
-
const existed =
|
|
6367
|
+
const path = join18(process.cwd(), ".sechroom", BINDING_FILE);
|
|
6368
|
+
const existed = existsSync16(path);
|
|
5961
6369
|
if (existed) rmSync4(path);
|
|
5962
6370
|
if (json) emit({ unbound: existed, path }, true);
|
|
5963
6371
|
else
|
|
@@ -6065,11 +6473,11 @@ async function postTelemetry(cfg, decompositionId, events) {
|
|
|
6065
6473
|
function findBinding(start) {
|
|
6066
6474
|
let dir = start;
|
|
6067
6475
|
for (; ; ) {
|
|
6068
|
-
const path =
|
|
6069
|
-
if (
|
|
6476
|
+
const path = join18(dir, ".sechroom", BINDING_FILE);
|
|
6477
|
+
if (existsSync16(path)) {
|
|
6070
6478
|
try {
|
|
6071
6479
|
const b = JSON.parse(
|
|
6072
|
-
|
|
6480
|
+
readFileSync12(path, "utf8")
|
|
6073
6481
|
);
|
|
6074
6482
|
if (b.decompositionId && b.taskId)
|
|
6075
6483
|
return { decompositionId: b.decompositionId, taskId: b.taskId };
|
|
@@ -6077,18 +6485,18 @@ function findBinding(start) {
|
|
|
6077
6485
|
}
|
|
6078
6486
|
return null;
|
|
6079
6487
|
}
|
|
6080
|
-
const parent =
|
|
6488
|
+
const parent = dirname11(dir);
|
|
6081
6489
|
if (parent === dir) return null;
|
|
6082
6490
|
dir = parent;
|
|
6083
6491
|
}
|
|
6084
6492
|
}
|
|
6085
6493
|
function parseTranscript(path) {
|
|
6086
|
-
if (!
|
|
6494
|
+
if (!existsSync16(path)) return null;
|
|
6087
6495
|
let tokensIn = 0;
|
|
6088
6496
|
let tokensOut = 0;
|
|
6089
6497
|
let contextUsed = 0;
|
|
6090
6498
|
let model = "";
|
|
6091
|
-
for (const line of
|
|
6499
|
+
for (const line of readFileSync12(path, "utf8").split("\n")) {
|
|
6092
6500
|
if (!line.trim()) continue;
|
|
6093
6501
|
let obj;
|
|
6094
6502
|
try {
|
|
@@ -6395,7 +6803,7 @@ Examples:
|
|
|
6395
6803
|
function resolveVersion() {
|
|
6396
6804
|
try {
|
|
6397
6805
|
const pkg = JSON.parse(
|
|
6398
|
-
|
|
6806
|
+
readFileSync13(new URL("../package.json", import.meta.url), "utf8")
|
|
6399
6807
|
);
|
|
6400
6808
|
return pkg.version ?? "0.0.0";
|
|
6401
6809
|
} catch {
|
|
@@ -6554,6 +6962,7 @@ registerRelationships(program);
|
|
|
6554
6962
|
registerWorkspace(program);
|
|
6555
6963
|
registerProject(program);
|
|
6556
6964
|
registerDecomposition(program);
|
|
6965
|
+
registerExecutor(program);
|
|
6557
6966
|
registerClose(program);
|
|
6558
6967
|
registerFiling(program);
|
|
6559
6968
|
registerContinuity(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sechroom/cli",
|
|
3
|
-
"version": "2026.7.
|
|
3
|
+
"version": "2026.7.8-rc.4b6e8ecc",
|
|
4
4
|
"description": "Sechroom CLI — a thin, generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|