omni-notify-mcp 1.1.5 → 1.1.6

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 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** and optional **text-to-speech** (natural neural voice via `msedge-tts`, no API key).
89
+ - **Desktop** — native `node-notifier` (macOS/Windows/Linux). Per-channel **system-sound toggle** and optional **text-to-speech** with a voice picker covering 30+ neural voices (US/UK/AU/CA/IN/IE/NZ/…) 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
package/dist/ui/server.js CHANGED
@@ -190,10 +190,12 @@ async function speakText(text, voice) {
190
190
  spawn("aplay", [audioFilePath], { stdio: "ignore" });
191
191
  }
192
192
  }
193
- app.post("/api/test/tts", async (_req, res) => {
193
+ app.post("/api/test/tts", async (req, res) => {
194
194
  try {
195
195
  const cfg = loadConfig();
196
- const voice = cfg.desktop?.ttsVoice ?? "en-US-AndrewMultilingualNeural";
196
+ const voice = (typeof req.body?.voice === "string" && req.body.voice) ||
197
+ cfg.desktop?.ttsVoice ||
198
+ "en-US-AndrewMultilingualNeural";
197
199
  await speakText("Notification from Claude. This is a voice test.", voice);
198
200
  res.json({ ok: true, message: `TTS played (${voice})` });
199
201
  }
@@ -201,6 +203,26 @@ app.post("/api/test/tts", async (_req, res) => {
201
203
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
202
204
  }
203
205
  });
206
+ let voiceCache = null;
207
+ app.get("/api/voices", async (_req, res) => {
208
+ try {
209
+ if (!voiceCache || Date.now() - voiceCache.ts > 24 * 60 * 60 * 1000) {
210
+ const mod = await import("msedge-tts");
211
+ const tts = new mod.MsEdgeTTS();
212
+ const all = await tts.getVoices();
213
+ voiceCache = {
214
+ ts: Date.now(),
215
+ voices: all
216
+ .filter((v) => v.Locale.startsWith("en-") && v.ShortName.includes("Neural"))
217
+ .map((v) => ({ shortName: v.ShortName, gender: v.Gender, locale: v.Locale })),
218
+ };
219
+ }
220
+ res.json({ voices: voiceCache.voices });
221
+ }
222
+ catch (err) {
223
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
224
+ }
225
+ });
204
226
  app.post("/api/test/desktop", (_req, res) => {
205
227
  const time = new Date().toLocaleTimeString();
206
228
  const cfg = loadConfig();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omni-notify-mcp",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
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",
package/ui/public/app.js CHANGED
@@ -45,6 +45,8 @@ function populateForm() {
45
45
  $("desktop-enabled").checked = !!config.desktop?.enabled;
46
46
  $("desktop-sound").checked = config.desktop?.sound !== false; // default on
47
47
  $("desktop-tts").checked = !!config.desktop?.tts; // default off
48
+ updateTtsVoiceRow();
49
+ loadVoices().catch(() => {});
48
50
 
49
51
  // Email / Gmail
50
52
  const email = config.email ?? {};
@@ -151,15 +153,49 @@ function setBadge(channel, type, text) {
151
153
  // ── Save handlers ─────────────────────────────────────────────────────────
152
154
 
153
155
  function saveDesktop() {
156
+ updateTtsVoiceRow();
157
+ const ttsVoice = $("desktop-tts-voice").value || undefined;
154
158
  patch({
155
159
  desktop: {
156
160
  enabled: $("desktop-enabled").checked,
157
161
  sound: $("desktop-sound").checked,
158
162
  tts: $("desktop-tts").checked,
163
+ ttsVoice,
159
164
  },
160
165
  });
161
166
  }
162
167
 
168
+ function updateTtsVoiceRow() {
169
+ const row = $("tts-voice-row");
170
+ row.style.display = $("desktop-tts").checked ? "" : "none";
171
+ }
172
+
173
+ let voicesLoaded = false;
174
+ async function loadVoices() {
175
+ if (voicesLoaded) return;
176
+ const res = await fetch("/api/voices");
177
+ if (!res.ok) return;
178
+ const { voices } = await res.json();
179
+ const sel = $("desktop-tts-voice");
180
+ const current = config.desktop?.ttsVoice || "en-US-AndrewMultilingualNeural";
181
+ const byLocale = {};
182
+ for (const v of voices) (byLocale[v.locale] ??= []).push(v);
183
+ sel.innerHTML = "";
184
+ for (const locale of Object.keys(byLocale).sort()) {
185
+ const og = document.createElement("optgroup");
186
+ og.label = locale;
187
+ for (const v of byLocale[locale].sort((a, b) => a.shortName.localeCompare(b.shortName))) {
188
+ const opt = document.createElement("option");
189
+ opt.value = v.shortName;
190
+ opt.textContent = `${v.shortName.replace(locale + "-", "")} (${v.gender})`;
191
+ if (v.shortName === current) opt.selected = true;
192
+ og.appendChild(opt);
193
+ }
194
+ sel.appendChild(og);
195
+ }
196
+ voicesLoaded = true;
197
+ }
198
+
163
199
  async function saveEmail() {
164
200
  const to = $("gmail-to-connected").value.trim();
165
201
  const enabled = $("email-enabled").checked;
@@ -402,7 +438,12 @@ async function testSound() {
402
438
 
403
439
  async function testTts() {
404
440
  try {
405
- const res = await fetch("/api/test/tts", { method: "POST" });
441
+ const voice = $("desktop-tts-voice").value || undefined;
442
+ const res = await fetch("/api/test/tts", {
443
+ method: "POST",
444
+ headers: { "Content-Type": "application/json" },
445
+ body: JSON.stringify({ voice }),
446
+ });
406
447
  const json = await res.json();
407
448
  if (!res.ok) throw new Error(json.error);
408
449
  toast(json.message, "ok");
@@ -63,6 +63,9 @@
63
63
  <span class="toggle-lbl">Speak notification</span>
64
64
  <button class="btn btn-sm btn-ghost" onclick="testTts()">Test voice</button>
65
65
  </div>
66
+ <div id="tts-voice-row" class="actions" style="margin-top:6px; display:none">
67
+ <select id="desktop-tts-voice" onchange="saveDesktop()" style="flex:1; min-width:0"></select>
68
+ </div>
66
69
  <span id="os-hint" class="os-tag hidden"></span>
67
70
  </div>
68
71
  </div>