@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.
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +12 -0
- package/dist/index.d.ts +23 -1
- package/dist/index.js +157 -1
- package/package.json +1 -1
- package/src/adapters/telegram/index.ts +159 -0
- package/src/adapters/telegram/utils.ts +79 -0
- package/src/bridge.ts +3 -1
- package/src/index.ts +6 -2
- package/src/types.ts +7 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/messaging@0.
|
|
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
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
[34mCLI[39m tsup v8.5.1
|
|
8
8
|
[34mCLI[39m Target: es2022
|
|
9
9
|
[34mESM[39m Build start
|
|
10
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
11
|
-
[32mESM[39m ⚡️ Build success in
|
|
10
|
+
[32mESM[39m [1mdist/index.js [22m[32m51.09 KB[39m
|
|
11
|
+
[32mESM[39m ⚡️ Build success in 73ms
|
|
12
12
|
[34mDTS[39m Build start
|
|
13
|
-
[32mDTS[39m ⚡️ Build success in
|
|
14
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
13
|
+
[32mDTS[39m ⚡️ Build success in 4701ms
|
|
14
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m11.24 KB[39m
|
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
|
@@ -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 {
|
|
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: {
|
|
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(
|