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 +14 -0
- package/dist/ui/messaging/notificationEngine.js +23 -1
- package/dist/ui/server.js +31 -39
- package/package.json +2 -2
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
|
-
+
|
|
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
|
-
|
|
158
|
-
c
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
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.
|
|
4
|
-
"description": "Reach your AI agents from anywhere
|
|
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": {
|