omni-notify-mcp 1.3.20 → 1.3.22

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
@@ -178,6 +178,20 @@ ending in `_b64` are base64-decoded at load, and on startup any credential it ho
178
178
  copied into **empty** fields of `config.json` (never over a value you already set).
179
179
  See `config.example.json` for a hand-written starting point.
180
180
 
181
+ **This only works if the running server can actually find that file.** The lookup path is
182
+ `NOTIFY_MCP_SECRETS_PATH`, defaulting to `<CONFIG_DIR>/notify-secrets.json` — and `CONFIG_DIR`
183
+ defaults to `~/.notify-mcp`, a **home-relative** path, not the repo root. A server started via
184
+ `npx` or the VS Code extension runs from an npm cache directory with no relationship to any git
185
+ checkout, so simply having `notify-secrets.json` committed at the repo root does nothing on its
186
+ own (story #438: this sat silently broken for months — every channel stayed disabled and every
187
+ `notify` reached nobody, because nothing ever pointed the server at the repo file). Set
188
+ `NOTIFY_MCP_SECRETS_PATH` to the repo's `notify-secrets.json` as a **persistent environment
189
+ variable** on each machine that has a checkout (`setx NOTIFY_MCP_SECRETS_PATH "C:\path\to\BullseyeNotif\notify-secrets.json"`
190
+ on Windows, or export it from your shell profile elsewhere) — set once, it survives reboots and
191
+ every future server restart on that machine picks it up. Check `GET /api/config` after a restart:
192
+ a channel listed in `notify-secrets.json` with `enabled: true` and its required fields populated
193
+ should show `enabled: true` there too — if it still shows `false`, the file was not found.
194
+
181
195
  ## Inbound — the inbox
182
196
 
183
197
  Messages arriving *from* the user (Telegram reply, Slack channel post,
@@ -156,13 +156,35 @@ export async function sendWithRouting(options) {
156
156
  summary: `Suppressed — ${decision.reason}. ${formatIdleReading(decision.idle)}.`,
157
157
  };
158
158
  }
159
+ // Nothing was even eligible to try — this is NOT a hold (the branches above
160
+ // already caught dnd/idle) and it is NOT a per-channel send error (nothing
161
+ // was attempted, so `errors` is necessarily empty). It is either "nothing is
162
+ // turned on at all" or "something is turned on but doesn't reach this
163
+ // priority/activity state" — story #438: the old text here ("reached 0 of 0
164
+ // enabled channels; the server log names which send failed") actively lied
165
+ // in this branch, since nothing was ever attempted and no log line exists to
166
+ // name. Naming what IS enabled (or that nothing is) is the whole difference
167
+ // between an agent that can act on the failure and one that cannot.
168
+ if (attempted.length === 0) {
169
+ const enabledChannels = Object.keys(enabled).filter((c) => enabled[c]);
170
+ const configHint = enabledChannels.length === 0
171
+ ? "no channel is enabled at all — enable at least one (desktop, telegram, email, ntfy, discord, slack, teams, sms) in the settings UI before calling notify again"
172
+ : `${enabledChannels.join(", ")} ${enabledChannels.length === 1 ? "is" : "are"} enabled but none of them reaches priority=${priority} while ${reach.join("/")}-class channels are reachable — check each channel's priority range and activity gating`;
173
+ return {
174
+ delivered,
175
+ attempted,
176
+ errors,
177
+ outcome: "failed",
178
+ summary: `NOT DELIVERED — no channel was eligible to try: ${configHint} — ${decision.reason}. ${formatIdleReading(decision.idle)}.`,
179
+ };
180
+ }
159
181
  return {
160
182
  delivered,
161
183
  attempted,
162
184
  errors,
163
185
  outcome: "failed",
164
186
  summary: `NOT DELIVERED — reached 0 of ${attempted.length} enabled channel${attempted.length === 1 ? "" : "s"}`
165
- + `${errors.length > 0 ? `; all ${errors.length} failed: ${errors.join("; ")}` : "; the server log names which send failed"}`
187
+ + `; all ${errors.length} failed: ${errors.join("; ")}`
166
188
  + ` — ${decision.reason}. ${formatIdleReading(decision.idle)}.`,
167
189
  };
168
190
  }
package/dist/ui/server.js CHANGED
@@ -152,32 +152,37 @@ const LEGACY_SECRETS_PATH = join(homedir(), ".notify-mcp-secrets");
152
152
  function e164(s) {
153
153
  return String(s ?? "").replace(/[^\d+]/g, "");
154
154
  }
155
+ // The ONE list of credential-shaped fields, read by both maskSecrets() (out)
156
+ // and mergePreservingSecrets() (back in). Before story #442 these were two
157
+ // hand-written lists of `if`/`guard` calls kept in sync by hand — that's
158
+ // exactly how ntfy.token got added to a channel (story #427's "brainless
159
+ // config" import) without ever being added to either list, and leaked in the
160
+ // clear over `GET /api/config` for as long as the channel existed. A field
161
+ // belongs here the moment its channel gets one that authenticates outbound
162
+ // (a token, password, or bearer webhook URL) — see
163
+ // tests/smoke.test.mjs's naming-convention test, which independently catches
164
+ // a field like this one being added to defaultConfig() but not to this list.
165
+ const SENSITIVE_FIELDS = [
166
+ ["email", "pass"],
167
+ ["email", "clientSecret"],
168
+ ["email", "refreshToken"],
169
+ ["email", "accessToken"],
170
+ ["sms", "secretAccessKey"],
171
+ ["telegram", "token"],
172
+ ["ntfy", "token"],
173
+ ["discord", "webhookUrl"],
174
+ ["discord", "clientSecret"],
175
+ ["slack", "webhookUrl"],
176
+ ["slack", "botToken"],
177
+ ["slack", "clientSecret"],
178
+ ["teams", "webhookUrl"],
179
+ ];
155
180
  function maskSecrets(config) {
156
181
  const c = JSON.parse(JSON.stringify(config));
157
- if (c.email?.pass)
158
- c.email.pass = MASKED;
159
- if (c.email?.clientSecret)
160
- c.email.clientSecret = MASKED;
161
- if (c.email?.refreshToken)
162
- c.email.refreshToken = MASKED;
163
- if (c.email?.accessToken)
164
- c.email.accessToken = MASKED;
165
- if (c.sms?.secretAccessKey)
166
- c.sms.secretAccessKey = MASKED;
167
- if (c.telegram?.token)
168
- c.telegram.token = MASKED;
169
- if (c.discord?.webhookUrl)
170
- c.discord.webhookUrl = MASKED;
171
- if (c.discord?.clientSecret)
172
- c.discord.clientSecret = MASKED;
173
- if (c.slack?.webhookUrl)
174
- c.slack.webhookUrl = MASKED;
175
- if (c.slack?.botToken)
176
- c.slack.botToken = MASKED;
177
- if (c.slack?.clientSecret)
178
- c.slack.clientSecret = MASKED;
179
- if (c.teams?.webhookUrl)
180
- c.teams.webhookUrl = MASKED;
182
+ for (const [sec, field] of SENSITIVE_FIELDS) {
183
+ if (c[sec]?.[field])
184
+ c[sec][field] = MASKED;
185
+ }
181
186
  delete c.whatsapp;
182
187
  return c;
183
188
  }
@@ -192,24 +197,11 @@ function mergePreservingSecrets(existing, update) {
192
197
  }
193
198
  if (typeof update.muteAll === "boolean")
194
199
  merged.muteAll = update.muteAll;
195
- const guard = (path) => {
196
- const [sec, field] = path;
200
+ for (const [sec, field] of SENSITIVE_FIELDS) {
197
201
  if (update[sec]?.[field] === MASKED) {
198
202
  merged[sec][field] = existing[sec]?.[field] ?? "";
199
203
  }
200
- };
201
- guard(["email", "pass"]);
202
- guard(["email", "clientSecret"]);
203
- guard(["email", "refreshToken"]);
204
- guard(["email", "accessToken"]);
205
- guard(["sms", "secretAccessKey"]);
206
- guard(["telegram", "token"]);
207
- guard(["discord", "webhookUrl"]);
208
- guard(["discord", "clientSecret"]);
209
- guard(["slack", "webhookUrl"]);
210
- guard(["slack", "botToken"]);
211
- guard(["slack", "clientSecret"]);
212
- guard(["teams", "webhookUrl"]);
204
+ }
213
205
  return merged;
214
206
  }
215
207
  const app = express();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "omni-notify-mcp",
3
- "version": "1.3.20",
4
- "description": "Reach your AI agents from anywhere \u2014 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 \u2014 with Do Not Disturb and idle detection deciding what actually gets through.",
3
+ "version": "1.3.22",
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",
7
7
  "bin": {