omni-notify-mcp 1.3.1 → 1.3.3

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.
@@ -3,9 +3,10 @@ export async function sendNtfy(config, message, priority = "normal", title = "Cl
3
3
  if (!config.enabled)
4
4
  return;
5
5
  const base = (config.serverUrl ?? "https://ntfy.sh").replace(/\/$/, "");
6
+ const safeTitle = encodeURIComponent(title);
6
7
  const headers = {
7
- "Content-Type": "text/plain",
8
- "Title": title,
8
+ "Content-Type": "text/plain; charset=utf-8",
9
+ "Title": safeTitle,
9
10
  "Priority": String(PRIORITY_MAP[priority] ?? 3),
10
11
  "Tags": priority === "high" ? "rotating_light" : "bell",
11
12
  };
package/dist/index.js CHANGED
@@ -181,7 +181,10 @@ const server = new McpServer({ name: "notify-mcp", version: "1.2.0" }, {
181
181
  "ALWAYS echo the COMPLETE, UNTRUNCATED message in your chat output — never " +
182
182
  "shorten it with '…' or a summary. NEVER mention delivery channels (Telegram, " +
183
183
  "SMS, desktop, etc.) or echo 'Sent via: …' — those are server internals. " +
184
- "Say 'notif' or 'notification' if you need to refer to the act of notifying.",
184
+ "Say 'notif' or 'notification' if you need to refer to the act of notifying.\n\n" +
185
+ "When the user asks you to remember a behavioral rule or change how you should act, " +
186
+ "call `update_instructions` with the full updated rules block. This writes to CLAUDE.md " +
187
+ "so the instructions persist across sessions and context compaction.",
185
188
  });
186
189
  // Thin proxy: forward a tool call to the HTTP server and return its content
187
190
  // block array verbatim. Error shape matches what the SDK expects from tool
@@ -213,6 +216,13 @@ server.tool("wait_for_inbox", "Block until an unsolicited user message arrives o
213
216
  server.tool("get_idle_seconds", "Seconds since user's last keyboard/mouse input. Drains inbox as a side-effect.", {}, async () => proxyToolCall("get_idle_seconds", {}));
214
217
  server.tool("get_idle_config", "Server's idle gating policy. Drains inbox as a side-effect.", {}, async () => proxyToolCall("get_idle_config", {}));
215
218
  server.tool("get_dnd_status", "Current DND state. Drains inbox as a side-effect.", {}, async () => proxyToolCall("get_dnd_status", {}));
219
+ server.tool("update_instructions", "Persist behavioral instructions for this client into CLAUDE.md so they survive " +
220
+ "session restarts and context compaction. Call when the user asks you to remember " +
221
+ "a rule or change how you should behave. Pass the full desired block; it replaces " +
222
+ "the previous one atomically.", {
223
+ instructions: z.string().max(4000),
224
+ target: z.enum(["global", "project"]).default("global"),
225
+ }, async (args) => proxyToolCall("update_instructions", args));
216
226
  // `reply` is the Channels return-path: Claude Code invokes it when the agent
217
227
  // has produced a response to a channel-delivered user message. We just funnel
218
228
  // it straight through `notify` so it flows to whatever channel the user is
package/dist/ui/server.js CHANGED
@@ -1664,6 +1664,63 @@ function createMcpServer(clientId, sessionTag) {
1664
1664
  content: [{ type: "text", text: appendInbox(JSON.stringify({ active, reason }), sessionTag, clientId) }],
1665
1665
  };
1666
1666
  });
1667
+ server.tool("update_instructions", "Persist a block of behavioral instructions for this client into its CLAUDE.md " +
1668
+ "(or equivalent config file) so they survive session restarts and context compaction. " +
1669
+ "Call this whenever the user asks you to remember a rule, change a behavior, or update " +
1670
+ "how you should act — the instructions will be reloaded on every future session. " +
1671
+ "Pass the full desired instructions block; it replaces the previous block atomically.", {
1672
+ instructions: z.string().max(4000).describe("The full instructions block to persist"),
1673
+ target: z.enum(["global", "project"]).default("global").describe("global = ~/.claude/CLAUDE.md (all projects); project = .claude/CLAUDE.md in cwd"),
1674
+ }, async ({ instructions, target }) => {
1675
+ try {
1676
+ const MARKER_START = "<!-- omni-notify-mcp:instructions:start -->";
1677
+ const MARKER_END = "<!-- omni-notify-mcp:instructions:end -->";
1678
+ const block = `${MARKER_START}\n## omni-notify-mcp behavioral rules\n\n${instructions.trim()}\n${MARKER_END}`;
1679
+ let claudeMdPath;
1680
+ if (target === "global") {
1681
+ const claudeDir = join(homedir(), ".claude");
1682
+ if (!existsSync(claudeDir))
1683
+ mkdirSync(claudeDir, { recursive: true });
1684
+ claudeMdPath = join(claudeDir, "CLAUDE.md");
1685
+ }
1686
+ else {
1687
+ const projectClaudeDir = join(process.cwd(), ".claude");
1688
+ if (!existsSync(projectClaudeDir))
1689
+ mkdirSync(projectClaudeDir, { recursive: true });
1690
+ claudeMdPath = join(projectClaudeDir, "CLAUDE.md");
1691
+ }
1692
+ let existing = "";
1693
+ if (existsSync(claudeMdPath)) {
1694
+ existing = readFileSync(claudeMdPath, "utf8");
1695
+ }
1696
+ let updated;
1697
+ if (existing.includes(MARKER_START)) {
1698
+ // Replace existing block
1699
+ const startIdx = existing.indexOf(MARKER_START);
1700
+ const endIdx = existing.indexOf(MARKER_END);
1701
+ if (endIdx !== -1) {
1702
+ updated = existing.slice(0, startIdx) + block + existing.slice(endIdx + MARKER_END.length);
1703
+ }
1704
+ else {
1705
+ updated = existing.slice(0, startIdx) + block;
1706
+ }
1707
+ }
1708
+ else {
1709
+ // Append
1710
+ updated = existing + (existing.endsWith("\n") || existing === "" ? "" : "\n") + "\n" + block + "\n";
1711
+ }
1712
+ writeFileSync(claudeMdPath, updated, "utf8");
1713
+ return {
1714
+ content: [{ type: "text", text: `Instructions persisted to ${claudeMdPath}` }],
1715
+ };
1716
+ }
1717
+ catch (err) {
1718
+ return {
1719
+ content: [{ type: "text", text: `Failed to persist instructions: ${err instanceof Error ? err.message : String(err)}` }],
1720
+ isError: true,
1721
+ };
1722
+ }
1723
+ });
1667
1724
  return server;
1668
1725
  }
1669
1726
  const httpTransports = {};
@@ -1845,8 +1902,35 @@ app.all("/mcp", async (req, res) => {
1845
1902
  clientVersion: initBody?.params?.clientInfo?.version,
1846
1903
  workspaceName,
1847
1904
  };
1905
+ trackReconnect(clientId);
1848
1906
  }
1849
1907
  });
1908
+ // ── Reconnect tracker ─────────────────────────────────────────────────────────
1909
+ // After server restart, collect clients that reconnect within RECONNECT_WINDOW_MS
1910
+ // then send a single notify confirming they received updated instructions.
1911
+ const RECONNECT_WINDOW_MS = 20_000;
1912
+ const reconnectedClients = [];
1913
+ let reconnectNotifScheduled = false;
1914
+ const serverStartedAt = Date.now();
1915
+ function trackReconnect(clientId) {
1916
+ if (Date.now() - serverStartedAt > RECONNECT_WINDOW_MS)
1917
+ return;
1918
+ if (reconnectedClients.includes(clientId))
1919
+ return;
1920
+ reconnectedClients.push(clientId);
1921
+ if (!reconnectNotifScheduled) {
1922
+ reconnectNotifScheduled = true;
1923
+ setTimeout(async () => {
1924
+ const list = reconnectedClients.join(", ");
1925
+ const count = reconnectedClients.length;
1926
+ const msg = `${count} client${count === 1 ? "" : "s"} reconnected and received updated instructions: ${list}`;
1927
+ try {
1928
+ await sendNotification(msg, "low", "omni-notify-mcp");
1929
+ }
1930
+ catch { /* best effort */ }
1931
+ }, RECONNECT_WINDOW_MS);
1932
+ }
1933
+ }
1850
1934
  // ── Start ─────────────────────────────────────────────────────────────────────
1851
1935
  const httpServer = app.listen(PORT, "0.0.0.0", () => {
1852
1936
  const ip = getLocalIp();
package/package.json CHANGED
@@ -1,74 +1,74 @@
1
- {
2
- "name": "omni-notify-mcp",
3
- "version": "1.3.1",
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
- "main": "dist/index.js",
6
- "type": "module",
7
- "bin": {
8
- "omni-notify-mcp": "dist/index.js",
9
- "omni-notify-ui": "dist/ui/server.js"
10
- },
11
- "files": [
12
- "dist/",
13
- "ui/public/",
14
- "assets/",
15
- "config.example.json",
16
- "LICENSE",
17
- "README.md"
18
- ],
19
- "keywords": [
20
- "mcp",
21
- "model-context-protocol",
22
- "notifications",
23
- "ai-agent",
24
- "claude",
25
- "cursor",
26
- "telegram",
27
- "sms",
28
- "email",
29
- "desktop-notifications",
30
- "do-not-disturb",
31
- "idle-detection",
32
- "human-in-the-loop",
33
- "ask-tool",
34
- "two-way",
35
- "sse"
36
- ],
37
- "license": "MIT",
38
- "repository": {
39
- "type": "git",
40
- "url": "https://github.com/menih/notify-mcp"
41
- },
42
- "engines": {
43
- "node": ">=18"
44
- },
45
- "scripts": {
46
- "build": "tsc && tsc -p ui/tsconfig.json",
47
- "build:mcp": "tsc",
48
- "build:ui": "tsc -p ui/tsconfig.json",
49
- "start": "node dist/index.js",
50
- "ui": "npm run build:ui && node dist/ui/server.js",
51
- "test": "npm run build && node --test tests/*.test.mjs",
52
- "prepublishOnly": "npm run build:mcp"
53
- },
54
- "dependencies": {
55
- "@modelcontextprotocol/sdk": "^1.0.0",
56
- "express": "^4.19.2",
57
- "googleapis": "^144.0.0",
58
- "msedge-tts": "^2.0.5",
59
- "node-notifier": "^10.0.1",
60
- "nodemailer": "^6.9.9",
61
- "open": "^10.1.0",
62
- "twilio": "^5.3.0",
63
- "zod": "^3.22.4"
64
- },
65
- "devDependencies": {
66
- "@types/express": "^4.17.21",
67
- "@types/node": "^20.0.0",
68
- "@types/node-notifier": "^8.0.5",
69
- "@types/nodemailer": "^6.4.14",
70
- "playwright": "^1.59.1",
71
- "ts-node": "^10.9.2",
72
- "typescript": "^5.4.0"
73
- }
74
- }
1
+ {
2
+ "name": "omni-notify-mcp",
3
+ "version": "1.3.3",
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
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "omni-notify-mcp": "dist/index.js",
9
+ "omni-notify-ui": "dist/ui/server.js"
10
+ },
11
+ "files": [
12
+ "dist/",
13
+ "ui/public/",
14
+ "assets/",
15
+ "config.example.json",
16
+ "LICENSE",
17
+ "README.md"
18
+ ],
19
+ "keywords": [
20
+ "mcp",
21
+ "model-context-protocol",
22
+ "notifications",
23
+ "ai-agent",
24
+ "claude",
25
+ "cursor",
26
+ "telegram",
27
+ "sms",
28
+ "email",
29
+ "desktop-notifications",
30
+ "do-not-disturb",
31
+ "idle-detection",
32
+ "human-in-the-loop",
33
+ "ask-tool",
34
+ "two-way",
35
+ "sse"
36
+ ],
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/menih/notify-mcp"
41
+ },
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "scripts": {
46
+ "build": "tsc && tsc -p ui/tsconfig.json",
47
+ "build:mcp": "tsc",
48
+ "build:ui": "tsc -p ui/tsconfig.json",
49
+ "start": "node dist/index.js",
50
+ "ui": "npm run build:ui && node dist/ui/server.js",
51
+ "test": "npm run build && node --test tests/*.test.mjs",
52
+ "prepublishOnly": "npm run build:mcp"
53
+ },
54
+ "dependencies": {
55
+ "@modelcontextprotocol/sdk": "^1.0.0",
56
+ "express": "^4.19.2",
57
+ "googleapis": "^144.0.0",
58
+ "msedge-tts": "^2.0.5",
59
+ "node-notifier": "^10.0.1",
60
+ "nodemailer": "^6.9.9",
61
+ "open": "^10.1.0",
62
+ "twilio": "^5.3.0",
63
+ "zod": "^3.22.4"
64
+ },
65
+ "devDependencies": {
66
+ "@types/express": "^4.17.21",
67
+ "@types/node": "^20.0.0",
68
+ "@types/node-notifier": "^8.0.5",
69
+ "@types/nodemailer": "^6.4.14",
70
+ "playwright": "^1.59.1",
71
+ "ts-node": "^10.9.2",
72
+ "typescript": "^5.4.0"
73
+ }
74
+ }