@sechroom/cli 2026.7.15 → 2026.7.17
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 +218 -77
- 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
|
|
@@ -1595,6 +1595,12 @@ function installHooksJson(path, commands, dryRun) {
|
|
|
1595
1595
|
function installClaudeCommands(claudeDir, commands, dryRun) {
|
|
1596
1596
|
return installHooksJson(join6(claudeDir, "settings.json"), commands, dryRun);
|
|
1597
1597
|
}
|
|
1598
|
+
function installCodexCommands(codexHome, commands, dryRun) {
|
|
1599
|
+
return [
|
|
1600
|
+
installHooksJson(join6(codexHome, "hooks.json"), commands, dryRun),
|
|
1601
|
+
installCodexFeatureFlag(join6(codexHome, "config.toml"), dryRun)
|
|
1602
|
+
];
|
|
1603
|
+
}
|
|
1598
1604
|
function ensureCodexFeaturesHooks(content) {
|
|
1599
1605
|
const lines = content.split("\n");
|
|
1600
1606
|
const headerIdx = lines.findIndex((l) => l.trim() === "[features]");
|
|
@@ -3141,10 +3147,122 @@ Examples:
|
|
|
3141
3147
|
}
|
|
3142
3148
|
|
|
3143
3149
|
// src/commands/executor.ts
|
|
3150
|
+
import { dirname as dirname8, join as join11 } from "path";
|
|
3151
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
|
|
3152
|
+
var EXECUTOR_STATE = "executor.json";
|
|
3153
|
+
var EXECUTOR_PULSE_COMMAND = "sechroom executor hook-pulse";
|
|
3154
|
+
var EXECUTOR_STOP_COMMAND = "sechroom executor hook-stop";
|
|
3155
|
+
var CLAUDE_EXECUTOR_HOOKS = {
|
|
3156
|
+
SessionStart: EXECUTOR_PULSE_COMMAND,
|
|
3157
|
+
UserPromptSubmit: EXECUTOR_PULSE_COMMAND,
|
|
3158
|
+
PreToolUse: EXECUTOR_PULSE_COMMAND,
|
|
3159
|
+
Stop: EXECUTOR_PULSE_COMMAND,
|
|
3160
|
+
SessionEnd: EXECUTOR_STOP_COMMAND
|
|
3161
|
+
};
|
|
3162
|
+
var CODEX_EXECUTOR_HOOKS = {
|
|
3163
|
+
SessionStart: EXECUTOR_PULSE_COMMAND,
|
|
3164
|
+
UserPromptSubmit: EXECUTOR_PULSE_COMMAND,
|
|
3165
|
+
PreToolUse: EXECUTOR_PULSE_COMMAND,
|
|
3166
|
+
Stop: EXECUTOR_PULSE_COMMAND
|
|
3167
|
+
};
|
|
3144
3168
|
function registerExecutor(program2) {
|
|
3145
3169
|
const executor = program2.command("executor").description(
|
|
3146
3170
|
"Register and operate a local Claude Code/Codex executor advertisement"
|
|
3147
3171
|
);
|
|
3172
|
+
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) => {
|
|
3173
|
+
const globals = cmd.optsWithGlobals();
|
|
3174
|
+
const lane = readSem()?.values["code-lane"];
|
|
3175
|
+
const detected = detectHookSurfaces(process.cwd());
|
|
3176
|
+
let surface = opts.surface;
|
|
3177
|
+
let instanceKey = opts.instanceKey;
|
|
3178
|
+
let runtime = opts.runtime;
|
|
3179
|
+
let connector = opts.connector;
|
|
3180
|
+
let capabilities = opts.capability;
|
|
3181
|
+
const surfaceDefault = detected.length === 1 ? detected[0] : lane?.includes("codex") ? "codex" : "claude";
|
|
3182
|
+
if (!opts.yes && canPrompt()) {
|
|
3183
|
+
surface = await promptText("Harness surface (claude or codex)?", surface ?? surfaceDefault);
|
|
3184
|
+
instanceKey = await promptText("Executor instance key?", instanceKey ?? lane ?? "");
|
|
3185
|
+
runtime = await promptText("Runtime (claude-code or codex)?", runtime ?? (surface === "codex" ? "codex" : "claude-code"));
|
|
3186
|
+
connector = await promptText("Approved local-session connector id?", connector ?? "");
|
|
3187
|
+
const capabilityText = await promptText("Capability keys (comma-separated; blank for none)?", capabilities?.join(",") ?? "");
|
|
3188
|
+
capabilities = capabilityText.split(",").map((x) => x.trim()).filter(Boolean);
|
|
3189
|
+
}
|
|
3190
|
+
surface ??= surfaceDefault;
|
|
3191
|
+
instanceKey ??= lane;
|
|
3192
|
+
runtime ??= surface === "codex" ? "codex" : "claude-code";
|
|
3193
|
+
if (!connector) fail("executor install requires --connector (or an interactive connector id)");
|
|
3194
|
+
if (!instanceKey) fail("no instance key resolved; pass --instance-key or pin .sechroom/lane.json code-lane");
|
|
3195
|
+
if (!opts.yes && !canPrompt()) fail("non-interactive executor install requires --yes");
|
|
3196
|
+
parseRuntimeKind(runtime);
|
|
3197
|
+
if (!["claude", "codex"].includes(surface)) fail("surface must be claude or codex");
|
|
3198
|
+
if (opts.refreshAfter >= opts.ttl) fail("refresh-after must be shorter than the TTL");
|
|
3199
|
+
const sem = readSem();
|
|
3200
|
+
const checkout = sem ? dirname8(dirname8(sem.path)) : process.cwd();
|
|
3201
|
+
const statePath = join11(checkout, ".sechroom", EXECUTOR_STATE);
|
|
3202
|
+
const state = {
|
|
3203
|
+
schemaVersion: 1,
|
|
3204
|
+
instanceKey,
|
|
3205
|
+
runtime: runtime.toLowerCase() === "codex" ? "codex" : "claude-code",
|
|
3206
|
+
connectorId: connector,
|
|
3207
|
+
capabilityKeys: capabilities ?? [],
|
|
3208
|
+
relayId: opts.relay,
|
|
3209
|
+
subscriptionName: opts.subscriptionName,
|
|
3210
|
+
ttlSeconds: opts.ttl,
|
|
3211
|
+
refreshAfterSeconds: opts.refreshAfter
|
|
3212
|
+
};
|
|
3213
|
+
if (!opts.dryRun) {
|
|
3214
|
+
mkdirSync9(dirname8(statePath), { recursive: true });
|
|
3215
|
+
writeFileSync9(statePath, JSON.stringify(state, null, 2) + "\n");
|
|
3216
|
+
ensureStateDirIgnored(checkout);
|
|
3217
|
+
}
|
|
3218
|
+
const configuredClaudeDirs = globals.claudeConfigDir || process.env.CLAUDE_CONFIG_DIR ? resolveClaudeTargets({ override: globals.claudeConfigDir }).map((target) => target.dir) : [join11(checkout, ".claude")];
|
|
3219
|
+
const configuredCodexHomes = globals.codexHome || process.env.CODEX_HOME ? resolveCodexHomes({ override: globals.codexHome }) : [join11(checkout, ".codex")];
|
|
3220
|
+
const hookTargets = surface === "claude" ? configuredClaudeDirs : configuredCodexHomes;
|
|
3221
|
+
for (const target of hookTargets) {
|
|
3222
|
+
const results = surface === "claude" ? [installClaudeCommands(target, CLAUDE_EXECUTOR_HOOKS, opts.dryRun)] : installCodexCommands(target, CODEX_EXECUTOR_HOOKS, opts.dryRun);
|
|
3223
|
+
for (const result of results) process.stderr.write(describe(result, opts.dryRun) + "\n");
|
|
3224
|
+
}
|
|
3225
|
+
warnIfSechroomNotOnPath();
|
|
3226
|
+
process.stderr.write(style.green("executor harness configured") + style.dim(` \u2014 ${instanceKey}
|
|
3227
|
+
`));
|
|
3228
|
+
});
|
|
3229
|
+
executor.command("hook-pulse").description("Hook adapter: register or refresh this checkout's executor").action(async (_opts, cmd) => {
|
|
3230
|
+
await drainStdin();
|
|
3231
|
+
const located = readExecutorState();
|
|
3232
|
+
if (!located) return;
|
|
3233
|
+
const { state, path } = located;
|
|
3234
|
+
const age = state.lastRefreshAt ? Date.now() - Date.parse(state.lastRefreshAt) : Number.POSITIVE_INFINITY;
|
|
3235
|
+
if (state.instanceId && age < state.refreshAfterSeconds * 1e3) return;
|
|
3236
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3237
|
+
try {
|
|
3238
|
+
let data;
|
|
3239
|
+
if (state.instanceId) {
|
|
3240
|
+
try {
|
|
3241
|
+
data = await refresh(cfg, state.instanceId, state.ttlSeconds);
|
|
3242
|
+
} catch {
|
|
3243
|
+
data = await registerInstance(cfg, state);
|
|
3244
|
+
}
|
|
3245
|
+
} else {
|
|
3246
|
+
data = await registerInstance(cfg, state);
|
|
3247
|
+
}
|
|
3248
|
+
state.instanceId = data.id;
|
|
3249
|
+
state.lastRefreshAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3250
|
+
writeFileSync9(path, JSON.stringify(state, null, 2) + "\n");
|
|
3251
|
+
} catch {
|
|
3252
|
+
}
|
|
3253
|
+
});
|
|
3254
|
+
executor.command("hook-stop").description("Hook adapter: deregister this checkout's executor").action(async (_opts, cmd) => {
|
|
3255
|
+
await drainStdin();
|
|
3256
|
+
const located = readExecutorState();
|
|
3257
|
+
if (!located?.state.instanceId) return;
|
|
3258
|
+
try {
|
|
3259
|
+
await api(resolveConfig(cmd.optsWithGlobals()), `/me/executor-instances/${encodeURIComponent(located.state.instanceId)}`, { method: "DELETE", body: JSON.stringify({}) });
|
|
3260
|
+
delete located.state.instanceId;
|
|
3261
|
+
delete located.state.lastRefreshAt;
|
|
3262
|
+
writeFileSync9(located.path, JSON.stringify(located.state, null, 2) + "\n");
|
|
3263
|
+
} catch {
|
|
3264
|
+
}
|
|
3265
|
+
});
|
|
3148
3266
|
executor.command("submit-connector").description(
|
|
3149
3267
|
"Submit a local-session connector definition for governed approval"
|
|
3150
3268
|
).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", [
|
|
@@ -3305,6 +3423,29 @@ async function refresh(cfg, id, ttlSeconds) {
|
|
|
3305
3423
|
}
|
|
3306
3424
|
);
|
|
3307
3425
|
}
|
|
3426
|
+
async function registerInstance(cfg, state) {
|
|
3427
|
+
if (!cfg.workspaceId) fail("executor registration requires a configured workspaceId");
|
|
3428
|
+
const subscription = await api(cfg, "/me/delivery-subscriptions/signalr", {
|
|
3429
|
+
method: "POST",
|
|
3430
|
+
body: JSON.stringify({ name: state.subscriptionName, enabled: true, filter: { tags: ["kind:task"], workspaceScope: [cfg.workspaceId] } })
|
|
3431
|
+
});
|
|
3432
|
+
return api(cfg, "/me/executor-instances", {
|
|
3433
|
+
method: "POST",
|
|
3434
|
+
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 })
|
|
3435
|
+
});
|
|
3436
|
+
}
|
|
3437
|
+
function readExecutorState(start = process.cwd()) {
|
|
3438
|
+
const semPath = resolveSemPathForRead(start);
|
|
3439
|
+
const sem = semPath ? readSem(semPath) : void 0;
|
|
3440
|
+
const path = join11(sem ? dirname8(sem.path) : join11(start, ".sechroom"), EXECUTOR_STATE);
|
|
3441
|
+
if (!existsSync9(path)) return void 0;
|
|
3442
|
+
return { state: JSON.parse(readFileSync8(path, "utf8")), path };
|
|
3443
|
+
}
|
|
3444
|
+
async function drainStdin() {
|
|
3445
|
+
if (process.stdin.isTTY) return;
|
|
3446
|
+
for await (const _chunk of process.stdin) {
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3308
3449
|
async function api(cfg, path, init) {
|
|
3309
3450
|
const token = await requireToken(cfg);
|
|
3310
3451
|
const response = await fetch(`${cfg.baseUrl}${path}`, {
|
|
@@ -3850,8 +3991,8 @@ Examples:
|
|
|
3850
3991
|
|
|
3851
3992
|
// src/setup/apply.ts
|
|
3852
3993
|
import { createHash as createHash3 } from "crypto";
|
|
3853
|
-
import { mkdirSync as
|
|
3854
|
-
import { dirname as
|
|
3994
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync10, existsSync as existsSync10 } from "fs";
|
|
3995
|
+
import { dirname as dirname9 } from "path";
|
|
3855
3996
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
3856
3997
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
3857
3998
|
function normalizeBody(s) {
|
|
@@ -3904,22 +4045,22 @@ function parseManagedBlock(content, block) {
|
|
|
3904
4045
|
return null;
|
|
3905
4046
|
}
|
|
3906
4047
|
function ensureDir2(path) {
|
|
3907
|
-
|
|
4048
|
+
mkdirSync10(dirname9(path), { recursive: true });
|
|
3908
4049
|
}
|
|
3909
4050
|
function readOr(path, fallback) {
|
|
3910
4051
|
try {
|
|
3911
|
-
return
|
|
4052
|
+
return readFileSync9(path, "utf8");
|
|
3912
4053
|
} catch {
|
|
3913
4054
|
return fallback;
|
|
3914
4055
|
}
|
|
3915
4056
|
}
|
|
3916
4057
|
function mergeMcpJson(path, snippet, dryRun) {
|
|
3917
4058
|
const incoming = JSON.parse(snippet);
|
|
3918
|
-
const existed =
|
|
4059
|
+
const existed = existsSync10(path);
|
|
3919
4060
|
let current = {};
|
|
3920
4061
|
if (existed) {
|
|
3921
4062
|
try {
|
|
3922
|
-
current = JSON.parse(
|
|
4063
|
+
current = JSON.parse(readFileSync9(path, "utf8"));
|
|
3923
4064
|
} catch {
|
|
3924
4065
|
return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
|
|
3925
4066
|
}
|
|
@@ -3927,26 +4068,26 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
3927
4068
|
current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
|
|
3928
4069
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3929
4070
|
ensureDir2(path);
|
|
3930
|
-
|
|
4071
|
+
writeFileSync10(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
3931
4072
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3932
4073
|
}
|
|
3933
4074
|
function mergeCodexToml(path, snippet, dryRun) {
|
|
3934
|
-
const existed =
|
|
4075
|
+
const existed = existsSync10(path);
|
|
3935
4076
|
let body = readOr(path, "");
|
|
3936
4077
|
body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
|
|
3937
4078
|
const trimmed = body.trim();
|
|
3938
4079
|
const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
|
|
3939
4080
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3940
4081
|
ensureDir2(path);
|
|
3941
|
-
|
|
4082
|
+
writeFileSync10(path, next, { mode: 384 });
|
|
3942
4083
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3943
4084
|
}
|
|
3944
4085
|
function writeInstructionBlock(path, write, dryRun) {
|
|
3945
|
-
const existed =
|
|
4086
|
+
const existed = existsSync10(path);
|
|
3946
4087
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
3947
4088
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
3948
4089
|
ensureDir2(path);
|
|
3949
|
-
|
|
4090
|
+
writeFileSync10(path, next);
|
|
3950
4091
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
3951
4092
|
}
|
|
3952
4093
|
function computeBlockFile(current, write) {
|
|
@@ -3987,7 +4128,7 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
3987
4128
|
const next = computeBlockFile(current, write);
|
|
3988
4129
|
if (!dryRun) {
|
|
3989
4130
|
ensureDir2(proposedPath);
|
|
3990
|
-
|
|
4131
|
+
writeFileSync10(proposedPath, next);
|
|
3991
4132
|
}
|
|
3992
4133
|
return {
|
|
3993
4134
|
kind: "instruction",
|
|
@@ -4117,8 +4258,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
4117
4258
|
}
|
|
4118
4259
|
|
|
4119
4260
|
// src/setup/skills-offer.ts
|
|
4120
|
-
import { mkdirSync as
|
|
4121
|
-
import { join as
|
|
4261
|
+
import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
|
|
4262
|
+
import { join as join12 } from "path";
|
|
4122
4263
|
|
|
4123
4264
|
// src/setup/lane-pin.ts
|
|
4124
4265
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -4234,8 +4375,8 @@ Found ${summary} available to you for ${surface}.
|
|
|
4234
4375
|
if (skills.length > 0) {
|
|
4235
4376
|
const written = [];
|
|
4236
4377
|
for (const s of skills) {
|
|
4237
|
-
|
|
4238
|
-
|
|
4378
|
+
mkdirSync11(join12(sDir, s.name), { recursive: true });
|
|
4379
|
+
writeFileSync11(join12(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
4239
4380
|
written.push(s.name);
|
|
4240
4381
|
}
|
|
4241
4382
|
recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -4243,11 +4384,11 @@ Found ${summary} available to you for ${surface}.
|
|
|
4243
4384
|
`);
|
|
4244
4385
|
}
|
|
4245
4386
|
if (agents.length > 0) {
|
|
4246
|
-
|
|
4387
|
+
mkdirSync11(aDir, { recursive: true });
|
|
4247
4388
|
const written = [];
|
|
4248
4389
|
for (const a of agents) {
|
|
4249
4390
|
const file = `${a.name}.md`;
|
|
4250
|
-
|
|
4391
|
+
writeFileSync11(join12(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
4251
4392
|
written.push(file);
|
|
4252
4393
|
}
|
|
4253
4394
|
recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -4630,13 +4771,13 @@ Wired to namespace '${slug}'. Restart your AI client (or reload MCP) to pick it
|
|
|
4630
4771
|
}
|
|
4631
4772
|
|
|
4632
4773
|
// src/commands/onboard.ts
|
|
4633
|
-
import { existsSync as
|
|
4634
|
-
import { basename as basename2, join as
|
|
4774
|
+
import { existsSync as existsSync12 } from "fs";
|
|
4775
|
+
import { basename as basename2, join as join14 } from "path";
|
|
4635
4776
|
|
|
4636
4777
|
// src/commands/fanout.ts
|
|
4637
4778
|
import { spawnSync } from "child_process";
|
|
4638
|
-
import { existsSync as
|
|
4639
|
-
import { isAbsolute, join as
|
|
4779
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
4780
|
+
import { isAbsolute, join as join13, resolve } from "path";
|
|
4640
4781
|
var ICON = {
|
|
4641
4782
|
refresh: "\u21BB",
|
|
4642
4783
|
bind: "+",
|
|
@@ -4656,21 +4797,21 @@ function discoverChildren(root) {
|
|
|
4656
4797
|
const out = [];
|
|
4657
4798
|
for (const name of names.sort()) {
|
|
4658
4799
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
4659
|
-
const dir =
|
|
4800
|
+
const dir = join13(root, name);
|
|
4660
4801
|
try {
|
|
4661
4802
|
if (!statSync3(dir).isDirectory()) continue;
|
|
4662
4803
|
} catch {
|
|
4663
4804
|
continue;
|
|
4664
4805
|
}
|
|
4665
|
-
if (
|
|
4806
|
+
if (existsSync11(join13(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
4666
4807
|
}
|
|
4667
4808
|
return out;
|
|
4668
4809
|
}
|
|
4669
4810
|
function readManifest(path) {
|
|
4670
|
-
if (!
|
|
4811
|
+
if (!existsSync11(path)) return null;
|
|
4671
4812
|
let parsed;
|
|
4672
4813
|
try {
|
|
4673
|
-
parsed = JSON.parse(
|
|
4814
|
+
parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
4674
4815
|
} catch (err2) {
|
|
4675
4816
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
4676
4817
|
}
|
|
@@ -5036,10 +5177,10 @@ async function chooseScope(scopeFlag, yes) {
|
|
|
5036
5177
|
}
|
|
5037
5178
|
async function planRecurseChild(entry, root, client, opts) {
|
|
5038
5179
|
const dir = resolveChildDir(entry.path, root);
|
|
5039
|
-
if (!
|
|
5180
|
+
if (!existsSync12(dir)) {
|
|
5040
5181
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
5041
5182
|
}
|
|
5042
|
-
if (
|
|
5183
|
+
if (existsSync12(join14(dir, ".sechroom.json"))) {
|
|
5043
5184
|
return {
|
|
5044
5185
|
label: entry.path,
|
|
5045
5186
|
dir,
|
|
@@ -5112,7 +5253,7 @@ This fan-out will pin the same lane in every repo:
|
|
|
5112
5253
|
async function runRecurse(cfg, g, opts) {
|
|
5113
5254
|
const { yes, dryRun, json } = opts;
|
|
5114
5255
|
const root = process.cwd();
|
|
5115
|
-
const manifestPath =
|
|
5256
|
+
const manifestPath = join14(root, ".sechroom", "repos.json");
|
|
5116
5257
|
const fromManifest = readManifest(manifestPath);
|
|
5117
5258
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
5118
5259
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
@@ -5645,23 +5786,23 @@ Examples:
|
|
|
5645
5786
|
|
|
5646
5787
|
// src/commands/reset.ts
|
|
5647
5788
|
import { homedir as homedir4 } from "os";
|
|
5648
|
-
import { join as
|
|
5649
|
-
import { existsSync as
|
|
5789
|
+
import { join as join15 } from "path";
|
|
5790
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11, rmSync as rmSync3 } from "fs";
|
|
5650
5791
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
5651
|
-
var localSkillsDir = () =>
|
|
5652
|
-
var globalSkillsDir = () =>
|
|
5653
|
-
var localAgentsDir = () =>
|
|
5654
|
-
var globalAgentsDir = () =>
|
|
5792
|
+
var localSkillsDir = () => join15(process.cwd(), ".claude", "skills");
|
|
5793
|
+
var globalSkillsDir = () => join15(homedir4(), ".claude", "skills");
|
|
5794
|
+
var localAgentsDir = () => join15(process.cwd(), ".claude", "agents");
|
|
5795
|
+
var globalAgentsDir = () => join15(homedir4(), ".claude", "agents");
|
|
5655
5796
|
function removeMaterialisedSkills(dir) {
|
|
5656
5797
|
const removed = [];
|
|
5657
|
-
const lockPath =
|
|
5658
|
-
if (!
|
|
5798
|
+
const lockPath = join15(dir, SKILLS_LOCK2);
|
|
5799
|
+
if (!existsSync13(lockPath)) return removed;
|
|
5659
5800
|
try {
|
|
5660
|
-
const lock = JSON.parse(
|
|
5801
|
+
const lock = JSON.parse(readFileSync11(lockPath, "utf8"));
|
|
5661
5802
|
for (const entry of Object.values(lock)) {
|
|
5662
5803
|
for (const name of entry.skills ?? []) {
|
|
5663
|
-
const p =
|
|
5664
|
-
if (
|
|
5804
|
+
const p = join15(dir, name);
|
|
5805
|
+
if (existsSync13(p)) {
|
|
5665
5806
|
rmSync3(p, { recursive: true, force: true });
|
|
5666
5807
|
removed.push(p);
|
|
5667
5808
|
}
|
|
@@ -5706,18 +5847,18 @@ function registerReset(program2) {
|
|
|
5706
5847
|
}
|
|
5707
5848
|
}
|
|
5708
5849
|
const removed = [];
|
|
5709
|
-
const stateDir =
|
|
5710
|
-
if (
|
|
5850
|
+
const stateDir = join15(process.cwd(), ".sechroom");
|
|
5851
|
+
if (existsSync13(stateDir)) {
|
|
5711
5852
|
rmSync3(stateDir, { recursive: true, force: true });
|
|
5712
5853
|
removed.push(stateDir);
|
|
5713
5854
|
}
|
|
5714
|
-
const legacyCfg =
|
|
5715
|
-
if (
|
|
5855
|
+
const legacyCfg = join15(process.cwd(), ".sechroom.json");
|
|
5856
|
+
if (existsSync13(legacyCfg)) {
|
|
5716
5857
|
rmSync3(legacyCfg, { force: true });
|
|
5717
5858
|
removed.push(legacyCfg);
|
|
5718
5859
|
}
|
|
5719
|
-
const legacySem =
|
|
5720
|
-
if (
|
|
5860
|
+
const legacySem = join15(process.cwd(), ".sem");
|
|
5861
|
+
if (existsSync13(legacySem)) {
|
|
5721
5862
|
rmSync3(legacySem, { force: true });
|
|
5722
5863
|
removed.push(legacySem);
|
|
5723
5864
|
}
|
|
@@ -5743,8 +5884,8 @@ function registerReset(program2) {
|
|
|
5743
5884
|
}
|
|
5744
5885
|
|
|
5745
5886
|
// src/commands/skills.ts
|
|
5746
|
-
import { existsSync as
|
|
5747
|
-
import { join as
|
|
5887
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync12, statSync as statSync4, writeFileSync as writeFileSync12 } from "fs";
|
|
5888
|
+
import { join as join16 } from "path";
|
|
5748
5889
|
function filenameFromDisposition(header) {
|
|
5749
5890
|
if (!header) return void 0;
|
|
5750
5891
|
const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
|
|
@@ -5752,11 +5893,11 @@ function filenameFromDisposition(header) {
|
|
|
5752
5893
|
}
|
|
5753
5894
|
function resolveOutputPath(output, serverFilename) {
|
|
5754
5895
|
const filename = serverFilename || "skills.zip";
|
|
5755
|
-
if (!output) return
|
|
5756
|
-
const looksLikeDir = output.endsWith("/") ||
|
|
5896
|
+
if (!output) return join16(process.cwd(), filename);
|
|
5897
|
+
const looksLikeDir = output.endsWith("/") || existsSync14(output) && statSync4(output).isDirectory();
|
|
5757
5898
|
if (looksLikeDir) {
|
|
5758
|
-
|
|
5759
|
-
return
|
|
5899
|
+
mkdirSync12(output, { recursive: true });
|
|
5900
|
+
return join16(output, filename);
|
|
5760
5901
|
}
|
|
5761
5902
|
return output;
|
|
5762
5903
|
}
|
|
@@ -5787,7 +5928,7 @@ async function downloadZip(label, call, output) {
|
|
|
5787
5928
|
const buf = Buffer.from(res.data);
|
|
5788
5929
|
const filename = filenameFromDisposition(res.response.headers.get("content-disposition")) ?? "skills.zip";
|
|
5789
5930
|
const path = resolveOutputPath(output, filename);
|
|
5790
|
-
|
|
5931
|
+
writeFileSync12(path, buf);
|
|
5791
5932
|
return { path, bytes: buf.length, filename };
|
|
5792
5933
|
}
|
|
5793
5934
|
function registerSkills(program2) {
|
|
@@ -5961,12 +6102,12 @@ clients select all; an unconfigured machine preserves the legacy Claude default.
|
|
|
5961
6102
|
}
|
|
5962
6103
|
|
|
5963
6104
|
// src/commands/sweep.ts
|
|
5964
|
-
import { existsSync as
|
|
5965
|
-
import { dirname as
|
|
5966
|
-
var DEFAULT_MANIFEST =
|
|
6105
|
+
import { existsSync as existsSync15 } from "fs";
|
|
6106
|
+
import { dirname as dirname10, join as join17, resolve as resolve2 } from "path";
|
|
6107
|
+
var DEFAULT_MANIFEST = join17(".sechroom", "repos.json");
|
|
5967
6108
|
function planEntry(entry, root) {
|
|
5968
6109
|
const dir = resolveChildDir(entry.path, root);
|
|
5969
|
-
if (!
|
|
6110
|
+
if (!existsSync15(dir)) {
|
|
5970
6111
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
5971
6112
|
}
|
|
5972
6113
|
if (committedBindingPath(dir)) {
|
|
@@ -6042,7 +6183,7 @@ Examples:
|
|
|
6042
6183
|
`);
|
|
6043
6184
|
return;
|
|
6044
6185
|
}
|
|
6045
|
-
const root =
|
|
6186
|
+
const root = dirname10(dirname10(manifestPath));
|
|
6046
6187
|
const plans = repos.map((entry) => planEntry(entry, root));
|
|
6047
6188
|
if (!json) {
|
|
6048
6189
|
process.stderr.write(
|
|
@@ -6061,13 +6202,13 @@ Examples:
|
|
|
6061
6202
|
|
|
6062
6203
|
// src/commands/telemetry.ts
|
|
6063
6204
|
import {
|
|
6064
|
-
existsSync as
|
|
6065
|
-
mkdirSync as
|
|
6066
|
-
readFileSync as
|
|
6205
|
+
existsSync as existsSync16,
|
|
6206
|
+
mkdirSync as mkdirSync13,
|
|
6207
|
+
readFileSync as readFileSync12,
|
|
6067
6208
|
rmSync as rmSync4,
|
|
6068
|
-
writeFileSync as
|
|
6209
|
+
writeFileSync as writeFileSync13
|
|
6069
6210
|
} from "fs";
|
|
6070
|
-
import { dirname as
|
|
6211
|
+
import { dirname as dirname11, join as join18 } from "path";
|
|
6071
6212
|
function registerTelemetry(program2) {
|
|
6072
6213
|
const telemetry = program2.command("telemetry").description(
|
|
6073
6214
|
"Emit WLP run telemetry (an executor leg's progress events) into a decomposition run"
|
|
@@ -6137,14 +6278,14 @@ function registerTelemetry(program2) {
|
|
|
6137
6278
|
"Decomposition id this session executes"
|
|
6138
6279
|
).requiredOption("--task <id>", "Task id this session executes").action((opts, cmd) => {
|
|
6139
6280
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
6140
|
-
const dir =
|
|
6141
|
-
|
|
6142
|
-
const path =
|
|
6281
|
+
const dir = join18(process.cwd(), ".sechroom");
|
|
6282
|
+
mkdirSync13(dir, { recursive: true });
|
|
6283
|
+
const path = join18(dir, BINDING_FILE);
|
|
6143
6284
|
const binding = {
|
|
6144
6285
|
decompositionId: opts.decomposition,
|
|
6145
6286
|
taskId: opts.task
|
|
6146
6287
|
};
|
|
6147
|
-
|
|
6288
|
+
writeFileSync13(path, JSON.stringify(binding, null, 2) + "\n");
|
|
6148
6289
|
ensureStateDirIgnored(process.cwd());
|
|
6149
6290
|
if (json) {
|
|
6150
6291
|
emit({ bound: true, ...binding, path }, true);
|
|
@@ -6159,8 +6300,8 @@ function registerTelemetry(program2) {
|
|
|
6159
6300
|
});
|
|
6160
6301
|
telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
|
|
6161
6302
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
6162
|
-
const path =
|
|
6163
|
-
const existed =
|
|
6303
|
+
const path = join18(process.cwd(), ".sechroom", BINDING_FILE);
|
|
6304
|
+
const existed = existsSync16(path);
|
|
6164
6305
|
if (existed) rmSync4(path);
|
|
6165
6306
|
if (json) emit({ unbound: existed, path }, true);
|
|
6166
6307
|
else
|
|
@@ -6268,11 +6409,11 @@ async function postTelemetry(cfg, decompositionId, events) {
|
|
|
6268
6409
|
function findBinding(start) {
|
|
6269
6410
|
let dir = start;
|
|
6270
6411
|
for (; ; ) {
|
|
6271
|
-
const path =
|
|
6272
|
-
if (
|
|
6412
|
+
const path = join18(dir, ".sechroom", BINDING_FILE);
|
|
6413
|
+
if (existsSync16(path)) {
|
|
6273
6414
|
try {
|
|
6274
6415
|
const b = JSON.parse(
|
|
6275
|
-
|
|
6416
|
+
readFileSync12(path, "utf8")
|
|
6276
6417
|
);
|
|
6277
6418
|
if (b.decompositionId && b.taskId)
|
|
6278
6419
|
return { decompositionId: b.decompositionId, taskId: b.taskId };
|
|
@@ -6280,18 +6421,18 @@ function findBinding(start) {
|
|
|
6280
6421
|
}
|
|
6281
6422
|
return null;
|
|
6282
6423
|
}
|
|
6283
|
-
const parent =
|
|
6424
|
+
const parent = dirname11(dir);
|
|
6284
6425
|
if (parent === dir) return null;
|
|
6285
6426
|
dir = parent;
|
|
6286
6427
|
}
|
|
6287
6428
|
}
|
|
6288
6429
|
function parseTranscript(path) {
|
|
6289
|
-
if (!
|
|
6430
|
+
if (!existsSync16(path)) return null;
|
|
6290
6431
|
let tokensIn = 0;
|
|
6291
6432
|
let tokensOut = 0;
|
|
6292
6433
|
let contextUsed = 0;
|
|
6293
6434
|
let model = "";
|
|
6294
|
-
for (const line of
|
|
6435
|
+
for (const line of readFileSync12(path, "utf8").split("\n")) {
|
|
6295
6436
|
if (!line.trim()) continue;
|
|
6296
6437
|
let obj;
|
|
6297
6438
|
try {
|
|
@@ -6598,7 +6739,7 @@ Examples:
|
|
|
6598
6739
|
function resolveVersion() {
|
|
6599
6740
|
try {
|
|
6600
6741
|
const pkg = JSON.parse(
|
|
6601
|
-
|
|
6742
|
+
readFileSync13(new URL("../package.json", import.meta.url), "utf8")
|
|
6602
6743
|
);
|
|
6603
6744
|
return pkg.version ?? "0.0.0";
|
|
6604
6745
|
} catch {
|
package/package.json
CHANGED