@sechroom/cli 2026.7.5 → 2026.7.7
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 +136 -11
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1544,11 +1544,16 @@ function registerChannel(program2) {
|
|
|
1544
1544
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
1545
1545
|
const filter = readFilter(opts);
|
|
1546
1546
|
const sub = await ensureSubscription(cfg, opts.name, filter);
|
|
1547
|
-
const
|
|
1548
|
-
|
|
1547
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1548
|
+
const deliver = makeDeliver(
|
|
1549
|
+
filter,
|
|
1550
|
+
seen,
|
|
1551
|
+
(payload) => process.stdout.write(
|
|
1549
1552
|
(typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
|
|
1550
|
-
)
|
|
1551
|
-
|
|
1553
|
+
)
|
|
1554
|
+
);
|
|
1555
|
+
const conn = await openConnection(cfg, deliver);
|
|
1556
|
+
await reconcile(cfg, filter, deliver);
|
|
1552
1557
|
if (json) {
|
|
1553
1558
|
emit(
|
|
1554
1559
|
{
|
|
@@ -1585,7 +1590,8 @@ function registerChannel(program2) {
|
|
|
1585
1590
|
);
|
|
1586
1591
|
await mcp.connect(new StdioServerTransport());
|
|
1587
1592
|
await ensureSubscription(cfg, opts.name, filter);
|
|
1588
|
-
const
|
|
1593
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1594
|
+
const deliver = makeDeliver(filter, seen, (payload) => {
|
|
1589
1595
|
const { content, meta } = summarizeEvent(payload);
|
|
1590
1596
|
void mcp.notification({
|
|
1591
1597
|
method: "notifications/claude/channel",
|
|
@@ -1595,6 +1601,8 @@ function registerChannel(program2) {
|
|
|
1595
1601
|
`))
|
|
1596
1602
|
);
|
|
1597
1603
|
});
|
|
1604
|
+
const conn = await openConnection(cfg, deliver);
|
|
1605
|
+
await reconcile(cfg, filter, deliver);
|
|
1598
1606
|
process.stderr.write(
|
|
1599
1607
|
style.dim(
|
|
1600
1608
|
`sechroom channel (mcp) \u2014 tenant ${cfg.tenant}, tags [${filter.tags.join(", ")}]
|
|
@@ -1605,7 +1613,10 @@ function registerChannel(program2) {
|
|
|
1605
1613
|
});
|
|
1606
1614
|
channel.command("install").description(
|
|
1607
1615
|
"Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
|
|
1608
|
-
).option(
|
|
1616
|
+
).option(
|
|
1617
|
+
"--workspace <wsp...>",
|
|
1618
|
+
"Restrict dispatches to these workspace id(s)"
|
|
1619
|
+
).option(
|
|
1609
1620
|
"--tag <tag...>",
|
|
1610
1621
|
"Tag(s) a dispatch must carry to match (repeatable). Default targets WLP task dispatches.",
|
|
1611
1622
|
["kind:task"]
|
|
@@ -1726,20 +1737,134 @@ function holdOpen(conn) {
|
|
|
1726
1737
|
process.on("SIGTERM", stop);
|
|
1727
1738
|
});
|
|
1728
1739
|
}
|
|
1729
|
-
function
|
|
1740
|
+
function parseEvent(payload) {
|
|
1730
1741
|
let data = payload;
|
|
1731
1742
|
if (typeof payload === "string") {
|
|
1732
1743
|
try {
|
|
1733
1744
|
data = JSON.parse(payload);
|
|
1734
1745
|
} catch {
|
|
1735
|
-
return {
|
|
1746
|
+
return { eventType: "", memoryId: "", workspaceId: "", tags: void 0 };
|
|
1736
1747
|
}
|
|
1737
1748
|
}
|
|
1738
1749
|
const obj = data ?? {};
|
|
1739
1750
|
const inner = obj.data ?? obj;
|
|
1740
|
-
const
|
|
1741
|
-
|
|
1742
|
-
|
|
1751
|
+
const rawTags = inner.tags ?? inner.Tags;
|
|
1752
|
+
return {
|
|
1753
|
+
eventType: str(inner.eventType ?? inner.EventType ?? obj.type) || "substrate.event",
|
|
1754
|
+
memoryId: str(inner.memoryId ?? inner.MemoryId),
|
|
1755
|
+
workspaceId: str(inner.workspaceId ?? inner.WorkspaceId),
|
|
1756
|
+
tags: Array.isArray(rawTags) ? rawTags.filter((t) => typeof t === "string") : void 0
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
function shouldDeliver(payload, filter) {
|
|
1760
|
+
const { workspaceId, tags } = parseEvent(payload);
|
|
1761
|
+
if (filter.workspaceScope.length > 0 && (!workspaceId || !filter.workspaceScope.includes(workspaceId)))
|
|
1762
|
+
return false;
|
|
1763
|
+
if (filter.tags.length > 0) {
|
|
1764
|
+
if (!tags) return false;
|
|
1765
|
+
return facetedTagMatch(tags, filter.tags);
|
|
1766
|
+
}
|
|
1767
|
+
return true;
|
|
1768
|
+
}
|
|
1769
|
+
function makeDeliver(filter, seen, forward) {
|
|
1770
|
+
return (payload) => {
|
|
1771
|
+
if (!shouldDeliver(payload, filter)) return;
|
|
1772
|
+
const { memoryId } = parseEvent(payload);
|
|
1773
|
+
if (memoryId) {
|
|
1774
|
+
if (seen.has(memoryId)) return;
|
|
1775
|
+
seen.add(memoryId);
|
|
1776
|
+
}
|
|
1777
|
+
forward(payload);
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
async function reconcile(cfg, filter, deliver) {
|
|
1781
|
+
if (filter.workspaceScope.length === 0) {
|
|
1782
|
+
process.stderr.write(
|
|
1783
|
+
style.dim(
|
|
1784
|
+
"channel: no --workspace to reconcile against; live feed only (a dropped dispatch won't be recovered).\n"
|
|
1785
|
+
)
|
|
1786
|
+
);
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
if (filter.tags.length === 0) return;
|
|
1790
|
+
let token;
|
|
1791
|
+
try {
|
|
1792
|
+
token = await requireToken(cfg);
|
|
1793
|
+
} catch {
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
const qs = `filterTags=${encodeURIComponent(filter.tags.join(","))}&limit=100`;
|
|
1797
|
+
let recovered = 0;
|
|
1798
|
+
for (const ws of filter.workspaceScope) {
|
|
1799
|
+
try {
|
|
1800
|
+
const resp = await fetch(
|
|
1801
|
+
`${cfg.baseUrl}/workspaces/${encodeURIComponent(ws)}/memories/feed?${qs}`,
|
|
1802
|
+
{
|
|
1803
|
+
headers: {
|
|
1804
|
+
authorization: `Bearer ${token}`,
|
|
1805
|
+
tenant: cfg.tenant,
|
|
1806
|
+
"x-sechroom-surface": "cli"
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
);
|
|
1810
|
+
if (!resp.ok) {
|
|
1811
|
+
process.stderr.write(
|
|
1812
|
+
err(
|
|
1813
|
+
`channel: reconcile query for ${ws} failed (HTTP ${resp.status})
|
|
1814
|
+
`
|
|
1815
|
+
)
|
|
1816
|
+
);
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
const data = await resp.json();
|
|
1820
|
+
for (const m of data.results ?? []) {
|
|
1821
|
+
if (!m.id) continue;
|
|
1822
|
+
deliver({
|
|
1823
|
+
eventType: "reconcile",
|
|
1824
|
+
memoryId: m.id,
|
|
1825
|
+
workspaceId: ws,
|
|
1826
|
+
tags: m.tags ?? []
|
|
1827
|
+
});
|
|
1828
|
+
recovered++;
|
|
1829
|
+
}
|
|
1830
|
+
} catch (e) {
|
|
1831
|
+
process.stderr.write(
|
|
1832
|
+
err(`channel: reconcile error for ${ws}: ${String(e)}
|
|
1833
|
+
`)
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
if (recovered > 0)
|
|
1838
|
+
process.stderr.write(
|
|
1839
|
+
style.dim(
|
|
1840
|
+
`channel: reconciled ${recovered} already-queued event(s) on connect.
|
|
1841
|
+
`
|
|
1842
|
+
)
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
function facetedTagMatch(eventTags, filterTags) {
|
|
1846
|
+
const have = new Set(eventTags);
|
|
1847
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1848
|
+
for (const f of filterTags) {
|
|
1849
|
+
const ns = f.endsWith(":*") ? f.slice(0, -2) : namespaceOf(f);
|
|
1850
|
+
const group = groups.get(ns) ?? [];
|
|
1851
|
+
group.push(f);
|
|
1852
|
+
groups.set(ns, group);
|
|
1853
|
+
}
|
|
1854
|
+
for (const [ns, group] of groups) {
|
|
1855
|
+
const ok2 = group.some(
|
|
1856
|
+
(f) => f.endsWith(":*") ? eventTags.some((t) => namespaceOf(t) === ns) : have.has(f)
|
|
1857
|
+
);
|
|
1858
|
+
if (!ok2) return false;
|
|
1859
|
+
}
|
|
1860
|
+
return true;
|
|
1861
|
+
}
|
|
1862
|
+
function namespaceOf(tag) {
|
|
1863
|
+
const i = tag.indexOf(":");
|
|
1864
|
+
return i >= 0 ? tag.slice(0, i) : tag;
|
|
1865
|
+
}
|
|
1866
|
+
function summarizeEvent(payload) {
|
|
1867
|
+
const { eventType, memoryId, workspaceId } = parseEvent(payload);
|
|
1743
1868
|
const content = memoryId ? `${eventType}: ${memoryId}${workspaceId ? ` (workspace ${workspaceId})` : ""}` : typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
1744
1869
|
const meta = {};
|
|
1745
1870
|
if (eventType) meta.event_type = eventType;
|
package/package.json
CHANGED