@sechroom/cli 2026.7.5 → 2026.7.6-rc.a2828c37
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 +567 -56
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/auth.ts
|
|
@@ -439,6 +439,9 @@ var quiet = false;
|
|
|
439
439
|
function setQuiet(q) {
|
|
440
440
|
quiet = q;
|
|
441
441
|
}
|
|
442
|
+
function isQuiet() {
|
|
443
|
+
return quiet;
|
|
444
|
+
}
|
|
442
445
|
function colorOn() {
|
|
443
446
|
return !quiet && !process.env.NO_COLOR && process.env.FORCE_COLOR !== "0" && Boolean(process.stdout.isTTY);
|
|
444
447
|
}
|
|
@@ -615,6 +618,22 @@ function emitAction(summary, data, json) {
|
|
|
615
618
|
process.stdout.write(`${ok("\u2713")} ${summary}
|
|
616
619
|
`);
|
|
617
620
|
}
|
|
621
|
+
var GOVERNANCE_QUEUED_PROBLEM_TYPE = "https://sechroom.dev/problems/governance-review-queued";
|
|
622
|
+
function isGovernanceQueued(body) {
|
|
623
|
+
return typeof body === "object" && body !== null && "type" in body && body.type === GOVERNANCE_QUEUED_PROBLEM_TYPE;
|
|
624
|
+
}
|
|
625
|
+
function formatQueuedForApproval(body, json) {
|
|
626
|
+
if (json) return JSON.stringify(body) + "\n";
|
|
627
|
+
const b = body ?? {};
|
|
628
|
+
const detail = typeof b.detail === "string" ? b.detail : void 0;
|
|
629
|
+
const requestId = typeof b.requestId === "string" ? b.requestId : void 0;
|
|
630
|
+
const message = detail ?? "This change requires operator approval before it takes effect.";
|
|
631
|
+
let out = `${warn("\u29D7")} Pending approval \u2014 ${message}
|
|
632
|
+
`;
|
|
633
|
+
if (requestId) out += ` ${style.dim(`request: ${requestId}`)}
|
|
634
|
+
`;
|
|
635
|
+
return out;
|
|
636
|
+
}
|
|
618
637
|
async function runApi(label, fn) {
|
|
619
638
|
const s = spinner(label);
|
|
620
639
|
let res;
|
|
@@ -624,6 +643,12 @@ async function runApi(label, fn) {
|
|
|
624
643
|
s.fail();
|
|
625
644
|
fail(err2);
|
|
626
645
|
}
|
|
646
|
+
const queuedBody = res.data ?? res.error;
|
|
647
|
+
if (res.response?.status === 202 && isGovernanceQueued(queuedBody)) {
|
|
648
|
+
s.stop();
|
|
649
|
+
process.stdout.write(formatQueuedForApproval(queuedBody, isQuiet()));
|
|
650
|
+
process.exit(0);
|
|
651
|
+
}
|
|
627
652
|
const httpFailed = res.response !== void 0 && !res.response.ok;
|
|
628
653
|
if (res.error !== void 0 && res.error !== null || httpFailed) {
|
|
629
654
|
s.fail();
|
|
@@ -633,7 +658,22 @@ async function runApi(label, fn) {
|
|
|
633
658
|
return res.data;
|
|
634
659
|
}
|
|
635
660
|
function fail(error) {
|
|
636
|
-
|
|
661
|
+
let msg;
|
|
662
|
+
if (typeof error === "object" && error !== null && "title" in error) {
|
|
663
|
+
const problem = error;
|
|
664
|
+
msg = String(problem.title);
|
|
665
|
+
if (problem.errors && typeof problem.errors === "object") {
|
|
666
|
+
const detail = Object.entries(problem.errors).flatMap(
|
|
667
|
+
([field, msgs]) => (Array.isArray(msgs) ? msgs : [msgs]).map((m) => ` ${field}: ${String(m)}`)
|
|
668
|
+
);
|
|
669
|
+
if (detail.length > 0) msg += `
|
|
670
|
+
${detail.join("\n")}`;
|
|
671
|
+
}
|
|
672
|
+
} else if (typeof error === "object" && error !== null) {
|
|
673
|
+
msg = JSON.stringify(error);
|
|
674
|
+
} else {
|
|
675
|
+
msg = String(error);
|
|
676
|
+
}
|
|
637
677
|
process.stderr.write(`error: ${msg}
|
|
638
678
|
`);
|
|
639
679
|
process.exit(1);
|
|
@@ -1388,10 +1428,15 @@ var CLAUDE_HOOK_COMMANDS = {
|
|
|
1388
1428
|
SessionStart: "sechroom hook session-start",
|
|
1389
1429
|
PreCompact: "sechroom hook pre-compact",
|
|
1390
1430
|
SessionEnd: "sechroom hook session-end",
|
|
1391
|
-
// WLP telemetry tap (D-WLP-10) — per-turn executor self-report.
|
|
1392
|
-
//
|
|
1393
|
-
//
|
|
1394
|
-
|
|
1431
|
+
// WLP telemetry tap (D-WLP-10 + FR-352 Tier 1) — per-turn executor self-report. The one
|
|
1432
|
+
// `telemetry hook` verb dispatches on hook_event_name: Stop/SubagentStop → parsed (token/context) +
|
|
1433
|
+
// terminal (turn end), Notification/PermissionDenied → approval. No-op (exit 0) unless this checkout
|
|
1434
|
+
// is bound via `sechroom telemetry bind`, so it's safe to wire for every Claude install; an event a
|
|
1435
|
+
// given Claude Code version doesn't know is inert (never fires). Claude-only.
|
|
1436
|
+
Stop: "sechroom telemetry hook",
|
|
1437
|
+
SubagentStop: "sechroom telemetry hook",
|
|
1438
|
+
Notification: "sechroom telemetry hook",
|
|
1439
|
+
PermissionDenied: "sechroom telemetry hook"
|
|
1395
1440
|
};
|
|
1396
1441
|
var CODEX_HOOK_COMMANDS = {
|
|
1397
1442
|
SessionStart: "sechroom hook session-start",
|
|
@@ -1544,11 +1589,16 @@ function registerChannel(program2) {
|
|
|
1544
1589
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1545
1590
|
const filter = readFilter(opts);
|
|
1546
1591
|
const sub = await ensureSubscription(cfg, opts.name, filter);
|
|
1547
|
-
const
|
|
1548
|
-
|
|
1592
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1593
|
+
const deliver = makeDeliver(
|
|
1594
|
+
filter,
|
|
1595
|
+
seen,
|
|
1596
|
+
(payload) => process.stdout.write(
|
|
1549
1597
|
(typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
|
|
1550
|
-
)
|
|
1551
|
-
|
|
1598
|
+
)
|
|
1599
|
+
);
|
|
1600
|
+
const conn = await openConnection(cfg, deliver);
|
|
1601
|
+
await reconcile(cfg, filter, deliver);
|
|
1552
1602
|
if (json) {
|
|
1553
1603
|
emit(
|
|
1554
1604
|
{
|
|
@@ -1585,7 +1635,8 @@ function registerChannel(program2) {
|
|
|
1585
1635
|
);
|
|
1586
1636
|
await mcp.connect(new StdioServerTransport());
|
|
1587
1637
|
await ensureSubscription(cfg, opts.name, filter);
|
|
1588
|
-
const
|
|
1638
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1639
|
+
const deliver = makeDeliver(filter, seen, (payload) => {
|
|
1589
1640
|
const { content, meta } = summarizeEvent(payload);
|
|
1590
1641
|
void mcp.notification({
|
|
1591
1642
|
method: "notifications/claude/channel",
|
|
@@ -1595,6 +1646,8 @@ function registerChannel(program2) {
|
|
|
1595
1646
|
`))
|
|
1596
1647
|
);
|
|
1597
1648
|
});
|
|
1649
|
+
const conn = await openConnection(cfg, deliver);
|
|
1650
|
+
await reconcile(cfg, filter, deliver);
|
|
1598
1651
|
process.stderr.write(
|
|
1599
1652
|
style.dim(
|
|
1600
1653
|
`sechroom channel (mcp) \u2014 tenant ${cfg.tenant}, tags [${filter.tags.join(", ")}]
|
|
@@ -1605,7 +1658,10 @@ function registerChannel(program2) {
|
|
|
1605
1658
|
});
|
|
1606
1659
|
channel.command("install").description(
|
|
1607
1660
|
"Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
|
|
1608
|
-
).option(
|
|
1661
|
+
).option(
|
|
1662
|
+
"--workspace <wsp...>",
|
|
1663
|
+
"Restrict dispatches to these workspace id(s)"
|
|
1664
|
+
).option(
|
|
1609
1665
|
"--tag <tag...>",
|
|
1610
1666
|
"Tag(s) a dispatch must carry to match (repeatable). Default targets WLP task dispatches.",
|
|
1611
1667
|
["kind:task"]
|
|
@@ -1726,20 +1782,134 @@ function holdOpen(conn) {
|
|
|
1726
1782
|
process.on("SIGTERM", stop);
|
|
1727
1783
|
});
|
|
1728
1784
|
}
|
|
1729
|
-
function
|
|
1785
|
+
function parseEvent(payload) {
|
|
1730
1786
|
let data = payload;
|
|
1731
1787
|
if (typeof payload === "string") {
|
|
1732
1788
|
try {
|
|
1733
1789
|
data = JSON.parse(payload);
|
|
1734
1790
|
} catch {
|
|
1735
|
-
return {
|
|
1791
|
+
return { eventType: "", memoryId: "", workspaceId: "", tags: void 0 };
|
|
1736
1792
|
}
|
|
1737
1793
|
}
|
|
1738
1794
|
const obj = data ?? {};
|
|
1739
1795
|
const inner = obj.data ?? obj;
|
|
1740
|
-
const
|
|
1741
|
-
|
|
1742
|
-
|
|
1796
|
+
const rawTags = inner.tags ?? inner.Tags;
|
|
1797
|
+
return {
|
|
1798
|
+
eventType: str(inner.eventType ?? inner.EventType ?? obj.type) || "substrate.event",
|
|
1799
|
+
memoryId: str(inner.memoryId ?? inner.MemoryId),
|
|
1800
|
+
workspaceId: str(inner.workspaceId ?? inner.WorkspaceId),
|
|
1801
|
+
tags: Array.isArray(rawTags) ? rawTags.filter((t) => typeof t === "string") : void 0
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
function shouldDeliver(payload, filter) {
|
|
1805
|
+
const { workspaceId, tags } = parseEvent(payload);
|
|
1806
|
+
if (filter.workspaceScope.length > 0 && (!workspaceId || !filter.workspaceScope.includes(workspaceId)))
|
|
1807
|
+
return false;
|
|
1808
|
+
if (filter.tags.length > 0) {
|
|
1809
|
+
if (!tags) return false;
|
|
1810
|
+
return facetedTagMatch(tags, filter.tags);
|
|
1811
|
+
}
|
|
1812
|
+
return true;
|
|
1813
|
+
}
|
|
1814
|
+
function makeDeliver(filter, seen, forward) {
|
|
1815
|
+
return (payload) => {
|
|
1816
|
+
if (!shouldDeliver(payload, filter)) return;
|
|
1817
|
+
const { memoryId } = parseEvent(payload);
|
|
1818
|
+
if (memoryId) {
|
|
1819
|
+
if (seen.has(memoryId)) return;
|
|
1820
|
+
seen.add(memoryId);
|
|
1821
|
+
}
|
|
1822
|
+
forward(payload);
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
async function reconcile(cfg, filter, deliver) {
|
|
1826
|
+
if (filter.workspaceScope.length === 0) {
|
|
1827
|
+
process.stderr.write(
|
|
1828
|
+
style.dim(
|
|
1829
|
+
"channel: no --workspace to reconcile against; live feed only (a dropped dispatch won't be recovered).\n"
|
|
1830
|
+
)
|
|
1831
|
+
);
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
if (filter.tags.length === 0) return;
|
|
1835
|
+
let token;
|
|
1836
|
+
try {
|
|
1837
|
+
token = await requireToken(cfg);
|
|
1838
|
+
} catch {
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
const qs = `filterTags=${encodeURIComponent(filter.tags.join(","))}&limit=100`;
|
|
1842
|
+
let recovered = 0;
|
|
1843
|
+
for (const ws of filter.workspaceScope) {
|
|
1844
|
+
try {
|
|
1845
|
+
const resp = await fetch(
|
|
1846
|
+
`${cfg.baseUrl}/workspaces/${encodeURIComponent(ws)}/memories/feed?${qs}`,
|
|
1847
|
+
{
|
|
1848
|
+
headers: {
|
|
1849
|
+
authorization: `Bearer ${token}`,
|
|
1850
|
+
tenant: cfg.tenant,
|
|
1851
|
+
"x-sechroom-surface": "cli"
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
);
|
|
1855
|
+
if (!resp.ok) {
|
|
1856
|
+
process.stderr.write(
|
|
1857
|
+
err(
|
|
1858
|
+
`channel: reconcile query for ${ws} failed (HTTP ${resp.status})
|
|
1859
|
+
`
|
|
1860
|
+
)
|
|
1861
|
+
);
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
const data = await resp.json();
|
|
1865
|
+
for (const m of data.results ?? []) {
|
|
1866
|
+
if (!m.id) continue;
|
|
1867
|
+
deliver({
|
|
1868
|
+
eventType: "reconcile",
|
|
1869
|
+
memoryId: m.id,
|
|
1870
|
+
workspaceId: ws,
|
|
1871
|
+
tags: m.tags ?? []
|
|
1872
|
+
});
|
|
1873
|
+
recovered++;
|
|
1874
|
+
}
|
|
1875
|
+
} catch (e) {
|
|
1876
|
+
process.stderr.write(
|
|
1877
|
+
err(`channel: reconcile error for ${ws}: ${String(e)}
|
|
1878
|
+
`)
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
if (recovered > 0)
|
|
1883
|
+
process.stderr.write(
|
|
1884
|
+
style.dim(
|
|
1885
|
+
`channel: reconciled ${recovered} already-queued event(s) on connect.
|
|
1886
|
+
`
|
|
1887
|
+
)
|
|
1888
|
+
);
|
|
1889
|
+
}
|
|
1890
|
+
function facetedTagMatch(eventTags, filterTags) {
|
|
1891
|
+
const have = new Set(eventTags);
|
|
1892
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1893
|
+
for (const f of filterTags) {
|
|
1894
|
+
const ns = f.endsWith(":*") ? f.slice(0, -2) : namespaceOf(f);
|
|
1895
|
+
const group = groups.get(ns) ?? [];
|
|
1896
|
+
group.push(f);
|
|
1897
|
+
groups.set(ns, group);
|
|
1898
|
+
}
|
|
1899
|
+
for (const [ns, group] of groups) {
|
|
1900
|
+
const ok2 = group.some(
|
|
1901
|
+
(f) => f.endsWith(":*") ? eventTags.some((t) => namespaceOf(t) === ns) : have.has(f)
|
|
1902
|
+
);
|
|
1903
|
+
if (!ok2) return false;
|
|
1904
|
+
}
|
|
1905
|
+
return true;
|
|
1906
|
+
}
|
|
1907
|
+
function namespaceOf(tag) {
|
|
1908
|
+
const i = tag.indexOf(":");
|
|
1909
|
+
return i >= 0 ? tag.slice(0, i) : tag;
|
|
1910
|
+
}
|
|
1911
|
+
function summarizeEvent(payload) {
|
|
1912
|
+
const { eventType, memoryId, workspaceId } = parseEvent(payload);
|
|
1743
1913
|
const content = memoryId ? `${eventType}: ${memoryId}${workspaceId ? ` (workspace ${workspaceId})` : ""}` : typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
1744
1914
|
const meta = {};
|
|
1745
1915
|
if (eventType) meta.event_type = eventType;
|
|
@@ -2408,6 +2578,178 @@ Examples:
|
|
|
2408
2578
|
});
|
|
2409
2579
|
}
|
|
2410
2580
|
|
|
2581
|
+
// src/commands/close.ts
|
|
2582
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
2583
|
+
var VERDICTS = ["pass", "soft-fail", "plan-invalid", "blocked"];
|
|
2584
|
+
function registerClose(program2) {
|
|
2585
|
+
program2.command("close").description(
|
|
2586
|
+
"Close a dispatched WorkTask in one call: closeout memo (verdict + correlation) + Reference edge + source status flip. Managed runs stamp the run's matchTags verbatim (SBC-1180)."
|
|
2587
|
+
).requiredOption("--task <id>", "The WorkTask memory id being closed").requiredOption("--verdict <verdict>", `Verdict: ${VERDICTS.join(" | ")}`).requiredOption(
|
|
2588
|
+
"--workspace <wsp>",
|
|
2589
|
+
"Workspace that owns the closeout memo"
|
|
2590
|
+
).requiredOption("--title <title>", "Closeout title").option(
|
|
2591
|
+
"--file <path>",
|
|
2592
|
+
"Read the closeout body from a file (default: stdin)"
|
|
2593
|
+
).option(
|
|
2594
|
+
"--decomposition <id>",
|
|
2595
|
+
"Managed-run selector \u2014 the sug_ decomposition the task belongs to. Locates the parked run so its matchTags/verdict contract are read from state, not assembled from args."
|
|
2596
|
+
).option(
|
|
2597
|
+
"--no-status-flip",
|
|
2598
|
+
"Skip the status flip on a BARE task. (Managed runs never flip here \u2014 the run engine reflects the verdict onto the task on resume.)"
|
|
2599
|
+
).option(
|
|
2600
|
+
"--to-version <n>",
|
|
2601
|
+
"Pin the Reference edge at this task version \u2014 the DISPATCHED version the executor worked against (D-continuity-2). Default: the task's current version (status flips are metadata edits that don't bump the version, so current == dispatched in the normal case; pass this when the task's content changed between dispatch and close)."
|
|
2602
|
+
).option("--source <source>", "Source / lane stamp", "cli").addHelpText(
|
|
2603
|
+
"after",
|
|
2604
|
+
`
|
|
2605
|
+
Examples:
|
|
2606
|
+
# Managed run \u2014 reads matchTags from run state, stamps them verbatim, un-parks the run:
|
|
2607
|
+
$ echo "Shipped X. Tests green." | sechroom close --task mem_XXXX --decomposition sug_YYYY \\
|
|
2608
|
+
--verdict pass --workspace wsp_ZZZZ --title "w4_build closeout"
|
|
2609
|
+
|
|
2610
|
+
# Bare task:
|
|
2611
|
+
$ sechroom close --task mem_XXXX --verdict pass --workspace wsp_ZZZZ \\
|
|
2612
|
+
--title "done" --file ./closeout.md`
|
|
2613
|
+
).action(async (opts, cmd) => {
|
|
2614
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2615
|
+
const json = cmd.optsWithGlobals().json;
|
|
2616
|
+
const verdict = String(opts.verdict);
|
|
2617
|
+
if (!VERDICTS.includes(verdict))
|
|
2618
|
+
fail(
|
|
2619
|
+
`--verdict must be one of ${VERDICTS.join(" | ")} \u2014 got '${opts.verdict}'`
|
|
2620
|
+
);
|
|
2621
|
+
let bodyText;
|
|
2622
|
+
try {
|
|
2623
|
+
bodyText = opts.file ? readFileSync7(opts.file, "utf8") : readFileSync7(0, "utf8");
|
|
2624
|
+
} catch {
|
|
2625
|
+
fail(
|
|
2626
|
+
opts.file ? `could not read --file ${opts.file}` : "no closeout body \u2014 pass --file <path> or pipe it on stdin"
|
|
2627
|
+
);
|
|
2628
|
+
}
|
|
2629
|
+
if (!bodyText.trim())
|
|
2630
|
+
fail(
|
|
2631
|
+
"closeout body is empty \u2014 pass --file <path> or pipe the body on stdin"
|
|
2632
|
+
);
|
|
2633
|
+
const client = await makeClient(cfg);
|
|
2634
|
+
let matchTags;
|
|
2635
|
+
let dispatchedVersion;
|
|
2636
|
+
if (opts.decomposition) {
|
|
2637
|
+
const run = await runApi(
|
|
2638
|
+
"Reading run contract",
|
|
2639
|
+
async () => client.GET("/decompositions/{id}/run", {
|
|
2640
|
+
params: { path: { id: opts.decomposition } }
|
|
2641
|
+
})
|
|
2642
|
+
);
|
|
2643
|
+
const await_ = run.awaitingCompletion;
|
|
2644
|
+
if (!await_ || !Array.isArray(await_.matchTags) || await_.matchTags.length === 0)
|
|
2645
|
+
fail(
|
|
2646
|
+
`decomposition ${opts.decomposition} has no parked run awaiting a completion \u2014 nothing to close (run outcome: ${run.outcome ?? "unknown"}). Idempotent no-op on an already-resumed/terminal run.`
|
|
2647
|
+
);
|
|
2648
|
+
if (run.awaitingTaskId && run.awaitingTaskId !== opts.task)
|
|
2649
|
+
fail(
|
|
2650
|
+
`run ${opts.decomposition} is awaiting task ${run.awaitingTaskId}, not ${opts.task} \u2014 refusing to mis-key the resume.`
|
|
2651
|
+
);
|
|
2652
|
+
matchTags = await_.matchTags;
|
|
2653
|
+
if (typeof await_.dispatchedTaskVersion === "number")
|
|
2654
|
+
dispatchedVersion = await_.dispatchedTaskVersion;
|
|
2655
|
+
} else {
|
|
2656
|
+
matchTags = [`wlp-task:${opts.task}`];
|
|
2657
|
+
}
|
|
2658
|
+
const tags = [
|
|
2659
|
+
.../* @__PURE__ */ new Set([
|
|
2660
|
+
...matchTags,
|
|
2661
|
+
`wlp-task:${opts.task}`,
|
|
2662
|
+
`verdict:${verdict}`
|
|
2663
|
+
])
|
|
2664
|
+
];
|
|
2665
|
+
const closeout = await runApi(
|
|
2666
|
+
"Creating closeout",
|
|
2667
|
+
async () => client.POST("/memories", {
|
|
2668
|
+
body: {
|
|
2669
|
+
text: bodyText,
|
|
2670
|
+
type: "reference",
|
|
2671
|
+
content: "{}",
|
|
2672
|
+
confidence: 1,
|
|
2673
|
+
source: opts.source,
|
|
2674
|
+
archetype: "Document",
|
|
2675
|
+
title: opts.title,
|
|
2676
|
+
tags,
|
|
2677
|
+
owner: { type: "Workspace", id: opts.workspace }
|
|
2678
|
+
}
|
|
2679
|
+
})
|
|
2680
|
+
);
|
|
2681
|
+
let toVersion;
|
|
2682
|
+
if (opts.toVersion !== void 0) {
|
|
2683
|
+
toVersion = Number(opts.toVersion);
|
|
2684
|
+
if (!Number.isInteger(toVersion) || toVersion < 1)
|
|
2685
|
+
fail(
|
|
2686
|
+
`--to-version must be an integer >= 1 \u2014 got '${opts.toVersion}'`
|
|
2687
|
+
);
|
|
2688
|
+
} else if (dispatchedVersion !== void 0) {
|
|
2689
|
+
toVersion = dispatchedVersion;
|
|
2690
|
+
} else {
|
|
2691
|
+
const task = await runApi(
|
|
2692
|
+
"Reading task version",
|
|
2693
|
+
async () => client.GET("/memories/{memoryId}", {
|
|
2694
|
+
params: { path: { memoryId: opts.task } }
|
|
2695
|
+
})
|
|
2696
|
+
);
|
|
2697
|
+
const v = task?.item?.currentVersion ?? task?.currentVersion;
|
|
2698
|
+
if (typeof v !== "number" || v < 1)
|
|
2699
|
+
fail(
|
|
2700
|
+
`could not read task ${opts.task} version to pin the Reference edge \u2014 pass --to-version <n>`
|
|
2701
|
+
);
|
|
2702
|
+
toVersion = v;
|
|
2703
|
+
}
|
|
2704
|
+
const edge = await runApi(
|
|
2705
|
+
"Wiring Reference edge",
|
|
2706
|
+
async () => client.POST("/memories/{memoryId}/relationships", {
|
|
2707
|
+
params: { path: { memoryId: closeout.id } },
|
|
2708
|
+
body: { toMemoryId: opts.task, type: "Reference", toVersion }
|
|
2709
|
+
})
|
|
2710
|
+
);
|
|
2711
|
+
let taskStatus;
|
|
2712
|
+
if (opts.statusFlip !== false && !opts.decomposition) {
|
|
2713
|
+
const current = await runApi(
|
|
2714
|
+
"Reading task tags",
|
|
2715
|
+
async () => client.GET("/memories/{memoryId}", {
|
|
2716
|
+
params: { path: { memoryId: opts.task } }
|
|
2717
|
+
})
|
|
2718
|
+
);
|
|
2719
|
+
const currentTags = current?.item?.tags ?? current?.tags ?? [];
|
|
2720
|
+
const target = verdict === "blocked" ? "status:blocked" : "status:done";
|
|
2721
|
+
const nextTags = [
|
|
2722
|
+
...new Set(currentTags.filter((t) => !t.startsWith("status:")))
|
|
2723
|
+
].concat(target);
|
|
2724
|
+
await runApi(
|
|
2725
|
+
"Flipping task status",
|
|
2726
|
+
async () => client.PATCH("/memories/{memoryId}/metadata", {
|
|
2727
|
+
params: { path: { memoryId: opts.task } },
|
|
2728
|
+
body: {
|
|
2729
|
+
memoryId: opts.task,
|
|
2730
|
+
source: opts.source,
|
|
2731
|
+
tags: nextTags
|
|
2732
|
+
}
|
|
2733
|
+
})
|
|
2734
|
+
);
|
|
2735
|
+
taskStatus = target.slice("status:".length);
|
|
2736
|
+
}
|
|
2737
|
+
const view = resolveViewUrl(cfg.baseUrl, closeout.url);
|
|
2738
|
+
const result = {
|
|
2739
|
+
closeoutId: closeout.id,
|
|
2740
|
+
edgeId: edge.id,
|
|
2741
|
+
taskStatus: taskStatus ?? null,
|
|
2742
|
+
verdict,
|
|
2743
|
+
tags
|
|
2744
|
+
};
|
|
2745
|
+
emitAction(
|
|
2746
|
+
`closed ${style.bold(opts.task)} verdict:${style.bold(verdict)} \u2192 closeout ${style.bold(closeout.id)}${taskStatus ? ` ${style.dim(`(task ${taskStatus})`)}` : ""}${view ? ` ${style.dim("\u2192")} ${view}` : ""}`,
|
|
2747
|
+
result,
|
|
2748
|
+
json
|
|
2749
|
+
);
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2411
2753
|
// src/commands/continuity.ts
|
|
2412
2754
|
function registerContinuity(program2) {
|
|
2413
2755
|
const continuity = program2.command("continuity").description("Continuity snapshots: checkpoint and resume work");
|
|
@@ -2568,6 +2910,108 @@ Examples:
|
|
|
2568
2910
|
});
|
|
2569
2911
|
}
|
|
2570
2912
|
|
|
2913
|
+
// src/commands/decomposition.ts
|
|
2914
|
+
function registerDecomposition(program2) {
|
|
2915
|
+
const decomposition = program2.command("decomposition").description(
|
|
2916
|
+
"Drive a WLP decomposition: decompose a brief, then execute / accept / reject"
|
|
2917
|
+
);
|
|
2918
|
+
decomposition.addHelpText(
|
|
2919
|
+
"after",
|
|
2920
|
+
`
|
|
2921
|
+
Examples:
|
|
2922
|
+
$ sechroom decomposition decompose mem_XXXX
|
|
2923
|
+
$ sechroom decomposition execute sug_XXXX
|
|
2924
|
+
$ sechroom decomposition publish-run sug_XXXX
|
|
2925
|
+
$ sechroom decomposition accept sug_XXXX
|
|
2926
|
+
$ sechroom decomposition reject sug_XXXX --reason "wrong shape"`
|
|
2927
|
+
);
|
|
2928
|
+
decomposition.command("decompose <briefId>").description(
|
|
2929
|
+
"Decompose a work brief into a candidate Task graph (POST /work-briefs/{id}/decompose)"
|
|
2930
|
+
).action(async (briefId, _opts, cmd) => {
|
|
2931
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2932
|
+
const data = await runApi("Queueing decomposition", async () => {
|
|
2933
|
+
const client = await makeClient(cfg);
|
|
2934
|
+
return client.POST("/work-briefs/{id}/decompose", {
|
|
2935
|
+
params: { path: { id: briefId } },
|
|
2936
|
+
body: { id: briefId }
|
|
2937
|
+
});
|
|
2938
|
+
});
|
|
2939
|
+
emitAction(
|
|
2940
|
+
`queued decomposition of ${style.bold(briefId)} \u2192 ${style.bold(data.suggestionId)}`,
|
|
2941
|
+
data,
|
|
2942
|
+
cmd.optsWithGlobals().json
|
|
2943
|
+
);
|
|
2944
|
+
});
|
|
2945
|
+
decomposition.command("execute <decompositionId>").description(
|
|
2946
|
+
"Execute a decomposition's Task graph (POST /decompositions/{id}/execute)"
|
|
2947
|
+
).action(async (decompositionId, _opts, cmd) => {
|
|
2948
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2949
|
+
const data = await runApi("Executing decomposition", async () => {
|
|
2950
|
+
const client = await makeClient(cfg);
|
|
2951
|
+
return client.POST("/decompositions/{id}/execute", {
|
|
2952
|
+
params: { path: { id: decompositionId } },
|
|
2953
|
+
body: {}
|
|
2954
|
+
});
|
|
2955
|
+
});
|
|
2956
|
+
emitAction(
|
|
2957
|
+
`executed ${style.bold(decompositionId)} \u2192 run ${style.bold(data.runRecordId)} (${data.outcome})`,
|
|
2958
|
+
data,
|
|
2959
|
+
cmd.optsWithGlobals().json
|
|
2960
|
+
);
|
|
2961
|
+
});
|
|
2962
|
+
decomposition.command("publish-run <decompositionId>").description(
|
|
2963
|
+
"Publish an accepted decomposition's context pack on demand (POST /decompositions/{id}/publish-run)"
|
|
2964
|
+
).action(async (decompositionId, _opts, cmd) => {
|
|
2965
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2966
|
+
const data = await runApi("Publishing context pack", async () => {
|
|
2967
|
+
const client = await makeClient(cfg);
|
|
2968
|
+
return client.POST("/decompositions/{id}/publish-run", {
|
|
2969
|
+
params: { path: { id: decompositionId } },
|
|
2970
|
+
body: {}
|
|
2971
|
+
});
|
|
2972
|
+
});
|
|
2973
|
+
emitAction(
|
|
2974
|
+
`published ${style.bold(decompositionId)} \u2192 run ${style.bold(data.runRecordId)} (${data.outcome})`,
|
|
2975
|
+
data,
|
|
2976
|
+
cmd.optsWithGlobals().json
|
|
2977
|
+
);
|
|
2978
|
+
});
|
|
2979
|
+
decomposition.command("accept <decompositionId>").description(
|
|
2980
|
+
"Accept a Pending decomposition \u2014 promote + ratify its Tasks (POST /decompositions/{id}/accept)"
|
|
2981
|
+
).action(async (decompositionId, _opts, cmd) => {
|
|
2982
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2983
|
+
const data = await runApi("Accepting decomposition", async () => {
|
|
2984
|
+
const client = await makeClient(cfg);
|
|
2985
|
+
return client.POST("/decompositions/{id}/accept", {
|
|
2986
|
+
params: { path: { id: decompositionId } },
|
|
2987
|
+
body: {}
|
|
2988
|
+
});
|
|
2989
|
+
});
|
|
2990
|
+
emitAction(
|
|
2991
|
+
`accepted decomposition ${style.bold(decompositionId)}`,
|
|
2992
|
+
data,
|
|
2993
|
+
cmd.optsWithGlobals().json
|
|
2994
|
+
);
|
|
2995
|
+
});
|
|
2996
|
+
decomposition.command("reject <decompositionId>").description(
|
|
2997
|
+
"Reject a Pending decomposition \u2014 archive its Tasks, bounce the brief (POST /decompositions/{id}/reject)"
|
|
2998
|
+
).option("--reason <reason>", "Optional free-text rejection reason").action(async (decompositionId, opts, cmd) => {
|
|
2999
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
3000
|
+
const data = await runApi("Rejecting decomposition", async () => {
|
|
3001
|
+
const client = await makeClient(cfg);
|
|
3002
|
+
return client.POST("/decompositions/{id}/reject", {
|
|
3003
|
+
params: { path: { id: decompositionId } },
|
|
3004
|
+
body: { reasonText: opts.reason ?? null }
|
|
3005
|
+
});
|
|
3006
|
+
});
|
|
3007
|
+
emitAction(
|
|
3008
|
+
`rejected decomposition ${style.bold(decompositionId)}`,
|
|
3009
|
+
data,
|
|
3010
|
+
cmd.optsWithGlobals().json
|
|
3011
|
+
);
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
3014
|
+
|
|
2571
3015
|
// src/commands/filing.ts
|
|
2572
3016
|
function registerFiling(program2) {
|
|
2573
3017
|
const filing = program2.command("filing").description("Review and act on filing suggestions");
|
|
@@ -3079,7 +3523,7 @@ Examples:
|
|
|
3079
3523
|
|
|
3080
3524
|
// src/setup/apply.ts
|
|
3081
3525
|
import { createHash as createHash3 } from "crypto";
|
|
3082
|
-
import { mkdirSync as mkdirSync9, readFileSync as
|
|
3526
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync9, existsSync as existsSync9 } from "fs";
|
|
3083
3527
|
import { dirname as dirname8 } from "path";
|
|
3084
3528
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
3085
3529
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
@@ -3137,7 +3581,7 @@ function ensureDir2(path) {
|
|
|
3137
3581
|
}
|
|
3138
3582
|
function readOr(path, fallback) {
|
|
3139
3583
|
try {
|
|
3140
|
-
return
|
|
3584
|
+
return readFileSync8(path, "utf8");
|
|
3141
3585
|
} catch {
|
|
3142
3586
|
return fallback;
|
|
3143
3587
|
}
|
|
@@ -3148,7 +3592,7 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
3148
3592
|
let current = {};
|
|
3149
3593
|
if (existed) {
|
|
3150
3594
|
try {
|
|
3151
|
-
current = JSON.parse(
|
|
3595
|
+
current = JSON.parse(readFileSync8(path, "utf8"));
|
|
3152
3596
|
} catch {
|
|
3153
3597
|
return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
|
|
3154
3598
|
}
|
|
@@ -3864,7 +4308,7 @@ import { basename as basename2, join as join13 } from "path";
|
|
|
3864
4308
|
|
|
3865
4309
|
// src/commands/fanout.ts
|
|
3866
4310
|
import { spawnSync } from "child_process";
|
|
3867
|
-
import { existsSync as existsSync10, readFileSync as
|
|
4311
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
3868
4312
|
import { isAbsolute, join as join12, resolve } from "path";
|
|
3869
4313
|
var ICON = {
|
|
3870
4314
|
refresh: "\u21BB",
|
|
@@ -3899,7 +4343,7 @@ function readManifest(path) {
|
|
|
3899
4343
|
if (!existsSync10(path)) return null;
|
|
3900
4344
|
let parsed;
|
|
3901
4345
|
try {
|
|
3902
|
-
parsed = JSON.parse(
|
|
4346
|
+
parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
3903
4347
|
} catch (err2) {
|
|
3904
4348
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
3905
4349
|
}
|
|
@@ -4713,24 +5157,45 @@ Examples:
|
|
|
4713
5157
|
$ sechroom relationship suggestions --status Pending --memory mem_XXXX
|
|
4714
5158
|
$ sechroom relationship suggestion accept rsg_XXXX`
|
|
4715
5159
|
);
|
|
4716
|
-
relationship.command("create <fromMemoryId> <toMemoryId>").description("Create a relationship (POST /memories/{memoryId}/relationships)").option("--type <type>", "Relationship type (Reference, Related, Parent, Child, Follows, \u2026)", "Reference").
|
|
5160
|
+
relationship.command("create <fromMemoryId> <toMemoryId>").description("Create a relationship (POST /memories/{memoryId}/relationships)").option("--type <type>", "Relationship type (Reference, Related, Parent, Child, Follows, \u2026)", "Reference").option(
|
|
5161
|
+
"--to-version <number>",
|
|
5162
|
+
"Version of the target memory to pin the edge to (defaults to the target's current version)"
|
|
5163
|
+
).action(async (fromMemoryId, toMemoryId, opts, cmd) => {
|
|
4717
5164
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
4718
|
-
const
|
|
4719
|
-
|
|
4720
|
-
|
|
5165
|
+
const client = await makeClient(cfg);
|
|
5166
|
+
let toVersion;
|
|
5167
|
+
if (opts.toVersion !== void 0) {
|
|
5168
|
+
toVersion = Number(opts.toVersion);
|
|
5169
|
+
if (!Number.isInteger(toVersion) || toVersion < 1) {
|
|
5170
|
+
fail(`--to-version must be a positive integer (got '${opts.toVersion}').`);
|
|
5171
|
+
}
|
|
5172
|
+
} else {
|
|
5173
|
+
const target = await runApi(
|
|
5174
|
+
"Resolving target version",
|
|
5175
|
+
async () => client.GET("/memories/{memoryId}", { params: { path: { memoryId: toMemoryId } } })
|
|
5176
|
+
);
|
|
5177
|
+
if (typeof target.item?.currentVersion !== "number") {
|
|
5178
|
+
fail(`Could not resolve the current version of ${toMemoryId}; pass --to-version explicitly.`);
|
|
5179
|
+
}
|
|
5180
|
+
toVersion = target.item.currentVersion;
|
|
5181
|
+
}
|
|
5182
|
+
const data = await runApi(
|
|
5183
|
+
"Creating relationship",
|
|
5184
|
+
async () => client.POST("/memories/{memoryId}/relationships", {
|
|
4721
5185
|
params: { path: { memoryId: fromMemoryId } },
|
|
4722
5186
|
body: {
|
|
4723
5187
|
toMemoryId,
|
|
4724
|
-
type: opts.type
|
|
5188
|
+
type: opts.type,
|
|
5189
|
+
toVersion
|
|
4725
5190
|
}
|
|
4726
|
-
})
|
|
4727
|
-
|
|
5191
|
+
})
|
|
5192
|
+
);
|
|
4728
5193
|
const inversePart = data.inverseId ? ` ${style.dim(`(inverse ${data.inverseId})`)}` : "";
|
|
4729
5194
|
const view = resolveViewUrl(cfg.baseUrl, data.url);
|
|
4730
5195
|
const urlPart = view ? ` ${style.dim("\u2192")} ${view}` : "";
|
|
4731
5196
|
emitAction(
|
|
4732
|
-
`created relationship ${style.bold(data.id)} ${style.dim(`${fromMemoryId} \u2192 ${toMemoryId}`)}${inversePart}${urlPart}`,
|
|
4733
|
-
data,
|
|
5197
|
+
`created relationship ${style.bold(data.id)} ${style.dim(`${fromMemoryId} \u2192 ${toMemoryId} @v${toVersion}`)}${inversePart}${urlPart}`,
|
|
5198
|
+
{ ...data, toVersion },
|
|
4734
5199
|
cmd.optsWithGlobals().json
|
|
4735
5200
|
);
|
|
4736
5201
|
});
|
|
@@ -4854,7 +5319,7 @@ Examples:
|
|
|
4854
5319
|
// src/commands/reset.ts
|
|
4855
5320
|
import { homedir as homedir4 } from "os";
|
|
4856
5321
|
import { join as join14 } from "path";
|
|
4857
|
-
import { existsSync as existsSync12, readFileSync as
|
|
5322
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10, rmSync as rmSync3 } from "fs";
|
|
4858
5323
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
4859
5324
|
var localSkillsDir = () => join14(process.cwd(), ".claude", "skills");
|
|
4860
5325
|
var globalSkillsDir = () => join14(homedir4(), ".claude", "skills");
|
|
@@ -4865,7 +5330,7 @@ function removeMaterialisedSkills(dir) {
|
|
|
4865
5330
|
const lockPath = join14(dir, SKILLS_LOCK2);
|
|
4866
5331
|
if (!existsSync12(lockPath)) return removed;
|
|
4867
5332
|
try {
|
|
4868
|
-
const lock = JSON.parse(
|
|
5333
|
+
const lock = JSON.parse(readFileSync10(lockPath, "utf8"));
|
|
4869
5334
|
for (const entry of Object.values(lock)) {
|
|
4870
5335
|
for (const name of entry.skills ?? []) {
|
|
4871
5336
|
const p = join14(dir, name);
|
|
@@ -5265,7 +5730,7 @@ Examples:
|
|
|
5265
5730
|
import {
|
|
5266
5731
|
existsSync as existsSync15,
|
|
5267
5732
|
mkdirSync as mkdirSync12,
|
|
5268
|
-
readFileSync as
|
|
5733
|
+
readFileSync as readFileSync11,
|
|
5269
5734
|
rmSync as rmSync4,
|
|
5270
5735
|
writeFileSync as writeFileSync12
|
|
5271
5736
|
} from "fs";
|
|
@@ -5371,7 +5836,7 @@ function registerTelemetry(program2) {
|
|
|
5371
5836
|
);
|
|
5372
5837
|
});
|
|
5373
5838
|
telemetry.command("hook").description(
|
|
5374
|
-
"Per-turn telemetry self-report for
|
|
5839
|
+
"Per-turn telemetry self-report for Claude Code hooks \u2014 Stop/SubagentStop \u2192 parsed + terminal, Notification/PermissionDenied \u2192 approval (reads stdin; no-op unless bound). Fail-soft."
|
|
5375
5840
|
).action(async (_opts, cmd) => {
|
|
5376
5841
|
try {
|
|
5377
5842
|
const raw = await readStdin2();
|
|
@@ -5380,21 +5845,10 @@ function registerTelemetry(program2) {
|
|
|
5380
5845
|
const binding = findBinding(cwd);
|
|
5381
5846
|
if (!binding) return process.exit(0);
|
|
5382
5847
|
const usage = input.transcript_path ? parseTranscript(input.transcript_path) : null;
|
|
5383
|
-
|
|
5848
|
+
const events = buildHookEvents(input, usage, binding.taskId);
|
|
5849
|
+
if (events.length === 0) return process.exit(0);
|
|
5384
5850
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
5385
|
-
await postTelemetry(cfg, binding.decompositionId,
|
|
5386
|
-
{
|
|
5387
|
-
taskId: binding.taskId,
|
|
5388
|
-
kind: "Parsed",
|
|
5389
|
-
tokensIn: usage.tokensIn,
|
|
5390
|
-
tokensOut: usage.tokensOut,
|
|
5391
|
-
contextUsed: usage.contextUsed,
|
|
5392
|
-
contextWindow: usage.contextWindow,
|
|
5393
|
-
text: null,
|
|
5394
|
-
approvalState: null,
|
|
5395
|
-
verdict: null
|
|
5396
|
-
}
|
|
5397
|
-
]);
|
|
5851
|
+
await postTelemetry(cfg, binding.decompositionId, events);
|
|
5398
5852
|
return process.exit(0);
|
|
5399
5853
|
} catch {
|
|
5400
5854
|
return process.exit(0);
|
|
@@ -5422,7 +5876,12 @@ function registerTelemetry(program2) {
|
|
|
5422
5876
|
scope,
|
|
5423
5877
|
cwd
|
|
5424
5878
|
});
|
|
5425
|
-
const commands = {
|
|
5879
|
+
const commands = {
|
|
5880
|
+
Stop: "sechroom telemetry hook",
|
|
5881
|
+
SubagentStop: "sechroom telemetry hook",
|
|
5882
|
+
Notification: "sechroom telemetry hook",
|
|
5883
|
+
PermissionDenied: "sechroom telemetry hook"
|
|
5884
|
+
};
|
|
5426
5885
|
try {
|
|
5427
5886
|
const multi = targets.length > 1;
|
|
5428
5887
|
const results = targets.map((t) => {
|
|
@@ -5480,7 +5939,7 @@ function findBinding(start) {
|
|
|
5480
5939
|
if (existsSync15(path)) {
|
|
5481
5940
|
try {
|
|
5482
5941
|
const b = JSON.parse(
|
|
5483
|
-
|
|
5942
|
+
readFileSync11(path, "utf8")
|
|
5484
5943
|
);
|
|
5485
5944
|
if (b.decompositionId && b.taskId)
|
|
5486
5945
|
return { decompositionId: b.decompositionId, taskId: b.taskId };
|
|
@@ -5499,7 +5958,7 @@ function parseTranscript(path) {
|
|
|
5499
5958
|
let tokensOut = 0;
|
|
5500
5959
|
let contextUsed = 0;
|
|
5501
5960
|
let model = "";
|
|
5502
|
-
for (const line of
|
|
5961
|
+
for (const line of readFileSync11(path, "utf8").split("\n")) {
|
|
5503
5962
|
if (!line.trim()) continue;
|
|
5504
5963
|
let obj;
|
|
5505
5964
|
try {
|
|
@@ -5516,11 +5975,61 @@ function parseTranscript(path) {
|
|
|
5516
5975
|
if (obj.message?.model) model = obj.message.model;
|
|
5517
5976
|
}
|
|
5518
5977
|
if (tokensIn === 0 && tokensOut === 0) return null;
|
|
5519
|
-
return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model) };
|
|
5978
|
+
return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model, contextUsed) };
|
|
5520
5979
|
}
|
|
5521
|
-
function windowFor(model) {
|
|
5980
|
+
function windowFor(model, contextUsed = 0) {
|
|
5522
5981
|
const m = model.toLowerCase();
|
|
5523
|
-
|
|
5982
|
+
if (m.includes("[1m]") || m.includes("-1m")) return 1e6;
|
|
5983
|
+
return contextUsed > 2e5 ? 1e6 : 2e5;
|
|
5984
|
+
}
|
|
5985
|
+
function buildHookEvents(input, usage, taskId) {
|
|
5986
|
+
const events = [];
|
|
5987
|
+
const base = (kind, over) => ({
|
|
5988
|
+
taskId,
|
|
5989
|
+
kind,
|
|
5990
|
+
tokensIn: null,
|
|
5991
|
+
tokensOut: null,
|
|
5992
|
+
contextUsed: null,
|
|
5993
|
+
contextWindow: null,
|
|
5994
|
+
text: null,
|
|
5995
|
+
approvalState: null,
|
|
5996
|
+
verdict: null,
|
|
5997
|
+
...over
|
|
5998
|
+
});
|
|
5999
|
+
if (usage) {
|
|
6000
|
+
events.push(
|
|
6001
|
+
base("Parsed", {
|
|
6002
|
+
tokensIn: usage.tokensIn,
|
|
6003
|
+
tokensOut: usage.tokensOut,
|
|
6004
|
+
contextUsed: usage.contextUsed,
|
|
6005
|
+
contextWindow: usage.contextWindow
|
|
6006
|
+
})
|
|
6007
|
+
);
|
|
6008
|
+
}
|
|
6009
|
+
switch (input.hook_event_name) {
|
|
6010
|
+
case "PermissionDenied":
|
|
6011
|
+
events.push(
|
|
6012
|
+
base("Approval", {
|
|
6013
|
+
approvalState: "denied",
|
|
6014
|
+
text: input.tool_name ?? input.message ?? null
|
|
6015
|
+
})
|
|
6016
|
+
);
|
|
6017
|
+
break;
|
|
6018
|
+
case "Notification":
|
|
6019
|
+
if (isPermissionNotification(input))
|
|
6020
|
+
events.push(base("Approval", { text: input.message ?? null }));
|
|
6021
|
+
break;
|
|
6022
|
+
case "Stop":
|
|
6023
|
+
case "SubagentStop":
|
|
6024
|
+
events.push(base("Terminal", { text: input.last_assistant_message ?? null }));
|
|
6025
|
+
break;
|
|
6026
|
+
}
|
|
6027
|
+
return events;
|
|
6028
|
+
}
|
|
6029
|
+
function isPermissionNotification(input) {
|
|
6030
|
+
const t = (input.notification_type ?? input.type ?? "").toLowerCase();
|
|
6031
|
+
if (t) return t.includes("permission");
|
|
6032
|
+
return (input.message ?? "").toLowerCase().includes("permission");
|
|
5524
6033
|
}
|
|
5525
6034
|
async function readStdin2() {
|
|
5526
6035
|
if (process.stdin.isTTY) return "";
|
|
@@ -5756,7 +6265,7 @@ Examples:
|
|
|
5756
6265
|
function resolveVersion() {
|
|
5757
6266
|
try {
|
|
5758
6267
|
const pkg = JSON.parse(
|
|
5759
|
-
|
|
6268
|
+
readFileSync12(new URL("../package.json", import.meta.url), "utf8")
|
|
5760
6269
|
);
|
|
5761
6270
|
return pkg.version ?? "0.0.0";
|
|
5762
6271
|
} catch {
|
|
@@ -5914,6 +6423,8 @@ registerLookup(program);
|
|
|
5914
6423
|
registerRelationships(program);
|
|
5915
6424
|
registerWorkspace(program);
|
|
5916
6425
|
registerProject(program);
|
|
6426
|
+
registerDecomposition(program);
|
|
6427
|
+
registerClose(program);
|
|
5917
6428
|
registerFiling(program);
|
|
5918
6429
|
registerContinuity(program);
|
|
5919
6430
|
registerCheckpoint(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sechroom/cli",
|
|
3
|
-
"version": "2026.7.
|
|
3
|
+
"version": "2026.7.6-rc.a2828c37",
|
|
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",
|
|
@@ -36,12 +36,14 @@
|
|
|
36
36
|
"openapi-typescript": "^7.4.0",
|
|
37
37
|
"tsup": "^8.3.0",
|
|
38
38
|
"tsx": "^4.19.0",
|
|
39
|
-
"typescript": "^5.6.0"
|
|
39
|
+
"typescript": "^5.6.0",
|
|
40
|
+
"typescript-7": "npm:typescript@^7.0.2"
|
|
40
41
|
},
|
|
41
42
|
"scripts": {
|
|
42
43
|
"gen": "openapi-typescript \"${SECHROOM_OPENAPI_URL:-https://app.sechroom.ai/api/openapi/v1.json}\" -o src/generated/api.d.ts",
|
|
43
44
|
"build": "tsup src/index.ts --format esm --target node20 --clean",
|
|
44
45
|
"dev": "tsx src/index.ts",
|
|
46
|
+
"test": "node --import tsx --test \"src/**/*.test.ts\"",
|
|
45
47
|
"check-types": "tsc --noEmit"
|
|
46
48
|
}
|
|
47
49
|
}
|