@poncho-ai/messaging 0.5.1 → 0.7.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.5.1 build /home/runner/work/poncho-ai/poncho-ai/packages/messaging
2
+ > @poncho-ai/messaging@0.7.0 build /home/runner/work/poncho-ai/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 45.94 KB
11
- ESM ⚡️ Build success in 66ms
10
+ ESM dist/index.js 51.09 KB
11
+ ESM ⚡️ Build success in 73ms
12
12
  DTS Build start
13
- DTS ⚡️ Build success in 5216ms
14
- DTS dist/index.d.ts 10.13 KB
13
+ DTS ⚡️ Build success in 4701ms
14
+ DTS dist/index.d.ts 11.24 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @poncho-ai/messaging
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`26be28a`](https://github.com/cesr/poncho-ai/commit/26be28a958f2eb27dd78225f1cf80b67b16d673d) Thanks [@cesr](https://github.com/cesr)! - Add tool approval support in Telegram via inline keyboard buttons. When the agent needs approval for a tool call, the bot sends Approve/Deny buttons to the chat. After all decisions are made, the run resumes and the response is delivered. Approvals from the web UI for Telegram conversations are also routed back to the chat.
8
+
9
+ ## 0.6.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [`d1e1bfb`](https://github.com/cesr/poncho-ai/commit/d1e1bfbf35b18788ab79231ca675774e949f5116) Thanks [@cesr](https://github.com/cesr)! - Add proactive scheduled messaging via channel-targeted cron jobs. Cron jobs with `channel: telegram` (or `slack`) now automatically discover known conversations and send the agent's response directly to each chat, continuing the existing conversation history.
14
+
3
15
  ## 0.5.1
4
16
 
5
17
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -65,6 +65,8 @@ interface AgentRunner {
65
65
  platform: string;
66
66
  ownerId: string;
67
67
  title?: string;
68
+ channelId?: string;
69
+ platformThreadId?: string;
68
70
  }): Promise<{
69
71
  messages: Message[];
70
72
  }>;
@@ -104,6 +106,12 @@ interface AgentBridgeOptions {
104
106
  ownerId?: string;
105
107
  }
106
108
 
109
+ /**
110
+ * Derive a deterministic UUID from a platform thread reference.
111
+ * SHA-256 hashes the composite key and formats 16 bytes as a UUID v4-shaped
112
+ * string, ensuring a valid UUID that's stable across requests for the same thread.
113
+ */
114
+ declare const conversationIdFromThread: (platform: string, ref: ThreadRef) => string;
107
115
  declare class AgentBridge {
108
116
  private readonly adapter;
109
117
  private readonly runner;
@@ -192,6 +200,12 @@ declare class ResendAdapter implements MessagingAdapter {
192
200
  private fetchAndDownloadAttachments;
193
201
  }
194
202
 
203
+ interface TelegramApprovalInfo {
204
+ approvalId: string;
205
+ tool: string;
206
+ input: Record<string, unknown>;
207
+ }
208
+ type TelegramApprovalDecisionHandler = (approvalId: string, approved: boolean, chatId: string) => Promise<void>;
195
209
  interface TelegramAdapterOptions {
196
210
  botTokenEnv?: string;
197
211
  webhookSecretEnv?: string;
@@ -209,7 +223,9 @@ declare class TelegramAdapter implements MessagingAdapter {
209
223
  private readonly webhookSecretEnv;
210
224
  private readonly allowedUserIds;
211
225
  private handler;
226
+ private approvalDecisionHandler;
212
227
  private readonly sessionCounters;
228
+ private readonly approvalMessageIds;
213
229
  private lastUpdateId;
214
230
  constructor(options?: TelegramAdapterOptions);
215
231
  initialize(): Promise<void>;
@@ -219,7 +235,13 @@ declare class TelegramAdapter implements MessagingAdapter {
219
235
  files?: FileAttachment[];
220
236
  }): Promise<void>;
221
237
  indicateProcessing(threadRef: ThreadRef): Promise<() => Promise<void>>;
238
+ onApprovalDecision(handler: TelegramApprovalDecisionHandler): void;
239
+ sendApprovalRequest(chatId: string, approvals: TelegramApprovalInfo[], opts?: {
240
+ message_thread_id?: number;
241
+ }): Promise<void>;
242
+ updateApprovalMessage(approvalId: string, decision: "approved" | "denied", tool: string): Promise<void>;
222
243
  private handleRequest;
244
+ private handleCallbackQuery;
223
245
  private sessionKey;
224
246
  private extractFiles;
225
247
  }
@@ -276,4 +298,4 @@ declare function markdownToEmailHtml(text: string): string;
276
298
  */
277
299
  declare function matchesSenderPattern(sender: string, patterns: string[] | undefined): boolean;
278
300
 
279
- 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 };
301
+ 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 TelegramApprovalDecisionHandler, type TelegramApprovalInfo, type ThreadRef, buildReplyHeaders, buildReplySubject, conversationIdFromThread, deriveRootMessageId, extractDisplayName, extractEmailAddress, markdownToEmailHtml, matchesSenderPattern, parseReferences, stripQuotedReply };
package/dist/index.js CHANGED
@@ -65,7 +65,9 @@ ${message.text}`;
65
65
  {
66
66
  platform: message.platform,
67
67
  ownerId: this.ownerIdOverride ?? message.sender.id,
68
- title
68
+ title,
69
+ channelId: message.threadRef.channelId,
70
+ platformThreadId: message.threadRef.platformThreadId
69
71
  }
70
72
  );
71
73
  const result = await this.runner.run(conversationId, {
@@ -1078,6 +1080,49 @@ var sendDocument = async (token, chatId, docData, opts) => {
1078
1080
  throw new Error(`Telegram sendDocument failed: ${result.description}`);
1079
1081
  }
1080
1082
  };
1083
+ var sendMessageWithKeyboard = async (token, chatId, text, keyboard, opts) => {
1084
+ const body = {
1085
+ chat_id: chatId,
1086
+ text,
1087
+ reply_markup: { inline_keyboard: keyboard }
1088
+ };
1089
+ if (opts?.reply_to_message_id) {
1090
+ body.reply_parameters = {
1091
+ message_id: opts.reply_to_message_id,
1092
+ allow_sending_without_reply: true
1093
+ };
1094
+ }
1095
+ if (opts?.message_thread_id) {
1096
+ body.message_thread_id = opts.message_thread_id;
1097
+ }
1098
+ const result = await telegramFetch(token, "sendMessage", body);
1099
+ if (!result.ok) {
1100
+ throw new Error(`Telegram sendMessage (keyboard) failed: ${result.description}`);
1101
+ }
1102
+ const msg = result.result;
1103
+ return msg.message_id;
1104
+ };
1105
+ var editMessageText = async (token, chatId, messageId, text, opts) => {
1106
+ const body = {
1107
+ chat_id: chatId,
1108
+ message_id: messageId,
1109
+ text
1110
+ };
1111
+ if (opts?.reply_markup) {
1112
+ body.reply_markup = opts.reply_markup;
1113
+ }
1114
+ const result = await telegramFetch(token, "editMessageText", body);
1115
+ if (!result.ok) {
1116
+ throw new Error(`Telegram editMessageText failed: ${result.description}`);
1117
+ }
1118
+ };
1119
+ var answerCallbackQuery = async (token, callbackQueryId, opts) => {
1120
+ const body = {
1121
+ callback_query_id: callbackQueryId
1122
+ };
1123
+ if (opts?.text) body.text = opts.text;
1124
+ await telegramFetch(token, "answerCallbackQuery", body);
1125
+ };
1081
1126
  var sendChatAction = async (token, chatId, action) => {
1082
1127
  await telegramFetch(token, "sendChatAction", {
1083
1128
  chat_id: chatId,
@@ -1159,7 +1204,9 @@ var TelegramAdapter = class {
1159
1204
  webhookSecretEnv;
1160
1205
  allowedUserIds;
1161
1206
  handler;
1207
+ approvalDecisionHandler;
1162
1208
  sessionCounters = /* @__PURE__ */ new Map();
1209
+ approvalMessageIds = /* @__PURE__ */ new Map();
1163
1210
  lastUpdateId = 0;
1164
1211
  constructor(options = {}) {
1165
1212
  this.botTokenEnv = options.botTokenEnv ?? "TELEGRAM_BOT_TOKEN";
@@ -1231,6 +1278,72 @@ var TelegramAdapter = class {
1231
1278
  };
1232
1279
  }
1233
1280
  // -----------------------------------------------------------------------
1281
+ // Approval support
1282
+ // -----------------------------------------------------------------------
1283
+ onApprovalDecision(handler) {
1284
+ this.approvalDecisionHandler = handler;
1285
+ }
1286
+ async sendApprovalRequest(chatId, approvals, opts) {
1287
+ const MAX_INPUT_LENGTH = 3500;
1288
+ for (const approval of approvals) {
1289
+ let inputSummary = Object.entries(approval.input).map(([k, v]) => ` ${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`).join("\n");
1290
+ if (inputSummary.length > MAX_INPUT_LENGTH) {
1291
+ inputSummary = inputSummary.slice(0, MAX_INPUT_LENGTH) + "\n \u2026(truncated)";
1292
+ }
1293
+ const text = [
1294
+ `\u{1F527} Tool approval required: ${approval.tool}`,
1295
+ "",
1296
+ inputSummary ? `Input:
1297
+ ${inputSummary}` : "(no input)"
1298
+ ].join("\n");
1299
+ const keyboard = [
1300
+ [
1301
+ { text: "\u2705 Approve", callback_data: `a:${approval.approvalId}` },
1302
+ { text: "\u274C Deny", callback_data: `d:${approval.approvalId}` }
1303
+ ]
1304
+ ];
1305
+ try {
1306
+ const messageId = await sendMessageWithKeyboard(
1307
+ this.botToken,
1308
+ chatId,
1309
+ text,
1310
+ keyboard,
1311
+ { message_thread_id: opts?.message_thread_id }
1312
+ );
1313
+ this.approvalMessageIds.set(approval.approvalId, {
1314
+ chatId,
1315
+ messageId
1316
+ });
1317
+ } catch (err) {
1318
+ console.error(
1319
+ "[telegram-adapter] failed to send approval request:",
1320
+ err instanceof Error ? err.message : err
1321
+ );
1322
+ }
1323
+ }
1324
+ }
1325
+ async updateApprovalMessage(approvalId, decision, tool) {
1326
+ const tracked = this.approvalMessageIds.get(approvalId);
1327
+ if (!tracked) return;
1328
+ const icon = decision === "approved" ? "\u2705" : "\u274C";
1329
+ const label = decision === "approved" ? "Approved" : "Denied";
1330
+ const text = `${icon} ${label}: ${tool}`;
1331
+ try {
1332
+ await editMessageText(
1333
+ this.botToken,
1334
+ tracked.chatId,
1335
+ tracked.messageId,
1336
+ text
1337
+ );
1338
+ } catch (err) {
1339
+ console.warn(
1340
+ "[telegram-adapter] failed to update approval message:",
1341
+ err instanceof Error ? err.message : err
1342
+ );
1343
+ }
1344
+ this.approvalMessageIds.delete(approvalId);
1345
+ }
1346
+ // -----------------------------------------------------------------------
1234
1347
  // HTTP request handling
1235
1348
  // -----------------------------------------------------------------------
1236
1349
  async handleRequest(req, res) {
@@ -1257,6 +1370,17 @@ var TelegramAdapter = class {
1257
1370
  return;
1258
1371
  }
1259
1372
  this.lastUpdateId = payload.update_id;
1373
+ if (payload.callback_query) {
1374
+ res.writeHead(200);
1375
+ res.end();
1376
+ void this.handleCallbackQuery(payload.callback_query).catch((err) => {
1377
+ console.error(
1378
+ "[telegram-adapter] callback_query error:",
1379
+ err instanceof Error ? err.message : err
1380
+ );
1381
+ });
1382
+ return;
1383
+ }
1260
1384
  const message = payload.message;
1261
1385
  if (!message) {
1262
1386
  res.writeHead(200);
@@ -1348,6 +1472,37 @@ var TelegramAdapter = class {
1348
1472
  });
1349
1473
  }
1350
1474
  // -----------------------------------------------------------------------
1475
+ // Callback query handling
1476
+ // -----------------------------------------------------------------------
1477
+ async handleCallbackQuery(query) {
1478
+ if (this.allowedUserIds && !this.allowedUserIds.includes(query.from.id)) {
1479
+ await answerCallbackQuery(this.botToken, query.id, {
1480
+ text: "You are not authorized to approve tools."
1481
+ });
1482
+ return;
1483
+ }
1484
+ const data = query.data;
1485
+ if (!data) {
1486
+ await answerCallbackQuery(this.botToken, query.id);
1487
+ return;
1488
+ }
1489
+ const isApprove = data.startsWith("a:");
1490
+ const isDeny = data.startsWith("d:");
1491
+ if (!isApprove && !isDeny) {
1492
+ await answerCallbackQuery(this.botToken, query.id);
1493
+ return;
1494
+ }
1495
+ const approvalId = data.slice(2);
1496
+ const approved = isApprove;
1497
+ const chatId = query.message?.chat.id ? String(query.message.chat.id) : void 0;
1498
+ await answerCallbackQuery(this.botToken, query.id, {
1499
+ text: approved ? "Approved" : "Denied"
1500
+ });
1501
+ if (this.approvalDecisionHandler && chatId) {
1502
+ await this.approvalDecisionHandler(approvalId, approved, chatId);
1503
+ }
1504
+ }
1505
+ // -----------------------------------------------------------------------
1351
1506
  // Helpers
1352
1507
  // -----------------------------------------------------------------------
1353
1508
  sessionKey(message) {
@@ -1391,6 +1546,7 @@ export {
1391
1546
  TelegramAdapter,
1392
1547
  buildReplyHeaders,
1393
1548
  buildReplySubject,
1549
+ conversationIdFromThread,
1394
1550
  deriveRootMessageId,
1395
1551
  extractDisplayName,
1396
1552
  extractEmailAddress,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/messaging",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "Messaging platform adapters for Poncho agents (Slack, Telegram, etc.)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,15 +9,19 @@ import type {
9
9
  } from "../../types.js";
10
10
  import { verifyTelegramSecret } from "./verify.js";
11
11
  import {
12
+ type TelegramInlineKeyboardButton,
12
13
  type TelegramMessage,
13
14
  type TelegramUpdate,
15
+ answerCallbackQuery,
14
16
  downloadFile,
17
+ editMessageText,
15
18
  getFile,
16
19
  getMe,
17
20
  isBotMentioned,
18
21
  sendChatAction,
19
22
  sendDocument,
20
23
  sendMessage,
24
+ sendMessageWithKeyboard,
21
25
  sendPhoto,
22
26
  splitMessage,
23
27
  stripMention,
@@ -26,6 +30,18 @@ import {
26
30
  const TYPING_INTERVAL_MS = 4_000;
27
31
  const NEW_COMMAND_RE = /^\/new(?:@(\S+))?$/i;
28
32
 
33
+ export interface TelegramApprovalInfo {
34
+ approvalId: string;
35
+ tool: string;
36
+ input: Record<string, unknown>;
37
+ }
38
+
39
+ export type TelegramApprovalDecisionHandler = (
40
+ approvalId: string,
41
+ approved: boolean,
42
+ chatId: string,
43
+ ) => Promise<void>;
44
+
29
45
  export interface TelegramAdapterOptions {
30
46
  botTokenEnv?: string;
31
47
  webhookSecretEnv?: string;
@@ -53,7 +69,9 @@ export class TelegramAdapter implements MessagingAdapter {
53
69
  private readonly webhookSecretEnv: string;
54
70
  private readonly allowedUserIds: number[] | undefined;
55
71
  private handler: IncomingMessageHandler | undefined;
72
+ private approvalDecisionHandler: TelegramApprovalDecisionHandler | undefined;
56
73
  private readonly sessionCounters = new Map<string, number>();
74
+ private readonly approvalMessageIds = new Map<string, { chatId: string; messageId: number }>();
57
75
  private lastUpdateId = 0;
58
76
 
59
77
  constructor(options: TelegramAdapterOptions = {}) {
@@ -148,6 +166,92 @@ export class TelegramAdapter implements MessagingAdapter {
148
166
  };
149
167
  }
150
168
 
169
+ // -----------------------------------------------------------------------
170
+ // Approval support
171
+ // -----------------------------------------------------------------------
172
+
173
+ onApprovalDecision(handler: TelegramApprovalDecisionHandler): void {
174
+ this.approvalDecisionHandler = handler;
175
+ }
176
+
177
+ async sendApprovalRequest(
178
+ chatId: string,
179
+ approvals: TelegramApprovalInfo[],
180
+ opts?: { message_thread_id?: number },
181
+ ): Promise<void> {
182
+ const MAX_INPUT_LENGTH = 3500;
183
+
184
+ for (const approval of approvals) {
185
+ let inputSummary = Object.entries(approval.input)
186
+ .map(([k, v]) => ` ${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`)
187
+ .join("\n");
188
+
189
+ if (inputSummary.length > MAX_INPUT_LENGTH) {
190
+ inputSummary = inputSummary.slice(0, MAX_INPUT_LENGTH) + "\n …(truncated)";
191
+ }
192
+
193
+ const text = [
194
+ `🔧 Tool approval required: ${approval.tool}`,
195
+ "",
196
+ inputSummary ? `Input:\n${inputSummary}` : "(no input)",
197
+ ].join("\n");
198
+
199
+ const keyboard: TelegramInlineKeyboardButton[][] = [
200
+ [
201
+ { text: "✅ Approve", callback_data: `a:${approval.approvalId}` },
202
+ { text: "❌ Deny", callback_data: `d:${approval.approvalId}` },
203
+ ],
204
+ ];
205
+
206
+ try {
207
+ const messageId = await sendMessageWithKeyboard(
208
+ this.botToken,
209
+ chatId,
210
+ text,
211
+ keyboard,
212
+ { message_thread_id: opts?.message_thread_id },
213
+ );
214
+ this.approvalMessageIds.set(approval.approvalId, {
215
+ chatId,
216
+ messageId,
217
+ });
218
+ } catch (err) {
219
+ console.error(
220
+ "[telegram-adapter] failed to send approval request:",
221
+ err instanceof Error ? err.message : err,
222
+ );
223
+ }
224
+ }
225
+ }
226
+
227
+ async updateApprovalMessage(
228
+ approvalId: string,
229
+ decision: "approved" | "denied",
230
+ tool: string,
231
+ ): Promise<void> {
232
+ const tracked = this.approvalMessageIds.get(approvalId);
233
+ if (!tracked) return;
234
+
235
+ const icon = decision === "approved" ? "✅" : "❌";
236
+ const label = decision === "approved" ? "Approved" : "Denied";
237
+ const text = `${icon} ${label}: ${tool}`;
238
+
239
+ try {
240
+ await editMessageText(
241
+ this.botToken,
242
+ tracked.chatId,
243
+ tracked.messageId,
244
+ text,
245
+ );
246
+ } catch (err) {
247
+ console.warn(
248
+ "[telegram-adapter] failed to update approval message:",
249
+ err instanceof Error ? err.message : err,
250
+ );
251
+ }
252
+ this.approvalMessageIds.delete(approvalId);
253
+ }
254
+
151
255
  // -----------------------------------------------------------------------
152
256
  // HTTP request handling
153
257
  // -----------------------------------------------------------------------
@@ -187,6 +291,19 @@ export class TelegramAdapter implements MessagingAdapter {
187
291
  }
188
292
  this.lastUpdateId = payload.update_id;
189
293
 
294
+ // -- Callback query (inline keyboard button press) ---------------------
295
+ if (payload.callback_query) {
296
+ res.writeHead(200);
297
+ res.end();
298
+ void this.handleCallbackQuery(payload.callback_query).catch((err) => {
299
+ console.error(
300
+ "[telegram-adapter] callback_query error:",
301
+ err instanceof Error ? err.message : err,
302
+ );
303
+ });
304
+ return;
305
+ }
306
+
190
307
  const message = payload.message;
191
308
  if (!message) {
192
309
  res.writeHead(200);
@@ -313,6 +430,48 @@ export class TelegramAdapter implements MessagingAdapter {
313
430
  });
314
431
  }
315
432
 
433
+ // -----------------------------------------------------------------------
434
+ // Callback query handling
435
+ // -----------------------------------------------------------------------
436
+
437
+ private async handleCallbackQuery(
438
+ query: import("./utils.js").TelegramCallbackQuery,
439
+ ): Promise<void> {
440
+ if (this.allowedUserIds && !this.allowedUserIds.includes(query.from.id)) {
441
+ await answerCallbackQuery(this.botToken, query.id, {
442
+ text: "You are not authorized to approve tools.",
443
+ });
444
+ return;
445
+ }
446
+
447
+ const data = query.data;
448
+ if (!data) {
449
+ await answerCallbackQuery(this.botToken, query.id);
450
+ return;
451
+ }
452
+
453
+ const isApprove = data.startsWith("a:");
454
+ const isDeny = data.startsWith("d:");
455
+ if (!isApprove && !isDeny) {
456
+ await answerCallbackQuery(this.botToken, query.id);
457
+ return;
458
+ }
459
+
460
+ const approvalId = data.slice(2);
461
+ const approved = isApprove;
462
+ const chatId = query.message?.chat.id
463
+ ? String(query.message.chat.id)
464
+ : undefined;
465
+
466
+ await answerCallbackQuery(this.botToken, query.id, {
467
+ text: approved ? "Approved" : "Denied",
468
+ });
469
+
470
+ if (this.approvalDecisionHandler && chatId) {
471
+ await this.approvalDecisionHandler(approvalId, approved, chatId);
472
+ }
473
+ }
474
+
316
475
  // -----------------------------------------------------------------------
317
476
  // Helpers
318
477
  // -----------------------------------------------------------------------
@@ -55,9 +55,22 @@ export interface TelegramMessage {
55
55
  message_thread_id?: number;
56
56
  }
57
57
 
58
+ export interface TelegramCallbackQuery {
59
+ id: string;
60
+ from: TelegramUser;
61
+ message?: TelegramMessage;
62
+ data?: string;
63
+ }
64
+
65
+ export interface TelegramInlineKeyboardButton {
66
+ text: string;
67
+ callback_data?: string;
68
+ }
69
+
58
70
  export interface TelegramUpdate {
59
71
  update_id: number;
60
72
  message?: TelegramMessage;
73
+ callback_query?: TelegramCallbackQuery;
61
74
  }
62
75
 
63
76
  // ---------------------------------------------------------------------------
@@ -259,6 +272,72 @@ export const sendDocument = async (
259
272
  }
260
273
  };
261
274
 
275
+ // ---------------------------------------------------------------------------
276
+ // Inline keyboard messages
277
+ // ---------------------------------------------------------------------------
278
+
279
+ export const sendMessageWithKeyboard = async (
280
+ token: string,
281
+ chatId: number | string,
282
+ text: string,
283
+ keyboard: TelegramInlineKeyboardButton[][],
284
+ opts?: { reply_to_message_id?: number; message_thread_id?: number },
285
+ ): Promise<number> => {
286
+ const body: Record<string, unknown> = {
287
+ chat_id: chatId,
288
+ text,
289
+ reply_markup: { inline_keyboard: keyboard },
290
+ };
291
+ if (opts?.reply_to_message_id) {
292
+ body.reply_parameters = {
293
+ message_id: opts.reply_to_message_id,
294
+ allow_sending_without_reply: true,
295
+ };
296
+ }
297
+ if (opts?.message_thread_id) {
298
+ body.message_thread_id = opts.message_thread_id;
299
+ }
300
+ const result = await telegramFetch(token, "sendMessage", body);
301
+ if (!result.ok) {
302
+ throw new Error(`Telegram sendMessage (keyboard) failed: ${result.description}`);
303
+ }
304
+ const msg = result.result as { message_id: number };
305
+ return msg.message_id;
306
+ };
307
+
308
+ export const editMessageText = async (
309
+ token: string,
310
+ chatId: number | string,
311
+ messageId: number,
312
+ text: string,
313
+ opts?: { reply_markup?: { inline_keyboard: TelegramInlineKeyboardButton[][] } },
314
+ ): Promise<void> => {
315
+ const body: Record<string, unknown> = {
316
+ chat_id: chatId,
317
+ message_id: messageId,
318
+ text,
319
+ };
320
+ if (opts?.reply_markup) {
321
+ body.reply_markup = opts.reply_markup;
322
+ }
323
+ const result = await telegramFetch(token, "editMessageText", body);
324
+ if (!result.ok) {
325
+ throw new Error(`Telegram editMessageText failed: ${result.description}`);
326
+ }
327
+ };
328
+
329
+ export const answerCallbackQuery = async (
330
+ token: string,
331
+ callbackQueryId: string,
332
+ opts?: { text?: string },
333
+ ): Promise<void> => {
334
+ const body: Record<string, unknown> = {
335
+ callback_query_id: callbackQueryId,
336
+ };
337
+ if (opts?.text) body.text = opts.text;
338
+ await telegramFetch(token, "answerCallbackQuery", body);
339
+ };
340
+
262
341
  // ---------------------------------------------------------------------------
263
342
  // Typing indicator
264
343
  // ---------------------------------------------------------------------------
package/src/bridge.ts CHANGED
@@ -13,7 +13,7 @@ import type {
13
13
  * SHA-256 hashes the composite key and formats 16 bytes as a UUID v4-shaped
14
14
  * string, ensuring a valid UUID that's stable across requests for the same thread.
15
15
  */
16
- const conversationIdFromThread = (
16
+ export const conversationIdFromThread = (
17
17
  platform: string,
18
18
  ref: ThreadRef,
19
19
  ): string => {
@@ -94,6 +94,8 @@ export class AgentBridge {
94
94
  platform: message.platform,
95
95
  ownerId: this.ownerIdOverride ?? message.sender.id,
96
96
  title,
97
+ channelId: message.threadRef.channelId,
98
+ platformThreadId: message.threadRef.platformThreadId,
97
99
  },
98
100
  );
99
101
 
package/src/index.ts CHANGED
@@ -10,13 +10,17 @@ export type {
10
10
  ThreadRef,
11
11
  } from "./types.js";
12
12
 
13
- export { AgentBridge } from "./bridge.js";
13
+ export { AgentBridge, conversationIdFromThread } from "./bridge.js";
14
14
  export { SlackAdapter } from "./adapters/slack/index.js";
15
15
  export type { SlackAdapterOptions } from "./adapters/slack/index.js";
16
16
  export { ResendAdapter } from "./adapters/resend/index.js";
17
17
  export type { ResendAdapterOptions } from "./adapters/resend/index.js";
18
18
  export { TelegramAdapter } from "./adapters/telegram/index.js";
19
- export type { TelegramAdapterOptions } from "./adapters/telegram/index.js";
19
+ export type {
20
+ TelegramAdapterOptions,
21
+ TelegramApprovalInfo,
22
+ TelegramApprovalDecisionHandler,
23
+ } from "./adapters/telegram/index.js";
20
24
 
21
25
  export {
22
26
  buildReplyHeaders,
package/src/types.ts CHANGED
@@ -105,7 +105,13 @@ export interface MessagingAdapter {
105
105
  export interface AgentRunner {
106
106
  getOrCreateConversation(
107
107
  conversationId: string,
108
- meta: { platform: string; ownerId: string; title?: string },
108
+ meta: {
109
+ platform: string;
110
+ ownerId: string;
111
+ title?: string;
112
+ channelId?: string;
113
+ platformThreadId?: string;
114
+ },
109
115
  ): Promise<{ messages: Message[] }>;
110
116
 
111
117
  run(