talon-agent 1.0.0 → 1.2.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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/package.json +15 -11
  4. package/prompts/dream.md +7 -3
  5. package/prompts/heartbeat.md +30 -0
  6. package/prompts/identity.md +1 -0
  7. package/prompts/teams.md +3 -0
  8. package/prompts/telegram.md +1 -0
  9. package/src/__tests__/chat-settings.test.ts +108 -2
  10. package/src/__tests__/cleanup-registry.test.ts +58 -0
  11. package/src/__tests__/config.test.ts +118 -52
  12. package/src/__tests__/cron-store-extended.test.ts +661 -0
  13. package/src/__tests__/cron-store.test.ts +145 -11
  14. package/src/__tests__/daily-log.test.ts +224 -13
  15. package/src/__tests__/dispatcher.test.ts +424 -23
  16. package/src/__tests__/dream.test.ts +1028 -0
  17. package/src/__tests__/errors-extended.test.ts +428 -0
  18. package/src/__tests__/errors.test.ts +95 -3
  19. package/src/__tests__/fuzz.test.ts +87 -15
  20. package/src/__tests__/gateway-actions.test.ts +1174 -433
  21. package/src/__tests__/gateway-http.test.ts +210 -19
  22. package/src/__tests__/gateway-retry.test.ts +359 -0
  23. package/src/__tests__/gateway-withRetry-extended.test.ts +343 -0
  24. package/src/__tests__/graph.test.ts +830 -0
  25. package/src/__tests__/handlers-stream.test.ts +208 -0
  26. package/src/__tests__/handlers.test.ts +2539 -70
  27. package/src/__tests__/heartbeat.test.ts +364 -0
  28. package/src/__tests__/history-extended.test.ts +775 -0
  29. package/src/__tests__/history-persistence.test.ts +74 -19
  30. package/src/__tests__/history.test.ts +113 -79
  31. package/src/__tests__/integration.test.ts +43 -8
  32. package/src/__tests__/log-init.test.ts +129 -0
  33. package/src/__tests__/log.test.ts +23 -5
  34. package/src/__tests__/media-index.test.ts +317 -35
  35. package/src/__tests__/plugin.test.ts +314 -0
  36. package/src/__tests__/prompt-builder-extended.test.ts +296 -0
  37. package/src/__tests__/prompt-builder.test.ts +44 -9
  38. package/src/__tests__/sessions.test.ts +258 -4
  39. package/src/__tests__/storage-save-errors.test.ts +342 -0
  40. package/src/__tests__/teams-frontend.test.ts +526 -31
  41. package/src/__tests__/telegram-formatting.test.ts +82 -0
  42. package/src/__tests__/terminal-commands.test.ts +208 -1
  43. package/src/__tests__/terminal-renderer.test.ts +223 -0
  44. package/src/__tests__/time.test.ts +107 -0
  45. package/src/__tests__/workspace-migrate.test.ts +256 -0
  46. package/src/__tests__/workspace.test.ts +63 -1
  47. package/src/backend/claude-sdk/tools.ts +64 -18
  48. package/src/bootstrap.ts +14 -14
  49. package/src/cli.ts +440 -125
  50. package/src/core/cron.ts +20 -5
  51. package/src/core/dispatcher.ts +27 -9
  52. package/src/core/dream.ts +79 -24
  53. package/src/core/errors.ts +12 -2
  54. package/src/core/gateway-actions.ts +182 -46
  55. package/src/core/gateway.ts +93 -41
  56. package/src/core/heartbeat.ts +515 -0
  57. package/src/core/plugin.ts +1 -1
  58. package/src/core/prompt-builder.ts +1 -4
  59. package/src/core/pulse.ts +4 -3
  60. package/src/frontend/teams/actions.ts +3 -1
  61. package/src/frontend/teams/formatting.ts +47 -8
  62. package/src/frontend/teams/graph.ts +35 -11
  63. package/src/frontend/teams/index.ts +155 -57
  64. package/src/frontend/teams/tools.ts +4 -6
  65. package/src/frontend/telegram/actions.ts +358 -82
  66. package/src/frontend/telegram/admin.ts +162 -72
  67. package/src/frontend/telegram/callbacks.ts +16 -10
  68. package/src/frontend/telegram/commands.ts +37 -21
  69. package/src/frontend/telegram/formatting.ts +2 -4
  70. package/src/frontend/telegram/handlers.ts +262 -66
  71. package/src/frontend/telegram/index.ts +39 -14
  72. package/src/frontend/telegram/middleware.ts +14 -4
  73. package/src/frontend/telegram/userbot.ts +16 -4
  74. package/src/frontend/terminal/renderer.ts +1 -4
  75. package/src/index.ts +28 -4
  76. package/src/storage/chat-settings.ts +32 -9
  77. package/src/storage/cron-store.ts +53 -11
  78. package/src/storage/daily-log.ts +72 -19
  79. package/src/storage/history.ts +39 -21
  80. package/src/storage/media-index.ts +37 -12
  81. package/src/storage/sessions.ts +3 -2
  82. package/src/util/cleanup-registry.ts +34 -0
  83. package/src/util/config.ts +85 -23
  84. package/src/util/log.ts +47 -17
  85. package/src/util/paths.ts +10 -0
  86. package/src/util/time.ts +29 -6
  87. package/src/util/watchdog.ts +5 -1
  88. package/src/util/workspace.ts +51 -10
@@ -6,7 +6,13 @@
6
6
  * by core/gateway-actions.ts before this is called.
7
7
  */
8
8
 
9
- import { readFileSync, statSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
9
+ import {
10
+ readFileSync,
11
+ statSync,
12
+ writeFileSync,
13
+ existsSync,
14
+ mkdirSync,
15
+ } from "node:fs";
10
16
  import { basename, resolve } from "node:path";
11
17
  import { dirs } from "../../util/paths.js";
12
18
  import type { Bot, InputFile as GrammyInputFile } from "grammy";
@@ -30,9 +36,13 @@ const TELEGRAM_MAX_TEXT = 4096;
30
36
 
31
37
  // ── Helpers ─────────────────────────────────────────────────────────────────
32
38
 
33
- function replyParams(body: Record<string, unknown>): { message_id: number } | undefined {
39
+ function replyParams(
40
+ body: Record<string, unknown>,
41
+ ): { message_id: number } | undefined {
34
42
  const replyTo = body.reply_to ?? body.reply_to_message_id;
35
- return typeof replyTo === "number" && replyTo > 0 ? { message_id: replyTo } : undefined;
43
+ return typeof replyTo === "number" && replyTo > 0
44
+ ? { message_id: replyTo }
45
+ : undefined;
36
46
  }
37
47
 
38
48
  export async function sendText(
@@ -42,7 +52,9 @@ export async function sendText(
42
52
  replyTo?: number,
43
53
  ): Promise<number> {
44
54
  if (text.length > TELEGRAM_MAX_TEXT) {
45
- throw new Error(`Message too long (${text.length} chars, max ${TELEGRAM_MAX_TEXT}).`);
55
+ throw new Error(
56
+ `Message too long (${text.length} chars, max ${TELEGRAM_MAX_TEXT}).`,
57
+ );
46
58
  }
47
59
  const html = markdownToTelegramHtml(text);
48
60
  try {
@@ -73,23 +85,33 @@ export function createTelegramActionHandler(
73
85
  ) {
74
86
  const scheduledMessages = new Map<string, ReturnType<typeof setTimeout>>();
75
87
 
76
- return async (body: Record<string, unknown>, chatId: number): Promise<ActionResult | null> => {
88
+ return async (
89
+ body: Record<string, unknown>,
90
+ chatId: number,
91
+ ): Promise<ActionResult | null> => {
77
92
  const action = body.action as string;
78
93
 
79
94
  switch (action) {
80
95
  // ── Messaging ─────────────────────────────────────────────────────
81
96
  case "send_message": {
82
97
  const text = String(body.text ?? "");
83
- const replyTo = typeof body.reply_to_message_id === "number" ? body.reply_to_message_id : undefined;
98
+ const replyTo =
99
+ typeof body.reply_to_message_id === "number"
100
+ ? body.reply_to_message_id
101
+ : undefined;
84
102
  gateway.incrementMessages(chatId);
85
- const msgId = await withRetry(() => sendText(bot, chatId, text, replyTo));
103
+ const msgId = await withRetry(() =>
104
+ sendText(bot, chatId, text, replyTo),
105
+ );
86
106
  return { ok: true, message_id: msgId };
87
107
  }
88
108
 
89
109
  case "reply_to": {
90
110
  const msgId = Number(body.message_id);
91
111
  gateway.incrementMessages(chatId);
92
- const sentId = await withRetry(() => sendText(bot, chatId, String(body.text ?? ""), msgId));
112
+ const sentId = await withRetry(() =>
113
+ sendText(bot, chatId, String(body.text ?? ""), msgId),
114
+ );
93
115
  return { ok: true, message_id: sentId };
94
116
  }
95
117
 
@@ -97,26 +119,49 @@ export function createTelegramActionHandler(
97
119
  gateway.incrementMessages(chatId);
98
120
  const emoji = String(body.emoji ?? "\uD83D\uDC4D");
99
121
  try {
100
- await withRetry(() => bot.api.setMessageReaction(chatId, Number(body.message_id), [
101
- { type: "emoji", emoji: emoji as "\uD83D\uDC4D" },
102
- ]));
122
+ await withRetry(() =>
123
+ bot.api.setMessageReaction(chatId, Number(body.message_id), [
124
+ { type: "emoji", emoji: emoji as "\uD83D\uDC4D" },
125
+ ]),
126
+ );
103
127
  } catch {
104
128
  try {
105
129
  await bot.api.setMessageReaction(chatId, Number(body.message_id), [
106
130
  { type: "emoji", emoji: "\uD83D\uDC4D" },
107
131
  ]);
108
- } catch (e) { return { ok: false, error: `Reaction failed: ${e instanceof Error ? e.message : e}` }; }
132
+ } catch (e) {
133
+ return {
134
+ ok: false,
135
+ error: `Reaction failed: ${e instanceof Error ? e.message : e}`,
136
+ };
137
+ }
109
138
  }
110
139
  return { ok: true };
111
140
  }
112
141
 
113
142
  case "edit_message": {
114
143
  const text = String(body.text ?? "");
115
- if (text.length > TELEGRAM_MAX_TEXT) return { ok: false, error: `Text too long (max ${TELEGRAM_MAX_TEXT})` };
144
+ if (text.length > TELEGRAM_MAX_TEXT)
145
+ return {
146
+ ok: false,
147
+ error: `Text too long (max ${TELEGRAM_MAX_TEXT})`,
148
+ };
116
149
  const html = markdownToTelegramHtml(text);
117
150
  await withRetry(async () => {
118
- try { await bot.api.editMessageText(chatId, Number(body.message_id), html, { parse_mode: "HTML" }); }
119
- catch { await bot.api.editMessageText(chatId, Number(body.message_id), text); }
151
+ try {
152
+ await bot.api.editMessageText(
153
+ chatId,
154
+ Number(body.message_id),
155
+ html,
156
+ { parse_mode: "HTML" },
157
+ );
158
+ } catch {
159
+ await bot.api.editMessageText(
160
+ chatId,
161
+ Number(body.message_id),
162
+ text,
163
+ );
164
+ }
120
165
  });
121
166
  return { ok: true };
122
167
  }
@@ -130,49 +175,85 @@ export function createTelegramActionHandler(
130
175
  return { ok: true };
131
176
 
132
177
  case "unpin_message":
133
- await bot.api.unpinChatMessage(chatId, body.message_id ? Number(body.message_id) : undefined);
178
+ await bot.api.unpinChatMessage(
179
+ chatId,
180
+ body.message_id ? Number(body.message_id) : undefined,
181
+ );
134
182
  return { ok: true };
135
183
 
136
184
  case "forward_message": {
137
185
  if (body.to_chat_id && Number(body.to_chat_id) !== chatId)
138
186
  return { ok: false, error: "Cross-chat forwarding not allowed." };
139
- const sent = await bot.api.forwardMessage(chatId, chatId, Number(body.message_id));
187
+ const sent = await bot.api.forwardMessage(
188
+ chatId,
189
+ chatId,
190
+ Number(body.message_id),
191
+ );
140
192
  return { ok: true, message_id: sent.message_id };
141
193
  }
142
194
 
143
195
  case "copy_message": {
144
- const sent = await bot.api.copyMessage(chatId, chatId, Number(body.message_id));
196
+ const sent = await bot.api.copyMessage(
197
+ chatId,
198
+ chatId,
199
+ Number(body.message_id),
200
+ );
145
201
  return { ok: true, message_id: sent.message_id };
146
202
  }
147
203
 
148
204
  case "send_chat_action":
149
- await bot.api.sendChatAction(chatId, String(body.chat_action ?? "typing") as "typing");
205
+ await bot.api.sendChatAction(
206
+ chatId,
207
+ String(body.chat_action ?? "typing") as "typing",
208
+ );
150
209
  return { ok: true };
151
210
 
152
211
  case "send_message_with_buttons": {
153
212
  const text = String(body.text ?? "");
154
- if (text.length > TELEGRAM_MAX_TEXT) return { ok: false, error: `Text too long` };
213
+ if (text.length > TELEGRAM_MAX_TEXT)
214
+ return { ok: false, error: `Text too long` };
155
215
  const html = markdownToTelegramHtml(text);
156
- const rows = body.rows as Array<Array<{ text: string; url?: string; callback_data?: string }>>;
216
+ const rows = body.rows as Array<
217
+ Array<{ text: string; url?: string; callback_data?: string }>
218
+ >;
157
219
  gateway.incrementMessages(chatId);
158
- const keyboard = rows.map((row) => row.map((btn) =>
159
- btn.url ? { text: btn.text, url: btn.url } : { text: btn.text, callback_data: btn.callback_data ?? btn.text },
160
- ));
220
+ const keyboard = rows.map((row) =>
221
+ row.map((btn) =>
222
+ btn.url
223
+ ? { text: btn.text, url: btn.url }
224
+ : {
225
+ text: btn.text,
226
+ callback_data: btn.callback_data ?? btn.text,
227
+ },
228
+ ),
229
+ );
161
230
  try {
162
- const sent = await bot.api.sendMessage(chatId, html, { parse_mode: "HTML", reply_markup: { inline_keyboard: keyboard } });
231
+ const sent = await bot.api.sendMessage(chatId, html, {
232
+ parse_mode: "HTML",
233
+ reply_markup: { inline_keyboard: keyboard },
234
+ });
163
235
  return { ok: true, message_id: sent.message_id };
164
236
  } catch {
165
- const sent = await bot.api.sendMessage(chatId, text, { reply_markup: { inline_keyboard: keyboard } });
237
+ const sent = await bot.api.sendMessage(chatId, text, {
238
+ reply_markup: { inline_keyboard: keyboard },
239
+ });
166
240
  return { ok: true, message_id: sent.message_id };
167
241
  }
168
242
  }
169
243
 
170
244
  case "schedule_message": {
171
245
  const text = String(body.text ?? "");
172
- const delaySec = Math.max(1, Math.min(3600, Number(body.delay_seconds ?? 60)));
246
+ const delaySec = Math.max(
247
+ 1,
248
+ Math.min(3600, Number(body.delay_seconds ?? 60)),
249
+ );
173
250
  const scheduleId = `sched_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
174
251
  const timer = setTimeout(async () => {
175
- try { await sendText(bot, chatId, text); } catch { /* scheduled send failed */ }
252
+ try {
253
+ await sendText(bot, chatId, text);
254
+ } catch {
255
+ /* scheduled send failed */
256
+ }
176
257
  scheduledMessages.delete(scheduleId);
177
258
  }, delaySec * 1000);
178
259
  scheduledMessages.set(scheduleId, timer);
@@ -181,88 +262,196 @@ export function createTelegramActionHandler(
181
262
 
182
263
  case "cancel_scheduled": {
183
264
  const timer = scheduledMessages.get(String(body.schedule_id ?? ""));
184
- if (timer) { clearTimeout(timer); scheduledMessages.delete(String(body.schedule_id)); return { ok: true, cancelled: true }; }
265
+ if (timer) {
266
+ clearTimeout(timer);
267
+ scheduledMessages.delete(String(body.schedule_id));
268
+ return { ok: true, cancelled: true };
269
+ }
185
270
  return { ok: false, error: "Schedule not found" };
186
271
  }
187
272
 
188
273
  // ── Media ──────────────────────────────────────────────────────────
189
- case "send_file": case "send_photo": case "send_video": case "send_animation": case "send_voice": case "send_audio": {
274
+ case "send_file":
275
+ case "send_photo":
276
+ case "send_video":
277
+ case "send_animation":
278
+ case "send_voice":
279
+ case "send_audio": {
190
280
  const filePath = String(body.file_path ?? "");
191
281
  const caption = body.caption ? String(body.caption) : undefined;
192
282
  gateway.incrementMessages(chatId);
193
283
  if (action === "send_file") {
194
284
  const stat = statSync(filePath);
195
- if (stat.size > 49 * 1024 * 1024) return { ok: false, error: "File too large (max 49MB)" };
285
+ if (stat.size > 49 * 1024 * 1024)
286
+ return { ok: false, error: "File too large (max 49MB)" };
196
287
  }
197
288
  const data = readFileSync(filePath);
198
289
  const file = new InputFileClass(data, basename(filePath));
199
290
  const rp = replyParams(body);
200
291
  let sent;
201
292
  switch (action) {
202
- case "send_file": sent = await withRetry(() => bot.api.sendDocument(chatId, file, { caption, reply_parameters: rp })); break;
203
- case "send_photo": sent = await withRetry(() => bot.api.sendPhoto(chatId, file, { caption, reply_parameters: rp })); break;
204
- case "send_video": sent = await withRetry(() => bot.api.sendVideo(chatId, file, { caption, reply_parameters: rp })); break;
205
- case "send_animation": sent = await withRetry(() => bot.api.sendAnimation(chatId, file, { caption, reply_parameters: rp })); break;
206
- case "send_audio": sent = await withRetry(() => bot.api.sendAudio(chatId, file, { caption, reply_parameters: rp, title: body.title as string | undefined, performer: body.performer as string | undefined })); break;
207
- default: sent = await withRetry(() => bot.api.sendVoice(chatId, file, { caption, reply_parameters: rp })); break;
293
+ case "send_file":
294
+ sent = await withRetry(() =>
295
+ bot.api.sendDocument(chatId, file, {
296
+ caption,
297
+ reply_parameters: rp,
298
+ }),
299
+ );
300
+ break;
301
+ case "send_photo":
302
+ sent = await withRetry(() =>
303
+ bot.api.sendPhoto(chatId, file, {
304
+ caption,
305
+ reply_parameters: rp,
306
+ }),
307
+ );
308
+ break;
309
+ case "send_video":
310
+ sent = await withRetry(() =>
311
+ bot.api.sendVideo(chatId, file, {
312
+ caption,
313
+ reply_parameters: rp,
314
+ }),
315
+ );
316
+ break;
317
+ case "send_animation":
318
+ sent = await withRetry(() =>
319
+ bot.api.sendAnimation(chatId, file, {
320
+ caption,
321
+ reply_parameters: rp,
322
+ }),
323
+ );
324
+ break;
325
+ case "send_audio":
326
+ sent = await withRetry(() =>
327
+ bot.api.sendAudio(chatId, file, {
328
+ caption,
329
+ reply_parameters: rp,
330
+ title: body.title as string | undefined,
331
+ performer: body.performer as string | undefined,
332
+ }),
333
+ );
334
+ break;
335
+ default:
336
+ sent = await withRetry(() =>
337
+ bot.api.sendVoice(chatId, file, {
338
+ caption,
339
+ reply_parameters: rp,
340
+ }),
341
+ );
342
+ break;
208
343
  }
209
344
  return { ok: true, message_id: sent.message_id };
210
345
  }
211
346
 
212
347
  case "send_sticker": {
213
348
  gateway.incrementMessages(chatId);
214
- const sent = await bot.api.sendSticker(chatId, String(body.file_id ?? ""), { reply_parameters: replyParams(body) });
349
+ const sent = await bot.api.sendSticker(
350
+ chatId,
351
+ String(body.file_id ?? ""),
352
+ { reply_parameters: replyParams(body) },
353
+ );
215
354
  return { ok: true, message_id: sent.message_id };
216
355
  }
217
356
 
218
357
  case "send_poll": {
219
358
  gateway.incrementMessages(chatId);
220
- const sent = await bot.api.sendPoll(chatId, String(body.question ?? ""),
221
- (body.options as string[] ?? []).map((o) => ({ text: o })),
222
- { is_anonymous: body.is_anonymous as boolean | undefined, allows_multiple_answers: body.allows_multiple_answers as boolean | undefined,
223
- type: body.type as "regular" | "quiz" | undefined, correct_option_id: body.correct_option_id as number | undefined,
224
- explanation: body.explanation as string | undefined });
359
+ const sent = await bot.api.sendPoll(
360
+ chatId,
361
+ String(body.question ?? ""),
362
+ ((body.options as string[]) ?? []).map((o) => ({ text: o })),
363
+ {
364
+ is_anonymous: body.is_anonymous as boolean | undefined,
365
+ allows_multiple_answers: body.allows_multiple_answers as
366
+ | boolean
367
+ | undefined,
368
+ type: body.type as "regular" | "quiz" | undefined,
369
+ correct_option_ids:
370
+ body.correct_option_id != null
371
+ ? [body.correct_option_id as number]
372
+ : undefined,
373
+ explanation: body.explanation as string | undefined,
374
+ },
375
+ );
225
376
  return { ok: true, message_id: sent.message_id };
226
377
  }
227
378
 
228
379
  case "send_location": {
229
380
  gateway.incrementMessages(chatId);
230
- const sent = await bot.api.sendLocation(chatId, Number(body.latitude), Number(body.longitude));
381
+ const sent = await bot.api.sendLocation(
382
+ chatId,
383
+ Number(body.latitude),
384
+ Number(body.longitude),
385
+ );
231
386
  return { ok: true, message_id: sent.message_id };
232
387
  }
233
388
 
234
389
  case "send_contact": {
235
390
  gateway.incrementMessages(chatId);
236
- const sent = await bot.api.sendContact(chatId, String(body.phone_number), String(body.first_name),
237
- { last_name: body.last_name as string | undefined });
391
+ const sent = await bot.api.sendContact(
392
+ chatId,
393
+ String(body.phone_number),
394
+ String(body.first_name),
395
+ { last_name: body.last_name as string | undefined },
396
+ );
238
397
  return { ok: true, message_id: sent.message_id };
239
398
  }
240
399
 
241
400
  case "send_dice": {
242
401
  gateway.incrementMessages(chatId);
243
- const sent = await bot.api.sendDice(chatId, (body.emoji as string) || "\uD83C\uDFB2");
244
- return { ok: true, message_id: sent.message_id, value: sent.dice?.value };
402
+ const sent = await bot.api.sendDice(
403
+ chatId,
404
+ (body.emoji as string) || "\uD83C\uDFB2",
405
+ );
406
+ return {
407
+ ok: true,
408
+ message_id: sent.message_id,
409
+ value: sent.dice?.value,
410
+ };
245
411
  }
246
412
 
247
413
  // ── Chat info ──────────────────────────────────────────────────────
248
414
  case "get_chat_info": {
249
415
  const chat = await bot.api.getChat(chatId);
250
- const count = await bot.api.getChatMemberCount(chatId).catch(() => null);
251
- return { ok: true, id: chat.id, type: chat.type, title: "title" in chat ? chat.title : undefined, member_count: count };
416
+ const count = await bot.api
417
+ .getChatMemberCount(chatId)
418
+ .catch(() => null);
419
+ return {
420
+ ok: true,
421
+ id: chat.id,
422
+ type: chat.type,
423
+ title: "title" in chat ? chat.title : undefined,
424
+ member_count: count,
425
+ };
252
426
  }
253
427
 
254
428
  case "get_chat_member": {
255
429
  const m = await bot.api.getChatMember(chatId, Number(body.user_id));
256
- return { ok: true, status: m.status, user: { id: m.user.id, first_name: m.user.first_name, username: m.user.username } };
430
+ return {
431
+ ok: true,
432
+ status: m.status,
433
+ user: {
434
+ id: m.user.id,
435
+ first_name: m.user.first_name,
436
+ username: m.user.username,
437
+ },
438
+ };
257
439
  }
258
440
 
259
441
  case "get_chat_admins": {
260
442
  const admins = await bot.api.getChatAdministrators(chatId);
261
- const text = admins.map((a) => {
262
- const name = [a.user.first_name, a.user.last_name].filter(Boolean).join(" ");
263
- const title = "custom_title" in a && a.custom_title ? ` "${a.custom_title}"` : "";
264
- return `${name}${title} (${a.status}) id:${a.user.id}`;
265
- }).join("\n");
443
+ const text = admins
444
+ .map((a) => {
445
+ const name = [a.user.first_name, a.user.last_name]
446
+ .filter(Boolean)
447
+ .join(" ");
448
+ const title =
449
+ "custom_title" in a && a.custom_title
450
+ ? ` "${a.custom_title}"`
451
+ : "";
452
+ return `${name}${title} (${a.status}) id:${a.user.id}`;
453
+ })
454
+ .join("\n");
266
455
  return { ok: true, text };
267
456
  }
268
457
 
@@ -274,79 +463,140 @@ export function createTelegramActionHandler(
274
463
  return { ok: true };
275
464
 
276
465
  case "set_chat_description":
277
- await bot.api.setChatDescription(chatId, String(body.description ?? ""));
466
+ await bot.api.setChatDescription(
467
+ chatId,
468
+ String(body.description ?? ""),
469
+ );
278
470
  return { ok: true };
279
471
 
280
472
  // ── History (userbot-enhanced) ─────────────────────────────────────
281
473
  // These OVERRIDE the shared in-memory history when userbot is available
282
474
  case "read_history":
283
475
  if (isUserClientReady()) {
284
- return { ok: true, text: await userbotHistory({ chatId, limit: Math.min(100, Number(body.limit ?? 30)), offsetId: body.offset_id as number | undefined, before: body.before as string | undefined }) };
476
+ return {
477
+ ok: true,
478
+ text: await userbotHistory({
479
+ chatId,
480
+ limit: Math.min(100, Number(body.limit ?? 30)),
481
+ offsetId: body.offset_id as number | undefined,
482
+ before: body.before as string | undefined,
483
+ }),
484
+ };
285
485
  }
286
486
  return null; // fall through to shared handler
287
487
 
288
488
  case "search_history":
289
489
  if (isUserClientReady()) {
290
- return { ok: true, text: await userbotSearch({ chatId, query: String(body.query ?? ""), limit: Math.min(100, Number(body.limit ?? 20)) }) };
490
+ return {
491
+ ok: true,
492
+ text: await userbotSearch({
493
+ chatId,
494
+ query: String(body.query ?? ""),
495
+ limit: Math.min(100, Number(body.limit ?? 20)),
496
+ }),
497
+ };
291
498
  }
292
499
  return null;
293
500
 
294
501
  case "get_user_messages":
295
502
  if (isUserClientReady()) {
296
- return { ok: true, text: await userbotSearch({ chatId, query: String(body.user_name ?? ""), limit: Math.min(50, Number(body.limit ?? 20)) }) };
503
+ return {
504
+ ok: true,
505
+ text: await userbotSearch({
506
+ chatId,
507
+ query: String(body.user_name ?? ""),
508
+ limit: Math.min(50, Number(body.limit ?? 20)),
509
+ }),
510
+ };
297
511
  }
298
512
  return null;
299
513
 
300
514
  case "list_known_users":
301
515
  if (isUserClientReady()) {
302
- return { ok: true, text: await userbotParticipantDetails({ chatId, limit: Number(body.limit ?? 50) }) };
516
+ return {
517
+ ok: true,
518
+ text: await userbotParticipantDetails({
519
+ chatId,
520
+ limit: Number(body.limit ?? 50),
521
+ }),
522
+ };
303
523
  }
304
524
  return null;
305
525
 
306
526
  case "get_member_info":
307
527
  if (isUserClientReady()) {
308
- return { ok: true, text: await userbotGetUserInfo({ chatId, userId: Number(body.user_id) }) };
528
+ return {
529
+ ok: true,
530
+ text: await userbotGetUserInfo({
531
+ chatId,
532
+ userId: Number(body.user_id),
533
+ }),
534
+ };
309
535
  }
310
536
  return { ok: false, error: "User client not connected." };
311
537
 
312
538
  case "get_message_by_id":
313
539
  if (isUserClientReady()) {
314
- return { ok: true, text: await userbotGetMessage({ chatId, messageId: Number(body.message_id) }) };
540
+ return {
541
+ ok: true,
542
+ text: await userbotGetMessage({
543
+ chatId,
544
+ messageId: Number(body.message_id),
545
+ }),
546
+ };
315
547
  }
316
548
  return { ok: false, error: "User client not connected." };
317
549
 
318
550
  case "get_pinned_messages":
319
- if (isUserClientReady()) return { ok: true, text: await userbotPinnedMessages({ chatId }) };
551
+ if (isUserClientReady())
552
+ return { ok: true, text: await userbotPinnedMessages({ chatId }) };
320
553
  return { ok: false, error: "User client not connected." };
321
554
 
322
555
  case "online_count":
323
- if (isUserClientReady()) return { ok: true, text: await userbotOnlineCount({ chatId }) };
556
+ if (isUserClientReady())
557
+ return { ok: true, text: await userbotOnlineCount({ chatId }) };
324
558
  return { ok: false, error: "User client not connected." };
325
559
 
326
560
  case "save_sticker_pack": {
327
- const text = await userbotSaveStickerPack({ setName: String(body.set_name ?? ""), bot });
561
+ const text = await userbotSaveStickerPack({
562
+ setName: String(body.set_name ?? ""),
563
+ bot,
564
+ });
328
565
  return { ok: true, text };
329
566
  }
330
567
 
331
568
  case "get_sticker_pack": {
332
- const stickerSet = await bot.api.getStickerSet(String(body.set_name ?? ""));
333
- const lines = stickerSet.stickers.map((s, i) => `${i + 1}. ${s.emoji ?? ""} [${s.is_animated ? "animated" : s.is_video ? "video" : "static"}] file_id: ${s.file_id}`);
334
- return { ok: true, text: `Sticker pack: "${stickerSet.title}" (${stickerSet.stickers.length} stickers)\nSet name: ${stickerSet.name}\n\n${lines.join("\n")}` };
569
+ const stickerSet = await bot.api.getStickerSet(
570
+ String(body.set_name ?? ""),
571
+ );
572
+ const lines = stickerSet.stickers.map(
573
+ (s, i) =>
574
+ `${i + 1}. ${s.emoji ?? ""} [${s.is_animated ? "animated" : s.is_video ? "video" : "static"}] file_id: ${s.file_id}`,
575
+ );
576
+ return {
577
+ ok: true,
578
+ text: `Sticker pack: "${stickerSet.title}" (${stickerSet.stickers.length} stickers)\nSet name: ${stickerSet.name}\n\n${lines.join("\n")}`,
579
+ };
335
580
  }
336
581
 
337
582
  case "download_sticker": {
338
583
  const file = await bot.api.getFile(String(body.file_id ?? ""));
339
- if (!file.file_path) return { ok: false, error: "Could not get file path" };
584
+ if (!file.file_path)
585
+ return { ok: false, error: "Could not get file path" };
340
586
  const url = `https://api.telegram.org/file/bot${botToken}/${file.file_path}`;
341
587
  const resp = await fetch(url);
342
- if (!resp.ok) return { ok: false, error: `Download failed: ${resp.status}` };
588
+ if (!resp.ok)
589
+ return { ok: false, error: `Download failed: ${resp.status}` };
343
590
  const buffer = Buffer.from(await resp.arrayBuffer());
344
591
  const ext = file.file_path.split(".").pop() ?? "webp";
345
592
  const uploadsDir = dirs.uploads;
346
593
  if (!existsSync(uploadsDir)) mkdirSync(uploadsDir, { recursive: true });
347
594
  const filePath = resolve(uploadsDir, `${Date.now()}-sticker.${ext}`);
348
595
  writeFileSync(filePath, buffer);
349
- return { ok: true, text: `Downloaded sticker to: ${filePath} (${buffer.length} bytes).` };
596
+ return {
597
+ ok: true,
598
+ text: `Downloaded sticker to: ${filePath} (${buffer.length} bytes).`,
599
+ };
350
600
  }
351
601
 
352
602
  // ── Sticker pack management ──────────────────────────────────────
@@ -356,13 +606,19 @@ export function createTelegramActionHandler(
356
606
  const title = String(body.title ?? "");
357
607
  const filePath = String(body.file_path ?? "");
358
608
  const emojis = (body.emoji_list as string[]) ?? ["🎨"];
359
- const format = (body.format as "static" | "animated" | "video") ?? "static";
609
+ const format =
610
+ (body.format as "static" | "animated" | "video") ?? "static";
360
611
  if (!userId || !name || !title || !filePath) {
361
- return { ok: false, error: "Required: user_id, name, title, file_path" };
612
+ return {
613
+ ok: false,
614
+ error: "Required: user_id, name, title, file_path",
615
+ };
362
616
  }
363
617
  // Sticker set names must end with _by_<bot_username>
364
618
  const botUsername = bot.botInfo?.username ?? "";
365
- const fullName = name.endsWith(`_by_${botUsername}`) ? name : `${name}_by_${botUsername}`;
619
+ const fullName = name.endsWith(`_by_${botUsername}`)
620
+ ? name
621
+ : `${name}_by_${botUsername}`;
366
622
  const data = readFileSync(filePath);
367
623
  const sticker = {
368
624
  sticker: new InputFileClass(data, basename(filePath)),
@@ -370,7 +626,10 @@ export function createTelegramActionHandler(
370
626
  emoji_list: emojis,
371
627
  };
372
628
  await bot.api.createNewStickerSet(userId, fullName, title, [sticker]);
373
- return { ok: true, text: `Created sticker pack "${title}" (${fullName}) with 1 sticker.` };
629
+ return {
630
+ ok: true,
631
+ text: `Created sticker pack "${title}" (${fullName}) with 1 sticker.`,
632
+ };
374
633
  }
375
634
 
376
635
  case "add_sticker_to_set": {
@@ -378,7 +637,8 @@ export function createTelegramActionHandler(
378
637
  const name = String(body.name ?? "");
379
638
  const filePath = String(body.file_path ?? "");
380
639
  const emojis = (body.emoji_list as string[]) ?? ["🎨"];
381
- const format = (body.format as "static" | "animated" | "video") ?? "static";
640
+ const format =
641
+ (body.format as "static" | "animated" | "video") ?? "static";
382
642
  if (!userId || !name || !filePath) {
383
643
  return { ok: false, error: "Required: user_id, name, file_path" };
384
644
  }
@@ -394,7 +654,8 @@ export function createTelegramActionHandler(
394
654
 
395
655
  case "delete_sticker_from_set": {
396
656
  const stickerId = String(body.sticker_file_id ?? "");
397
- if (!stickerId) return { ok: false, error: "Required: sticker_file_id" };
657
+ if (!stickerId)
658
+ return { ok: false, error: "Required: sticker_file_id" };
398
659
  await bot.api.deleteStickerFromSet(stickerId);
399
660
  return { ok: true, text: "Sticker deleted from pack." };
400
661
  }
@@ -402,7 +663,8 @@ export function createTelegramActionHandler(
402
663
  case "set_sticker_set_title": {
403
664
  const name = String(body.name ?? "");
404
665
  const title = String(body.title ?? "");
405
- if (!name || !title) return { ok: false, error: "Required: name, title" };
666
+ if (!name || !title)
667
+ return { ok: false, error: "Required: name, title" };
406
668
  await bot.api.setStickerSetTitle(name, title);
407
669
  return { ok: true, text: `Pack title updated to "${title}".` };
408
670
  }
@@ -418,14 +680,28 @@ export function createTelegramActionHandler(
418
680
  const msgId = Number(body.message_id);
419
681
  if (!msgId) return { ok: false, error: "Required: message_id" };
420
682
  const poll = await bot.api.stopPoll(chatId, msgId);
421
- const results = poll.options.map((o) => ` ${o.text}: ${o.voter_count} vote${o.voter_count === 1 ? "" : "s"}`).join("\n");
422
- return { ok: true, text: `Poll closed: "${poll.question}"\nTotal voters: ${poll.total_voter_count}\n\nResults:\n${results}` };
683
+ const results = poll.options
684
+ .map(
685
+ (o) =>
686
+ ` ${o.text}: ${o.voter_count} vote${o.voter_count === 1 ? "" : "s"}`,
687
+ )
688
+ .join("\n");
689
+ return {
690
+ ok: true,
691
+ text: `Poll closed: "${poll.question}"\nTotal voters: ${poll.total_voter_count}\n\nResults:\n${results}`,
692
+ };
423
693
  }
424
694
 
425
695
  case "download_media": {
426
696
  if (isUserClientReady()) {
427
697
  const { downloadMessageMedia } = await import("./userbot.js");
428
- return { ok: true, text: await downloadMessageMedia({ chatId, messageId: Number(body.message_id) }) };
698
+ return {
699
+ ok: true,
700
+ text: await downloadMessageMedia({
701
+ chatId,
702
+ messageId: Number(body.message_id),
703
+ }),
704
+ };
429
705
  }
430
706
  return { ok: false, error: "User client not connected." };
431
707
  }