@sechroom/cli 2026.7.20 → 2026.7.21-rc.59d9df67
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 +171 -19
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2213,7 +2213,7 @@ function registerExecutor(program2) {
|
|
|
2213
2213
|
);
|
|
2214
2214
|
});
|
|
2215
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
|
|
2216
|
+
const data = await refreshExecutorInstance(
|
|
2217
2217
|
resolveConfig(cmd.optsWithGlobals()),
|
|
2218
2218
|
id,
|
|
2219
2219
|
opts.ttl
|
|
@@ -2224,13 +2224,13 @@ function registerExecutor(program2) {
|
|
|
2224
2224
|
if (opts.interval >= opts.ttl)
|
|
2225
2225
|
fail("heartbeat interval must be shorter than the TTL");
|
|
2226
2226
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2227
|
-
await
|
|
2227
|
+
await refreshExecutorInstance(cfg, id, opts.ttl);
|
|
2228
2228
|
process.stderr.write(
|
|
2229
2229
|
style.green("executor heartbeat active") + style.dim(` \u2014 ${id}
|
|
2230
2230
|
`)
|
|
2231
2231
|
);
|
|
2232
2232
|
await holdHeartbeat(async () => {
|
|
2233
|
-
await
|
|
2233
|
+
await refreshExecutorInstance(cfg, id, opts.ttl);
|
|
2234
2234
|
}, opts.interval * 1e3);
|
|
2235
2235
|
});
|
|
2236
2236
|
executor.command("offers <id>").description("List live dispatch offers addressed to this exact instance").action(async (id, _opts, cmd) => {
|
|
@@ -2275,7 +2275,7 @@ function parseTransport(value) {
|
|
|
2275
2275
|
return fail("transport must be push or pull");
|
|
2276
2276
|
}
|
|
2277
2277
|
}
|
|
2278
|
-
async function
|
|
2278
|
+
async function refreshExecutorInstance(cfg, id, ttlSeconds) {
|
|
2279
2279
|
return api(
|
|
2280
2280
|
cfg,
|
|
2281
2281
|
`/me/executor-instances/${encodeURIComponent(id)}/refresh`,
|
|
@@ -2367,8 +2367,7 @@ function registerChannel(program2) {
|
|
|
2367
2367
|
);
|
|
2368
2368
|
const withFilterOpts = (c) => c.option(
|
|
2369
2369
|
"--name <name>",
|
|
2370
|
-
"
|
|
2371
|
-
"wlp-dispatch"
|
|
2370
|
+
"Deprecated: ignored; the installed executor selects its delivery subscription"
|
|
2372
2371
|
).option(
|
|
2373
2372
|
"--tag <tag...>",
|
|
2374
2373
|
"Deprecated: executor eligibility comes from the installed capability advertisement"
|
|
@@ -2404,6 +2403,11 @@ function registerChannel(program2) {
|
|
|
2404
2403
|
instance.id
|
|
2405
2404
|
);
|
|
2406
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
|
|
2410
|
+
);
|
|
2407
2411
|
if (json) {
|
|
2408
2412
|
emit(
|
|
2409
2413
|
{
|
|
@@ -2423,7 +2427,12 @@ function registerChannel(program2) {
|
|
|
2423
2427
|
) + style.dim("streaming matched events to stdout; Ctrl-C to stop.\n")
|
|
2424
2428
|
);
|
|
2425
2429
|
}
|
|
2426
|
-
|
|
2430
|
+
try {
|
|
2431
|
+
await holdOpen(conn);
|
|
2432
|
+
} finally {
|
|
2433
|
+
stopReconciliation();
|
|
2434
|
+
stopHeartbeat();
|
|
2435
|
+
}
|
|
2427
2436
|
});
|
|
2428
2437
|
withFilterOpts(
|
|
2429
2438
|
channel.command("mcp").description(
|
|
@@ -2464,13 +2473,23 @@ function registerChannel(program2) {
|
|
|
2464
2473
|
instance.id
|
|
2465
2474
|
);
|
|
2466
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
|
+
);
|
|
2467
2481
|
process.stderr.write(
|
|
2468
2482
|
style.dim(
|
|
2469
2483
|
`sechroom channel (mcp) \u2014 tenant ${cfg.tenant}, executor ${located.state.instanceKey}
|
|
2470
2484
|
`
|
|
2471
2485
|
)
|
|
2472
2486
|
);
|
|
2473
|
-
|
|
2487
|
+
try {
|
|
2488
|
+
await holdOpen(conn);
|
|
2489
|
+
} finally {
|
|
2490
|
+
stopReconciliation();
|
|
2491
|
+
stopHeartbeat();
|
|
2492
|
+
}
|
|
2474
2493
|
});
|
|
2475
2494
|
channel.command("install").description(
|
|
2476
2495
|
"Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
|
|
@@ -2487,7 +2506,7 @@ function registerChannel(program2) {
|
|
|
2487
2506
|
).option("--dry-run", "Print what would change; write nothing").action((opts) => {
|
|
2488
2507
|
const path = join9(process.cwd(), ".mcp.json");
|
|
2489
2508
|
const dryRun = Boolean(opts.dryRun);
|
|
2490
|
-
const args = ["channel", "mcp"
|
|
2509
|
+
const args = ["channel", "mcp"];
|
|
2491
2510
|
const entry = { command: "sechroom", args };
|
|
2492
2511
|
const config2 = readMcpConfig(path);
|
|
2493
2512
|
config2.mcpServers ??= {};
|
|
@@ -2544,11 +2563,11 @@ function requireExecutorState() {
|
|
|
2544
2563
|
return located;
|
|
2545
2564
|
}
|
|
2546
2565
|
function warnLegacyChannelOptions(opts) {
|
|
2547
|
-
if ((opts.workspace?.length ?? 0) === 0 && (opts.tag?.length ?? 0) === 0 && !opts.executorInstance)
|
|
2566
|
+
if (!opts.name && (opts.workspace?.length ?? 0) === 0 && (opts.tag?.length ?? 0) === 0 && !opts.executorInstance)
|
|
2548
2567
|
return;
|
|
2549
2568
|
process.stderr.write(
|
|
2550
2569
|
style.dim(
|
|
2551
|
-
"channel: --workspace, --tag, and --executor-instance are retired; eligibility and identity come from the installed executor advertisement.\n"
|
|
2570
|
+
"channel: --name, --workspace, --tag, and --executor-instance are retired; delivery, eligibility, and identity come from the installed executor advertisement.\n"
|
|
2552
2571
|
)
|
|
2553
2572
|
);
|
|
2554
2573
|
}
|
|
@@ -2565,6 +2584,26 @@ function createClaimDrain(cfg, executorInstanceId, deliver, dependencies = {}) {
|
|
|
2565
2584
|
return active2;
|
|
2566
2585
|
};
|
|
2567
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
|
+
}
|
|
2568
2607
|
async function drainClaims(cfg, executorInstanceId, deliver, dependencies = {}) {
|
|
2569
2608
|
const request = dependencies.request ?? api;
|
|
2570
2609
|
const sleep = dependencies.sleep ?? ((milliseconds) => new Promise((resolve3) => setTimeout(resolve3, milliseconds)));
|
|
@@ -3518,6 +3557,7 @@ Examples:
|
|
|
3518
3557
|
}
|
|
3519
3558
|
|
|
3520
3559
|
// src/commands/decomposition.ts
|
|
3560
|
+
import { readFile } from "fs/promises";
|
|
3521
3561
|
function registerDecomposition(program2) {
|
|
3522
3562
|
const decomposition = program2.command("decomposition").description(
|
|
3523
3563
|
"Drive a WLP decomposition: decompose a brief, then execute / accept / reject"
|
|
@@ -3527,6 +3567,8 @@ function registerDecomposition(program2) {
|
|
|
3527
3567
|
`
|
|
3528
3568
|
Examples:
|
|
3529
3569
|
$ sechroom decomposition decompose mem_XXXX
|
|
3570
|
+
$ sechroom decomposition from-plan mem_XXXX --file plan.json
|
|
3571
|
+
$ sechroom decomposition plan wlp_XXXX
|
|
3530
3572
|
$ sechroom decomposition execute sug_XXXX
|
|
3531
3573
|
$ sechroom decomposition publish-run sug_XXXX
|
|
3532
3574
|
$ sechroom decomposition accept sug_XXXX
|
|
@@ -3549,6 +3591,47 @@ Examples:
|
|
|
3549
3591
|
cmd.optsWithGlobals().json
|
|
3550
3592
|
);
|
|
3551
3593
|
});
|
|
3594
|
+
decomposition.command("from-plan <briefId>").description(
|
|
3595
|
+
"Create a decomposition from a hand-authored JSON plan; tasks may carry Unix-millisecond wakeAtMs"
|
|
3596
|
+
).requiredOption(
|
|
3597
|
+
"--file <path>",
|
|
3598
|
+
"JSON file containing { project, tasks, source? }; use - for stdin"
|
|
3599
|
+
).action(async (briefId, opts, cmd) => {
|
|
3600
|
+
const raw = opts.file === "-" ? await readStdin2() : await readFile(opts.file, "utf8");
|
|
3601
|
+
const body = parsePlanInput(raw, opts.file);
|
|
3602
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3603
|
+
const data = await runApi(
|
|
3604
|
+
"Creating decomposition from plan",
|
|
3605
|
+
async () => {
|
|
3606
|
+
const client = await makeClient(cfg);
|
|
3607
|
+
return client.POST("/work-briefs/{id}/decompose-from-plan", {
|
|
3608
|
+
params: { path: { id: briefId } },
|
|
3609
|
+
body
|
|
3610
|
+
});
|
|
3611
|
+
}
|
|
3612
|
+
);
|
|
3613
|
+
emitAction(
|
|
3614
|
+
`created ${style.bold(data.suggestionId)} from ${data.taskCount} hand-authored task(s)`,
|
|
3615
|
+
data,
|
|
3616
|
+
cmd.optsWithGlobals().json
|
|
3617
|
+
);
|
|
3618
|
+
});
|
|
3619
|
+
decomposition.command("plan <decompositionId>").description(
|
|
3620
|
+
"Inspect the compiled plan, including each scheduled step's Unix-millisecond wakeAtMs"
|
|
3621
|
+
).action(async (decompositionId, _opts, cmd) => {
|
|
3622
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3623
|
+
const data = await runApi("Reading decomposition plan", async () => {
|
|
3624
|
+
const client = await makeClient(cfg);
|
|
3625
|
+
return client.GET("/decompositions/{id}/plan", {
|
|
3626
|
+
params: { path: { id: decompositionId } }
|
|
3627
|
+
});
|
|
3628
|
+
});
|
|
3629
|
+
emitAction(
|
|
3630
|
+
`read ${style.bold(decompositionId)} plan (${data.steps.length} step(s))`,
|
|
3631
|
+
data,
|
|
3632
|
+
cmd.optsWithGlobals().json
|
|
3633
|
+
);
|
|
3634
|
+
});
|
|
3552
3635
|
decomposition.command("execute <decompositionId>").description(
|
|
3553
3636
|
"Execute a decomposition's Task graph (POST /decompositions/{id}/execute)"
|
|
3554
3637
|
).action(async (decompositionId, _opts, cmd) => {
|
|
@@ -3618,6 +3701,24 @@ Examples:
|
|
|
3618
3701
|
);
|
|
3619
3702
|
});
|
|
3620
3703
|
}
|
|
3704
|
+
function parsePlanInput(raw, sourceName = "plan input") {
|
|
3705
|
+
let value;
|
|
3706
|
+
try {
|
|
3707
|
+
value = JSON.parse(raw);
|
|
3708
|
+
} catch (error) {
|
|
3709
|
+
throw new Error(
|
|
3710
|
+
`${sourceName} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
3711
|
+
);
|
|
3712
|
+
}
|
|
3713
|
+
if (!value || typeof value !== "object" || !Array.isArray(value.tasks))
|
|
3714
|
+
throw new Error(`${sourceName} must contain an object with a tasks array`);
|
|
3715
|
+
return value;
|
|
3716
|
+
}
|
|
3717
|
+
async function readStdin2() {
|
|
3718
|
+
const chunks = [];
|
|
3719
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
3720
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
3721
|
+
}
|
|
3621
3722
|
|
|
3622
3723
|
// src/commands/filing.ts
|
|
3623
3724
|
function registerFiling(program2) {
|
|
@@ -5883,23 +5984,23 @@ Examples:
|
|
|
5883
5984
|
});
|
|
5884
5985
|
emit(data, cmd.optsWithGlobals().json);
|
|
5885
5986
|
});
|
|
5886
|
-
suggestion.command("accept <id>").description("Accept a suggestion (POST /relationship-suggestions/{
|
|
5987
|
+
suggestion.command("accept <id>").description("Accept a suggestion (POST /relationship-suggestions/{instanceId}/accept)").action(async (id, _opts, cmd) => {
|
|
5887
5988
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
5888
5989
|
const data = await runApi("Accepting suggestion", async () => {
|
|
5889
5990
|
const client = await makeClient(cfg);
|
|
5890
|
-
return client.POST("/relationship-suggestions/{
|
|
5891
|
-
params: { path: { id } },
|
|
5991
|
+
return client.POST("/relationship-suggestions/{instanceId}/accept", {
|
|
5992
|
+
params: { path: { instanceId: id } },
|
|
5892
5993
|
body: {}
|
|
5893
5994
|
});
|
|
5894
5995
|
});
|
|
5895
5996
|
emitAction(`accepted suggestion ${style.bold(id)}`, data, cmd.optsWithGlobals().json);
|
|
5896
5997
|
});
|
|
5897
|
-
suggestion.command("reject <id>").description("Reject a suggestion (POST /relationship-suggestions/{
|
|
5998
|
+
suggestion.command("reject <id>").description("Reject a suggestion (POST /relationship-suggestions/{instanceId}/reject)").option("--reason <reason>", "Why it's being rejected").option("--reason-code <code>", "Structured reason code").action(async (id, opts, cmd) => {
|
|
5898
5999
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
5899
6000
|
const data = await runApi("Rejecting suggestion", async () => {
|
|
5900
6001
|
const client = await makeClient(cfg);
|
|
5901
|
-
return client.POST("/relationship-suggestions/{
|
|
5902
|
-
params: { path: { id } },
|
|
6002
|
+
return client.POST("/relationship-suggestions/{instanceId}/reject", {
|
|
6003
|
+
params: { path: { instanceId: id } },
|
|
5903
6004
|
body: {
|
|
5904
6005
|
reason: opts.reason ?? null,
|
|
5905
6006
|
...opts.reasonCode ? { reasonCode: opts.reasonCode } : {}
|
|
@@ -6452,7 +6553,7 @@ function registerTelemetry(program2) {
|
|
|
6452
6553
|
"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."
|
|
6453
6554
|
).action(async (_opts, cmd) => {
|
|
6454
6555
|
try {
|
|
6455
|
-
const raw = await
|
|
6556
|
+
const raw = await readStdin3();
|
|
6456
6557
|
const input = parseHookInput2(raw);
|
|
6457
6558
|
const cwd = input.cwd ?? process.cwd();
|
|
6458
6559
|
const binding = findBinding(cwd);
|
|
@@ -6644,7 +6745,7 @@ function isPermissionNotification(input) {
|
|
|
6644
6745
|
if (t) return t.includes("permission");
|
|
6645
6746
|
return (input.message ?? "").toLowerCase().includes("permission");
|
|
6646
6747
|
}
|
|
6647
|
-
async function
|
|
6748
|
+
async function readStdin3() {
|
|
6648
6749
|
if (process.stdin.isTTY) return "";
|
|
6649
6750
|
const chunks = [];
|
|
6650
6751
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
@@ -6874,6 +6975,56 @@ Examples:
|
|
|
6874
6975
|
});
|
|
6875
6976
|
}
|
|
6876
6977
|
|
|
6978
|
+
// src/commands/work-brief.ts
|
|
6979
|
+
function registerWorkBrief(program2) {
|
|
6980
|
+
const workBrief = program2.command("work-brief").description("Control an active work brief run");
|
|
6981
|
+
workBrief.addHelpText(
|
|
6982
|
+
"after",
|
|
6983
|
+
`
|
|
6984
|
+
Examples:
|
|
6985
|
+
$ sechroom work-brief pause mem_XXXX --reason-code operator-hold --source claude-code-chris
|
|
6986
|
+
$ sechroom work-brief resume mem_XXXX --reason-code operator-resume --source claude-code-chris --reason "Ready to continue"
|
|
6987
|
+
$ sechroom work-brief cancel mem_XXXX --reason-code operator-stopped --source claude-code-chris --reason "Work no longer required"`
|
|
6988
|
+
);
|
|
6989
|
+
registerLifecycleAction(workBrief, "pause");
|
|
6990
|
+
registerLifecycleAction(workBrief, "resume");
|
|
6991
|
+
registerLifecycleAction(workBrief, "cancel");
|
|
6992
|
+
}
|
|
6993
|
+
function registerLifecycleAction(workBrief, action) {
|
|
6994
|
+
const presentParticiple = action === "pause" ? "Pausing" : action === "resume" ? "Resuming" : "Cancelling";
|
|
6995
|
+
const pastTense = action === "pause" ? "paused" : action === "resume" ? "resumed" : "cancelled";
|
|
6996
|
+
workBrief.command(`${action} <briefId>`).description(`${capitalize(action)} an active work brief run`).requiredOption(
|
|
6997
|
+
"--reason-code <code>",
|
|
6998
|
+
"Stable machine-readable reason code"
|
|
6999
|
+
).requiredOption(
|
|
7000
|
+
"--source <source>",
|
|
7001
|
+
"Calling surface or lane recorded in the audit"
|
|
7002
|
+
).option("--reason <text>", "Optional human-readable reason").action(async (briefId, opts, cmd) => {
|
|
7003
|
+
const globals = cmd.optsWithGlobals();
|
|
7004
|
+
const cfg = resolveConfig(globals);
|
|
7005
|
+
const data = await runApi(`${presentParticiple} work brief`, async () => {
|
|
7006
|
+
const client = await makeClient(cfg);
|
|
7007
|
+
return client.POST("/work-briefs/{id}/lifecycle", {
|
|
7008
|
+
params: { path: { id: briefId } },
|
|
7009
|
+
body: {
|
|
7010
|
+
action,
|
|
7011
|
+
reasonCode: opts.reasonCode,
|
|
7012
|
+
source: opts.source,
|
|
7013
|
+
reasonText: opts.reason ?? null
|
|
7014
|
+
}
|
|
7015
|
+
});
|
|
7016
|
+
});
|
|
7017
|
+
emitAction(
|
|
7018
|
+
`${pastTense} work brief ${style.bold(briefId)} \u2192 ${data.outcome}`,
|
|
7019
|
+
data,
|
|
7020
|
+
globals.json
|
|
7021
|
+
);
|
|
7022
|
+
});
|
|
7023
|
+
}
|
|
7024
|
+
function capitalize(value) {
|
|
7025
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
7026
|
+
}
|
|
7027
|
+
|
|
6877
7028
|
// src/index.ts
|
|
6878
7029
|
function resolveVersion() {
|
|
6879
7030
|
try {
|
|
@@ -7036,6 +7187,7 @@ registerLookup(program);
|
|
|
7036
7187
|
registerRelationships(program);
|
|
7037
7188
|
registerWorkspace(program);
|
|
7038
7189
|
registerProject(program);
|
|
7190
|
+
registerWorkBrief(program);
|
|
7039
7191
|
registerDecomposition(program);
|
|
7040
7192
|
registerExecutor(program);
|
|
7041
7193
|
registerClose(program);
|
package/package.json
CHANGED