@sechroom/cli 2026.7.4 → 2026.7.5-rc.ef6a4180
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 +474 -36
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/auth.ts
|
|
@@ -633,7 +633,22 @@ async function runApi(label, fn) {
|
|
|
633
633
|
return res.data;
|
|
634
634
|
}
|
|
635
635
|
function fail(error) {
|
|
636
|
-
|
|
636
|
+
let msg;
|
|
637
|
+
if (typeof error === "object" && error !== null && "title" in error) {
|
|
638
|
+
const problem = error;
|
|
639
|
+
msg = String(problem.title);
|
|
640
|
+
if (problem.errors && typeof problem.errors === "object") {
|
|
641
|
+
const detail = Object.entries(problem.errors).flatMap(
|
|
642
|
+
([field, msgs]) => (Array.isArray(msgs) ? msgs : [msgs]).map((m) => ` ${field}: ${String(m)}`)
|
|
643
|
+
);
|
|
644
|
+
if (detail.length > 0) msg += `
|
|
645
|
+
${detail.join("\n")}`;
|
|
646
|
+
}
|
|
647
|
+
} else if (typeof error === "object" && error !== null) {
|
|
648
|
+
msg = JSON.stringify(error);
|
|
649
|
+
} else {
|
|
650
|
+
msg = String(error);
|
|
651
|
+
}
|
|
637
652
|
process.stderr.write(`error: ${msg}
|
|
638
653
|
`);
|
|
639
654
|
process.exit(1);
|
|
@@ -1544,11 +1559,16 @@ function registerChannel(program2) {
|
|
|
1544
1559
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1545
1560
|
const filter = readFilter(opts);
|
|
1546
1561
|
const sub = await ensureSubscription(cfg, opts.name, filter);
|
|
1547
|
-
const
|
|
1548
|
-
|
|
1562
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1563
|
+
const deliver = makeDeliver(
|
|
1564
|
+
filter,
|
|
1565
|
+
seen,
|
|
1566
|
+
(payload) => process.stdout.write(
|
|
1549
1567
|
(typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
|
|
1550
|
-
)
|
|
1551
|
-
|
|
1568
|
+
)
|
|
1569
|
+
);
|
|
1570
|
+
const conn = await openConnection(cfg, deliver);
|
|
1571
|
+
await reconcile(cfg, filter, deliver);
|
|
1552
1572
|
if (json) {
|
|
1553
1573
|
emit(
|
|
1554
1574
|
{
|
|
@@ -1585,7 +1605,8 @@ function registerChannel(program2) {
|
|
|
1585
1605
|
);
|
|
1586
1606
|
await mcp.connect(new StdioServerTransport());
|
|
1587
1607
|
await ensureSubscription(cfg, opts.name, filter);
|
|
1588
|
-
const
|
|
1608
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1609
|
+
const deliver = makeDeliver(filter, seen, (payload) => {
|
|
1589
1610
|
const { content, meta } = summarizeEvent(payload);
|
|
1590
1611
|
void mcp.notification({
|
|
1591
1612
|
method: "notifications/claude/channel",
|
|
@@ -1595,6 +1616,8 @@ function registerChannel(program2) {
|
|
|
1595
1616
|
`))
|
|
1596
1617
|
);
|
|
1597
1618
|
});
|
|
1619
|
+
const conn = await openConnection(cfg, deliver);
|
|
1620
|
+
await reconcile(cfg, filter, deliver);
|
|
1598
1621
|
process.stderr.write(
|
|
1599
1622
|
style.dim(
|
|
1600
1623
|
`sechroom channel (mcp) \u2014 tenant ${cfg.tenant}, tags [${filter.tags.join(", ")}]
|
|
@@ -1605,7 +1628,10 @@ function registerChannel(program2) {
|
|
|
1605
1628
|
});
|
|
1606
1629
|
channel.command("install").description(
|
|
1607
1630
|
"Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
|
|
1608
|
-
).option(
|
|
1631
|
+
).option(
|
|
1632
|
+
"--workspace <wsp...>",
|
|
1633
|
+
"Restrict dispatches to these workspace id(s)"
|
|
1634
|
+
).option(
|
|
1609
1635
|
"--tag <tag...>",
|
|
1610
1636
|
"Tag(s) a dispatch must carry to match (repeatable). Default targets WLP task dispatches.",
|
|
1611
1637
|
["kind:task"]
|
|
@@ -1726,20 +1752,134 @@ function holdOpen(conn) {
|
|
|
1726
1752
|
process.on("SIGTERM", stop);
|
|
1727
1753
|
});
|
|
1728
1754
|
}
|
|
1729
|
-
function
|
|
1755
|
+
function parseEvent(payload) {
|
|
1730
1756
|
let data = payload;
|
|
1731
1757
|
if (typeof payload === "string") {
|
|
1732
1758
|
try {
|
|
1733
1759
|
data = JSON.parse(payload);
|
|
1734
1760
|
} catch {
|
|
1735
|
-
return {
|
|
1761
|
+
return { eventType: "", memoryId: "", workspaceId: "", tags: void 0 };
|
|
1736
1762
|
}
|
|
1737
1763
|
}
|
|
1738
1764
|
const obj = data ?? {};
|
|
1739
1765
|
const inner = obj.data ?? obj;
|
|
1740
|
-
const
|
|
1741
|
-
|
|
1742
|
-
|
|
1766
|
+
const rawTags = inner.tags ?? inner.Tags;
|
|
1767
|
+
return {
|
|
1768
|
+
eventType: str(inner.eventType ?? inner.EventType ?? obj.type) || "substrate.event",
|
|
1769
|
+
memoryId: str(inner.memoryId ?? inner.MemoryId),
|
|
1770
|
+
workspaceId: str(inner.workspaceId ?? inner.WorkspaceId),
|
|
1771
|
+
tags: Array.isArray(rawTags) ? rawTags.filter((t) => typeof t === "string") : void 0
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
function shouldDeliver(payload, filter) {
|
|
1775
|
+
const { workspaceId, tags } = parseEvent(payload);
|
|
1776
|
+
if (filter.workspaceScope.length > 0 && (!workspaceId || !filter.workspaceScope.includes(workspaceId)))
|
|
1777
|
+
return false;
|
|
1778
|
+
if (filter.tags.length > 0) {
|
|
1779
|
+
if (!tags) return false;
|
|
1780
|
+
return facetedTagMatch(tags, filter.tags);
|
|
1781
|
+
}
|
|
1782
|
+
return true;
|
|
1783
|
+
}
|
|
1784
|
+
function makeDeliver(filter, seen, forward) {
|
|
1785
|
+
return (payload) => {
|
|
1786
|
+
if (!shouldDeliver(payload, filter)) return;
|
|
1787
|
+
const { memoryId } = parseEvent(payload);
|
|
1788
|
+
if (memoryId) {
|
|
1789
|
+
if (seen.has(memoryId)) return;
|
|
1790
|
+
seen.add(memoryId);
|
|
1791
|
+
}
|
|
1792
|
+
forward(payload);
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
async function reconcile(cfg, filter, deliver) {
|
|
1796
|
+
if (filter.workspaceScope.length === 0) {
|
|
1797
|
+
process.stderr.write(
|
|
1798
|
+
style.dim(
|
|
1799
|
+
"channel: no --workspace to reconcile against; live feed only (a dropped dispatch won't be recovered).\n"
|
|
1800
|
+
)
|
|
1801
|
+
);
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
if (filter.tags.length === 0) return;
|
|
1805
|
+
let token;
|
|
1806
|
+
try {
|
|
1807
|
+
token = await requireToken(cfg);
|
|
1808
|
+
} catch {
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
const qs = `filterTags=${encodeURIComponent(filter.tags.join(","))}&limit=100`;
|
|
1812
|
+
let recovered = 0;
|
|
1813
|
+
for (const ws of filter.workspaceScope) {
|
|
1814
|
+
try {
|
|
1815
|
+
const resp = await fetch(
|
|
1816
|
+
`${cfg.baseUrl}/workspaces/${encodeURIComponent(ws)}/memories/feed?${qs}`,
|
|
1817
|
+
{
|
|
1818
|
+
headers: {
|
|
1819
|
+
authorization: `Bearer ${token}`,
|
|
1820
|
+
tenant: cfg.tenant,
|
|
1821
|
+
"x-sechroom-surface": "cli"
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
);
|
|
1825
|
+
if (!resp.ok) {
|
|
1826
|
+
process.stderr.write(
|
|
1827
|
+
err(
|
|
1828
|
+
`channel: reconcile query for ${ws} failed (HTTP ${resp.status})
|
|
1829
|
+
`
|
|
1830
|
+
)
|
|
1831
|
+
);
|
|
1832
|
+
continue;
|
|
1833
|
+
}
|
|
1834
|
+
const data = await resp.json();
|
|
1835
|
+
for (const m of data.results ?? []) {
|
|
1836
|
+
if (!m.id) continue;
|
|
1837
|
+
deliver({
|
|
1838
|
+
eventType: "reconcile",
|
|
1839
|
+
memoryId: m.id,
|
|
1840
|
+
workspaceId: ws,
|
|
1841
|
+
tags: m.tags ?? []
|
|
1842
|
+
});
|
|
1843
|
+
recovered++;
|
|
1844
|
+
}
|
|
1845
|
+
} catch (e) {
|
|
1846
|
+
process.stderr.write(
|
|
1847
|
+
err(`channel: reconcile error for ${ws}: ${String(e)}
|
|
1848
|
+
`)
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
if (recovered > 0)
|
|
1853
|
+
process.stderr.write(
|
|
1854
|
+
style.dim(
|
|
1855
|
+
`channel: reconciled ${recovered} already-queued event(s) on connect.
|
|
1856
|
+
`
|
|
1857
|
+
)
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
function facetedTagMatch(eventTags, filterTags) {
|
|
1861
|
+
const have = new Set(eventTags);
|
|
1862
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1863
|
+
for (const f of filterTags) {
|
|
1864
|
+
const ns = f.endsWith(":*") ? f.slice(0, -2) : namespaceOf(f);
|
|
1865
|
+
const group = groups.get(ns) ?? [];
|
|
1866
|
+
group.push(f);
|
|
1867
|
+
groups.set(ns, group);
|
|
1868
|
+
}
|
|
1869
|
+
for (const [ns, group] of groups) {
|
|
1870
|
+
const ok2 = group.some(
|
|
1871
|
+
(f) => f.endsWith(":*") ? eventTags.some((t) => namespaceOf(t) === ns) : have.has(f)
|
|
1872
|
+
);
|
|
1873
|
+
if (!ok2) return false;
|
|
1874
|
+
}
|
|
1875
|
+
return true;
|
|
1876
|
+
}
|
|
1877
|
+
function namespaceOf(tag) {
|
|
1878
|
+
const i = tag.indexOf(":");
|
|
1879
|
+
return i >= 0 ? tag.slice(0, i) : tag;
|
|
1880
|
+
}
|
|
1881
|
+
function summarizeEvent(payload) {
|
|
1882
|
+
const { eventType, memoryId, workspaceId } = parseEvent(payload);
|
|
1743
1883
|
const content = memoryId ? `${eventType}: ${memoryId}${workspaceId ? ` (workspace ${workspaceId})` : ""}` : typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
1744
1884
|
const meta = {};
|
|
1745
1885
|
if (eventType) meta.event_type = eventType;
|
|
@@ -2408,6 +2548,178 @@ Examples:
|
|
|
2408
2548
|
});
|
|
2409
2549
|
}
|
|
2410
2550
|
|
|
2551
|
+
// src/commands/close.ts
|
|
2552
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
2553
|
+
var VERDICTS = ["pass", "soft-fail", "plan-invalid", "blocked"];
|
|
2554
|
+
function registerClose(program2) {
|
|
2555
|
+
program2.command("close").description(
|
|
2556
|
+
"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)."
|
|
2557
|
+
).requiredOption("--task <id>", "The WorkTask memory id being closed").requiredOption("--verdict <verdict>", `Verdict: ${VERDICTS.join(" | ")}`).requiredOption(
|
|
2558
|
+
"--workspace <wsp>",
|
|
2559
|
+
"Workspace that owns the closeout memo"
|
|
2560
|
+
).requiredOption("--title <title>", "Closeout title").option(
|
|
2561
|
+
"--file <path>",
|
|
2562
|
+
"Read the closeout body from a file (default: stdin)"
|
|
2563
|
+
).option(
|
|
2564
|
+
"--decomposition <id>",
|
|
2565
|
+
"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."
|
|
2566
|
+
).option(
|
|
2567
|
+
"--no-status-flip",
|
|
2568
|
+
"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.)"
|
|
2569
|
+
).option(
|
|
2570
|
+
"--to-version <n>",
|
|
2571
|
+
"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)."
|
|
2572
|
+
).option("--source <source>", "Source / lane stamp", "cli").addHelpText(
|
|
2573
|
+
"after",
|
|
2574
|
+
`
|
|
2575
|
+
Examples:
|
|
2576
|
+
# Managed run \u2014 reads matchTags from run state, stamps them verbatim, un-parks the run:
|
|
2577
|
+
$ echo "Shipped X. Tests green." | sechroom close --task mem_XXXX --decomposition sug_YYYY \\
|
|
2578
|
+
--verdict pass --workspace wsp_ZZZZ --title "w4_build closeout"
|
|
2579
|
+
|
|
2580
|
+
# Bare task:
|
|
2581
|
+
$ sechroom close --task mem_XXXX --verdict pass --workspace wsp_ZZZZ \\
|
|
2582
|
+
--title "done" --file ./closeout.md`
|
|
2583
|
+
).action(async (opts, cmd) => {
|
|
2584
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2585
|
+
const json = cmd.optsWithGlobals().json;
|
|
2586
|
+
const verdict = String(opts.verdict);
|
|
2587
|
+
if (!VERDICTS.includes(verdict))
|
|
2588
|
+
fail(
|
|
2589
|
+
`--verdict must be one of ${VERDICTS.join(" | ")} \u2014 got '${opts.verdict}'`
|
|
2590
|
+
);
|
|
2591
|
+
let bodyText;
|
|
2592
|
+
try {
|
|
2593
|
+
bodyText = opts.file ? readFileSync7(opts.file, "utf8") : readFileSync7(0, "utf8");
|
|
2594
|
+
} catch {
|
|
2595
|
+
fail(
|
|
2596
|
+
opts.file ? `could not read --file ${opts.file}` : "no closeout body \u2014 pass --file <path> or pipe it on stdin"
|
|
2597
|
+
);
|
|
2598
|
+
}
|
|
2599
|
+
if (!bodyText.trim())
|
|
2600
|
+
fail(
|
|
2601
|
+
"closeout body is empty \u2014 pass --file <path> or pipe the body on stdin"
|
|
2602
|
+
);
|
|
2603
|
+
const client = await makeClient(cfg);
|
|
2604
|
+
let matchTags;
|
|
2605
|
+
let dispatchedVersion;
|
|
2606
|
+
if (opts.decomposition) {
|
|
2607
|
+
const run = await runApi(
|
|
2608
|
+
"Reading run contract",
|
|
2609
|
+
async () => client.GET("/decompositions/{id}/run", {
|
|
2610
|
+
params: { path: { id: opts.decomposition } }
|
|
2611
|
+
})
|
|
2612
|
+
);
|
|
2613
|
+
const await_ = run.awaitingCompletion;
|
|
2614
|
+
if (!await_ || !Array.isArray(await_.matchTags) || await_.matchTags.length === 0)
|
|
2615
|
+
fail(
|
|
2616
|
+
`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.`
|
|
2617
|
+
);
|
|
2618
|
+
if (run.awaitingTaskId && run.awaitingTaskId !== opts.task)
|
|
2619
|
+
fail(
|
|
2620
|
+
`run ${opts.decomposition} is awaiting task ${run.awaitingTaskId}, not ${opts.task} \u2014 refusing to mis-key the resume.`
|
|
2621
|
+
);
|
|
2622
|
+
matchTags = await_.matchTags;
|
|
2623
|
+
if (typeof await_.dispatchedTaskVersion === "number")
|
|
2624
|
+
dispatchedVersion = await_.dispatchedTaskVersion;
|
|
2625
|
+
} else {
|
|
2626
|
+
matchTags = [`wlp-task:${opts.task}`];
|
|
2627
|
+
}
|
|
2628
|
+
const tags = [
|
|
2629
|
+
.../* @__PURE__ */ new Set([
|
|
2630
|
+
...matchTags,
|
|
2631
|
+
`wlp-task:${opts.task}`,
|
|
2632
|
+
`verdict:${verdict}`
|
|
2633
|
+
])
|
|
2634
|
+
];
|
|
2635
|
+
const closeout = await runApi(
|
|
2636
|
+
"Creating closeout",
|
|
2637
|
+
async () => client.POST("/memories", {
|
|
2638
|
+
body: {
|
|
2639
|
+
text: bodyText,
|
|
2640
|
+
type: "reference",
|
|
2641
|
+
content: "{}",
|
|
2642
|
+
confidence: 1,
|
|
2643
|
+
source: opts.source,
|
|
2644
|
+
archetype: "Document",
|
|
2645
|
+
title: opts.title,
|
|
2646
|
+
tags,
|
|
2647
|
+
owner: { type: "Workspace", id: opts.workspace }
|
|
2648
|
+
}
|
|
2649
|
+
})
|
|
2650
|
+
);
|
|
2651
|
+
let toVersion;
|
|
2652
|
+
if (opts.toVersion !== void 0) {
|
|
2653
|
+
toVersion = Number(opts.toVersion);
|
|
2654
|
+
if (!Number.isInteger(toVersion) || toVersion < 1)
|
|
2655
|
+
fail(
|
|
2656
|
+
`--to-version must be an integer >= 1 \u2014 got '${opts.toVersion}'`
|
|
2657
|
+
);
|
|
2658
|
+
} else if (dispatchedVersion !== void 0) {
|
|
2659
|
+
toVersion = dispatchedVersion;
|
|
2660
|
+
} else {
|
|
2661
|
+
const task = await runApi(
|
|
2662
|
+
"Reading task version",
|
|
2663
|
+
async () => client.GET("/memories/{memoryId}", {
|
|
2664
|
+
params: { path: { memoryId: opts.task } }
|
|
2665
|
+
})
|
|
2666
|
+
);
|
|
2667
|
+
const v = task?.item?.currentVersion ?? task?.currentVersion;
|
|
2668
|
+
if (typeof v !== "number" || v < 1)
|
|
2669
|
+
fail(
|
|
2670
|
+
`could not read task ${opts.task} version to pin the Reference edge \u2014 pass --to-version <n>`
|
|
2671
|
+
);
|
|
2672
|
+
toVersion = v;
|
|
2673
|
+
}
|
|
2674
|
+
const edge = await runApi(
|
|
2675
|
+
"Wiring Reference edge",
|
|
2676
|
+
async () => client.POST("/memories/{memoryId}/relationships", {
|
|
2677
|
+
params: { path: { memoryId: closeout.id } },
|
|
2678
|
+
body: { toMemoryId: opts.task, type: "Reference", toVersion }
|
|
2679
|
+
})
|
|
2680
|
+
);
|
|
2681
|
+
let taskStatus;
|
|
2682
|
+
if (opts.statusFlip !== false && !opts.decomposition) {
|
|
2683
|
+
const current = await runApi(
|
|
2684
|
+
"Reading task tags",
|
|
2685
|
+
async () => client.GET("/memories/{memoryId}", {
|
|
2686
|
+
params: { path: { memoryId: opts.task } }
|
|
2687
|
+
})
|
|
2688
|
+
);
|
|
2689
|
+
const currentTags = current?.item?.tags ?? current?.tags ?? [];
|
|
2690
|
+
const target = verdict === "blocked" ? "status:blocked" : "status:done";
|
|
2691
|
+
const nextTags = [
|
|
2692
|
+
...new Set(currentTags.filter((t) => !t.startsWith("status:")))
|
|
2693
|
+
].concat(target);
|
|
2694
|
+
await runApi(
|
|
2695
|
+
"Flipping task status",
|
|
2696
|
+
async () => client.PATCH("/memories/{memoryId}/metadata", {
|
|
2697
|
+
params: { path: { memoryId: opts.task } },
|
|
2698
|
+
body: {
|
|
2699
|
+
memoryId: opts.task,
|
|
2700
|
+
source: opts.source,
|
|
2701
|
+
tags: nextTags
|
|
2702
|
+
}
|
|
2703
|
+
})
|
|
2704
|
+
);
|
|
2705
|
+
taskStatus = target.slice("status:".length);
|
|
2706
|
+
}
|
|
2707
|
+
const view = resolveViewUrl(cfg.baseUrl, closeout.url);
|
|
2708
|
+
const result = {
|
|
2709
|
+
closeoutId: closeout.id,
|
|
2710
|
+
edgeId: edge.id,
|
|
2711
|
+
taskStatus: taskStatus ?? null,
|
|
2712
|
+
verdict,
|
|
2713
|
+
tags
|
|
2714
|
+
};
|
|
2715
|
+
emitAction(
|
|
2716
|
+
`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}` : ""}`,
|
|
2717
|
+
result,
|
|
2718
|
+
json
|
|
2719
|
+
);
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2411
2723
|
// src/commands/continuity.ts
|
|
2412
2724
|
function registerContinuity(program2) {
|
|
2413
2725
|
const continuity = program2.command("continuity").description("Continuity snapshots: checkpoint and resume work");
|
|
@@ -2568,6 +2880,108 @@ Examples:
|
|
|
2568
2880
|
});
|
|
2569
2881
|
}
|
|
2570
2882
|
|
|
2883
|
+
// src/commands/decomposition.ts
|
|
2884
|
+
function registerDecomposition(program2) {
|
|
2885
|
+
const decomposition = program2.command("decomposition").description(
|
|
2886
|
+
"Drive a WLP decomposition: decompose a brief, then execute / accept / reject"
|
|
2887
|
+
);
|
|
2888
|
+
decomposition.addHelpText(
|
|
2889
|
+
"after",
|
|
2890
|
+
`
|
|
2891
|
+
Examples:
|
|
2892
|
+
$ sechroom decomposition decompose mem_XXXX
|
|
2893
|
+
$ sechroom decomposition execute sug_XXXX
|
|
2894
|
+
$ sechroom decomposition publish-run sug_XXXX
|
|
2895
|
+
$ sechroom decomposition accept sug_XXXX
|
|
2896
|
+
$ sechroom decomposition reject sug_XXXX --reason "wrong shape"`
|
|
2897
|
+
);
|
|
2898
|
+
decomposition.command("decompose <briefId>").description(
|
|
2899
|
+
"Decompose a work brief into a candidate Task graph (POST /work-briefs/{id}/decompose)"
|
|
2900
|
+
).action(async (briefId, _opts, cmd) => {
|
|
2901
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2902
|
+
const data = await runApi("Queueing decomposition", async () => {
|
|
2903
|
+
const client = await makeClient(cfg);
|
|
2904
|
+
return client.POST("/work-briefs/{id}/decompose", {
|
|
2905
|
+
params: { path: { id: briefId } },
|
|
2906
|
+
body: { id: briefId }
|
|
2907
|
+
});
|
|
2908
|
+
});
|
|
2909
|
+
emitAction(
|
|
2910
|
+
`queued decomposition of ${style.bold(briefId)} \u2192 ${style.bold(data.suggestionId)}`,
|
|
2911
|
+
data,
|
|
2912
|
+
cmd.optsWithGlobals().json
|
|
2913
|
+
);
|
|
2914
|
+
});
|
|
2915
|
+
decomposition.command("execute <decompositionId>").description(
|
|
2916
|
+
"Execute a decomposition's Task graph (POST /decompositions/{id}/execute)"
|
|
2917
|
+
).action(async (decompositionId, _opts, cmd) => {
|
|
2918
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2919
|
+
const data = await runApi("Executing decomposition", async () => {
|
|
2920
|
+
const client = await makeClient(cfg);
|
|
2921
|
+
return client.POST("/decompositions/{id}/execute", {
|
|
2922
|
+
params: { path: { id: decompositionId } },
|
|
2923
|
+
body: {}
|
|
2924
|
+
});
|
|
2925
|
+
});
|
|
2926
|
+
emitAction(
|
|
2927
|
+
`executed ${style.bold(decompositionId)} \u2192 run ${style.bold(data.runRecordId)} (${data.outcome})`,
|
|
2928
|
+
data,
|
|
2929
|
+
cmd.optsWithGlobals().json
|
|
2930
|
+
);
|
|
2931
|
+
});
|
|
2932
|
+
decomposition.command("publish-run <decompositionId>").description(
|
|
2933
|
+
"Publish an accepted decomposition's context pack on demand (POST /decompositions/{id}/publish-run)"
|
|
2934
|
+
).action(async (decompositionId, _opts, cmd) => {
|
|
2935
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2936
|
+
const data = await runApi("Publishing context pack", async () => {
|
|
2937
|
+
const client = await makeClient(cfg);
|
|
2938
|
+
return client.POST("/decompositions/{id}/publish-run", {
|
|
2939
|
+
params: { path: { id: decompositionId } },
|
|
2940
|
+
body: {}
|
|
2941
|
+
});
|
|
2942
|
+
});
|
|
2943
|
+
emitAction(
|
|
2944
|
+
`published ${style.bold(decompositionId)} \u2192 run ${style.bold(data.runRecordId)} (${data.outcome})`,
|
|
2945
|
+
data,
|
|
2946
|
+
cmd.optsWithGlobals().json
|
|
2947
|
+
);
|
|
2948
|
+
});
|
|
2949
|
+
decomposition.command("accept <decompositionId>").description(
|
|
2950
|
+
"Accept a Pending decomposition \u2014 promote + ratify its Tasks (POST /decompositions/{id}/accept)"
|
|
2951
|
+
).action(async (decompositionId, _opts, cmd) => {
|
|
2952
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2953
|
+
const data = await runApi("Accepting decomposition", async () => {
|
|
2954
|
+
const client = await makeClient(cfg);
|
|
2955
|
+
return client.POST("/decompositions/{id}/accept", {
|
|
2956
|
+
params: { path: { id: decompositionId } },
|
|
2957
|
+
body: {}
|
|
2958
|
+
});
|
|
2959
|
+
});
|
|
2960
|
+
emitAction(
|
|
2961
|
+
`accepted decomposition ${style.bold(decompositionId)}`,
|
|
2962
|
+
data,
|
|
2963
|
+
cmd.optsWithGlobals().json
|
|
2964
|
+
);
|
|
2965
|
+
});
|
|
2966
|
+
decomposition.command("reject <decompositionId>").description(
|
|
2967
|
+
"Reject a Pending decomposition \u2014 archive its Tasks, bounce the brief (POST /decompositions/{id}/reject)"
|
|
2968
|
+
).option("--reason <reason>", "Optional free-text rejection reason").action(async (decompositionId, opts, cmd) => {
|
|
2969
|
+
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
2970
|
+
const data = await runApi("Rejecting decomposition", async () => {
|
|
2971
|
+
const client = await makeClient(cfg);
|
|
2972
|
+
return client.POST("/decompositions/{id}/reject", {
|
|
2973
|
+
params: { path: { id: decompositionId } },
|
|
2974
|
+
body: { reasonText: opts.reason ?? null }
|
|
2975
|
+
});
|
|
2976
|
+
});
|
|
2977
|
+
emitAction(
|
|
2978
|
+
`rejected decomposition ${style.bold(decompositionId)}`,
|
|
2979
|
+
data,
|
|
2980
|
+
cmd.optsWithGlobals().json
|
|
2981
|
+
);
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2571
2985
|
// src/commands/filing.ts
|
|
2572
2986
|
function registerFiling(program2) {
|
|
2573
2987
|
const filing = program2.command("filing").description("Review and act on filing suggestions");
|
|
@@ -3079,7 +3493,7 @@ Examples:
|
|
|
3079
3493
|
|
|
3080
3494
|
// src/setup/apply.ts
|
|
3081
3495
|
import { createHash as createHash3 } from "crypto";
|
|
3082
|
-
import { mkdirSync as mkdirSync9, readFileSync as
|
|
3496
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync9, existsSync as existsSync9 } from "fs";
|
|
3083
3497
|
import { dirname as dirname8 } from "path";
|
|
3084
3498
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
3085
3499
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
@@ -3137,7 +3551,7 @@ function ensureDir2(path) {
|
|
|
3137
3551
|
}
|
|
3138
3552
|
function readOr(path, fallback) {
|
|
3139
3553
|
try {
|
|
3140
|
-
return
|
|
3554
|
+
return readFileSync8(path, "utf8");
|
|
3141
3555
|
} catch {
|
|
3142
3556
|
return fallback;
|
|
3143
3557
|
}
|
|
@@ -3148,7 +3562,7 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
3148
3562
|
let current = {};
|
|
3149
3563
|
if (existed) {
|
|
3150
3564
|
try {
|
|
3151
|
-
current = JSON.parse(
|
|
3565
|
+
current = JSON.parse(readFileSync8(path, "utf8"));
|
|
3152
3566
|
} catch {
|
|
3153
3567
|
return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
|
|
3154
3568
|
}
|
|
@@ -3864,7 +4278,7 @@ import { basename as basename2, join as join13 } from "path";
|
|
|
3864
4278
|
|
|
3865
4279
|
// src/commands/fanout.ts
|
|
3866
4280
|
import { spawnSync } from "child_process";
|
|
3867
|
-
import { existsSync as existsSync10, readFileSync as
|
|
4281
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
3868
4282
|
import { isAbsolute, join as join12, resolve } from "path";
|
|
3869
4283
|
var ICON = {
|
|
3870
4284
|
refresh: "\u21BB",
|
|
@@ -3899,7 +4313,7 @@ function readManifest(path) {
|
|
|
3899
4313
|
if (!existsSync10(path)) return null;
|
|
3900
4314
|
let parsed;
|
|
3901
4315
|
try {
|
|
3902
|
-
parsed = JSON.parse(
|
|
4316
|
+
parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
3903
4317
|
} catch (err2) {
|
|
3904
4318
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
3905
4319
|
}
|
|
@@ -4713,24 +5127,45 @@ Examples:
|
|
|
4713
5127
|
$ sechroom relationship suggestions --status Pending --memory mem_XXXX
|
|
4714
5128
|
$ sechroom relationship suggestion accept rsg_XXXX`
|
|
4715
5129
|
);
|
|
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").
|
|
5130
|
+
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(
|
|
5131
|
+
"--to-version <number>",
|
|
5132
|
+
"Version of the target memory to pin the edge to (defaults to the target's current version)"
|
|
5133
|
+
).action(async (fromMemoryId, toMemoryId, opts, cmd) => {
|
|
4717
5134
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
4718
|
-
const
|
|
4719
|
-
|
|
4720
|
-
|
|
5135
|
+
const client = await makeClient(cfg);
|
|
5136
|
+
let toVersion;
|
|
5137
|
+
if (opts.toVersion !== void 0) {
|
|
5138
|
+
toVersion = Number(opts.toVersion);
|
|
5139
|
+
if (!Number.isInteger(toVersion) || toVersion < 1) {
|
|
5140
|
+
fail(`--to-version must be a positive integer (got '${opts.toVersion}').`);
|
|
5141
|
+
}
|
|
5142
|
+
} else {
|
|
5143
|
+
const target = await runApi(
|
|
5144
|
+
"Resolving target version",
|
|
5145
|
+
async () => client.GET("/memories/{memoryId}", { params: { path: { memoryId: toMemoryId } } })
|
|
5146
|
+
);
|
|
5147
|
+
if (typeof target.item?.currentVersion !== "number") {
|
|
5148
|
+
fail(`Could not resolve the current version of ${toMemoryId}; pass --to-version explicitly.`);
|
|
5149
|
+
}
|
|
5150
|
+
toVersion = target.item.currentVersion;
|
|
5151
|
+
}
|
|
5152
|
+
const data = await runApi(
|
|
5153
|
+
"Creating relationship",
|
|
5154
|
+
async () => client.POST("/memories/{memoryId}/relationships", {
|
|
4721
5155
|
params: { path: { memoryId: fromMemoryId } },
|
|
4722
5156
|
body: {
|
|
4723
5157
|
toMemoryId,
|
|
4724
|
-
type: opts.type
|
|
5158
|
+
type: opts.type,
|
|
5159
|
+
toVersion
|
|
4725
5160
|
}
|
|
4726
|
-
})
|
|
4727
|
-
|
|
5161
|
+
})
|
|
5162
|
+
);
|
|
4728
5163
|
const inversePart = data.inverseId ? ` ${style.dim(`(inverse ${data.inverseId})`)}` : "";
|
|
4729
5164
|
const view = resolveViewUrl(cfg.baseUrl, data.url);
|
|
4730
5165
|
const urlPart = view ? ` ${style.dim("\u2192")} ${view}` : "";
|
|
4731
5166
|
emitAction(
|
|
4732
|
-
`created relationship ${style.bold(data.id)} ${style.dim(`${fromMemoryId} \u2192 ${toMemoryId}`)}${inversePart}${urlPart}`,
|
|
4733
|
-
data,
|
|
5167
|
+
`created relationship ${style.bold(data.id)} ${style.dim(`${fromMemoryId} \u2192 ${toMemoryId} @v${toVersion}`)}${inversePart}${urlPart}`,
|
|
5168
|
+
{ ...data, toVersion },
|
|
4734
5169
|
cmd.optsWithGlobals().json
|
|
4735
5170
|
);
|
|
4736
5171
|
});
|
|
@@ -4854,7 +5289,7 @@ Examples:
|
|
|
4854
5289
|
// src/commands/reset.ts
|
|
4855
5290
|
import { homedir as homedir4 } from "os";
|
|
4856
5291
|
import { join as join14 } from "path";
|
|
4857
|
-
import { existsSync as existsSync12, readFileSync as
|
|
5292
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10, rmSync as rmSync3 } from "fs";
|
|
4858
5293
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
4859
5294
|
var localSkillsDir = () => join14(process.cwd(), ".claude", "skills");
|
|
4860
5295
|
var globalSkillsDir = () => join14(homedir4(), ".claude", "skills");
|
|
@@ -4865,7 +5300,7 @@ function removeMaterialisedSkills(dir) {
|
|
|
4865
5300
|
const lockPath = join14(dir, SKILLS_LOCK2);
|
|
4866
5301
|
if (!existsSync12(lockPath)) return removed;
|
|
4867
5302
|
try {
|
|
4868
|
-
const lock = JSON.parse(
|
|
5303
|
+
const lock = JSON.parse(readFileSync10(lockPath, "utf8"));
|
|
4869
5304
|
for (const entry of Object.values(lock)) {
|
|
4870
5305
|
for (const name of entry.skills ?? []) {
|
|
4871
5306
|
const p = join14(dir, name);
|
|
@@ -5265,7 +5700,7 @@ Examples:
|
|
|
5265
5700
|
import {
|
|
5266
5701
|
existsSync as existsSync15,
|
|
5267
5702
|
mkdirSync as mkdirSync12,
|
|
5268
|
-
readFileSync as
|
|
5703
|
+
readFileSync as readFileSync11,
|
|
5269
5704
|
rmSync as rmSync4,
|
|
5270
5705
|
writeFileSync as writeFileSync12
|
|
5271
5706
|
} from "fs";
|
|
@@ -5480,7 +5915,7 @@ function findBinding(start) {
|
|
|
5480
5915
|
if (existsSync15(path)) {
|
|
5481
5916
|
try {
|
|
5482
5917
|
const b = JSON.parse(
|
|
5483
|
-
|
|
5918
|
+
readFileSync11(path, "utf8")
|
|
5484
5919
|
);
|
|
5485
5920
|
if (b.decompositionId && b.taskId)
|
|
5486
5921
|
return { decompositionId: b.decompositionId, taskId: b.taskId };
|
|
@@ -5499,7 +5934,7 @@ function parseTranscript(path) {
|
|
|
5499
5934
|
let tokensOut = 0;
|
|
5500
5935
|
let contextUsed = 0;
|
|
5501
5936
|
let model = "";
|
|
5502
|
-
for (const line of
|
|
5937
|
+
for (const line of readFileSync11(path, "utf8").split("\n")) {
|
|
5503
5938
|
if (!line.trim()) continue;
|
|
5504
5939
|
let obj;
|
|
5505
5940
|
try {
|
|
@@ -5516,11 +5951,12 @@ function parseTranscript(path) {
|
|
|
5516
5951
|
if (obj.message?.model) model = obj.message.model;
|
|
5517
5952
|
}
|
|
5518
5953
|
if (tokensIn === 0 && tokensOut === 0) return null;
|
|
5519
|
-
return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model) };
|
|
5954
|
+
return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model, contextUsed) };
|
|
5520
5955
|
}
|
|
5521
|
-
function windowFor(model) {
|
|
5956
|
+
function windowFor(model, contextUsed = 0) {
|
|
5522
5957
|
const m = model.toLowerCase();
|
|
5523
|
-
|
|
5958
|
+
if (m.includes("[1m]") || m.includes("-1m")) return 1e6;
|
|
5959
|
+
return contextUsed > 2e5 ? 1e6 : 2e5;
|
|
5524
5960
|
}
|
|
5525
5961
|
async function readStdin2() {
|
|
5526
5962
|
if (process.stdin.isTTY) return "";
|
|
@@ -5756,7 +6192,7 @@ Examples:
|
|
|
5756
6192
|
function resolveVersion() {
|
|
5757
6193
|
try {
|
|
5758
6194
|
const pkg = JSON.parse(
|
|
5759
|
-
|
|
6195
|
+
readFileSync12(new URL("../package.json", import.meta.url), "utf8")
|
|
5760
6196
|
);
|
|
5761
6197
|
return pkg.version ?? "0.0.0";
|
|
5762
6198
|
} catch {
|
|
@@ -5914,6 +6350,8 @@ registerLookup(program);
|
|
|
5914
6350
|
registerRelationships(program);
|
|
5915
6351
|
registerWorkspace(program);
|
|
5916
6352
|
registerProject(program);
|
|
6353
|
+
registerDecomposition(program);
|
|
6354
|
+
registerClose(program);
|
|
5917
6355
|
registerFiling(program);
|
|
5918
6356
|
registerContinuity(program);
|
|
5919
6357
|
registerCheckpoint(program);
|
package/package.json
CHANGED