@sechroom/cli 2026.6.238-rc.10da7380 → 2026.7.1
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 +513 -341
- package/package.json +4 -3
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
|
|
@@ -433,6 +433,7 @@ async function requireToken(cfg) {
|
|
|
433
433
|
import createClient from "openapi-fetch";
|
|
434
434
|
|
|
435
435
|
// src/ui.ts
|
|
436
|
+
import * as clack from "@clack/prompts";
|
|
436
437
|
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
437
438
|
var quiet = false;
|
|
438
439
|
function setQuiet(q) {
|
|
@@ -458,7 +459,7 @@ var err = (s) => style.red(s);
|
|
|
458
459
|
function active() {
|
|
459
460
|
return !quiet && Boolean(process.stderr.isTTY);
|
|
460
461
|
}
|
|
461
|
-
function spinner(
|
|
462
|
+
function spinner(text2) {
|
|
462
463
|
if (!active()) {
|
|
463
464
|
return { succeed() {
|
|
464
465
|
}, fail() {
|
|
@@ -468,9 +469,9 @@ function spinner(text) {
|
|
|
468
469
|
let i = 0;
|
|
469
470
|
const render = () => {
|
|
470
471
|
i = (i + 1) % FRAMES.length;
|
|
471
|
-
process.stderr.write(`\r${FRAMES[i]} ${
|
|
472
|
+
process.stderr.write(`\r${FRAMES[i]} ${text2}`);
|
|
472
473
|
};
|
|
473
|
-
process.stderr.write(`\r${FRAMES[0]} ${
|
|
474
|
+
process.stderr.write(`\r${FRAMES[0]} ${text2}`);
|
|
474
475
|
const timer = setInterval(render, 80);
|
|
475
476
|
timer.unref?.();
|
|
476
477
|
const clear = () => {
|
|
@@ -480,12 +481,12 @@ function spinner(text) {
|
|
|
480
481
|
return {
|
|
481
482
|
succeed(t) {
|
|
482
483
|
clear();
|
|
483
|
-
process.stderr.write(`${ok("\u2713")} ${t ??
|
|
484
|
+
process.stderr.write(`${ok("\u2713")} ${t ?? text2}
|
|
484
485
|
`);
|
|
485
486
|
},
|
|
486
487
|
fail(t) {
|
|
487
488
|
clear();
|
|
488
|
-
process.stderr.write(`${err("\u2717")} ${t ??
|
|
489
|
+
process.stderr.write(`${err("\u2717")} ${t ?? text2}
|
|
489
490
|
`);
|
|
490
491
|
},
|
|
491
492
|
stop: clear
|
|
@@ -496,31 +497,24 @@ function canPrompt() {
|
|
|
496
497
|
}
|
|
497
498
|
async function promptYesNo(question) {
|
|
498
499
|
if (!canPrompt()) return false;
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
return /^y(es)?$/i.test(answer.trim());
|
|
506
|
-
} finally {
|
|
507
|
-
rl.close();
|
|
508
|
-
}
|
|
500
|
+
const r = await clack.confirm({
|
|
501
|
+
message: question,
|
|
502
|
+
initialValue: false,
|
|
503
|
+
output: process.stderr
|
|
504
|
+
});
|
|
505
|
+
return clack.isCancel(r) ? false : r;
|
|
509
506
|
}
|
|
510
507
|
async function promptText(question, def) {
|
|
511
508
|
if (!canPrompt()) return def ?? "";
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
} finally {
|
|
522
|
-
rl.close();
|
|
523
|
-
}
|
|
509
|
+
const r = await clack.text({
|
|
510
|
+
message: question,
|
|
511
|
+
defaultValue: def,
|
|
512
|
+
placeholder: def,
|
|
513
|
+
output: process.stderr
|
|
514
|
+
});
|
|
515
|
+
if (clack.isCancel(r)) return def ?? "";
|
|
516
|
+
const trimmed = (r ?? "").trim();
|
|
517
|
+
return trimmed.length > 0 ? trimmed : def ?? "";
|
|
524
518
|
}
|
|
525
519
|
async function promptSelect(question, choices, def) {
|
|
526
520
|
if (choices.length === 0) throw new Error("promptSelect: no choices");
|
|
@@ -529,74 +523,50 @@ async function promptSelect(question, choices, def) {
|
|
|
529
523
|
def !== void 0 ? choices.findIndex((c) => c.value === def) : 0
|
|
530
524
|
);
|
|
531
525
|
if (!canPrompt()) return choices[defIdx].value;
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
choices.
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
`);
|
|
542
|
-
});
|
|
543
|
-
const answer = await new Promise((resolve3) => {
|
|
544
|
-
rl.question(`Choose ${style.dim(`[${defIdx + 1}]`)} `, resolve3);
|
|
545
|
-
});
|
|
546
|
-
const trimmed = answer.trim();
|
|
547
|
-
if (!trimmed) return choices[defIdx].value;
|
|
548
|
-
const n = Number(trimmed);
|
|
549
|
-
if (Number.isInteger(n) && n >= 1 && n <= choices.length) return choices[n - 1].value;
|
|
550
|
-
const byLabel = choices.find(
|
|
551
|
-
(c) => c.label.toLowerCase().startsWith(trimmed.toLowerCase())
|
|
552
|
-
);
|
|
553
|
-
return byLabel ? byLabel.value : choices[defIdx].value;
|
|
554
|
-
} finally {
|
|
555
|
-
rl.close();
|
|
556
|
-
}
|
|
526
|
+
const r = await clack.select({
|
|
527
|
+
message: question,
|
|
528
|
+
// Cast: clack's Option<T> is a conditional type (primitive vs not) that TS
|
|
529
|
+
// can't verify against a generic T; the runtime shape is correct.
|
|
530
|
+
options: choices.map((c) => ({ value: c.value, label: c.label, hint: c.hint })),
|
|
531
|
+
initialValue: choices[defIdx].value,
|
|
532
|
+
output: process.stderr
|
|
533
|
+
});
|
|
534
|
+
return clack.isCancel(r) ? choices[defIdx].value : r;
|
|
557
535
|
}
|
|
558
536
|
async function promptMultiSelect(question, choices, preselected = []) {
|
|
559
537
|
if (choices.length === 0) return [];
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
const hint = c.hint ? ` ${style.dim(`\u2014 ${c.hint}`)}` : "";
|
|
573
|
-
process.stderr.write(` ${box} ${style.bold(String(i + 1))}. ${c.label}${hint}
|
|
574
|
-
`);
|
|
575
|
-
});
|
|
576
|
-
const answer = await new Promise((resolve3) => {
|
|
577
|
-
rl.question(`Select ${style.dim("[Enter = \u25C9]")} `, resolve3);
|
|
578
|
-
});
|
|
579
|
-
const trimmed = answer.trim().toLowerCase();
|
|
580
|
-
if (!trimmed) return preValues();
|
|
581
|
-
if (trimmed === "all") return choices.map((c) => c.value);
|
|
582
|
-
const picks = [];
|
|
583
|
-
for (const tok of trimmed.split(",").map((t) => t.trim()).filter(Boolean)) {
|
|
584
|
-
const n = Number(tok);
|
|
585
|
-
if (Number.isInteger(n) && n >= 1 && n <= choices.length) {
|
|
586
|
-
picks.push(choices[n - 1].value);
|
|
587
|
-
continue;
|
|
588
|
-
}
|
|
589
|
-
const byLabel = choices.find((c) => c.label.toLowerCase().startsWith(tok));
|
|
590
|
-
if (byLabel) picks.push(byLabel.value);
|
|
591
|
-
}
|
|
592
|
-
const uniq = [...new Set(picks)];
|
|
593
|
-
return uniq.length > 0 ? uniq : preValues();
|
|
594
|
-
} finally {
|
|
595
|
-
rl.close();
|
|
596
|
-
}
|
|
538
|
+
const preValues = choices.filter((c) => preselected.includes(c.value)).map((c) => c.value);
|
|
539
|
+
if (!canPrompt()) return preValues;
|
|
540
|
+
const r = await clack.multiselect({
|
|
541
|
+
message: question,
|
|
542
|
+
// Cast: clack's Option<T> is a conditional type (primitive vs not) that TS
|
|
543
|
+
// can't verify against a generic T; the runtime shape is correct.
|
|
544
|
+
options: choices.map((c) => ({ value: c.value, label: c.label, hint: c.hint })),
|
|
545
|
+
initialValues: preValues,
|
|
546
|
+
required: false,
|
|
547
|
+
output: process.stderr
|
|
548
|
+
});
|
|
549
|
+
return clack.isCancel(r) ? preValues : r;
|
|
597
550
|
}
|
|
598
|
-
async function
|
|
599
|
-
|
|
551
|
+
async function promptAutocomplete(question, choices, def) {
|
|
552
|
+
if (choices.length === 0) throw new Error("promptAutocomplete: no choices");
|
|
553
|
+
const defIdx = Math.max(
|
|
554
|
+
0,
|
|
555
|
+
def !== void 0 ? choices.findIndex((c) => c.value === def) : 0
|
|
556
|
+
);
|
|
557
|
+
if (!canPrompt()) return choices[defIdx].value;
|
|
558
|
+
const r = await clack.autocomplete({
|
|
559
|
+
message: question,
|
|
560
|
+
// Cast: clack's Option<T> is a conditional type (primitive vs not) that TS
|
|
561
|
+
// can't verify against a generic T; the runtime shape is correct.
|
|
562
|
+
options: choices.map((c) => ({ value: c.value, label: c.label, hint: c.hint })),
|
|
563
|
+
placeholder: "Type to filter\u2026",
|
|
564
|
+
output: process.stderr
|
|
565
|
+
});
|
|
566
|
+
return clack.isCancel(r) ? choices[defIdx].value : r;
|
|
567
|
+
}
|
|
568
|
+
async function withSpinner(text2, fn) {
|
|
569
|
+
const s = spinner(text2);
|
|
600
570
|
try {
|
|
601
571
|
const result = await fn();
|
|
602
572
|
s.succeed();
|
|
@@ -1521,7 +1491,7 @@ Examples:
|
|
|
1521
1491
|
$ sechroom chat replies 1718049600.123456 --surface slack
|
|
1522
1492
|
$ sechroom chat stop-tracking 1718049600.123456 --surface slack`
|
|
1523
1493
|
);
|
|
1524
|
-
chat.command("send <channelId> <text>").description("Send a message to a channel (POST /chat/channel-messages/{surface})").option("--guild <guildId>", "Discord guild snowflake \u2014 required for --surface discord").option("--memory <memoryId>", "Attach a sechroom memory id").option("--no-track", "Don't capture replies to this message").option("--parent <parentMessage>", "Thread under a parent (Slack thread_ts / Discord message id)").option("--source <source>", "Source / lane stamp (renders an attribution footer)", "cli").option("--as <as>", "Slack only: 'bot' (default) or 'user' (your linked Slack identity)", "bot").action(async (channelId,
|
|
1494
|
+
chat.command("send <channelId> <text>").description("Send a message to a channel (POST /chat/channel-messages/{surface})").option("--guild <guildId>", "Discord guild snowflake \u2014 required for --surface discord").option("--memory <memoryId>", "Attach a sechroom memory id").option("--no-track", "Don't capture replies to this message").option("--parent <parentMessage>", "Thread under a parent (Slack thread_ts / Discord message id)").option("--source <source>", "Source / lane stamp (renders an attribution footer)", "cli").option("--as <as>", "Slack only: 'bot' (default) or 'user' (your linked Slack identity)", "bot").action(async (channelId, text2, opts, cmd) => {
|
|
1525
1495
|
const { surface, ...globals } = cmd.optsWithGlobals();
|
|
1526
1496
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
1527
1497
|
const cfg = resolveConfig(globals);
|
|
@@ -1531,7 +1501,7 @@ Examples:
|
|
|
1531
1501
|
params: { path: { surface: String(surface) } },
|
|
1532
1502
|
body: {
|
|
1533
1503
|
channelId,
|
|
1534
|
-
text,
|
|
1504
|
+
text: text2,
|
|
1535
1505
|
guildId: opts.guild ?? null,
|
|
1536
1506
|
attachedMemoryId: opts.memory ?? null,
|
|
1537
1507
|
trackReplies: opts.track,
|
|
@@ -1590,13 +1560,13 @@ Examples:
|
|
|
1590
1560
|
}
|
|
1591
1561
|
|
|
1592
1562
|
// src/commands/checkpoint.ts
|
|
1593
|
-
import { mkdirSync as
|
|
1594
|
-
import { dirname as
|
|
1563
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
1564
|
+
import { dirname as dirname6, join as join9 } from "path";
|
|
1595
1565
|
|
|
1596
1566
|
// src/commands/hook.ts
|
|
1597
1567
|
import { createHash as createHash2 } from "crypto";
|
|
1598
|
-
import { existsSync as
|
|
1599
|
-
import {
|
|
1568
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
1569
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
1600
1570
|
|
|
1601
1571
|
// src/sem.ts
|
|
1602
1572
|
import { dirname as dirname2, join as join5 } from "path";
|
|
@@ -1666,9 +1636,9 @@ function readLocalSemValues(cwd = process.cwd()) {
|
|
|
1666
1636
|
if (existsSync4(next)) return readSem(next)?.values ?? {};
|
|
1667
1637
|
return {};
|
|
1668
1638
|
}
|
|
1669
|
-
function parseLaneJson(
|
|
1639
|
+
function parseLaneJson(text2) {
|
|
1670
1640
|
try {
|
|
1671
|
-
const parsed = JSON.parse(
|
|
1641
|
+
const parsed = JSON.parse(text2);
|
|
1672
1642
|
const out = {};
|
|
1673
1643
|
for (const [k, v] of Object.entries(parsed)) {
|
|
1674
1644
|
if (typeof v === "string") out[k] = v;
|
|
@@ -1686,6 +1656,9 @@ function writeSem(values, path = localSemPath()) {
|
|
|
1686
1656
|
ensureContinuityScaffold(path);
|
|
1687
1657
|
return path;
|
|
1688
1658
|
}
|
|
1659
|
+
function ensureStateDirIgnored(cwd = process.cwd()) {
|
|
1660
|
+
ensureSemIgnored(localSemPath(cwd));
|
|
1661
|
+
}
|
|
1689
1662
|
var CONTINUITY_FILE_NAME = "continuity.json";
|
|
1690
1663
|
var CONTINUITY_SCAFFOLD = JSON.stringify(
|
|
1691
1664
|
{
|
|
@@ -1757,6 +1730,10 @@ function ensureSemIgnored(semPath) {
|
|
|
1757
1730
|
}
|
|
1758
1731
|
}
|
|
1759
1732
|
|
|
1733
|
+
// src/commands/hook-install.ts
|
|
1734
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
1735
|
+
import { delimiter, dirname as dirname4, join as join7 } from "path";
|
|
1736
|
+
|
|
1760
1737
|
// src/setup/clients.ts
|
|
1761
1738
|
import { existsSync as existsSync5 } from "fs";
|
|
1762
1739
|
import { homedir as homedir3 } from "os";
|
|
@@ -1827,6 +1804,144 @@ function detectInstalledClients(cwd) {
|
|
|
1827
1804
|
return detected;
|
|
1828
1805
|
}
|
|
1829
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
|
+
|
|
1830
1945
|
// src/commands/hook.ts
|
|
1831
1946
|
async function readStdin() {
|
|
1832
1947
|
if (process.stdin.isTTY) return "";
|
|
@@ -1851,13 +1966,13 @@ function resolveLane(flagLane, cwd) {
|
|
|
1851
1966
|
if (!base) return void 0;
|
|
1852
1967
|
return applyWorktreeLaneSuffix(base, start);
|
|
1853
1968
|
}
|
|
1854
|
-
var INTENT_FILE =
|
|
1969
|
+
var INTENT_FILE = join8(".sechroom", "continuity.json");
|
|
1855
1970
|
function resolveIntentPath(start) {
|
|
1856
1971
|
let dir = start;
|
|
1857
1972
|
for (; ; ) {
|
|
1858
|
-
const candidate =
|
|
1859
|
-
if (
|
|
1860
|
-
const parent =
|
|
1973
|
+
const candidate = join8(dir, INTENT_FILE);
|
|
1974
|
+
if (existsSync7(candidate)) return candidate;
|
|
1975
|
+
const parent = dirname5(dir);
|
|
1861
1976
|
if (parent === dir) return void 0;
|
|
1862
1977
|
dir = parent;
|
|
1863
1978
|
}
|
|
@@ -1866,7 +1981,7 @@ function readIntent(start) {
|
|
|
1866
1981
|
const path = resolveIntentPath(start);
|
|
1867
1982
|
if (!path) return void 0;
|
|
1868
1983
|
try {
|
|
1869
|
-
return JSON.parse(
|
|
1984
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
1870
1985
|
} catch {
|
|
1871
1986
|
return void 0;
|
|
1872
1987
|
}
|
|
@@ -1908,14 +2023,14 @@ async function saveSnapshotFromIntent(cmd, cwd, laneFlag, scopeFlag, defaultScop
|
|
|
1908
2023
|
}
|
|
1909
2024
|
function ledgerPath(start) {
|
|
1910
2025
|
const intent = resolveIntentPath(start);
|
|
1911
|
-
const dir = intent ?
|
|
1912
|
-
return
|
|
2026
|
+
const dir = intent ? dirname5(intent) : join8(start, ".sechroom");
|
|
2027
|
+
return join8(dir, ".checkpoint-state.json");
|
|
1913
2028
|
}
|
|
1914
2029
|
function readLedger(start) {
|
|
1915
2030
|
try {
|
|
1916
2031
|
const p = ledgerPath(start);
|
|
1917
|
-
if (!
|
|
1918
|
-
return JSON.parse(
|
|
2032
|
+
if (!existsSync7(p)) return {};
|
|
2033
|
+
return JSON.parse(readFileSync5(p, "utf8"));
|
|
1919
2034
|
} catch {
|
|
1920
2035
|
return {};
|
|
1921
2036
|
}
|
|
@@ -1962,13 +2077,13 @@ function recordPush(start, intent) {
|
|
|
1962
2077
|
} catch {
|
|
1963
2078
|
mtimeMs = void 0;
|
|
1964
2079
|
}
|
|
1965
|
-
|
|
2080
|
+
mkdirSync6(dirname5(p), { recursive: true });
|
|
1966
2081
|
const ledger = {
|
|
1967
2082
|
lastEpochMs: Date.now(),
|
|
1968
2083
|
lastMtimeMs: mtimeMs,
|
|
1969
2084
|
lastHash: intentHash(intent)
|
|
1970
2085
|
};
|
|
1971
|
-
|
|
2086
|
+
writeFileSync6(p, JSON.stringify(ledger) + "\n");
|
|
1972
2087
|
} catch {
|
|
1973
2088
|
}
|
|
1974
2089
|
}
|
|
@@ -2008,135 +2123,6 @@ function emitSessionStart(additionalContext) {
|
|
|
2008
2123
|
}) + "\n"
|
|
2009
2124
|
);
|
|
2010
2125
|
}
|
|
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
2126
|
function registerHook(program2) {
|
|
2141
2127
|
const hook = program2.command("hook").description("Agent-lifecycle hook adapter (Claude Code / Codex) \u2014 bridges hooks to continuity");
|
|
2142
2128
|
hook.addHelpText(
|
|
@@ -2344,10 +2330,10 @@ Examples:
|
|
|
2344
2330
|
const client = await makeClient(cfg);
|
|
2345
2331
|
return client.POST("/continuity/snapshots", { body });
|
|
2346
2332
|
});
|
|
2347
|
-
const path = resolveIntentPath(cwd) ??
|
|
2333
|
+
const path = resolveIntentPath(cwd) ?? join9(cwd, INTENT_FILE);
|
|
2348
2334
|
const fileBody = { ...merged, scope, lastSnapshotId: data.snapshotId };
|
|
2349
|
-
|
|
2350
|
-
|
|
2335
|
+
mkdirSync7(dirname6(path), { recursive: true });
|
|
2336
|
+
writeFileSync7(path, JSON.stringify(fileBody, null, 2) + "\n");
|
|
2351
2337
|
recordPush(cwd, merged);
|
|
2352
2338
|
if (json) {
|
|
2353
2339
|
emit({ snapshotId: data.snapshotId, lane, scope, file: path }, true);
|
|
@@ -3031,8 +3017,8 @@ Examples:
|
|
|
3031
3017
|
|
|
3032
3018
|
// src/setup/apply.ts
|
|
3033
3019
|
import { createHash as createHash3 } from "crypto";
|
|
3034
|
-
import { mkdirSync as
|
|
3035
|
-
import { dirname as
|
|
3020
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync8, existsSync as existsSync8 } from "fs";
|
|
3021
|
+
import { dirname as dirname7 } from "path";
|
|
3036
3022
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
3037
3023
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
3038
3024
|
function normalizeBody(s) {
|
|
@@ -3085,22 +3071,22 @@ function parseManagedBlock(content, block) {
|
|
|
3085
3071
|
return null;
|
|
3086
3072
|
}
|
|
3087
3073
|
function ensureDir2(path) {
|
|
3088
|
-
|
|
3074
|
+
mkdirSync8(dirname7(path), { recursive: true });
|
|
3089
3075
|
}
|
|
3090
3076
|
function readOr(path, fallback) {
|
|
3091
3077
|
try {
|
|
3092
|
-
return
|
|
3078
|
+
return readFileSync6(path, "utf8");
|
|
3093
3079
|
} catch {
|
|
3094
3080
|
return fallback;
|
|
3095
3081
|
}
|
|
3096
3082
|
}
|
|
3097
3083
|
function mergeMcpJson(path, snippet, dryRun) {
|
|
3098
3084
|
const incoming = JSON.parse(snippet);
|
|
3099
|
-
const existed =
|
|
3085
|
+
const existed = existsSync8(path);
|
|
3100
3086
|
let current = {};
|
|
3101
3087
|
if (existed) {
|
|
3102
3088
|
try {
|
|
3103
|
-
current = JSON.parse(
|
|
3089
|
+
current = JSON.parse(readFileSync6(path, "utf8"));
|
|
3104
3090
|
} catch {
|
|
3105
3091
|
return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
|
|
3106
3092
|
}
|
|
@@ -3108,26 +3094,26 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
3108
3094
|
current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
|
|
3109
3095
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3110
3096
|
ensureDir2(path);
|
|
3111
|
-
|
|
3097
|
+
writeFileSync8(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
3112
3098
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3113
3099
|
}
|
|
3114
3100
|
function mergeCodexToml(path, snippet, dryRun) {
|
|
3115
|
-
const existed =
|
|
3101
|
+
const existed = existsSync8(path);
|
|
3116
3102
|
let body = readOr(path, "");
|
|
3117
3103
|
body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
|
|
3118
3104
|
const trimmed = body.trim();
|
|
3119
3105
|
const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
|
|
3120
3106
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
3121
3107
|
ensureDir2(path);
|
|
3122
|
-
|
|
3108
|
+
writeFileSync8(path, next, { mode: 384 });
|
|
3123
3109
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
3124
3110
|
}
|
|
3125
3111
|
function writeInstructionBlock(path, write, dryRun) {
|
|
3126
|
-
const existed =
|
|
3112
|
+
const existed = existsSync8(path);
|
|
3127
3113
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
3128
3114
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
3129
3115
|
ensureDir2(path);
|
|
3130
|
-
|
|
3116
|
+
writeFileSync8(path, next);
|
|
3131
3117
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
3132
3118
|
}
|
|
3133
3119
|
function computeBlockFile(current, write) {
|
|
@@ -3168,7 +3154,7 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
3168
3154
|
const next = computeBlockFile(current, write);
|
|
3169
3155
|
if (!dryRun) {
|
|
3170
3156
|
ensureDir2(proposedPath);
|
|
3171
|
-
|
|
3157
|
+
writeFileSync8(proposedPath, next);
|
|
3172
3158
|
}
|
|
3173
3159
|
return {
|
|
3174
3160
|
kind: "instruction",
|
|
@@ -3292,8 +3278,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
3292
3278
|
}
|
|
3293
3279
|
|
|
3294
3280
|
// src/setup/skills-offer.ts
|
|
3295
|
-
import { mkdirSync as
|
|
3296
|
-
import { join as
|
|
3281
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
3282
|
+
import { join as join10 } from "path";
|
|
3297
3283
|
|
|
3298
3284
|
// src/setup/lane-pin.ts
|
|
3299
3285
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -3409,8 +3395,8 @@ Found ${summary} available to you for ${surface}.
|
|
|
3409
3395
|
if (skills.length > 0) {
|
|
3410
3396
|
const written = [];
|
|
3411
3397
|
for (const s of skills) {
|
|
3412
|
-
|
|
3413
|
-
|
|
3398
|
+
mkdirSync9(join10(sDir, s.name), { recursive: true });
|
|
3399
|
+
writeFileSync9(join10(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
3414
3400
|
written.push(s.name);
|
|
3415
3401
|
}
|
|
3416
3402
|
recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -3418,11 +3404,11 @@ Found ${summary} available to you for ${surface}.
|
|
|
3418
3404
|
`);
|
|
3419
3405
|
}
|
|
3420
3406
|
if (agents.length > 0) {
|
|
3421
|
-
|
|
3407
|
+
mkdirSync9(aDir, { recursive: true });
|
|
3422
3408
|
const written = [];
|
|
3423
3409
|
for (const a of agents) {
|
|
3424
3410
|
const file = `${a.name}.md`;
|
|
3425
|
-
|
|
3411
|
+
writeFileSync9(join10(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
3426
3412
|
written.push(file);
|
|
3427
3413
|
}
|
|
3428
3414
|
recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
@@ -3651,20 +3637,20 @@ Examples:
|
|
|
3651
3637
|
fail("no workspace \u2014 pass --workspace <id> or bind one (`sechroom config set --local workspaceId <id>`).");
|
|
3652
3638
|
const kind = String(opts.kind).toLowerCase() === "standard" ? "standard" : "reference";
|
|
3653
3639
|
const body = typeof opts.body === "string" && opts.body.trim().length > 0 ? opts.body.trim() : "_TODO: write this section, then edit the memo and re-run the regen._";
|
|
3654
|
-
const
|
|
3640
|
+
const text2 = `# ${title}
|
|
3655
3641
|
|
|
3656
3642
|
${body}
|
|
3657
3643
|
`;
|
|
3658
3644
|
const tags = ["agent-setup-bundle", "scope:sechroom", `kind:${kind}`, "archetype:document"];
|
|
3659
3645
|
if (opts.dryRun) {
|
|
3660
|
-
emit({ dryRun: true, workspaceId, title, kind, tags, text }, json);
|
|
3646
|
+
emit({ dryRun: true, workspaceId, title, kind, tags, text: text2 }, json);
|
|
3661
3647
|
return;
|
|
3662
3648
|
}
|
|
3663
3649
|
const data = await runApi("Authoring convention memo", async () => {
|
|
3664
3650
|
const client = await makeClient(cfg);
|
|
3665
3651
|
return client.POST("/memories", {
|
|
3666
3652
|
body: {
|
|
3667
|
-
text,
|
|
3653
|
+
text: text2,
|
|
3668
3654
|
type: kind,
|
|
3669
3655
|
content: "{}",
|
|
3670
3656
|
confidence: 1,
|
|
@@ -3805,13 +3791,13 @@ Wired to namespace '${slug}'. Restart your AI client (or reload MCP) to pick it
|
|
|
3805
3791
|
}
|
|
3806
3792
|
|
|
3807
3793
|
// src/commands/onboard.ts
|
|
3808
|
-
import { existsSync as
|
|
3809
|
-
import { basename as basename2, join as
|
|
3794
|
+
import { existsSync as existsSync10 } from "fs";
|
|
3795
|
+
import { basename as basename2, join as join12 } from "path";
|
|
3810
3796
|
|
|
3811
3797
|
// src/commands/fanout.ts
|
|
3812
3798
|
import { spawnSync } from "child_process";
|
|
3813
|
-
import { existsSync as
|
|
3814
|
-
import { isAbsolute, join as
|
|
3799
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
3800
|
+
import { isAbsolute, join as join11, resolve } from "path";
|
|
3815
3801
|
var ICON = {
|
|
3816
3802
|
refresh: "\u21BB",
|
|
3817
3803
|
bind: "+",
|
|
@@ -3831,21 +3817,21 @@ function discoverChildren(root) {
|
|
|
3831
3817
|
const out = [];
|
|
3832
3818
|
for (const name of names.sort()) {
|
|
3833
3819
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
3834
|
-
const dir =
|
|
3820
|
+
const dir = join11(root, name);
|
|
3835
3821
|
try {
|
|
3836
3822
|
if (!statSync3(dir).isDirectory()) continue;
|
|
3837
3823
|
} catch {
|
|
3838
3824
|
continue;
|
|
3839
3825
|
}
|
|
3840
|
-
if (
|
|
3826
|
+
if (existsSync9(join11(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
3841
3827
|
}
|
|
3842
3828
|
return out;
|
|
3843
3829
|
}
|
|
3844
3830
|
function readManifest(path) {
|
|
3845
|
-
if (!
|
|
3831
|
+
if (!existsSync9(path)) return null;
|
|
3846
3832
|
let parsed;
|
|
3847
3833
|
try {
|
|
3848
|
-
parsed = JSON.parse(
|
|
3834
|
+
parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
3849
3835
|
} catch (err2) {
|
|
3850
3836
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
3851
3837
|
}
|
|
@@ -4029,28 +4015,17 @@ async function pickWorkspace(client, opts = {}) {
|
|
|
4029
4015
|
if (candidates.length === 0) candidates = all;
|
|
4030
4016
|
const dirToks = new Set(nameTokens(dirName));
|
|
4031
4017
|
const isMatch = (w) => nameTokens(w.name).some((t) => dirToks.has(t));
|
|
4032
|
-
const suggestions = candidates.filter(isMatch);
|
|
4033
|
-
let pool = candidates;
|
|
4034
|
-
if (candidates.length > 12 && suggestions.length === 0) {
|
|
4035
|
-
const q = (await promptText(`Filter ${candidates.length} workspaces (substring, Enter to list all)?`, "")).trim().toLowerCase();
|
|
4036
|
-
if (q) {
|
|
4037
|
-
const hits = candidates.filter((w) => `${w.name} ${workspacePath(w, byId)}`.toLowerCase().includes(q));
|
|
4038
|
-
if (hits.length > 0) pool = hits;
|
|
4039
|
-
else process.stderr.write(`no match for "${q}" \u2014 listing all
|
|
4040
|
-
`);
|
|
4041
|
-
}
|
|
4042
|
-
}
|
|
4043
4018
|
const SKIP = "__skip__";
|
|
4044
4019
|
const byPath = (a, b) => workspacePath(a, byId).localeCompare(workspacePath(b, byId));
|
|
4045
|
-
const matched =
|
|
4046
|
-
const rest =
|
|
4020
|
+
const matched = candidates.filter(isMatch).sort(byPath);
|
|
4021
|
+
const rest = candidates.filter((w) => !isMatch(w)).sort(byPath);
|
|
4047
4022
|
const choices = [
|
|
4048
4023
|
...matched.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: style.dim(`matches "${dirName}"`) })),
|
|
4049
4024
|
...rest.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: w.id })),
|
|
4050
4025
|
{ label: style.dim("skip \u2014 don't bind a workspace"), value: SKIP, hint: void 0 }
|
|
4051
4026
|
];
|
|
4052
4027
|
const defaultValue = matched.length === 1 ? matched[0].id : SKIP;
|
|
4053
|
-
const chosen = await promptSelect(promptLabel, choices, defaultValue);
|
|
4028
|
+
const chosen = candidates.length > 12 ? await promptAutocomplete(promptLabel, choices, defaultValue) : await promptSelect(promptLabel, choices, defaultValue);
|
|
4054
4029
|
if (chosen === SKIP) return void 0;
|
|
4055
4030
|
const picked = byId.get(chosen);
|
|
4056
4031
|
const collisions = all.filter((w) => w.id !== picked.id && namesCollide(w.name, picked.name));
|
|
@@ -4222,10 +4197,10 @@ async function chooseScope(scopeFlag, yes) {
|
|
|
4222
4197
|
}
|
|
4223
4198
|
async function planRecurseChild(entry, root, client, opts) {
|
|
4224
4199
|
const dir = resolveChildDir(entry.path, root);
|
|
4225
|
-
if (!
|
|
4200
|
+
if (!existsSync10(dir)) {
|
|
4226
4201
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
4227
4202
|
}
|
|
4228
|
-
if (
|
|
4203
|
+
if (existsSync10(join12(dir, ".sechroom.json"))) {
|
|
4229
4204
|
return {
|
|
4230
4205
|
label: entry.path,
|
|
4231
4206
|
dir,
|
|
@@ -4298,7 +4273,7 @@ This fan-out will pin the same lane in every repo:
|
|
|
4298
4273
|
async function runRecurse(cfg, g, opts) {
|
|
4299
4274
|
const { yes, dryRun, json } = opts;
|
|
4300
4275
|
const root = process.cwd();
|
|
4301
|
-
const manifestPath =
|
|
4276
|
+
const manifestPath = join12(root, ".sechroom", "repos.json");
|
|
4302
4277
|
const fromManifest = readManifest(manifestPath);
|
|
4303
4278
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
4304
4279
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
@@ -4810,23 +4785,23 @@ Examples:
|
|
|
4810
4785
|
|
|
4811
4786
|
// src/commands/reset.ts
|
|
4812
4787
|
import { homedir as homedir4 } from "os";
|
|
4813
|
-
import { join as
|
|
4814
|
-
import { existsSync as
|
|
4788
|
+
import { join as join13 } from "path";
|
|
4789
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8, rmSync as rmSync3 } from "fs";
|
|
4815
4790
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
4816
|
-
var localSkillsDir = () =>
|
|
4817
|
-
var globalSkillsDir = () =>
|
|
4818
|
-
var localAgentsDir = () =>
|
|
4819
|
-
var globalAgentsDir = () =>
|
|
4791
|
+
var localSkillsDir = () => join13(process.cwd(), ".claude", "skills");
|
|
4792
|
+
var globalSkillsDir = () => join13(homedir4(), ".claude", "skills");
|
|
4793
|
+
var localAgentsDir = () => join13(process.cwd(), ".claude", "agents");
|
|
4794
|
+
var globalAgentsDir = () => join13(homedir4(), ".claude", "agents");
|
|
4820
4795
|
function removeMaterialisedSkills(dir) {
|
|
4821
4796
|
const removed = [];
|
|
4822
|
-
const lockPath =
|
|
4823
|
-
if (!
|
|
4797
|
+
const lockPath = join13(dir, SKILLS_LOCK2);
|
|
4798
|
+
if (!existsSync11(lockPath)) return removed;
|
|
4824
4799
|
try {
|
|
4825
|
-
const lock = JSON.parse(
|
|
4800
|
+
const lock = JSON.parse(readFileSync8(lockPath, "utf8"));
|
|
4826
4801
|
for (const entry of Object.values(lock)) {
|
|
4827
4802
|
for (const name of entry.skills ?? []) {
|
|
4828
|
-
const p =
|
|
4829
|
-
if (
|
|
4803
|
+
const p = join13(dir, name);
|
|
4804
|
+
if (existsSync11(p)) {
|
|
4830
4805
|
rmSync3(p, { recursive: true, force: true });
|
|
4831
4806
|
removed.push(p);
|
|
4832
4807
|
}
|
|
@@ -4871,18 +4846,18 @@ function registerReset(program2) {
|
|
|
4871
4846
|
}
|
|
4872
4847
|
}
|
|
4873
4848
|
const removed = [];
|
|
4874
|
-
const stateDir =
|
|
4875
|
-
if (
|
|
4849
|
+
const stateDir = join13(process.cwd(), ".sechroom");
|
|
4850
|
+
if (existsSync11(stateDir)) {
|
|
4876
4851
|
rmSync3(stateDir, { recursive: true, force: true });
|
|
4877
4852
|
removed.push(stateDir);
|
|
4878
4853
|
}
|
|
4879
|
-
const legacyCfg =
|
|
4880
|
-
if (
|
|
4854
|
+
const legacyCfg = join13(process.cwd(), ".sechroom.json");
|
|
4855
|
+
if (existsSync11(legacyCfg)) {
|
|
4881
4856
|
rmSync3(legacyCfg, { force: true });
|
|
4882
4857
|
removed.push(legacyCfg);
|
|
4883
4858
|
}
|
|
4884
|
-
const legacySem =
|
|
4885
|
-
if (
|
|
4859
|
+
const legacySem = join13(process.cwd(), ".sem");
|
|
4860
|
+
if (existsSync11(legacySem)) {
|
|
4886
4861
|
rmSync3(legacySem, { force: true });
|
|
4887
4862
|
removed.push(legacySem);
|
|
4888
4863
|
}
|
|
@@ -4908,8 +4883,8 @@ function registerReset(program2) {
|
|
|
4908
4883
|
}
|
|
4909
4884
|
|
|
4910
4885
|
// src/commands/skills.ts
|
|
4911
|
-
import { existsSync as
|
|
4912
|
-
import { join as
|
|
4886
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync10, statSync as statSync4, writeFileSync as writeFileSync10 } from "fs";
|
|
4887
|
+
import { join as join14 } from "path";
|
|
4913
4888
|
function filenameFromDisposition(header) {
|
|
4914
4889
|
if (!header) return void 0;
|
|
4915
4890
|
const m = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
|
|
@@ -4917,11 +4892,11 @@ function filenameFromDisposition(header) {
|
|
|
4917
4892
|
}
|
|
4918
4893
|
function resolveOutputPath(output, serverFilename) {
|
|
4919
4894
|
const filename = serverFilename || "skills.zip";
|
|
4920
|
-
if (!output) return
|
|
4921
|
-
const looksLikeDir = output.endsWith("/") ||
|
|
4895
|
+
if (!output) return join14(process.cwd(), filename);
|
|
4896
|
+
const looksLikeDir = output.endsWith("/") || existsSync12(output) && statSync4(output).isDirectory();
|
|
4922
4897
|
if (looksLikeDir) {
|
|
4923
|
-
|
|
4924
|
-
return
|
|
4898
|
+
mkdirSync10(output, { recursive: true });
|
|
4899
|
+
return join14(output, filename);
|
|
4925
4900
|
}
|
|
4926
4901
|
return output;
|
|
4927
4902
|
}
|
|
@@ -4952,7 +4927,7 @@ async function downloadZip(label, call, output) {
|
|
|
4952
4927
|
const buf = Buffer.from(res.data);
|
|
4953
4928
|
const filename = filenameFromDisposition(res.response.headers.get("content-disposition")) ?? "skills.zip";
|
|
4954
4929
|
const path = resolveOutputPath(output, filename);
|
|
4955
|
-
|
|
4930
|
+
writeFileSync10(path, buf);
|
|
4956
4931
|
return { path, bytes: buf.length, filename };
|
|
4957
4932
|
}
|
|
4958
4933
|
function registerSkills(program2) {
|
|
@@ -5120,12 +5095,12 @@ Examples:
|
|
|
5120
5095
|
}
|
|
5121
5096
|
|
|
5122
5097
|
// src/commands/sweep.ts
|
|
5123
|
-
import { existsSync as
|
|
5124
|
-
import { dirname as
|
|
5125
|
-
var DEFAULT_MANIFEST =
|
|
5098
|
+
import { existsSync as existsSync13 } from "fs";
|
|
5099
|
+
import { dirname as dirname8, join as join15, resolve as resolve2 } from "path";
|
|
5100
|
+
var DEFAULT_MANIFEST = join15(".sechroom", "repos.json");
|
|
5126
5101
|
function planEntry(entry, root) {
|
|
5127
5102
|
const dir = resolveChildDir(entry.path, root);
|
|
5128
|
-
if (!
|
|
5103
|
+
if (!existsSync13(dir)) {
|
|
5129
5104
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
5130
5105
|
}
|
|
5131
5106
|
if (committedBindingPath(dir)) {
|
|
@@ -5201,7 +5176,7 @@ Examples:
|
|
|
5201
5176
|
`);
|
|
5202
5177
|
return;
|
|
5203
5178
|
}
|
|
5204
|
-
const root =
|
|
5179
|
+
const root = dirname8(dirname8(manifestPath));
|
|
5205
5180
|
const plans = repos.map((entry) => planEntry(entry, root));
|
|
5206
5181
|
if (!json) {
|
|
5207
5182
|
process.stderr.write(
|
|
@@ -5219,6 +5194,14 @@ Examples:
|
|
|
5219
5194
|
}
|
|
5220
5195
|
|
|
5221
5196
|
// src/commands/telemetry.ts
|
|
5197
|
+
import {
|
|
5198
|
+
existsSync as existsSync14,
|
|
5199
|
+
mkdirSync as mkdirSync11,
|
|
5200
|
+
readFileSync as readFileSync9,
|
|
5201
|
+
rmSync as rmSync4,
|
|
5202
|
+
writeFileSync as writeFileSync11
|
|
5203
|
+
} from "fs";
|
|
5204
|
+
import { dirname as dirname9, join as join16 } from "path";
|
|
5222
5205
|
function registerTelemetry(program2) {
|
|
5223
5206
|
const telemetry = program2.command("telemetry").description(
|
|
5224
5207
|
"Emit WLP run telemetry (an executor leg's progress events) into a decomposition run"
|
|
@@ -5253,10 +5236,9 @@ function registerTelemetry(program2) {
|
|
|
5253
5236
|
).action(async (opts, cmd) => {
|
|
5254
5237
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
5255
5238
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
5256
|
-
const kind = normalizeKind(opts.kind);
|
|
5257
5239
|
const event = {
|
|
5258
5240
|
taskId: opts.task,
|
|
5259
|
-
kind,
|
|
5241
|
+
kind: normalizeKind(opts.kind),
|
|
5260
5242
|
tokensIn: opts.tokensIn ?? null,
|
|
5261
5243
|
tokensOut: opts.tokensOut ?? null,
|
|
5262
5244
|
contextUsed: opts.contextUsed ?? null,
|
|
@@ -5265,36 +5247,226 @@ function registerTelemetry(program2) {
|
|
|
5265
5247
|
approvalState: opts.approval ?? null,
|
|
5266
5248
|
verdict: opts.verdict ?? null
|
|
5267
5249
|
};
|
|
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();
|
|
5250
|
+
let body;
|
|
5251
|
+
try {
|
|
5252
|
+
body = await postTelemetry(cfg, opts.decomposition, [event]);
|
|
5253
|
+
} catch (e) {
|
|
5254
|
+
return fail(`Telemetry emit failed: ${e.message}`);
|
|
5255
|
+
}
|
|
5287
5256
|
if (json) {
|
|
5288
5257
|
emit(body, true);
|
|
5289
5258
|
} else {
|
|
5290
5259
|
process.stderr.write(
|
|
5291
5260
|
style.green("telemetry emitted") + style.dim(
|
|
5292
|
-
` \u2014 ${kind} for task ${opts.task}; run now carries ${body.eventCount} event${body.eventCount === 1 ? "" : "s"}
|
|
5261
|
+
` \u2014 ${event.kind} for task ${opts.task}; run now carries ${body.eventCount} event${body.eventCount === 1 ? "" : "s"}
|
|
5293
5262
|
`
|
|
5294
5263
|
)
|
|
5295
5264
|
);
|
|
5296
5265
|
}
|
|
5297
5266
|
});
|
|
5267
|
+
telemetry.command("bind").description(
|
|
5268
|
+
"Bind this checkout to a decomposition+task so the Stop hook auto-emits per-turn telemetry"
|
|
5269
|
+
).requiredOption(
|
|
5270
|
+
"--decomposition <id>",
|
|
5271
|
+
"Decomposition id this session executes"
|
|
5272
|
+
).requiredOption("--task <id>", "Task id this session executes").action((opts, cmd) => {
|
|
5273
|
+
const json = Boolean(cmd.optsWithGlobals().json);
|
|
5274
|
+
const dir = join16(process.cwd(), ".sechroom");
|
|
5275
|
+
mkdirSync11(dir, { recursive: true });
|
|
5276
|
+
const path = join16(dir, BINDING_FILE);
|
|
5277
|
+
const binding = {
|
|
5278
|
+
decompositionId: opts.decomposition,
|
|
5279
|
+
taskId: opts.task
|
|
5280
|
+
};
|
|
5281
|
+
writeFileSync11(path, JSON.stringify(binding, null, 2) + "\n");
|
|
5282
|
+
ensureStateDirIgnored(process.cwd());
|
|
5283
|
+
if (json) {
|
|
5284
|
+
emit({ bound: true, ...binding, path }, true);
|
|
5285
|
+
} else {
|
|
5286
|
+
process.stdout.write(
|
|
5287
|
+
style.green("telemetry bound") + style.dim(
|
|
5288
|
+
` \u2014 decomposition ${binding.decompositionId}, task ${binding.taskId} (${path})
|
|
5289
|
+
`
|
|
5290
|
+
)
|
|
5291
|
+
);
|
|
5292
|
+
}
|
|
5293
|
+
});
|
|
5294
|
+
telemetry.command("unbind").description("Clear this checkout's telemetry binding").action((_opts, cmd) => {
|
|
5295
|
+
const json = Boolean(cmd.optsWithGlobals().json);
|
|
5296
|
+
const path = join16(process.cwd(), ".sechroom", BINDING_FILE);
|
|
5297
|
+
const existed = existsSync14(path);
|
|
5298
|
+
if (existed) rmSync4(path);
|
|
5299
|
+
if (json) emit({ unbound: existed, path }, true);
|
|
5300
|
+
else
|
|
5301
|
+
process.stdout.write(
|
|
5302
|
+
existed ? "telemetry binding cleared\n" : "no telemetry binding to clear\n"
|
|
5303
|
+
);
|
|
5304
|
+
});
|
|
5305
|
+
telemetry.command("hook").description(
|
|
5306
|
+
"Per-turn telemetry self-report for a Claude Code Stop hook (reads stdin; no-op unless bound). Fail-soft."
|
|
5307
|
+
).action(async (_opts, cmd) => {
|
|
5308
|
+
try {
|
|
5309
|
+
const raw = await readStdin2();
|
|
5310
|
+
const input = parseHookInput2(raw);
|
|
5311
|
+
const cwd = input.cwd ?? process.cwd();
|
|
5312
|
+
const binding = findBinding(cwd);
|
|
5313
|
+
if (!binding) return process.exit(0);
|
|
5314
|
+
const usage = input.transcript_path ? parseTranscript(input.transcript_path) : null;
|
|
5315
|
+
if (!usage) return process.exit(0);
|
|
5316
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
5317
|
+
await postTelemetry(cfg, binding.decompositionId, [
|
|
5318
|
+
{
|
|
5319
|
+
taskId: binding.taskId,
|
|
5320
|
+
kind: "Parsed",
|
|
5321
|
+
tokensIn: usage.tokensIn,
|
|
5322
|
+
tokensOut: usage.tokensOut,
|
|
5323
|
+
contextUsed: usage.contextUsed,
|
|
5324
|
+
contextWindow: usage.contextWindow,
|
|
5325
|
+
text: null,
|
|
5326
|
+
approvalState: null,
|
|
5327
|
+
verdict: null
|
|
5328
|
+
}
|
|
5329
|
+
]);
|
|
5330
|
+
return process.exit(0);
|
|
5331
|
+
} catch {
|
|
5332
|
+
return process.exit(0);
|
|
5333
|
+
}
|
|
5334
|
+
});
|
|
5335
|
+
telemetry.command("install").description(
|
|
5336
|
+
"Wire the per-turn telemetry Stop hook into Claude Code settings (also folded into `sechroom hook install`)"
|
|
5337
|
+
).option(
|
|
5338
|
+
"--scope <scope>",
|
|
5339
|
+
"global (config dir / CLAUDE_CONFIG_DIR) or project (<cwd>/.claude) \u2014 default global"
|
|
5340
|
+
).option("--local", "alias for --scope project").option("--dry-run", "Print what would change; write nothing").action((opts, cmd) => {
|
|
5341
|
+
const g = cmd.optsWithGlobals();
|
|
5342
|
+
const dryRun = Boolean(opts.dryRun);
|
|
5343
|
+
const cwd = process.cwd();
|
|
5344
|
+
let scope;
|
|
5345
|
+
try {
|
|
5346
|
+
scope = opts.local ? "project" : resolveScope(opts.scope);
|
|
5347
|
+
} catch (err2) {
|
|
5348
|
+
process.stderr.write(`${err2.message}
|
|
5349
|
+
`);
|
|
5350
|
+
return process.exit(2);
|
|
5351
|
+
}
|
|
5352
|
+
const targets = resolveClaudeTargets({
|
|
5353
|
+
override: g.claudeConfigDir,
|
|
5354
|
+
scope,
|
|
5355
|
+
cwd
|
|
5356
|
+
});
|
|
5357
|
+
const commands = { Stop: "sechroom telemetry hook" };
|
|
5358
|
+
try {
|
|
5359
|
+
const multi = targets.length > 1;
|
|
5360
|
+
const results = targets.map((t) => {
|
|
5361
|
+
const r = installClaudeCommands(t.dir, commands, dryRun);
|
|
5362
|
+
process.stdout.write(
|
|
5363
|
+
`${HOOK_SURFACE_LABEL.claude}${multi ? ` (${t.label})` : ""}:
|
|
5364
|
+
`
|
|
5365
|
+
);
|
|
5366
|
+
process.stdout.write(describe(r, dryRun) + "\n");
|
|
5367
|
+
return r;
|
|
5368
|
+
});
|
|
5369
|
+
if (dryRun) {
|
|
5370
|
+
process.stdout.write("\n(dry run \u2014 no files were written.)\n");
|
|
5371
|
+
} else if (results.every((r) => r.status === "current")) {
|
|
5372
|
+
process.stdout.write("\nAlready up to date \u2014 nothing to change.\n");
|
|
5373
|
+
} else {
|
|
5374
|
+
process.stdout.write(
|
|
5375
|
+
"\nRestart your agent for the hook to take effect, then bind a task with `sechroom telemetry bind`.\n"
|
|
5376
|
+
);
|
|
5377
|
+
}
|
|
5378
|
+
} catch (err2) {
|
|
5379
|
+
process.stderr.write(
|
|
5380
|
+
`telemetry install failed: ${err2.message}
|
|
5381
|
+
`
|
|
5382
|
+
);
|
|
5383
|
+
return process.exit(1);
|
|
5384
|
+
}
|
|
5385
|
+
warnIfSechroomNotOnPath();
|
|
5386
|
+
return process.exit(0);
|
|
5387
|
+
});
|
|
5388
|
+
}
|
|
5389
|
+
var BINDING_FILE = "telemetry.json";
|
|
5390
|
+
async function postTelemetry(cfg, decompositionId, events) {
|
|
5391
|
+
const token = await requireToken(cfg);
|
|
5392
|
+
const resp = await fetch(
|
|
5393
|
+
`${cfg.baseUrl}/decompositions/${encodeURIComponent(decompositionId)}/run/telemetry`,
|
|
5394
|
+
{
|
|
5395
|
+
method: "POST",
|
|
5396
|
+
headers: {
|
|
5397
|
+
authorization: `Bearer ${token}`,
|
|
5398
|
+
tenant: cfg.tenant,
|
|
5399
|
+
"content-type": "application/json",
|
|
5400
|
+
"x-sechroom-surface": "cli"
|
|
5401
|
+
},
|
|
5402
|
+
body: JSON.stringify({ events })
|
|
5403
|
+
}
|
|
5404
|
+
);
|
|
5405
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);
|
|
5406
|
+
return await resp.json();
|
|
5407
|
+
}
|
|
5408
|
+
function findBinding(start) {
|
|
5409
|
+
let dir = start;
|
|
5410
|
+
for (; ; ) {
|
|
5411
|
+
const path = join16(dir, ".sechroom", BINDING_FILE);
|
|
5412
|
+
if (existsSync14(path)) {
|
|
5413
|
+
try {
|
|
5414
|
+
const b = JSON.parse(
|
|
5415
|
+
readFileSync9(path, "utf8")
|
|
5416
|
+
);
|
|
5417
|
+
if (b.decompositionId && b.taskId)
|
|
5418
|
+
return { decompositionId: b.decompositionId, taskId: b.taskId };
|
|
5419
|
+
} catch {
|
|
5420
|
+
}
|
|
5421
|
+
return null;
|
|
5422
|
+
}
|
|
5423
|
+
const parent = dirname9(dir);
|
|
5424
|
+
if (parent === dir) return null;
|
|
5425
|
+
dir = parent;
|
|
5426
|
+
}
|
|
5427
|
+
}
|
|
5428
|
+
function parseTranscript(path) {
|
|
5429
|
+
if (!existsSync14(path)) return null;
|
|
5430
|
+
let tokensIn = 0;
|
|
5431
|
+
let tokensOut = 0;
|
|
5432
|
+
let contextUsed = 0;
|
|
5433
|
+
let model = "";
|
|
5434
|
+
for (const line of readFileSync9(path, "utf8").split("\n")) {
|
|
5435
|
+
if (!line.trim()) continue;
|
|
5436
|
+
let obj;
|
|
5437
|
+
try {
|
|
5438
|
+
obj = JSON.parse(line);
|
|
5439
|
+
} catch {
|
|
5440
|
+
continue;
|
|
5441
|
+
}
|
|
5442
|
+
const usage = obj.type === "assistant" ? obj.message?.usage : void 0;
|
|
5443
|
+
if (!usage) continue;
|
|
5444
|
+
const input = (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
|
|
5445
|
+
tokensIn += input;
|
|
5446
|
+
tokensOut += usage.output_tokens ?? 0;
|
|
5447
|
+
contextUsed = input;
|
|
5448
|
+
if (obj.message?.model) model = obj.message.model;
|
|
5449
|
+
}
|
|
5450
|
+
if (tokensIn === 0 && tokensOut === 0) return null;
|
|
5451
|
+
return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model) };
|
|
5452
|
+
}
|
|
5453
|
+
function windowFor(model) {
|
|
5454
|
+
const m = model.toLowerCase();
|
|
5455
|
+
return m.includes("[1m]") || m.includes("-1m") ? 1e6 : 2e5;
|
|
5456
|
+
}
|
|
5457
|
+
async function readStdin2() {
|
|
5458
|
+
if (process.stdin.isTTY) return "";
|
|
5459
|
+
const chunks = [];
|
|
5460
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
5461
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
5462
|
+
}
|
|
5463
|
+
function parseHookInput2(raw) {
|
|
5464
|
+
if (!raw.trim()) return {};
|
|
5465
|
+
try {
|
|
5466
|
+
return JSON.parse(raw);
|
|
5467
|
+
} catch {
|
|
5468
|
+
return {};
|
|
5469
|
+
}
|
|
5298
5470
|
}
|
|
5299
5471
|
var KINDS = {
|
|
5300
5472
|
raw: "Raw",
|
|
@@ -5516,7 +5688,7 @@ Examples:
|
|
|
5516
5688
|
function resolveVersion() {
|
|
5517
5689
|
try {
|
|
5518
5690
|
const pkg = JSON.parse(
|
|
5519
|
-
|
|
5691
|
+
readFileSync10(new URL("../package.json", import.meta.url), "utf8")
|
|
5520
5692
|
);
|
|
5521
5693
|
return pkg.version ?? "0.0.0";
|
|
5522
5694
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sechroom/cli",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.7.1",
|
|
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",
|
|
@@ -24,11 +24,12 @@
|
|
|
24
24
|
"registry": "https://registry.npmjs.org/"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
+
"@clack/prompts": "^1.6.0",
|
|
27
28
|
"@microsoft/signalr": "^10.0.0",
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
28
30
|
"commander": "^12.1.0",
|
|
29
31
|
"open": "^10.1.0",
|
|
30
|
-
"openapi-fetch": "^0.13.0"
|
|
31
|
-
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
32
|
+
"openapi-fetch": "^0.13.0"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
34
35
|
"@types/node": "^20.14.0",
|