telegram-notify-mcp 1.0.0 → 2.0.0
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/.env.example +4 -0
- package/ARCHITECTURE-PHASE2.md +324 -0
- package/README.md +63 -23
- package/ROADMAP.md +17 -9
- package/dist/index.d.ts +3 -1
- package/dist/index.js +143 -11
- package/dist/index.js.map +1 -1
- package/dist/routing.d.ts +49 -0
- package/dist/routing.js +146 -0
- package/dist/routing.js.map +1 -0
- package/dist/store.d.ts +56 -0
- package/dist/store.js +126 -0
- package/dist/store.js.map +1 -0
- package/dist/telegram.d.ts +1 -0
- package/dist/telegram.js.map +1 -1
- package/package.json +4 -3
- package/skill-snippet.md +53 -5
package/dist/index.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* telegram-notify-mcp — stdio MCP server for Telegram Bot API notifications.
|
|
4
|
-
* Env: TELEGRAM_BOT_TOKEN (required for tool calls), TELEGRAM_CHAT_ID (optional default)
|
|
4
|
+
* Env: TELEGRAM_BOT_TOKEN (required for tool calls), TELEGRAM_CHAT_ID (optional default),
|
|
5
|
+
* TELEGRAM_CHAT_ALLOWLIST (optional inbound), TELEGRAM_NOTIFY_DATA_DIR (optional store path).
|
|
5
6
|
* Starts cleanly without a token; tools fail with a clear error when invoked.
|
|
6
7
|
* Public release is env-only — no disk/secret-store token loading.
|
|
8
|
+
* Phase 2: outbound mapping + inbound pending queue (host must poll + SendToAgent + ack).
|
|
7
9
|
*/
|
|
8
10
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
11
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
12
|
import { z } from 'zod';
|
|
11
13
|
import { TelegramApiError, TelegramClient, formatNotifyMessage, getDefaultChatId, } from './telegram.js';
|
|
14
|
+
import { LocalStore } from './store.js';
|
|
15
|
+
import { formatAgentFooter, formatNotifyWithFooter, processUpdates, } from './routing.js';
|
|
12
16
|
const SERVER_NAME = 'telegram-notify-mcp';
|
|
13
|
-
const SERVER_VERSION = '
|
|
17
|
+
const SERVER_VERSION = '2.0.0';
|
|
14
18
|
function textResult(text, isError = false) {
|
|
15
19
|
return {
|
|
16
20
|
content: [{ type: 'text', text }],
|
|
@@ -27,6 +31,9 @@ function errorResult(err) {
|
|
|
27
31
|
function getClient() {
|
|
28
32
|
return new TelegramClient(process.env.TELEGRAM_BOT_TOKEN);
|
|
29
33
|
}
|
|
34
|
+
function getStore() {
|
|
35
|
+
return new LocalStore();
|
|
36
|
+
}
|
|
30
37
|
function createServer() {
|
|
31
38
|
const server = new McpServer({
|
|
32
39
|
name: SERVER_NAME,
|
|
@@ -46,7 +53,7 @@ function createServer() {
|
|
|
46
53
|
return errorResult(err);
|
|
47
54
|
}
|
|
48
55
|
});
|
|
49
|
-
server.tool('telegram_send_message', 'Send a text message via the Bot API. chat_id is optional when TELEGRAM_CHAT_ID is set.', {
|
|
56
|
+
server.tool('telegram_send_message', 'Send a text message via the Bot API. chat_id is optional when TELEGRAM_CHAT_ID is set. Optional source_agent_id appends footer and persists reply mapping.', {
|
|
50
57
|
text: z.string().min(1).describe('Message text to send'),
|
|
51
58
|
chat_id: z
|
|
52
59
|
.union([z.string(), z.number()])
|
|
@@ -56,52 +63,94 @@ function createServer() {
|
|
|
56
63
|
.enum(['Markdown', 'MarkdownV2', 'HTML'])
|
|
57
64
|
.optional()
|
|
58
65
|
.describe('Optional Telegram parse_mode'),
|
|
59
|
-
|
|
66
|
+
source_agent_id: z
|
|
67
|
+
.string()
|
|
68
|
+
.min(1)
|
|
69
|
+
.optional()
|
|
70
|
+
.describe('Originating agent id — when set, append footer and store mapping for replies'),
|
|
71
|
+
source_agent_name: z
|
|
72
|
+
.string()
|
|
73
|
+
.optional()
|
|
74
|
+
.describe('Optional display name for footer'),
|
|
75
|
+
}, async ({ text, chat_id, parse_mode, source_agent_id, source_agent_name }) => {
|
|
60
76
|
try {
|
|
61
77
|
const chatId = getDefaultChatId(chat_id);
|
|
78
|
+
let outbound = text;
|
|
79
|
+
if (source_agent_id) {
|
|
80
|
+
outbound = text + formatAgentFooter(source_agent_id, source_agent_name);
|
|
81
|
+
}
|
|
62
82
|
const msg = await getClient().sendMessage({
|
|
63
83
|
chat_id: chatId,
|
|
64
|
-
text,
|
|
84
|
+
text: outbound,
|
|
65
85
|
parse_mode,
|
|
66
86
|
});
|
|
87
|
+
let mapped = false;
|
|
88
|
+
if (source_agent_id) {
|
|
89
|
+
getStore().putMapping({
|
|
90
|
+
chat_id: String(msg.chat.id),
|
|
91
|
+
telegram_message_id: msg.message_id,
|
|
92
|
+
source_agent_id,
|
|
93
|
+
source_agent_name: source_agent_name || undefined,
|
|
94
|
+
});
|
|
95
|
+
mapped = true;
|
|
96
|
+
}
|
|
67
97
|
return textResult(JSON.stringify({
|
|
68
98
|
ok: true,
|
|
69
99
|
message_id: msg.message_id,
|
|
70
100
|
chat_id: msg.chat.id,
|
|
71
|
-
text: msg.text ??
|
|
101
|
+
text: msg.text ?? outbound,
|
|
102
|
+
mapped,
|
|
103
|
+
source_agent_id: source_agent_id ?? null,
|
|
72
104
|
}, null, 2));
|
|
73
105
|
}
|
|
74
106
|
catch (err) {
|
|
75
107
|
return errorResult(err);
|
|
76
108
|
}
|
|
77
109
|
});
|
|
78
|
-
server.tool('telegram_notify', 'Send a short completion-style notification (title + body)
|
|
110
|
+
server.tool('telegram_notify', 'Send a short completion-style notification (title + body) with required source_agent_id. Appends agent footer and persists message→agent mapping for reply routing.', {
|
|
79
111
|
body: z.string().min(1).describe('Notification body / details'),
|
|
80
112
|
title: z.string().optional().describe('Optional short title (e.g. "Build finished")'),
|
|
81
113
|
chat_id: z
|
|
82
114
|
.union([z.string(), z.number()])
|
|
83
115
|
.optional()
|
|
84
116
|
.describe('Telegram chat id (falls back to TELEGRAM_CHAT_ID)'),
|
|
85
|
-
|
|
117
|
+
source_agent_id: z
|
|
118
|
+
.string()
|
|
119
|
+
.min(1)
|
|
120
|
+
.describe('Required stable id of the originating agent (for reply routing)'),
|
|
121
|
+
source_agent_name: z
|
|
122
|
+
.string()
|
|
123
|
+
.optional()
|
|
124
|
+
.describe('Optional short human label for the footer'),
|
|
125
|
+
}, async ({ body, title, chat_id, source_agent_id, source_agent_name }) => {
|
|
86
126
|
try {
|
|
87
127
|
const chatId = getDefaultChatId(chat_id);
|
|
88
|
-
const text =
|
|
128
|
+
const text = formatNotifyWithFooter(title, body, source_agent_id, source_agent_name, formatNotifyMessage);
|
|
89
129
|
const msg = await getClient().sendMessage({
|
|
90
130
|
chat_id: chatId,
|
|
91
131
|
text,
|
|
92
132
|
});
|
|
133
|
+
getStore().putMapping({
|
|
134
|
+
chat_id: String(msg.chat.id),
|
|
135
|
+
telegram_message_id: msg.message_id,
|
|
136
|
+
source_agent_id,
|
|
137
|
+
source_agent_name: source_agent_name || undefined,
|
|
138
|
+
});
|
|
93
139
|
return textResult(JSON.stringify({
|
|
94
140
|
ok: true,
|
|
95
141
|
message_id: msg.message_id,
|
|
96
142
|
chat_id: msg.chat.id,
|
|
97
143
|
text,
|
|
144
|
+
mapped: true,
|
|
145
|
+
source_agent_id,
|
|
146
|
+
source_agent_name: source_agent_name ?? null,
|
|
98
147
|
}, null, 2));
|
|
99
148
|
}
|
|
100
149
|
catch (err) {
|
|
101
150
|
return errorResult(err);
|
|
102
151
|
}
|
|
103
152
|
});
|
|
104
|
-
server.tool('telegram_get_updates', 'Fetch recent bot updates so you can discover chat_id after messaging the bot with /start.', {
|
|
153
|
+
server.tool('telegram_get_updates', 'Fetch recent bot updates so you can discover chat_id after messaging the bot with /start. Does not enqueue pending commands (use telegram_poll_inbound for that).', {
|
|
105
154
|
limit: z
|
|
106
155
|
.number()
|
|
107
156
|
.int()
|
|
@@ -142,6 +191,87 @@ function createServer() {
|
|
|
142
191
|
return errorResult(err);
|
|
143
192
|
}
|
|
144
193
|
});
|
|
194
|
+
server.tool('telegram_poll_inbound', 'Poll Telegram getUpdates, allowlist-filter, route replies/mappings and /to commands into the pending queue. Unroutable allowlisted messages are ignored (counted). Host must then list + SendToAgent + ack.', {
|
|
195
|
+
limit: z
|
|
196
|
+
.number()
|
|
197
|
+
.int()
|
|
198
|
+
.min(1)
|
|
199
|
+
.max(100)
|
|
200
|
+
.optional()
|
|
201
|
+
.describe('Max updates to fetch (default 50)'),
|
|
202
|
+
timeout: z
|
|
203
|
+
.number()
|
|
204
|
+
.int()
|
|
205
|
+
.min(0)
|
|
206
|
+
.max(50)
|
|
207
|
+
.optional()
|
|
208
|
+
.describe('Long-poll timeout seconds (default 0)'),
|
|
209
|
+
}, async ({ limit, timeout }) => {
|
|
210
|
+
try {
|
|
211
|
+
const store = getStore();
|
|
212
|
+
const offset = store.getUpdateOffset();
|
|
213
|
+
const updates = await getClient().getUpdates({
|
|
214
|
+
limit: limit ?? 50,
|
|
215
|
+
timeout: timeout ?? 0,
|
|
216
|
+
offset,
|
|
217
|
+
});
|
|
218
|
+
const summary = processUpdates(store, updates);
|
|
219
|
+
if (summary.next_offset !== undefined) {
|
|
220
|
+
store.setUpdateOffset(summary.next_offset);
|
|
221
|
+
}
|
|
222
|
+
return textResult(JSON.stringify({
|
|
223
|
+
ok: true,
|
|
224
|
+
...summary,
|
|
225
|
+
note: 'Unroutable messages (no reply-to mapping and no /to <agent_id> …) are ignored. Host should call telegram_list_pending_commands, deliver via SendToAgent, then telegram_ack_command.',
|
|
226
|
+
}, null, 2));
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
return errorResult(err);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
server.tool('telegram_list_pending_commands', 'List pending inbound commands (status=pending). Optional filter by source_agent_id. Host delivers each then calls telegram_ack_command.', {
|
|
233
|
+
source_agent_id: z
|
|
234
|
+
.string()
|
|
235
|
+
.optional()
|
|
236
|
+
.describe('If set, only list commands for this agent'),
|
|
237
|
+
}, async ({ source_agent_id }) => {
|
|
238
|
+
try {
|
|
239
|
+
const store = getStore();
|
|
240
|
+
const commands = store.listPending(source_agent_id ? { source_agent_id } : undefined);
|
|
241
|
+
return textResult(JSON.stringify({
|
|
242
|
+
ok: true,
|
|
243
|
+
count: commands.length,
|
|
244
|
+
commands,
|
|
245
|
+
}, null, 2));
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
return errorResult(err);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
server.tool('telegram_ack_command', 'Acknowledge (delete) a pending command by id after the host has delivered it to the target agent. Idempotent: unknown id returns ok=false.', {
|
|
252
|
+
command_id: z.string().min(1).describe('Pending command id from list/poll'),
|
|
253
|
+
}, async ({ command_id }) => {
|
|
254
|
+
try {
|
|
255
|
+
const store = getStore();
|
|
256
|
+
const cmd = store.ackCommand(command_id);
|
|
257
|
+
if (!cmd) {
|
|
258
|
+
return textResult(JSON.stringify({
|
|
259
|
+
ok: false,
|
|
260
|
+
error: 'unknown_command_id',
|
|
261
|
+
command_id,
|
|
262
|
+
}), true);
|
|
263
|
+
}
|
|
264
|
+
return textResult(JSON.stringify({
|
|
265
|
+
ok: true,
|
|
266
|
+
acked: true,
|
|
267
|
+
command_id: cmd.id,
|
|
268
|
+
source_agent_id: cmd.source_agent_id,
|
|
269
|
+
}), false);
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
return errorResult(err);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
145
275
|
return server;
|
|
146
276
|
}
|
|
147
277
|
async function main() {
|
|
@@ -149,7 +279,9 @@ async function main() {
|
|
|
149
279
|
const transport = new StdioServerTransport();
|
|
150
280
|
await server.connect(transport);
|
|
151
281
|
console.error(`${SERVER_NAME} v${SERVER_VERSION} running on stdio` +
|
|
152
|
-
(process.env.TELEGRAM_BOT_TOKEN
|
|
282
|
+
(process.env.TELEGRAM_BOT_TOKEN
|
|
283
|
+
? ''
|
|
284
|
+
: ' (TELEGRAM_BOT_TOKEN not set — tools will error until configured)'));
|
|
153
285
|
}
|
|
154
286
|
main().catch((err) => {
|
|
155
287
|
console.error('Fatal:', err);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound routing: allowlist, reply→mapping, /to <agent_id> fallback.
|
|
3
|
+
* Unroutable allowlisted messages are ignored (counted); never executed as shell.
|
|
4
|
+
*/
|
|
5
|
+
import type { PendingCommand, LocalStore } from './store.js';
|
|
6
|
+
import type { TelegramMessage, TelegramUpdate } from './telegram.js';
|
|
7
|
+
export interface AllowlistResult {
|
|
8
|
+
allowed: boolean;
|
|
9
|
+
chatId: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Allowed chats from TELEGRAM_CHAT_ALLOWLIST (comma-separated) or TELEGRAM_CHAT_ID.
|
|
13
|
+
* If neither is set, no chat is allowed for inbound commands.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getAllowedChatIds(env?: NodeJS.ProcessEnv): Set<string>;
|
|
16
|
+
export declare function isChatAllowed(chatId: string | number, env?: NodeJS.ProcessEnv): boolean;
|
|
17
|
+
/** Parse `/to <agent_id> <rest…>` — agent_id is the first whitespace-delimited token after /to. */
|
|
18
|
+
export declare function parseToAgentCommand(text: string): {
|
|
19
|
+
agentId: string;
|
|
20
|
+
body: string;
|
|
21
|
+
} | undefined;
|
|
22
|
+
export declare function truncateAgentLabel(idOrName: string, maxLen?: number): string;
|
|
23
|
+
/**
|
|
24
|
+
* Footer appended to outbound notify text (UX only; routing uses reply_to mapping).
|
|
25
|
+
*/
|
|
26
|
+
export declare function formatAgentFooter(sourceAgentId: string, sourceAgentName?: string): string;
|
|
27
|
+
export declare function formatNotifyWithFooter(title: string | undefined, body: string, sourceAgentId: string, sourceAgentName?: string, formatBase?: (title: string | undefined, body: string) => string): string;
|
|
28
|
+
export type InboundDisposition = {
|
|
29
|
+
kind: 'enqueued';
|
|
30
|
+
command: PendingCommand;
|
|
31
|
+
via: 'reply' | 'to_command';
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'ignored_not_allowlisted';
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'ignored_unroutable';
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'ignored_no_text';
|
|
38
|
+
};
|
|
39
|
+
export declare function routeInboundMessage(store: LocalStore, message: TelegramMessage, env?: NodeJS.ProcessEnv): InboundDisposition;
|
|
40
|
+
export interface PollSummary {
|
|
41
|
+
updates_seen: number;
|
|
42
|
+
enqueued: number;
|
|
43
|
+
ignored_not_allowlisted: number;
|
|
44
|
+
ignored_unroutable: number;
|
|
45
|
+
ignored_no_text: number;
|
|
46
|
+
commands: PendingCommand[];
|
|
47
|
+
next_offset?: number;
|
|
48
|
+
}
|
|
49
|
+
export declare function processUpdates(store: LocalStore, updates: TelegramUpdate[], env?: NodeJS.ProcessEnv): PollSummary;
|
package/dist/routing.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound routing: allowlist, reply→mapping, /to <agent_id> fallback.
|
|
3
|
+
* Unroutable allowlisted messages are ignored (counted); never executed as shell.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Allowed chats from TELEGRAM_CHAT_ALLOWLIST (comma-separated) or TELEGRAM_CHAT_ID.
|
|
7
|
+
* If neither is set, no chat is allowed for inbound commands.
|
|
8
|
+
*/
|
|
9
|
+
export function getAllowedChatIds(env = process.env) {
|
|
10
|
+
const allowlist = (env.TELEGRAM_CHAT_ALLOWLIST ?? '').trim();
|
|
11
|
+
const ids = new Set();
|
|
12
|
+
if (allowlist) {
|
|
13
|
+
for (const part of allowlist.split(',')) {
|
|
14
|
+
const id = part.trim();
|
|
15
|
+
if (id)
|
|
16
|
+
ids.add(id);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const single = (env.TELEGRAM_CHAT_ID ?? '').trim();
|
|
20
|
+
if (single)
|
|
21
|
+
ids.add(single);
|
|
22
|
+
return ids;
|
|
23
|
+
}
|
|
24
|
+
export function isChatAllowed(chatId, env = process.env) {
|
|
25
|
+
const allowed = getAllowedChatIds(env);
|
|
26
|
+
if (allowed.size === 0)
|
|
27
|
+
return false;
|
|
28
|
+
return allowed.has(String(chatId));
|
|
29
|
+
}
|
|
30
|
+
/** Parse `/to <agent_id> <rest…>` — agent_id is the first whitespace-delimited token after /to. */
|
|
31
|
+
export function parseToAgentCommand(text) {
|
|
32
|
+
const trimmed = text.trim();
|
|
33
|
+
const match = trimmed.match(/^\/to\s+(\S+)\s+([\s\S]+)$/i);
|
|
34
|
+
if (!match)
|
|
35
|
+
return undefined;
|
|
36
|
+
const agentId = match[1];
|
|
37
|
+
const body = match[2].trim();
|
|
38
|
+
if (!agentId || !body)
|
|
39
|
+
return undefined;
|
|
40
|
+
return { agentId, body };
|
|
41
|
+
}
|
|
42
|
+
export function truncateAgentLabel(idOrName, maxLen = 32) {
|
|
43
|
+
const s = idOrName.trim();
|
|
44
|
+
if (s.length <= maxLen)
|
|
45
|
+
return s;
|
|
46
|
+
return `${s.slice(0, maxLen - 1)}…`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Footer appended to outbound notify text (UX only; routing uses reply_to mapping).
|
|
50
|
+
*/
|
|
51
|
+
export function formatAgentFooter(sourceAgentId, sourceAgentName) {
|
|
52
|
+
const label = truncateAgentLabel((sourceAgentName ?? '').trim() || sourceAgentId);
|
|
53
|
+
return `\n\n— from agent ${label}\nReply to this message to continue with this agent.`;
|
|
54
|
+
}
|
|
55
|
+
export function formatNotifyWithFooter(title, body, sourceAgentId, sourceAgentName, formatBase = defaultFormatBase) {
|
|
56
|
+
return formatBase(title, body) + formatAgentFooter(sourceAgentId, sourceAgentName);
|
|
57
|
+
}
|
|
58
|
+
function defaultFormatBase(title, body) {
|
|
59
|
+
const t = (title ?? '').trim();
|
|
60
|
+
const b = body.trim();
|
|
61
|
+
if (t)
|
|
62
|
+
return `✅ ${t}\n\n${b}`;
|
|
63
|
+
return `✅ ${b}`;
|
|
64
|
+
}
|
|
65
|
+
export function routeInboundMessage(store, message, env = process.env) {
|
|
66
|
+
const chatId = message.chat?.id;
|
|
67
|
+
if (chatId === undefined || chatId === null) {
|
|
68
|
+
return { kind: 'ignored_unroutable' };
|
|
69
|
+
}
|
|
70
|
+
if (!isChatAllowed(chatId, env)) {
|
|
71
|
+
return { kind: 'ignored_not_allowlisted' };
|
|
72
|
+
}
|
|
73
|
+
const text = (message.text ?? '').trim();
|
|
74
|
+
if (!text) {
|
|
75
|
+
return { kind: 'ignored_no_text' };
|
|
76
|
+
}
|
|
77
|
+
const replyToId = message.reply_to_message?.message_id;
|
|
78
|
+
if (replyToId !== undefined) {
|
|
79
|
+
const mapping = store.getMapping(chatId, replyToId);
|
|
80
|
+
if (mapping) {
|
|
81
|
+
const command = store.enqueuePending({
|
|
82
|
+
source_agent_id: mapping.source_agent_id,
|
|
83
|
+
source_agent_name: mapping.source_agent_name,
|
|
84
|
+
conversation_hint: mapping.conversation_hint,
|
|
85
|
+
chat_id: String(chatId),
|
|
86
|
+
telegram_message_id: message.message_id,
|
|
87
|
+
reply_to_telegram_message_id: replyToId,
|
|
88
|
+
text,
|
|
89
|
+
});
|
|
90
|
+
return { kind: 'enqueued', command, via: 'reply' };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const toCmd = parseToAgentCommand(text);
|
|
94
|
+
if (toCmd) {
|
|
95
|
+
const command = store.enqueuePending({
|
|
96
|
+
source_agent_id: toCmd.agentId,
|
|
97
|
+
chat_id: String(chatId),
|
|
98
|
+
telegram_message_id: message.message_id,
|
|
99
|
+
reply_to_telegram_message_id: replyToId,
|
|
100
|
+
text: toCmd.body,
|
|
101
|
+
});
|
|
102
|
+
return { kind: 'enqueued', command, via: 'to_command' };
|
|
103
|
+
}
|
|
104
|
+
// Prefer ignore with count (documented): no mapping and no /to → drop
|
|
105
|
+
return { kind: 'ignored_unroutable' };
|
|
106
|
+
}
|
|
107
|
+
export function processUpdates(store, updates, env = process.env) {
|
|
108
|
+
const summary = {
|
|
109
|
+
updates_seen: updates.length,
|
|
110
|
+
enqueued: 0,
|
|
111
|
+
ignored_not_allowlisted: 0,
|
|
112
|
+
ignored_unroutable: 0,
|
|
113
|
+
ignored_no_text: 0,
|
|
114
|
+
commands: [],
|
|
115
|
+
};
|
|
116
|
+
let maxUpdateId;
|
|
117
|
+
for (const u of updates) {
|
|
118
|
+
if (maxUpdateId === undefined || u.update_id > maxUpdateId) {
|
|
119
|
+
maxUpdateId = u.update_id;
|
|
120
|
+
}
|
|
121
|
+
const message = u.message ?? u.edited_message;
|
|
122
|
+
if (!message)
|
|
123
|
+
continue;
|
|
124
|
+
const result = routeInboundMessage(store, message, env);
|
|
125
|
+
switch (result.kind) {
|
|
126
|
+
case 'enqueued':
|
|
127
|
+
summary.enqueued += 1;
|
|
128
|
+
summary.commands.push(result.command);
|
|
129
|
+
break;
|
|
130
|
+
case 'ignored_not_allowlisted':
|
|
131
|
+
summary.ignored_not_allowlisted += 1;
|
|
132
|
+
break;
|
|
133
|
+
case 'ignored_unroutable':
|
|
134
|
+
summary.ignored_unroutable += 1;
|
|
135
|
+
break;
|
|
136
|
+
case 'ignored_no_text':
|
|
137
|
+
summary.ignored_no_text += 1;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (maxUpdateId !== undefined) {
|
|
142
|
+
summary.next_offset = maxUpdateId + 1;
|
|
143
|
+
}
|
|
144
|
+
return summary;
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=routing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routing.js","sourceRoot":"","sources":["../src/routing.ts"],"names":[],"mappings":"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"}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local JSON persistence for outbound message→agent mappings and pending inbound commands.
|
|
3
|
+
* Path: TELEGRAM_NOTIFY_DATA_DIR or ./.telegram-notify-data
|
|
4
|
+
* Does not log secrets. Never execute Telegram text as shell.
|
|
5
|
+
*/
|
|
6
|
+
export interface OutboundMapping {
|
|
7
|
+
chat_id: string;
|
|
8
|
+
telegram_message_id: number;
|
|
9
|
+
source_agent_id: string;
|
|
10
|
+
source_agent_name?: string;
|
|
11
|
+
conversation_hint?: string;
|
|
12
|
+
created_at: string;
|
|
13
|
+
}
|
|
14
|
+
export type PendingCommandStatus = 'pending' | 'acked';
|
|
15
|
+
export interface PendingCommand {
|
|
16
|
+
id: string;
|
|
17
|
+
source_agent_id: string;
|
|
18
|
+
source_agent_name?: string;
|
|
19
|
+
conversation_hint?: string;
|
|
20
|
+
chat_id: string;
|
|
21
|
+
telegram_message_id: number;
|
|
22
|
+
reply_to_telegram_message_id?: number;
|
|
23
|
+
text: string;
|
|
24
|
+
created_at: string;
|
|
25
|
+
status: PendingCommandStatus;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function getDataDir(env?: NodeJS.ProcessEnv): string;
|
|
29
|
+
export declare class LocalStore {
|
|
30
|
+
readonly dataDir: string;
|
|
31
|
+
private readonly filePath;
|
|
32
|
+
constructor(dataDir?: string);
|
|
33
|
+
private ensureDir;
|
|
34
|
+
private read;
|
|
35
|
+
private write;
|
|
36
|
+
putMapping(entry: Omit<OutboundMapping, 'created_at'> & {
|
|
37
|
+
created_at?: string;
|
|
38
|
+
}): OutboundMapping;
|
|
39
|
+
getMapping(chatId: string | number, messageId: number): OutboundMapping | undefined;
|
|
40
|
+
enqueuePending(input: Omit<PendingCommand, 'id' | 'created_at' | 'status'> & {
|
|
41
|
+
id?: string;
|
|
42
|
+
created_at?: string;
|
|
43
|
+
status?: PendingCommandStatus;
|
|
44
|
+
}): PendingCommand;
|
|
45
|
+
listPending(filter?: {
|
|
46
|
+
source_agent_id?: string;
|
|
47
|
+
includeAcked?: boolean;
|
|
48
|
+
}): PendingCommand[];
|
|
49
|
+
/**
|
|
50
|
+
* Idempotent ack: marks pending→acked and removes from active list (or keeps as acked).
|
|
51
|
+
* Returns the command if found; undefined if unknown id.
|
|
52
|
+
*/
|
|
53
|
+
ackCommand(id: string): PendingCommand | undefined;
|
|
54
|
+
getUpdateOffset(): number | undefined;
|
|
55
|
+
setUpdateOffset(offset: number): void;
|
|
56
|
+
}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local JSON persistence for outbound message→agent mappings and pending inbound commands.
|
|
3
|
+
* Path: TELEGRAM_NOTIFY_DATA_DIR or ./.telegram-notify-data
|
|
4
|
+
* Does not log secrets. Never execute Telegram text as shell.
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
function mappingKey(chatId, messageId) {
|
|
10
|
+
return `${chatId}:${messageId}`;
|
|
11
|
+
}
|
|
12
|
+
export function getDataDir(env = process.env) {
|
|
13
|
+
const fromEnv = (env.TELEGRAM_NOTIFY_DATA_DIR ?? '').trim();
|
|
14
|
+
if (fromEnv)
|
|
15
|
+
return fromEnv;
|
|
16
|
+
return join(process.cwd(), '.telegram-notify-data');
|
|
17
|
+
}
|
|
18
|
+
function emptyStore() {
|
|
19
|
+
return { mappings: {}, pending: [], update_offset: undefined };
|
|
20
|
+
}
|
|
21
|
+
export class LocalStore {
|
|
22
|
+
dataDir;
|
|
23
|
+
filePath;
|
|
24
|
+
constructor(dataDir) {
|
|
25
|
+
this.dataDir = dataDir ?? getDataDir();
|
|
26
|
+
this.filePath = join(this.dataDir, 'store.json');
|
|
27
|
+
}
|
|
28
|
+
ensureDir() {
|
|
29
|
+
if (!existsSync(this.dataDir)) {
|
|
30
|
+
mkdirSync(this.dataDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
read() {
|
|
34
|
+
this.ensureDir();
|
|
35
|
+
if (!existsSync(this.filePath)) {
|
|
36
|
+
return emptyStore();
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const raw = readFileSync(this.filePath, 'utf8');
|
|
40
|
+
const parsed = JSON.parse(raw);
|
|
41
|
+
return {
|
|
42
|
+
mappings: parsed.mappings ?? {},
|
|
43
|
+
pending: Array.isArray(parsed.pending) ? parsed.pending : [],
|
|
44
|
+
update_offset: parsed.update_offset,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return emptyStore();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
write(data) {
|
|
52
|
+
this.ensureDir();
|
|
53
|
+
writeFileSync(this.filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
54
|
+
}
|
|
55
|
+
putMapping(entry) {
|
|
56
|
+
const data = this.read();
|
|
57
|
+
const full = {
|
|
58
|
+
chat_id: String(entry.chat_id),
|
|
59
|
+
telegram_message_id: entry.telegram_message_id,
|
|
60
|
+
source_agent_id: entry.source_agent_id,
|
|
61
|
+
source_agent_name: entry.source_agent_name,
|
|
62
|
+
conversation_hint: entry.conversation_hint,
|
|
63
|
+
created_at: entry.created_at ?? new Date().toISOString(),
|
|
64
|
+
};
|
|
65
|
+
data.mappings[mappingKey(full.chat_id, full.telegram_message_id)] = full;
|
|
66
|
+
this.write(data);
|
|
67
|
+
return full;
|
|
68
|
+
}
|
|
69
|
+
getMapping(chatId, messageId) {
|
|
70
|
+
const data = this.read();
|
|
71
|
+
return data.mappings[mappingKey(chatId, messageId)];
|
|
72
|
+
}
|
|
73
|
+
enqueuePending(input) {
|
|
74
|
+
const data = this.read();
|
|
75
|
+
const cmd = {
|
|
76
|
+
id: input.id ?? randomUUID(),
|
|
77
|
+
source_agent_id: input.source_agent_id,
|
|
78
|
+
source_agent_name: input.source_agent_name,
|
|
79
|
+
conversation_hint: input.conversation_hint,
|
|
80
|
+
chat_id: String(input.chat_id),
|
|
81
|
+
telegram_message_id: input.telegram_message_id,
|
|
82
|
+
reply_to_telegram_message_id: input.reply_to_telegram_message_id,
|
|
83
|
+
text: input.text,
|
|
84
|
+
created_at: input.created_at ?? new Date().toISOString(),
|
|
85
|
+
status: input.status ?? 'pending',
|
|
86
|
+
error: input.error,
|
|
87
|
+
};
|
|
88
|
+
data.pending.push(cmd);
|
|
89
|
+
this.write(data);
|
|
90
|
+
return cmd;
|
|
91
|
+
}
|
|
92
|
+
listPending(filter) {
|
|
93
|
+
const data = this.read();
|
|
94
|
+
return data.pending.filter((c) => {
|
|
95
|
+
if (!filter?.includeAcked && c.status !== 'pending')
|
|
96
|
+
return false;
|
|
97
|
+
if (filter?.source_agent_id && c.source_agent_id !== filter.source_agent_id)
|
|
98
|
+
return false;
|
|
99
|
+
return true;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Idempotent ack: marks pending→acked and removes from active list (or keeps as acked).
|
|
104
|
+
* Returns the command if found; undefined if unknown id.
|
|
105
|
+
*/
|
|
106
|
+
ackCommand(id) {
|
|
107
|
+
const data = this.read();
|
|
108
|
+
const idx = data.pending.findIndex((c) => c.id === id);
|
|
109
|
+
if (idx < 0)
|
|
110
|
+
return undefined;
|
|
111
|
+
const cmd = { ...data.pending[idx], status: 'acked' };
|
|
112
|
+
// Drop acked rows to keep the file small (at-least-once already delivered)
|
|
113
|
+
data.pending.splice(idx, 1);
|
|
114
|
+
this.write(data);
|
|
115
|
+
return cmd;
|
|
116
|
+
}
|
|
117
|
+
getUpdateOffset() {
|
|
118
|
+
return this.read().update_offset;
|
|
119
|
+
}
|
|
120
|
+
setUpdateOffset(offset) {
|
|
121
|
+
const data = this.read();
|
|
122
|
+
data.update_offset = offset;
|
|
123
|
+
this.write(data);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=store.js.map
|