@sechroom/cli 2026.7.1 → 2026.7.2-rc.61703375
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +855 -432
- 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 readFileSync12 } from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/auth.ts
|
|
@@ -1300,12 +1300,228 @@ workers your loop skills call (e.g. find-prior-art \u2192 substrate-miner).`
|
|
|
1300
1300
|
}
|
|
1301
1301
|
|
|
1302
1302
|
// src/commands/channel.ts
|
|
1303
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
1304
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
1303
1305
|
import {
|
|
1304
1306
|
HttpTransportType,
|
|
1305
1307
|
HubConnectionBuilder
|
|
1306
1308
|
} from "@microsoft/signalr";
|
|
1307
1309
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1308
1310
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1311
|
+
|
|
1312
|
+
// src/commands/hook-install.ts
|
|
1313
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1314
|
+
import { delimiter, dirname as dirname3, join as join6 } from "path";
|
|
1315
|
+
|
|
1316
|
+
// src/setup/clients.ts
|
|
1317
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1318
|
+
import { homedir as homedir3 } from "os";
|
|
1319
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
1320
|
+
function claudeDesktopConfigPath(home) {
|
|
1321
|
+
switch (process.platform) {
|
|
1322
|
+
case "darwin":
|
|
1323
|
+
return join5(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
1324
|
+
case "win32":
|
|
1325
|
+
return join5(process.env.APPDATA ?? join5(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
1326
|
+
default:
|
|
1327
|
+
return join5(home, ".config", "Claude", "claude_desktop_config.json");
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function clientTargets(cwd, opts = {}) {
|
|
1331
|
+
const home = homedir3();
|
|
1332
|
+
const claudeDir = opts.claudeDir ?? join5(home, ".claude");
|
|
1333
|
+
const codexHome = opts.codexHome ?? join5(home, ".codex");
|
|
1334
|
+
return {
|
|
1335
|
+
"claude-code": {
|
|
1336
|
+
key: "claude-code",
|
|
1337
|
+
label: "Claude Code",
|
|
1338
|
+
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join5(cwd, ".mcp.json"), format: "json" },
|
|
1339
|
+
instruction: { surfaceKey: "claude-code", path: join5(cwd, "CLAUDE.md") }
|
|
1340
|
+
},
|
|
1341
|
+
"claude-desktop": {
|
|
1342
|
+
key: "claude-desktop",
|
|
1343
|
+
label: "Claude Desktop",
|
|
1344
|
+
mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
|
|
1345
|
+
instruction: { surfaceKey: "claude-desktop", path: join5(claudeDir, "CLAUDE.md") }
|
|
1346
|
+
},
|
|
1347
|
+
codex: {
|
|
1348
|
+
key: "codex",
|
|
1349
|
+
label: "Codex CLI",
|
|
1350
|
+
mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join5(codexHome, "config.toml"), format: "toml" },
|
|
1351
|
+
instruction: { surfaceKey: "chatgpt", path: join5(cwd, "AGENTS.md") }
|
|
1352
|
+
},
|
|
1353
|
+
cursor: {
|
|
1354
|
+
key: "cursor",
|
|
1355
|
+
label: "Cursor",
|
|
1356
|
+
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join5(cwd, ".cursor", "mcp.json"), format: "json" },
|
|
1357
|
+
instruction: { surfaceKey: "chatgpt", path: join5(cwd, "AGENTS.md") }
|
|
1358
|
+
},
|
|
1359
|
+
antigravity: {
|
|
1360
|
+
key: "antigravity",
|
|
1361
|
+
label: "Google Antigravity",
|
|
1362
|
+
// FR-sechroom-247 — Antigravity reads MCP from a GLOBAL, home-relative
|
|
1363
|
+
// `~/.gemini/config/mcp_config.json` (not cwd; not affected by
|
|
1364
|
+
// CLAUDE_CONFIG_DIR / CODEX_HOME). The snippet — `serverUrl`-shaped, no
|
|
1365
|
+
// `type` — comes from the `antigravity` server surface, so we don't
|
|
1366
|
+
// hardcode it here. Instructions go in the project `AGENTS.md`
|
|
1367
|
+
// (cross-tool, shared with Codex/Cursor).
|
|
1368
|
+
mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path: join5(home, ".gemini", "config", "mcp_config.json"), format: "json" },
|
|
1369
|
+
instruction: { surfaceKey: "antigravity", path: join5(cwd, "AGENTS.md") }
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor", "antigravity"];
|
|
1374
|
+
var DEFAULT_CLIENT_KEY = "claude-code";
|
|
1375
|
+
function detectInstalledClients(cwd) {
|
|
1376
|
+
const home = homedir3();
|
|
1377
|
+
const detected = [];
|
|
1378
|
+
if (resolveClaudeTargets({}).some((t) => existsSync4(t.dir))) detected.push("claude-code");
|
|
1379
|
+
if (existsSync4(dirname2(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
|
|
1380
|
+
if (resolveCodexHomes({}).some((d) => existsSync4(d))) detected.push("codex");
|
|
1381
|
+
if (existsSync4(join5(home, ".cursor")) || existsSync4(join5(cwd, ".cursor"))) detected.push("cursor");
|
|
1382
|
+
if (existsSync4(join5(home, ".gemini"))) detected.push("antigravity");
|
|
1383
|
+
return detected;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// src/commands/hook-install.ts
|
|
1387
|
+
var CLAUDE_HOOK_COMMANDS = {
|
|
1388
|
+
SessionStart: "sechroom hook session-start",
|
|
1389
|
+
PreCompact: "sechroom hook pre-compact",
|
|
1390
|
+
SessionEnd: "sechroom hook session-end",
|
|
1391
|
+
// WLP telemetry tap (D-WLP-10) — per-turn executor self-report. No-op (exit 0) unless this
|
|
1392
|
+
// checkout is bound to a decomposition+task via `sechroom telemetry bind`, so it's safe to wire
|
|
1393
|
+
// for every Claude install. Claude-only (it parses a Claude Code transcript).
|
|
1394
|
+
Stop: "sechroom telemetry hook"
|
|
1395
|
+
};
|
|
1396
|
+
var CODEX_HOOK_COMMANDS = {
|
|
1397
|
+
SessionStart: "sechroom hook session-start",
|
|
1398
|
+
Stop: "sechroom hook session-end --debounce-minutes 10"
|
|
1399
|
+
};
|
|
1400
|
+
function hasHookCommand(config2, event, command) {
|
|
1401
|
+
const groups = config2.hooks?.[event] ?? [];
|
|
1402
|
+
return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
|
|
1403
|
+
}
|
|
1404
|
+
function mergeHooks(config2, commands) {
|
|
1405
|
+
config2.hooks ??= {};
|
|
1406
|
+
let added = 0;
|
|
1407
|
+
for (const [event, command] of Object.entries(commands)) {
|
|
1408
|
+
if (hasHookCommand(config2, event, command)) continue;
|
|
1409
|
+
const groups = config2.hooks[event] ??= [];
|
|
1410
|
+
groups.push({ hooks: [{ type: "command", command }] });
|
|
1411
|
+
added += 1;
|
|
1412
|
+
}
|
|
1413
|
+
return added;
|
|
1414
|
+
}
|
|
1415
|
+
function readJsonConfig2(path) {
|
|
1416
|
+
if (!existsSync5(path)) return {};
|
|
1417
|
+
const raw = readFileSync3(path, "utf8");
|
|
1418
|
+
if (!raw.trim()) return {};
|
|
1419
|
+
return JSON.parse(raw);
|
|
1420
|
+
}
|
|
1421
|
+
function installHooksJson(path, commands, dryRun) {
|
|
1422
|
+
const existed = existsSync5(path) && readFileSync3(path, "utf8").trim().length > 0;
|
|
1423
|
+
const config2 = readJsonConfig2(path);
|
|
1424
|
+
const added = mergeHooks(config2, commands);
|
|
1425
|
+
if (added === 0 && existed) return { path, status: "current" };
|
|
1426
|
+
if (!dryRun) {
|
|
1427
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
1428
|
+
writeFileSync4(path, JSON.stringify(config2, null, 2) + "\n");
|
|
1429
|
+
}
|
|
1430
|
+
return { path, status: existed ? "merged" : "created" };
|
|
1431
|
+
}
|
|
1432
|
+
function installClaudeCommands(claudeDir, commands, dryRun) {
|
|
1433
|
+
return installHooksJson(join6(claudeDir, "settings.json"), commands, dryRun);
|
|
1434
|
+
}
|
|
1435
|
+
function ensureCodexFeaturesHooks(content) {
|
|
1436
|
+
const lines = content.split("\n");
|
|
1437
|
+
const headerIdx = lines.findIndex((l) => l.trim() === "[features]");
|
|
1438
|
+
if (headerIdx === -1) {
|
|
1439
|
+
const base = content.length === 0 || content.endsWith("\n") ? content : content + "\n";
|
|
1440
|
+
return { next: base + "\n[features]\nhooks = true\n", changed: true };
|
|
1441
|
+
}
|
|
1442
|
+
for (let i = headerIdx + 1; i < lines.length; i += 1) {
|
|
1443
|
+
const trimmed = lines[i].trim();
|
|
1444
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) break;
|
|
1445
|
+
const m = lines[i].match(/^(\s*)hooks(\s*)=(\s*)(.*)$/);
|
|
1446
|
+
if (!m) continue;
|
|
1447
|
+
const value = m[4].replace(/\s*#.*$/, "").trim();
|
|
1448
|
+
if (value === "true") return { next: content, changed: false };
|
|
1449
|
+
lines[i] = `${m[1]}hooks${m[2]}=${m[3]}true`;
|
|
1450
|
+
return { next: lines.join("\n"), changed: true };
|
|
1451
|
+
}
|
|
1452
|
+
lines.splice(headerIdx + 1, 0, "hooks = true");
|
|
1453
|
+
return { next: lines.join("\n"), changed: true };
|
|
1454
|
+
}
|
|
1455
|
+
function installCodexFeatureFlag(path, dryRun) {
|
|
1456
|
+
const existed = existsSync5(path);
|
|
1457
|
+
const content = existed ? readFileSync3(path, "utf8") : "";
|
|
1458
|
+
const { next, changed } = ensureCodexFeaturesHooks(content);
|
|
1459
|
+
if (!changed) return { path, status: "current" };
|
|
1460
|
+
if (!dryRun) {
|
|
1461
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
1462
|
+
writeFileSync4(path, next);
|
|
1463
|
+
}
|
|
1464
|
+
return { path, status: existed ? "merged" : "created" };
|
|
1465
|
+
}
|
|
1466
|
+
function resolveSurfaces(surface, cwd) {
|
|
1467
|
+
if (surface === "claude") return ["claude"];
|
|
1468
|
+
if (surface === "codex") return ["codex"];
|
|
1469
|
+
if (surface === "both") return ["claude", "codex"];
|
|
1470
|
+
if (surface) throw new Error(`--surface must be one of claude | codex | both (got '${surface}')`);
|
|
1471
|
+
const surfaces = detectHookSurfaces(cwd);
|
|
1472
|
+
return surfaces.length > 0 ? surfaces : ["claude", "codex"];
|
|
1473
|
+
}
|
|
1474
|
+
function describe(result, dryRun) {
|
|
1475
|
+
if (result.status === "current") return ` \u2713 ${result.path} (already configured)`;
|
|
1476
|
+
const verb = dryRun ? "would" : result.status === "created" ? "created" : "updated";
|
|
1477
|
+
return ` \u2713 ${result.path} (${dryRun ? `${verb} ${result.status === "created" ? "create" : "update"}` : verb})`;
|
|
1478
|
+
}
|
|
1479
|
+
var HOOK_SURFACE_LABEL = {
|
|
1480
|
+
claude: "Claude Code",
|
|
1481
|
+
codex: "Codex"
|
|
1482
|
+
};
|
|
1483
|
+
function installHookSurfaces(surfaces, opts) {
|
|
1484
|
+
const out = [];
|
|
1485
|
+
for (const surface of surfaces) {
|
|
1486
|
+
if (surface === "claude") {
|
|
1487
|
+
const path = join6(opts.claudeDir, "settings.json");
|
|
1488
|
+
out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
|
|
1489
|
+
} else {
|
|
1490
|
+
const hooksJson = installHooksJson(join6(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
|
|
1491
|
+
const featureFlag = installCodexFeatureFlag(join6(opts.codexHome, "config.toml"), opts.dryRun);
|
|
1492
|
+
out.push({ surface, results: [hooksJson, featureFlag] });
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
return out;
|
|
1496
|
+
}
|
|
1497
|
+
function detectHookSurfaces(cwd) {
|
|
1498
|
+
const detected = detectInstalledClients(cwd);
|
|
1499
|
+
const surfaces = [];
|
|
1500
|
+
if (detected.includes("claude-code")) surfaces.push("claude");
|
|
1501
|
+
if (detected.includes("codex")) surfaces.push("codex");
|
|
1502
|
+
return surfaces;
|
|
1503
|
+
}
|
|
1504
|
+
function isSechroomOnPath() {
|
|
1505
|
+
const pathEnv = process.env.PATH ?? "";
|
|
1506
|
+
if (!pathEnv) return false;
|
|
1507
|
+
const names = process.platform === "win32" ? ["sechroom.cmd", "sechroom.exe", "sechroom.bat", "sechroom"] : ["sechroom"];
|
|
1508
|
+
for (const dir of pathEnv.split(delimiter)) {
|
|
1509
|
+
if (!dir) continue;
|
|
1510
|
+
for (const name of names) {
|
|
1511
|
+
if (existsSync5(join6(dir, name))) return true;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
return false;
|
|
1515
|
+
}
|
|
1516
|
+
function warnIfSechroomNotOnPath(write = (s) => void process.stderr.write(s)) {
|
|
1517
|
+
if (isSechroomOnPath()) return false;
|
|
1518
|
+
write(
|
|
1519
|
+
"\n\u26A0 `sechroom` isn't on your PATH. The hooks run a bare `sechroom hook \u2026` command\n when your agent fires them, so a non-global install (npx / local) will fail at\n that point. Install globally so the command resolves:\n npm i -g @sechroom/cli\n"
|
|
1520
|
+
);
|
|
1521
|
+
return true;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// src/commands/channel.ts
|
|
1309
1525
|
function registerChannel(program2) {
|
|
1310
1526
|
const channel = program2.command("channel").description(
|
|
1311
1527
|
"Receive matched substrate events over the held SignalR push leg (D-WLP-9)"
|
|
@@ -1328,11 +1544,16 @@ function registerChannel(program2) {
|
|
|
1328
1544
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1329
1545
|
const filter = readFilter(opts);
|
|
1330
1546
|
const sub = await ensureSubscription(cfg, opts.name, filter);
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1547
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1548
|
+
const deliver = makeDeliver(
|
|
1549
|
+
filter,
|
|
1550
|
+
seen,
|
|
1551
|
+
(payload) => process.stdout.write(
|
|
1333
1552
|
(typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
|
|
1334
|
-
)
|
|
1335
|
-
|
|
1553
|
+
)
|
|
1554
|
+
);
|
|
1555
|
+
const conn = await openConnection(cfg, deliver);
|
|
1556
|
+
await reconcile(cfg, filter, deliver);
|
|
1336
1557
|
if (json) {
|
|
1337
1558
|
emit(
|
|
1338
1559
|
{
|
|
@@ -1369,7 +1590,8 @@ function registerChannel(program2) {
|
|
|
1369
1590
|
);
|
|
1370
1591
|
await mcp.connect(new StdioServerTransport());
|
|
1371
1592
|
await ensureSubscription(cfg, opts.name, filter);
|
|
1372
|
-
const
|
|
1593
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1594
|
+
const deliver = makeDeliver(filter, seen, (payload) => {
|
|
1373
1595
|
const { content, meta } = summarizeEvent(payload);
|
|
1374
1596
|
void mcp.notification({
|
|
1375
1597
|
method: "notifications/claude/channel",
|
|
@@ -1379,6 +1601,8 @@ function registerChannel(program2) {
|
|
|
1379
1601
|
`))
|
|
1380
1602
|
);
|
|
1381
1603
|
});
|
|
1604
|
+
const conn = await openConnection(cfg, deliver);
|
|
1605
|
+
await reconcile(cfg, filter, deliver);
|
|
1382
1606
|
process.stderr.write(
|
|
1383
1607
|
style.dim(
|
|
1384
1608
|
`sechroom channel (mcp) \u2014 tenant ${cfg.tenant}, tags [${filter.tags.join(", ")}]
|
|
@@ -1387,6 +1611,55 @@ function registerChannel(program2) {
|
|
|
1387
1611
|
);
|
|
1388
1612
|
await holdOpen(conn);
|
|
1389
1613
|
});
|
|
1614
|
+
channel.command("install").description(
|
|
1615
|
+
"Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
|
|
1616
|
+
).option(
|
|
1617
|
+
"--workspace <wsp...>",
|
|
1618
|
+
"Restrict dispatches to these workspace id(s)"
|
|
1619
|
+
).option(
|
|
1620
|
+
"--tag <tag...>",
|
|
1621
|
+
"Tag(s) a dispatch must carry to match (repeatable). Default targets WLP task dispatches.",
|
|
1622
|
+
["kind:task"]
|
|
1623
|
+
).option(
|
|
1624
|
+
"--name <name>",
|
|
1625
|
+
"MCP server + subscription name (idempotent per name)",
|
|
1626
|
+
"sechroom-channel"
|
|
1627
|
+
).option("--dry-run", "Print what would change; write nothing").action((opts) => {
|
|
1628
|
+
const path = join7(process.cwd(), ".mcp.json");
|
|
1629
|
+
const dryRun = Boolean(opts.dryRun);
|
|
1630
|
+
const args = ["channel", "mcp", "--name", opts.name];
|
|
1631
|
+
for (const w of opts.workspace ?? [])
|
|
1632
|
+
args.push("--workspace", w);
|
|
1633
|
+
for (const t of opts.tag ?? []) args.push("--tag", t);
|
|
1634
|
+
const entry = { command: "sechroom", args };
|
|
1635
|
+
const config2 = readMcpConfig(path);
|
|
1636
|
+
config2.mcpServers ??= {};
|
|
1637
|
+
const existing = config2.mcpServers[opts.name];
|
|
1638
|
+
const status = JSON.stringify(existing) === JSON.stringify(entry) ? "current" : existing ? "updated" : "created";
|
|
1639
|
+
if (status !== "current" && !dryRun) {
|
|
1640
|
+
config2.mcpServers[opts.name] = entry;
|
|
1641
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
1642
|
+
writeFileSync5(path, JSON.stringify(config2, null, 2) + "\n");
|
|
1643
|
+
}
|
|
1644
|
+
const verb = status === "current" ? "already configured" : dryRun ? `would ${status === "created" ? "create" : "update"}` : status;
|
|
1645
|
+
process.stdout.write(`${style.green("channel")} ${path} (${verb})
|
|
1646
|
+
`);
|
|
1647
|
+
process.stdout.write(
|
|
1648
|
+
style.dim(` server "${opts.name}": sechroom ${args.join(" ")}
|
|
1649
|
+
`)
|
|
1650
|
+
);
|
|
1651
|
+
if (status !== "current") {
|
|
1652
|
+
process.stdout.write(
|
|
1653
|
+
style.dim(
|
|
1654
|
+
`
|
|
1655
|
+
Load it (Channels research preview) by launching your agent with:
|
|
1656
|
+
claude --dangerously-load-development-channels server:${opts.name}
|
|
1657
|
+
`
|
|
1658
|
+
)
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
warnIfSechroomNotOnPath();
|
|
1662
|
+
});
|
|
1390
1663
|
channel.addHelpText(
|
|
1391
1664
|
"after",
|
|
1392
1665
|
`
|
|
@@ -1395,11 +1668,23 @@ Examples:
|
|
|
1395
1668
|
$ sechroom channel connect --tag kind:task --tag status:in-progress
|
|
1396
1669
|
$ sechroom channel connect --workspace wsp_X --json | jq .
|
|
1397
1670
|
|
|
1398
|
-
#
|
|
1399
|
-
|
|
1400
|
-
# then: claude --dangerously-load-development-channels server:sechroom`
|
|
1671
|
+
# Wire it as a Claude Code channel MCP server (research preview, v2.1.80+):
|
|
1672
|
+
$ sechroom channel install --workspace wsp_X --tag kind:task --tag status:in-progress
|
|
1673
|
+
# then: claude --dangerously-load-development-channels server:sechroom-channel`
|
|
1401
1674
|
);
|
|
1402
1675
|
}
|
|
1676
|
+
function readMcpConfig(path) {
|
|
1677
|
+
if (!existsSync6(path)) return {};
|
|
1678
|
+
const raw = readFileSync4(path, "utf8");
|
|
1679
|
+
if (!raw.trim()) return {};
|
|
1680
|
+
try {
|
|
1681
|
+
return JSON.parse(raw);
|
|
1682
|
+
} catch {
|
|
1683
|
+
return fail(
|
|
1684
|
+
`Could not parse ${path} as JSON \u2014 fix or remove it before installing the channel.`
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1403
1688
|
function readFilter(opts) {
|
|
1404
1689
|
const tags = opts.tag ?? [];
|
|
1405
1690
|
const workspaceScope = opts.workspace ?? [];
|
|
@@ -1452,20 +1737,134 @@ function holdOpen(conn) {
|
|
|
1452
1737
|
process.on("SIGTERM", stop);
|
|
1453
1738
|
});
|
|
1454
1739
|
}
|
|
1455
|
-
function
|
|
1740
|
+
function parseEvent(payload) {
|
|
1456
1741
|
let data = payload;
|
|
1457
1742
|
if (typeof payload === "string") {
|
|
1458
1743
|
try {
|
|
1459
1744
|
data = JSON.parse(payload);
|
|
1460
1745
|
} catch {
|
|
1461
|
-
return {
|
|
1746
|
+
return { eventType: "", memoryId: "", workspaceId: "", tags: void 0 };
|
|
1462
1747
|
}
|
|
1463
1748
|
}
|
|
1464
1749
|
const obj = data ?? {};
|
|
1465
1750
|
const inner = obj.data ?? obj;
|
|
1466
|
-
const
|
|
1467
|
-
|
|
1468
|
-
|
|
1751
|
+
const rawTags = inner.tags ?? inner.Tags;
|
|
1752
|
+
return {
|
|
1753
|
+
eventType: str(inner.eventType ?? inner.EventType ?? obj.type) || "substrate.event",
|
|
1754
|
+
memoryId: str(inner.memoryId ?? inner.MemoryId),
|
|
1755
|
+
workspaceId: str(inner.workspaceId ?? inner.WorkspaceId),
|
|
1756
|
+
tags: Array.isArray(rawTags) ? rawTags.filter((t) => typeof t === "string") : void 0
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
function shouldDeliver(payload, filter) {
|
|
1760
|
+
const { workspaceId, tags } = parseEvent(payload);
|
|
1761
|
+
if (filter.workspaceScope.length > 0 && (!workspaceId || !filter.workspaceScope.includes(workspaceId)))
|
|
1762
|
+
return false;
|
|
1763
|
+
if (filter.tags.length > 0) {
|
|
1764
|
+
if (!tags) return false;
|
|
1765
|
+
return facetedTagMatch(tags, filter.tags);
|
|
1766
|
+
}
|
|
1767
|
+
return true;
|
|
1768
|
+
}
|
|
1769
|
+
function makeDeliver(filter, seen, forward) {
|
|
1770
|
+
return (payload) => {
|
|
1771
|
+
if (!shouldDeliver(payload, filter)) return;
|
|
1772
|
+
const { memoryId } = parseEvent(payload);
|
|
1773
|
+
if (memoryId) {
|
|
1774
|
+
if (seen.has(memoryId)) return;
|
|
1775
|
+
seen.add(memoryId);
|
|
1776
|
+
}
|
|
1777
|
+
forward(payload);
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
async function reconcile(cfg, filter, deliver) {
|
|
1781
|
+
if (filter.workspaceScope.length === 0) {
|
|
1782
|
+
process.stderr.write(
|
|
1783
|
+
style.dim(
|
|
1784
|
+
"channel: no --workspace to reconcile against; live feed only (a dropped dispatch won't be recovered).\n"
|
|
1785
|
+
)
|
|
1786
|
+
);
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
if (filter.tags.length === 0) return;
|
|
1790
|
+
let token;
|
|
1791
|
+
try {
|
|
1792
|
+
token = await requireToken(cfg);
|
|
1793
|
+
} catch {
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
const qs = `filterTags=${encodeURIComponent(filter.tags.join(","))}&limit=100`;
|
|
1797
|
+
let recovered = 0;
|
|
1798
|
+
for (const ws of filter.workspaceScope) {
|
|
1799
|
+
try {
|
|
1800
|
+
const resp = await fetch(
|
|
1801
|
+
`${cfg.baseUrl}/workspaces/${encodeURIComponent(ws)}/memories/feed?${qs}`,
|
|
1802
|
+
{
|
|
1803
|
+
headers: {
|
|
1804
|
+
authorization: `Bearer ${token}`,
|
|
1805
|
+
tenant: cfg.tenant,
|
|
1806
|
+
"x-sechroom-surface": "cli"
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
);
|
|
1810
|
+
if (!resp.ok) {
|
|
1811
|
+
process.stderr.write(
|
|
1812
|
+
err(
|
|
1813
|
+
`channel: reconcile query for ${ws} failed (HTTP ${resp.status})
|
|
1814
|
+
`
|
|
1815
|
+
)
|
|
1816
|
+
);
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
const data = await resp.json();
|
|
1820
|
+
for (const m of data.results ?? []) {
|
|
1821
|
+
if (!m.id) continue;
|
|
1822
|
+
deliver({
|
|
1823
|
+
eventType: "reconcile",
|
|
1824
|
+
memoryId: m.id,
|
|
1825
|
+
workspaceId: ws,
|
|
1826
|
+
tags: m.tags ?? []
|
|
1827
|
+
});
|
|
1828
|
+
recovered++;
|
|
1829
|
+
}
|
|
1830
|
+
} catch (e) {
|
|
1831
|
+
process.stderr.write(
|
|
1832
|
+
err(`channel: reconcile error for ${ws}: ${String(e)}
|
|
1833
|
+
`)
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
if (recovered > 0)
|
|
1838
|
+
process.stderr.write(
|
|
1839
|
+
style.dim(
|
|
1840
|
+
`channel: reconciled ${recovered} already-queued event(s) on connect.
|
|
1841
|
+
`
|
|
1842
|
+
)
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
function facetedTagMatch(eventTags, filterTags) {
|
|
1846
|
+
const have = new Set(eventTags);
|
|
1847
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1848
|
+
for (const f of filterTags) {
|
|
1849
|
+
const ns = f.endsWith(":*") ? f.slice(0, -2) : namespaceOf(f);
|
|
1850
|
+
const group = groups.get(ns) ?? [];
|
|
1851
|
+
group.push(f);
|
|
1852
|
+
groups.set(ns, group);
|
|
1853
|
+
}
|
|
1854
|
+
for (const [ns, group] of groups) {
|
|
1855
|
+
const ok2 = group.some(
|
|
1856
|
+
(f) => f.endsWith(":*") ? eventTags.some((t) => namespaceOf(t) === ns) : have.has(f)
|
|
1857
|
+
);
|
|
1858
|
+
if (!ok2) return false;
|
|
1859
|
+
}
|
|
1860
|
+
return true;
|
|
1861
|
+
}
|
|
1862
|
+
function namespaceOf(tag) {
|
|
1863
|
+
const i = tag.indexOf(":");
|
|
1864
|
+
return i >= 0 ? tag.slice(0, i) : tag;
|
|
1865
|
+
}
|
|
1866
|
+
function summarizeEvent(payload) {
|
|
1867
|
+
const { eventType, memoryId, workspaceId } = parseEvent(payload);
|
|
1469
1868
|
const content = memoryId ? `${eventType}: ${memoryId}${workspaceId ? ` (workspace ${workspaceId})` : ""}` : typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
1470
1869
|
const meta = {};
|
|
1471
1870
|
if (eventType) meta.event_type = eventType;
|
|
@@ -1560,28 +1959,28 @@ Examples:
|
|
|
1560
1959
|
}
|
|
1561
1960
|
|
|
1562
1961
|
// src/commands/checkpoint.ts
|
|
1563
|
-
import { mkdirSync as
|
|
1564
|
-
import { dirname as
|
|
1962
|
+
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
1963
|
+
import { dirname as dirname7, join as join10 } from "path";
|
|
1565
1964
|
|
|
1566
1965
|
// src/commands/hook.ts
|
|
1567
1966
|
import { createHash as createHash2 } from "crypto";
|
|
1568
|
-
import { existsSync as
|
|
1569
|
-
import { dirname as
|
|
1967
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync6, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
|
|
1968
|
+
import { dirname as dirname6, join as join9 } from "path";
|
|
1570
1969
|
|
|
1571
1970
|
// src/sem.ts
|
|
1572
|
-
import { dirname as
|
|
1573
|
-
import { appendFileSync, existsSync as
|
|
1574
|
-
var SEM_FILE =
|
|
1971
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
1972
|
+
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync6 } from "fs";
|
|
1973
|
+
var SEM_FILE = join8(".sechroom", "lane.json");
|
|
1575
1974
|
var STATE_DIR_NAME2 = ".sechroom";
|
|
1576
1975
|
function localSemPath(cwd = process.cwd()) {
|
|
1577
|
-
return
|
|
1976
|
+
return join8(cwd, SEM_FILE);
|
|
1578
1977
|
}
|
|
1579
1978
|
function resolveSemPathForRead(start = process.cwd()) {
|
|
1580
1979
|
let dir = start;
|
|
1581
1980
|
while (true) {
|
|
1582
|
-
const candidate =
|
|
1583
|
-
if (
|
|
1584
|
-
const parent =
|
|
1981
|
+
const candidate = join8(dir, SEM_FILE);
|
|
1982
|
+
if (existsSync7(candidate)) return candidate;
|
|
1983
|
+
const parent = dirname5(dir);
|
|
1585
1984
|
if (parent === dir) return void 0;
|
|
1586
1985
|
dir = parent;
|
|
1587
1986
|
}
|
|
@@ -1591,23 +1990,23 @@ function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
|
|
|
1591
1990
|
let dir = start;
|
|
1592
1991
|
let gitPath;
|
|
1593
1992
|
for (; ; ) {
|
|
1594
|
-
const candidate =
|
|
1595
|
-
if (
|
|
1993
|
+
const candidate = join8(dir, ".git");
|
|
1994
|
+
if (existsSync7(candidate)) {
|
|
1596
1995
|
gitPath = candidate;
|
|
1597
1996
|
break;
|
|
1598
1997
|
}
|
|
1599
|
-
const parent =
|
|
1998
|
+
const parent = dirname5(dir);
|
|
1600
1999
|
if (parent === dir) break;
|
|
1601
2000
|
dir = parent;
|
|
1602
2001
|
}
|
|
1603
2002
|
if (!gitPath || statSync(gitPath).isDirectory()) return lane;
|
|
1604
|
-
const gitFile =
|
|
2003
|
+
const gitFile = readFileSync5(gitPath, "utf8");
|
|
1605
2004
|
const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
|
|
1606
2005
|
if (!common) return lane;
|
|
1607
|
-
const worktreesDir =
|
|
2006
|
+
const worktreesDir = join8(common[1], "worktrees");
|
|
1608
2007
|
const siblings = readdirSync(worktreesDir).filter((n) => {
|
|
1609
2008
|
try {
|
|
1610
|
-
return statSync(
|
|
2009
|
+
return statSync(join8(worktreesDir, n)).isDirectory();
|
|
1611
2010
|
} catch {
|
|
1612
2011
|
return false;
|
|
1613
2012
|
}
|
|
@@ -1619,327 +2018,115 @@ function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
|
|
|
1619
2018
|
}
|
|
1620
2019
|
function laneWithWorktreeSuffix(lane, gitFile, siblings) {
|
|
1621
2020
|
const m = gitFile.trim().match(/\/worktrees\/([^/\s]+)\s*$/);
|
|
1622
|
-
if (!m) return lane;
|
|
1623
|
-
const idx = [...siblings].sort().indexOf(m[1]);
|
|
1624
|
-
return idx < 0 ? lane : `${lane}-${idx + 2}`;
|
|
1625
|
-
}
|
|
1626
|
-
function serializeSem(values) {
|
|
1627
|
-
return JSON.stringify(values, null, 2) + "\n";
|
|
1628
|
-
}
|
|
1629
|
-
function readSem(path) {
|
|
1630
|
-
const p = path ?? resolveSemPathForRead();
|
|
1631
|
-
if (!p || !existsSync4(p)) return void 0;
|
|
1632
|
-
return { path: p, values: parseLaneJson(readFileSync3(p, "utf8")) };
|
|
1633
|
-
}
|
|
1634
|
-
function readLocalSemValues(cwd = process.cwd()) {
|
|
1635
|
-
const next = join5(cwd, SEM_FILE);
|
|
1636
|
-
if (existsSync4(next)) return readSem(next)?.values ?? {};
|
|
1637
|
-
return {};
|
|
1638
|
-
}
|
|
1639
|
-
function parseLaneJson(text2) {
|
|
1640
|
-
try {
|
|
1641
|
-
const parsed = JSON.parse(text2);
|
|
1642
|
-
const out = {};
|
|
1643
|
-
for (const [k, v] of Object.entries(parsed)) {
|
|
1644
|
-
if (typeof v === "string") out[k] = v;
|
|
1645
|
-
}
|
|
1646
|
-
return out;
|
|
1647
|
-
} catch {
|
|
1648
|
-
return {};
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
var STATE_DIR_IGNORE = `${STATE_DIR_NAME2}/`;
|
|
1652
|
-
function writeSem(values, path = localSemPath()) {
|
|
1653
|
-
mkdirSync4(dirname2(path), { recursive: true });
|
|
1654
|
-
writeFileSync4(path, serializeSem(values));
|
|
1655
|
-
ensureSemIgnored(path);
|
|
1656
|
-
ensureContinuityScaffold(path);
|
|
1657
|
-
return path;
|
|
1658
|
-
}
|
|
1659
|
-
function ensureStateDirIgnored(cwd = process.cwd()) {
|
|
1660
|
-
ensureSemIgnored(localSemPath(cwd));
|
|
1661
|
-
}
|
|
1662
|
-
var CONTINUITY_FILE_NAME = "continuity.json";
|
|
1663
|
-
var CONTINUITY_SCAFFOLD = JSON.stringify(
|
|
1664
|
-
{
|
|
1665
|
-
_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.",
|
|
1666
|
-
objective: "",
|
|
1667
|
-
state: "",
|
|
1668
|
-
lastAction: "",
|
|
1669
|
-
nextAction: "",
|
|
1670
|
-
resumeInstruction: "",
|
|
1671
|
-
constraints: [],
|
|
1672
|
-
questions: [],
|
|
1673
|
-
artifacts: [],
|
|
1674
|
-
confidence: null
|
|
1675
|
-
},
|
|
1676
|
-
null,
|
|
1677
|
-
2
|
|
1678
|
-
) + "\n";
|
|
1679
|
-
function ensureContinuityScaffold(semPath) {
|
|
1680
|
-
try {
|
|
1681
|
-
const target = join5(dirname2(semPath), CONTINUITY_FILE_NAME);
|
|
1682
|
-
if (existsSync4(target)) return;
|
|
1683
|
-
writeFileSync4(target, CONTINUITY_SCAFFOLD);
|
|
1684
|
-
} catch {
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
function ignoresSem(content) {
|
|
1688
|
-
return content.split("\n").some((line) => {
|
|
1689
|
-
const t = line.trim();
|
|
1690
|
-
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}`;
|
|
1691
|
-
});
|
|
1692
|
-
}
|
|
1693
|
-
function inGitRepo(startDir) {
|
|
1694
|
-
let dir = startDir;
|
|
1695
|
-
for (; ; ) {
|
|
1696
|
-
if (existsSync4(join5(dir, ".git"))) return true;
|
|
1697
|
-
const parent = dirname2(dir);
|
|
1698
|
-
if (parent === dir) return false;
|
|
1699
|
-
dir = parent;
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
function resolveGitignoreTarget(startDir) {
|
|
1703
|
-
let dir = startDir;
|
|
1704
|
-
for (; ; ) {
|
|
1705
|
-
const gi = join5(dir, ".gitignore");
|
|
1706
|
-
if (existsSync4(gi)) return { path: gi, exists: true };
|
|
1707
|
-
const parent = dirname2(dir);
|
|
1708
|
-
if (existsSync4(join5(dir, ".git")) || parent === dir) {
|
|
1709
|
-
return { path: join5(startDir, ".gitignore"), exists: false };
|
|
1710
|
-
}
|
|
1711
|
-
dir = parent;
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
function ensureSemIgnored(semPath) {
|
|
1715
|
-
try {
|
|
1716
|
-
const checkoutDir = dirname2(dirname2(semPath));
|
|
1717
|
-
if (!inGitRepo(checkoutDir)) return;
|
|
1718
|
-
const target = resolveGitignoreTarget(checkoutDir);
|
|
1719
|
-
if (target.exists) {
|
|
1720
|
-
const content = readFileSync3(target.path, "utf8");
|
|
1721
|
-
if (ignoresSem(content)) return;
|
|
1722
|
-
const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
1723
|
-
appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
|
|
1724
|
-
`);
|
|
1725
|
-
} else {
|
|
1726
|
-
writeFileSync4(target.path, `${STATE_DIR_IGNORE}
|
|
1727
|
-
`);
|
|
1728
|
-
}
|
|
1729
|
-
} catch {
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
// src/commands/hook-install.ts
|
|
1734
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
1735
|
-
import { delimiter, dirname as dirname4, join as join7 } from "path";
|
|
1736
|
-
|
|
1737
|
-
// src/setup/clients.ts
|
|
1738
|
-
import { existsSync as existsSync5 } from "fs";
|
|
1739
|
-
import { homedir as homedir3 } from "os";
|
|
1740
|
-
import { dirname as dirname3, join as join6 } from "path";
|
|
1741
|
-
function claudeDesktopConfigPath(home) {
|
|
1742
|
-
switch (process.platform) {
|
|
1743
|
-
case "darwin":
|
|
1744
|
-
return join6(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
1745
|
-
case "win32":
|
|
1746
|
-
return join6(process.env.APPDATA ?? join6(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
1747
|
-
default:
|
|
1748
|
-
return join6(home, ".config", "Claude", "claude_desktop_config.json");
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
function clientTargets(cwd, opts = {}) {
|
|
1752
|
-
const home = homedir3();
|
|
1753
|
-
const claudeDir = opts.claudeDir ?? join6(home, ".claude");
|
|
1754
|
-
const codexHome = opts.codexHome ?? join6(home, ".codex");
|
|
1755
|
-
return {
|
|
1756
|
-
"claude-code": {
|
|
1757
|
-
key: "claude-code",
|
|
1758
|
-
label: "Claude Code",
|
|
1759
|
-
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join6(cwd, ".mcp.json"), format: "json" },
|
|
1760
|
-
instruction: { surfaceKey: "claude-code", path: join6(cwd, "CLAUDE.md") }
|
|
1761
|
-
},
|
|
1762
|
-
"claude-desktop": {
|
|
1763
|
-
key: "claude-desktop",
|
|
1764
|
-
label: "Claude Desktop",
|
|
1765
|
-
mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
|
|
1766
|
-
instruction: { surfaceKey: "claude-desktop", path: join6(claudeDir, "CLAUDE.md") }
|
|
1767
|
-
},
|
|
1768
|
-
codex: {
|
|
1769
|
-
key: "codex",
|
|
1770
|
-
label: "Codex CLI",
|
|
1771
|
-
mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join6(codexHome, "config.toml"), format: "toml" },
|
|
1772
|
-
instruction: { surfaceKey: "chatgpt", path: join6(cwd, "AGENTS.md") }
|
|
1773
|
-
},
|
|
1774
|
-
cursor: {
|
|
1775
|
-
key: "cursor",
|
|
1776
|
-
label: "Cursor",
|
|
1777
|
-
mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join6(cwd, ".cursor", "mcp.json"), format: "json" },
|
|
1778
|
-
instruction: { surfaceKey: "chatgpt", path: join6(cwd, "AGENTS.md") }
|
|
1779
|
-
},
|
|
1780
|
-
antigravity: {
|
|
1781
|
-
key: "antigravity",
|
|
1782
|
-
label: "Google Antigravity",
|
|
1783
|
-
// FR-sechroom-247 — Antigravity reads MCP from a GLOBAL, home-relative
|
|
1784
|
-
// `~/.gemini/config/mcp_config.json` (not cwd; not affected by
|
|
1785
|
-
// CLAUDE_CONFIG_DIR / CODEX_HOME). The snippet — `serverUrl`-shaped, no
|
|
1786
|
-
// `type` — comes from the `antigravity` server surface, so we don't
|
|
1787
|
-
// hardcode it here. Instructions go in the project `AGENTS.md`
|
|
1788
|
-
// (cross-tool, shared with Codex/Cursor).
|
|
1789
|
-
mcp: { surfaceKey: "antigravity", sectionType: SectionType.McpConfig, path: join6(home, ".gemini", "config", "mcp_config.json"), format: "json" },
|
|
1790
|
-
instruction: { surfaceKey: "antigravity", path: join6(cwd, "AGENTS.md") }
|
|
1791
|
-
}
|
|
1792
|
-
};
|
|
1793
|
-
}
|
|
1794
|
-
var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor", "antigravity"];
|
|
1795
|
-
var DEFAULT_CLIENT_KEY = "claude-code";
|
|
1796
|
-
function detectInstalledClients(cwd) {
|
|
1797
|
-
const home = homedir3();
|
|
1798
|
-
const detected = [];
|
|
1799
|
-
if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir))) detected.push("claude-code");
|
|
1800
|
-
if (existsSync5(dirname3(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
|
|
1801
|
-
if (resolveCodexHomes({}).some((d) => existsSync5(d))) detected.push("codex");
|
|
1802
|
-
if (existsSync5(join6(home, ".cursor")) || existsSync5(join6(cwd, ".cursor"))) detected.push("cursor");
|
|
1803
|
-
if (existsSync5(join6(home, ".gemini"))) detected.push("antigravity");
|
|
1804
|
-
return detected;
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
// src/commands/hook-install.ts
|
|
1808
|
-
var CLAUDE_HOOK_COMMANDS = {
|
|
1809
|
-
SessionStart: "sechroom hook session-start",
|
|
1810
|
-
PreCompact: "sechroom hook pre-compact",
|
|
1811
|
-
SessionEnd: "sechroom hook session-end",
|
|
1812
|
-
// WLP telemetry tap (D-WLP-10) — per-turn executor self-report. No-op (exit 0) unless this
|
|
1813
|
-
// checkout is bound to a decomposition+task via `sechroom telemetry bind`, so it's safe to wire
|
|
1814
|
-
// for every Claude install. Claude-only (it parses a Claude Code transcript).
|
|
1815
|
-
Stop: "sechroom telemetry hook"
|
|
1816
|
-
};
|
|
1817
|
-
var CODEX_HOOK_COMMANDS = {
|
|
1818
|
-
SessionStart: "sechroom hook session-start",
|
|
1819
|
-
Stop: "sechroom hook session-end --debounce-minutes 10"
|
|
1820
|
-
};
|
|
1821
|
-
function hasHookCommand(config2, event, command) {
|
|
1822
|
-
const groups = config2.hooks?.[event] ?? [];
|
|
1823
|
-
return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
|
|
1824
|
-
}
|
|
1825
|
-
function mergeHooks(config2, commands) {
|
|
1826
|
-
config2.hooks ??= {};
|
|
1827
|
-
let added = 0;
|
|
1828
|
-
for (const [event, command] of Object.entries(commands)) {
|
|
1829
|
-
if (hasHookCommand(config2, event, command)) continue;
|
|
1830
|
-
const groups = config2.hooks[event] ??= [];
|
|
1831
|
-
groups.push({ hooks: [{ type: "command", command }] });
|
|
1832
|
-
added += 1;
|
|
1833
|
-
}
|
|
1834
|
-
return added;
|
|
2021
|
+
if (!m) return lane;
|
|
2022
|
+
const idx = [...siblings].sort().indexOf(m[1]);
|
|
2023
|
+
return idx < 0 ? lane : `${lane}-${idx + 2}`;
|
|
1835
2024
|
}
|
|
1836
|
-
function
|
|
1837
|
-
|
|
1838
|
-
const raw = readFileSync4(path, "utf8");
|
|
1839
|
-
if (!raw.trim()) return {};
|
|
1840
|
-
return JSON.parse(raw);
|
|
2025
|
+
function serializeSem(values) {
|
|
2026
|
+
return JSON.stringify(values, null, 2) + "\n";
|
|
1841
2027
|
}
|
|
1842
|
-
function
|
|
1843
|
-
const
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
if (added === 0 && existed) return { path, status: "current" };
|
|
1847
|
-
if (!dryRun) {
|
|
1848
|
-
mkdirSync5(dirname4(path), { recursive: true });
|
|
1849
|
-
writeFileSync5(path, JSON.stringify(config2, null, 2) + "\n");
|
|
1850
|
-
}
|
|
1851
|
-
return { path, status: existed ? "merged" : "created" };
|
|
2028
|
+
function readSem(path) {
|
|
2029
|
+
const p = path ?? resolveSemPathForRead();
|
|
2030
|
+
if (!p || !existsSync7(p)) return void 0;
|
|
2031
|
+
return { path: p, values: parseLaneJson(readFileSync5(p, "utf8")) };
|
|
1852
2032
|
}
|
|
1853
|
-
function
|
|
1854
|
-
|
|
2033
|
+
function readLocalSemValues(cwd = process.cwd()) {
|
|
2034
|
+
const next = join8(cwd, SEM_FILE);
|
|
2035
|
+
if (existsSync7(next)) return readSem(next)?.values ?? {};
|
|
2036
|
+
return {};
|
|
1855
2037
|
}
|
|
1856
|
-
function
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
const
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
const m = lines[i].match(/^(\s*)hooks(\s*)=(\s*)(.*)$/);
|
|
1867
|
-
if (!m) continue;
|
|
1868
|
-
const value = m[4].replace(/\s*#.*$/, "").trim();
|
|
1869
|
-
if (value === "true") return { next: content, changed: false };
|
|
1870
|
-
lines[i] = `${m[1]}hooks${m[2]}=${m[3]}true`;
|
|
1871
|
-
return { next: lines.join("\n"), changed: true };
|
|
2038
|
+
function parseLaneJson(text2) {
|
|
2039
|
+
try {
|
|
2040
|
+
const parsed = JSON.parse(text2);
|
|
2041
|
+
const out = {};
|
|
2042
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
2043
|
+
if (typeof v === "string") out[k] = v;
|
|
2044
|
+
}
|
|
2045
|
+
return out;
|
|
2046
|
+
} catch {
|
|
2047
|
+
return {};
|
|
1872
2048
|
}
|
|
1873
|
-
lines.splice(headerIdx + 1, 0, "hooks = true");
|
|
1874
|
-
return { next: lines.join("\n"), changed: true };
|
|
1875
2049
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
2050
|
+
var STATE_DIR_IGNORE = `${STATE_DIR_NAME2}/`;
|
|
2051
|
+
function writeSem(values, path = localSemPath()) {
|
|
2052
|
+
mkdirSync6(dirname5(path), { recursive: true });
|
|
2053
|
+
writeFileSync6(path, serializeSem(values));
|
|
2054
|
+
ensureSemIgnored(path);
|
|
2055
|
+
ensureContinuityScaffold(path);
|
|
2056
|
+
return path;
|
|
2057
|
+
}
|
|
2058
|
+
function ensureStateDirIgnored(cwd = process.cwd()) {
|
|
2059
|
+
ensureSemIgnored(localSemPath(cwd));
|
|
2060
|
+
}
|
|
2061
|
+
var CONTINUITY_FILE_NAME = "continuity.json";
|
|
2062
|
+
var CONTINUITY_SCAFFOLD = JSON.stringify(
|
|
2063
|
+
{
|
|
2064
|
+
_readme: "Agent-maintained continuity intent. Keep these current during the session; `sechroom checkpoint` and the PreCompact hook snapshot from here. The five required fields (objective, state, lastAction, nextAction, resumeInstruction) must all be non-empty for a snapshot to be created.",
|
|
2065
|
+
objective: "",
|
|
2066
|
+
state: "",
|
|
2067
|
+
lastAction: "",
|
|
2068
|
+
nextAction: "",
|
|
2069
|
+
resumeInstruction: "",
|
|
2070
|
+
constraints: [],
|
|
2071
|
+
questions: [],
|
|
2072
|
+
artifacts: [],
|
|
2073
|
+
confidence: null
|
|
2074
|
+
},
|
|
2075
|
+
null,
|
|
2076
|
+
2
|
|
2077
|
+
) + "\n";
|
|
2078
|
+
function ensureContinuityScaffold(semPath) {
|
|
2079
|
+
try {
|
|
2080
|
+
const target = join8(dirname5(semPath), CONTINUITY_FILE_NAME);
|
|
2081
|
+
if (existsSync7(target)) return;
|
|
2082
|
+
writeFileSync6(target, CONTINUITY_SCAFFOLD);
|
|
2083
|
+
} catch {
|
|
1884
2084
|
}
|
|
1885
|
-
return { path, status: existed ? "merged" : "created" };
|
|
1886
2085
|
}
|
|
1887
|
-
function
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
const surfaces = detectHookSurfaces(cwd);
|
|
1893
|
-
return surfaces.length > 0 ? surfaces : ["claude", "codex"];
|
|
2086
|
+
function ignoresSem(content) {
|
|
2087
|
+
return content.split("\n").some((line) => {
|
|
2088
|
+
const t = line.trim();
|
|
2089
|
+
return t === STATE_DIR_NAME2 || t === STATE_DIR_IGNORE || t === `/${STATE_DIR_NAME2}` || t === `/${STATE_DIR_IGNORE}` || t === `**/${STATE_DIR_NAME2}` || t === `**/${STATE_DIR_IGNORE}`;
|
|
2090
|
+
});
|
|
1894
2091
|
}
|
|
1895
|
-
function
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
2092
|
+
function inGitRepo(startDir) {
|
|
2093
|
+
let dir = startDir;
|
|
2094
|
+
for (; ; ) {
|
|
2095
|
+
if (existsSync7(join8(dir, ".git"))) return true;
|
|
2096
|
+
const parent = dirname5(dir);
|
|
2097
|
+
if (parent === dir) return false;
|
|
2098
|
+
dir = parent;
|
|
2099
|
+
}
|
|
1899
2100
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
const path = join7(opts.claudeDir, "settings.json");
|
|
1909
|
-
out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
|
|
1910
|
-
} else {
|
|
1911
|
-
const hooksJson = installHooksJson(join7(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
|
|
1912
|
-
const featureFlag = installCodexFeatureFlag(join7(opts.codexHome, "config.toml"), opts.dryRun);
|
|
1913
|
-
out.push({ surface, results: [hooksJson, featureFlag] });
|
|
2101
|
+
function resolveGitignoreTarget(startDir) {
|
|
2102
|
+
let dir = startDir;
|
|
2103
|
+
for (; ; ) {
|
|
2104
|
+
const gi = join8(dir, ".gitignore");
|
|
2105
|
+
if (existsSync7(gi)) return { path: gi, exists: true };
|
|
2106
|
+
const parent = dirname5(dir);
|
|
2107
|
+
if (existsSync7(join8(dir, ".git")) || parent === dir) {
|
|
2108
|
+
return { path: join8(startDir, ".gitignore"), exists: false };
|
|
1914
2109
|
}
|
|
2110
|
+
dir = parent;
|
|
1915
2111
|
}
|
|
1916
|
-
return out;
|
|
1917
|
-
}
|
|
1918
|
-
function detectHookSurfaces(cwd) {
|
|
1919
|
-
const detected = detectInstalledClients(cwd);
|
|
1920
|
-
const surfaces = [];
|
|
1921
|
-
if (detected.includes("claude-code")) surfaces.push("claude");
|
|
1922
|
-
if (detected.includes("codex")) surfaces.push("codex");
|
|
1923
|
-
return surfaces;
|
|
1924
2112
|
}
|
|
1925
|
-
function
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
if (
|
|
1931
|
-
|
|
1932
|
-
if (
|
|
2113
|
+
function ensureSemIgnored(semPath) {
|
|
2114
|
+
try {
|
|
2115
|
+
const checkoutDir = dirname5(dirname5(semPath));
|
|
2116
|
+
if (!inGitRepo(checkoutDir)) return;
|
|
2117
|
+
const target = resolveGitignoreTarget(checkoutDir);
|
|
2118
|
+
if (target.exists) {
|
|
2119
|
+
const content = readFileSync5(target.path, "utf8");
|
|
2120
|
+
if (ignoresSem(content)) return;
|
|
2121
|
+
const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
2122
|
+
appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
|
|
2123
|
+
`);
|
|
2124
|
+
} else {
|
|
2125
|
+
writeFileSync6(target.path, `${STATE_DIR_IGNORE}
|
|
2126
|
+
`);
|
|
1933
2127
|
}
|
|
2128
|
+
} catch {
|
|
1934
2129
|
}
|
|
1935
|
-
return false;
|
|
1936
|
-
}
|
|
1937
|
-
function warnIfSechroomNotOnPath(write = (s) => void process.stderr.write(s)) {
|
|
1938
|
-
if (isSechroomOnPath()) return false;
|
|
1939
|
-
write(
|
|
1940
|
-
"\n\u26A0 `sechroom` isn't on your PATH. The hooks run a bare `sechroom hook \u2026` command\n when your agent fires them, so a non-global install (npx / local) will fail at\n that point. Install globally so the command resolves:\n npm i -g @sechroom/cli\n"
|
|
1941
|
-
);
|
|
1942
|
-
return true;
|
|
1943
2130
|
}
|
|
1944
2131
|
|
|
1945
2132
|
// src/commands/hook.ts
|
|
@@ -1966,13 +2153,13 @@ function resolveLane(flagLane, cwd) {
|
|
|
1966
2153
|
if (!base) return void 0;
|
|
1967
2154
|
return applyWorktreeLaneSuffix(base, start);
|
|
1968
2155
|
}
|
|
1969
|
-
var INTENT_FILE =
|
|
2156
|
+
var INTENT_FILE = join9(".sechroom", "continuity.json");
|
|
1970
2157
|
function resolveIntentPath(start) {
|
|
1971
2158
|
let dir = start;
|
|
1972
2159
|
for (; ; ) {
|
|
1973
|
-
const candidate =
|
|
1974
|
-
if (
|
|
1975
|
-
const parent =
|
|
2160
|
+
const candidate = join9(dir, INTENT_FILE);
|
|
2161
|
+
if (existsSync8(candidate)) return candidate;
|
|
2162
|
+
const parent = dirname6(dir);
|
|
1976
2163
|
if (parent === dir) return void 0;
|
|
1977
2164
|
dir = parent;
|
|
1978
2165
|
}
|
|
@@ -1981,7 +2168,7 @@ function readIntent(start) {
|
|
|
1981
2168
|
const path = resolveIntentPath(start);
|
|
1982
2169
|
if (!path) return void 0;
|
|
1983
2170
|
try {
|
|
1984
|
-
return JSON.parse(
|
|
2171
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
1985
2172
|
} catch {
|
|
1986
2173
|
return void 0;
|
|
1987
2174
|
}
|
|
@@ -2023,14 +2210,14 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
|
|
|
2023
2210
|
}
|
|
2024
2211
|
function ledgerPath(start) {
|
|
2025
2212
|
const intent = resolveIntentPath(start);
|
|
2026
|
-
const dir = intent ?
|
|
2027
|
-
return
|
|
2213
|
+
const dir = intent ? dirname6(intent) : join9(start, ".sechroom");
|
|
2214
|
+
return join9(dir, ".checkpoint-state.json");
|
|
2028
2215
|
}
|
|
2029
2216
|
function readLedger(start) {
|
|
2030
2217
|
try {
|
|
2031
2218
|
const p = ledgerPath(start);
|
|
2032
|
-
if (!
|
|
2033
|
-
return JSON.parse(
|
|
2219
|
+
if (!existsSync8(p)) return {};
|
|
2220
|
+
return JSON.parse(readFileSync6(p, "utf8"));
|
|
2034
2221
|
} catch {
|
|
2035
2222
|
return {};
|
|
2036
2223
|
}
|
|
@@ -2077,13 +2264,13 @@ function recordPush(start, intent) {
|
|
|
2077
2264
|
} catch {
|
|
2078
2265
|
mtimeMs = void 0;
|
|
2079
2266
|
}
|
|
2080
|
-
|
|
2267
|
+
mkdirSync7(dirname6(p), { recursive: true });
|
|
2081
2268
|
const ledger = {
|
|
2082
2269
|
lastEpochMs: Date.now(),
|
|
2083
2270
|
lastMtimeMs: mtimeMs,
|
|
2084
2271
|
lastHash: intentHash(intent)
|
|
2085
2272
|
};
|
|
2086
|
-
|
|
2273
|
+
writeFileSync7(p, JSON.stringify(ledger) + "\n");
|
|
2087
2274
|
} catch {
|
|
2088
2275
|
}
|
|
2089
2276
|
}
|
|
@@ -2330,10 +2517,10 @@ Examples:
|
|
|
2330
2517
|
const client = await makeClient(cfg);
|
|
2331
2518
|
return client.POST("/continuity/snapshots", { body });
|
|
2332
2519
|
});
|
|
2333
|
-
const path = resolveIntentPath(cwd) ??
|
|
2520
|
+
const path = resolveIntentPath(cwd) ?? join10(cwd, INTENT_FILE);
|
|
2334
2521
|
const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
|
|
2335
|
-
|
|
2336
|
-
|
|
2522
|
+
mkdirSync8(dirname7(path), { recursive: true });
|
|
2523
|
+
writeFileSync8(path, JSON.stringify(fileBody, null, 2) + "\n");
|
|
2337
2524
|
recordPush(cwd, merged);
|
|
2338
2525
|
if (json) {
|
|
2339
2526
|
emit({ snapshotId: data.snapshotId, lane, scope, file: path }, true);
|
|
@@ -2346,6 +2533,149 @@ Examples:
|
|
|
2346
2533
|
});
|
|
2347
2534
|
}
|
|
2348
2535
|
|
|
2536
|
+
// src/commands/close.ts
|
|
2537
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
2538
|
+
var VERDICTS = ["pass", "soft-fail", "plan-invalid", "blocked"];
|
|
2539
|
+
function registerClose(program2) {
|
|
2540
|
+
program2.command("close").description(
|
|
2541
|
+
"Close a dispatched WorkTask in one call: closeout memo (verdict + correlation) + Reference edge + source status flip. Managed runs stamp the run's matchTags verbatim (SBC-1180)."
|
|
2542
|
+
).requiredOption("--task <id>", "The WorkTask memory id being closed").requiredOption("--verdict <verdict>", `Verdict: ${VERDICTS.join(" | ")}`).requiredOption(
|
|
2543
|
+
"--workspace <wsp>",
|
|
2544
|
+
"Workspace that owns the closeout memo"
|
|
2545
|
+
).requiredOption("--title <title>", "Closeout title").option(
|
|
2546
|
+
"--file <path>",
|
|
2547
|
+
"Read the closeout body from a file (default: stdin)"
|
|
2548
|
+
).option(
|
|
2549
|
+
"--decomposition <id>",
|
|
2550
|
+
"Managed-run selector \u2014 the sug_ decomposition the task belongs to. Locates the parked run so its matchTags/verdict contract are read from state, not assembled from args."
|
|
2551
|
+
).option(
|
|
2552
|
+
"--no-status-flip",
|
|
2553
|
+
"Skip the status flip on a BARE task. (Managed runs never flip here \u2014 the run engine reflects the verdict onto the task on resume.)"
|
|
2554
|
+
).option("--source <source>", "Source / lane stamp", "cli").addHelpText(
|
|
2555
|
+
"after",
|
|
2556
|
+
`
|
|
2557
|
+
Examples:
|
|
2558
|
+
# Managed run \u2014 reads matchTags from run state, stamps them verbatim, un-parks the run:
|
|
2559
|
+
$ echo "Shipped X. Tests green." | sechroom close --task mem_XXXX --decomposition sug_YYYY \\
|
|
2560
|
+
--verdict pass --workspace wsp_ZZZZ --title "w4_build closeout"
|
|
2561
|
+
|
|
2562
|
+
# Bare task:
|
|
2563
|
+
$ sechroom close --task mem_XXXX --verdict pass --workspace wsp_ZZZZ \\
|
|
2564
|
+
--title "done" --file ./closeout.md`
|
|
2565
|
+
).action(async (opts, cmd) => {
|
|
2566
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2567
|
+
const json = cmd.optsWithGlobals().json;
|
|
2568
|
+
const verdict = String(opts.verdict);
|
|
2569
|
+
if (!VERDICTS.includes(verdict))
|
|
2570
|
+
fail(
|
|
2571
|
+
`--verdict must be one of ${VERDICTS.join(" | ")} \u2014 got '${opts.verdict}'`
|
|
2572
|
+
);
|
|
2573
|
+
let bodyText;
|
|
2574
|
+
try {
|
|
2575
|
+
bodyText = opts.file ? readFileSync7(opts.file, "utf8") : readFileSync7(0, "utf8");
|
|
2576
|
+
} catch {
|
|
2577
|
+
fail(
|
|
2578
|
+
opts.file ? `could not read --file ${opts.file}` : "no closeout body \u2014 pass --file <path> or pipe it on stdin"
|
|
2579
|
+
);
|
|
2580
|
+
}
|
|
2581
|
+
if (!bodyText.trim())
|
|
2582
|
+
fail(
|
|
2583
|
+
"closeout body is empty \u2014 pass --file <path> or pipe the body on stdin"
|
|
2584
|
+
);
|
|
2585
|
+
const client = await makeClient(cfg);
|
|
2586
|
+
let matchTags;
|
|
2587
|
+
if (opts.decomposition) {
|
|
2588
|
+
const run = await runApi(
|
|
2589
|
+
"Reading run contract",
|
|
2590
|
+
async () => client.GET("/decompositions/{id}/run", {
|
|
2591
|
+
params: { path: { id: opts.decomposition } }
|
|
2592
|
+
})
|
|
2593
|
+
);
|
|
2594
|
+
const await_ = run.awaitingCompletion;
|
|
2595
|
+
if (!await_ || !Array.isArray(await_.matchTags) || await_.matchTags.length === 0)
|
|
2596
|
+
fail(
|
|
2597
|
+
`decomposition ${opts.decomposition} has no parked run awaiting a completion \u2014 nothing to close (run outcome: ${run.outcome ?? "unknown"}). Idempotent no-op on an already-resumed/terminal run.`
|
|
2598
|
+
);
|
|
2599
|
+
if (run.awaitingTaskId && run.awaitingTaskId !== opts.task)
|
|
2600
|
+
fail(
|
|
2601
|
+
`run ${opts.decomposition} is awaiting task ${run.awaitingTaskId}, not ${opts.task} \u2014 refusing to mis-key the resume.`
|
|
2602
|
+
);
|
|
2603
|
+
matchTags = await_.matchTags;
|
|
2604
|
+
} else {
|
|
2605
|
+
matchTags = [`wlp-task:${opts.task}`];
|
|
2606
|
+
}
|
|
2607
|
+
const tags = [
|
|
2608
|
+
.../* @__PURE__ */ new Set([
|
|
2609
|
+
...matchTags,
|
|
2610
|
+
`wlp-task:${opts.task}`,
|
|
2611
|
+
`verdict:${verdict}`
|
|
2612
|
+
])
|
|
2613
|
+
];
|
|
2614
|
+
const closeout = await runApi(
|
|
2615
|
+
"Creating closeout",
|
|
2616
|
+
async () => client.POST("/memories", {
|
|
2617
|
+
body: {
|
|
2618
|
+
text: bodyText,
|
|
2619
|
+
type: "reference",
|
|
2620
|
+
content: "{}",
|
|
2621
|
+
confidence: 1,
|
|
2622
|
+
source: opts.source,
|
|
2623
|
+
archetype: "Document",
|
|
2624
|
+
title: opts.title,
|
|
2625
|
+
tags,
|
|
2626
|
+
owner: { type: "Workspace", id: opts.workspace }
|
|
2627
|
+
}
|
|
2628
|
+
})
|
|
2629
|
+
);
|
|
2630
|
+
const edge = await runApi(
|
|
2631
|
+
"Wiring Reference edge",
|
|
2632
|
+
async () => client.POST("/memories/{memoryId}/relationships", {
|
|
2633
|
+
params: { path: { memoryId: closeout.id } },
|
|
2634
|
+
body: { toMemoryId: opts.task, type: "Reference" }
|
|
2635
|
+
})
|
|
2636
|
+
);
|
|
2637
|
+
let taskStatus;
|
|
2638
|
+
if (opts.statusFlip !== false && !opts.decomposition) {
|
|
2639
|
+
const current = await runApi(
|
|
2640
|
+
"Reading task tags",
|
|
2641
|
+
async () => client.GET("/memories/{memoryId}", {
|
|
2642
|
+
params: { path: { memoryId: opts.task } }
|
|
2643
|
+
})
|
|
2644
|
+
);
|
|
2645
|
+
const currentTags = current?.item?.tags ?? current?.tags ?? [];
|
|
2646
|
+
const target = verdict === "blocked" ? "status:blocked" : "status:done";
|
|
2647
|
+
const nextTags = [
|
|
2648
|
+
...new Set(currentTags.filter((t) => !t.startsWith("status:")))
|
|
2649
|
+
].concat(target);
|
|
2650
|
+
await runApi(
|
|
2651
|
+
"Flipping task status",
|
|
2652
|
+
async () => client.PATCH("/memories/{memoryId}/metadata", {
|
|
2653
|
+
params: { path: { memoryId: opts.task } },
|
|
2654
|
+
body: {
|
|
2655
|
+
memoryId: opts.task,
|
|
2656
|
+
source: opts.source,
|
|
2657
|
+
tags: nextTags
|
|
2658
|
+
}
|
|
2659
|
+
})
|
|
2660
|
+
);
|
|
2661
|
+
taskStatus = target.slice("status:".length);
|
|
2662
|
+
}
|
|
2663
|
+
const view = resolveViewUrl(cfg.baseUrl, closeout.url);
|
|
2664
|
+
const result = {
|
|
2665
|
+
closeoutId: closeout.id,
|
|
2666
|
+
edgeId: edge.id,
|
|
2667
|
+
taskStatus: taskStatus ?? null,
|
|
2668
|
+
verdict,
|
|
2669
|
+
tags
|
|
2670
|
+
};
|
|
2671
|
+
emitAction(
|
|
2672
|
+
`closed ${style.bold(opts.task)} verdict:${style.bold(verdict)} \u2192 closeout ${style.bold(closeout.id)}${taskStatus ? ` ${style.dim(`(task ${taskStatus})`)}` : ""}${view ? ` ${style.dim("\u2192")} ${view}` : ""}`,
|
|
2673
|
+
result,
|
|
2674
|
+
json
|
|
2675
|
+
);
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2349
2679
|
// src/commands/continuity.ts
|
|
2350
2680
|
function registerContinuity(program2) {
|
|
2351
2681
|
const continuity = program2.command("continuity").description("Continuity snapshots: checkpoint and resume work");
|
|
@@ -2506,6 +2836,90 @@ Examples:
|
|
|
2506
2836
|
});
|
|
2507
2837
|
}
|
|
2508
2838
|
|
|
2839
|
+
// src/commands/decomposition.ts
|
|
2840
|
+
function registerDecomposition(program2) {
|
|
2841
|
+
const decomposition = program2.command("decomposition").description(
|
|
2842
|
+
"Drive a WLP decomposition: decompose a brief, then execute / accept / reject"
|
|
2843
|
+
);
|
|
2844
|
+
decomposition.addHelpText(
|
|
2845
|
+
"after",
|
|
2846
|
+
`
|
|
2847
|
+
Examples:
|
|
2848
|
+
$ sechroom decomposition decompose mem_XXXX
|
|
2849
|
+
$ sechroom decomposition execute sug_XXXX
|
|
2850
|
+
$ sechroom decomposition accept sug_XXXX
|
|
2851
|
+
$ sechroom decomposition reject sug_XXXX --reason "wrong shape"`
|
|
2852
|
+
);
|
|
2853
|
+
decomposition.command("decompose <briefId>").description(
|
|
2854
|
+
"Decompose a work brief into a candidate Task graph (POST /work-briefs/{id}/decompose)"
|
|
2855
|
+
).action(async (briefId, _opts, cmd) => {
|
|
2856
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2857
|
+
const data = await runApi("Queueing decomposition", async () => {
|
|
2858
|
+
const client = await makeClient(cfg);
|
|
2859
|
+
return client.POST("/work-briefs/{id}/decompose", {
|
|
2860
|
+
params: { path: { id: briefId } },
|
|
2861
|
+
body: { id: briefId }
|
|
2862
|
+
});
|
|
2863
|
+
});
|
|
2864
|
+
emitAction(
|
|
2865
|
+
`queued decomposition of ${style.bold(briefId)} \u2192 ${style.bold(data.suggestionId)}`,
|
|
2866
|
+
data,
|
|
2867
|
+
cmd.optsWithGlobals().json
|
|
2868
|
+
);
|
|
2869
|
+
});
|
|
2870
|
+
decomposition.command("execute <decompositionId>").description(
|
|
2871
|
+
"Execute a decomposition's Task graph (POST /decompositions/{id}/execute)"
|
|
2872
|
+
).action(async (decompositionId, _opts, cmd) => {
|
|
2873
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2874
|
+
const data = await runApi("Executing decomposition", async () => {
|
|
2875
|
+
const client = await makeClient(cfg);
|
|
2876
|
+
return client.POST("/decompositions/{id}/execute", {
|
|
2877
|
+
params: { path: { id: decompositionId } },
|
|
2878
|
+
body: {}
|
|
2879
|
+
});
|
|
2880
|
+
});
|
|
2881
|
+
emitAction(
|
|
2882
|
+
`executed ${style.bold(decompositionId)} \u2192 run ${style.bold(data.runRecordId)} (${data.outcome})`,
|
|
2883
|
+
data,
|
|
2884
|
+
cmd.optsWithGlobals().json
|
|
2885
|
+
);
|
|
2886
|
+
});
|
|
2887
|
+
decomposition.command("accept <decompositionId>").description(
|
|
2888
|
+
"Accept a Pending decomposition \u2014 promote + ratify its Tasks (POST /decompositions/{id}/accept)"
|
|
2889
|
+
).action(async (decompositionId, _opts, cmd) => {
|
|
2890
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2891
|
+
const data = await runApi("Accepting decomposition", async () => {
|
|
2892
|
+
const client = await makeClient(cfg);
|
|
2893
|
+
return client.POST("/decompositions/{id}/accept", {
|
|
2894
|
+
params: { path: { id: decompositionId } },
|
|
2895
|
+
body: {}
|
|
2896
|
+
});
|
|
2897
|
+
});
|
|
2898
|
+
emitAction(
|
|
2899
|
+
`accepted decomposition ${style.bold(decompositionId)}`,
|
|
2900
|
+
data,
|
|
2901
|
+
cmd.optsWithGlobals().json
|
|
2902
|
+
);
|
|
2903
|
+
});
|
|
2904
|
+
decomposition.command("reject <decompositionId>").description(
|
|
2905
|
+
"Reject a Pending decomposition \u2014 archive its Tasks, bounce the brief (POST /decompositions/{id}/reject)"
|
|
2906
|
+
).option("--reason <reason>", "Optional free-text rejection reason").action(async (decompositionId, opts, cmd) => {
|
|
2907
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2908
|
+
const data = await runApi("Rejecting decomposition", async () => {
|
|
2909
|
+
const client = await makeClient(cfg);
|
|
2910
|
+
return client.POST("/decompositions/{id}/reject", {
|
|
2911
|
+
params: { path: { id: decompositionId } },
|
|
2912
|
+
body: { reasonText: opts.reason ?? null }
|
|
2913
|
+
});
|
|
2914
|
+
});
|
|
2915
|
+
emitAction(
|
|
2916
|
+
`rejected decomposition ${style.bold(decompositionId)}`,
|
|
2917
|
+
data,
|
|
2918
|
+
cmd.optsWithGlobals().json
|
|
2919
|
+
);
|
|
2920
|
+
});
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2509
2923
|
// src/commands/filing.ts
|
|
2510
2924
|
function registerFiling(program2) {
|
|
2511
2925
|
const filing = program2.command("filing").description("Review and act on filing suggestions");
|
|
@@ -3017,8 +3431,8 @@ Examples:
|
|
|
3017
3431
|
|
|
3018
3432
|
// src/setup/apply.ts
|
|
3019
3433
|
import { createHash as createHash3 } from "crypto";
|
|
3020
|
-
import { mkdirSync as
|
|
3021
|
-
import { dirname as
|
|
3434
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync9, existsSync as existsSync9 } from "fs";
|
|
3435
|
+
import { dirname as dirname8 } from "path";
|
|
3022
3436
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
3023
3437
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
3024
3438
|
function normalizeBody(s) {
|
|
@@ -3071,22 +3485,22 @@ function parseManagedBlock(content, block) {
|
|
|
3071
3485
|
return null;
|
|
3072
3486
|
}
|
|
3073
3487
|
function ensureDir2(path) {
|
|
3074
|
-
|
|
3488
|
+
mkdirSync9(dirname8(path), { recursive: true });
|
|
3075
3489
|
}
|
|
3076
3490
|
function readOr(path, fallback) {
|
|
3077
3491
|
try {
|
|
3078
|
-
return
|
|
3492
|
+
return readFileSync8(path, "utf8");
|
|
3079
3493
|
} catch {
|
|
3080
3494
|
return fallback;
|
|
3081
3495
|
}
|
|
3082
3496
|
}
|
|
3083
3497
|
function mergeMcpJson(path, snippet, dryRun) {
|
|
3084
3498
|
const incoming = JSON.parse(snippet);
|
|
3085
|
-
const existed =
|
|
3499
|
+
const existed = existsSync9(path);
|
|
3086
3500
|
let current = {};
|
|
3087
3501
|
if (existed) {
|
|
3088
3502
|
try {
|
|
3089
|
-
current = JSON.parse(
|
|
3503
|
+
current = JSON.parse(readFileSync8(path, "utf8"));
|
|
3090
3504
|
} catch {
|
|
3091
3505
|
return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
|
|
3092
3506
|
}
|
|
@@ -3094,26 +3508,26 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
3094
3508
|
current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
|
|
3095
3509
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3096
3510
|
ensureDir2(path);
|
|
3097
|
-
|
|
3511
|
+
writeFileSync9(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
3098
3512
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3099
3513
|
}
|
|
3100
3514
|
function mergeCodexToml(path, snippet, dryRun) {
|
|
3101
|
-
const existed =
|
|
3515
|
+
const existed = existsSync9(path);
|
|
3102
3516
|
let body = readOr(path, "");
|
|
3103
3517
|
body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
|
|
3104
3518
|
const trimmed = body.trim();
|
|
3105
3519
|
const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
|
|
3106
3520
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3107
3521
|
ensureDir2(path);
|
|
3108
|
-
|
|
3522
|
+
writeFileSync9(path, next, { mode: 384 });
|
|
3109
3523
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3110
3524
|
}
|
|
3111
3525
|
function writeInstructionBlock(path, write, dryRun) {
|
|
3112
|
-
const existed =
|
|
3526
|
+
const existed = existsSync9(path);
|
|
3113
3527
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
3114
3528
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
3115
3529
|
ensureDir2(path);
|
|
3116
|
-
|
|
3530
|
+
writeFileSync9(path, next);
|
|
3117
3531
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
3118
3532
|
}
|
|
3119
3533
|
function computeBlockFile(current, write) {
|
|
@@ -3154,7 +3568,7 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
3154
3568
|
const next = computeBlockFile(current, write);
|
|
3155
3569
|
if (!dryRun) {
|
|
3156
3570
|
ensureDir2(proposedPath);
|
|
3157
|
-
|
|
3571
|
+
writeFileSync9(proposedPath, next);
|
|
3158
3572
|
}
|
|
3159
3573
|
return {
|
|
3160
3574
|
kind: "instruction",
|
|
@@ -3194,7 +3608,10 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
3194
3608
|
if (!section) {
|
|
3195
3609
|
actions.push({ kind: "instruction", path: target.instruction.path, status: "skipped", note: `no instruction-file section on surface '${target.instruction.surfaceKey}'` });
|
|
3196
3610
|
} else {
|
|
3197
|
-
const resolved = await
|
|
3611
|
+
const resolved = await withSpinner(
|
|
3612
|
+
`Resolving ${target.label} agent instructions`,
|
|
3613
|
+
() => resolveInstruction(cfg, section, opts.personalWorkspaceId)
|
|
3614
|
+
);
|
|
3198
3615
|
if (!resolved) {
|
|
3199
3616
|
actions.push({ kind: "instruction", path: target.instruction.path, status: "skipped", note: "no role template found in this tenant \u2014 install the SEM Starter bundle, then re-run `sechroom setup agent-files`" });
|
|
3200
3617
|
} else {
|
|
@@ -3209,7 +3626,10 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
3209
3626
|
}
|
|
3210
3627
|
const conventionsSection = findSection(surface, SectionType.WorkspaceConventions);
|
|
3211
3628
|
if (conventionsSection) {
|
|
3212
|
-
const conventions = await
|
|
3629
|
+
const conventions = await withSpinner(
|
|
3630
|
+
`Composing ${target.label} workspace conventions`,
|
|
3631
|
+
() => resolveWorkspaceConventions(cfg, conventionsSection)
|
|
3632
|
+
);
|
|
3213
3633
|
if (conventions) {
|
|
3214
3634
|
const action = applyBlock(
|
|
3215
3635
|
target.instruction.path,
|
|
@@ -3278,8 +3698,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
3278
3698
|
}
|
|
3279
3699
|
|
|
3280
3700
|
// src/setup/skills-offer.ts
|
|
3281
|
-
import { mkdirSync as
|
|
3282
|
-
import { join as
|
|
3701
|
+
import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
3702
|
+
import { join as join11 } from "path";
|
|
3283
3703
|
|
|
3284
3704
|
// src/setup/lane-pin.ts
|
|
3285
3705
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -3395,8 +3815,8 @@ Found ${summary} available to you for ${surface}.
|
|
|
3395
3815
|
if (skills.length > 0) {
|
|
3396
3816
|
const written = [];
|
|
3397
3817
|
for (const s of skills) {
|
|
3398
|
-
|
|
3399
|
-
|
|
3818
|
+
mkdirSync10(join11(sDir, s.name), { recursive: true });
|
|
3819
|
+
writeFileSync10(join11(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
3400
3820
|
written.push(s.name);
|
|
3401
3821
|
}
|
|
3402
3822
|
recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -3404,11 +3824,11 @@ Found ${summary} available to you for ${surface}.
|
|
|
3404
3824
|
`);
|
|
3405
3825
|
}
|
|
3406
3826
|
if (agents.length > 0) {
|
|
3407
|
-
|
|
3827
|
+
mkdirSync10(aDir, { recursive: true });
|
|
3408
3828
|
const written = [];
|
|
3409
3829
|
for (const a of agents) {
|
|
3410
3830
|
const file = `${a.name}.md`;
|
|
3411
|
-
|
|
3831
|
+
writeFileSync10(join11(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
3412
3832
|
written.push(file);
|
|
3413
3833
|
}
|
|
3414
3834
|
recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -3791,13 +4211,13 @@ Wired to namespace '${slug}'. Restart your AI client (or reload MCP) to pick it
|
|
|
3791
4211
|
}
|
|
3792
4212
|
|
|
3793
4213
|
// src/commands/onboard.ts
|
|
3794
|
-
import { existsSync as
|
|
3795
|
-
import { basename as basename2, join as
|
|
4214
|
+
import { existsSync as existsSync11 } from "fs";
|
|
4215
|
+
import { basename as basename2, join as join13 } from "path";
|
|
3796
4216
|
|
|
3797
4217
|
// src/commands/fanout.ts
|
|
3798
4218
|
import { spawnSync } from "child_process";
|
|
3799
|
-
import { existsSync as
|
|
3800
|
-
import { isAbsolute, join as
|
|
4219
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
4220
|
+
import { isAbsolute, join as join12, resolve } from "path";
|
|
3801
4221
|
var ICON = {
|
|
3802
4222
|
refresh: "\u21BB",
|
|
3803
4223
|
bind: "+",
|
|
@@ -3817,21 +4237,21 @@ function discoverChildren(root) {
|
|
|
3817
4237
|
const out = [];
|
|
3818
4238
|
for (const name of names.sort()) {
|
|
3819
4239
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
3820
|
-
const dir =
|
|
4240
|
+
const dir = join12(root, name);
|
|
3821
4241
|
try {
|
|
3822
4242
|
if (!statSync3(dir).isDirectory()) continue;
|
|
3823
4243
|
} catch {
|
|
3824
4244
|
continue;
|
|
3825
4245
|
}
|
|
3826
|
-
if (
|
|
4246
|
+
if (existsSync10(join12(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
3827
4247
|
}
|
|
3828
4248
|
return out;
|
|
3829
4249
|
}
|
|
3830
4250
|
function readManifest(path) {
|
|
3831
|
-
if (!
|
|
4251
|
+
if (!existsSync10(path)) return null;
|
|
3832
4252
|
let parsed;
|
|
3833
4253
|
try {
|
|
3834
|
-
parsed = JSON.parse(
|
|
4254
|
+
parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
3835
4255
|
} catch (err2) {
|
|
3836
4256
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
3837
4257
|
}
|
|
@@ -4197,10 +4617,10 @@ async function chooseScope(scopeFlag, yes) {
|
|
|
4197
4617
|
}
|
|
4198
4618
|
async function planRecurseChild(entry, root, client, opts) {
|
|
4199
4619
|
const dir = resolveChildDir(entry.path, root);
|
|
4200
|
-
if (!
|
|
4620
|
+
if (!existsSync11(dir)) {
|
|
4201
4621
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
4202
4622
|
}
|
|
4203
|
-
if (
|
|
4623
|
+
if (existsSync11(join13(dir, ".sechroom.json"))) {
|
|
4204
4624
|
return {
|
|
4205
4625
|
label: entry.path,
|
|
4206
4626
|
dir,
|
|
@@ -4273,7 +4693,7 @@ This fan-out will pin the same lane in every repo:
|
|
|
4273
4693
|
async function runRecurse(cfg, g, opts) {
|
|
4274
4694
|
const { yes, dryRun, json } = opts;
|
|
4275
4695
|
const root = process.cwd();
|
|
4276
|
-
const manifestPath =
|
|
4696
|
+
const manifestPath = join13(root, ".sechroom", "repos.json");
|
|
4277
4697
|
const fromManifest = readManifest(manifestPath);
|
|
4278
4698
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
4279
4699
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
@@ -4785,23 +5205,23 @@ Examples:
|
|
|
4785
5205
|
|
|
4786
5206
|
// src/commands/reset.ts
|
|
4787
5207
|
import { homedir as homedir4 } from "os";
|
|
4788
|
-
import { join as
|
|
4789
|
-
import { existsSync as
|
|
5208
|
+
import { join as join14 } from "path";
|
|
5209
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10, rmSync as rmSync3 } from "fs";
|
|
4790
5210
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
4791
|
-
var localSkillsDir = () =>
|
|
4792
|
-
var globalSkillsDir = () =>
|
|
4793
|
-
var localAgentsDir = () =>
|
|
4794
|
-
var globalAgentsDir = () =>
|
|
5211
|
+
var localSkillsDir = () => join14(process.cwd(), ".claude", "skills");
|
|
5212
|
+
var globalSkillsDir = () => join14(homedir4(), ".claude", "skills");
|
|
5213
|
+
var localAgentsDir = () => join14(process.cwd(), ".claude", "agents");
|
|
5214
|
+
var globalAgentsDir = () => join14(homedir4(), ".claude", "agents");
|
|
4795
5215
|
function removeMaterialisedSkills(dir) {
|
|
4796
5216
|
const removed = [];
|
|
4797
|
-
const lockPath =
|
|
4798
|
-
if (!
|
|
5217
|
+
const lockPath = join14(dir, SKILLS_LOCK2);
|
|
5218
|
+
if (!existsSync12(lockPath)) return removed;
|
|
4799
5219
|
try {
|
|
4800
|
-
const lock = JSON.parse(
|
|
5220
|
+
const lock = JSON.parse(readFileSync10(lockPath, "utf8"));
|
|
4801
5221
|
for (const entry of Object.values(lock)) {
|
|
4802
5222
|
for (const name of entry.skills ?? []) {
|
|
4803
|
-
const p =
|
|
4804
|
-
if (
|
|
5223
|
+
const p = join14(dir, name);
|
|
5224
|
+
if (existsSync12(p)) {
|
|
4805
5225
|
rmSync3(p, { recursive: true, force: true });
|
|
4806
5226
|
removed.push(p);
|
|
4807
5227
|
}
|
|
@@ -4846,18 +5266,18 @@ function registerReset(program2) {
|
|
|
4846
5266
|
}
|
|
4847
5267
|
}
|
|
4848
5268
|
const removed = [];
|
|
4849
|
-
const stateDir =
|
|
4850
|
-
if (
|
|
5269
|
+
const stateDir = join14(process.cwd(), ".sechroom");
|
|
5270
|
+
if (existsSync12(stateDir)) {
|
|
4851
5271
|
rmSync3(stateDir, { recursive: true, force: true });
|
|
4852
5272
|
removed.push(stateDir);
|
|
4853
5273
|
}
|
|
4854
|
-
const legacyCfg =
|
|
4855
|
-
if (
|
|
5274
|
+
const legacyCfg = join14(process.cwd(), ".sechroom.json");
|
|
5275
|
+
if (existsSync12(legacyCfg)) {
|
|
4856
5276
|
rmSync3(legacyCfg, { force: true });
|
|
4857
5277
|
removed.push(legacyCfg);
|
|
4858
5278
|
}
|
|
4859
|
-
const legacySem =
|
|
4860
|
-
if (
|
|
5279
|
+
const legacySem = join14(process.cwd(), ".sem");
|
|
5280
|
+
if (existsSync12(legacySem)) {
|
|
4861
5281
|
rmSync3(legacySem, { force: true });
|
|
4862
5282
|
removed.push(legacySem);
|
|
4863
5283
|
}
|
|
@@ -4883,8 +5303,8 @@ function registerReset(program2) {
|
|
|
4883
5303
|
}
|
|
4884
5304
|
|
|
4885
5305
|
// src/commands/skills.ts
|
|
4886
|
-
import { existsSync as
|
|
4887
|
-
import { join as
|
|
5306
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync11, statSync as statSync4, writeFileSync as writeFileSync11 } from "fs";
|
|
5307
|
+
import { join as join15 } from "path";
|
|
4888
5308
|
function filenameFromDisposition(header) {
|
|
4889
5309
|
if (!header) return void 0;
|
|
4890
5310
|
const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
|
|
@@ -4892,11 +5312,11 @@ function filenameFromDisposition(header) {
|
|
|
4892
5312
|
}
|
|
4893
5313
|
function resolveOutputPath(output, serverFilename) {
|
|
4894
5314
|
const filename = serverFilename || "skills.zip";
|
|
4895
|
-
if (!output) return
|
|
4896
|
-
const looksLikeDir = output.endsWith("/") ||
|
|
5315
|
+
if (!output) return join15(process.cwd(), filename);
|
|
5316
|
+
const looksLikeDir = output.endsWith("/") || existsSync13(output) && statSync4(output).isDirectory();
|
|
4897
5317
|
if (looksLikeDir) {
|
|
4898
|
-
|
|
4899
|
-
return
|
|
5318
|
+
mkdirSync11(output, { recursive: true });
|
|
5319
|
+
return join15(output, filename);
|
|
4900
5320
|
}
|
|
4901
5321
|
return output;
|
|
4902
5322
|
}
|
|
@@ -4927,7 +5347,7 @@ async function downloadZip(label, call, output) {
|
|
|
4927
5347
|
const buf = Buffer.from(res.data);
|
|
4928
5348
|
const filename = filenameFromDisposition(res.response.headers.get("content-disposition")) ?? "skills.zip";
|
|
4929
5349
|
const path = resolveOutputPath(output, filename);
|
|
4930
|
-
|
|
5350
|
+
writeFileSync11(path, buf);
|
|
4931
5351
|
return { path, bytes: buf.length, filename };
|
|
4932
5352
|
}
|
|
4933
5353
|
function registerSkills(program2) {
|
|
@@ -5095,12 +5515,12 @@ Examples:
|
|
|
5095
5515
|
}
|
|
5096
5516
|
|
|
5097
5517
|
// src/commands/sweep.ts
|
|
5098
|
-
import { existsSync as
|
|
5099
|
-
import { dirname as
|
|
5100
|
-
var DEFAULT_MANIFEST =
|
|
5518
|
+
import { existsSync as existsSync14 } from "fs";
|
|
5519
|
+
import { dirname as dirname9, join as join16, resolve as resolve2 } from "path";
|
|
5520
|
+
var DEFAULT_MANIFEST = join16(".sechroom", "repos.json");
|
|
5101
5521
|
function planEntry(entry, root) {
|
|
5102
5522
|
const dir = resolveChildDir(entry.path, root);
|
|
5103
|
-
if (!
|
|
5523
|
+
if (!existsSync14(dir)) {
|
|
5104
5524
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
5105
5525
|
}
|
|
5106
5526
|
if (committedBindingPath(dir)) {
|
|
@@ -5176,7 +5596,7 @@ Examples:
|
|
|
5176
5596
|
`);
|
|
5177
5597
|
return;
|
|
5178
5598
|
}
|
|
5179
|
-
const root =
|
|
5599
|
+
const root = dirname9(dirname9(manifestPath));
|
|
5180
5600
|
const plans = repos.map((entry) => planEntry(entry, root));
|
|
5181
5601
|
if (!json) {
|
|
5182
5602
|
process.stderr.write(
|
|
@@ -5195,13 +5615,13 @@ Examples:
|
|
|
5195
5615
|
|
|
5196
5616
|
// src/commands/telemetry.ts
|
|
5197
5617
|
import {
|
|
5198
|
-
existsSync as
|
|
5199
|
-
mkdirSync as
|
|
5200
|
-
readFileSync as
|
|
5618
|
+
existsSync as existsSync15,
|
|
5619
|
+
mkdirSync as mkdirSync12,
|
|
5620
|
+
readFileSync as readFileSync11,
|
|
5201
5621
|
rmSync as rmSync4,
|
|
5202
|
-
writeFileSync as
|
|
5622
|
+
writeFileSync as writeFileSync12
|
|
5203
5623
|
} from "fs";
|
|
5204
|
-
import { dirname as
|
|
5624
|
+
import { dirname as dirname10, join as join17 } from "path";
|
|
5205
5625
|
function registerTelemetry(program2) {
|
|
5206
5626
|
const telemetry = program2.command("telemetry").description(
|
|
5207
5627
|
"Emit WLP run telemetry (an executor leg's progress events) into a decomposition run"
|
|
@@ -5271,14 +5691,14 @@ function registerTelemetry(program2) {
|
|
|
5271
5691
|
"Decomposition id this session executes"
|
|
5272
5692
|
).requiredOption("--task <id>", "Task id this session executes").action((opts, cmd) => {
|
|
5273
5693
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
5274
|
-
const dir =
|
|
5275
|
-
|
|
5276
|
-
const path =
|
|
5694
|
+
const dir = join17(process.cwd(), ".sechroom");
|
|
5695
|
+
mkdirSync12(dir, { recursive: true });
|
|
5696
|
+
const path = join17(dir, BINDING_FILE);
|
|
5277
5697
|
const binding = {
|
|
5278
5698
|
decompositionId: opts.decomposition,
|
|
5279
5699
|
taskId: opts.task
|
|
5280
5700
|
};
|
|
5281
|
-
|
|
5701
|
+
writeFileSync12(path, JSON.stringify(binding, null, 2) + "\n");
|
|
5282
5702
|
ensureStateDirIgnored(process.cwd());
|
|
5283
5703
|
if (json) {
|
|
5284
5704
|
emit({ bound: true, ...binding, path }, true);
|
|
@@ -5293,8 +5713,8 @@ function registerTelemetry(program2) {
|
|
|
5293
5713
|
});
|
|
5294
5714
|
telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
|
|
5295
5715
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
5296
|
-
const path =
|
|
5297
|
-
const existed =
|
|
5716
|
+
const path = join17(process.cwd(), ".sechroom", BINDING_FILE);
|
|
5717
|
+
const existed = existsSync15(path);
|
|
5298
5718
|
if (existed) rmSync4(path);
|
|
5299
5719
|
if (json) emit({ unbound: existed, path }, true);
|
|
5300
5720
|
else
|
|
@@ -5408,11 +5828,11 @@ async function postTelemetry(cfg, decompositionId, events) {
|
|
|
5408
5828
|
function findBinding(start) {
|
|
5409
5829
|
let dir = start;
|
|
5410
5830
|
for (; ; ) {
|
|
5411
|
-
const path =
|
|
5412
|
-
if (
|
|
5831
|
+
const path = join17(dir, ".sechroom", BINDING_FILE);
|
|
5832
|
+
if (existsSync15(path)) {
|
|
5413
5833
|
try {
|
|
5414
5834
|
const b = JSON.parse(
|
|
5415
|
-
|
|
5835
|
+
readFileSync11(path, "utf8")
|
|
5416
5836
|
);
|
|
5417
5837
|
if (b.decompositionId && b.taskId)
|
|
5418
5838
|
return { decompositionId: b.decompositionId, taskId: b.taskId };
|
|
@@ -5420,18 +5840,18 @@ function findBinding(start) {
|
|
|
5420
5840
|
}
|
|
5421
5841
|
return null;
|
|
5422
5842
|
}
|
|
5423
|
-
const parent =
|
|
5843
|
+
const parent = dirname10(dir);
|
|
5424
5844
|
if (parent === dir) return null;
|
|
5425
5845
|
dir = parent;
|
|
5426
5846
|
}
|
|
5427
5847
|
}
|
|
5428
5848
|
function parseTranscript(path) {
|
|
5429
|
-
if (!
|
|
5849
|
+
if (!existsSync15(path)) return null;
|
|
5430
5850
|
let tokensIn = 0;
|
|
5431
5851
|
let tokensOut = 0;
|
|
5432
5852
|
let contextUsed = 0;
|
|
5433
5853
|
let model = "";
|
|
5434
|
-
for (const line of
|
|
5854
|
+
for (const line of readFileSync11(path, "utf8").split("\n")) {
|
|
5435
5855
|
if (!line.trim()) continue;
|
|
5436
5856
|
let obj;
|
|
5437
5857
|
try {
|
|
@@ -5448,11 +5868,12 @@ function parseTranscript(path) {
|
|
|
5448
5868
|
if (obj.message?.model) model = obj.message.model;
|
|
5449
5869
|
}
|
|
5450
5870
|
if (tokensIn === 0 && tokensOut === 0) return null;
|
|
5451
|
-
return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model) };
|
|
5871
|
+
return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model, contextUsed) };
|
|
5452
5872
|
}
|
|
5453
|
-
function windowFor(model) {
|
|
5873
|
+
function windowFor(model, contextUsed = 0) {
|
|
5454
5874
|
const m = model.toLowerCase();
|
|
5455
|
-
|
|
5875
|
+
if (m.includes("[1m]") || m.includes("-1m")) return 1e6;
|
|
5876
|
+
return contextUsed > 2e5 ? 1e6 : 2e5;
|
|
5456
5877
|
}
|
|
5457
5878
|
async function readStdin2() {
|
|
5458
5879
|
if (process.stdin.isTTY) return "";
|
|
@@ -5688,7 +6109,7 @@ Examples:
|
|
|
5688
6109
|
function resolveVersion() {
|
|
5689
6110
|
try {
|
|
5690
6111
|
const pkg = JSON.parse(
|
|
5691
|
-
|
|
6112
|
+
readFileSync12(new URL("../package.json", import.meta.url), "utf8")
|
|
5692
6113
|
);
|
|
5693
6114
|
return pkg.version ?? "0.0.0";
|
|
5694
6115
|
} catch {
|
|
@@ -5846,6 +6267,8 @@ registerLookup(program);
|
|
|
5846
6267
|
registerRelationships(program);
|
|
5847
6268
|
registerWorkspace(program);
|
|
5848
6269
|
registerProject(program);
|
|
6270
|
+
registerDecomposition(program);
|
|
6271
|
+
registerClose(program);
|
|
5849
6272
|
registerFiling(program);
|
|
5850
6273
|
registerContinuity(program);
|
|
5851
6274
|
registerCheckpoint(program);
|