omni-notify-mcp 1.3.22 → 1.3.24

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.
@@ -0,0 +1,6 @@
1
+ export async function telegramSendErrorHint(r) {
2
+ if (r.status === 401) {
3
+ return "the bot token is invalid or has been revoked — open @BotFather, check the bot behind this token (or create a new one), and update it in the settings UI";
4
+ }
5
+ return r.text();
6
+ }
package/dist/ui/server.js CHANGED
@@ -13,6 +13,7 @@ import nodemailer from "nodemailer";
13
13
  import { PinpointSMSVoiceV2Client, SendTextMessageCommand, DescribePhoneNumbersCommand, DescribeVerifiedDestinationNumbersCommand } from "@aws-sdk/client-pinpoint-sms-voice-v2";
14
14
  import { tmpdir } from "os";
15
15
  import { sendWithRouting, idleReading, formatIdleReading, describePriorityRouting } from "./messaging/notificationEngine.js";
16
+ import { telegramSendErrorHint } from "./messaging/telegramError.js";
16
17
  import { z } from "zod";
17
18
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
19
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -146,6 +147,16 @@ const SECRETS_PATH = process.env.NOTIFY_MCP_SECRETS_PATH
146
147
  || join(CONFIG_DIR, "notify-secrets.json");
147
148
  // Backward compatibility for an existing per-user install: older builds,
148
149
  // `setup-secrets.sh` and the README all placed the file at `~/.notify-mcp-secrets`.
150
+ //
151
+ // #444: that exact filename is ALSO where release.sh/setup-secrets.sh (this
152
+ // repo, and BullseyeShared's shared scripts/vscode-extension/release.sh and
153
+ // publish-ext.sh) now write PUBLISH credentials — shell `KEY=VALUE` lines
154
+ // (NPM_TOKEN=…, VSCE_PAT=…), a different, incompatible format from the JSON
155
+ // this constant reads. Those scripts were moved to `~/.bullseye-release-secrets`
156
+ // on 2026-09-13 specifically to stop colliding with this legacy path. Do NOT
157
+ // widen this constant's name, and do not let release tooling point back at it
158
+ // — see loadSecrets() below, which now names the mismatch instead of throwing
159
+ // a bare parse error when it happens anyway.
149
160
  const LEGACY_SECRETS_PATH = join(homedir(), ".notify-mcp-secrets");
150
161
  // AWS SendTextMessage requires E.164 (e.g. +14089812202) — strip spaces, dashes,
151
162
  // parens, dots so a user-entered "+1 408 981 2202" is accepted.
@@ -554,7 +565,7 @@ app.post("/api/test/telegram", async (_req, res) => {
554
565
  if (r.ok)
555
566
  sent++;
556
567
  else
557
- errors.push(`${chatId}: ${r.status} ${await r.text()}`);
568
+ errors.push(`${chatId}: ${await telegramSendErrorHint(r)}`);
558
569
  }
559
570
  catch (err) {
560
571
  errors.push(`${chatId}: ${err instanceof Error ? err.message : String(err)}`);
@@ -1510,10 +1521,11 @@ async function sendNotification(message, priority, client) {
1510
1521
  headers: { "Content-Type": "application/json" },
1511
1522
  body: JSON.stringify(body),
1512
1523
  });
1513
- if (r.ok)
1524
+ if (r.ok) {
1514
1525
  sent++;
1515
- else
1516
- errors.push(`${chatId}: ${await r.text()}`);
1526
+ continue;
1527
+ }
1528
+ errors.push(`${chatId}: ${await telegramSendErrorHint(r)}`);
1517
1529
  }
1518
1530
  if (sent === 0 && errors.length)
1519
1531
  throw new Error(errors.join("; "));
@@ -2304,14 +2316,37 @@ async function sendNotificationSummary(message, priority, client) {
2304
2316
  const out = await sendNotification(message, priority, client);
2305
2317
  return out.text;
2306
2318
  }
2319
+ // A shell `KEY=VALUE` file — the format release.sh/setup-secrets.sh (this repo
2320
+ // and BullseyeShared's shared release scripts) write — never starts with `{`
2321
+ // and always has at least one bare `NAME=` line. Used only to make the #444
2322
+ // mismatch diagnosable: this is a heuristic, not a parser, and only runs after
2323
+ // JSON.parse already failed.
2324
+ function looksLikeShellSecrets(raw) {
2325
+ const trimmed = raw.trimStart();
2326
+ return !trimmed.startsWith("{") && /^[A-Z_][A-Z0-9_]*=/m.test(raw);
2327
+ }
2307
2328
  function loadSecrets() {
2308
2329
  const path = secretsFilePath();
2309
2330
  if (!path)
2310
2331
  return {};
2332
+ let raw;
2333
+ try {
2334
+ raw = readFileSync(path, "utf8");
2335
+ }
2336
+ catch (err) {
2337
+ throw new SecretsUnreadable(path, err);
2338
+ }
2311
2339
  try {
2312
- return decodeB64Fields(JSON.parse(readFileSync(path, "utf8")));
2340
+ return decodeB64Fields(JSON.parse(raw));
2313
2341
  }
2314
2342
  catch (err) {
2343
+ if (path === LEGACY_SECRETS_PATH && looksLikeShellSecrets(raw)) {
2344
+ throw new SecretsUnreadable(path, err, "this looks like a shell KEY=VALUE file (NPM_TOKEN=…/VSCE_PAT=…), the " +
2345
+ "format release.sh/setup-secrets.sh write — those moved to " +
2346
+ "~/.bullseye-release-secrets on 2026-09-13 (#444) specifically because " +
2347
+ "this path is reserved for JSON notify-channel secrets; move or delete " +
2348
+ "the file at this path rather than editing it");
2349
+ }
2315
2350
  throw new SecretsUnreadable(path, err);
2316
2351
  }
2317
2352
  }
@@ -2319,8 +2354,9 @@ function loadSecrets() {
2319
2354
  // result: the caller would otherwise import nothing and believe the user has no
2320
2355
  // credentials, which is the silent-escalation failure this file exists to stop.
2321
2356
  class SecretsUnreadable extends Error {
2322
- constructor(path, cause) {
2323
- super(`secrets file at ${path} exists but could not be read: ${cause instanceof Error ? cause.message : String(cause)}`);
2357
+ constructor(path, cause, hint) {
2358
+ const base = `secrets file at ${path} exists but could not be read: ${cause instanceof Error ? cause.message : String(cause)}`;
2359
+ super(hint ? `${base} — ${hint}` : base);
2324
2360
  }
2325
2361
  }
2326
2362
  function secretsFilePath() {
@@ -2434,6 +2470,36 @@ function importCredsOnStart() {
2434
2470
  log("·", "import", "secrets file present but every config field it maps to is already set — nothing imported");
2435
2471
  }
2436
2472
  }
2473
+ // Story #445 (#438's real lesson): a server with no channel configured
2474
+ // answers `/v1/health` and every MCP tool call exactly like a healthy one —
2475
+ // nothing else here would ever say otherwise. That is how a machine ran for
2476
+ // five days delivering nothing while looking fine. Called once at startup,
2477
+ // right after importCredsOnStart() has had its one chance to fix the
2478
+ // problem — loud, impossible to miss in the console, and naming every path
2479
+ // this process actually checked, so the next time this happens it is
2480
+ // diagnosed in the first minute, not five days later by someone measuring
2481
+ // real sends.
2482
+ const CHANNEL_LABELS = [
2483
+ ["desktop", "Desktop"], ["telegram", "Telegram"], ["email", "Email"],
2484
+ ["ntfy", "ntfy"], ["discord", "Discord"], ["slack", "Slack"],
2485
+ ["teams", "Teams"], ["sms", "SMS"],
2486
+ ];
2487
+ function warnIfNoChannelConfigured(cfg) {
2488
+ if (CHANNEL_LABELS.some(([key]) => cfg[key]?.enabled === true))
2489
+ return;
2490
+ const bar = "!".repeat(78);
2491
+ const exists = (p) => (existsSync(p) ? "exists" : "MISSING");
2492
+ console.log(`\n${bar}`);
2493
+ console.log(" NO NOTIFICATION CHANNEL IS CONFIGURED — every notify call will reach nobody.");
2494
+ console.log(bar);
2495
+ console.log(` Config file (${exists(CONFIG_PATH)}) : ${CONFIG_PATH}`);
2496
+ console.log(` Secrets file (${exists(SECRETS_PATH)}) : ${SECRETS_PATH}`);
2497
+ console.log(` Legacy path (${exists(LEGACY_SECRETS_PATH)}) : ${LEGACY_SECRETS_PATH}`);
2498
+ console.log(` Secrets actually in use: ${secretsFilePath() ?? "none of the above"}`);
2499
+ console.log(` Fix: open http://localhost:${PORT} and turn a channel on, or set`);
2500
+ console.log(` NOTIFY_MCP_SECRETS_PATH to a notify-secrets.json with real credentials.`);
2501
+ console.log(`${bar}\n`);
2502
+ }
2437
2503
  async function slackPost(text) {
2438
2504
  const { webhook } = slackCreds();
2439
2505
  if (!webhook)
@@ -2835,6 +2901,10 @@ function createMcpServer(clientId, sessionTag) {
2835
2901
  ? `\n\nReply with: @${sessionTag} <your answer>`
2836
2902
  : `\n\nReply to this message with your answer.`;
2837
2903
  for (const chatId of cfg.telegram.chatIds) {
2904
+ // A non-2xx Telegram response does NOT reject this fetch — only a
2905
+ // network-level failure does — so this used to silently do nothing
2906
+ // on e.g. a revoked token: no error, no log line, just a question
2907
+ // the user never saw and no clue why. Story #443: check r.ok too.
2838
2908
  await fetch(`https://api.telegram.org/bot${cfg.telegram.token}/sendMessage`, {
2839
2909
  method: "POST",
2840
2910
  headers: { "Content-Type": "application/json" },
@@ -2842,7 +2912,10 @@ function createMcpServer(clientId, sessionTag) {
2842
2912
  chat_id: chatId,
2843
2913
  text: `${askPrefix} ${question}${replyHint}`,
2844
2914
  }),
2845
- }).catch((err) => log("→", "ask:telegram", `ERROR: ${err}`, clientId));
2915
+ })
2916
+ .then(async (r) => { if (!r.ok)
2917
+ log("→", "ask:telegram", `ERROR: ${chatId}: ${await telegramSendErrorHint(r)}`, clientId); })
2918
+ .catch((err) => log("→", "ask:telegram", `ERROR: ${chatId}: ${err}`, clientId));
2846
2919
  }
2847
2920
  }
2848
2921
  const email = cfg.email ?? {};
@@ -3292,6 +3365,9 @@ const httpServer = app.listen(PORT, () => {
3292
3365
  // config.json cannot serve a process whose secrets file IS populated. It
3293
3366
  // reports the channel names it filled — never a secret value.
3294
3367
  importCredsOnStart();
3368
+ // Import has now had its one chance to fix a channel-less config — if
3369
+ // nothing is enabled even after that, say so loudly (story #445).
3370
+ warnIfNoChannelConfigured(loadConfig());
3295
3371
  // Live Telegram/Slack pollers hit real external channels — never under test.
3296
3372
  if (!TEST_ENDPOINTS) {
3297
3373
  startTelegramListener();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omni-notify-mcp",
3
- "version": "1.3.22",
3
+ "version": "1.3.24",
4
4
  "description": "Reach your AI agents from anywhere — and let them reach you. Every notification channel is set up in one config UI with one-click server launch: desktop, Telegram, Slack, SMS, email, ntfy. Agents then notify you when long work finishes or a decision is needed, ask you a question and wait for your answer, and receive and reply to the messages you send them — with Do Not Disturb and idle detection deciding what actually gets through.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",