@sechroom/cli 2026.7.14 → 2026.7.15-rc.dc284b75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -19
- package/dist/index.js +1125 -806
- 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,22 +1502,24 @@ 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
|
|
1457
|
-
import { existsSync as
|
|
1458
|
-
import { dirname as
|
|
1521
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
1522
|
+
import { dirname as dirname6, join as join9 } from "path";
|
|
1459
1523
|
import {
|
|
1460
1524
|
HttpTransportType,
|
|
1461
1525
|
HubConnectionBuilder
|
|
@@ -1463,52 +1527,218 @@ import {
|
|
|
1463
1527
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1464
1528
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1465
1529
|
|
|
1530
|
+
// src/commands/executor.ts
|
|
1531
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
1532
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
1533
|
+
|
|
1534
|
+
// src/sem.ts
|
|
1535
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
1536
|
+
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync4 } from "fs";
|
|
1537
|
+
var SEM_FILE = join5(".sechroom", "lane.json");
|
|
1538
|
+
var STATE_DIR_NAME2 = ".sechroom";
|
|
1539
|
+
function localSemPath(cwd = process.cwd()) {
|
|
1540
|
+
return join5(cwd, SEM_FILE);
|
|
1541
|
+
}
|
|
1542
|
+
function resolveSemPathForRead(start = process.cwd()) {
|
|
1543
|
+
let dir = start;
|
|
1544
|
+
while (true) {
|
|
1545
|
+
const candidate = join5(dir, SEM_FILE);
|
|
1546
|
+
if (existsSync4(candidate)) return candidate;
|
|
1547
|
+
const parent = dirname2(dir);
|
|
1548
|
+
if (parent === dir) return void 0;
|
|
1549
|
+
dir = parent;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
|
|
1553
|
+
try {
|
|
1554
|
+
let dir = start;
|
|
1555
|
+
let gitPath;
|
|
1556
|
+
for (; ; ) {
|
|
1557
|
+
const candidate = join5(dir, ".git");
|
|
1558
|
+
if (existsSync4(candidate)) {
|
|
1559
|
+
gitPath = candidate;
|
|
1560
|
+
break;
|
|
1561
|
+
}
|
|
1562
|
+
const parent = dirname2(dir);
|
|
1563
|
+
if (parent === dir) break;
|
|
1564
|
+
dir = parent;
|
|
1565
|
+
}
|
|
1566
|
+
if (!gitPath || statSync(gitPath).isDirectory()) return lane;
|
|
1567
|
+
const gitFile = readFileSync3(gitPath, "utf8");
|
|
1568
|
+
const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
|
|
1569
|
+
if (!common) return lane;
|
|
1570
|
+
const worktreesDir = join5(common[1], "worktrees");
|
|
1571
|
+
const siblings = readdirSync(worktreesDir).filter((n) => {
|
|
1572
|
+
try {
|
|
1573
|
+
return statSync(join5(worktreesDir, n)).isDirectory();
|
|
1574
|
+
} catch {
|
|
1575
|
+
return false;
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
return laneWithWorktreeSuffix(lane, gitFile, siblings);
|
|
1579
|
+
} catch {
|
|
1580
|
+
return lane;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
function laneWithWorktreeSuffix(lane, gitFile, siblings) {
|
|
1584
|
+
const m = gitFile.trim().match(/\/worktrees\/([^/\s]+)\s*$/);
|
|
1585
|
+
if (!m) return lane;
|
|
1586
|
+
const idx = [...siblings].sort().indexOf(m[1]);
|
|
1587
|
+
return idx < 0 ? lane : `${lane}-${idx + 2}`;
|
|
1588
|
+
}
|
|
1589
|
+
function serializeSem(values) {
|
|
1590
|
+
return JSON.stringify(values, null, 2) + "\n";
|
|
1591
|
+
}
|
|
1592
|
+
function readSem(path) {
|
|
1593
|
+
const p = path ?? resolveSemPathForRead();
|
|
1594
|
+
if (!p || !existsSync4(p)) return void 0;
|
|
1595
|
+
return { path: p, values: parseLaneJson(readFileSync3(p, "utf8")) };
|
|
1596
|
+
}
|
|
1597
|
+
function readLocalSemValues(cwd = process.cwd()) {
|
|
1598
|
+
const next = join5(cwd, SEM_FILE);
|
|
1599
|
+
if (existsSync4(next)) return readSem(next)?.values ?? {};
|
|
1600
|
+
return {};
|
|
1601
|
+
}
|
|
1602
|
+
function parseLaneJson(text2) {
|
|
1603
|
+
try {
|
|
1604
|
+
const parsed = JSON.parse(text2);
|
|
1605
|
+
const out = {};
|
|
1606
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
1607
|
+
if (typeof v === "string") out[k] = v;
|
|
1608
|
+
}
|
|
1609
|
+
return out;
|
|
1610
|
+
} catch {
|
|
1611
|
+
return {};
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
var STATE_DIR_IGNORE = `${STATE_DIR_NAME2}/`;
|
|
1615
|
+
function writeSem(values, path = localSemPath()) {
|
|
1616
|
+
mkdirSync4(dirname2(path), { recursive: true });
|
|
1617
|
+
writeFileSync4(path, serializeSem(values));
|
|
1618
|
+
ensureSemIgnored(path);
|
|
1619
|
+
ensureContinuityScaffold(path);
|
|
1620
|
+
return path;
|
|
1621
|
+
}
|
|
1622
|
+
function ensureStateDirIgnored(cwd = process.cwd()) {
|
|
1623
|
+
ensureSemIgnored(localSemPath(cwd));
|
|
1624
|
+
}
|
|
1625
|
+
var CONTINUITY_FILE_NAME = "continuity.json";
|
|
1626
|
+
var CONTINUITY_SCAFFOLD = JSON.stringify(
|
|
1627
|
+
{
|
|
1628
|
+
_readme: "Agent-maintained continuity intent. Keep these current during the session; `sechroom checkpoint` and the PreCompact hook snapshot from here. The five required fields (objective, state, lastAction, nextAction, resumeInstruction) must all be non-empty for a snapshot to be created.",
|
|
1629
|
+
objective: "",
|
|
1630
|
+
state: "",
|
|
1631
|
+
lastAction: "",
|
|
1632
|
+
nextAction: "",
|
|
1633
|
+
resumeInstruction: "",
|
|
1634
|
+
constraints: [],
|
|
1635
|
+
questions: [],
|
|
1636
|
+
artifacts: [],
|
|
1637
|
+
confidence: null
|
|
1638
|
+
},
|
|
1639
|
+
null,
|
|
1640
|
+
2
|
|
1641
|
+
) + "\n";
|
|
1642
|
+
function ensureContinuityScaffold(semPath) {
|
|
1643
|
+
try {
|
|
1644
|
+
const target = join5(dirname2(semPath), CONTINUITY_FILE_NAME);
|
|
1645
|
+
if (existsSync4(target)) return;
|
|
1646
|
+
writeFileSync4(target, CONTINUITY_SCAFFOLD);
|
|
1647
|
+
} catch {
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
function ignoresSem(content) {
|
|
1651
|
+
return content.split("\n").some((line) => {
|
|
1652
|
+
const t = line.trim();
|
|
1653
|
+
return t === STATE_DIR_NAME2 || t === STATE_DIR_IGNORE || t === `/${STATE_DIR_NAME2}` || t === `/${STATE_DIR_IGNORE}` || t === `**/${STATE_DIR_NAME2}` || t === `**/${STATE_DIR_IGNORE}`;
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
function inGitRepo(startDir) {
|
|
1657
|
+
let dir = startDir;
|
|
1658
|
+
for (; ; ) {
|
|
1659
|
+
if (existsSync4(join5(dir, ".git"))) return true;
|
|
1660
|
+
const parent = dirname2(dir);
|
|
1661
|
+
if (parent === dir) return false;
|
|
1662
|
+
dir = parent;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
function resolveGitignoreTarget(startDir) {
|
|
1666
|
+
let dir = startDir;
|
|
1667
|
+
for (; ; ) {
|
|
1668
|
+
const gi = join5(dir, ".gitignore");
|
|
1669
|
+
if (existsSync4(gi)) return { path: gi, exists: true };
|
|
1670
|
+
const parent = dirname2(dir);
|
|
1671
|
+
if (existsSync4(join5(dir, ".git")) || parent === dir) {
|
|
1672
|
+
return { path: join5(startDir, ".gitignore"), exists: false };
|
|
1673
|
+
}
|
|
1674
|
+
dir = parent;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
function ensureSemIgnored(semPath) {
|
|
1678
|
+
try {
|
|
1679
|
+
const checkoutDir = dirname2(dirname2(semPath));
|
|
1680
|
+
if (!inGitRepo(checkoutDir)) return;
|
|
1681
|
+
const target = resolveGitignoreTarget(checkoutDir);
|
|
1682
|
+
if (target.exists) {
|
|
1683
|
+
const content = readFileSync3(target.path, "utf8");
|
|
1684
|
+
if (ignoresSem(content)) return;
|
|
1685
|
+
const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
1686
|
+
appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
|
|
1687
|
+
`);
|
|
1688
|
+
} else {
|
|
1689
|
+
writeFileSync4(target.path, `${STATE_DIR_IGNORE}
|
|
1690
|
+
`);
|
|
1691
|
+
}
|
|
1692
|
+
} catch {
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1466
1696
|
// src/commands/hook-install.ts
|
|
1467
|
-
import { existsSync as
|
|
1468
|
-
import { delimiter, dirname as
|
|
1697
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
1698
|
+
import { delimiter, dirname as dirname4, join as join7 } from "path";
|
|
1469
1699
|
|
|
1470
1700
|
// src/setup/clients.ts
|
|
1471
|
-
import { existsSync as
|
|
1701
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1472
1702
|
import { homedir as homedir3 } from "os";
|
|
1473
|
-
import { dirname as
|
|
1703
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
1474
1704
|
function claudeDesktopConfigPath(home) {
|
|
1475
1705
|
switch (process.platform) {
|
|
1476
1706
|
case "darwin":
|
|
1477
|
-
return
|
|
1707
|
+
return join6(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
1478
1708
|
case "win32":
|
|
1479
|
-
return
|
|
1709
|
+
return join6(process.env.APPDATA ?? join6(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
1480
1710
|
default:
|
|
1481
|
-
return
|
|
1711
|
+
return join6(home, ".config", "Claude", "claude_desktop_config.json");
|
|
1482
1712
|
}
|
|
1483
1713
|
}
|
|
1484
1714
|
function clientTargets(cwd, opts = {}) {
|
|
1485
1715
|
const home = homedir3();
|
|
1486
|
-
const claudeDir = opts.claudeDir ??
|
|
1487
|
-
const codexHome = opts.codexHome ??
|
|
1716
|
+
const claudeDir = opts.claudeDir ?? join6(home, ".claude");
|
|
1717
|
+
const codexHome = opts.codexHome ?? join6(home, ".codex");
|
|
1488
1718
|
return {
|
|
1489
1719
|
"claude-code": {
|
|
1490
1720
|
key: "claude-code",
|
|
1491
1721
|
label: "Claude Code",
|
|
1492
|
-
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path:
|
|
1493
|
-
instruction: { surfaceKey: "claude-code", path:
|
|
1722
|
+
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join6(cwd, ".mcp.json"), format: "json" },
|
|
1723
|
+
instruction: { surfaceKey: "claude-code", path: join6(cwd, "CLAUDE.md") }
|
|
1494
1724
|
},
|
|
1495
1725
|
"claude-desktop": {
|
|
1496
1726
|
key: "claude-desktop",
|
|
1497
1727
|
label: "Claude Desktop",
|
|
1498
1728
|
mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
|
|
1499
|
-
instruction: { surfaceKey: "claude-desktop", path:
|
|
1729
|
+
instruction: { surfaceKey: "claude-desktop", path: join6(claudeDir, "CLAUDE.md") }
|
|
1500
1730
|
},
|
|
1501
1731
|
codex: {
|
|
1502
1732
|
key: "codex",
|
|
1503
1733
|
label: "Codex CLI",
|
|
1504
|
-
mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path:
|
|
1505
|
-
instruction: { surfaceKey: "chatgpt", path:
|
|
1734
|
+
mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join6(codexHome, "config.toml"), format: "toml" },
|
|
1735
|
+
instruction: { surfaceKey: "chatgpt", path: join6(cwd, "AGENTS.md") }
|
|
1506
1736
|
},
|
|
1507
1737
|
cursor: {
|
|
1508
1738
|
key: "cursor",
|
|
1509
1739
|
label: "Cursor",
|
|
1510
|
-
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path:
|
|
1511
|
-
instruction: { surfaceKey: "chatgpt", path:
|
|
1740
|
+
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join6(cwd, ".cursor", "mcp.json"), format: "json" },
|
|
1741
|
+
instruction: { surfaceKey: "chatgpt", path: join6(cwd, "AGENTS.md") }
|
|
1512
1742
|
},
|
|
1513
1743
|
antigravity: {
|
|
1514
1744
|
key: "antigravity",
|
|
@@ -1519,8 +1749,8 @@ function clientTargets(cwd, opts = {}) {
|
|
|
1519
1749
|
// `type` — comes from the `antigravity` server surface, so we don't
|
|
1520
1750
|
// hardcode it here. Instructions go in the project `AGENTS.md`
|
|
1521
1751
|
// (cross-tool, shared with Codex/Cursor).
|
|
1522
|
-
mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path:
|
|
1523
|
-
instruction: { surfaceKey: "antigravity", path:
|
|
1752
|
+
mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path: join6(home, ".gemini", "config", "mcp_config.json"), format: "json" },
|
|
1753
|
+
instruction: { surfaceKey: "antigravity", path: join6(cwd, "AGENTS.md") }
|
|
1524
1754
|
}
|
|
1525
1755
|
};
|
|
1526
1756
|
}
|
|
@@ -1529,11 +1759,11 @@ var DEFAULT_CLIENT_KEY = "claude-code";
|
|
|
1529
1759
|
function detectInstalledClients(cwd) {
|
|
1530
1760
|
const home = homedir3();
|
|
1531
1761
|
const detected = [];
|
|
1532
|
-
if (resolveClaudeTargets({}).some((t) =>
|
|
1533
|
-
if (
|
|
1534
|
-
if (resolveCodexHomes({}).some((d) =>
|
|
1535
|
-
if (
|
|
1536
|
-
if (
|
|
1762
|
+
if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir))) detected.push("claude-code");
|
|
1763
|
+
if (existsSync5(dirname3(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
|
|
1764
|
+
if (resolveCodexHomes({}).some((d) => existsSync5(d))) detected.push("codex");
|
|
1765
|
+
if (existsSync5(join6(home, ".cursor")) || existsSync5(join6(cwd, ".cursor"))) detected.push("cursor");
|
|
1766
|
+
if (existsSync5(join6(home, ".gemini"))) detected.push("antigravity");
|
|
1537
1767
|
return detected;
|
|
1538
1768
|
}
|
|
1539
1769
|
|
|
@@ -1576,24 +1806,30 @@ function mergeHooks(config2, commands) {
|
|
|
1576
1806
|
return added;
|
|
1577
1807
|
}
|
|
1578
1808
|
function readJsonConfig2(path) {
|
|
1579
|
-
if (!
|
|
1580
|
-
const raw =
|
|
1809
|
+
if (!existsSync6(path)) return {};
|
|
1810
|
+
const raw = readFileSync4(path, "utf8");
|
|
1581
1811
|
if (!raw.trim()) return {};
|
|
1582
1812
|
return JSON.parse(raw);
|
|
1583
1813
|
}
|
|
1584
1814
|
function installHooksJson(path, commands, dryRun) {
|
|
1585
|
-
const existed =
|
|
1815
|
+
const existed = existsSync6(path) && readFileSync4(path, "utf8").trim().length > 0;
|
|
1586
1816
|
const config2 = readJsonConfig2(path);
|
|
1587
1817
|
const added = mergeHooks(config2, commands);
|
|
1588
1818
|
if (added === 0 && existed) return { path, status: "current" };
|
|
1589
1819
|
if (!dryRun) {
|
|
1590
|
-
|
|
1591
|
-
|
|
1820
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
1821
|
+
writeFileSync5(path, JSON.stringify(config2, null, 2) + "\n");
|
|
1592
1822
|
}
|
|
1593
1823
|
return { path, status: existed ? "merged" : "created" };
|
|
1594
1824
|
}
|
|
1595
1825
|
function installClaudeCommands(claudeDir, commands, dryRun) {
|
|
1596
|
-
return installHooksJson(
|
|
1826
|
+
return installHooksJson(join7(claudeDir, "settings.json"), commands, dryRun);
|
|
1827
|
+
}
|
|
1828
|
+
function installCodexCommands(codexHome, commands, dryRun) {
|
|
1829
|
+
return [
|
|
1830
|
+
installHooksJson(join7(codexHome, "hooks.json"), commands, dryRun),
|
|
1831
|
+
installCodexFeatureFlag(join7(codexHome, "config.toml"), dryRun)
|
|
1832
|
+
];
|
|
1597
1833
|
}
|
|
1598
1834
|
function ensureCodexFeaturesHooks(content) {
|
|
1599
1835
|
const lines = content.split("\n");
|
|
@@ -1616,13 +1852,13 @@ function ensureCodexFeaturesHooks(content) {
|
|
|
1616
1852
|
return { next: lines.join("\n"), changed: true };
|
|
1617
1853
|
}
|
|
1618
1854
|
function installCodexFeatureFlag(path, dryRun) {
|
|
1619
|
-
const existed =
|
|
1620
|
-
const content = existed ?
|
|
1855
|
+
const existed = existsSync6(path);
|
|
1856
|
+
const content = existed ? readFileSync4(path, "utf8") : "";
|
|
1621
1857
|
const { next, changed } = ensureCodexFeaturesHooks(content);
|
|
1622
1858
|
if (!changed) return { path, status: "current" };
|
|
1623
1859
|
if (!dryRun) {
|
|
1624
|
-
|
|
1625
|
-
|
|
1860
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
1861
|
+
writeFileSync5(path, next);
|
|
1626
1862
|
}
|
|
1627
1863
|
return { path, status: existed ? "merged" : "created" };
|
|
1628
1864
|
}
|
|
@@ -1647,11 +1883,11 @@ function installHookSurfaces(surfaces, opts) {
|
|
|
1647
1883
|
const out = [];
|
|
1648
1884
|
for (const surface of surfaces) {
|
|
1649
1885
|
if (surface === "claude") {
|
|
1650
|
-
const path =
|
|
1886
|
+
const path = join7(opts.claudeDir, "settings.json");
|
|
1651
1887
|
out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
|
|
1652
1888
|
} else {
|
|
1653
|
-
const hooksJson = installHooksJson(
|
|
1654
|
-
const featureFlag = installCodexFeatureFlag(
|
|
1889
|
+
const hooksJson = installHooksJson(join7(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
|
|
1890
|
+
const featureFlag = installCodexFeatureFlag(join7(opts.codexHome, "config.toml"), opts.dryRun);
|
|
1655
1891
|
out.push({ surface, results: [hooksJson, featureFlag] });
|
|
1656
1892
|
}
|
|
1657
1893
|
}
|
|
@@ -1671,7 +1907,7 @@ function isSechroomOnPath() {
|
|
|
1671
1907
|
for (const dir of pathEnv.split(delimiter)) {
|
|
1672
1908
|
if (!dir) continue;
|
|
1673
1909
|
for (const name of names) {
|
|
1674
|
-
if (
|
|
1910
|
+
if (existsSync6(join7(dir, name))) return true;
|
|
1675
1911
|
}
|
|
1676
1912
|
}
|
|
1677
1913
|
return false;
|
|
@@ -1684,119 +1920,593 @@ function warnIfSechroomNotOnPath(write = (s) => void process.stderr.write(s)) {
|
|
|
1684
1920
|
return true;
|
|
1685
1921
|
}
|
|
1686
1922
|
|
|
1687
|
-
// src/commands/
|
|
1688
|
-
function
|
|
1689
|
-
|
|
1690
|
-
|
|
1923
|
+
// src/commands/executor.ts
|
|
1924
|
+
function executorSubscriptionInput(name) {
|
|
1925
|
+
return {
|
|
1926
|
+
name,
|
|
1927
|
+
enabled: true,
|
|
1928
|
+
filter: { tags: ["kind:task"], workspaceScope: [] }
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
function executorRegistrationInput(state, deliverySubscriptionId) {
|
|
1932
|
+
return {
|
|
1933
|
+
relayId: state.relayId,
|
|
1934
|
+
instanceKey: state.instanceKey,
|
|
1935
|
+
laneId: state.laneId ?? state.instanceKey,
|
|
1936
|
+
runtimeKind: parseRuntimeKind(state.runtime),
|
|
1937
|
+
activationMode: "Attached",
|
|
1938
|
+
deliverySubscriptionId,
|
|
1939
|
+
connectorId: state.connectorId,
|
|
1940
|
+
claimedCapabilityKeys: state.capabilityKeys,
|
|
1941
|
+
toolSetRef: null,
|
|
1942
|
+
ttlSeconds: state.ttlSeconds
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
var EXECUTOR_STATE = "executor.json";
|
|
1946
|
+
var EXECUTOR_PULSE_COMMAND = "sechroom executor hook-pulse";
|
|
1947
|
+
var EXECUTOR_STOP_COMMAND = "sechroom executor hook-stop";
|
|
1948
|
+
var CLAUDE_EXECUTOR_HOOKS = {
|
|
1949
|
+
SessionStart: EXECUTOR_PULSE_COMMAND,
|
|
1950
|
+
UserPromptSubmit: EXECUTOR_PULSE_COMMAND,
|
|
1951
|
+
PreToolUse: EXECUTOR_PULSE_COMMAND,
|
|
1952
|
+
Stop: EXECUTOR_PULSE_COMMAND,
|
|
1953
|
+
SessionEnd: EXECUTOR_STOP_COMMAND
|
|
1954
|
+
};
|
|
1955
|
+
var CODEX_EXECUTOR_HOOKS = {
|
|
1956
|
+
SessionStart: EXECUTOR_PULSE_COMMAND,
|
|
1957
|
+
UserPromptSubmit: EXECUTOR_PULSE_COMMAND,
|
|
1958
|
+
PreToolUse: EXECUTOR_PULSE_COMMAND,
|
|
1959
|
+
Stop: EXECUTOR_PULSE_COMMAND
|
|
1960
|
+
};
|
|
1961
|
+
function registerExecutor(program2) {
|
|
1962
|
+
const executor = program2.command("executor").description(
|
|
1963
|
+
"Register and operate a local Claude Code/Codex executor advertisement"
|
|
1691
1964
|
);
|
|
1692
|
-
|
|
1693
|
-
"
|
|
1694
|
-
|
|
1695
|
-
"
|
|
1965
|
+
executor.command("install").description(
|
|
1966
|
+
"Configure this checkout's harness to advertise itself as a WLP executor"
|
|
1967
|
+
).option("--connector <id>", "Approved local-session ConnectorDefinition id").option(
|
|
1968
|
+
"--instance-key <key>",
|
|
1969
|
+
"Stable executor identity (defaults to .sechroom/lane.json code-lane)"
|
|
1696
1970
|
).option(
|
|
1697
|
-
"--
|
|
1698
|
-
"
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
"
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
);
|
|
1721
|
-
const
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1971
|
+
"--lane-id <lane>",
|
|
1972
|
+
"Canonical affinity lane (defaults to .sechroom/lane.json code-lane)"
|
|
1973
|
+
).option("--runtime <kind>", "claude-code | codex").option("--surface <surface>", "claude | codex").option(
|
|
1974
|
+
"--capability <key...>",
|
|
1975
|
+
"Capability operation keys claimed by this instance"
|
|
1976
|
+
).option(
|
|
1977
|
+
"--relay <id>",
|
|
1978
|
+
"Relay identity shared by sibling instances",
|
|
1979
|
+
"sechroom-cli-local"
|
|
1980
|
+
).option(
|
|
1981
|
+
"--subscription-name <name>",
|
|
1982
|
+
"SignalR delivery binding name",
|
|
1983
|
+
"executor-dispatch"
|
|
1984
|
+
).option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 600).option(
|
|
1985
|
+
"--refresh-after <seconds>",
|
|
1986
|
+
"Minimum age before a hook refreshes",
|
|
1987
|
+
parseInteger,
|
|
1988
|
+
40
|
|
1989
|
+
).option(
|
|
1990
|
+
"-y, --yes",
|
|
1991
|
+
"Non-interactive: accept detected surface and lane defaults",
|
|
1992
|
+
false
|
|
1993
|
+
).option("--dry-run", "Show hook files without writing", false).action(async (opts, cmd) => {
|
|
1994
|
+
const globals = cmd.optsWithGlobals();
|
|
1995
|
+
const lane = readSem()?.values["code-lane"];
|
|
1996
|
+
const detected = detectHookSurfaces(process.cwd());
|
|
1997
|
+
let surface = opts.surface;
|
|
1998
|
+
let instanceKey = opts.instanceKey;
|
|
1999
|
+
let runtime = opts.runtime;
|
|
2000
|
+
let laneId = opts.laneId;
|
|
2001
|
+
let connector = opts.connector;
|
|
2002
|
+
let capabilities = opts.capability;
|
|
2003
|
+
const surfaceDefault = detected.length === 1 ? detected[0] : lane?.includes("codex") ? "codex" : "claude";
|
|
2004
|
+
if (!opts.yes && canPrompt()) {
|
|
2005
|
+
surface = await promptText(
|
|
2006
|
+
"Harness surface (claude or codex)?",
|
|
2007
|
+
surface ?? surfaceDefault
|
|
1732
2008
|
);
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
` \u2014 tenant ${cfg.tenant}, sub ${sub.id ?? opts.name}, tags [${filter.tags.join(", ")}]
|
|
1737
|
-
`
|
|
1738
|
-
) + style.dim("streaming matched events to stdout; Ctrl-C to stop.\n")
|
|
2009
|
+
instanceKey = await promptText(
|
|
2010
|
+
"Executor instance key?",
|
|
2011
|
+
instanceKey ?? lane ?? ""
|
|
1739
2012
|
);
|
|
2013
|
+
laneId = await promptText(
|
|
2014
|
+
"Executor affinity lane?",
|
|
2015
|
+
laneId ?? lane ?? ""
|
|
2016
|
+
);
|
|
2017
|
+
runtime = await promptText(
|
|
2018
|
+
"Runtime (claude-code or codex)?",
|
|
2019
|
+
runtime ?? (surface === "codex" ? "codex" : "claude-code")
|
|
2020
|
+
);
|
|
2021
|
+
connector = await promptText(
|
|
2022
|
+
"Approved local-session connector id?",
|
|
2023
|
+
connector ?? ""
|
|
2024
|
+
);
|
|
2025
|
+
const capabilityText = await promptText(
|
|
2026
|
+
"Capability keys (comma-separated; blank for none)?",
|
|
2027
|
+
capabilities?.join(",") ?? ""
|
|
2028
|
+
);
|
|
2029
|
+
capabilities = capabilityText.split(",").map((x) => x.trim()).filter(Boolean);
|
|
1740
2030
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1749
|
-
const filter = readFilter(opts);
|
|
1750
|
-
const mcp = new Server(
|
|
1751
|
-
{ name: "sechroom", version: "0.1.0" },
|
|
1752
|
-
{
|
|
1753
|
-
capabilities: { experimental: { "claude/channel": {} } },
|
|
1754
|
-
instructions: 'Matched Sechroom substrate events arrive as <channel source="sechroom"> tags. A WLP dispatch (kind:task \u2192 status:in-progress) means a task is runnable now: load the memory id from the event, do the work, then write a closeout memory tagged wlp-decomposition:{id} + wlp-task:{taskId} + verdict:{pass|soft-fail|plan-invalid|blocked}.'
|
|
1755
|
-
}
|
|
1756
|
-
);
|
|
1757
|
-
await mcp.connect(new StdioServerTransport());
|
|
1758
|
-
await ensureSubscription(cfg, opts.name, filter);
|
|
1759
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1760
|
-
const deliver = makeDeliver(filter, seen, (payload) => {
|
|
1761
|
-
const { content, meta } = summarizeEvent(payload);
|
|
1762
|
-
void mcp.notification({
|
|
1763
|
-
method: "notifications/claude/channel",
|
|
1764
|
-
params: { content, meta }
|
|
1765
|
-
}).catch(
|
|
1766
|
-
(e) => process.stderr.write(err(`channel push failed: ${String(e)}
|
|
1767
|
-
`))
|
|
2031
|
+
surface ??= surfaceDefault;
|
|
2032
|
+
instanceKey ??= lane;
|
|
2033
|
+
laneId ??= lane;
|
|
2034
|
+
runtime ??= surface === "codex" ? "codex" : "claude-code";
|
|
2035
|
+
if (!connector)
|
|
2036
|
+
fail(
|
|
2037
|
+
"executor install requires --connector (or an interactive connector id)"
|
|
1768
2038
|
);
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
2039
|
+
if (!instanceKey)
|
|
2040
|
+
fail(
|
|
2041
|
+
"no instance key resolved; pass --instance-key or pin .sechroom/lane.json code-lane"
|
|
2042
|
+
);
|
|
2043
|
+
if (!opts.yes && !canPrompt())
|
|
2044
|
+
fail("non-interactive executor install requires --yes");
|
|
2045
|
+
parseRuntimeKind(runtime);
|
|
2046
|
+
if (!["claude", "codex"].includes(surface))
|
|
2047
|
+
fail("surface must be claude or codex");
|
|
2048
|
+
if (opts.refreshAfter >= opts.ttl)
|
|
2049
|
+
fail("refresh-after must be shorter than the TTL");
|
|
2050
|
+
const sem = readSem();
|
|
2051
|
+
const checkout = sem ? dirname5(dirname5(sem.path)) : process.cwd();
|
|
2052
|
+
const statePath = join8(checkout, ".sechroom", EXECUTOR_STATE);
|
|
2053
|
+
const state = {
|
|
2054
|
+
schemaVersion: 1,
|
|
2055
|
+
instanceKey,
|
|
2056
|
+
laneId,
|
|
2057
|
+
runtime: runtime.toLowerCase() === "codex" ? "codex" : "claude-code",
|
|
2058
|
+
connectorId: connector,
|
|
2059
|
+
capabilityKeys: capabilities ?? [],
|
|
2060
|
+
relayId: opts.relay,
|
|
2061
|
+
subscriptionName: opts.subscriptionName,
|
|
2062
|
+
ttlSeconds: opts.ttl,
|
|
2063
|
+
refreshAfterSeconds: opts.refreshAfter
|
|
2064
|
+
};
|
|
2065
|
+
if (!opts.dryRun) {
|
|
2066
|
+
mkdirSync6(dirname5(statePath), { recursive: true });
|
|
2067
|
+
writeFileSync6(statePath, JSON.stringify(state, null, 2) + "\n");
|
|
2068
|
+
ensureStateDirIgnored(checkout);
|
|
2069
|
+
}
|
|
2070
|
+
const configuredClaudeDirs = globals.claudeConfigDir || process.env.CLAUDE_CONFIG_DIR ? resolveClaudeTargets({ override: globals.claudeConfigDir }).map(
|
|
2071
|
+
(target) => target.dir
|
|
2072
|
+
) : [join8(checkout, ".claude")];
|
|
2073
|
+
const configuredCodexHomes = globals.codexHome || process.env.CODEX_HOME ? resolveCodexHomes({ override: globals.codexHome }) : [join8(checkout, ".codex")];
|
|
2074
|
+
const hookTargets = surface === "claude" ? configuredClaudeDirs : configuredCodexHomes;
|
|
2075
|
+
for (const target of hookTargets) {
|
|
2076
|
+
const results = surface === "claude" ? [
|
|
2077
|
+
installClaudeCommands(
|
|
2078
|
+
target,
|
|
2079
|
+
CLAUDE_EXECUTOR_HOOKS,
|
|
2080
|
+
opts.dryRun
|
|
2081
|
+
)
|
|
2082
|
+
] : installCodexCommands(target, CODEX_EXECUTOR_HOOKS, opts.dryRun);
|
|
2083
|
+
for (const result of results)
|
|
2084
|
+
process.stderr.write(describe(result, opts.dryRun) + "\n");
|
|
2085
|
+
}
|
|
2086
|
+
warnIfSechroomNotOnPath();
|
|
1772
2087
|
process.stderr.write(
|
|
1773
|
-
style.dim(
|
|
1774
|
-
|
|
1775
|
-
`
|
|
1776
|
-
)
|
|
2088
|
+
style.green("executor harness configured") + style.dim(` \u2014 ${instanceKey}
|
|
2089
|
+
`)
|
|
1777
2090
|
);
|
|
1778
|
-
await holdOpen(conn);
|
|
1779
2091
|
});
|
|
1780
|
-
|
|
1781
|
-
|
|
2092
|
+
executor.command("hook-pulse").description("Hook adapter: register or refresh this checkout's executor").action(async (_opts, cmd) => {
|
|
2093
|
+
await drainStdin();
|
|
2094
|
+
const located = readExecutorState();
|
|
2095
|
+
if (!located) return;
|
|
2096
|
+
const { state, path } = located;
|
|
2097
|
+
const age = state.lastRefreshAt ? Date.now() - Date.parse(state.lastRefreshAt) : Number.POSITIVE_INFINITY;
|
|
2098
|
+
if (state.instanceId && age < state.refreshAfterSeconds * 1e3) return;
|
|
2099
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2100
|
+
try {
|
|
2101
|
+
await ensureExecutorInstance(cfg, { state, path });
|
|
2102
|
+
} catch {
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
executor.command("hook-stop").description("Hook adapter: deregister this checkout's executor").action(async (_opts, cmd) => {
|
|
2106
|
+
await drainStdin();
|
|
2107
|
+
const located = readExecutorState();
|
|
2108
|
+
if (!located?.state.instanceId) return;
|
|
2109
|
+
try {
|
|
2110
|
+
await api(
|
|
2111
|
+
resolveConfig(cmd.optsWithGlobals()),
|
|
2112
|
+
`/me/executor-instances/${encodeURIComponent(located.state.instanceId)}`,
|
|
2113
|
+
{ method: "DELETE", body: JSON.stringify({}) }
|
|
2114
|
+
);
|
|
2115
|
+
delete located.state.instanceId;
|
|
2116
|
+
delete located.state.lastRefreshAt;
|
|
2117
|
+
writeFileSync6(
|
|
2118
|
+
located.path,
|
|
2119
|
+
JSON.stringify(located.state, null, 2) + "\n"
|
|
2120
|
+
);
|
|
2121
|
+
} catch {
|
|
2122
|
+
}
|
|
2123
|
+
});
|
|
2124
|
+
executor.command("submit-connector").description(
|
|
2125
|
+
"Submit a local-session connector definition for governed approval"
|
|
2126
|
+
).requiredOption("--slug <slug>", "Unique connector definition slug").requiredOption("--display-name <name>", "Human-readable connector name").requiredOption("--transport <kind>", "push | pull").option("--profile <profile...>", "Advertised runtime profiles", [
|
|
2127
|
+
"base",
|
|
2128
|
+
"dotnet-10"
|
|
2129
|
+
]).action(async (opts, cmd) => {
|
|
2130
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2131
|
+
const data = await api(cfg, "/connectors/definitions", {
|
|
2132
|
+
method: "POST",
|
|
2133
|
+
body: JSON.stringify({
|
|
2134
|
+
slug: opts.slug,
|
|
2135
|
+
displayName: opts.displayName,
|
|
2136
|
+
runtimeProfiles: opts.profile,
|
|
2137
|
+
connectorKind: "ExecutionRuntime",
|
|
2138
|
+
providerKind: "local-session",
|
|
2139
|
+
dispatchTransport: parseTransport(opts.transport)
|
|
2140
|
+
})
|
|
2141
|
+
});
|
|
2142
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
2143
|
+
if (!cmd.optsWithGlobals().json) {
|
|
2144
|
+
process.stderr.write(
|
|
2145
|
+
style.dim(
|
|
2146
|
+
"approve this connector definition before registering executors\n"
|
|
2147
|
+
)
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
});
|
|
2151
|
+
executor.command("register").description(
|
|
2152
|
+
"Create/reuse a SignalR binding and register this local executor instance"
|
|
2153
|
+
).requiredOption(
|
|
2154
|
+
"--instance-key <key>",
|
|
2155
|
+
"Stable key for this concrete session/lane"
|
|
2156
|
+
).requiredOption(
|
|
2157
|
+
"--connector <id>",
|
|
2158
|
+
"Approved local-session ConnectorDefinition id"
|
|
2159
|
+
).option("--runtime <kind>", "claude-code | codex", "claude-code").option(
|
|
2160
|
+
"--lane-id <lane>",
|
|
2161
|
+
"Canonical affinity lane (defaults to instance key)"
|
|
2162
|
+
).option(
|
|
2163
|
+
"--relay <id>",
|
|
2164
|
+
"Relay identity shared by sibling instances",
|
|
2165
|
+
"sechroom-cli-local"
|
|
2166
|
+
).option(
|
|
2167
|
+
"--subscription-name <name>",
|
|
2168
|
+
"SignalR delivery binding name",
|
|
2169
|
+
"executor-dispatch"
|
|
2170
|
+
).option(
|
|
2171
|
+
"--capability <key...>",
|
|
2172
|
+
"Capability operation keys claimed by this instance"
|
|
2173
|
+
).option("--tool-set-ref <ref>", "Optional governed tool-set reference").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (opts, cmd) => {
|
|
2174
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2175
|
+
const subscription = await api(
|
|
2176
|
+
cfg,
|
|
2177
|
+
"/me/delivery-subscriptions/signalr",
|
|
2178
|
+
{
|
|
2179
|
+
method: "POST",
|
|
2180
|
+
body: JSON.stringify({
|
|
2181
|
+
name: opts.subscriptionName,
|
|
2182
|
+
enabled: true,
|
|
2183
|
+
// Exact executor fanout ignores this generic filter; the fixed tag only
|
|
2184
|
+
// satisfies the legacy SignalR subscription shape.
|
|
2185
|
+
filter: { tags: ["kind:task"], workspaceScope: [] }
|
|
2186
|
+
})
|
|
2187
|
+
}
|
|
2188
|
+
);
|
|
2189
|
+
const data = await api(
|
|
2190
|
+
cfg,
|
|
2191
|
+
"/me/executor-instances",
|
|
2192
|
+
{
|
|
2193
|
+
method: "POST",
|
|
2194
|
+
body: JSON.stringify({
|
|
2195
|
+
relayId: opts.relay,
|
|
2196
|
+
instanceKey: opts.instanceKey,
|
|
2197
|
+
laneId: opts.laneId ?? opts.instanceKey,
|
|
2198
|
+
runtimeKind: parseRuntimeKind(opts.runtime),
|
|
2199
|
+
activationMode: "Attached",
|
|
2200
|
+
deliverySubscriptionId: subscription.id,
|
|
2201
|
+
connectorId: opts.connector,
|
|
2202
|
+
claimedCapabilityKeys: opts.capability ?? [],
|
|
2203
|
+
toolSetRef: opts.toolSetRef ?? null,
|
|
2204
|
+
ttlSeconds: opts.ttl
|
|
2205
|
+
})
|
|
2206
|
+
}
|
|
2207
|
+
);
|
|
2208
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
2209
|
+
if (!cmd.optsWithGlobals().json)
|
|
2210
|
+
process.stderr.write(
|
|
2211
|
+
style.dim(`refresh with: sechroom executor heartbeat ${data.id}
|
|
2212
|
+
`)
|
|
2213
|
+
);
|
|
2214
|
+
});
|
|
2215
|
+
executor.command("refresh <id>").description("Refresh one advertisement lease once").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (id, opts, cmd) => {
|
|
2216
|
+
const data = await refreshExecutorInstance(
|
|
2217
|
+
resolveConfig(cmd.optsWithGlobals()),
|
|
2218
|
+
id,
|
|
2219
|
+
opts.ttl
|
|
2220
|
+
);
|
|
2221
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
2222
|
+
});
|
|
2223
|
+
executor.command("heartbeat <id>").description("Keep an advertisement alive until interrupted").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).option("--interval <seconds>", "Refresh interval", parseInteger, 40).action(async (id, opts, cmd) => {
|
|
2224
|
+
if (opts.interval >= opts.ttl)
|
|
2225
|
+
fail("heartbeat interval must be shorter than the TTL");
|
|
2226
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2227
|
+
await refreshExecutorInstance(cfg, id, opts.ttl);
|
|
2228
|
+
process.stderr.write(
|
|
2229
|
+
style.green("executor heartbeat active") + style.dim(` \u2014 ${id}
|
|
2230
|
+
`)
|
|
2231
|
+
);
|
|
2232
|
+
await holdHeartbeat(async () => {
|
|
2233
|
+
await refreshExecutorInstance(cfg, id, opts.ttl);
|
|
2234
|
+
}, opts.interval * 1e3);
|
|
2235
|
+
});
|
|
2236
|
+
executor.command("offers <id>").description("List live dispatch offers addressed to this exact instance").action(async (id, _opts, cmd) => {
|
|
2237
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2238
|
+
const data = await api(
|
|
2239
|
+
cfg,
|
|
2240
|
+
`/me/executor-instances/${encodeURIComponent(id)}/dispatch-offers`
|
|
2241
|
+
);
|
|
2242
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
2243
|
+
});
|
|
2244
|
+
executor.command("deregister <id>").description("Stop advertising this executor instance").action(async (id, _opts, cmd) => {
|
|
2245
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2246
|
+
const data = await api(
|
|
2247
|
+
cfg,
|
|
2248
|
+
`/me/executor-instances/${encodeURIComponent(id)}`,
|
|
2249
|
+
{
|
|
2250
|
+
method: "DELETE",
|
|
2251
|
+
body: JSON.stringify({})
|
|
2252
|
+
}
|
|
2253
|
+
);
|
|
2254
|
+
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
function parseRuntimeKind(value) {
|
|
2258
|
+
switch (value.trim().toLowerCase()) {
|
|
2259
|
+
case "claude":
|
|
2260
|
+
case "claude-code":
|
|
2261
|
+
return "ClaudeCode";
|
|
2262
|
+
case "codex":
|
|
2263
|
+
return "Codex";
|
|
2264
|
+
default:
|
|
2265
|
+
return fail("runtime must be claude-code or codex");
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
function parseTransport(value) {
|
|
2269
|
+
switch (value.trim().toLowerCase()) {
|
|
2270
|
+
case "push":
|
|
2271
|
+
return "Push";
|
|
2272
|
+
case "pull":
|
|
2273
|
+
return "Pull";
|
|
2274
|
+
default:
|
|
2275
|
+
return fail("transport must be push or pull");
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
async function refreshExecutorInstance(cfg, id, ttlSeconds) {
|
|
2279
|
+
return api(
|
|
2280
|
+
cfg,
|
|
2281
|
+
`/me/executor-instances/${encodeURIComponent(id)}/refresh`,
|
|
2282
|
+
{
|
|
2283
|
+
method: "POST",
|
|
2284
|
+
body: JSON.stringify({ ttlSeconds })
|
|
2285
|
+
}
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
async function registerInstance(cfg, state) {
|
|
2289
|
+
const subscription = await api(
|
|
2290
|
+
cfg,
|
|
2291
|
+
"/me/delivery-subscriptions/signalr",
|
|
2292
|
+
{
|
|
2293
|
+
method: "POST",
|
|
2294
|
+
body: JSON.stringify(executorSubscriptionInput(state.subscriptionName))
|
|
2295
|
+
}
|
|
2296
|
+
);
|
|
2297
|
+
return api(cfg, "/me/executor-instances", {
|
|
2298
|
+
method: "POST",
|
|
2299
|
+
body: JSON.stringify(executorRegistrationInput(state, subscription.id))
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
async function ensureExecutorInstance(cfg, located) {
|
|
2303
|
+
const { state, path } = located;
|
|
2304
|
+
state.laneId ??= state.instanceKey;
|
|
2305
|
+
const data = await registerInstance(cfg, state);
|
|
2306
|
+
state.instanceId = data.id;
|
|
2307
|
+
state.lastRefreshAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2308
|
+
writeFileSync6(path, JSON.stringify(state, null, 2) + "\n");
|
|
2309
|
+
return data;
|
|
2310
|
+
}
|
|
2311
|
+
function readExecutorState(start = process.cwd()) {
|
|
2312
|
+
const semPath = resolveSemPathForRead(start);
|
|
2313
|
+
const sem = semPath ? readSem(semPath) : void 0;
|
|
2314
|
+
const path = join8(
|
|
2315
|
+
sem ? dirname5(sem.path) : join8(start, ".sechroom"),
|
|
2316
|
+
EXECUTOR_STATE
|
|
2317
|
+
);
|
|
2318
|
+
if (!existsSync7(path)) return void 0;
|
|
2319
|
+
return {
|
|
2320
|
+
state: JSON.parse(readFileSync5(path, "utf8")),
|
|
2321
|
+
path
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
async function drainStdin() {
|
|
2325
|
+
if (process.stdin.isTTY) return;
|
|
2326
|
+
for await (const _chunk of process.stdin) {
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
async function api(cfg, path, init) {
|
|
2330
|
+
const token = await requireToken(cfg);
|
|
2331
|
+
const response = await fetch(`${cfg.baseUrl}${path}`, {
|
|
2332
|
+
...init,
|
|
2333
|
+
headers: {
|
|
2334
|
+
authorization: `Bearer ${token}`,
|
|
2335
|
+
tenant: cfg.tenant,
|
|
2336
|
+
"content-type": "application/json",
|
|
2337
|
+
"x-sechroom-surface": "cli"
|
|
2338
|
+
}
|
|
2339
|
+
});
|
|
2340
|
+
if (!response.ok)
|
|
2341
|
+
fail(
|
|
2342
|
+
`${init?.method ?? "GET"} ${path} failed (${response.status}): ${await response.text()}`
|
|
2343
|
+
);
|
|
2344
|
+
return response.json();
|
|
2345
|
+
}
|
|
2346
|
+
function parseInteger(value) {
|
|
2347
|
+
const parsed = Number.parseInt(value, 10);
|
|
2348
|
+
if (!Number.isFinite(parsed)) fail(`expected an integer, got '${value}'`);
|
|
2349
|
+
return parsed;
|
|
2350
|
+
}
|
|
2351
|
+
function holdHeartbeat(tick, intervalMs) {
|
|
2352
|
+
return new Promise((resolve3, reject) => {
|
|
2353
|
+
const timer = setInterval(() => void tick().catch(reject), intervalMs);
|
|
2354
|
+
const stop = () => {
|
|
2355
|
+
clearInterval(timer);
|
|
2356
|
+
resolve3();
|
|
2357
|
+
};
|
|
2358
|
+
process.once("SIGINT", stop);
|
|
2359
|
+
process.once("SIGTERM", stop);
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
// src/commands/channel.ts
|
|
2364
|
+
function registerChannel(program2) {
|
|
2365
|
+
const channel = program2.command("channel").description(
|
|
2366
|
+
"Receive matched substrate events over the held SignalR push leg (D-WLP-9)"
|
|
2367
|
+
);
|
|
2368
|
+
const withFilterOpts = (c) => c.option(
|
|
2369
|
+
"--name <name>",
|
|
2370
|
+
"Deprecated: ignored; the installed executor selects its delivery subscription"
|
|
2371
|
+
).option(
|
|
2372
|
+
"--tag <tag...>",
|
|
2373
|
+
"Deprecated: executor eligibility comes from the installed capability advertisement"
|
|
1782
2374
|
).option(
|
|
1783
2375
|
"--workspace <wsp...>",
|
|
1784
|
-
"
|
|
2376
|
+
"Deprecated: workspace authority is resolved by the server"
|
|
2377
|
+
).option(
|
|
2378
|
+
"--executor-instance <id>",
|
|
2379
|
+
"Deprecated: the instance is read from .sechroom/executor.json"
|
|
2380
|
+
);
|
|
2381
|
+
withFilterOpts(
|
|
2382
|
+
channel.command("connect").description(
|
|
2383
|
+
"Register a SignalR subscription and stream matched events to stdout"
|
|
2384
|
+
)
|
|
2385
|
+
).action(async (opts, cmd) => {
|
|
2386
|
+
const json = Boolean(cmd.optsWithGlobals().json);
|
|
2387
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2388
|
+
warnLegacyChannelOptions(opts);
|
|
2389
|
+
const located = requireExecutorState();
|
|
2390
|
+
const instance = await ensureExecutorInstance(cfg, located);
|
|
2391
|
+
const deliver = (payload) => process.stdout.write(
|
|
2392
|
+
(typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
|
|
2393
|
+
);
|
|
2394
|
+
const drain = createClaimDrain(cfg, instance.id, deliver);
|
|
2395
|
+
const conn = await openConnection(
|
|
2396
|
+
cfg,
|
|
2397
|
+
() => {
|
|
2398
|
+
void drain().catch(
|
|
2399
|
+
(e) => process.stderr.write(err(`channel claim failed: ${String(e)}
|
|
2400
|
+
`))
|
|
2401
|
+
);
|
|
2402
|
+
},
|
|
2403
|
+
instance.id
|
|
2404
|
+
);
|
|
2405
|
+
await drain();
|
|
2406
|
+
const stopReconciliation = startOfferReconciliation(drain);
|
|
2407
|
+
const stopHeartbeat = startExecutorHeartbeat(
|
|
2408
|
+
() => refreshExecutorInstance(cfg, instance.id, located.state.ttlSeconds),
|
|
2409
|
+
located.state.refreshAfterSeconds * 1e3
|
|
2410
|
+
);
|
|
2411
|
+
if (json) {
|
|
2412
|
+
emit(
|
|
2413
|
+
{
|
|
2414
|
+
connected: true,
|
|
2415
|
+
tenant: cfg.tenant,
|
|
2416
|
+
executorInstanceId: instance.id,
|
|
2417
|
+
instanceKey: located.state.instanceKey,
|
|
2418
|
+
laneId: located.state.laneId
|
|
2419
|
+
},
|
|
2420
|
+
true
|
|
2421
|
+
);
|
|
2422
|
+
} else {
|
|
2423
|
+
process.stderr.write(
|
|
2424
|
+
style.green("channel connected") + style.dim(
|
|
2425
|
+
` \u2014 tenant ${cfg.tenant}, executor ${located.state.instanceKey}
|
|
2426
|
+
`
|
|
2427
|
+
) + style.dim("streaming matched events to stdout; Ctrl-C to stop.\n")
|
|
2428
|
+
);
|
|
2429
|
+
}
|
|
2430
|
+
try {
|
|
2431
|
+
await holdOpen(conn);
|
|
2432
|
+
} finally {
|
|
2433
|
+
stopReconciliation();
|
|
2434
|
+
stopHeartbeat();
|
|
2435
|
+
}
|
|
2436
|
+
});
|
|
2437
|
+
withFilterOpts(
|
|
2438
|
+
channel.command("mcp").description(
|
|
2439
|
+
"Run as a Claude Code channel (local-stdio MCP server) \u2014 push matched events into the session"
|
|
2440
|
+
)
|
|
2441
|
+
).action(async (opts, cmd) => {
|
|
2442
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2443
|
+
warnLegacyChannelOptions(opts);
|
|
2444
|
+
const located = requireExecutorState();
|
|
2445
|
+
const instance = await ensureExecutorInstance(cfg, located);
|
|
2446
|
+
const mcp = new Server(
|
|
2447
|
+
{ name: "sechroom", version: "0.1.0" },
|
|
2448
|
+
{
|
|
2449
|
+
capabilities: { experimental: { "claude/channel": {} } },
|
|
2450
|
+
instructions: 'Matched Sechroom substrate events arrive as <channel source="sechroom"> tags. A WLP dispatch delivered here has already been atomically claimed for this executor. Load the memory id from the event and retain the lease and claim token for holder-bound completion.'
|
|
2451
|
+
}
|
|
2452
|
+
);
|
|
2453
|
+
await mcp.connect(new StdioServerTransport());
|
|
2454
|
+
const deliver = (payload) => {
|
|
2455
|
+
const { content, meta } = summarizeEvent(payload);
|
|
2456
|
+
void mcp.notification({
|
|
2457
|
+
method: "notifications/claude/channel",
|
|
2458
|
+
params: { content, meta }
|
|
2459
|
+
}).catch(
|
|
2460
|
+
(e) => process.stderr.write(err(`channel push failed: ${String(e)}
|
|
2461
|
+
`))
|
|
2462
|
+
);
|
|
2463
|
+
};
|
|
2464
|
+
const drain = createClaimDrain(cfg, instance.id, deliver);
|
|
2465
|
+
const conn = await openConnection(
|
|
2466
|
+
cfg,
|
|
2467
|
+
() => {
|
|
2468
|
+
void drain().catch(
|
|
2469
|
+
(e) => process.stderr.write(err(`channel claim failed: ${String(e)}
|
|
2470
|
+
`))
|
|
2471
|
+
);
|
|
2472
|
+
},
|
|
2473
|
+
instance.id
|
|
2474
|
+
);
|
|
2475
|
+
await drain();
|
|
2476
|
+
const stopReconciliation = startOfferReconciliation(drain);
|
|
2477
|
+
const stopHeartbeat = startExecutorHeartbeat(
|
|
2478
|
+
() => refreshExecutorInstance(cfg, instance.id, located.state.ttlSeconds),
|
|
2479
|
+
located.state.refreshAfterSeconds * 1e3
|
|
2480
|
+
);
|
|
2481
|
+
process.stderr.write(
|
|
2482
|
+
style.dim(
|
|
2483
|
+
`sechroom channel (mcp) \u2014 tenant ${cfg.tenant}, executor ${located.state.instanceKey}
|
|
2484
|
+
`
|
|
2485
|
+
)
|
|
2486
|
+
);
|
|
2487
|
+
try {
|
|
2488
|
+
await holdOpen(conn);
|
|
2489
|
+
} finally {
|
|
2490
|
+
stopReconciliation();
|
|
2491
|
+
stopHeartbeat();
|
|
2492
|
+
}
|
|
2493
|
+
});
|
|
2494
|
+
channel.command("install").description(
|
|
2495
|
+
"Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
|
|
2496
|
+
).option(
|
|
2497
|
+
"--workspace <wsp...>",
|
|
2498
|
+
"Deprecated: accepted only to migrate an existing managed entry"
|
|
1785
2499
|
).option(
|
|
1786
2500
|
"--tag <tag...>",
|
|
1787
|
-
"
|
|
1788
|
-
["kind:task"]
|
|
2501
|
+
"Deprecated: accepted only to migrate an existing managed entry"
|
|
1789
2502
|
).option(
|
|
1790
2503
|
"--name <name>",
|
|
1791
2504
|
"MCP server + subscription name (idempotent per name)",
|
|
1792
2505
|
"sechroom-channel"
|
|
1793
2506
|
).option("--dry-run", "Print what would change; write nothing").action((opts) => {
|
|
1794
|
-
const path =
|
|
2507
|
+
const path = join9(process.cwd(), ".mcp.json");
|
|
1795
2508
|
const dryRun = Boolean(opts.dryRun);
|
|
1796
|
-
const args = ["channel", "mcp"
|
|
1797
|
-
for (const w of opts.workspace ?? [])
|
|
1798
|
-
args.push("--workspace", w);
|
|
1799
|
-
for (const t of opts.tag ?? []) args.push("--tag", t);
|
|
2509
|
+
const args = ["channel", "mcp"];
|
|
1800
2510
|
const entry = { command: "sechroom", args };
|
|
1801
2511
|
const config2 = readMcpConfig(path);
|
|
1802
2512
|
config2.mcpServers ??= {};
|
|
@@ -1804,8 +2514,8 @@ function registerChannel(program2) {
|
|
|
1804
2514
|
const status = JSON.stringify(existing) === JSON.stringify(entry) ? "current" : existing ? "updated" : "created";
|
|
1805
2515
|
if (status !== "current" && !dryRun) {
|
|
1806
2516
|
config2.mcpServers[opts.name] = entry;
|
|
1807
|
-
|
|
1808
|
-
|
|
2517
|
+
mkdirSync7(dirname6(path), { recursive: true });
|
|
2518
|
+
writeFileSync7(path, JSON.stringify(config2, null, 2) + "\n");
|
|
1809
2519
|
}
|
|
1810
2520
|
const verb = status === "current" ? "already configured" : dryRun ? `would ${status === "created" ? "create" : "update"}` : status;
|
|
1811
2521
|
process.stdout.write(`${style.green("channel")} ${path} (${verb})
|
|
@@ -1825,23 +2535,124 @@ Load it (Channels research preview) by launching your agent with:
|
|
|
1825
2535
|
);
|
|
1826
2536
|
}
|
|
1827
2537
|
warnIfSechroomNotOnPath();
|
|
2538
|
+
if ((opts.workspace?.length ?? 0) > 0 || (opts.tag?.length ?? 0) > 0)
|
|
2539
|
+
process.stderr.write(
|
|
2540
|
+
style.dim(
|
|
2541
|
+
"channel: --workspace/--tag are retired; the managed entry now uses the installed executor advertisement.\n"
|
|
2542
|
+
)
|
|
2543
|
+
);
|
|
1828
2544
|
});
|
|
1829
2545
|
channel.addHelpText(
|
|
1830
2546
|
"after",
|
|
1831
2547
|
`
|
|
1832
2548
|
Examples:
|
|
1833
|
-
$ sechroom
|
|
1834
|
-
$ sechroom channel connect
|
|
1835
|
-
$ sechroom channel connect --workspace wsp_X --json | jq .
|
|
2549
|
+
$ sechroom executor install configure capability + lane advertisement
|
|
2550
|
+
$ sechroom channel connect claim WLP dispatches and stream them to stdout
|
|
1836
2551
|
|
|
1837
2552
|
# Wire it as a Claude Code channel MCP server (research preview, v2.1.80+):
|
|
1838
|
-
$ sechroom channel install
|
|
2553
|
+
$ sechroom channel install migrate/install the exact-instance channel
|
|
1839
2554
|
# then: claude --dangerously-load-development-channels server:sechroom-channel`
|
|
1840
2555
|
);
|
|
1841
2556
|
}
|
|
2557
|
+
function requireExecutorState() {
|
|
2558
|
+
const located = readExecutorState();
|
|
2559
|
+
if (!located)
|
|
2560
|
+
return fail(
|
|
2561
|
+
"channel requires an installed executor advertisement; run `sechroom executor install` first."
|
|
2562
|
+
);
|
|
2563
|
+
return located;
|
|
2564
|
+
}
|
|
2565
|
+
function warnLegacyChannelOptions(opts) {
|
|
2566
|
+
if (!opts.name && (opts.workspace?.length ?? 0) === 0 && (opts.tag?.length ?? 0) === 0 && !opts.executorInstance)
|
|
2567
|
+
return;
|
|
2568
|
+
process.stderr.write(
|
|
2569
|
+
style.dim(
|
|
2570
|
+
"channel: --name, --workspace, --tag, and --executor-instance are retired; delivery, eligibility, and identity come from the installed executor advertisement.\n"
|
|
2571
|
+
)
|
|
2572
|
+
);
|
|
2573
|
+
}
|
|
2574
|
+
function createClaimDrain(cfg, executorInstanceId, deliver, dependencies = {}) {
|
|
2575
|
+
let active2;
|
|
2576
|
+
const state = {};
|
|
2577
|
+
return () => {
|
|
2578
|
+
active2 ??= drainClaims(cfg, executorInstanceId, deliver, {
|
|
2579
|
+
...dependencies,
|
|
2580
|
+
state
|
|
2581
|
+
}).finally(() => {
|
|
2582
|
+
active2 = void 0;
|
|
2583
|
+
});
|
|
2584
|
+
return active2;
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
2587
|
+
function startOfferReconciliation(drain, intervalMilliseconds = 5e3, dependencies = {}) {
|
|
2588
|
+
const schedule = dependencies.setInterval ?? setInterval;
|
|
2589
|
+
const cancel = dependencies.clearInterval ?? clearInterval;
|
|
2590
|
+
const onError = dependencies.onError ?? ((error) => process.stderr.write(err(`channel claim failed: ${String(error)}
|
|
2591
|
+
`)));
|
|
2592
|
+
const timer = schedule(() => {
|
|
2593
|
+
void drain().catch(onError);
|
|
2594
|
+
}, intervalMilliseconds);
|
|
2595
|
+
return () => cancel(timer);
|
|
2596
|
+
}
|
|
2597
|
+
function startExecutorHeartbeat(refresh, intervalMilliseconds, dependencies = {}) {
|
|
2598
|
+
const schedule = dependencies.setInterval ?? setInterval;
|
|
2599
|
+
const cancel = dependencies.clearInterval ?? clearInterval;
|
|
2600
|
+
const onError = dependencies.onError ?? ((error) => process.stderr.write(err(`channel heartbeat failed: ${String(error)}
|
|
2601
|
+
`)));
|
|
2602
|
+
const timer = schedule(() => {
|
|
2603
|
+
void refresh().catch(onError);
|
|
2604
|
+
}, intervalMilliseconds);
|
|
2605
|
+
return () => cancel(timer);
|
|
2606
|
+
}
|
|
2607
|
+
async function drainClaims(cfg, executorInstanceId, deliver, dependencies = {}) {
|
|
2608
|
+
const request = dependencies.request ?? api;
|
|
2609
|
+
const sleep = dependencies.sleep ?? ((milliseconds) => new Promise((resolve3) => setTimeout(resolve3, milliseconds)));
|
|
2610
|
+
const idempotencyKey = dependencies.idempotencyKey ?? ((offer) => `channel:${offer.generationId}`);
|
|
2611
|
+
const state = dependencies.state ?? {};
|
|
2612
|
+
for (; ; ) {
|
|
2613
|
+
if (state.pendingIdempotencyKey) {
|
|
2614
|
+
const replay = await request(
|
|
2615
|
+
cfg,
|
|
2616
|
+
`/me/executor-instances/${encodeURIComponent(executorInstanceId)}/dispatch-offers/claim-next`,
|
|
2617
|
+
{
|
|
2618
|
+
method: "POST",
|
|
2619
|
+
body: JSON.stringify({ idempotencyKey: state.pendingIdempotencyKey })
|
|
2620
|
+
}
|
|
2621
|
+
);
|
|
2622
|
+
state.pendingIdempotencyKey = void 0;
|
|
2623
|
+
if (replay.outcome === "Claimed" || replay.outcome === "AlreadyHeld") {
|
|
2624
|
+
deliver(replay);
|
|
2625
|
+
continue;
|
|
2626
|
+
}
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
const offers = await request(
|
|
2630
|
+
cfg,
|
|
2631
|
+
`/me/executor-instances/${encodeURIComponent(executorInstanceId)}/dispatch-offers`
|
|
2632
|
+
);
|
|
2633
|
+
const offer = offers[0];
|
|
2634
|
+
if (!offer) return;
|
|
2635
|
+
if (offer.suggestedClaimDelayMs > 0)
|
|
2636
|
+
await sleep(offer.suggestedClaimDelayMs);
|
|
2637
|
+
state.pendingIdempotencyKey = idempotencyKey(offer);
|
|
2638
|
+
const claim = await request(
|
|
2639
|
+
cfg,
|
|
2640
|
+
`/me/executor-instances/${encodeURIComponent(executorInstanceId)}/dispatch-offers/claim-next`,
|
|
2641
|
+
{
|
|
2642
|
+
method: "POST",
|
|
2643
|
+
body: JSON.stringify({ idempotencyKey: state.pendingIdempotencyKey })
|
|
2644
|
+
}
|
|
2645
|
+
);
|
|
2646
|
+
state.pendingIdempotencyKey = void 0;
|
|
2647
|
+
if (claim.outcome === "NoOffer") return;
|
|
2648
|
+
if (claim.outcome === "Claimed" || claim.outcome === "AlreadyHeld")
|
|
2649
|
+
deliver(claim);
|
|
2650
|
+
else return;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
1842
2653
|
function readMcpConfig(path) {
|
|
1843
|
-
if (!
|
|
1844
|
-
const raw =
|
|
2654
|
+
if (!existsSync8(path)) return {};
|
|
2655
|
+
const raw = readFileSync6(path, "utf8");
|
|
1845
2656
|
if (!raw.trim()) return {};
|
|
1846
2657
|
try {
|
|
1847
2658
|
return JSON.parse(raw);
|
|
@@ -1851,33 +2662,6 @@ function readMcpConfig(path) {
|
|
|
1851
2662
|
);
|
|
1852
2663
|
}
|
|
1853
2664
|
}
|
|
1854
|
-
function readFilter(opts) {
|
|
1855
|
-
const tags = opts.tag ?? [];
|
|
1856
|
-
const workspaceScope = opts.workspace ?? [];
|
|
1857
|
-
if (tags.length === 0 && workspaceScope.length === 0)
|
|
1858
|
-
fail(
|
|
1859
|
-
"A channel subscription needs at least one --tag or --workspace (an empty filter receives nothing)."
|
|
1860
|
-
);
|
|
1861
|
-
return { tags, workspaceScope };
|
|
1862
|
-
}
|
|
1863
|
-
async function ensureSubscription(cfg, name, filter) {
|
|
1864
|
-
const token = await requireToken(cfg);
|
|
1865
|
-
const resp = await fetch(`${cfg.baseUrl}/me/delivery-subscriptions/signalr`, {
|
|
1866
|
-
method: "POST",
|
|
1867
|
-
headers: {
|
|
1868
|
-
authorization: `Bearer ${token}`,
|
|
1869
|
-
tenant: cfg.tenant,
|
|
1870
|
-
"content-type": "application/json",
|
|
1871
|
-
"x-sechroom-surface": "cli"
|
|
1872
|
-
},
|
|
1873
|
-
body: JSON.stringify({ name, enabled: true, filter })
|
|
1874
|
-
});
|
|
1875
|
-
if (!resp.ok)
|
|
1876
|
-
fail(
|
|
1877
|
-
`Could not register the SignalR subscription (HTTP ${resp.status}): ${await resp.text()}`
|
|
1878
|
-
);
|
|
1879
|
-
return await resp.json();
|
|
1880
|
-
}
|
|
1881
2665
|
async function openConnection(cfg, onEvent, executorInstanceId) {
|
|
1882
2666
|
const query = executorInstanceId ? `?executorInstanceId=${encodeURIComponent(executorInstanceId)}` : "";
|
|
1883
2667
|
const conn = new HubConnectionBuilder().withUrl(`${cfg.baseUrl}/notifications/${cfg.tenant}${query}`, {
|
|
@@ -1914,7 +2698,8 @@ function parseEvent(payload) {
|
|
|
1914
2698
|
}
|
|
1915
2699
|
}
|
|
1916
2700
|
const obj = data ?? {};
|
|
1917
|
-
const
|
|
2701
|
+
const envelope = obj.data ?? obj;
|
|
2702
|
+
const inner = envelope.offer ?? envelope;
|
|
1918
2703
|
const rawTags = inner.tags ?? inner.Tags;
|
|
1919
2704
|
return {
|
|
1920
2705
|
eventType: str(inner.eventType ?? inner.EventType ?? obj.type) || "substrate.event",
|
|
@@ -1923,113 +2708,6 @@ function parseEvent(payload) {
|
|
|
1923
2708
|
tags: Array.isArray(rawTags) ? rawTags.filter((t) => typeof t === "string") : void 0
|
|
1924
2709
|
};
|
|
1925
2710
|
}
|
|
1926
|
-
function shouldDeliver(payload, filter) {
|
|
1927
|
-
const { workspaceId, tags } = parseEvent(payload);
|
|
1928
|
-
if (filter.workspaceScope.length > 0 && (!workspaceId || !filter.workspaceScope.includes(workspaceId)))
|
|
1929
|
-
return false;
|
|
1930
|
-
if (filter.tags.length > 0) {
|
|
1931
|
-
if (!tags) return false;
|
|
1932
|
-
return facetedTagMatch(tags, filter.tags);
|
|
1933
|
-
}
|
|
1934
|
-
return true;
|
|
1935
|
-
}
|
|
1936
|
-
function makeDeliver(filter, seen, forward) {
|
|
1937
|
-
return (payload) => {
|
|
1938
|
-
if (!shouldDeliver(payload, filter)) return;
|
|
1939
|
-
const { memoryId } = parseEvent(payload);
|
|
1940
|
-
if (memoryId) {
|
|
1941
|
-
if (seen.has(memoryId)) return;
|
|
1942
|
-
seen.add(memoryId);
|
|
1943
|
-
}
|
|
1944
|
-
forward(payload);
|
|
1945
|
-
};
|
|
1946
|
-
}
|
|
1947
|
-
async function reconcile(cfg, filter, deliver) {
|
|
1948
|
-
if (filter.workspaceScope.length === 0) {
|
|
1949
|
-
process.stderr.write(
|
|
1950
|
-
style.dim(
|
|
1951
|
-
"channel: no --workspace to reconcile against; live feed only (a dropped dispatch won't be recovered).\n"
|
|
1952
|
-
)
|
|
1953
|
-
);
|
|
1954
|
-
return;
|
|
1955
|
-
}
|
|
1956
|
-
if (filter.tags.length === 0) return;
|
|
1957
|
-
let token;
|
|
1958
|
-
try {
|
|
1959
|
-
token = await requireToken(cfg);
|
|
1960
|
-
} catch {
|
|
1961
|
-
return;
|
|
1962
|
-
}
|
|
1963
|
-
const qs = `filterTags=${encodeURIComponent(filter.tags.join(","))}&limit=100`;
|
|
1964
|
-
let recovered = 0;
|
|
1965
|
-
for (const ws of filter.workspaceScope) {
|
|
1966
|
-
try {
|
|
1967
|
-
const resp = await fetch(
|
|
1968
|
-
`${cfg.baseUrl}/workspaces/${encodeURIComponent(ws)}/memories/feed?${qs}`,
|
|
1969
|
-
{
|
|
1970
|
-
headers: {
|
|
1971
|
-
authorization: `Bearer ${token}`,
|
|
1972
|
-
tenant: cfg.tenant,
|
|
1973
|
-
"x-sechroom-surface": "cli"
|
|
1974
|
-
}
|
|
1975
|
-
}
|
|
1976
|
-
);
|
|
1977
|
-
if (!resp.ok) {
|
|
1978
|
-
process.stderr.write(
|
|
1979
|
-
err(
|
|
1980
|
-
`channel: reconcile query for ${ws} failed (HTTP ${resp.status})
|
|
1981
|
-
`
|
|
1982
|
-
)
|
|
1983
|
-
);
|
|
1984
|
-
continue;
|
|
1985
|
-
}
|
|
1986
|
-
const data = await resp.json();
|
|
1987
|
-
for (const m of data.results ?? []) {
|
|
1988
|
-
if (!m.id) continue;
|
|
1989
|
-
deliver({
|
|
1990
|
-
eventType: "reconcile",
|
|
1991
|
-
memoryId: m.id,
|
|
1992
|
-
workspaceId: ws,
|
|
1993
|
-
tags: m.tags ?? []
|
|
1994
|
-
});
|
|
1995
|
-
recovered++;
|
|
1996
|
-
}
|
|
1997
|
-
} catch (e) {
|
|
1998
|
-
process.stderr.write(
|
|
1999
|
-
err(`channel: reconcile error for ${ws}: ${String(e)}
|
|
2000
|
-
`)
|
|
2001
|
-
);
|
|
2002
|
-
}
|
|
2003
|
-
}
|
|
2004
|
-
if (recovered > 0)
|
|
2005
|
-
process.stderr.write(
|
|
2006
|
-
style.dim(
|
|
2007
|
-
`channel: reconciled ${recovered} already-queued event(s) on connect.
|
|
2008
|
-
`
|
|
2009
|
-
)
|
|
2010
|
-
);
|
|
2011
|
-
}
|
|
2012
|
-
function facetedTagMatch(eventTags, filterTags) {
|
|
2013
|
-
const have = new Set(eventTags);
|
|
2014
|
-
const groups = /* @__PURE__ */ new Map();
|
|
2015
|
-
for (const f of filterTags) {
|
|
2016
|
-
const ns = f.endsWith(":*") ? f.slice(0, -2) : namespaceOf(f);
|
|
2017
|
-
const group = groups.get(ns) ?? [];
|
|
2018
|
-
group.push(f);
|
|
2019
|
-
groups.set(ns, group);
|
|
2020
|
-
}
|
|
2021
|
-
for (const [ns, group] of groups) {
|
|
2022
|
-
const ok2 = group.some(
|
|
2023
|
-
(f) => f.endsWith(":*") ? eventTags.some((t) => namespaceOf(t) === ns) : have.has(f)
|
|
2024
|
-
);
|
|
2025
|
-
if (!ok2) return false;
|
|
2026
|
-
}
|
|
2027
|
-
return true;
|
|
2028
|
-
}
|
|
2029
|
-
function namespaceOf(tag) {
|
|
2030
|
-
const i = tag.indexOf(":");
|
|
2031
|
-
return i >= 0 ? tag.slice(0, i) : tag;
|
|
2032
|
-
}
|
|
2033
2711
|
function summarizeEvent(payload) {
|
|
2034
2712
|
const { eventType, memoryId, workspaceId } = parseEvent(payload);
|
|
2035
2713
|
const content = memoryId ? `${eventType}: ${memoryId}${workspaceId ? ` (workspace ${workspaceId})` : ""}` : typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
@@ -2037,6 +2715,10 @@ function summarizeEvent(payload) {
|
|
|
2037
2715
|
if (eventType) meta.event_type = eventType;
|
|
2038
2716
|
if (memoryId) meta.memory_id = memoryId;
|
|
2039
2717
|
if (workspaceId) meta.workspace_id = workspaceId;
|
|
2718
|
+
const claim = payload ?? {};
|
|
2719
|
+
if (claim.outcome) meta.claim_outcome = claim.outcome;
|
|
2720
|
+
if (claim.lease?.id) meta.lease_id = claim.lease.id;
|
|
2721
|
+
if (claim.claimToken) meta.claim_token = claim.claimToken;
|
|
2040
2722
|
return { content, meta };
|
|
2041
2723
|
}
|
|
2042
2724
|
function str(v) {
|
|
@@ -2066,237 +2748,73 @@ Examples:
|
|
|
2066
2748
|
return client.POST("/chat/channel-messages/{surface}", {
|
|
2067
2749
|
params: { path: { surface: String(surface) } },
|
|
2068
2750
|
body: {
|
|
2069
|
-
channelId,
|
|
2070
|
-
text: text2,
|
|
2071
|
-
guildId: opts.guild ?? null,
|
|
2072
|
-
attachedMemoryId: opts.memory ?? null,
|
|
2073
|
-
trackReplies: opts.track,
|
|
2074
|
-
parentMessage: opts.parent ?? null,
|
|
2075
|
-
source: opts.source,
|
|
2076
|
-
as: opts.as
|
|
2077
|
-
}
|
|
2078
|
-
});
|
|
2079
|
-
});
|
|
2080
|
-
if (!data.ok) {
|
|
2081
|
-
if (json) {
|
|
2082
|
-
emit(data, true);
|
|
2083
|
-
} else {
|
|
2084
|
-
process.stderr.write(
|
|
2085
|
-
`${err("\u2717")} send failed: ${data.upstreamError ?? "error"}${data.errorDescription ? ` \u2014 ${data.errorDescription}` : ""}
|
|
2086
|
-
`
|
|
2087
|
-
);
|
|
2088
|
-
}
|
|
2089
|
-
process.exit(1);
|
|
2090
|
-
}
|
|
2091
|
-
const idPart = data.persistedId ? ` ${style.dim(`(${data.persistedId})`)}` : "";
|
|
2092
|
-
emitAction(`sent to ${surface} ${style.bold(channelId)}${idPart}`, data, json);
|
|
2093
|
-
});
|
|
2094
|
-
chat.command("messages").description("List recent channel messages (GET /chat/channel-messages/{surface})").action(async (_opts, cmd) => {
|
|
2095
|
-
const { surface, ...globals } = cmd.optsWithGlobals();
|
|
2096
|
-
const cfg = resolveConfig(globals);
|
|
2097
|
-
const data = await runApi("Fetching messages", async () => {
|
|
2098
|
-
const client = await makeClient(cfg);
|
|
2099
|
-
return client.GET("/chat/channel-messages/{surface}", {
|
|
2100
|
-
params: { path: { surface: String(surface) } }
|
|
2101
|
-
});
|
|
2102
|
-
});
|
|
2103
|
-
emit(data, cmd.optsWithGlobals().json);
|
|
2104
|
-
});
|
|
2105
|
-
chat.command("replies <messageId>").description("List thread replies for a message (GET /chat/channel-messages/by-id/{id}/replies)").action(async (messageId, _opts, cmd) => {
|
|
2106
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2107
|
-
const data = await runApi("Fetching replies", async () => {
|
|
2108
|
-
const client = await makeClient(cfg);
|
|
2109
|
-
return client.GET("/chat/channel-messages/by-id/{id}/replies", {
|
|
2110
|
-
params: { path: { id: messageId } }
|
|
2111
|
-
});
|
|
2112
|
-
});
|
|
2113
|
-
emit(data, cmd.optsWithGlobals().json);
|
|
2114
|
-
});
|
|
2115
|
-
chat.command("stop-tracking <messageId>").description("Stop watching a message for replies (POST .../by-id/{id}/stop-tracking-replies)").action(async (messageId, _opts, cmd) => {
|
|
2116
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2117
|
-
const data = await runApi("Stopping reply tracking", async () => {
|
|
2118
|
-
const client = await makeClient(cfg);
|
|
2119
|
-
return client.POST("/chat/channel-messages/by-id/{id}/stop-tracking-replies", {
|
|
2120
|
-
params: { path: { id: messageId } },
|
|
2121
|
-
body: {}
|
|
2122
|
-
});
|
|
2123
|
-
});
|
|
2124
|
-
emitAction(`stopped tracking replies on ${style.bold(messageId)}`, data, cmd.optsWithGlobals().json);
|
|
2125
|
-
});
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
// src/commands/checkpoint.ts
|
|
2129
|
-
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
2130
|
-
import { dirname as dirname7, join as join10 } from "path";
|
|
2131
|
-
|
|
2132
|
-
// src/commands/hook.ts
|
|
2133
|
-
import { createHash as createHash2 } from "crypto";
|
|
2134
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync6, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
|
|
2135
|
-
import { dirname as dirname6, join as join9 } from "path";
|
|
2136
|
-
|
|
2137
|
-
// src/sem.ts
|
|
2138
|
-
import { dirname as dirname5, join as join8 } from "path";
|
|
2139
|
-
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync6 } from "fs";
|
|
2140
|
-
var SEM_FILE = join8(".sechroom", "lane.json");
|
|
2141
|
-
var STATE_DIR_NAME2 = ".sechroom";
|
|
2142
|
-
function localSemPath(cwd = process.cwd()) {
|
|
2143
|
-
return join8(cwd, SEM_FILE);
|
|
2144
|
-
}
|
|
2145
|
-
function resolveSemPathForRead(start = process.cwd()) {
|
|
2146
|
-
let dir = start;
|
|
2147
|
-
while (true) {
|
|
2148
|
-
const candidate = join8(dir, SEM_FILE);
|
|
2149
|
-
if (existsSync7(candidate)) return candidate;
|
|
2150
|
-
const parent = dirname5(dir);
|
|
2151
|
-
if (parent === dir) return void 0;
|
|
2152
|
-
dir = parent;
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
|
|
2156
|
-
try {
|
|
2157
|
-
let dir = start;
|
|
2158
|
-
let gitPath;
|
|
2159
|
-
for (; ; ) {
|
|
2160
|
-
const candidate = join8(dir, ".git");
|
|
2161
|
-
if (existsSync7(candidate)) {
|
|
2162
|
-
gitPath = candidate;
|
|
2163
|
-
break;
|
|
2164
|
-
}
|
|
2165
|
-
const parent = dirname5(dir);
|
|
2166
|
-
if (parent === dir) break;
|
|
2167
|
-
dir = parent;
|
|
2168
|
-
}
|
|
2169
|
-
if (!gitPath || statSync(gitPath).isDirectory()) return lane;
|
|
2170
|
-
const gitFile = readFileSync5(gitPath, "utf8");
|
|
2171
|
-
const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
|
|
2172
|
-
if (!common) return lane;
|
|
2173
|
-
const worktreesDir = join8(common[1], "worktrees");
|
|
2174
|
-
const siblings = readdirSync(worktreesDir).filter((n) => {
|
|
2175
|
-
try {
|
|
2176
|
-
return statSync(join8(worktreesDir, n)).isDirectory();
|
|
2177
|
-
} catch {
|
|
2178
|
-
return false;
|
|
2179
|
-
}
|
|
2180
|
-
});
|
|
2181
|
-
return laneWithWorktreeSuffix(lane, gitFile, siblings);
|
|
2182
|
-
} catch {
|
|
2183
|
-
return lane;
|
|
2184
|
-
}
|
|
2185
|
-
}
|
|
2186
|
-
function laneWithWorktreeSuffix(lane, gitFile, siblings) {
|
|
2187
|
-
const m = gitFile.trim().match(/\/worktrees\/([^/\s]+)\s*$/);
|
|
2188
|
-
if (!m) return lane;
|
|
2189
|
-
const idx = [...siblings].sort().indexOf(m[1]);
|
|
2190
|
-
return idx < 0 ? lane : `${lane}-${idx + 2}`;
|
|
2191
|
-
}
|
|
2192
|
-
function serializeSem(values) {
|
|
2193
|
-
return JSON.stringify(values, null, 2) + "\n";
|
|
2194
|
-
}
|
|
2195
|
-
function readSem(path) {
|
|
2196
|
-
const p = path ?? resolveSemPathForRead();
|
|
2197
|
-
if (!p || !existsSync7(p)) return void 0;
|
|
2198
|
-
return { path: p, values: parseLaneJson(readFileSync5(p, "utf8")) };
|
|
2199
|
-
}
|
|
2200
|
-
function readLocalSemValues(cwd = process.cwd()) {
|
|
2201
|
-
const next = join8(cwd, SEM_FILE);
|
|
2202
|
-
if (existsSync7(next)) return readSem(next)?.values ?? {};
|
|
2203
|
-
return {};
|
|
2204
|
-
}
|
|
2205
|
-
function parseLaneJson(text2) {
|
|
2206
|
-
try {
|
|
2207
|
-
const parsed = JSON.parse(text2);
|
|
2208
|
-
const out = {};
|
|
2209
|
-
for (const [k, v] of Object.entries(parsed)) {
|
|
2210
|
-
if (typeof v === "string") out[k] = v;
|
|
2211
|
-
}
|
|
2212
|
-
return out;
|
|
2213
|
-
} catch {
|
|
2214
|
-
return {};
|
|
2215
|
-
}
|
|
2216
|
-
}
|
|
2217
|
-
var STATE_DIR_IGNORE = `${STATE_DIR_NAME2}/`;
|
|
2218
|
-
function writeSem(values, path = localSemPath()) {
|
|
2219
|
-
mkdirSync6(dirname5(path), { recursive: true });
|
|
2220
|
-
writeFileSync6(path, serializeSem(values));
|
|
2221
|
-
ensureSemIgnored(path);
|
|
2222
|
-
ensureContinuityScaffold(path);
|
|
2223
|
-
return path;
|
|
2224
|
-
}
|
|
2225
|
-
function ensureStateDirIgnored(cwd = process.cwd()) {
|
|
2226
|
-
ensureSemIgnored(localSemPath(cwd));
|
|
2227
|
-
}
|
|
2228
|
-
var CONTINUITY_FILE_NAME = "continuity.json";
|
|
2229
|
-
var CONTINUITY_SCAFFOLD = JSON.stringify(
|
|
2230
|
-
{
|
|
2231
|
-
_readme: "Agent-maintained continuity intent. Keep these current during the session; `sechroom checkpoint` and the PreCompact hook snapshot from here. The five required fields (objective, state, lastAction, nextAction, resumeInstruction) must all be non-empty for a snapshot to be created.",
|
|
2232
|
-
objective: "",
|
|
2233
|
-
state: "",
|
|
2234
|
-
lastAction: "",
|
|
2235
|
-
nextAction: "",
|
|
2236
|
-
resumeInstruction: "",
|
|
2237
|
-
constraints: [],
|
|
2238
|
-
questions: [],
|
|
2239
|
-
artifacts: [],
|
|
2240
|
-
confidence: null
|
|
2241
|
-
},
|
|
2242
|
-
null,
|
|
2243
|
-
2
|
|
2244
|
-
) + "\n";
|
|
2245
|
-
function ensureContinuityScaffold(semPath) {
|
|
2246
|
-
try {
|
|
2247
|
-
const target = join8(dirname5(semPath), CONTINUITY_FILE_NAME);
|
|
2248
|
-
if (existsSync7(target)) return;
|
|
2249
|
-
writeFileSync6(target, CONTINUITY_SCAFFOLD);
|
|
2250
|
-
} catch {
|
|
2251
|
-
}
|
|
2252
|
-
}
|
|
2253
|
-
function ignoresSem(content) {
|
|
2254
|
-
return content.split("\n").some((line) => {
|
|
2255
|
-
const t = line.trim();
|
|
2256
|
-
return t === STATE_DIR_NAME2 || t === STATE_DIR_IGNORE || t === `/${STATE_DIR_NAME2}` || t === `/${STATE_DIR_IGNORE}` || t === `**/${STATE_DIR_NAME2}` || t === `**/${STATE_DIR_IGNORE}`;
|
|
2257
|
-
});
|
|
2258
|
-
}
|
|
2259
|
-
function inGitRepo(startDir) {
|
|
2260
|
-
let dir = startDir;
|
|
2261
|
-
for (; ; ) {
|
|
2262
|
-
if (existsSync7(join8(dir, ".git"))) return true;
|
|
2263
|
-
const parent = dirname5(dir);
|
|
2264
|
-
if (parent === dir) return false;
|
|
2265
|
-
dir = parent;
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
function resolveGitignoreTarget(startDir) {
|
|
2269
|
-
let dir = startDir;
|
|
2270
|
-
for (; ; ) {
|
|
2271
|
-
const gi = join8(dir, ".gitignore");
|
|
2272
|
-
if (existsSync7(gi)) return { path: gi, exists: true };
|
|
2273
|
-
const parent = dirname5(dir);
|
|
2274
|
-
if (existsSync7(join8(dir, ".git")) || parent === dir) {
|
|
2275
|
-
return { path: join8(startDir, ".gitignore"), exists: false };
|
|
2276
|
-
}
|
|
2277
|
-
dir = parent;
|
|
2278
|
-
}
|
|
2279
|
-
}
|
|
2280
|
-
function ensureSemIgnored(semPath) {
|
|
2281
|
-
try {
|
|
2282
|
-
const checkoutDir = dirname5(dirname5(semPath));
|
|
2283
|
-
if (!inGitRepo(checkoutDir)) return;
|
|
2284
|
-
const target = resolveGitignoreTarget(checkoutDir);
|
|
2285
|
-
if (target.exists) {
|
|
2286
|
-
const content = readFileSync5(target.path, "utf8");
|
|
2287
|
-
if (ignoresSem(content)) return;
|
|
2288
|
-
const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
2289
|
-
appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
|
|
2290
|
-
`);
|
|
2291
|
-
} else {
|
|
2292
|
-
writeFileSync6(target.path, `${STATE_DIR_IGNORE}
|
|
2293
|
-
`);
|
|
2751
|
+
channelId,
|
|
2752
|
+
text: text2,
|
|
2753
|
+
guildId: opts.guild ?? null,
|
|
2754
|
+
attachedMemoryId: opts.memory ?? null,
|
|
2755
|
+
trackReplies: opts.track,
|
|
2756
|
+
parentMessage: opts.parent ?? null,
|
|
2757
|
+
source: opts.source,
|
|
2758
|
+
as: opts.as
|
|
2759
|
+
}
|
|
2760
|
+
});
|
|
2761
|
+
});
|
|
2762
|
+
if (!data.ok) {
|
|
2763
|
+
if (json) {
|
|
2764
|
+
emit(data, true);
|
|
2765
|
+
} else {
|
|
2766
|
+
process.stderr.write(
|
|
2767
|
+
`${err("\u2717")} send failed: ${data.upstreamError ?? "error"}${data.errorDescription ? ` \u2014 ${data.errorDescription}` : ""}
|
|
2768
|
+
`
|
|
2769
|
+
);
|
|
2770
|
+
}
|
|
2771
|
+
process.exit(1);
|
|
2294
2772
|
}
|
|
2295
|
-
|
|
2296
|
-
|
|
2773
|
+
const idPart = data.persistedId ? ` ${style.dim(`(${data.persistedId})`)}` : "";
|
|
2774
|
+
emitAction(`sent to ${surface} ${style.bold(channelId)}${idPart}`, data, json);
|
|
2775
|
+
});
|
|
2776
|
+
chat.command("messages").description("List recent channel messages (GET /chat/channel-messages/{surface})").action(async (_opts, cmd) => {
|
|
2777
|
+
const { surface, ...globals } = cmd.optsWithGlobals();
|
|
2778
|
+
const cfg = resolveConfig(globals);
|
|
2779
|
+
const data = await runApi("Fetching messages", async () => {
|
|
2780
|
+
const client = await makeClient(cfg);
|
|
2781
|
+
return client.GET("/chat/channel-messages/{surface}", {
|
|
2782
|
+
params: { path: { surface: String(surface) } }
|
|
2783
|
+
});
|
|
2784
|
+
});
|
|
2785
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
2786
|
+
});
|
|
2787
|
+
chat.command("replies <messageId>").description("List thread replies for a message (GET /chat/channel-messages/by-id/{id}/replies)").action(async (messageId, _opts, cmd) => {
|
|
2788
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2789
|
+
const data = await runApi("Fetching replies", async () => {
|
|
2790
|
+
const client = await makeClient(cfg);
|
|
2791
|
+
return client.GET("/chat/channel-messages/by-id/{id}/replies", {
|
|
2792
|
+
params: { path: { id: messageId } }
|
|
2793
|
+
});
|
|
2794
|
+
});
|
|
2795
|
+
emit(data, cmd.optsWithGlobals().json);
|
|
2796
|
+
});
|
|
2797
|
+
chat.command("stop-tracking <messageId>").description("Stop watching a message for replies (POST .../by-id/{id}/stop-tracking-replies)").action(async (messageId, _opts, cmd) => {
|
|
2798
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2799
|
+
const data = await runApi("Stopping reply tracking", async () => {
|
|
2800
|
+
const client = await makeClient(cfg);
|
|
2801
|
+
return client.POST("/chat/channel-messages/by-id/{id}/stop-tracking-replies", {
|
|
2802
|
+
params: { path: { id: messageId } },
|
|
2803
|
+
body: {}
|
|
2804
|
+
});
|
|
2805
|
+
});
|
|
2806
|
+
emitAction(`stopped tracking replies on ${style.bold(messageId)}`, data, cmd.optsWithGlobals().json);
|
|
2807
|
+
});
|
|
2297
2808
|
}
|
|
2298
2809
|
|
|
2810
|
+
// src/commands/checkpoint.ts
|
|
2811
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
2812
|
+
import { dirname as dirname8, join as join11 } from "path";
|
|
2813
|
+
|
|
2299
2814
|
// src/commands/hook.ts
|
|
2815
|
+
import { createHash as createHash2 } from "crypto";
|
|
2816
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync7, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
|
|
2817
|
+
import { dirname as dirname7, join as join10 } from "path";
|
|
2300
2818
|
async function readStdin() {
|
|
2301
2819
|
if (process.stdin.isTTY) return "";
|
|
2302
2820
|
const chunks = [];
|
|
@@ -2320,13 +2838,13 @@ function resolveLane(flagLane, cwd) {
|
|
|
2320
2838
|
if (!base) return void 0;
|
|
2321
2839
|
return applyWorktreeLaneSuffix(base, start);
|
|
2322
2840
|
}
|
|
2323
|
-
var INTENT_FILE =
|
|
2841
|
+
var INTENT_FILE = join10(".sechroom", "continuity.json");
|
|
2324
2842
|
function resolveIntentPath(start) {
|
|
2325
2843
|
let dir = start;
|
|
2326
2844
|
for (; ; ) {
|
|
2327
|
-
const candidate =
|
|
2328
|
-
if (
|
|
2329
|
-
const parent =
|
|
2845
|
+
const candidate = join10(dir, INTENT_FILE);
|
|
2846
|
+
if (existsSync9(candidate)) return candidate;
|
|
2847
|
+
const parent = dirname7(dir);
|
|
2330
2848
|
if (parent === dir) return void 0;
|
|
2331
2849
|
dir = parent;
|
|
2332
2850
|
}
|
|
@@ -2335,7 +2853,7 @@ function readIntent(start) {
|
|
|
2335
2853
|
const path = resolveIntentPath(start);
|
|
2336
2854
|
if (!path) return void 0;
|
|
2337
2855
|
try {
|
|
2338
|
-
return JSON.parse(
|
|
2856
|
+
return JSON.parse(readFileSync7(path, "utf8"));
|
|
2339
2857
|
} catch {
|
|
2340
2858
|
return void 0;
|
|
2341
2859
|
}
|
|
@@ -2377,14 +2895,14 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
|
|
|
2377
2895
|
}
|
|
2378
2896
|
function ledgerPath(start) {
|
|
2379
2897
|
const intent = resolveIntentPath(start);
|
|
2380
|
-
const dir = intent ?
|
|
2381
|
-
return
|
|
2898
|
+
const dir = intent ? dirname7(intent) : join10(start, ".sechroom");
|
|
2899
|
+
return join10(dir, ".checkpoint-state.json");
|
|
2382
2900
|
}
|
|
2383
2901
|
function readLedger(start) {
|
|
2384
2902
|
try {
|
|
2385
2903
|
const p = ledgerPath(start);
|
|
2386
|
-
if (!
|
|
2387
|
-
return JSON.parse(
|
|
2904
|
+
if (!existsSync9(p)) return {};
|
|
2905
|
+
return JSON.parse(readFileSync7(p, "utf8"));
|
|
2388
2906
|
} catch {
|
|
2389
2907
|
return {};
|
|
2390
2908
|
}
|
|
@@ -2431,13 +2949,13 @@ function recordPush(start, intent) {
|
|
|
2431
2949
|
} catch {
|
|
2432
2950
|
mtimeMs = void 0;
|
|
2433
2951
|
}
|
|
2434
|
-
|
|
2952
|
+
mkdirSync8(dirname7(p), { recursive: true });
|
|
2435
2953
|
const ledger = {
|
|
2436
2954
|
lastEpochMs: Date.now(),
|
|
2437
2955
|
lastMtimeMs: mtimeMs,
|
|
2438
2956
|
lastHash: intentHash(intent)
|
|
2439
2957
|
};
|
|
2440
|
-
|
|
2958
|
+
writeFileSync8(p, JSON.stringify(ledger) + "\n");
|
|
2441
2959
|
} catch {
|
|
2442
2960
|
}
|
|
2443
2961
|
}
|
|
@@ -2690,10 +3208,10 @@ Examples:
|
|
|
2690
3208
|
const client = await makeClient(cfg);
|
|
2691
3209
|
return client.POST("/continuity/snapshots", { body });
|
|
2692
3210
|
});
|
|
2693
|
-
const path = resolveIntentPath(cwd) ??
|
|
3211
|
+
const path = resolveIntentPath(cwd) ?? join11(cwd, INTENT_FILE);
|
|
2694
3212
|
const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
|
|
2695
|
-
|
|
2696
|
-
|
|
3213
|
+
mkdirSync9(dirname8(path), { recursive: true });
|
|
3214
|
+
writeFileSync9(path, JSON.stringify(fileBody, null, 2) + "\n");
|
|
2697
3215
|
recordPush(cwd, merged);
|
|
2698
3216
|
if (json) {
|
|
2699
3217
|
emit({ snapshotId: data.snapshotId, lane, scope, file: path }, true);
|
|
@@ -2707,7 +3225,7 @@ Examples:
|
|
|
2707
3225
|
}
|
|
2708
3226
|
|
|
2709
3227
|
// src/commands/close.ts
|
|
2710
|
-
import { readFileSync as
|
|
3228
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
2711
3229
|
var VERDICTS = ["pass", "soft-fail", "plan-invalid", "blocked"];
|
|
2712
3230
|
function registerClose(program2) {
|
|
2713
3231
|
program2.command("close").description(
|
|
@@ -2748,7 +3266,7 @@ Examples:
|
|
|
2748
3266
|
);
|
|
2749
3267
|
let bodyText;
|
|
2750
3268
|
try {
|
|
2751
|
-
bodyText = opts.file ?
|
|
3269
|
+
bodyText = opts.file ? readFileSync8(opts.file, "utf8") : readFileSync8(0, "utf8");
|
|
2752
3270
|
} catch {
|
|
2753
3271
|
fail(
|
|
2754
3272
|
opts.file ? `could not read --file ${opts.file}` : "no closeout body \u2014 pass --file <path> or pipe it on stdin"
|
|
@@ -3140,205 +3658,6 @@ Examples:
|
|
|
3140
3658
|
});
|
|
3141
3659
|
}
|
|
3142
3660
|
|
|
3143
|
-
// src/commands/executor.ts
|
|
3144
|
-
function registerExecutor(program2) {
|
|
3145
|
-
const executor = program2.command("executor").description(
|
|
3146
|
-
"Register and operate a local Claude Code/Codex executor advertisement"
|
|
3147
|
-
);
|
|
3148
|
-
executor.command("submit-connector").description(
|
|
3149
|
-
"Submit a local-session connector definition for governed approval"
|
|
3150
|
-
).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", [
|
|
3151
|
-
"base",
|
|
3152
|
-
"dotnet-10"
|
|
3153
|
-
]).action(async (opts, cmd) => {
|
|
3154
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3155
|
-
const data = await api(cfg, "/connectors/definitions", {
|
|
3156
|
-
method: "POST",
|
|
3157
|
-
body: JSON.stringify({
|
|
3158
|
-
slug: opts.slug,
|
|
3159
|
-
displayName: opts.displayName,
|
|
3160
|
-
runtimeProfiles: opts.profile,
|
|
3161
|
-
connectorKind: "ExecutionRuntime",
|
|
3162
|
-
providerKind: "local-session",
|
|
3163
|
-
dispatchTransport: parseTransport(opts.transport)
|
|
3164
|
-
})
|
|
3165
|
-
});
|
|
3166
|
-
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3167
|
-
if (!cmd.optsWithGlobals().json) {
|
|
3168
|
-
process.stderr.write(
|
|
3169
|
-
style.dim(
|
|
3170
|
-
"approve this connector definition before registering executors\n"
|
|
3171
|
-
)
|
|
3172
|
-
);
|
|
3173
|
-
}
|
|
3174
|
-
});
|
|
3175
|
-
executor.command("register").description(
|
|
3176
|
-
"Create/reuse a SignalR binding and register this local executor instance"
|
|
3177
|
-
).requiredOption(
|
|
3178
|
-
"--instance-key <key>",
|
|
3179
|
-
"Stable key for this concrete session/lane"
|
|
3180
|
-
).requiredOption(
|
|
3181
|
-
"--connector <id>",
|
|
3182
|
-
"Approved local-session ConnectorDefinition id"
|
|
3183
|
-
).option("--runtime <kind>", "claude-code | codex", "claude-code").option(
|
|
3184
|
-
"--relay <id>",
|
|
3185
|
-
"Relay identity shared by sibling instances",
|
|
3186
|
-
"sechroom-cli-local"
|
|
3187
|
-
).option(
|
|
3188
|
-
"--subscription-name <name>",
|
|
3189
|
-
"SignalR delivery binding name",
|
|
3190
|
-
"executor-dispatch"
|
|
3191
|
-
).option(
|
|
3192
|
-
"--capability <key...>",
|
|
3193
|
-
"Capability operation keys claimed by this instance"
|
|
3194
|
-
).option("--tool-set-ref <ref>", "Optional governed tool-set reference").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (opts, cmd) => {
|
|
3195
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3196
|
-
if (!cfg.workspaceId)
|
|
3197
|
-
fail("executor register requires a configured workspaceId");
|
|
3198
|
-
const subscription = await api(
|
|
3199
|
-
cfg,
|
|
3200
|
-
"/me/delivery-subscriptions/signalr",
|
|
3201
|
-
{
|
|
3202
|
-
method: "POST",
|
|
3203
|
-
body: JSON.stringify({
|
|
3204
|
-
name: opts.subscriptionName,
|
|
3205
|
-
enabled: true,
|
|
3206
|
-
filter: { tags: ["kind:task"], workspaceScope: [cfg.workspaceId] }
|
|
3207
|
-
})
|
|
3208
|
-
}
|
|
3209
|
-
);
|
|
3210
|
-
const data = await api(
|
|
3211
|
-
cfg,
|
|
3212
|
-
"/me/executor-instances",
|
|
3213
|
-
{
|
|
3214
|
-
method: "POST",
|
|
3215
|
-
body: JSON.stringify({
|
|
3216
|
-
relayId: opts.relay,
|
|
3217
|
-
instanceKey: opts.instanceKey,
|
|
3218
|
-
runtimeKind: parseRuntimeKind(opts.runtime),
|
|
3219
|
-
activationMode: "Attached",
|
|
3220
|
-
deliverySubscriptionId: subscription.id,
|
|
3221
|
-
connectorId: opts.connector,
|
|
3222
|
-
claimedCapabilityKeys: opts.capability ?? [],
|
|
3223
|
-
toolSetRef: opts.toolSetRef ?? null,
|
|
3224
|
-
ttlSeconds: opts.ttl
|
|
3225
|
-
})
|
|
3226
|
-
}
|
|
3227
|
-
);
|
|
3228
|
-
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3229
|
-
if (!cmd.optsWithGlobals().json)
|
|
3230
|
-
process.stderr.write(
|
|
3231
|
-
style.dim(`refresh with: sechroom executor heartbeat ${data.id}
|
|
3232
|
-
`)
|
|
3233
|
-
);
|
|
3234
|
-
});
|
|
3235
|
-
executor.command("refresh <id>").description("Refresh one advertisement lease once").option("--ttl <seconds>", "Advertisement TTL (30-600)", parseInteger, 120).action(async (id, opts, cmd) => {
|
|
3236
|
-
const data = await refresh(
|
|
3237
|
-
resolveConfig(cmd.optsWithGlobals()),
|
|
3238
|
-
id,
|
|
3239
|
-
opts.ttl
|
|
3240
|
-
);
|
|
3241
|
-
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3242
|
-
});
|
|
3243
|
-
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) => {
|
|
3244
|
-
if (opts.interval >= opts.ttl)
|
|
3245
|
-
fail("heartbeat interval must be shorter than the TTL");
|
|
3246
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3247
|
-
await refresh(cfg, id, opts.ttl);
|
|
3248
|
-
process.stderr.write(
|
|
3249
|
-
style.green("executor heartbeat active") + style.dim(` \u2014 ${id}
|
|
3250
|
-
`)
|
|
3251
|
-
);
|
|
3252
|
-
await holdHeartbeat(async () => {
|
|
3253
|
-
await refresh(cfg, id, opts.ttl);
|
|
3254
|
-
}, opts.interval * 1e3);
|
|
3255
|
-
});
|
|
3256
|
-
executor.command("offers <id>").description("List live dispatch offers addressed to this exact instance").action(async (id, _opts, cmd) => {
|
|
3257
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3258
|
-
const data = await api(
|
|
3259
|
-
cfg,
|
|
3260
|
-
`/me/executor-instances/${encodeURIComponent(id)}/dispatch-offers`
|
|
3261
|
-
);
|
|
3262
|
-
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3263
|
-
});
|
|
3264
|
-
executor.command("deregister <id>").description("Stop advertising this executor instance").action(async (id, _opts, cmd) => {
|
|
3265
|
-
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3266
|
-
const data = await api(
|
|
3267
|
-
cfg,
|
|
3268
|
-
`/me/executor-instances/${encodeURIComponent(id)}`,
|
|
3269
|
-
{
|
|
3270
|
-
method: "DELETE",
|
|
3271
|
-
body: JSON.stringify({})
|
|
3272
|
-
}
|
|
3273
|
-
);
|
|
3274
|
-
emit(data, Boolean(cmd.optsWithGlobals().json));
|
|
3275
|
-
});
|
|
3276
|
-
}
|
|
3277
|
-
function parseRuntimeKind(value) {
|
|
3278
|
-
switch (value.trim().toLowerCase()) {
|
|
3279
|
-
case "claude":
|
|
3280
|
-
case "claude-code":
|
|
3281
|
-
return "ClaudeCode";
|
|
3282
|
-
case "codex":
|
|
3283
|
-
return "Codex";
|
|
3284
|
-
default:
|
|
3285
|
-
return fail("runtime must be claude-code or codex");
|
|
3286
|
-
}
|
|
3287
|
-
}
|
|
3288
|
-
function parseTransport(value) {
|
|
3289
|
-
switch (value.trim().toLowerCase()) {
|
|
3290
|
-
case "push":
|
|
3291
|
-
return "Push";
|
|
3292
|
-
case "pull":
|
|
3293
|
-
return "Pull";
|
|
3294
|
-
default:
|
|
3295
|
-
return fail("transport must be push or pull");
|
|
3296
|
-
}
|
|
3297
|
-
}
|
|
3298
|
-
async function refresh(cfg, id, ttlSeconds) {
|
|
3299
|
-
return api(
|
|
3300
|
-
cfg,
|
|
3301
|
-
`/me/executor-instances/${encodeURIComponent(id)}/refresh`,
|
|
3302
|
-
{
|
|
3303
|
-
method: "POST",
|
|
3304
|
-
body: JSON.stringify({ ttlSeconds })
|
|
3305
|
-
}
|
|
3306
|
-
);
|
|
3307
|
-
}
|
|
3308
|
-
async function api(cfg, path, init) {
|
|
3309
|
-
const token = await requireToken(cfg);
|
|
3310
|
-
const response = await fetch(`${cfg.baseUrl}${path}`, {
|
|
3311
|
-
...init,
|
|
3312
|
-
headers: {
|
|
3313
|
-
authorization: `Bearer ${token}`,
|
|
3314
|
-
tenant: cfg.tenant,
|
|
3315
|
-
"content-type": "application/json",
|
|
3316
|
-
"x-sechroom-surface": "cli"
|
|
3317
|
-
}
|
|
3318
|
-
});
|
|
3319
|
-
if (!response.ok)
|
|
3320
|
-
fail(
|
|
3321
|
-
`${init?.method ?? "GET"} ${path} failed (${response.status}): ${await response.text()}`
|
|
3322
|
-
);
|
|
3323
|
-
return response.json();
|
|
3324
|
-
}
|
|
3325
|
-
function parseInteger(value) {
|
|
3326
|
-
const parsed = Number.parseInt(value, 10);
|
|
3327
|
-
if (!Number.isFinite(parsed)) fail(`expected an integer, got '${value}'`);
|
|
3328
|
-
return parsed;
|
|
3329
|
-
}
|
|
3330
|
-
function holdHeartbeat(tick, intervalMs) {
|
|
3331
|
-
return new Promise((resolve3, reject) => {
|
|
3332
|
-
const timer = setInterval(() => void tick().catch(reject), intervalMs);
|
|
3333
|
-
const stop = () => {
|
|
3334
|
-
clearInterval(timer);
|
|
3335
|
-
resolve3();
|
|
3336
|
-
};
|
|
3337
|
-
process.once("SIGINT", stop);
|
|
3338
|
-
process.once("SIGTERM", stop);
|
|
3339
|
-
});
|
|
3340
|
-
}
|
|
3341
|
-
|
|
3342
3661
|
// src/commands/filing.ts
|
|
3343
3662
|
function registerFiling(program2) {
|
|
3344
3663
|
const filing = program2.command("filing").description("Review and act on filing suggestions");
|
|
@@ -3850,8 +4169,8 @@ Examples:
|
|
|
3850
4169
|
|
|
3851
4170
|
// src/setup/apply.ts
|
|
3852
4171
|
import { createHash as createHash3 } from "crypto";
|
|
3853
|
-
import { mkdirSync as
|
|
3854
|
-
import { dirname as
|
|
4172
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync10, existsSync as existsSync10 } from "fs";
|
|
4173
|
+
import { dirname as dirname9 } from "path";
|
|
3855
4174
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
3856
4175
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
3857
4176
|
function normalizeBody(s) {
|
|
@@ -3904,22 +4223,22 @@ function parseManagedBlock(content, block) {
|
|
|
3904
4223
|
return null;
|
|
3905
4224
|
}
|
|
3906
4225
|
function ensureDir2(path) {
|
|
3907
|
-
|
|
4226
|
+
mkdirSync10(dirname9(path), { recursive: true });
|
|
3908
4227
|
}
|
|
3909
4228
|
function readOr(path, fallback) {
|
|
3910
4229
|
try {
|
|
3911
|
-
return
|
|
4230
|
+
return readFileSync9(path, "utf8");
|
|
3912
4231
|
} catch {
|
|
3913
4232
|
return fallback;
|
|
3914
4233
|
}
|
|
3915
4234
|
}
|
|
3916
4235
|
function mergeMcpJson(path, snippet, dryRun) {
|
|
3917
4236
|
const incoming = JSON.parse(snippet);
|
|
3918
|
-
const existed =
|
|
4237
|
+
const existed = existsSync10(path);
|
|
3919
4238
|
let current = {};
|
|
3920
4239
|
if (existed) {
|
|
3921
4240
|
try {
|
|
3922
|
-
current = JSON.parse(
|
|
4241
|
+
current = JSON.parse(readFileSync9(path, "utf8"));
|
|
3923
4242
|
} catch {
|
|
3924
4243
|
return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
|
|
3925
4244
|
}
|
|
@@ -3927,26 +4246,26 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
3927
4246
|
current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
|
|
3928
4247
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3929
4248
|
ensureDir2(path);
|
|
3930
|
-
|
|
4249
|
+
writeFileSync10(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
3931
4250
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3932
4251
|
}
|
|
3933
4252
|
function mergeCodexToml(path, snippet, dryRun) {
|
|
3934
|
-
const existed =
|
|
4253
|
+
const existed = existsSync10(path);
|
|
3935
4254
|
let body = readOr(path, "");
|
|
3936
4255
|
body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
|
|
3937
4256
|
const trimmed = body.trim();
|
|
3938
4257
|
const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
|
|
3939
4258
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3940
4259
|
ensureDir2(path);
|
|
3941
|
-
|
|
4260
|
+
writeFileSync10(path, next, { mode: 384 });
|
|
3942
4261
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3943
4262
|
}
|
|
3944
4263
|
function writeInstructionBlock(path, write, dryRun) {
|
|
3945
|
-
const existed =
|
|
4264
|
+
const existed = existsSync10(path);
|
|
3946
4265
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
3947
4266
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
3948
4267
|
ensureDir2(path);
|
|
3949
|
-
|
|
4268
|
+
writeFileSync10(path, next);
|
|
3950
4269
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
3951
4270
|
}
|
|
3952
4271
|
function computeBlockFile(current, write) {
|
|
@@ -3987,7 +4306,7 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
3987
4306
|
const next = computeBlockFile(current, write);
|
|
3988
4307
|
if (!dryRun) {
|
|
3989
4308
|
ensureDir2(proposedPath);
|
|
3990
|
-
|
|
4309
|
+
writeFileSync10(proposedPath, next);
|
|
3991
4310
|
}
|
|
3992
4311
|
return {
|
|
3993
4312
|
kind: "instruction",
|
|
@@ -4117,8 +4436,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
4117
4436
|
}
|
|
4118
4437
|
|
|
4119
4438
|
// src/setup/skills-offer.ts
|
|
4120
|
-
import { mkdirSync as
|
|
4121
|
-
import { join as
|
|
4439
|
+
import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
|
|
4440
|
+
import { join as join12 } from "path";
|
|
4122
4441
|
|
|
4123
4442
|
// src/setup/lane-pin.ts
|
|
4124
4443
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -4234,8 +4553,8 @@ Found ${summary} available to you for ${surface}.
|
|
|
4234
4553
|
if (skills.length > 0) {
|
|
4235
4554
|
const written = [];
|
|
4236
4555
|
for (const s of skills) {
|
|
4237
|
-
|
|
4238
|
-
|
|
4556
|
+
mkdirSync11(join12(sDir, s.name), { recursive: true });
|
|
4557
|
+
writeFileSync11(join12(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
4239
4558
|
written.push(s.name);
|
|
4240
4559
|
}
|
|
4241
4560
|
recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -4243,11 +4562,11 @@ Found ${summary} available to you for ${surface}.
|
|
|
4243
4562
|
`);
|
|
4244
4563
|
}
|
|
4245
4564
|
if (agents.length > 0) {
|
|
4246
|
-
|
|
4565
|
+
mkdirSync11(aDir, { recursive: true });
|
|
4247
4566
|
const written = [];
|
|
4248
4567
|
for (const a of agents) {
|
|
4249
4568
|
const file = `${a.name}.md`;
|
|
4250
|
-
|
|
4569
|
+
writeFileSync11(join12(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
4251
4570
|
written.push(file);
|
|
4252
4571
|
}
|
|
4253
4572
|
recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -4630,13 +4949,13 @@ Wired to namespace '${slug}'. Restart your AI client (or reload MCP) to pick it
|
|
|
4630
4949
|
}
|
|
4631
4950
|
|
|
4632
4951
|
// src/commands/onboard.ts
|
|
4633
|
-
import { existsSync as
|
|
4634
|
-
import { basename as basename2, join as
|
|
4952
|
+
import { existsSync as existsSync12 } from "fs";
|
|
4953
|
+
import { basename as basename2, join as join14 } from "path";
|
|
4635
4954
|
|
|
4636
4955
|
// src/commands/fanout.ts
|
|
4637
4956
|
import { spawnSync } from "child_process";
|
|
4638
|
-
import { existsSync as
|
|
4639
|
-
import { isAbsolute, join as
|
|
4957
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
4958
|
+
import { isAbsolute, join as join13, resolve } from "path";
|
|
4640
4959
|
var ICON = {
|
|
4641
4960
|
refresh: "\u21BB",
|
|
4642
4961
|
bind: "+",
|
|
@@ -4656,21 +4975,21 @@ function discoverChildren(root) {
|
|
|
4656
4975
|
const out = [];
|
|
4657
4976
|
for (const name of names.sort()) {
|
|
4658
4977
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
4659
|
-
const dir =
|
|
4978
|
+
const dir = join13(root, name);
|
|
4660
4979
|
try {
|
|
4661
4980
|
if (!statSync3(dir).isDirectory()) continue;
|
|
4662
4981
|
} catch {
|
|
4663
4982
|
continue;
|
|
4664
4983
|
}
|
|
4665
|
-
if (
|
|
4984
|
+
if (existsSync11(join13(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
4666
4985
|
}
|
|
4667
4986
|
return out;
|
|
4668
4987
|
}
|
|
4669
4988
|
function readManifest(path) {
|
|
4670
|
-
if (!
|
|
4989
|
+
if (!existsSync11(path)) return null;
|
|
4671
4990
|
let parsed;
|
|
4672
4991
|
try {
|
|
4673
|
-
parsed = JSON.parse(
|
|
4992
|
+
parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
4674
4993
|
} catch (err2) {
|
|
4675
4994
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
4676
4995
|
}
|
|
@@ -5036,10 +5355,10 @@ async function chooseScope(scopeFlag, yes) {
|
|
|
5036
5355
|
}
|
|
5037
5356
|
async function planRecurseChild(entry, root, client, opts) {
|
|
5038
5357
|
const dir = resolveChildDir(entry.path, root);
|
|
5039
|
-
if (!
|
|
5358
|
+
if (!existsSync12(dir)) {
|
|
5040
5359
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
5041
5360
|
}
|
|
5042
|
-
if (
|
|
5361
|
+
if (existsSync12(join14(dir, ".sechroom.json"))) {
|
|
5043
5362
|
return {
|
|
5044
5363
|
label: entry.path,
|
|
5045
5364
|
dir,
|
|
@@ -5112,7 +5431,7 @@ This fan-out will pin the same lane in every repo:
|
|
|
5112
5431
|
async function runRecurse(cfg, g, opts) {
|
|
5113
5432
|
const { yes, dryRun, json } = opts;
|
|
5114
5433
|
const root = process.cwd();
|
|
5115
|
-
const manifestPath =
|
|
5434
|
+
const manifestPath = join14(root, ".sechroom", "repos.json");
|
|
5116
5435
|
const fromManifest = readManifest(manifestPath);
|
|
5117
5436
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
5118
5437
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
@@ -5645,23 +5964,23 @@ Examples:
|
|
|
5645
5964
|
|
|
5646
5965
|
// src/commands/reset.ts
|
|
5647
5966
|
import { homedir as homedir4 } from "os";
|
|
5648
|
-
import { join as
|
|
5649
|
-
import { existsSync as
|
|
5967
|
+
import { join as join15 } from "path";
|
|
5968
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11, rmSync as rmSync3 } from "fs";
|
|
5650
5969
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
5651
|
-
var localSkillsDir = () =>
|
|
5652
|
-
var globalSkillsDir = () =>
|
|
5653
|
-
var localAgentsDir = () =>
|
|
5654
|
-
var globalAgentsDir = () =>
|
|
5970
|
+
var localSkillsDir = () => join15(process.cwd(), ".claude", "skills");
|
|
5971
|
+
var globalSkillsDir = () => join15(homedir4(), ".claude", "skills");
|
|
5972
|
+
var localAgentsDir = () => join15(process.cwd(), ".claude", "agents");
|
|
5973
|
+
var globalAgentsDir = () => join15(homedir4(), ".claude", "agents");
|
|
5655
5974
|
function removeMaterialisedSkills(dir) {
|
|
5656
5975
|
const removed = [];
|
|
5657
|
-
const lockPath =
|
|
5658
|
-
if (!
|
|
5976
|
+
const lockPath = join15(dir, SKILLS_LOCK2);
|
|
5977
|
+
if (!existsSync13(lockPath)) return removed;
|
|
5659
5978
|
try {
|
|
5660
|
-
const lock = JSON.parse(
|
|
5979
|
+
const lock = JSON.parse(readFileSync11(lockPath, "utf8"));
|
|
5661
5980
|
for (const entry of Object.values(lock)) {
|
|
5662
5981
|
for (const name of entry.skills ?? []) {
|
|
5663
|
-
const p =
|
|
5664
|
-
if (
|
|
5982
|
+
const p = join15(dir, name);
|
|
5983
|
+
if (existsSync13(p)) {
|
|
5665
5984
|
rmSync3(p, { recursive: true, force: true });
|
|
5666
5985
|
removed.push(p);
|
|
5667
5986
|
}
|
|
@@ -5706,18 +6025,18 @@ function registerReset(program2) {
|
|
|
5706
6025
|
}
|
|
5707
6026
|
}
|
|
5708
6027
|
const removed = [];
|
|
5709
|
-
const stateDir =
|
|
5710
|
-
if (
|
|
6028
|
+
const stateDir = join15(process.cwd(), ".sechroom");
|
|
6029
|
+
if (existsSync13(stateDir)) {
|
|
5711
6030
|
rmSync3(stateDir, { recursive: true, force: true });
|
|
5712
6031
|
removed.push(stateDir);
|
|
5713
6032
|
}
|
|
5714
|
-
const legacyCfg =
|
|
5715
|
-
if (
|
|
6033
|
+
const legacyCfg = join15(process.cwd(), ".sechroom.json");
|
|
6034
|
+
if (existsSync13(legacyCfg)) {
|
|
5716
6035
|
rmSync3(legacyCfg, { force: true });
|
|
5717
6036
|
removed.push(legacyCfg);
|
|
5718
6037
|
}
|
|
5719
|
-
const legacySem =
|
|
5720
|
-
if (
|
|
6038
|
+
const legacySem = join15(process.cwd(), ".sem");
|
|
6039
|
+
if (existsSync13(legacySem)) {
|
|
5721
6040
|
rmSync3(legacySem, { force: true });
|
|
5722
6041
|
removed.push(legacySem);
|
|
5723
6042
|
}
|
|
@@ -5743,8 +6062,8 @@ function registerReset(program2) {
|
|
|
5743
6062
|
}
|
|
5744
6063
|
|
|
5745
6064
|
// src/commands/skills.ts
|
|
5746
|
-
import { existsSync as
|
|
5747
|
-
import { join as
|
|
6065
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync12, statSync as statSync4, writeFileSync as writeFileSync12 } from "fs";
|
|
6066
|
+
import { join as join16 } from "path";
|
|
5748
6067
|
function filenameFromDisposition(header) {
|
|
5749
6068
|
if (!header) return void 0;
|
|
5750
6069
|
const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
|
|
@@ -5752,11 +6071,11 @@ function filenameFromDisposition(header) {
|
|
|
5752
6071
|
}
|
|
5753
6072
|
function resolveOutputPath(output, serverFilename) {
|
|
5754
6073
|
const filename = serverFilename || "skills.zip";
|
|
5755
|
-
if (!output) return
|
|
5756
|
-
const looksLikeDir = output.endsWith("/") ||
|
|
6074
|
+
if (!output) return join16(process.cwd(), filename);
|
|
6075
|
+
const looksLikeDir = output.endsWith("/") || existsSync14(output) && statSync4(output).isDirectory();
|
|
5757
6076
|
if (looksLikeDir) {
|
|
5758
|
-
|
|
5759
|
-
return
|
|
6077
|
+
mkdirSync12(output, { recursive: true });
|
|
6078
|
+
return join16(output, filename);
|
|
5760
6079
|
}
|
|
5761
6080
|
return output;
|
|
5762
6081
|
}
|
|
@@ -5787,7 +6106,7 @@ async function downloadZip(label, call, output) {
|
|
|
5787
6106
|
const buf = Buffer.from(res.data);
|
|
5788
6107
|
const filename = filenameFromDisposition(res.response.headers.get("content-disposition")) ?? "skills.zip";
|
|
5789
6108
|
const path = resolveOutputPath(output, filename);
|
|
5790
|
-
|
|
6109
|
+
writeFileSync12(path, buf);
|
|
5791
6110
|
return { path, bytes: buf.length, filename };
|
|
5792
6111
|
}
|
|
5793
6112
|
function registerSkills(program2) {
|
|
@@ -5961,12 +6280,12 @@ clients select all; an unconfigured machine preserves the legacy Claude default.
|
|
|
5961
6280
|
}
|
|
5962
6281
|
|
|
5963
6282
|
// src/commands/sweep.ts
|
|
5964
|
-
import { existsSync as
|
|
5965
|
-
import { dirname as
|
|
5966
|
-
var DEFAULT_MANIFEST =
|
|
6283
|
+
import { existsSync as existsSync15 } from "fs";
|
|
6284
|
+
import { dirname as dirname10, join as join17, resolve as resolve2 } from "path";
|
|
6285
|
+
var DEFAULT_MANIFEST = join17(".sechroom", "repos.json");
|
|
5967
6286
|
function planEntry(entry, root) {
|
|
5968
6287
|
const dir = resolveChildDir(entry.path, root);
|
|
5969
|
-
if (!
|
|
6288
|
+
if (!existsSync15(dir)) {
|
|
5970
6289
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
5971
6290
|
}
|
|
5972
6291
|
if (committedBindingPath(dir)) {
|
|
@@ -6042,7 +6361,7 @@ Examples:
|
|
|
6042
6361
|
`);
|
|
6043
6362
|
return;
|
|
6044
6363
|
}
|
|
6045
|
-
const root =
|
|
6364
|
+
const root = dirname10(dirname10(manifestPath));
|
|
6046
6365
|
const plans = repos.map((entry) => planEntry(entry, root));
|
|
6047
6366
|
if (!json) {
|
|
6048
6367
|
process.stderr.write(
|
|
@@ -6061,13 +6380,13 @@ Examples:
|
|
|
6061
6380
|
|
|
6062
6381
|
// src/commands/telemetry.ts
|
|
6063
6382
|
import {
|
|
6064
|
-
existsSync as
|
|
6065
|
-
mkdirSync as
|
|
6066
|
-
readFileSync as
|
|
6383
|
+
existsSync as existsSync16,
|
|
6384
|
+
mkdirSync as mkdirSync13,
|
|
6385
|
+
readFileSync as readFileSync12,
|
|
6067
6386
|
rmSync as rmSync4,
|
|
6068
|
-
writeFileSync as
|
|
6387
|
+
writeFileSync as writeFileSync13
|
|
6069
6388
|
} from "fs";
|
|
6070
|
-
import { dirname as
|
|
6389
|
+
import { dirname as dirname11, join as join18 } from "path";
|
|
6071
6390
|
function registerTelemetry(program2) {
|
|
6072
6391
|
const telemetry = program2.command("telemetry").description(
|
|
6073
6392
|
"Emit WLP run telemetry (an executor leg's progress events) into a decomposition run"
|
|
@@ -6137,14 +6456,14 @@ function registerTelemetry(program2) {
|
|
|
6137
6456
|
"Decomposition id this session executes"
|
|
6138
6457
|
).requiredOption("--task <id>", "Task id this session executes").action((opts, cmd) => {
|
|
6139
6458
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
6140
|
-
const dir =
|
|
6141
|
-
|
|
6142
|
-
const path =
|
|
6459
|
+
const dir = join18(process.cwd(), ".sechroom");
|
|
6460
|
+
mkdirSync13(dir, { recursive: true });
|
|
6461
|
+
const path = join18(dir, BINDING_FILE);
|
|
6143
6462
|
const binding = {
|
|
6144
6463
|
decompositionId: opts.decomposition,
|
|
6145
6464
|
taskId: opts.task
|
|
6146
6465
|
};
|
|
6147
|
-
|
|
6466
|
+
writeFileSync13(path, JSON.stringify(binding, null, 2) + "\n");
|
|
6148
6467
|
ensureStateDirIgnored(process.cwd());
|
|
6149
6468
|
if (json) {
|
|
6150
6469
|
emit({ bound: true, ...binding, path }, true);
|
|
@@ -6159,8 +6478,8 @@ function registerTelemetry(program2) {
|
|
|
6159
6478
|
});
|
|
6160
6479
|
telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
|
|
6161
6480
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
6162
|
-
const path =
|
|
6163
|
-
const existed =
|
|
6481
|
+
const path = join18(process.cwd(), ".sechroom", BINDING_FILE);
|
|
6482
|
+
const existed = existsSync16(path);
|
|
6164
6483
|
if (existed) rmSync4(path);
|
|
6165
6484
|
if (json) emit({ unbound: existed, path }, true);
|
|
6166
6485
|
else
|
|
@@ -6268,11 +6587,11 @@ async function postTelemetry(cfg, decompositionId, events) {
|
|
|
6268
6587
|
function findBinding(start) {
|
|
6269
6588
|
let dir = start;
|
|
6270
6589
|
for (; ; ) {
|
|
6271
|
-
const path =
|
|
6272
|
-
if (
|
|
6590
|
+
const path = join18(dir, ".sechroom", BINDING_FILE);
|
|
6591
|
+
if (existsSync16(path)) {
|
|
6273
6592
|
try {
|
|
6274
6593
|
const b = JSON.parse(
|
|
6275
|
-
|
|
6594
|
+
readFileSync12(path, "utf8")
|
|
6276
6595
|
);
|
|
6277
6596
|
if (b.decompositionId && b.taskId)
|
|
6278
6597
|
return { decompositionId: b.decompositionId, taskId: b.taskId };
|
|
@@ -6280,18 +6599,18 @@ function findBinding(start) {
|
|
|
6280
6599
|
}
|
|
6281
6600
|
return null;
|
|
6282
6601
|
}
|
|
6283
|
-
const parent =
|
|
6602
|
+
const parent = dirname11(dir);
|
|
6284
6603
|
if (parent === dir) return null;
|
|
6285
6604
|
dir = parent;
|
|
6286
6605
|
}
|
|
6287
6606
|
}
|
|
6288
6607
|
function parseTranscript(path) {
|
|
6289
|
-
if (!
|
|
6608
|
+
if (!existsSync16(path)) return null;
|
|
6290
6609
|
let tokensIn = 0;
|
|
6291
6610
|
let tokensOut = 0;
|
|
6292
6611
|
let contextUsed = 0;
|
|
6293
6612
|
let model = "";
|
|
6294
|
-
for (const line of
|
|
6613
|
+
for (const line of readFileSync12(path, "utf8").split("\n")) {
|
|
6295
6614
|
if (!line.trim()) continue;
|
|
6296
6615
|
let obj;
|
|
6297
6616
|
try {
|
|
@@ -6598,7 +6917,7 @@ Examples:
|
|
|
6598
6917
|
function resolveVersion() {
|
|
6599
6918
|
try {
|
|
6600
6919
|
const pkg = JSON.parse(
|
|
6601
|
-
|
|
6920
|
+
readFileSync13(new URL("../package.json", import.meta.url), "utf8")
|
|
6602
6921
|
);
|
|
6603
6922
|
return pkg.version ?? "0.0.0";
|
|
6604
6923
|
} catch {
|