omni-notify-mcp 1.3.11 → 1.3.12

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 CHANGED
@@ -33,7 +33,33 @@ import { fileURLToPath } from "url";
33
33
  import { z } from "zod";
34
34
  const PORT = process.env.NOTIFY_MCP_PORT ? parseInt(process.env.NOTIFY_MCP_PORT) : 3737;
35
35
  const BASE = `http://localhost:${PORT}`;
36
- const VSC_ID = (process.env.NOTIFY_MCP_TAG || basename(process.cwd())).toLowerCase().replace(/[^a-z0-9_-]/g, "");
36
+ const CLEAN_ID = (s) => s.toLowerCase().replace(/[^a-z0-9_-]/g, "");
37
+ // NOTIFY_MCP_TAG is the explicit per-window name; otherwise use the nearest
38
+ // meaningful working-dir folder, skipping generic launcher/tool/system dirs so a
39
+ // bridge spawned from an editor's launch dir names itself after the project
40
+ // (e.g. "bullseyenotify"), never the host ("claude-code").
41
+ function deriveVscId() {
42
+ const explicit = CLEAN_ID(process.env.NOTIFY_MCP_TAG ?? "");
43
+ if (explicit)
44
+ return explicit;
45
+ const generic = new Set([
46
+ "claude-code", "claude", "code", "cursor", "vscode", "windsurf",
47
+ "bin", "dist", "build", "src", "out", "node_modules",
48
+ "windows", "system32", "users", "appdata", "roaming", "local", "programs", "temp", "tmp",
49
+ ]);
50
+ let dir = process.cwd();
51
+ for (let i = 0; i < 5; i++) {
52
+ const name = CLEAN_ID(basename(dir));
53
+ if (name && !generic.has(name))
54
+ return name;
55
+ const parent = dirname(dir);
56
+ if (!parent || parent === dir)
57
+ break;
58
+ dir = parent;
59
+ }
60
+ return CLEAN_ID(basename(process.cwd())) || "agent";
61
+ }
62
+ const VSC_ID = deriveVscId();
37
63
  const SESSION_TAG = `${hostname().toLowerCase().replace(/[^a-z0-9_-]/g, "")}-${VSC_ID}`;
38
64
  const CLIENT_NAME = "claude-channel-bridge";
39
65
  // ── 1. Ensure the HTTP server is up ───────────────────────────────────────────
package/dist/ui/server.js CHANGED
@@ -1659,18 +1659,34 @@ async function slackPost(text) {
1659
1659
  catch { /* webhook post is best-effort */ }
1660
1660
  }
1661
1661
  function slackClientTags() {
1662
+ pruneDeadSessions();
1662
1663
  const tags = new Set();
1663
1664
  for (const sess of listActiveSessions())
1664
1665
  if (sess.tag)
1665
1666
  tags.add(sess.tag);
1666
- for (const c of inboxStreamClients)
1667
+ for (const c of inboxStreamClients) {
1668
+ if (c.res.destroyed || c.res.writableEnded || !c.res.writable)
1669
+ continue;
1667
1670
  if (c.tag)
1668
1671
  tags.add(c.tag);
1672
+ }
1669
1673
  for (const [, w] of inboxWaiters)
1670
1674
  if (w.tag)
1671
1675
  tags.add(w.tag);
1672
1676
  return [...tags].sort();
1673
1677
  }
1678
+ // How many live agents would actually receive a message with this tag (undefined
1679
+ // = broadcast). Counts live SSE subscribers, parked long-poll waiters, and MCP
1680
+ // sessions. Used to gate the Slack ack: a "ack" posted when nobody is connected
1681
+ // is a lie — the message only sits queued for the next connector.
1682
+ function liveListenerCount(tag) {
1683
+ pruneDeadSessions();
1684
+ let waiters = 0;
1685
+ for (const [, w] of inboxWaiters)
1686
+ if (!tag || w.tag === tag)
1687
+ waiters++;
1688
+ return sseSubscribersForTag(tag) + waiters + sessionsMatchingTag(tag).length;
1689
+ }
1674
1690
  function slackClientsNumbered() {
1675
1691
  const tags = slackClientTags();
1676
1692
  return tags.length ? tags.map((t, i) => `${i + 1}. ${t}`).join("\n") : "(none connected)";
@@ -1724,11 +1740,13 @@ async function pollSlackOnce(token, channel) {
1724
1740
  continue;
1725
1741
  }
1726
1742
  ingestInboxEntry({ text: msg, ts: new Date().toISOString(), tag, origin: "slack" }, "slack");
1727
- await slackPost(sessionBusyNote(tag) || "ack");
1743
+ if (liveListenerCount(tag) > 0)
1744
+ await slackPost(sessionBusyNote(tag) || "ack");
1728
1745
  }
1729
1746
  else {
1730
1747
  ingestInboxEntry({ text, ts: new Date().toISOString(), origin: "slack" }, "slack");
1731
- await slackPost("ack");
1748
+ if (liveListenerCount(undefined) > 0)
1749
+ await slackPost("ack");
1732
1750
  }
1733
1751
  }
1734
1752
  const newest = all.reduce((acc, m) => (Number(m.ts) > Number(acc || 0) ? String(m.ts) : acc), slackCursor);
@@ -2433,8 +2451,11 @@ const httpServer = app.listen(PORT, "0.0.0.0", () => {
2433
2451
  else {
2434
2452
  console.log(" MCP endpoint (remote) → disabled (set ENABLE_MCP=1 to enable)\n");
2435
2453
  }
2436
- startTelegramListener();
2437
- startSlackListener();
2454
+ // Live Slack/Telegram pollers hit real external channels — never under test.
2455
+ if (process.env.NOTIFY_MCP_TEST_ENDPOINTS !== "1") {
2456
+ startTelegramListener();
2457
+ startSlackListener();
2458
+ }
2438
2459
  open(`http://localhost:${PORT}`).catch(() => { });
2439
2460
  });
2440
2461
  // TCP-level keepalive on every incoming socket. Without this, a client that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omni-notify-mcp",
3
- "version": "1.3.11",
3
+ "version": "1.3.12",
4
4
  "description": "An MCP server that lets AI agents (Claude, Cursor, etc.) reach you on any channel — desktop, Telegram, SMS, email — with two-way ask/reply, real-time inbox push, Do Not Disturb, idle gating, multi-session routing, and a one-page web UI for setup. Zero config code; configure once, agents call notify/ask.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",