@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.
Files changed (2) hide show
  1. package/dist/index.js +136 -11
  2. 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 conn = await openConnection(cfg, (payload) => {
1548
- process.stdout.write(
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 conn = await openConnection(cfg, (payload) => {
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("--workspace <wsp...>", "Restrict dispatches to these workspace id(s)").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 summarizeEvent(payload) {
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 { content: payload, meta: {} };
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 eventType = str(inner.eventType ?? inner.EventType ?? obj.type) || "substrate.event";
1741
- const memoryId = str(inner.memoryId ?? inner.MemoryId);
1742
- const workspaceId = str(inner.workspaceId ?? inner.WorkspaceId);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sechroom/cli",
3
- "version": "2026.7.5",
3
+ "version": "2026.7.7",
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",