@sechroom/cli 2026.6.238-rc.10da7380 → 2026.6.239-rc.6418da97
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 +442 -229
- 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 readFileSync10 } from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/auth.ts
|
|
@@ -1590,13 +1590,13 @@ Examples:
|
|
|
1590
1590
|
}
|
|
1591
1591
|
|
|
1592
1592
|
// src/commands/checkpoint.ts
|
|
1593
|
-
import { mkdirSync as
|
|
1594
|
-
import { dirname as
|
|
1593
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
1594
|
+
import { dirname as dirname6, join as join9 } from "path";
|
|
1595
1595
|
|
|
1596
1596
|
// src/commands/hook.ts
|
|
1597
1597
|
import { createHash as createHash2 } from "crypto";
|
|
1598
|
-
import { existsSync as
|
|
1599
|
-
import {
|
|
1598
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
1599
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
1600
1600
|
|
|
1601
1601
|
// src/sem.ts
|
|
1602
1602
|
import { dirname as dirname2, join as join5 } from "path";
|
|
@@ -1686,6 +1686,9 @@ function writeSem(values, path = localSemPath()) {
|
|
|
1686
1686
|
ensureContinuityScaffold(path);
|
|
1687
1687
|
return path;
|
|
1688
1688
|
}
|
|
1689
|
+
function ensureStateDirIgnored(cwd = process.cwd()) {
|
|
1690
|
+
ensureSemIgnored(localSemPath(cwd));
|
|
1691
|
+
}
|
|
1689
1692
|
var CONTINUITY_FILE_NAME = "continuity.json";
|
|
1690
1693
|
var CONTINUITY_SCAFFOLD = JSON.stringify(
|
|
1691
1694
|
{
|
|
@@ -1757,6 +1760,10 @@ function ensureSemIgnored(semPath) {
|
|
|
1757
1760
|
}
|
|
1758
1761
|
}
|
|
1759
1762
|
|
|
1763
|
+
// src/commands/hook-install.ts
|
|
1764
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
1765
|
+
import { delimiter, dirname as dirname4, join as join7 } from "path";
|
|
1766
|
+
|
|
1760
1767
|
// src/setup/clients.ts
|
|
1761
1768
|
import { existsSync as existsSync5 } from "fs";
|
|
1762
1769
|
import { homedir as homedir3 } from "os";
|
|
@@ -1827,6 +1834,144 @@ function detectInstalledClients(cwd) {
|
|
|
1827
1834
|
return detected;
|
|
1828
1835
|
}
|
|
1829
1836
|
|
|
1837
|
+
// src/commands/hook-install.ts
|
|
1838
|
+
var CLAUDE_HOOK_COMMANDS = {
|
|
1839
|
+
SessionStart: "sechroom hook session-start",
|
|
1840
|
+
PreCompact: "sechroom hook pre-compact",
|
|
1841
|
+
SessionEnd: "sechroom hook session-end",
|
|
1842
|
+
// WLP telemetry tap (D-WLP-10) — per-turn executor self-report. No-op (exit 0) unless this
|
|
1843
|
+
// checkout is bound to a decomposition+task via `sechroom telemetry bind`, so it's safe to wire
|
|
1844
|
+
// for every Claude install. Claude-only (it parses a Claude Code transcript).
|
|
1845
|
+
Stop: "sechroom telemetry hook"
|
|
1846
|
+
};
|
|
1847
|
+
var CODEX_HOOK_COMMANDS = {
|
|
1848
|
+
SessionStart: "sechroom hook session-start",
|
|
1849
|
+
Stop: "sechroom hook session-end --debounce-minutes 10"
|
|
1850
|
+
};
|
|
1851
|
+
function hasHookCommand(config2, event, command) {
|
|
1852
|
+
const groups = config2.hooks?.[event] ?? [];
|
|
1853
|
+
return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
|
|
1854
|
+
}
|
|
1855
|
+
function mergeHooks(config2, commands) {
|
|
1856
|
+
config2.hooks ??= {};
|
|
1857
|
+
let added = 0;
|
|
1858
|
+
for (const [event, command] of Object.entries(commands)) {
|
|
1859
|
+
if (hasHookCommand(config2, event, command)) continue;
|
|
1860
|
+
const groups = config2.hooks[event] ??= [];
|
|
1861
|
+
groups.push({ hooks: [{ type: "command", command }] });
|
|
1862
|
+
added += 1;
|
|
1863
|
+
}
|
|
1864
|
+
return added;
|
|
1865
|
+
}
|
|
1866
|
+
function readJsonConfig2(path) {
|
|
1867
|
+
if (!existsSync6(path)) return {};
|
|
1868
|
+
const raw = readFileSync4(path, "utf8");
|
|
1869
|
+
if (!raw.trim()) return {};
|
|
1870
|
+
return JSON.parse(raw);
|
|
1871
|
+
}
|
|
1872
|
+
function installHooksJson(path, commands, dryRun) {
|
|
1873
|
+
const existed = existsSync6(path) && readFileSync4(path, "utf8").trim().length > 0;
|
|
1874
|
+
const config2 = readJsonConfig2(path);
|
|
1875
|
+
const added = mergeHooks(config2, commands);
|
|
1876
|
+
if (added === 0 && existed) return { path, status: "current" };
|
|
1877
|
+
if (!dryRun) {
|
|
1878
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
1879
|
+
writeFileSync5(path, JSON.stringify(config2, null, 2) + "\n");
|
|
1880
|
+
}
|
|
1881
|
+
return { path, status: existed ? "merged" : "created" };
|
|
1882
|
+
}
|
|
1883
|
+
function installClaudeCommands(claudeDir, commands, dryRun) {
|
|
1884
|
+
return installHooksJson(join7(claudeDir, "settings.json"), commands, dryRun);
|
|
1885
|
+
}
|
|
1886
|
+
function ensureCodexFeaturesHooks(content) {
|
|
1887
|
+
const lines = content.split("\n");
|
|
1888
|
+
const headerIdx = lines.findIndex((l) => l.trim() === "[features]");
|
|
1889
|
+
if (headerIdx === -1) {
|
|
1890
|
+
const base = content.length === 0 || content.endsWith("\n") ? content : content + "\n";
|
|
1891
|
+
return { next: base + "\n[features]\nhooks = true\n", changed: true };
|
|
1892
|
+
}
|
|
1893
|
+
for (let i = headerIdx + 1; i < lines.length; i += 1) {
|
|
1894
|
+
const trimmed = lines[i].trim();
|
|
1895
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) break;
|
|
1896
|
+
const m = lines[i].match(/^(\s*)hooks(\s*)=(\s*)(.*)$/);
|
|
1897
|
+
if (!m) continue;
|
|
1898
|
+
const value = m[4].replace(/\s*#.*$/, "").trim();
|
|
1899
|
+
if (value === "true") return { next: content, changed: false };
|
|
1900
|
+
lines[i] = `${m[1]}hooks${m[2]}=${m[3]}true`;
|
|
1901
|
+
return { next: lines.join("\n"), changed: true };
|
|
1902
|
+
}
|
|
1903
|
+
lines.splice(headerIdx + 1, 0, "hooks = true");
|
|
1904
|
+
return { next: lines.join("\n"), changed: true };
|
|
1905
|
+
}
|
|
1906
|
+
function installCodexFeatureFlag(path, dryRun) {
|
|
1907
|
+
const existed = existsSync6(path);
|
|
1908
|
+
const content = existed ? readFileSync4(path, "utf8") : "";
|
|
1909
|
+
const { next, changed } = ensureCodexFeaturesHooks(content);
|
|
1910
|
+
if (!changed) return { path, status: "current" };
|
|
1911
|
+
if (!dryRun) {
|
|
1912
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
1913
|
+
writeFileSync5(path, next);
|
|
1914
|
+
}
|
|
1915
|
+
return { path, status: existed ? "merged" : "created" };
|
|
1916
|
+
}
|
|
1917
|
+
function resolveSurfaces(surface, cwd) {
|
|
1918
|
+
if (surface === "claude") return ["claude"];
|
|
1919
|
+
if (surface === "codex") return ["codex"];
|
|
1920
|
+
if (surface === "both") return ["claude", "codex"];
|
|
1921
|
+
if (surface) throw new Error(`--surface must be one of claude | codex | both (got '${surface}')`);
|
|
1922
|
+
const surfaces = detectHookSurfaces(cwd);
|
|
1923
|
+
return surfaces.length > 0 ? surfaces : ["claude", "codex"];
|
|
1924
|
+
}
|
|
1925
|
+
function describe(result, dryRun) {
|
|
1926
|
+
if (result.status === "current") return ` \u2713 ${result.path} (already configured)`;
|
|
1927
|
+
const verb = dryRun ? "would" : result.status === "created" ? "created" : "updated";
|
|
1928
|
+
return ` \u2713 ${result.path} (${dryRun ? `${verb} ${result.status === "created" ? "create" : "update"}` : verb})`;
|
|
1929
|
+
}
|
|
1930
|
+
var HOOK_SURFACE_LABEL = {
|
|
1931
|
+
claude: "Claude Code",
|
|
1932
|
+
codex: "Codex"
|
|
1933
|
+
};
|
|
1934
|
+
function installHookSurfaces(surfaces, opts) {
|
|
1935
|
+
const out = [];
|
|
1936
|
+
for (const surface of surfaces) {
|
|
1937
|
+
if (surface === "claude") {
|
|
1938
|
+
const path = join7(opts.claudeDir, "settings.json");
|
|
1939
|
+
out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
|
|
1940
|
+
} else {
|
|
1941
|
+
const hooksJson = installHooksJson(join7(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
|
|
1942
|
+
const featureFlag = installCodexFeatureFlag(join7(opts.codexHome, "config.toml"), opts.dryRun);
|
|
1943
|
+
out.push({ surface, results: [hooksJson, featureFlag] });
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
return out;
|
|
1947
|
+
}
|
|
1948
|
+
function detectHookSurfaces(cwd) {
|
|
1949
|
+
const detected = detectInstalledClients(cwd);
|
|
1950
|
+
const surfaces = [];
|
|
1951
|
+
if (detected.includes("claude-code")) surfaces.push("claude");
|
|
1952
|
+
if (detected.includes("codex")) surfaces.push("codex");
|
|
1953
|
+
return surfaces;
|
|
1954
|
+
}
|
|
1955
|
+
function isSechroomOnPath() {
|
|
1956
|
+
const pathEnv = process.env.PATH ?? "";
|
|
1957
|
+
if (!pathEnv) return false;
|
|
1958
|
+
const names = process.platform === "win32" ? ["sechroom.cmd", "sechroom.exe", "sechroom.bat", "sechroom"] : ["sechroom"];
|
|
1959
|
+
for (const dir of pathEnv.split(delimiter)) {
|
|
1960
|
+
if (!dir) continue;
|
|
1961
|
+
for (const name of names) {
|
|
1962
|
+
if (existsSync6(join7(dir, name))) return true;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
return false;
|
|
1966
|
+
}
|
|
1967
|
+
function warnIfSechroomNotOnPath(write = (s) => void process.stderr.write(s)) {
|
|
1968
|
+
if (isSechroomOnPath()) return false;
|
|
1969
|
+
write(
|
|
1970
|
+
"\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"
|
|
1971
|
+
);
|
|
1972
|
+
return true;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1830
1975
|
// src/commands/hook.ts
|
|
1831
1976
|
async function readStdin() {
|
|
1832
1977
|
if (process.stdin.isTTY) return "";
|
|
@@ -1851,13 +1996,13 @@ function resolveLane(flagLane, cwd) {
|
|
|
1851
1996
|
if (!base) return void 0;
|
|
1852
1997
|
return applyWorktreeLaneSuffix(base, start);
|
|
1853
1998
|
}
|
|
1854
|
-
var INTENT_FILE =
|
|
1999
|
+
var INTENT_FILE = join8(".sechroom", "continuity.json");
|
|
1855
2000
|
function resolveIntentPath(start) {
|
|
1856
2001
|
let dir = start;
|
|
1857
2002
|
for (; ; ) {
|
|
1858
|
-
const candidate =
|
|
1859
|
-
if (
|
|
1860
|
-
const parent =
|
|
2003
|
+
const candidate = join8(dir, INTENT_FILE);
|
|
2004
|
+
if (existsSync7(candidate)) return candidate;
|
|
2005
|
+
const parent = dirname5(dir);
|
|
1861
2006
|
if (parent === dir) return void 0;
|
|
1862
2007
|
dir = parent;
|
|
1863
2008
|
}
|
|
@@ -1866,7 +2011,7 @@ function readIntent(start) {
|
|
|
1866
2011
|
const path = resolveIntentPath(start);
|
|
1867
2012
|
if (!path) return void 0;
|
|
1868
2013
|
try {
|
|
1869
|
-
return JSON.parse(
|
|
2014
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
1870
2015
|
} catch {
|
|
1871
2016
|
return void 0;
|
|
1872
2017
|
}
|
|
@@ -1908,14 +2053,14 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
|
|
|
1908
2053
|
}
|
|
1909
2054
|
function ledgerPath(start) {
|
|
1910
2055
|
const intent = resolveIntentPath(start);
|
|
1911
|
-
const dir = intent ?
|
|
1912
|
-
return
|
|
2056
|
+
const dir = intent ? dirname5(intent) : join8(start, ".sechroom");
|
|
2057
|
+
return join8(dir, ".checkpoint-state.json");
|
|
1913
2058
|
}
|
|
1914
2059
|
function readLedger(start) {
|
|
1915
2060
|
try {
|
|
1916
2061
|
const p = ledgerPath(start);
|
|
1917
|
-
if (!
|
|
1918
|
-
return JSON.parse(
|
|
2062
|
+
if (!existsSync7(p)) return {};
|
|
2063
|
+
return JSON.parse(readFileSync5(p, "utf8"));
|
|
1919
2064
|
} catch {
|
|
1920
2065
|
return {};
|
|
1921
2066
|
}
|
|
@@ -1962,13 +2107,13 @@ function recordPush(start, intent) {
|
|
|
1962
2107
|
} catch {
|
|
1963
2108
|
mtimeMs = void 0;
|
|
1964
2109
|
}
|
|
1965
|
-
|
|
2110
|
+
mkdirSync6(dirname5(p), { recursive: true });
|
|
1966
2111
|
const ledger = {
|
|
1967
2112
|
lastEpochMs: Date.now(),
|
|
1968
2113
|
lastMtimeMs: mtimeMs,
|
|
1969
2114
|
lastHash: intentHash(intent)
|
|
1970
2115
|
};
|
|
1971
|
-
|
|
2116
|
+
writeFileSync6(p, JSON.stringify(ledger) + "\n");
|
|
1972
2117
|
} catch {
|
|
1973
2118
|
}
|
|
1974
2119
|
}
|
|
@@ -2008,135 +2153,6 @@ function emitSessionStart(additionalContext) {
|
|
|
2008
2153
|
}) + "\n"
|
|
2009
2154
|
);
|
|
2010
2155
|
}
|
|
2011
|
-
var CLAUDE_HOOK_COMMANDS = {
|
|
2012
|
-
SessionStart: "sechroom hook session-start",
|
|
2013
|
-
PreCompact: "sechroom hook pre-compact",
|
|
2014
|
-
SessionEnd: "sechroom hook session-end"
|
|
2015
|
-
};
|
|
2016
|
-
var CODEX_HOOK_COMMANDS = {
|
|
2017
|
-
SessionStart: "sechroom hook session-start",
|
|
2018
|
-
Stop: "sechroom hook session-end --debounce-minutes 10"
|
|
2019
|
-
};
|
|
2020
|
-
function hasHookCommand(config2, event, command) {
|
|
2021
|
-
const groups = config2.hooks?.[event] ?? [];
|
|
2022
|
-
return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
|
|
2023
|
-
}
|
|
2024
|
-
function mergeHooks(config2, commands) {
|
|
2025
|
-
config2.hooks ??= {};
|
|
2026
|
-
let added = 0;
|
|
2027
|
-
for (const [event, command] of Object.entries(commands)) {
|
|
2028
|
-
if (hasHookCommand(config2, event, command)) continue;
|
|
2029
|
-
const groups = config2.hooks[event] ??= [];
|
|
2030
|
-
groups.push({ hooks: [{ type: "command", command }] });
|
|
2031
|
-
added += 1;
|
|
2032
|
-
}
|
|
2033
|
-
return added;
|
|
2034
|
-
}
|
|
2035
|
-
function readJsonConfig2(path) {
|
|
2036
|
-
if (!existsSync6(path)) return {};
|
|
2037
|
-
const raw = readFileSync4(path, "utf8");
|
|
2038
|
-
if (!raw.trim()) return {};
|
|
2039
|
-
return JSON.parse(raw);
|
|
2040
|
-
}
|
|
2041
|
-
function installHooksJson(path, commands, dryRun) {
|
|
2042
|
-
const existed = existsSync6(path) && readFileSync4(path, "utf8").trim().length > 0;
|
|
2043
|
-
const config2 = readJsonConfig2(path);
|
|
2044
|
-
const added = mergeHooks(config2, commands);
|
|
2045
|
-
if (added === 0 && existed) return { path, status: "current" };
|
|
2046
|
-
if (!dryRun) {
|
|
2047
|
-
mkdirSync5(dirname4(path), { recursive: true });
|
|
2048
|
-
writeFileSync5(path, JSON.stringify(config2, null, 2) + "\n");
|
|
2049
|
-
}
|
|
2050
|
-
return { path, status: existed ? "merged" : "created" };
|
|
2051
|
-
}
|
|
2052
|
-
function ensureCodexFeaturesHooks(content) {
|
|
2053
|
-
const lines = content.split("\n");
|
|
2054
|
-
const headerIdx = lines.findIndex((l) => l.trim() === "[features]");
|
|
2055
|
-
if (headerIdx === -1) {
|
|
2056
|
-
const base = content.length === 0 || content.endsWith("\n") ? content : content + "\n";
|
|
2057
|
-
return { next: base + "\n[features]\nhooks = true\n", changed: true };
|
|
2058
|
-
}
|
|
2059
|
-
for (let i = headerIdx + 1; i < lines.length; i += 1) {
|
|
2060
|
-
const trimmed = lines[i].trim();
|
|
2061
|
-
if (trimmed.startsWith("[") && trimmed.endsWith("]")) break;
|
|
2062
|
-
const m = lines[i].match(/^(\s*)hooks(\s*)=(\s*)(.*)$/);
|
|
2063
|
-
if (!m) continue;
|
|
2064
|
-
const value = m[4].replace(/\s*#.*$/, "").trim();
|
|
2065
|
-
if (value === "true") return { next: content, changed: false };
|
|
2066
|
-
lines[i] = `${m[1]}hooks${m[2]}=${m[3]}true`;
|
|
2067
|
-
return { next: lines.join("\n"), changed: true };
|
|
2068
|
-
}
|
|
2069
|
-
lines.splice(headerIdx + 1, 0, "hooks = true");
|
|
2070
|
-
return { next: lines.join("\n"), changed: true };
|
|
2071
|
-
}
|
|
2072
|
-
function installCodexFeatureFlag(path, dryRun) {
|
|
2073
|
-
const existed = existsSync6(path);
|
|
2074
|
-
const content = existed ? readFileSync4(path, "utf8") : "";
|
|
2075
|
-
const { next, changed } = ensureCodexFeaturesHooks(content);
|
|
2076
|
-
if (!changed) return { path, status: "current" };
|
|
2077
|
-
if (!dryRun) {
|
|
2078
|
-
mkdirSync5(dirname4(path), { recursive: true });
|
|
2079
|
-
writeFileSync5(path, next);
|
|
2080
|
-
}
|
|
2081
|
-
return { path, status: existed ? "merged" : "created" };
|
|
2082
|
-
}
|
|
2083
|
-
function resolveSurfaces(surface, cwd) {
|
|
2084
|
-
if (surface === "claude") return ["claude"];
|
|
2085
|
-
if (surface === "codex") return ["codex"];
|
|
2086
|
-
if (surface === "both") return ["claude", "codex"];
|
|
2087
|
-
if (surface) throw new Error(`--surface must be one of claude | codex | both (got '${surface}')`);
|
|
2088
|
-
const surfaces = detectHookSurfaces(cwd);
|
|
2089
|
-
return surfaces.length > 0 ? surfaces : ["claude", "codex"];
|
|
2090
|
-
}
|
|
2091
|
-
function describe(result, dryRun) {
|
|
2092
|
-
if (result.status === "current") return ` \u2713 ${result.path} (already configured)`;
|
|
2093
|
-
const verb = dryRun ? "would" : result.status === "created" ? "created" : "updated";
|
|
2094
|
-
return ` \u2713 ${result.path} (${dryRun ? `${verb} ${result.status === "created" ? "create" : "update"}` : verb})`;
|
|
2095
|
-
}
|
|
2096
|
-
var HOOK_SURFACE_LABEL = {
|
|
2097
|
-
claude: "Claude Code",
|
|
2098
|
-
codex: "Codex"
|
|
2099
|
-
};
|
|
2100
|
-
function installHookSurfaces(surfaces, opts) {
|
|
2101
|
-
const out = [];
|
|
2102
|
-
for (const surface of surfaces) {
|
|
2103
|
-
if (surface === "claude") {
|
|
2104
|
-
const path = join7(opts.claudeDir, "settings.json");
|
|
2105
|
-
out.push({ surface, results: [installHooksJson(path, CLAUDE_HOOK_COMMANDS, opts.dryRun)] });
|
|
2106
|
-
} else {
|
|
2107
|
-
const hooksJson = installHooksJson(join7(opts.codexHome, "hooks.json"), CODEX_HOOK_COMMANDS, opts.dryRun);
|
|
2108
|
-
const featureFlag = installCodexFeatureFlag(join7(opts.codexHome, "config.toml"), opts.dryRun);
|
|
2109
|
-
out.push({ surface, results: [hooksJson, featureFlag] });
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
return out;
|
|
2113
|
-
}
|
|
2114
|
-
function detectHookSurfaces(cwd) {
|
|
2115
|
-
const detected = detectInstalledClients(cwd);
|
|
2116
|
-
const surfaces = [];
|
|
2117
|
-
if (detected.includes("claude-code")) surfaces.push("claude");
|
|
2118
|
-
if (detected.includes("codex")) surfaces.push("codex");
|
|
2119
|
-
return surfaces;
|
|
2120
|
-
}
|
|
2121
|
-
function isSechroomOnPath() {
|
|
2122
|
-
const pathEnv = process.env.PATH ?? "";
|
|
2123
|
-
if (!pathEnv) return false;
|
|
2124
|
-
const names = process.platform === "win32" ? ["sechroom.cmd", "sechroom.exe", "sechroom.bat", "sechroom"] : ["sechroom"];
|
|
2125
|
-
for (const dir of pathEnv.split(delimiter)) {
|
|
2126
|
-
if (!dir) continue;
|
|
2127
|
-
for (const name of names) {
|
|
2128
|
-
if (existsSync6(join7(dir, name))) return true;
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
return false;
|
|
2132
|
-
}
|
|
2133
|
-
function warnIfSechroomNotOnPath(write = (s) => void process.stderr.write(s)) {
|
|
2134
|
-
if (isSechroomOnPath()) return false;
|
|
2135
|
-
write(
|
|
2136
|
-
"\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"
|
|
2137
|
-
);
|
|
2138
|
-
return true;
|
|
2139
|
-
}
|
|
2140
2156
|
function registerHook(program2) {
|
|
2141
2157
|
const hook = program2.command("hook").description("Agent-lifecycle hook adapter (Claude Code / Codex) \u2014 bridges hooks to continuity");
|
|
2142
2158
|
hook.addHelpText(
|
|
@@ -2344,10 +2360,10 @@ Examples:
|
|
|
2344
2360
|
const client = await makeClient(cfg);
|
|
2345
2361
|
return client.POST("/continuity/snapshots", { body });
|
|
2346
2362
|
});
|
|
2347
|
-
const path = resolveIntentPath(cwd) ??
|
|
2363
|
+
const path = resolveIntentPath(cwd) ?? join9(cwd, INTENT_FILE);
|
|
2348
2364
|
const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
|
|
2349
|
-
|
|
2350
|
-
|
|
2365
|
+
mkdirSync7(dirname6(path), { recursive: true });
|
|
2366
|
+
writeFileSync7(path, JSON.stringify(fileBody, null, 2) + "\n");
|
|
2351
2367
|
recordPush(cwd, merged);
|
|
2352
2368
|
if (json) {
|
|
2353
2369
|
emit({ snapshotId: data.snapshotId, lane, scope, file: path }, true);
|
|
@@ -3031,8 +3047,8 @@ Examples:
|
|
|
3031
3047
|
|
|
3032
3048
|
// src/setup/apply.ts
|
|
3033
3049
|
import { createHash as createHash3 } from "crypto";
|
|
3034
|
-
import { mkdirSync as
|
|
3035
|
-
import { dirname as
|
|
3050
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync8, existsSync as existsSync8 } from "fs";
|
|
3051
|
+
import { dirname as dirname7 } from "path";
|
|
3036
3052
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
3037
3053
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
3038
3054
|
function normalizeBody(s) {
|
|
@@ -3085,22 +3101,22 @@ function parseManagedBlock(content, block) {
|
|
|
3085
3101
|
return null;
|
|
3086
3102
|
}
|
|
3087
3103
|
function ensureDir2(path) {
|
|
3088
|
-
|
|
3104
|
+
mkdirSync8(dirname7(path), { recursive: true });
|
|
3089
3105
|
}
|
|
3090
3106
|
function readOr(path, fallback) {
|
|
3091
3107
|
try {
|
|
3092
|
-
return
|
|
3108
|
+
return readFileSync6(path, "utf8");
|
|
3093
3109
|
} catch {
|
|
3094
3110
|
return fallback;
|
|
3095
3111
|
}
|
|
3096
3112
|
}
|
|
3097
3113
|
function mergeMcpJson(path, snippet, dryRun) {
|
|
3098
3114
|
const incoming = JSON.parse(snippet);
|
|
3099
|
-
const existed =
|
|
3115
|
+
const existed = existsSync8(path);
|
|
3100
3116
|
let current = {};
|
|
3101
3117
|
if (existed) {
|
|
3102
3118
|
try {
|
|
3103
|
-
current = JSON.parse(
|
|
3119
|
+
current = JSON.parse(readFileSync6(path, "utf8"));
|
|
3104
3120
|
} catch {
|
|
3105
3121
|
return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
|
|
3106
3122
|
}
|
|
@@ -3108,26 +3124,26 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
3108
3124
|
current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
|
|
3109
3125
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3110
3126
|
ensureDir2(path);
|
|
3111
|
-
|
|
3127
|
+
writeFileSync8(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
3112
3128
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3113
3129
|
}
|
|
3114
3130
|
function mergeCodexToml(path, snippet, dryRun) {
|
|
3115
|
-
const existed =
|
|
3131
|
+
const existed = existsSync8(path);
|
|
3116
3132
|
let body = readOr(path, "");
|
|
3117
3133
|
body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
|
|
3118
3134
|
const trimmed = body.trim();
|
|
3119
3135
|
const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
|
|
3120
3136
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3121
3137
|
ensureDir2(path);
|
|
3122
|
-
|
|
3138
|
+
writeFileSync8(path, next, { mode: 384 });
|
|
3123
3139
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3124
3140
|
}
|
|
3125
3141
|
function writeInstructionBlock(path, write, dryRun) {
|
|
3126
|
-
const existed =
|
|
3142
|
+
const existed = existsSync8(path);
|
|
3127
3143
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
3128
3144
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
3129
3145
|
ensureDir2(path);
|
|
3130
|
-
|
|
3146
|
+
writeFileSync8(path, next);
|
|
3131
3147
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
3132
3148
|
}
|
|
3133
3149
|
function computeBlockFile(current, write) {
|
|
@@ -3168,7 +3184,7 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
3168
3184
|
const next = computeBlockFile(current, write);
|
|
3169
3185
|
if (!dryRun) {
|
|
3170
3186
|
ensureDir2(proposedPath);
|
|
3171
|
-
|
|
3187
|
+
writeFileSync8(proposedPath, next);
|
|
3172
3188
|
}
|
|
3173
3189
|
return {
|
|
3174
3190
|
kind: "instruction",
|
|
@@ -3292,8 +3308,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
3292
3308
|
}
|
|
3293
3309
|
|
|
3294
3310
|
// src/setup/skills-offer.ts
|
|
3295
|
-
import { mkdirSync as
|
|
3296
|
-
import { join as
|
|
3311
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
3312
|
+
import { join as join10 } from "path";
|
|
3297
3313
|
|
|
3298
3314
|
// src/setup/lane-pin.ts
|
|
3299
3315
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -3409,8 +3425,8 @@ Found ${summary} available to you for ${surface}.
|
|
|
3409
3425
|
if (skills.length > 0) {
|
|
3410
3426
|
const written = [];
|
|
3411
3427
|
for (const s of skills) {
|
|
3412
|
-
|
|
3413
|
-
|
|
3428
|
+
mkdirSync9(join10(sDir, s.name), { recursive: true });
|
|
3429
|
+
writeFileSync9(join10(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
3414
3430
|
written.push(s.name);
|
|
3415
3431
|
}
|
|
3416
3432
|
recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -3418,11 +3434,11 @@ Found ${summary} available to you for ${surface}.
|
|
|
3418
3434
|
`);
|
|
3419
3435
|
}
|
|
3420
3436
|
if (agents.length > 0) {
|
|
3421
|
-
|
|
3437
|
+
mkdirSync9(aDir, { recursive: true });
|
|
3422
3438
|
const written = [];
|
|
3423
3439
|
for (const a of agents) {
|
|
3424
3440
|
const file = `${a.name}.md`;
|
|
3425
|
-
|
|
3441
|
+
writeFileSync9(join10(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
3426
3442
|
written.push(file);
|
|
3427
3443
|
}
|
|
3428
3444
|
recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -3805,13 +3821,13 @@ Wired to namespace '${slug}'. Restart your AI client (or reload MCP) to pick it
|
|
|
3805
3821
|
}
|
|
3806
3822
|
|
|
3807
3823
|
// src/commands/onboard.ts
|
|
3808
|
-
import { existsSync as
|
|
3809
|
-
import { basename as basename2, join as
|
|
3824
|
+
import { existsSync as existsSync10 } from "fs";
|
|
3825
|
+
import { basename as basename2, join as join12 } from "path";
|
|
3810
3826
|
|
|
3811
3827
|
// src/commands/fanout.ts
|
|
3812
3828
|
import { spawnSync } from "child_process";
|
|
3813
|
-
import { existsSync as
|
|
3814
|
-
import { isAbsolute, join as
|
|
3829
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
3830
|
+
import { isAbsolute, join as join11, resolve } from "path";
|
|
3815
3831
|
var ICON = {
|
|
3816
3832
|
refresh: "\u21BB",
|
|
3817
3833
|
bind: "+",
|
|
@@ -3831,21 +3847,21 @@ function discoverChildren(root) {
|
|
|
3831
3847
|
const out = [];
|
|
3832
3848
|
for (const name of names.sort()) {
|
|
3833
3849
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
3834
|
-
const dir =
|
|
3850
|
+
const dir = join11(root, name);
|
|
3835
3851
|
try {
|
|
3836
3852
|
if (!statSync3(dir).isDirectory()) continue;
|
|
3837
3853
|
} catch {
|
|
3838
3854
|
continue;
|
|
3839
3855
|
}
|
|
3840
|
-
if (
|
|
3856
|
+
if (existsSync9(join11(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
3841
3857
|
}
|
|
3842
3858
|
return out;
|
|
3843
3859
|
}
|
|
3844
3860
|
function readManifest(path) {
|
|
3845
|
-
if (!
|
|
3861
|
+
if (!existsSync9(path)) return null;
|
|
3846
3862
|
let parsed;
|
|
3847
3863
|
try {
|
|
3848
|
-
parsed = JSON.parse(
|
|
3864
|
+
parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
3849
3865
|
} catch (err2) {
|
|
3850
3866
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
3851
3867
|
}
|
|
@@ -4222,10 +4238,10 @@ async function chooseScope(scopeFlag, yes) {
|
|
|
4222
4238
|
}
|
|
4223
4239
|
async function planRecurseChild(entry, root, client, opts) {
|
|
4224
4240
|
const dir = resolveChildDir(entry.path, root);
|
|
4225
|
-
if (!
|
|
4241
|
+
if (!existsSync10(dir)) {
|
|
4226
4242
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
4227
4243
|
}
|
|
4228
|
-
if (
|
|
4244
|
+
if (existsSync10(join12(dir, ".sechroom.json"))) {
|
|
4229
4245
|
return {
|
|
4230
4246
|
label: entry.path,
|
|
4231
4247
|
dir,
|
|
@@ -4298,7 +4314,7 @@ This fan-out will pin the same lane in every repo:
|
|
|
4298
4314
|
async function runRecurse(cfg, g, opts) {
|
|
4299
4315
|
const { yes, dryRun, json } = opts;
|
|
4300
4316
|
const root = process.cwd();
|
|
4301
|
-
const manifestPath =
|
|
4317
|
+
const manifestPath = join12(root, ".sechroom", "repos.json");
|
|
4302
4318
|
const fromManifest = readManifest(manifestPath);
|
|
4303
4319
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
4304
4320
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
@@ -4810,23 +4826,23 @@ Examples:
|
|
|
4810
4826
|
|
|
4811
4827
|
// src/commands/reset.ts
|
|
4812
4828
|
import { homedir as homedir4 } from "os";
|
|
4813
|
-
import { join as
|
|
4814
|
-
import { existsSync as
|
|
4829
|
+
import { join as join13 } from "path";
|
|
4830
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8, rmSync as rmSync3 } from "fs";
|
|
4815
4831
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
4816
|
-
var localSkillsDir = () =>
|
|
4817
|
-
var globalSkillsDir = () =>
|
|
4818
|
-
var localAgentsDir = () =>
|
|
4819
|
-
var globalAgentsDir = () =>
|
|
4832
|
+
var localSkillsDir = () => join13(process.cwd(), ".claude", "skills");
|
|
4833
|
+
var globalSkillsDir = () => join13(homedir4(), ".claude", "skills");
|
|
4834
|
+
var localAgentsDir = () => join13(process.cwd(), ".claude", "agents");
|
|
4835
|
+
var globalAgentsDir = () => join13(homedir4(), ".claude", "agents");
|
|
4820
4836
|
function removeMaterialisedSkills(dir) {
|
|
4821
4837
|
const removed = [];
|
|
4822
|
-
const lockPath =
|
|
4823
|
-
if (!
|
|
4838
|
+
const lockPath = join13(dir, SKILLS_LOCK2);
|
|
4839
|
+
if (!existsSync11(lockPath)) return removed;
|
|
4824
4840
|
try {
|
|
4825
|
-
const lock = JSON.parse(
|
|
4841
|
+
const lock = JSON.parse(readFileSync8(lockPath, "utf8"));
|
|
4826
4842
|
for (const entry of Object.values(lock)) {
|
|
4827
4843
|
for (const name of entry.skills ?? []) {
|
|
4828
|
-
const p =
|
|
4829
|
-
if (
|
|
4844
|
+
const p = join13(dir, name);
|
|
4845
|
+
if (existsSync11(p)) {
|
|
4830
4846
|
rmSync3(p, { recursive: true, force: true });
|
|
4831
4847
|
removed.push(p);
|
|
4832
4848
|
}
|
|
@@ -4871,18 +4887,18 @@ function registerReset(program2) {
|
|
|
4871
4887
|
}
|
|
4872
4888
|
}
|
|
4873
4889
|
const removed = [];
|
|
4874
|
-
const stateDir =
|
|
4875
|
-
if (
|
|
4890
|
+
const stateDir = join13(process.cwd(), ".sechroom");
|
|
4891
|
+
if (existsSync11(stateDir)) {
|
|
4876
4892
|
rmSync3(stateDir, { recursive: true, force: true });
|
|
4877
4893
|
removed.push(stateDir);
|
|
4878
4894
|
}
|
|
4879
|
-
const legacyCfg =
|
|
4880
|
-
if (
|
|
4895
|
+
const legacyCfg = join13(process.cwd(), ".sechroom.json");
|
|
4896
|
+
if (existsSync11(legacyCfg)) {
|
|
4881
4897
|
rmSync3(legacyCfg, { force: true });
|
|
4882
4898
|
removed.push(legacyCfg);
|
|
4883
4899
|
}
|
|
4884
|
-
const legacySem =
|
|
4885
|
-
if (
|
|
4900
|
+
const legacySem = join13(process.cwd(), ".sem");
|
|
4901
|
+
if (existsSync11(legacySem)) {
|
|
4886
4902
|
rmSync3(legacySem, { force: true });
|
|
4887
4903
|
removed.push(legacySem);
|
|
4888
4904
|
}
|
|
@@ -4908,8 +4924,8 @@ function registerReset(program2) {
|
|
|
4908
4924
|
}
|
|
4909
4925
|
|
|
4910
4926
|
// src/commands/skills.ts
|
|
4911
|
-
import { existsSync as
|
|
4912
|
-
import { join as
|
|
4927
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync10, statSync as statSync4, writeFileSync as writeFileSync10 } from "fs";
|
|
4928
|
+
import { join as join14 } from "path";
|
|
4913
4929
|
function filenameFromDisposition(header) {
|
|
4914
4930
|
if (!header) return void 0;
|
|
4915
4931
|
const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
|
|
@@ -4917,11 +4933,11 @@ function filenameFromDisposition(header) {
|
|
|
4917
4933
|
}
|
|
4918
4934
|
function resolveOutputPath(output, serverFilename) {
|
|
4919
4935
|
const filename = serverFilename || "skills.zip";
|
|
4920
|
-
if (!output) return
|
|
4921
|
-
const looksLikeDir = output.endsWith("/") ||
|
|
4936
|
+
if (!output) return join14(process.cwd(), filename);
|
|
4937
|
+
const looksLikeDir = output.endsWith("/") || existsSync12(output) && statSync4(output).isDirectory();
|
|
4922
4938
|
if (looksLikeDir) {
|
|
4923
|
-
|
|
4924
|
-
return
|
|
4939
|
+
mkdirSync10(output, { recursive: true });
|
|
4940
|
+
return join14(output, filename);
|
|
4925
4941
|
}
|
|
4926
4942
|
return output;
|
|
4927
4943
|
}
|
|
@@ -4952,7 +4968,7 @@ async function downloadZip(label, call, output) {
|
|
|
4952
4968
|
const buf = Buffer.from(res.data);
|
|
4953
4969
|
const filename = filenameFromDisposition(res.response.headers.get("content-disposition")) ?? "skills.zip";
|
|
4954
4970
|
const path = resolveOutputPath(output, filename);
|
|
4955
|
-
|
|
4971
|
+
writeFileSync10(path, buf);
|
|
4956
4972
|
return { path, bytes: buf.length, filename };
|
|
4957
4973
|
}
|
|
4958
4974
|
function registerSkills(program2) {
|
|
@@ -5120,12 +5136,12 @@ Examples:
|
|
|
5120
5136
|
}
|
|
5121
5137
|
|
|
5122
5138
|
// src/commands/sweep.ts
|
|
5123
|
-
import { existsSync as
|
|
5124
|
-
import { dirname as
|
|
5125
|
-
var DEFAULT_MANIFEST =
|
|
5139
|
+
import { existsSync as existsSync13 } from "fs";
|
|
5140
|
+
import { dirname as dirname8, join as join15, resolve as resolve2 } from "path";
|
|
5141
|
+
var DEFAULT_MANIFEST = join15(".sechroom", "repos.json");
|
|
5126
5142
|
function planEntry(entry, root) {
|
|
5127
5143
|
const dir = resolveChildDir(entry.path, root);
|
|
5128
|
-
if (!
|
|
5144
|
+
if (!existsSync13(dir)) {
|
|
5129
5145
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
5130
5146
|
}
|
|
5131
5147
|
if (committedBindingPath(dir)) {
|
|
@@ -5201,7 +5217,7 @@ Examples:
|
|
|
5201
5217
|
`);
|
|
5202
5218
|
return;
|
|
5203
5219
|
}
|
|
5204
|
-
const root =
|
|
5220
|
+
const root = dirname8(dirname8(manifestPath));
|
|
5205
5221
|
const plans = repos.map((entry) => planEntry(entry, root));
|
|
5206
5222
|
if (!json) {
|
|
5207
5223
|
process.stderr.write(
|
|
@@ -5219,6 +5235,14 @@ Examples:
|
|
|
5219
5235
|
}
|
|
5220
5236
|
|
|
5221
5237
|
// src/commands/telemetry.ts
|
|
5238
|
+
import {
|
|
5239
|
+
existsSync as existsSync14,
|
|
5240
|
+
mkdirSync as mkdirSync11,
|
|
5241
|
+
readFileSync as readFileSync9,
|
|
5242
|
+
rmSync as rmSync4,
|
|
5243
|
+
writeFileSync as writeFileSync11
|
|
5244
|
+
} from "fs";
|
|
5245
|
+
import { dirname as dirname9, join as join16 } from "path";
|
|
5222
5246
|
function registerTelemetry(program2) {
|
|
5223
5247
|
const telemetry = program2.command("telemetry").description(
|
|
5224
5248
|
"Emit WLP run telemetry (an executor leg's progress events) into a decomposition run"
|
|
@@ -5253,10 +5277,9 @@ function registerTelemetry(program2) {
|
|
|
5253
5277
|
).action(async (opts, cmd) => {
|
|
5254
5278
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
5255
5279
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
5256
|
-
const kind = normalizeKind(opts.kind);
|
|
5257
5280
|
const event = {
|
|
5258
5281
|
taskId: opts.task,
|
|
5259
|
-
kind,
|
|
5282
|
+
kind: normalizeKind(opts.kind),
|
|
5260
5283
|
tokensIn: opts.tokensIn ?? null,
|
|
5261
5284
|
tokensOut: opts.tokensOut ?? null,
|
|
5262
5285
|
contextUsed: opts.contextUsed ?? null,
|
|
@@ -5265,36 +5288,226 @@ function registerTelemetry(program2) {
|
|
|
5265
5288
|
approvalState: opts.approval ?? null,
|
|
5266
5289
|
verdict: opts.verdict ?? null
|
|
5267
5290
|
};
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
authorization: `Bearer ${token}`,
|
|
5275
|
-
tenant: cfg.tenant,
|
|
5276
|
-
"content-type": "application/json",
|
|
5277
|
-
"x-sechroom-surface": "cli"
|
|
5278
|
-
},
|
|
5279
|
-
body: JSON.stringify({ events: [event] })
|
|
5280
|
-
}
|
|
5281
|
-
);
|
|
5282
|
-
if (!resp.ok)
|
|
5283
|
-
fail(
|
|
5284
|
-
`Telemetry emit failed (HTTP ${resp.status}): ${await resp.text()}`
|
|
5285
|
-
);
|
|
5286
|
-
const body = await resp.json();
|
|
5291
|
+
let body;
|
|
5292
|
+
try {
|
|
5293
|
+
body = await postTelemetry(cfg, opts.decomposition, [event]);
|
|
5294
|
+
} catch (e) {
|
|
5295
|
+
return fail(`Telemetry emit failed: ${e.message}`);
|
|
5296
|
+
}
|
|
5287
5297
|
if (json) {
|
|
5288
5298
|
emit(body, true);
|
|
5289
5299
|
} else {
|
|
5290
5300
|
process.stderr.write(
|
|
5291
5301
|
style.green("telemetry emitted") + style.dim(
|
|
5292
|
-
` \u2014 ${kind} for task ${opts.task}; run now carries ${body.eventCount} event${body.eventCount === 1 ? "" : "s"}
|
|
5302
|
+
` \u2014 ${event.kind} for task ${opts.task}; run now carries ${body.eventCount} event${body.eventCount === 1 ? "" : "s"}
|
|
5293
5303
|
`
|
|
5294
5304
|
)
|
|
5295
5305
|
);
|
|
5296
5306
|
}
|
|
5297
5307
|
});
|
|
5308
|
+
telemetry.command("bind").description(
|
|
5309
|
+
"Bind this checkout to a decomposition+task so the Stop hook auto-emits per-turn telemetry"
|
|
5310
|
+
).requiredOption(
|
|
5311
|
+
"--decomposition <id>",
|
|
5312
|
+
"Decomposition id this session executes"
|
|
5313
|
+
).requiredOption("--task <id>", "Task id this session executes").action((opts, cmd) => {
|
|
5314
|
+
const json = Boolean(cmd.optsWithGlobals().json);
|
|
5315
|
+
const dir = join16(process.cwd(), ".sechroom");
|
|
5316
|
+
mkdirSync11(dir, { recursive: true });
|
|
5317
|
+
const path = join16(dir, BINDING_FILE);
|
|
5318
|
+
const binding = {
|
|
5319
|
+
decompositionId: opts.decomposition,
|
|
5320
|
+
taskId: opts.task
|
|
5321
|
+
};
|
|
5322
|
+
writeFileSync11(path, JSON.stringify(binding, null, 2) + "\n");
|
|
5323
|
+
ensureStateDirIgnored(process.cwd());
|
|
5324
|
+
if (json) {
|
|
5325
|
+
emit({ bound: true, ...binding, path }, true);
|
|
5326
|
+
} else {
|
|
5327
|
+
process.stdout.write(
|
|
5328
|
+
style.green("telemetry bound") + style.dim(
|
|
5329
|
+
` \u2014 decomposition ${binding.decompositionId}, task ${binding.taskId} (${path})
|
|
5330
|
+
`
|
|
5331
|
+
)
|
|
5332
|
+
);
|
|
5333
|
+
}
|
|
5334
|
+
});
|
|
5335
|
+
telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
|
|
5336
|
+
const json = Boolean(cmd.optsWithGlobals().json);
|
|
5337
|
+
const path = join16(process.cwd(), ".sechroom", BINDING_FILE);
|
|
5338
|
+
const existed = existsSync14(path);
|
|
5339
|
+
if (existed) rmSync4(path);
|
|
5340
|
+
if (json) emit({ unbound: existed, path }, true);
|
|
5341
|
+
else
|
|
5342
|
+
process.stdout.write(
|
|
5343
|
+
existed ? "telemetry binding cleared\n" : "no telemetry binding to clear\n"
|
|
5344
|
+
);
|
|
5345
|
+
});
|
|
5346
|
+
telemetry.command("hook").description(
|
|
5347
|
+
"Per-turn telemetry self-report for a Claude Code Stop hook (reads stdin; no-op unless bound). Fail-soft."
|
|
5348
|
+
).action(async (_opts, cmd) => {
|
|
5349
|
+
try {
|
|
5350
|
+
const raw = await readStdin2();
|
|
5351
|
+
const input = parseHookInput2(raw);
|
|
5352
|
+
const cwd = input.cwd ?? process.cwd();
|
|
5353
|
+
const binding = findBinding(cwd);
|
|
5354
|
+
if (!binding) return process.exit(0);
|
|
5355
|
+
const usage = input.transcript_path ? parseTranscript(input.transcript_path) : null;
|
|
5356
|
+
if (!usage) return process.exit(0);
|
|
5357
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
5358
|
+
await postTelemetry(cfg, binding.decompositionId, [
|
|
5359
|
+
{
|
|
5360
|
+
taskId: binding.taskId,
|
|
5361
|
+
kind: "Parsed",
|
|
5362
|
+
tokensIn: usage.tokensIn,
|
|
5363
|
+
tokensOut: usage.tokensOut,
|
|
5364
|
+
contextUsed: usage.contextUsed,
|
|
5365
|
+
contextWindow: usage.contextWindow,
|
|
5366
|
+
text: null,
|
|
5367
|
+
approvalState: null,
|
|
5368
|
+
verdict: null
|
|
5369
|
+
}
|
|
5370
|
+
]);
|
|
5371
|
+
return process.exit(0);
|
|
5372
|
+
} catch {
|
|
5373
|
+
return process.exit(0);
|
|
5374
|
+
}
|
|
5375
|
+
});
|
|
5376
|
+
telemetry.command("install").description(
|
|
5377
|
+
"Wire the per-turn telemetry Stop hook into Claude Code settings (also folded into `sechroom hook install`)"
|
|
5378
|
+
).option(
|
|
5379
|
+
"--scope <scope>",
|
|
5380
|
+
"global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global"
|
|
5381
|
+
).option("--local", "alias for --scope project").option("--dry-run", "Print what would change; write nothing").action((opts, cmd) => {
|
|
5382
|
+
const g = cmd.optsWithGlobals();
|
|
5383
|
+
const dryRun = Boolean(opts.dryRun);
|
|
5384
|
+
const cwd = process.cwd();
|
|
5385
|
+
let scope;
|
|
5386
|
+
try {
|
|
5387
|
+
scope = opts.local ? "project" : resolveScope(opts.scope);
|
|
5388
|
+
} catch (err2) {
|
|
5389
|
+
process.stderr.write(`${err2.message}
|
|
5390
|
+
`);
|
|
5391
|
+
return process.exit(2);
|
|
5392
|
+
}
|
|
5393
|
+
const targets = resolveClaudeTargets({
|
|
5394
|
+
override: g.claudeConfigDir,
|
|
5395
|
+
scope,
|
|
5396
|
+
cwd
|
|
5397
|
+
});
|
|
5398
|
+
const commands = { Stop: "sechroom telemetry hook" };
|
|
5399
|
+
try {
|
|
5400
|
+
const multi = targets.length > 1;
|
|
5401
|
+
const results = targets.map((t) => {
|
|
5402
|
+
const r = installClaudeCommands(t.dir, commands, dryRun);
|
|
5403
|
+
process.stdout.write(
|
|
5404
|
+
`${HOOK_SURFACE_LABEL.claude}${multi ? ` (${t.label})` : ""}:
|
|
5405
|
+
`
|
|
5406
|
+
);
|
|
5407
|
+
process.stdout.write(describe(r, dryRun) + "\n");
|
|
5408
|
+
return r;
|
|
5409
|
+
});
|
|
5410
|
+
if (dryRun) {
|
|
5411
|
+
process.stdout.write("\n(dry run \u2014 no files were written.)\n");
|
|
5412
|
+
} else if (results.every((r) => r.status === "current")) {
|
|
5413
|
+
process.stdout.write("\nAlready up to date \u2014 nothing to change.\n");
|
|
5414
|
+
} else {
|
|
5415
|
+
process.stdout.write(
|
|
5416
|
+
"\nRestart your agent for the hook to take effect, then bind a task with `sechroom telemetry bind`.\n"
|
|
5417
|
+
);
|
|
5418
|
+
}
|
|
5419
|
+
} catch (err2) {
|
|
5420
|
+
process.stderr.write(
|
|
5421
|
+
`telemetry install failed: ${err2.message}
|
|
5422
|
+
`
|
|
5423
|
+
);
|
|
5424
|
+
return process.exit(1);
|
|
5425
|
+
}
|
|
5426
|
+
warnIfSechroomNotOnPath();
|
|
5427
|
+
return process.exit(0);
|
|
5428
|
+
});
|
|
5429
|
+
}
|
|
5430
|
+
var BINDING_FILE = "telemetry.json";
|
|
5431
|
+
async function postTelemetry(cfg, decompositionId, events) {
|
|
5432
|
+
const token = await requireToken(cfg);
|
|
5433
|
+
const resp = await fetch(
|
|
5434
|
+
`${cfg.baseUrl}/decompositions/${encodeURIComponent(decompositionId)}/run/telemetry`,
|
|
5435
|
+
{
|
|
5436
|
+
method: "POST",
|
|
5437
|
+
headers: {
|
|
5438
|
+
authorization: `Bearer ${token}`,
|
|
5439
|
+
tenant: cfg.tenant,
|
|
5440
|
+
"content-type": "application/json",
|
|
5441
|
+
"x-sechroom-surface": "cli"
|
|
5442
|
+
},
|
|
5443
|
+
body: JSON.stringify({ events })
|
|
5444
|
+
}
|
|
5445
|
+
);
|
|
5446
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);
|
|
5447
|
+
return await resp.json();
|
|
5448
|
+
}
|
|
5449
|
+
function findBinding(start) {
|
|
5450
|
+
let dir = start;
|
|
5451
|
+
for (; ; ) {
|
|
5452
|
+
const path = join16(dir, ".sechroom", BINDING_FILE);
|
|
5453
|
+
if (existsSync14(path)) {
|
|
5454
|
+
try {
|
|
5455
|
+
const b = JSON.parse(
|
|
5456
|
+
readFileSync9(path, "utf8")
|
|
5457
|
+
);
|
|
5458
|
+
if (b.decompositionId && b.taskId)
|
|
5459
|
+
return { decompositionId: b.decompositionId, taskId: b.taskId };
|
|
5460
|
+
} catch {
|
|
5461
|
+
}
|
|
5462
|
+
return null;
|
|
5463
|
+
}
|
|
5464
|
+
const parent = dirname9(dir);
|
|
5465
|
+
if (parent === dir) return null;
|
|
5466
|
+
dir = parent;
|
|
5467
|
+
}
|
|
5468
|
+
}
|
|
5469
|
+
function parseTranscript(path) {
|
|
5470
|
+
if (!existsSync14(path)) return null;
|
|
5471
|
+
let tokensIn = 0;
|
|
5472
|
+
let tokensOut = 0;
|
|
5473
|
+
let contextUsed = 0;
|
|
5474
|
+
let model = "";
|
|
5475
|
+
for (const line of readFileSync9(path, "utf8").split("\n")) {
|
|
5476
|
+
if (!line.trim()) continue;
|
|
5477
|
+
let obj;
|
|
5478
|
+
try {
|
|
5479
|
+
obj = JSON.parse(line);
|
|
5480
|
+
} catch {
|
|
5481
|
+
continue;
|
|
5482
|
+
}
|
|
5483
|
+
const usage = obj.type === "assistant" ? obj.message?.usage : void 0;
|
|
5484
|
+
if (!usage) continue;
|
|
5485
|
+
const input = (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
|
|
5486
|
+
tokensIn += input;
|
|
5487
|
+
tokensOut += usage.output_tokens ?? 0;
|
|
5488
|
+
contextUsed = input;
|
|
5489
|
+
if (obj.message?.model) model = obj.message.model;
|
|
5490
|
+
}
|
|
5491
|
+
if (tokensIn === 0 && tokensOut === 0) return null;
|
|
5492
|
+
return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model) };
|
|
5493
|
+
}
|
|
5494
|
+
function windowFor(model) {
|
|
5495
|
+
const m = model.toLowerCase();
|
|
5496
|
+
return m.includes("[1m]") || m.includes("-1m") ? 1e6 : 2e5;
|
|
5497
|
+
}
|
|
5498
|
+
async function readStdin2() {
|
|
5499
|
+
if (process.stdin.isTTY) return "";
|
|
5500
|
+
const chunks = [];
|
|
5501
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
5502
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
5503
|
+
}
|
|
5504
|
+
function parseHookInput2(raw) {
|
|
5505
|
+
if (!raw.trim()) return {};
|
|
5506
|
+
try {
|
|
5507
|
+
return JSON.parse(raw);
|
|
5508
|
+
} catch {
|
|
5509
|
+
return {};
|
|
5510
|
+
}
|
|
5298
5511
|
}
|
|
5299
5512
|
var KINDS = {
|
|
5300
5513
|
raw: "Raw",
|
|
@@ -5516,7 +5729,7 @@ Examples:
|
|
|
5516
5729
|
function resolveVersion() {
|
|
5517
5730
|
try {
|
|
5518
5731
|
const pkg = JSON.parse(
|
|
5519
|
-
|
|
5732
|
+
readFileSync10(new URL("../package.json", import.meta.url), "utf8")
|
|
5520
5733
|
);
|
|
5521
5734
|
return pkg.version ?? "0.0.0";
|
|
5522
5735
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sechroom/cli",
|
|
3
|
-
"version": "2026.6.
|
|
3
|
+
"version": "2026.6.239-rc.6418da97",
|
|
4
4
|
"description": "Sechroom CLI — a thin, generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|