omni-notify-mcp 1.1.4 → 1.1.5
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/README.md +1 -1
- package/assets/screenshots/help-page.png +0 -0
- package/assets/screenshots/main-ui.png +0 -0
- package/dist/channels/desktop.js +30 -0
- package/dist/ui/server.js +37 -0
- package/package.json +2 -1
- package/ui/public/app.js +13 -0
- package/ui/public/index.html +8 -0
package/README.md
CHANGED
|
@@ -86,7 +86,7 @@ Priority routing for `notify`:
|
|
|
86
86
|
## Features
|
|
87
87
|
|
|
88
88
|
### Channels
|
|
89
|
-
- **Desktop** — native `node-notifier` (macOS/Windows/Linux). Per-channel **system-sound toggle
|
|
89
|
+
- **Desktop** — native `node-notifier` (macOS/Windows/Linux). Per-channel **system-sound toggle** and optional **text-to-speech** (natural neural voice via `msedge-tts`, no API key).
|
|
90
90
|
- **Telegram** — bidirectional. The bot **replies in-thread** to user messages and acknowledges every inbound message so the user knows it landed.
|
|
91
91
|
- **SMS** — Twilio.
|
|
92
92
|
- **Email** — Gmail App Password (one click) or any SMTP. `ask` over email sends a reply link the user clicks to answer.
|
|
Binary file
|
|
Binary file
|
package/dist/channels/desktop.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import notifier from "node-notifier";
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
const DEFAULT_TTS_VOICE = "en-US-AndrewMultilingualNeural";
|
|
3
6
|
export async function sendDesktop(config, message) {
|
|
4
7
|
if (!config.enabled)
|
|
5
8
|
return;
|
|
@@ -12,7 +15,34 @@ export async function sendDesktop(config, message) {
|
|
|
12
15
|
"Add-Type -AssemblyName System.Windows.Forms; [System.Media.SystemSounds]::Asterisk.Play(); Start-Sleep -Milliseconds 600",
|
|
13
16
|
], { windowsHide: true, stdio: "ignore" });
|
|
14
17
|
}
|
|
18
|
+
if (config.tts) {
|
|
19
|
+
// Fire-and-forget: synthesize and play in the background so we don't
|
|
20
|
+
// block the MCP request on network + audio playback.
|
|
21
|
+
speak(message, config.ttsVoice ?? DEFAULT_TTS_VOICE).catch(() => { });
|
|
22
|
+
}
|
|
15
23
|
await new Promise((resolve, reject) => {
|
|
16
24
|
notifier.notify({ title: "Claude", message, sound: wantSound && process.platform !== "win32" }, (err) => err ? reject(err) : resolve());
|
|
17
25
|
});
|
|
18
26
|
}
|
|
27
|
+
export async function speak(text, voice = DEFAULT_TTS_VOICE) {
|
|
28
|
+
// Lazy import so boot doesn't pay for msedge-tts when TTS is off.
|
|
29
|
+
const mod = await import("msedge-tts");
|
|
30
|
+
const { MsEdgeTTS, OUTPUT_FORMAT } = mod;
|
|
31
|
+
const tts = new MsEdgeTTS();
|
|
32
|
+
await tts.setMetadata(voice, OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3);
|
|
33
|
+
const { mkdtempSync } = await import("fs");
|
|
34
|
+
const outDir = mkdtempSync(join(tmpdir(), "notify-tts-"));
|
|
35
|
+
const { audioFilePath } = await tts.toFile(outDir, text);
|
|
36
|
+
if (process.platform === "win32") {
|
|
37
|
+
spawn("powershell", [
|
|
38
|
+
"-NoProfile", "-Command",
|
|
39
|
+
`Add-Type -AssemblyName presentationCore; $p = New-Object System.Windows.Media.MediaPlayer; $p.Open([uri]'${audioFilePath.replace(/\\/g, "\\\\")}'); $p.Play(); Start-Sleep -Seconds 10`,
|
|
40
|
+
], { windowsHide: true, stdio: "ignore" });
|
|
41
|
+
}
|
|
42
|
+
else if (process.platform === "darwin") {
|
|
43
|
+
spawn("afplay", [audioFilePath], { stdio: "ignore" });
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
spawn("aplay", [audioFilePath], { stdio: "ignore" });
|
|
47
|
+
}
|
|
48
|
+
}
|
package/dist/ui/server.js
CHANGED
|
@@ -13,6 +13,7 @@ import twilio from "twilio";
|
|
|
13
13
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
14
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
15
15
|
import { z } from "zod";
|
|
16
|
+
import { tmpdir } from "os";
|
|
16
17
|
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3737;
|
|
17
18
|
const REDIRECT_URI = `http://localhost:${PORT}/auth/google/callback`;
|
|
18
19
|
const PUBLIC_DIR = join(fileURLToPath(new URL("../../ui/public", import.meta.url)));
|
|
@@ -168,6 +169,38 @@ app.post("/api/test/sound", (_req, res) => {
|
|
|
168
169
|
res.json({ ok: true, message: "System sound triggered" });
|
|
169
170
|
});
|
|
170
171
|
});
|
|
172
|
+
async function speakText(text, voice) {
|
|
173
|
+
const mod = await import("msedge-tts");
|
|
174
|
+
const { MsEdgeTTS, OUTPUT_FORMAT } = mod;
|
|
175
|
+
const tts = new MsEdgeTTS();
|
|
176
|
+
await tts.setMetadata(voice, OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3);
|
|
177
|
+
const { mkdtempSync } = await import("fs");
|
|
178
|
+
const outDir = mkdtempSync(join(tmpdir(), "notify-tts-"));
|
|
179
|
+
const { audioFilePath } = await tts.toFile(outDir, text);
|
|
180
|
+
if (process.platform === "win32") {
|
|
181
|
+
spawn("powershell", [
|
|
182
|
+
"-NoProfile", "-Command",
|
|
183
|
+
`Add-Type -AssemblyName presentationCore; $p = New-Object System.Windows.Media.MediaPlayer; $p.Open([uri]'${audioFilePath.replace(/\\/g, "\\\\")}'); $p.Play(); Start-Sleep -Seconds 10`,
|
|
184
|
+
], { windowsHide: true, stdio: "ignore" });
|
|
185
|
+
}
|
|
186
|
+
else if (process.platform === "darwin") {
|
|
187
|
+
spawn("afplay", [audioFilePath], { stdio: "ignore" });
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
spawn("aplay", [audioFilePath], { stdio: "ignore" });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
app.post("/api/test/tts", async (_req, res) => {
|
|
194
|
+
try {
|
|
195
|
+
const cfg = loadConfig();
|
|
196
|
+
const voice = cfg.desktop?.ttsVoice ?? "en-US-AndrewMultilingualNeural";
|
|
197
|
+
await speakText("Notification from Claude. This is a voice test.", voice);
|
|
198
|
+
res.json({ ok: true, message: `TTS played (${voice})` });
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
171
204
|
app.post("/api/test/desktop", (_req, res) => {
|
|
172
205
|
const time = new Date().toLocaleTimeString();
|
|
173
206
|
const cfg = loadConfig();
|
|
@@ -568,6 +601,10 @@ async function sendNotification(message, priority, client) {
|
|
|
568
601
|
], { windowsHide: true, stdio: "ignore" });
|
|
569
602
|
}
|
|
570
603
|
const soundOpt = wantSound && process.platform !== "win32";
|
|
604
|
+
if (cfg.desktop?.tts) {
|
|
605
|
+
const voice = cfg.desktop?.ttsVoice ?? "en-US-AndrewMultilingualNeural";
|
|
606
|
+
speakText(message, voice).catch((err) => log("→", "tts", `ERROR: ${err instanceof Error ? err.message : String(err)}`, client));
|
|
607
|
+
}
|
|
571
608
|
await send("desktop", () => new Promise((res, rej) => notifier.notify({ title: "Claude Notify", message, sound: soundOpt }, (err) => err ? rej(err) : res())));
|
|
572
609
|
}
|
|
573
610
|
if (!desktopOnlyMode && cfg.telegram?.enabled && cfg.telegram.token && cfg.telegram.chatId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omni-notify-mcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
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",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
54
54
|
"express": "^4.19.2",
|
|
55
55
|
"googleapis": "^144.0.0",
|
|
56
|
+
"msedge-tts": "^2.0.5",
|
|
56
57
|
"node-notifier": "^10.0.1",
|
|
57
58
|
"nodemailer": "^6.9.9",
|
|
58
59
|
"open": "^10.1.0",
|
package/ui/public/app.js
CHANGED
|
@@ -44,6 +44,7 @@ function populateForm() {
|
|
|
44
44
|
// Desktop
|
|
45
45
|
$("desktop-enabled").checked = !!config.desktop?.enabled;
|
|
46
46
|
$("desktop-sound").checked = config.desktop?.sound !== false; // default on
|
|
47
|
+
$("desktop-tts").checked = !!config.desktop?.tts; // default off
|
|
47
48
|
|
|
48
49
|
// Email / Gmail
|
|
49
50
|
const email = config.email ?? {};
|
|
@@ -154,6 +155,7 @@ function saveDesktop() {
|
|
|
154
155
|
desktop: {
|
|
155
156
|
enabled: $("desktop-enabled").checked,
|
|
156
157
|
sound: $("desktop-sound").checked,
|
|
158
|
+
tts: $("desktop-tts").checked,
|
|
157
159
|
},
|
|
158
160
|
});
|
|
159
161
|
}
|
|
@@ -398,6 +400,17 @@ async function testSound() {
|
|
|
398
400
|
}
|
|
399
401
|
}
|
|
400
402
|
|
|
403
|
+
async function testTts() {
|
|
404
|
+
try {
|
|
405
|
+
const res = await fetch("/api/test/tts", { method: "POST" });
|
|
406
|
+
const json = await res.json();
|
|
407
|
+
if (!res.ok) throw new Error(json.error);
|
|
408
|
+
toast(json.message, "ok");
|
|
409
|
+
} catch (e) {
|
|
410
|
+
toast("TTS test failed: " + e, "error");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
401
414
|
// ── Test channels ─────────────────────────────────────────────────────────
|
|
402
415
|
|
|
403
416
|
async function testChannel(channel) {
|
package/ui/public/index.html
CHANGED
|
@@ -55,6 +55,14 @@
|
|
|
55
55
|
<span class="toggle-lbl">Play system sound</span>
|
|
56
56
|
<button class="btn btn-sm btn-ghost" onclick="testSound()">Test sound</button>
|
|
57
57
|
</div>
|
|
58
|
+
<div class="actions" style="margin-top:6px">
|
|
59
|
+
<label class="toggle-wrap">
|
|
60
|
+
<input type="checkbox" id="desktop-tts" onchange="saveDesktop()">
|
|
61
|
+
<span class="toggle"></span>
|
|
62
|
+
</label>
|
|
63
|
+
<span class="toggle-lbl">Speak notification</span>
|
|
64
|
+
<button class="btn btn-sm btn-ghost" onclick="testTts()">Test voice</button>
|
|
65
|
+
</div>
|
|
58
66
|
<span id="os-hint" class="os-tag hidden"></span>
|
|
59
67
|
</div>
|
|
60
68
|
</div>
|