@poncho-ai/messaging 0.2.8 → 0.3.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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/messaging@0.2.8 build /home/runner/work/poncho-ai/poncho-ai/packages/messaging
2
+ > @poncho-ai/messaging@0.3.0 build /Users/cesar/Dev/latitude/poncho-ai/packages/messaging
3
3
  > tsup src/index.ts --format esm --dts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -7,8 +7,8 @@
7
7
  CLI tsup v8.5.1
8
8
  CLI Target: es2022
9
9
  ESM Build start
10
- ESM dist/index.js 30.13 KB
11
- ESM ⚡️ Build success in 77ms
10
+ ESM dist/index.js 44.67 KB
11
+ ESM ⚡️ Build success in 55ms
12
12
  DTS Build start
13
- DTS ⚡️ Build success in 5331ms
14
- DTS dist/index.d.ts 8.98 KB
13
+ DTS ⚡️ Build success in 3450ms
14
+ DTS dist/index.d.ts 9.98 KB
@@ -0,0 +1,29 @@
1
+
2
+ > @poncho-ai/messaging@0.2.4 test /Users/cesar/Dev/latitude/poncho-ai/packages/messaging
3
+ > vitest
4
+
5
+
6
+  RUN  v1.6.1 /Users/cesar/Dev/latitude/poncho-ai/packages/messaging
7
+
8
+ stderr | test/bridge.test.ts > AgentBridge > posts an error message and cleans up on runner failure
9
+ [agent-bridge] handleMessage error: Model overloaded
10
+
11
+ stderr | test/bridge.test.ts > AgentBridge > skips sendReply when autoReply is false
12
+ [agent-bridge] tool mode completed without send_email being called; no reply sent
13
+
14
+ stderr | test/bridge.test.ts > AgentBridge > suppresses error reply when hasSentInCurrentRequest is true
15
+ [agent-bridge] handleMessage error: Oops
16
+
17
+ stderr | test/bridge.test.ts > AgentBridge > calls resetRequestState before handling each message
18
+ [agent-bridge] tool mode completed without send_email being called; no reply sent
19
+
20
+ ✓ test/bridge.test.ts  (15 tests) 7ms
21
+ ✓ test/adapters/email-utils.test.ts  (44 tests) 13ms
22
+ ✓ test/adapters/resend.test.ts  (13 tests) 7ms
23
+ ✓ test/adapters/slack.test.ts  (17 tests) 61ms
24
+
25
+  Test Files  4 passed (4)
26
+  Tests  89 passed (89)
27
+  Start at  17:47:42
28
+  Duration  442ms (transform 223ms, setup 0ms, collect 336ms, tests 88ms, environment 0ms, prepare 304ms)
29
+
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @poncho-ai/messaging
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`de28ef5`](https://github.com/cesr/poncho-ai/commit/de28ef5acceed921269d15816acd392f5208f03d) Thanks [@cesr](https://github.com/cesr)! - Add Telegram messaging adapter with private/group chat support, file attachments, /new command, and typing indicators.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [[`de28ef5`](https://github.com/cesr/poncho-ai/commit/de28ef5acceed921269d15816acd392f5208f03d)]:
12
+ - @poncho-ai/sdk@1.4.1
13
+
14
+ ## 0.2.9
15
+
16
+ ### Patch Changes
17
+
18
+ - [`e9b801f`](https://github.com/cesr/poncho-ai/commit/e9b801f0c70ffab6cb434b7adf05df22b29ea9fe) Thanks [@cesr](https://github.com/cesr)! - Derive deterministic UUIDs for messaging conversation IDs instead of composite strings. Fixes Latitude telemetry rejection and ensures consistency with web UI/API conversations.
19
+
3
20
  ## 0.2.8
4
21
 
5
22
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -189,6 +189,36 @@ declare class ResendAdapter implements MessagingAdapter {
189
189
  private fetchAndDownloadAttachments;
190
190
  }
191
191
 
192
+ interface TelegramAdapterOptions {
193
+ botTokenEnv?: string;
194
+ webhookSecretEnv?: string;
195
+ }
196
+ declare class TelegramAdapter implements MessagingAdapter {
197
+ readonly platform: "telegram";
198
+ readonly autoReply = true;
199
+ readonly hasSentInCurrentRequest = false;
200
+ private botToken;
201
+ private webhookSecret;
202
+ private botUsername;
203
+ private botId;
204
+ private readonly botTokenEnv;
205
+ private readonly webhookSecretEnv;
206
+ private handler;
207
+ private readonly sessionCounters;
208
+ private lastUpdateId;
209
+ constructor(options?: TelegramAdapterOptions);
210
+ initialize(): Promise<void>;
211
+ onMessage(handler: IncomingMessageHandler): void;
212
+ registerRoutes(router: RouteRegistrar): void;
213
+ sendReply(threadRef: ThreadRef, content: string, options?: {
214
+ files?: FileAttachment[];
215
+ }): Promise<void>;
216
+ indicateProcessing(threadRef: ThreadRef): Promise<() => Promise<void>>;
217
+ private handleRequest;
218
+ private sessionKey;
219
+ private extractFiles;
220
+ }
221
+
192
222
  /** Extract the bare email address from a formatted string like `"Name <addr>"`. */
193
223
  declare function extractEmailAddress(formatted: string): string;
194
224
  /** Extract the display name from `"Name <addr>"`, or return `undefined`. */
@@ -241,4 +271,4 @@ declare function markdownToEmailHtml(text: string): string;
241
271
  */
242
272
  declare function matchesSenderPattern(sender: string, patterns: string[] | undefined): boolean;
243
273
 
244
- export { AgentBridge, type AgentBridgeOptions, type AgentRunner, type FileAttachment, type IncomingMessage, type IncomingMessageHandler, type MessagingAdapter, ResendAdapter, type ResendAdapterOptions, type RouteHandler, type RouteRegistrar, SlackAdapter, type SlackAdapterOptions, type ThreadRef, buildReplyHeaders, buildReplySubject, deriveRootMessageId, extractDisplayName, extractEmailAddress, markdownToEmailHtml, matchesSenderPattern, parseReferences, stripQuotedReply };
274
+ export { AgentBridge, type AgentBridgeOptions, type AgentRunner, type FileAttachment, type IncomingMessage, type IncomingMessageHandler, type MessagingAdapter, ResendAdapter, type ResendAdapterOptions, type RouteHandler, type RouteRegistrar, SlackAdapter, type SlackAdapterOptions, TelegramAdapter, type TelegramAdapterOptions, type ThreadRef, buildReplyHeaders, buildReplySubject, deriveRootMessageId, extractDisplayName, extractEmailAddress, markdownToEmailHtml, matchesSenderPattern, parseReferences, stripQuotedReply };
package/dist/index.js CHANGED
@@ -1,5 +1,16 @@
1
1
  // src/bridge.ts
2
- var conversationIdFromThread = (platform, ref) => `${platform}:${ref.channelId}:${ref.platformThreadId}`;
2
+ import { createHash } from "crypto";
3
+ var conversationIdFromThread = (platform, ref) => {
4
+ const key = `${platform}:${ref.channelId}:${ref.platformThreadId}`;
5
+ const hex = createHash("sha256").update(key).digest("hex").slice(0, 32);
6
+ return [
7
+ hex.slice(0, 8),
8
+ hex.slice(8, 12),
9
+ `4${hex.slice(13, 16)}`,
10
+ (parseInt(hex.slice(16, 18), 16) & 63 | 128).toString(16).padStart(2, "0") + hex.slice(18, 20),
11
+ hex.slice(20, 32)
12
+ ].join("-");
13
+ };
3
14
  var AgentBridge = class {
4
15
  adapter;
5
16
  runner;
@@ -30,9 +41,11 @@ var AgentBridge = class {
30
41
  message.platform,
31
42
  message.threadRef
32
43
  );
33
- const titleParts = [message.sender.id];
44
+ const platformTag = `[${message.platform.charAt(0).toUpperCase()}${message.platform.slice(1)}]`;
45
+ const senderLabel = message.sender.name || message.sender.id;
46
+ const titleParts = [platformTag, senderLabel];
34
47
  if (message.subject) titleParts.push(message.subject);
35
- const title = titleParts.join(" \u2014 ") || `${message.platform} thread`;
48
+ const title = titleParts.join(" ") || `${message.platform} thread`;
36
49
  const conversation = await this.runner.getOrCreateConversation(
37
50
  conversationId,
38
51
  {
@@ -319,7 +332,7 @@ var SlackAdapter = class {
319
332
  import { createHmac as createHmac2 } from "crypto";
320
333
 
321
334
  // src/adapters/email/utils.ts
322
- import { createHash } from "crypto";
335
+ import { createHash as createHash2 } from "crypto";
323
336
  var ADDR_RE = /<([^>]+)>/;
324
337
  function extractEmailAddress(formatted) {
325
338
  const match = ADDR_RE.exec(formatted);
@@ -350,7 +363,7 @@ function deriveRootMessageId(references, currentMessageId, fallback) {
350
363
  if (references.length > 0) return references[0];
351
364
  if (fallback) {
352
365
  const normalised = normaliseSubject(fallback.subject) + "\0" + fallback.sender.toLowerCase();
353
- const hash = createHash("sha256").update(normalised).digest("hex").slice(0, 16);
366
+ const hash = createHash2("sha256").update(normalised).digest("hex").slice(0, 16);
354
367
  return `<fallback:${hash}>`;
355
368
  }
356
369
  return currentMessageId;
@@ -907,10 +920,445 @@ var ResendAdapter = class {
907
920
  return results;
908
921
  }
909
922
  };
923
+
924
+ // src/adapters/telegram/verify.ts
925
+ import { timingSafeEqual as timingSafeEqual2 } from "crypto";
926
+ var verifyTelegramSecret = (expected, received) => {
927
+ if (!received) return false;
928
+ if (expected.length !== received.length) return false;
929
+ return timingSafeEqual2(Buffer.from(expected), Buffer.from(received));
930
+ };
931
+
932
+ // src/adapters/telegram/utils.ts
933
+ var TELEGRAM_API = "https://api.telegram.org";
934
+ var TELEGRAM_MAX_MESSAGE_LENGTH = 4096;
935
+ var telegramFetch = async (token, method, body) => {
936
+ const res = await fetch(`${TELEGRAM_API}/bot${token}/${method}`, {
937
+ method: "POST",
938
+ headers: { "content-type": "application/json" },
939
+ body: JSON.stringify(body)
940
+ });
941
+ return await res.json();
942
+ };
943
+ var telegramUpload = async (token, method, formData) => {
944
+ const res = await fetch(`${TELEGRAM_API}/bot${token}/${method}`, {
945
+ method: "POST",
946
+ body: formData
947
+ });
948
+ return await res.json();
949
+ };
950
+ var getMe = async (token) => {
951
+ const result = await telegramFetch(token, "getMe", {});
952
+ if (!result.ok) {
953
+ throw new Error(`Telegram getMe failed: ${result.description}`);
954
+ }
955
+ const user = result.result;
956
+ return { id: user.id, username: user.username ?? "" };
957
+ };
958
+ var getFile = async (token, fileId) => {
959
+ const result = await telegramFetch(token, "getFile", { file_id: fileId });
960
+ if (!result.ok) {
961
+ throw new Error(`Telegram getFile failed: ${result.description}`);
962
+ }
963
+ const file = result.result;
964
+ if (!file.file_path) {
965
+ throw new Error("Telegram getFile: no file_path returned");
966
+ }
967
+ return file.file_path;
968
+ };
969
+ var EXTENSION_MEDIA_TYPES = {
970
+ jpg: "image/jpeg",
971
+ jpeg: "image/jpeg",
972
+ png: "image/png",
973
+ gif: "image/gif",
974
+ webp: "image/webp",
975
+ pdf: "application/pdf",
976
+ mp4: "video/mp4",
977
+ ogg: "audio/ogg",
978
+ mp3: "audio/mpeg"
979
+ };
980
+ var inferMediaType = (filePath, header) => {
981
+ if (header && header !== "application/octet-stream") return header;
982
+ const ext = filePath.split(".").pop()?.toLowerCase();
983
+ if (ext && EXTENSION_MEDIA_TYPES[ext]) return EXTENSION_MEDIA_TYPES[ext];
984
+ return header ?? "application/octet-stream";
985
+ };
986
+ var downloadFile = async (token, filePath) => {
987
+ const url = `${TELEGRAM_API}/file/bot${token}/${filePath}`;
988
+ const res = await fetch(url);
989
+ if (!res.ok) {
990
+ throw new Error(`Telegram file download failed: ${res.status}`);
991
+ }
992
+ const buffer = Buffer.from(await res.arrayBuffer());
993
+ const mediaType = inferMediaType(filePath, res.headers.get("content-type"));
994
+ return { data: buffer.toString("base64"), mediaType };
995
+ };
996
+ var sendMessage = async (token, chatId, text, opts) => {
997
+ const body = { chat_id: chatId, text };
998
+ if (opts?.reply_to_message_id) {
999
+ body.reply_parameters = {
1000
+ message_id: opts.reply_to_message_id,
1001
+ allow_sending_without_reply: true
1002
+ };
1003
+ }
1004
+ if (opts?.message_thread_id) {
1005
+ body.message_thread_id = opts.message_thread_id;
1006
+ }
1007
+ const result = await telegramFetch(token, "sendMessage", body);
1008
+ if (!result.ok) {
1009
+ throw new Error(`Telegram sendMessage failed: ${result.description}`);
1010
+ }
1011
+ };
1012
+ var sendPhoto = async (token, chatId, photoData, opts) => {
1013
+ const formData = new FormData();
1014
+ formData.append("chat_id", String(chatId));
1015
+ const blob = new Blob([Buffer.from(photoData, "base64")]);
1016
+ formData.append("photo", blob, opts?.filename ?? "photo.jpg");
1017
+ if (opts?.caption) formData.append("caption", opts.caption);
1018
+ if (opts?.reply_to_message_id) {
1019
+ formData.append(
1020
+ "reply_parameters",
1021
+ JSON.stringify({
1022
+ message_id: opts.reply_to_message_id,
1023
+ allow_sending_without_reply: true
1024
+ })
1025
+ );
1026
+ }
1027
+ if (opts?.message_thread_id) {
1028
+ formData.append("message_thread_id", String(opts.message_thread_id));
1029
+ }
1030
+ const result = await telegramUpload(token, "sendPhoto", formData);
1031
+ if (!result.ok) {
1032
+ throw new Error(`Telegram sendPhoto failed: ${result.description}`);
1033
+ }
1034
+ };
1035
+ var sendDocument = async (token, chatId, docData, opts) => {
1036
+ const formData = new FormData();
1037
+ formData.append("chat_id", String(chatId));
1038
+ const blob = new Blob([Buffer.from(docData, "base64")], {
1039
+ type: opts?.mediaType
1040
+ });
1041
+ formData.append("document", blob, opts?.filename ?? "file");
1042
+ if (opts?.caption) formData.append("caption", opts.caption);
1043
+ if (opts?.reply_to_message_id) {
1044
+ formData.append(
1045
+ "reply_parameters",
1046
+ JSON.stringify({
1047
+ message_id: opts.reply_to_message_id,
1048
+ allow_sending_without_reply: true
1049
+ })
1050
+ );
1051
+ }
1052
+ if (opts?.message_thread_id) {
1053
+ formData.append("message_thread_id", String(opts.message_thread_id));
1054
+ }
1055
+ const result = await telegramUpload(token, "sendDocument", formData);
1056
+ if (!result.ok) {
1057
+ throw new Error(`Telegram sendDocument failed: ${result.description}`);
1058
+ }
1059
+ };
1060
+ var sendChatAction = async (token, chatId, action) => {
1061
+ await telegramFetch(token, "sendChatAction", {
1062
+ chat_id: chatId,
1063
+ action
1064
+ });
1065
+ };
1066
+ var splitMessage2 = (text) => {
1067
+ if (text.length <= TELEGRAM_MAX_MESSAGE_LENGTH) return [text];
1068
+ const chunks = [];
1069
+ let remaining = text;
1070
+ while (remaining.length > 0) {
1071
+ if (remaining.length <= TELEGRAM_MAX_MESSAGE_LENGTH) {
1072
+ chunks.push(remaining);
1073
+ break;
1074
+ }
1075
+ let cutPoint = remaining.lastIndexOf(
1076
+ "\n",
1077
+ TELEGRAM_MAX_MESSAGE_LENGTH
1078
+ );
1079
+ if (cutPoint <= 0) {
1080
+ cutPoint = TELEGRAM_MAX_MESSAGE_LENGTH;
1081
+ }
1082
+ chunks.push(remaining.slice(0, cutPoint));
1083
+ remaining = remaining.slice(cutPoint).replace(/^\n/, "");
1084
+ }
1085
+ return chunks;
1086
+ };
1087
+ var isBotMentioned = (entities, botUsername, botId, text) => {
1088
+ if (!entities || entities.length === 0) return false;
1089
+ const lower = botUsername.toLowerCase();
1090
+ for (const entity of entities) {
1091
+ if (entity.type === "mention") {
1092
+ const mentioned = text.slice(entity.offset, entity.offset + entity.length);
1093
+ if (mentioned.toLowerCase() === `@${lower}`) return true;
1094
+ }
1095
+ if (entity.type === "text_mention" && entity.user?.id === botId) {
1096
+ return true;
1097
+ }
1098
+ }
1099
+ return false;
1100
+ };
1101
+ var stripMention2 = (text, entities, botUsername, botId) => {
1102
+ if (!entities || entities.length === 0) return text.trim();
1103
+ const lower = botUsername.toLowerCase();
1104
+ for (const entity of entities) {
1105
+ let match = false;
1106
+ if (entity.type === "mention") {
1107
+ const mentioned = text.slice(entity.offset, entity.offset + entity.length);
1108
+ if (mentioned.toLowerCase() === `@${lower}`) match = true;
1109
+ }
1110
+ if (entity.type === "text_mention" && entity.user?.id === botId) {
1111
+ match = true;
1112
+ }
1113
+ if (match) {
1114
+ return (text.slice(0, entity.offset) + text.slice(entity.offset + entity.length)).trim();
1115
+ }
1116
+ }
1117
+ return text.trim();
1118
+ };
1119
+
1120
+ // src/adapters/telegram/index.ts
1121
+ var TYPING_INTERVAL_MS = 4e3;
1122
+ var NEW_COMMAND_RE = /^\/new(?:@(\S+))?$/i;
1123
+ var collectBody3 = (req) => new Promise((resolve, reject) => {
1124
+ const chunks = [];
1125
+ req.on("data", (chunk) => chunks.push(chunk));
1126
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
1127
+ req.on("error", reject);
1128
+ });
1129
+ var TelegramAdapter = class {
1130
+ platform = "telegram";
1131
+ autoReply = true;
1132
+ hasSentInCurrentRequest = false;
1133
+ botToken = "";
1134
+ webhookSecret = "";
1135
+ botUsername = "";
1136
+ botId = 0;
1137
+ botTokenEnv;
1138
+ webhookSecretEnv;
1139
+ handler;
1140
+ sessionCounters = /* @__PURE__ */ new Map();
1141
+ lastUpdateId = 0;
1142
+ constructor(options = {}) {
1143
+ this.botTokenEnv = options.botTokenEnv ?? "TELEGRAM_BOT_TOKEN";
1144
+ this.webhookSecretEnv = options.webhookSecretEnv ?? "TELEGRAM_WEBHOOK_SECRET";
1145
+ }
1146
+ // -----------------------------------------------------------------------
1147
+ // MessagingAdapter implementation
1148
+ // -----------------------------------------------------------------------
1149
+ async initialize() {
1150
+ this.botToken = process.env[this.botTokenEnv] ?? "";
1151
+ this.webhookSecret = process.env[this.webhookSecretEnv] ?? "";
1152
+ if (!this.botToken) {
1153
+ throw new Error(
1154
+ `Telegram messaging: ${this.botTokenEnv} environment variable is not set`
1155
+ );
1156
+ }
1157
+ const me = await getMe(this.botToken);
1158
+ this.botUsername = me.username;
1159
+ this.botId = me.id;
1160
+ }
1161
+ onMessage(handler) {
1162
+ this.handler = handler;
1163
+ }
1164
+ registerRoutes(router) {
1165
+ router(
1166
+ "POST",
1167
+ "/api/messaging/telegram",
1168
+ (req, res) => this.handleRequest(req, res)
1169
+ );
1170
+ }
1171
+ async sendReply(threadRef, content, options) {
1172
+ const chatId = threadRef.channelId;
1173
+ const replyTo = threadRef.messageId ? Number(threadRef.messageId) : void 0;
1174
+ if (content) {
1175
+ const chunks = splitMessage2(content);
1176
+ for (const chunk of chunks) {
1177
+ await sendMessage(this.botToken, chatId, chunk, {
1178
+ reply_to_message_id: replyTo
1179
+ });
1180
+ }
1181
+ }
1182
+ if (options?.files) {
1183
+ for (const file of options.files) {
1184
+ if (file.mediaType.startsWith("image/")) {
1185
+ await sendPhoto(this.botToken, chatId, file.data, {
1186
+ reply_to_message_id: replyTo,
1187
+ filename: file.filename
1188
+ });
1189
+ } else {
1190
+ await sendDocument(this.botToken, chatId, file.data, {
1191
+ reply_to_message_id: replyTo,
1192
+ filename: file.filename,
1193
+ mediaType: file.mediaType
1194
+ });
1195
+ }
1196
+ }
1197
+ }
1198
+ }
1199
+ async indicateProcessing(threadRef) {
1200
+ const chatId = threadRef.channelId;
1201
+ await sendChatAction(this.botToken, chatId, "typing");
1202
+ const interval = setInterval(() => {
1203
+ void sendChatAction(this.botToken, chatId, "typing").catch(() => {
1204
+ });
1205
+ }, TYPING_INTERVAL_MS);
1206
+ return async () => {
1207
+ clearInterval(interval);
1208
+ };
1209
+ }
1210
+ // -----------------------------------------------------------------------
1211
+ // HTTP request handling
1212
+ // -----------------------------------------------------------------------
1213
+ async handleRequest(req, res) {
1214
+ const rawBody = await collectBody3(req);
1215
+ if (this.webhookSecret) {
1216
+ const headerSecret = req.headers["x-telegram-bot-api-secret-token"];
1217
+ if (!verifyTelegramSecret(this.webhookSecret, headerSecret)) {
1218
+ res.writeHead(401);
1219
+ res.end("Invalid secret");
1220
+ return;
1221
+ }
1222
+ }
1223
+ let payload;
1224
+ try {
1225
+ payload = JSON.parse(rawBody);
1226
+ } catch {
1227
+ res.writeHead(400);
1228
+ res.end("Invalid JSON");
1229
+ return;
1230
+ }
1231
+ if (payload.update_id <= this.lastUpdateId) {
1232
+ res.writeHead(200);
1233
+ res.end();
1234
+ return;
1235
+ }
1236
+ this.lastUpdateId = payload.update_id;
1237
+ const message = payload.message;
1238
+ if (!message) {
1239
+ res.writeHead(200);
1240
+ res.end();
1241
+ return;
1242
+ }
1243
+ const text = message.text ?? message.caption ?? "";
1244
+ const hasFiles = !!(message.photo || message.document);
1245
+ if (!text && !hasFiles) {
1246
+ res.writeHead(200);
1247
+ res.end();
1248
+ return;
1249
+ }
1250
+ const chatId = String(message.chat.id);
1251
+ const chatType = message.chat.type;
1252
+ const isGroup = chatType === "group" || chatType === "supergroup";
1253
+ const entities = message.entities ?? message.caption_entities;
1254
+ const newMatch = text.match(NEW_COMMAND_RE);
1255
+ if (newMatch) {
1256
+ const suffix = newMatch[1];
1257
+ if (isGroup && suffix && suffix.toLowerCase() !== this.botUsername.toLowerCase()) {
1258
+ res.writeHead(200);
1259
+ res.end();
1260
+ return;
1261
+ }
1262
+ const key2 = this.sessionKey(message);
1263
+ const current = this.sessionCounters.get(key2) ?? 0;
1264
+ this.sessionCounters.set(key2, current + 1);
1265
+ res.writeHead(200);
1266
+ res.end();
1267
+ await sendMessage(
1268
+ this.botToken,
1269
+ chatId,
1270
+ "Conversation reset. Send a new message to start fresh.",
1271
+ {
1272
+ reply_to_message_id: message.message_id,
1273
+ message_thread_id: message.message_thread_id
1274
+ }
1275
+ );
1276
+ return;
1277
+ }
1278
+ if (isGroup) {
1279
+ if (!isBotMentioned(entities, this.botUsername, this.botId, text)) {
1280
+ res.writeHead(200);
1281
+ res.end();
1282
+ return;
1283
+ }
1284
+ }
1285
+ const cleanText = isGroup ? stripMention2(text, entities, this.botUsername, this.botId) : text;
1286
+ if (!cleanText && !hasFiles) {
1287
+ res.writeHead(200);
1288
+ res.end();
1289
+ return;
1290
+ }
1291
+ res.writeHead(200);
1292
+ res.end();
1293
+ if (!this.handler) return;
1294
+ const files = await this.extractFiles(message);
1295
+ const key = this.sessionKey(message);
1296
+ const session = this.sessionCounters.get(key) ?? 0;
1297
+ const topicId = message.message_thread_id;
1298
+ const platformThreadId = topicId ? `${chatId}:${topicId}:${session}` : `${chatId}:${session}`;
1299
+ const userId = String(message.from?.id ?? "unknown");
1300
+ const userName = [message.from?.first_name, message.from?.last_name].filter(Boolean).join(" ") || void 0;
1301
+ const ponchoMessage = {
1302
+ text: cleanText,
1303
+ files: files.length > 0 ? files : void 0,
1304
+ threadRef: {
1305
+ platformThreadId,
1306
+ channelId: chatId,
1307
+ messageId: String(message.message_id)
1308
+ },
1309
+ sender: { id: userId, name: userName },
1310
+ platform: "telegram",
1311
+ raw: message
1312
+ };
1313
+ void this.handler(ponchoMessage).catch((err) => {
1314
+ console.error(
1315
+ "[telegram-adapter] unhandled message handler error",
1316
+ err
1317
+ );
1318
+ });
1319
+ }
1320
+ // -----------------------------------------------------------------------
1321
+ // Helpers
1322
+ // -----------------------------------------------------------------------
1323
+ sessionKey(message) {
1324
+ const chatId = String(message.chat.id);
1325
+ return message.message_thread_id ? `${chatId}:${message.message_thread_id}` : chatId;
1326
+ }
1327
+ async extractFiles(message) {
1328
+ const files = [];
1329
+ try {
1330
+ if (message.photo && message.photo.length > 0) {
1331
+ const largest = message.photo[message.photo.length - 1];
1332
+ const filePath = await getFile(this.botToken, largest.file_id);
1333
+ const { data } = await downloadFile(this.botToken, filePath);
1334
+ files.push({ data, mediaType: "image/jpeg", filename: "photo.jpg" });
1335
+ }
1336
+ if (message.document) {
1337
+ const filePath = await getFile(
1338
+ this.botToken,
1339
+ message.document.file_id
1340
+ );
1341
+ const downloaded = await downloadFile(this.botToken, filePath);
1342
+ files.push({
1343
+ data: downloaded.data,
1344
+ mediaType: message.document.mime_type ?? downloaded.mediaType,
1345
+ filename: message.document.file_name
1346
+ });
1347
+ }
1348
+ } catch (err) {
1349
+ console.warn(
1350
+ "[telegram-adapter] failed to download file:",
1351
+ err instanceof Error ? err.message : err
1352
+ );
1353
+ }
1354
+ return files;
1355
+ }
1356
+ };
910
1357
  export {
911
1358
  AgentBridge,
912
1359
  ResendAdapter,
913
1360
  SlackAdapter,
1361
+ TelegramAdapter,
914
1362
  buildReplyHeaders,
915
1363
  buildReplySubject,
916
1364
  deriveRootMessageId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/messaging",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "description": "Messaging platform adapters for Poncho agents (Slack, Telegram, etc.)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,7 +20,7 @@
20
20
  }
21
21
  },
22
22
  "dependencies": {
23
- "@poncho-ai/sdk": "1.4.0"
23
+ "@poncho-ai/sdk": "1.4.1"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "resend": ">=4.0.0"
@@ -40,6 +40,7 @@
40
40
  "agent",
41
41
  "messaging",
42
42
  "slack",
43
+ "telegram",
43
44
  "email",
44
45
  "resend"
45
46
  ],