@sechroom/cli 2026.7.1 → 2026.7.3
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 +412 -344
- 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 readFileSync11 } 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)"
|
|
@@ -1387,6 +1603,52 @@ function registerChannel(program2) {
|
|
|
1387
1603
|
);
|
|
1388
1604
|
await holdOpen(conn);
|
|
1389
1605
|
});
|
|
1606
|
+
channel.command("install").description(
|
|
1607
|
+
"Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
|
|
1608
|
+
).option("--workspace <wsp...>", "Restrict dispatches to these workspace id(s)").option(
|
|
1609
|
+
"--tag <tag...>",
|
|
1610
|
+
"Tag(s) a dispatch must carry to match (repeatable). Default targets WLP task dispatches.",
|
|
1611
|
+
["kind:task"]
|
|
1612
|
+
).option(
|
|
1613
|
+
"--name <name>",
|
|
1614
|
+
"MCP server + subscription name (idempotent per name)",
|
|
1615
|
+
"sechroom-channel"
|
|
1616
|
+
).option("--dry-run", "Print what would change; write nothing").action((opts) => {
|
|
1617
|
+
const path = join7(process.cwd(), ".mcp.json");
|
|
1618
|
+
const dryRun = Boolean(opts.dryRun);
|
|
1619
|
+
const args = ["channel", "mcp", "--name", opts.name];
|
|
1620
|
+
for (const w of opts.workspace ?? [])
|
|
1621
|
+
args.push("--workspace", w);
|
|
1622
|
+
for (const t of opts.tag ?? []) args.push("--tag", t);
|
|
1623
|
+
const entry = { command: "sechroom", args };
|
|
1624
|
+
const config2 = readMcpConfig(path);
|
|
1625
|
+
config2.mcpServers ??= {};
|
|
1626
|
+
const existing = config2.mcpServers[opts.name];
|
|
1627
|
+
const status = JSON.stringify(existing) === JSON.stringify(entry) ? "current" : existing ? "updated" : "created";
|
|
1628
|
+
if (status !== "current" && !dryRun) {
|
|
1629
|
+
config2.mcpServers[opts.name] = entry;
|
|
1630
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
1631
|
+
writeFileSync5(path, JSON.stringify(config2, null, 2) + "\n");
|
|
1632
|
+
}
|
|
1633
|
+
const verb = status === "current" ? "already configured" : dryRun ? `would ${status === "created" ? "create" : "update"}` : status;
|
|
1634
|
+
process.stdout.write(`${style.green("channel")} ${path} (${verb})
|
|
1635
|
+
`);
|
|
1636
|
+
process.stdout.write(
|
|
1637
|
+
style.dim(` server "${opts.name}": sechroom ${args.join(" ")}
|
|
1638
|
+
`)
|
|
1639
|
+
);
|
|
1640
|
+
if (status !== "current") {
|
|
1641
|
+
process.stdout.write(
|
|
1642
|
+
style.dim(
|
|
1643
|
+
`
|
|
1644
|
+
Load it (Channels research preview) by launching your agent with:
|
|
1645
|
+
claude --dangerously-load-development-channels server:${opts.name}
|
|
1646
|
+
`
|
|
1647
|
+
)
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
warnIfSechroomNotOnPath();
|
|
1651
|
+
});
|
|
1390
1652
|
channel.addHelpText(
|
|
1391
1653
|
"after",
|
|
1392
1654
|
`
|
|
@@ -1395,11 +1657,23 @@ Examples:
|
|
|
1395
1657
|
$ sechroom channel connect --tag kind:task --tag status:in-progress
|
|
1396
1658
|
$ sechroom channel connect --workspace wsp_X --json | jq .
|
|
1397
1659
|
|
|
1398
|
-
#
|
|
1399
|
-
|
|
1400
|
-
# then: claude --dangerously-load-development-channels server:sechroom`
|
|
1660
|
+
# Wire it as a Claude Code channel MCP server (research preview, v2.1.80+):
|
|
1661
|
+
$ sechroom channel install --workspace wsp_X --tag kind:task --tag status:in-progress
|
|
1662
|
+
# then: claude --dangerously-load-development-channels server:sechroom-channel`
|
|
1401
1663
|
);
|
|
1402
1664
|
}
|
|
1665
|
+
function readMcpConfig(path) {
|
|
1666
|
+
if (!existsSync6(path)) return {};
|
|
1667
|
+
const raw = readFileSync4(path, "utf8");
|
|
1668
|
+
if (!raw.trim()) return {};
|
|
1669
|
+
try {
|
|
1670
|
+
return JSON.parse(raw);
|
|
1671
|
+
} catch {
|
|
1672
|
+
return fail(
|
|
1673
|
+
`Could not parse ${path} as JSON \u2014 fix or remove it before installing the channel.`
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1403
1677
|
function readFilter(opts) {
|
|
1404
1678
|
const tags = opts.tag ?? [];
|
|
1405
1679
|
const workspaceScope = opts.workspace ?? [];
|
|
@@ -1560,28 +1834,28 @@ Examples:
|
|
|
1560
1834
|
}
|
|
1561
1835
|
|
|
1562
1836
|
// src/commands/checkpoint.ts
|
|
1563
|
-
import { mkdirSync as
|
|
1564
|
-
import { dirname as
|
|
1837
|
+
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
1838
|
+
import { dirname as dirname7, join as join10 } from "path";
|
|
1565
1839
|
|
|
1566
1840
|
// src/commands/hook.ts
|
|
1567
1841
|
import { createHash as createHash2 } from "crypto";
|
|
1568
|
-
import { existsSync as
|
|
1569
|
-
import { dirname as
|
|
1842
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync6, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
|
|
1843
|
+
import { dirname as dirname6, join as join9 } from "path";
|
|
1570
1844
|
|
|
1571
1845
|
// src/sem.ts
|
|
1572
|
-
import { dirname as
|
|
1573
|
-
import { appendFileSync, existsSync as
|
|
1574
|
-
var SEM_FILE =
|
|
1846
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
1847
|
+
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync6 } from "fs";
|
|
1848
|
+
var SEM_FILE = join8(".sechroom", "lane.json");
|
|
1575
1849
|
var STATE_DIR_NAME2 = ".sechroom";
|
|
1576
1850
|
function localSemPath(cwd = process.cwd()) {
|
|
1577
|
-
return
|
|
1851
|
+
return join8(cwd, SEM_FILE);
|
|
1578
1852
|
}
|
|
1579
1853
|
function resolveSemPathForRead(start = process.cwd()) {
|
|
1580
1854
|
let dir = start;
|
|
1581
1855
|
while (true) {
|
|
1582
|
-
const candidate =
|
|
1583
|
-
if (
|
|
1584
|
-
const parent =
|
|
1856
|
+
const candidate = join8(dir, SEM_FILE);
|
|
1857
|
+
if (existsSync7(candidate)) return candidate;
|
|
1858
|
+
const parent = dirname5(dir);
|
|
1585
1859
|
if (parent === dir) return void 0;
|
|
1586
1860
|
dir = parent;
|
|
1587
1861
|
}
|
|
@@ -1591,23 +1865,23 @@ function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
|
|
|
1591
1865
|
let dir = start;
|
|
1592
1866
|
let gitPath;
|
|
1593
1867
|
for (; ; ) {
|
|
1594
|
-
const candidate =
|
|
1595
|
-
if (
|
|
1868
|
+
const candidate = join8(dir, ".git");
|
|
1869
|
+
if (existsSync7(candidate)) {
|
|
1596
1870
|
gitPath = candidate;
|
|
1597
1871
|
break;
|
|
1598
1872
|
}
|
|
1599
|
-
const parent =
|
|
1873
|
+
const parent = dirname5(dir);
|
|
1600
1874
|
if (parent === dir) break;
|
|
1601
1875
|
dir = parent;
|
|
1602
1876
|
}
|
|
1603
1877
|
if (!gitPath || statSync(gitPath).isDirectory()) return lane;
|
|
1604
|
-
const gitFile =
|
|
1878
|
+
const gitFile = readFileSync5(gitPath, "utf8");
|
|
1605
1879
|
const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
|
|
1606
1880
|
if (!common) return lane;
|
|
1607
|
-
const worktreesDir =
|
|
1881
|
+
const worktreesDir = join8(common[1], "worktrees");
|
|
1608
1882
|
const siblings = readdirSync(worktreesDir).filter((n) => {
|
|
1609
1883
|
try {
|
|
1610
|
-
return statSync(
|
|
1884
|
+
return statSync(join8(worktreesDir, n)).isDirectory();
|
|
1611
1885
|
} catch {
|
|
1612
1886
|
return false;
|
|
1613
1887
|
}
|
|
@@ -1628,12 +1902,12 @@ function serializeSem(values) {
|
|
|
1628
1902
|
}
|
|
1629
1903
|
function readSem(path) {
|
|
1630
1904
|
const p = path ?? resolveSemPathForRead();
|
|
1631
|
-
if (!p || !
|
|
1632
|
-
return { path: p, values: parseLaneJson(
|
|
1905
|
+
if (!p || !existsSync7(p)) return void 0;
|
|
1906
|
+
return { path: p, values: parseLaneJson(readFileSync5(p, "utf8")) };
|
|
1633
1907
|
}
|
|
1634
1908
|
function readLocalSemValues(cwd = process.cwd()) {
|
|
1635
|
-
const next =
|
|
1636
|
-
if (
|
|
1909
|
+
const next = join8(cwd, SEM_FILE);
|
|
1910
|
+
if (existsSync7(next)) return readSem(next)?.values ?? {};
|
|
1637
1911
|
return {};
|
|
1638
1912
|
}
|
|
1639
1913
|
function parseLaneJson(text2) {
|
|
@@ -1650,8 +1924,8 @@ function parseLaneJson(text2) {
|
|
|
1650
1924
|
}
|
|
1651
1925
|
var STATE_DIR_IGNORE = `${STATE_DIR_NAME2}/`;
|
|
1652
1926
|
function writeSem(values, path = localSemPath()) {
|
|
1653
|
-
|
|
1654
|
-
|
|
1927
|
+
mkdirSync6(dirname5(path), { recursive: true });
|
|
1928
|
+
writeFileSync6(path, serializeSem(values));
|
|
1655
1929
|
ensureSemIgnored(path);
|
|
1656
1930
|
ensureContinuityScaffold(path);
|
|
1657
1931
|
return path;
|
|
@@ -1678,9 +1952,9 @@ var CONTINUITY_SCAFFOLD = JSON.stringify(
|
|
|
1678
1952
|
) + "\n";
|
|
1679
1953
|
function ensureContinuityScaffold(semPath) {
|
|
1680
1954
|
try {
|
|
1681
|
-
const target =
|
|
1682
|
-
if (
|
|
1683
|
-
|
|
1955
|
+
const target = join8(dirname5(semPath), CONTINUITY_FILE_NAME);
|
|
1956
|
+
if (existsSync7(target)) return;
|
|
1957
|
+
writeFileSync6(target, CONTINUITY_SCAFFOLD);
|
|
1684
1958
|
} catch {
|
|
1685
1959
|
}
|
|
1686
1960
|
}
|
|
@@ -1693,8 +1967,8 @@ function ignoresSem(content) {
|
|
|
1693
1967
|
function inGitRepo(startDir) {
|
|
1694
1968
|
let dir = startDir;
|
|
1695
1969
|
for (; ; ) {
|
|
1696
|
-
if (
|
|
1697
|
-
const parent =
|
|
1970
|
+
if (existsSync7(join8(dir, ".git"))) return true;
|
|
1971
|
+
const parent = dirname5(dir);
|
|
1698
1972
|
if (parent === dir) return false;
|
|
1699
1973
|
dir = parent;
|
|
1700
1974
|
}
|
|
@@ -1702,246 +1976,34 @@ function inGitRepo(startDir) {
|
|
|
1702
1976
|
function resolveGitignoreTarget(startDir) {
|
|
1703
1977
|
let dir = startDir;
|
|
1704
1978
|
for (; ; ) {
|
|
1705
|
-
const gi =
|
|
1706
|
-
if (
|
|
1707
|
-
const parent =
|
|
1708
|
-
if (
|
|
1709
|
-
return { path:
|
|
1979
|
+
const gi = join8(dir, ".gitignore");
|
|
1980
|
+
if (existsSync7(gi)) return { path: gi, exists: true };
|
|
1981
|
+
const parent = dirname5(dir);
|
|
1982
|
+
if (existsSync7(join8(dir, ".git")) || parent === dir) {
|
|
1983
|
+
return { path: join8(startDir, ".gitignore"), exists: false };
|
|
1710
1984
|
}
|
|
1711
1985
|
dir = parent;
|
|
1712
1986
|
}
|
|
1713
1987
|
}
|
|
1714
1988
|
function ensureSemIgnored(semPath) {
|
|
1715
1989
|
try {
|
|
1716
|
-
const checkoutDir =
|
|
1990
|
+
const checkoutDir = dirname5(dirname5(semPath));
|
|
1717
1991
|
if (!inGitRepo(checkoutDir)) return;
|
|
1718
1992
|
const target = resolveGitignoreTarget(checkoutDir);
|
|
1719
1993
|
if (target.exists) {
|
|
1720
|
-
const content =
|
|
1994
|
+
const content = readFileSync5(target.path, "utf8");
|
|
1721
1995
|
if (ignoresSem(content)) return;
|
|
1722
1996
|
const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
1723
1997
|
appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
|
|
1724
1998
|
`);
|
|
1725
1999
|
} else {
|
|
1726
|
-
|
|
2000
|
+
writeFileSync6(target.path, `${STATE_DIR_IGNORE}
|
|
1727
2001
|
`);
|
|
1728
2002
|
}
|
|
1729
2003
|
} catch {
|
|
1730
2004
|
}
|
|
1731
2005
|
}
|
|
1732
2006
|
|
|
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;
|
|
1835
|
-
}
|
|
1836
|
-
function readJsonConfig2(path) {
|
|
1837
|
-
if (!existsSync6(path)) return {};
|
|
1838
|
-
const raw = readFileSync4(path, "utf8");
|
|
1839
|
-
if (!raw.trim()) return {};
|
|
1840
|
-
return JSON.parse(raw);
|
|
1841
|
-
}
|
|
1842
|
-
function installHooksJson(path, commands, dryRun) {
|
|
1843
|
-
const existed = existsSync6(path) && readFileSync4(path, "utf8").trim().length > 0;
|
|
1844
|
-
const config2 = readJsonConfig2(path);
|
|
1845
|
-
const added = mergeHooks(config2, commands);
|
|
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" };
|
|
1852
|
-
}
|
|
1853
|
-
function installClaudeCommands(claudeDir, commands, dryRun) {
|
|
1854
|
-
return installHooksJson(join7(claudeDir, "settings.json"), commands, dryRun);
|
|
1855
|
-
}
|
|
1856
|
-
function ensureCodexFeaturesHooks(content) {
|
|
1857
|
-
const lines = content.split("\n");
|
|
1858
|
-
const headerIdx = lines.findIndex((l) => l.trim() === "[features]");
|
|
1859
|
-
if (headerIdx === -1) {
|
|
1860
|
-
const base = content.length === 0 || content.endsWith("\n") ? content : content + "\n";
|
|
1861
|
-
return { next: base + "\n[features]\nhooks = true\n", changed: true };
|
|
1862
|
-
}
|
|
1863
|
-
for (let i = headerIdx + 1; i < lines.length; i += 1) {
|
|
1864
|
-
const trimmed = lines[i].trim();
|
|
1865
|
-
if (trimmed.startsWith("[") && trimmed.endsWith("]")) break;
|
|
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 };
|
|
1872
|
-
}
|
|
1873
|
-
lines.splice(headerIdx + 1, 0, "hooks = true");
|
|
1874
|
-
return { next: lines.join("\n"), changed: true };
|
|
1875
|
-
}
|
|
1876
|
-
function installCodexFeatureFlag(path, dryRun) {
|
|
1877
|
-
const existed = existsSync6(path);
|
|
1878
|
-
const content = existed ? readFileSync4(path, "utf8") : "";
|
|
1879
|
-
const { next, changed } = ensureCodexFeaturesHooks(content);
|
|
1880
|
-
if (!changed) return { path, status: "current" };
|
|
1881
|
-
if (!dryRun) {
|
|
1882
|
-
mkdirSync5(dirname4(path), { recursive: true });
|
|
1883
|
-
writeFileSync5(path, next);
|
|
1884
|
-
}
|
|
1885
|
-
return { path, status: existed ? "merged" : "created" };
|
|
1886
|
-
}
|
|
1887
|
-
function resolveSurfaces(surface, cwd) {
|
|
1888
|
-
if (surface === "claude") return ["claude"];
|
|
1889
|
-
if (surface === "codex") return ["codex"];
|
|
1890
|
-
if (surface === "both") return ["claude", "codex"];
|
|
1891
|
-
if (surface) throw new Error(`--surface must be one of claude | codex | both (got '${surface}')`);
|
|
1892
|
-
const surfaces = detectHookSurfaces(cwd);
|
|
1893
|
-
return surfaces.length > 0 ? surfaces : ["claude", "codex"];
|
|
1894
|
-
}
|
|
1895
|
-
function describe(result, dryRun) {
|
|
1896
|
-
if (result.status === "current") return ` \u2713 ${result.path} (already configured)`;
|
|
1897
|
-
const verb = dryRun ? "would" : result.status === "created" ? "created" : "updated";
|
|
1898
|
-
return ` \u2713 ${result.path} (${dryRun ? `${verb} ${result.status === "created" ? "create" : "update"}` : verb})`;
|
|
1899
|
-
}
|
|
1900
|
-
var HOOK_SURFACE_LABEL = {
|
|
1901
|
-
claude: "Claude Code",
|
|
1902
|
-
codex: "Codex"
|
|
1903
|
-
};
|
|
1904
|
-
function installHookSurfaces(surfaces, opts) {
|
|
1905
|
-
const out = [];
|
|
1906
|
-
for (const surface of surfaces) {
|
|
1907
|
-
if (surface === "claude") {
|
|
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] });
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
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
|
-
}
|
|
1925
|
-
function isSechroomOnPath() {
|
|
1926
|
-
const pathEnv = process.env.PATH ?? "";
|
|
1927
|
-
if (!pathEnv) return false;
|
|
1928
|
-
const names = process.platform === "win32" ? ["sechroom.cmd", "sechroom.exe", "sechroom.bat", "sechroom"] : ["sechroom"];
|
|
1929
|
-
for (const dir of pathEnv.split(delimiter)) {
|
|
1930
|
-
if (!dir) continue;
|
|
1931
|
-
for (const name of names) {
|
|
1932
|
-
if (existsSync6(join7(dir, name))) return true;
|
|
1933
|
-
}
|
|
1934
|
-
}
|
|
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
|
-
}
|
|
1944
|
-
|
|
1945
2007
|
// src/commands/hook.ts
|
|
1946
2008
|
async function readStdin() {
|
|
1947
2009
|
if (process.stdin.isTTY) return "";
|
|
@@ -1966,13 +2028,13 @@ function resolveLane(flagLane, cwd) {
|
|
|
1966
2028
|
if (!base) return void 0;
|
|
1967
2029
|
return applyWorktreeLaneSuffix(base, start);
|
|
1968
2030
|
}
|
|
1969
|
-
var INTENT_FILE =
|
|
2031
|
+
var INTENT_FILE = join9(".sechroom", "continuity.json");
|
|
1970
2032
|
function resolveIntentPath(start) {
|
|
1971
2033
|
let dir = start;
|
|
1972
2034
|
for (; ; ) {
|
|
1973
|
-
const candidate =
|
|
1974
|
-
if (
|
|
1975
|
-
const parent =
|
|
2035
|
+
const candidate = join9(dir, INTENT_FILE);
|
|
2036
|
+
if (existsSync8(candidate)) return candidate;
|
|
2037
|
+
const parent = dirname6(dir);
|
|
1976
2038
|
if (parent === dir) return void 0;
|
|
1977
2039
|
dir = parent;
|
|
1978
2040
|
}
|
|
@@ -1981,7 +2043,7 @@ function readIntent(start) {
|
|
|
1981
2043
|
const path = resolveIntentPath(start);
|
|
1982
2044
|
if (!path) return void 0;
|
|
1983
2045
|
try {
|
|
1984
|
-
return JSON.parse(
|
|
2046
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
1985
2047
|
} catch {
|
|
1986
2048
|
return void 0;
|
|
1987
2049
|
}
|
|
@@ -2023,14 +2085,14 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
|
|
|
2023
2085
|
}
|
|
2024
2086
|
function ledgerPath(start) {
|
|
2025
2087
|
const intent = resolveIntentPath(start);
|
|
2026
|
-
const dir = intent ?
|
|
2027
|
-
return
|
|
2088
|
+
const dir = intent ? dirname6(intent) : join9(start, ".sechroom");
|
|
2089
|
+
return join9(dir, ".checkpoint-state.json");
|
|
2028
2090
|
}
|
|
2029
2091
|
function readLedger(start) {
|
|
2030
2092
|
try {
|
|
2031
2093
|
const p = ledgerPath(start);
|
|
2032
|
-
if (!
|
|
2033
|
-
return JSON.parse(
|
|
2094
|
+
if (!existsSync8(p)) return {};
|
|
2095
|
+
return JSON.parse(readFileSync6(p, "utf8"));
|
|
2034
2096
|
} catch {
|
|
2035
2097
|
return {};
|
|
2036
2098
|
}
|
|
@@ -2077,13 +2139,13 @@ function recordPush(start, intent) {
|
|
|
2077
2139
|
} catch {
|
|
2078
2140
|
mtimeMs = void 0;
|
|
2079
2141
|
}
|
|
2080
|
-
|
|
2142
|
+
mkdirSync7(dirname6(p), { recursive: true });
|
|
2081
2143
|
const ledger = {
|
|
2082
2144
|
lastEpochMs: Date.now(),
|
|
2083
2145
|
lastMtimeMs: mtimeMs,
|
|
2084
2146
|
lastHash: intentHash(intent)
|
|
2085
2147
|
};
|
|
2086
|
-
|
|
2148
|
+
writeFileSync7(p, JSON.stringify(ledger) + "\n");
|
|
2087
2149
|
} catch {
|
|
2088
2150
|
}
|
|
2089
2151
|
}
|
|
@@ -2330,10 +2392,10 @@ Examples:
|
|
|
2330
2392
|
const client = await makeClient(cfg);
|
|
2331
2393
|
return client.POST("/continuity/snapshots", { body });
|
|
2332
2394
|
});
|
|
2333
|
-
const path = resolveIntentPath(cwd) ??
|
|
2395
|
+
const path = resolveIntentPath(cwd) ?? join10(cwd, INTENT_FILE);
|
|
2334
2396
|
const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
|
|
2335
|
-
|
|
2336
|
-
|
|
2397
|
+
mkdirSync8(dirname7(path), { recursive: true });
|
|
2398
|
+
writeFileSync8(path, JSON.stringify(fileBody, null, 2) + "\n");
|
|
2337
2399
|
recordPush(cwd, merged);
|
|
2338
2400
|
if (json) {
|
|
2339
2401
|
emit({ snapshotId: data.snapshotId, lane, scope, file: path }, true);
|
|
@@ -3017,8 +3079,8 @@ Examples:
|
|
|
3017
3079
|
|
|
3018
3080
|
// src/setup/apply.ts
|
|
3019
3081
|
import { createHash as createHash3 } from "crypto";
|
|
3020
|
-
import { mkdirSync as
|
|
3021
|
-
import { dirname as
|
|
3082
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync9, existsSync as existsSync9 } from "fs";
|
|
3083
|
+
import { dirname as dirname8 } from "path";
|
|
3022
3084
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
3023
3085
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
3024
3086
|
function normalizeBody(s) {
|
|
@@ -3071,22 +3133,22 @@ function parseManagedBlock(content, block) {
|
|
|
3071
3133
|
return null;
|
|
3072
3134
|
}
|
|
3073
3135
|
function ensureDir2(path) {
|
|
3074
|
-
|
|
3136
|
+
mkdirSync9(dirname8(path), { recursive: true });
|
|
3075
3137
|
}
|
|
3076
3138
|
function readOr(path, fallback) {
|
|
3077
3139
|
try {
|
|
3078
|
-
return
|
|
3140
|
+
return readFileSync7(path, "utf8");
|
|
3079
3141
|
} catch {
|
|
3080
3142
|
return fallback;
|
|
3081
3143
|
}
|
|
3082
3144
|
}
|
|
3083
3145
|
function mergeMcpJson(path, snippet, dryRun) {
|
|
3084
3146
|
const incoming = JSON.parse(snippet);
|
|
3085
|
-
const existed =
|
|
3147
|
+
const existed = existsSync9(path);
|
|
3086
3148
|
let current = {};
|
|
3087
3149
|
if (existed) {
|
|
3088
3150
|
try {
|
|
3089
|
-
current = JSON.parse(
|
|
3151
|
+
current = JSON.parse(readFileSync7(path, "utf8"));
|
|
3090
3152
|
} catch {
|
|
3091
3153
|
return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
|
|
3092
3154
|
}
|
|
@@ -3094,26 +3156,26 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
3094
3156
|
current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
|
|
3095
3157
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3096
3158
|
ensureDir2(path);
|
|
3097
|
-
|
|
3159
|
+
writeFileSync9(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
3098
3160
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3099
3161
|
}
|
|
3100
3162
|
function mergeCodexToml(path, snippet, dryRun) {
|
|
3101
|
-
const existed =
|
|
3163
|
+
const existed = existsSync9(path);
|
|
3102
3164
|
let body = readOr(path, "");
|
|
3103
3165
|
body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
|
|
3104
3166
|
const trimmed = body.trim();
|
|
3105
3167
|
const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
|
|
3106
3168
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3107
3169
|
ensureDir2(path);
|
|
3108
|
-
|
|
3170
|
+
writeFileSync9(path, next, { mode: 384 });
|
|
3109
3171
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3110
3172
|
}
|
|
3111
3173
|
function writeInstructionBlock(path, write, dryRun) {
|
|
3112
|
-
const existed =
|
|
3174
|
+
const existed = existsSync9(path);
|
|
3113
3175
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
3114
3176
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
3115
3177
|
ensureDir2(path);
|
|
3116
|
-
|
|
3178
|
+
writeFileSync9(path, next);
|
|
3117
3179
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
3118
3180
|
}
|
|
3119
3181
|
function computeBlockFile(current, write) {
|
|
@@ -3154,7 +3216,7 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
3154
3216
|
const next = computeBlockFile(current, write);
|
|
3155
3217
|
if (!dryRun) {
|
|
3156
3218
|
ensureDir2(proposedPath);
|
|
3157
|
-
|
|
3219
|
+
writeFileSync9(proposedPath, next);
|
|
3158
3220
|
}
|
|
3159
3221
|
return {
|
|
3160
3222
|
kind: "instruction",
|
|
@@ -3194,7 +3256,10 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
3194
3256
|
if (!section) {
|
|
3195
3257
|
actions.push({ kind: "instruction", path: target.instruction.path, status: "skipped", note: `no instruction-file section on surface '${target.instruction.surfaceKey}'` });
|
|
3196
3258
|
} else {
|
|
3197
|
-
const resolved = await
|
|
3259
|
+
const resolved = await withSpinner(
|
|
3260
|
+
`Resolving ${target.label} agent instructions`,
|
|
3261
|
+
() => resolveInstruction(cfg, section, opts.personalWorkspaceId)
|
|
3262
|
+
);
|
|
3198
3263
|
if (!resolved) {
|
|
3199
3264
|
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
3265
|
} else {
|
|
@@ -3209,7 +3274,10 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
3209
3274
|
}
|
|
3210
3275
|
const conventionsSection = findSection(surface, SectionType.WorkspaceConventions);
|
|
3211
3276
|
if (conventionsSection) {
|
|
3212
|
-
const conventions = await
|
|
3277
|
+
const conventions = await withSpinner(
|
|
3278
|
+
`Composing ${target.label} workspace conventions`,
|
|
3279
|
+
() => resolveWorkspaceConventions(cfg, conventionsSection)
|
|
3280
|
+
);
|
|
3213
3281
|
if (conventions) {
|
|
3214
3282
|
const action = applyBlock(
|
|
3215
3283
|
target.instruction.path,
|
|
@@ -3278,8 +3346,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
3278
3346
|
}
|
|
3279
3347
|
|
|
3280
3348
|
// src/setup/skills-offer.ts
|
|
3281
|
-
import { mkdirSync as
|
|
3282
|
-
import { join as
|
|
3349
|
+
import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
3350
|
+
import { join as join11 } from "path";
|
|
3283
3351
|
|
|
3284
3352
|
// src/setup/lane-pin.ts
|
|
3285
3353
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -3395,8 +3463,8 @@ Found ${summary} available to you for ${surface}.
|
|
|
3395
3463
|
if (skills.length > 0) {
|
|
3396
3464
|
const written = [];
|
|
3397
3465
|
for (const s of skills) {
|
|
3398
|
-
|
|
3399
|
-
|
|
3466
|
+
mkdirSync10(join11(sDir, s.name), { recursive: true });
|
|
3467
|
+
writeFileSync10(join11(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
3400
3468
|
written.push(s.name);
|
|
3401
3469
|
}
|
|
3402
3470
|
recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -3404,11 +3472,11 @@ Found ${summary} available to you for ${surface}.
|
|
|
3404
3472
|
`);
|
|
3405
3473
|
}
|
|
3406
3474
|
if (agents.length > 0) {
|
|
3407
|
-
|
|
3475
|
+
mkdirSync10(aDir, { recursive: true });
|
|
3408
3476
|
const written = [];
|
|
3409
3477
|
for (const a of agents) {
|
|
3410
3478
|
const file = `${a.name}.md`;
|
|
3411
|
-
|
|
3479
|
+
writeFileSync10(join11(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
3412
3480
|
written.push(file);
|
|
3413
3481
|
}
|
|
3414
3482
|
recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -3791,13 +3859,13 @@ Wired to namespace '${slug}'. Restart your AI client (or reload MCP) to pick it
|
|
|
3791
3859
|
}
|
|
3792
3860
|
|
|
3793
3861
|
// src/commands/onboard.ts
|
|
3794
|
-
import { existsSync as
|
|
3795
|
-
import { basename as basename2, join as
|
|
3862
|
+
import { existsSync as existsSync11 } from "fs";
|
|
3863
|
+
import { basename as basename2, join as join13 } from "path";
|
|
3796
3864
|
|
|
3797
3865
|
// src/commands/fanout.ts
|
|
3798
3866
|
import { spawnSync } from "child_process";
|
|
3799
|
-
import { existsSync as
|
|
3800
|
-
import { isAbsolute, join as
|
|
3867
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
3868
|
+
import { isAbsolute, join as join12, resolve } from "path";
|
|
3801
3869
|
var ICON = {
|
|
3802
3870
|
refresh: "\u21BB",
|
|
3803
3871
|
bind: "+",
|
|
@@ -3817,21 +3885,21 @@ function discoverChildren(root) {
|
|
|
3817
3885
|
const out = [];
|
|
3818
3886
|
for (const name of names.sort()) {
|
|
3819
3887
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
3820
|
-
const dir =
|
|
3888
|
+
const dir = join12(root, name);
|
|
3821
3889
|
try {
|
|
3822
3890
|
if (!statSync3(dir).isDirectory()) continue;
|
|
3823
3891
|
} catch {
|
|
3824
3892
|
continue;
|
|
3825
3893
|
}
|
|
3826
|
-
if (
|
|
3894
|
+
if (existsSync10(join12(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
3827
3895
|
}
|
|
3828
3896
|
return out;
|
|
3829
3897
|
}
|
|
3830
3898
|
function readManifest(path) {
|
|
3831
|
-
if (!
|
|
3899
|
+
if (!existsSync10(path)) return null;
|
|
3832
3900
|
let parsed;
|
|
3833
3901
|
try {
|
|
3834
|
-
parsed = JSON.parse(
|
|
3902
|
+
parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
3835
3903
|
} catch (err2) {
|
|
3836
3904
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
3837
3905
|
}
|
|
@@ -4197,10 +4265,10 @@ async function chooseScope(scopeFlag, yes) {
|
|
|
4197
4265
|
}
|
|
4198
4266
|
async function planRecurseChild(entry, root, client, opts) {
|
|
4199
4267
|
const dir = resolveChildDir(entry.path, root);
|
|
4200
|
-
if (!
|
|
4268
|
+
if (!existsSync11(dir)) {
|
|
4201
4269
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
4202
4270
|
}
|
|
4203
|
-
if (
|
|
4271
|
+
if (existsSync11(join13(dir, ".sechroom.json"))) {
|
|
4204
4272
|
return {
|
|
4205
4273
|
label: entry.path,
|
|
4206
4274
|
dir,
|
|
@@ -4273,7 +4341,7 @@ This fan-out will pin the same lane in every repo:
|
|
|
4273
4341
|
async function runRecurse(cfg, g, opts) {
|
|
4274
4342
|
const { yes, dryRun, json } = opts;
|
|
4275
4343
|
const root = process.cwd();
|
|
4276
|
-
const manifestPath =
|
|
4344
|
+
const manifestPath = join13(root, ".sechroom", "repos.json");
|
|
4277
4345
|
const fromManifest = readManifest(manifestPath);
|
|
4278
4346
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
4279
4347
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
@@ -4785,23 +4853,23 @@ Examples:
|
|
|
4785
4853
|
|
|
4786
4854
|
// src/commands/reset.ts
|
|
4787
4855
|
import { homedir as homedir4 } from "os";
|
|
4788
|
-
import { join as
|
|
4789
|
-
import { existsSync as
|
|
4856
|
+
import { join as join14 } from "path";
|
|
4857
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9, rmSync as rmSync3 } from "fs";
|
|
4790
4858
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
4791
|
-
var localSkillsDir = () =>
|
|
4792
|
-
var globalSkillsDir = () =>
|
|
4793
|
-
var localAgentsDir = () =>
|
|
4794
|
-
var globalAgentsDir = () =>
|
|
4859
|
+
var localSkillsDir = () => join14(process.cwd(), ".claude", "skills");
|
|
4860
|
+
var globalSkillsDir = () => join14(homedir4(), ".claude", "skills");
|
|
4861
|
+
var localAgentsDir = () => join14(process.cwd(), ".claude", "agents");
|
|
4862
|
+
var globalAgentsDir = () => join14(homedir4(), ".claude", "agents");
|
|
4795
4863
|
function removeMaterialisedSkills(dir) {
|
|
4796
4864
|
const removed = [];
|
|
4797
|
-
const lockPath =
|
|
4798
|
-
if (!
|
|
4865
|
+
const lockPath = join14(dir, SKILLS_LOCK2);
|
|
4866
|
+
if (!existsSync12(lockPath)) return removed;
|
|
4799
4867
|
try {
|
|
4800
|
-
const lock = JSON.parse(
|
|
4868
|
+
const lock = JSON.parse(readFileSync9(lockPath, "utf8"));
|
|
4801
4869
|
for (const entry of Object.values(lock)) {
|
|
4802
4870
|
for (const name of entry.skills ?? []) {
|
|
4803
|
-
const p =
|
|
4804
|
-
if (
|
|
4871
|
+
const p = join14(dir, name);
|
|
4872
|
+
if (existsSync12(p)) {
|
|
4805
4873
|
rmSync3(p, { recursive: true, force: true });
|
|
4806
4874
|
removed.push(p);
|
|
4807
4875
|
}
|
|
@@ -4846,18 +4914,18 @@ function registerReset(program2) {
|
|
|
4846
4914
|
}
|
|
4847
4915
|
}
|
|
4848
4916
|
const removed = [];
|
|
4849
|
-
const stateDir =
|
|
4850
|
-
if (
|
|
4917
|
+
const stateDir = join14(process.cwd(), ".sechroom");
|
|
4918
|
+
if (existsSync12(stateDir)) {
|
|
4851
4919
|
rmSync3(stateDir, { recursive: true, force: true });
|
|
4852
4920
|
removed.push(stateDir);
|
|
4853
4921
|
}
|
|
4854
|
-
const legacyCfg =
|
|
4855
|
-
if (
|
|
4922
|
+
const legacyCfg = join14(process.cwd(), ".sechroom.json");
|
|
4923
|
+
if (existsSync12(legacyCfg)) {
|
|
4856
4924
|
rmSync3(legacyCfg, { force: true });
|
|
4857
4925
|
removed.push(legacyCfg);
|
|
4858
4926
|
}
|
|
4859
|
-
const legacySem =
|
|
4860
|
-
if (
|
|
4927
|
+
const legacySem = join14(process.cwd(), ".sem");
|
|
4928
|
+
if (existsSync12(legacySem)) {
|
|
4861
4929
|
rmSync3(legacySem, { force: true });
|
|
4862
4930
|
removed.push(legacySem);
|
|
4863
4931
|
}
|
|
@@ -4883,8 +4951,8 @@ function registerReset(program2) {
|
|
|
4883
4951
|
}
|
|
4884
4952
|
|
|
4885
4953
|
// src/commands/skills.ts
|
|
4886
|
-
import { existsSync as
|
|
4887
|
-
import { join as
|
|
4954
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync11, statSync as statSync4, writeFileSync as writeFileSync11 } from "fs";
|
|
4955
|
+
import { join as join15 } from "path";
|
|
4888
4956
|
function filenameFromDisposition(header) {
|
|
4889
4957
|
if (!header) return void 0;
|
|
4890
4958
|
const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
|
|
@@ -4892,11 +4960,11 @@ function filenameFromDisposition(header) {
|
|
|
4892
4960
|
}
|
|
4893
4961
|
function resolveOutputPath(output, serverFilename) {
|
|
4894
4962
|
const filename = serverFilename || "skills.zip";
|
|
4895
|
-
if (!output) return
|
|
4896
|
-
const looksLikeDir = output.endsWith("/") ||
|
|
4963
|
+
if (!output) return join15(process.cwd(), filename);
|
|
4964
|
+
const looksLikeDir = output.endsWith("/") || existsSync13(output) && statSync4(output).isDirectory();
|
|
4897
4965
|
if (looksLikeDir) {
|
|
4898
|
-
|
|
4899
|
-
return
|
|
4966
|
+
mkdirSync11(output, { recursive: true });
|
|
4967
|
+
return join15(output, filename);
|
|
4900
4968
|
}
|
|
4901
4969
|
return output;
|
|
4902
4970
|
}
|
|
@@ -4927,7 +4995,7 @@ async function downloadZip(label, call, output) {
|
|
|
4927
4995
|
const buf = Buffer.from(res.data);
|
|
4928
4996
|
const filename = filenameFromDisposition(res.response.headers.get("content-disposition")) ?? "skills.zip";
|
|
4929
4997
|
const path = resolveOutputPath(output, filename);
|
|
4930
|
-
|
|
4998
|
+
writeFileSync11(path, buf);
|
|
4931
4999
|
return { path, bytes: buf.length, filename };
|
|
4932
5000
|
}
|
|
4933
5001
|
function registerSkills(program2) {
|
|
@@ -5095,12 +5163,12 @@ Examples:
|
|
|
5095
5163
|
}
|
|
5096
5164
|
|
|
5097
5165
|
// src/commands/sweep.ts
|
|
5098
|
-
import { existsSync as
|
|
5099
|
-
import { dirname as
|
|
5100
|
-
var DEFAULT_MANIFEST =
|
|
5166
|
+
import { existsSync as existsSync14 } from "fs";
|
|
5167
|
+
import { dirname as dirname9, join as join16, resolve as resolve2 } from "path";
|
|
5168
|
+
var DEFAULT_MANIFEST = join16(".sechroom", "repos.json");
|
|
5101
5169
|
function planEntry(entry, root) {
|
|
5102
5170
|
const dir = resolveChildDir(entry.path, root);
|
|
5103
|
-
if (!
|
|
5171
|
+
if (!existsSync14(dir)) {
|
|
5104
5172
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
5105
5173
|
}
|
|
5106
5174
|
if (committedBindingPath(dir)) {
|
|
@@ -5176,7 +5244,7 @@ Examples:
|
|
|
5176
5244
|
`);
|
|
5177
5245
|
return;
|
|
5178
5246
|
}
|
|
5179
|
-
const root =
|
|
5247
|
+
const root = dirname9(dirname9(manifestPath));
|
|
5180
5248
|
const plans = repos.map((entry) => planEntry(entry, root));
|
|
5181
5249
|
if (!json) {
|
|
5182
5250
|
process.stderr.write(
|
|
@@ -5195,13 +5263,13 @@ Examples:
|
|
|
5195
5263
|
|
|
5196
5264
|
// src/commands/telemetry.ts
|
|
5197
5265
|
import {
|
|
5198
|
-
existsSync as
|
|
5199
|
-
mkdirSync as
|
|
5200
|
-
readFileSync as
|
|
5266
|
+
existsSync as existsSync15,
|
|
5267
|
+
mkdirSync as mkdirSync12,
|
|
5268
|
+
readFileSync as readFileSync10,
|
|
5201
5269
|
rmSync as rmSync4,
|
|
5202
|
-
writeFileSync as
|
|
5270
|
+
writeFileSync as writeFileSync12
|
|
5203
5271
|
} from "fs";
|
|
5204
|
-
import { dirname as
|
|
5272
|
+
import { dirname as dirname10, join as join17 } from "path";
|
|
5205
5273
|
function registerTelemetry(program2) {
|
|
5206
5274
|
const telemetry = program2.command("telemetry").description(
|
|
5207
5275
|
"Emit WLP run telemetry (an executor leg's progress events) into a decomposition run"
|
|
@@ -5271,14 +5339,14 @@ function registerTelemetry(program2) {
|
|
|
5271
5339
|
"Decomposition id this session executes"
|
|
5272
5340
|
).requiredOption("--task <id>", "Task id this session executes").action((opts, cmd) => {
|
|
5273
5341
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
5274
|
-
const dir =
|
|
5275
|
-
|
|
5276
|
-
const path =
|
|
5342
|
+
const dir = join17(process.cwd(), ".sechroom");
|
|
5343
|
+
mkdirSync12(dir, { recursive: true });
|
|
5344
|
+
const path = join17(dir, BINDING_FILE);
|
|
5277
5345
|
const binding = {
|
|
5278
5346
|
decompositionId: opts.decomposition,
|
|
5279
5347
|
taskId: opts.task
|
|
5280
5348
|
};
|
|
5281
|
-
|
|
5349
|
+
writeFileSync12(path, JSON.stringify(binding, null, 2) + "\n");
|
|
5282
5350
|
ensureStateDirIgnored(process.cwd());
|
|
5283
5351
|
if (json) {
|
|
5284
5352
|
emit({ bound: true, ...binding, path }, true);
|
|
@@ -5293,8 +5361,8 @@ function registerTelemetry(program2) {
|
|
|
5293
5361
|
});
|
|
5294
5362
|
telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
|
|
5295
5363
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
5296
|
-
const path =
|
|
5297
|
-
const existed =
|
|
5364
|
+
const path = join17(process.cwd(), ".sechroom", BINDING_FILE);
|
|
5365
|
+
const existed = existsSync15(path);
|
|
5298
5366
|
if (existed) rmSync4(path);
|
|
5299
5367
|
if (json) emit({ unbound: existed, path }, true);
|
|
5300
5368
|
else
|
|
@@ -5408,11 +5476,11 @@ async function postTelemetry(cfg, decompositionId, events) {
|
|
|
5408
5476
|
function findBinding(start) {
|
|
5409
5477
|
let dir = start;
|
|
5410
5478
|
for (; ; ) {
|
|
5411
|
-
const path =
|
|
5412
|
-
if (
|
|
5479
|
+
const path = join17(dir, ".sechroom", BINDING_FILE);
|
|
5480
|
+
if (existsSync15(path)) {
|
|
5413
5481
|
try {
|
|
5414
5482
|
const b = JSON.parse(
|
|
5415
|
-
|
|
5483
|
+
readFileSync10(path, "utf8")
|
|
5416
5484
|
);
|
|
5417
5485
|
if (b.decompositionId && b.taskId)
|
|
5418
5486
|
return { decompositionId: b.decompositionId, taskId: b.taskId };
|
|
@@ -5420,18 +5488,18 @@ function findBinding(start) {
|
|
|
5420
5488
|
}
|
|
5421
5489
|
return null;
|
|
5422
5490
|
}
|
|
5423
|
-
const parent =
|
|
5491
|
+
const parent = dirname10(dir);
|
|
5424
5492
|
if (parent === dir) return null;
|
|
5425
5493
|
dir = parent;
|
|
5426
5494
|
}
|
|
5427
5495
|
}
|
|
5428
5496
|
function parseTranscript(path) {
|
|
5429
|
-
if (!
|
|
5497
|
+
if (!existsSync15(path)) return null;
|
|
5430
5498
|
let tokensIn = 0;
|
|
5431
5499
|
let tokensOut = 0;
|
|
5432
5500
|
let contextUsed = 0;
|
|
5433
5501
|
let model = "";
|
|
5434
|
-
for (const line of
|
|
5502
|
+
for (const line of readFileSync10(path, "utf8").split("\n")) {
|
|
5435
5503
|
if (!line.trim()) continue;
|
|
5436
5504
|
let obj;
|
|
5437
5505
|
try {
|
|
@@ -5688,7 +5756,7 @@ Examples:
|
|
|
5688
5756
|
function resolveVersion() {
|
|
5689
5757
|
try {
|
|
5690
5758
|
const pkg = JSON.parse(
|
|
5691
|
-
|
|
5759
|
+
readFileSync11(new URL("../package.json", import.meta.url), "utf8")
|
|
5692
5760
|
);
|
|
5693
5761
|
return pkg.version ?? "0.0.0";
|
|
5694
5762
|
} catch {
|
package/package.json
CHANGED