talon-agent 3.21.0 → 3.22.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "talon-agent",
3
- "version": "3.21.0",
3
+ "version": "3.22.0",
4
4
  "description": "Multi-frontend AI agent with full tool access, streaming, cron jobs, and plugin system",
5
5
  "author": "Dylan Neve",
6
6
  "license": "MIT",
@@ -113,7 +113,7 @@
113
113
  "cross-spawn": "^7.0.6",
114
114
  "discord.js": "^14.16.3",
115
115
  "file-type": "^22.0.1",
116
- "grammy": "^1.42.0",
116
+ "grammy": "^1.45.1",
117
117
  "liquidjs": "^10.27.0",
118
118
  "marked": "^18.0.0",
119
119
  "mem0ai": "^3.0.13",
@@ -22,6 +22,7 @@ import { webTools } from "./web.js";
22
22
  import { adminTools } from "./admin.js";
23
23
  import { modelTools } from "./models.js";
24
24
  import { meshTools } from "./mesh.js";
25
+ import { moderationTools } from "./moderation.js";
25
26
  import { nativeTools } from "./native.js";
26
27
 
27
28
  /** All built-in tool definitions. */
@@ -41,6 +42,7 @@ export const ALL_TOOLS: readonly ToolDefinition[] = [
41
42
  ...adminTools,
42
43
  ...modelTools,
43
44
  ...meshTools,
45
+ ...moderationTools,
44
46
  ];
45
47
 
46
48
  /**
@@ -135,7 +135,10 @@ Examples:
135
135
  Dice: send(type="dice")
136
136
  Location: send(type="location", latitude=37.7749, longitude=-122.4194)
137
137
  Sticker by feeling: send(type="sticker", emoji="😂") — picks a matching sticker from your saved packs (add set_name to pin one pack)
138
- Sticker by id: send(type="sticker", file_id="CAACAgI...")`,
138
+ Sticker by id: send(type="sticker", file_id="CAACAgI...")
139
+ Album: send(type="album", media=[{"type":"photo","file_path":"a.jpg"},{"type":"photo","url":"https://…/b.jpg","caption":"the good one"}]) — 2-10 photos/videos as one grouped message
140
+ Round video: send(type="video_note", file_path="/path/clip.mp4") — circular video bubble (square video, ≤60s)
141
+ Venue: send(type="venue", latitude=53.34, longitude=-6.26, title="The Long Hall", address="51 South Great George's St")`,
139
142
  schema: {
140
143
  type: z
141
144
  .enum([
@@ -151,6 +154,9 @@ Examples:
151
154
  "location",
152
155
  "contact",
153
156
  "dice",
157
+ "album",
158
+ "video_note",
159
+ "venue",
154
160
  ])
155
161
  .describe("Content type to send"),
156
162
  text: z
@@ -208,7 +214,30 @@ Examples:
208
214
  phone_number: z.string().optional().describe("Contact phone"),
209
215
  first_name: z.string().optional().describe("Contact first name"),
210
216
  last_name: z.string().optional().describe("Contact last name"),
211
- title: z.string().optional().describe("Audio title (for type=audio)"),
217
+ title: z
218
+ .string()
219
+ .optional()
220
+ .describe("Audio title (type=audio) or venue name (type=venue)"),
221
+ address: z
222
+ .string()
223
+ .optional()
224
+ .describe("Venue street address (for type=venue)"),
225
+ media: z
226
+ .array(
227
+ z.object({
228
+ type: z
229
+ .enum(["photo", "video", "document", "audio"])
230
+ .describe("Kind of this album item"),
231
+ file_path: z.string().optional(),
232
+ url: z.string().optional(),
233
+ file_id: z.string().optional(),
234
+ caption: z.string().optional(),
235
+ }),
236
+ )
237
+ .optional()
238
+ .describe(
239
+ "Album items (for type=album): 2-10 entries, each sourced from file_path, url, or file_id. Photos and videos mix; documents/audio group only with their own kind.",
240
+ ),
212
241
  performer: z
213
242
  .string()
214
243
  .optional()
@@ -230,9 +259,41 @@ Examples:
230
259
  .describe(
231
260
  "Target chat ID. Omit to send to the current chat (chat mode). Required from heartbeat mode where there is no ambient chat — use list_chats or known IDs from memory. Telegram supergroup/channel IDs are negative (e.g. -1001426819337); user DMs are positive.",
232
261
  ),
262
+ silent: z
263
+ .boolean()
264
+ .optional()
265
+ .describe("Send without a notification sound (Telegram)"),
266
+ protect: z
267
+ .boolean()
268
+ .optional()
269
+ .describe("Protect content from forwarding and saving (Telegram)"),
270
+ spoiler: z
271
+ .boolean()
272
+ .optional()
273
+ .describe(
274
+ "Blur photo/video/animation behind a spoiler tap-to-reveal (Telegram)",
275
+ ),
276
+ no_link_preview: z
277
+ .boolean()
278
+ .optional()
279
+ .describe("Disable the link preview for type=text (Telegram)"),
280
+ thread_id: z
281
+ .union([z.number(), z.literal("general")])
282
+ .optional()
283
+ .describe(
284
+ 'Forum topic to post into (Telegram supergroups with topics). Defaults to the topic the conversation is happening in; pass "general" to force the General topic.',
285
+ ),
233
286
  },
234
287
  execute: async (params, bridge) => {
235
288
  const { type } = params;
289
+ // Delivery modifiers every Telegram send action understands. Harmless
290
+ // on frontends that don't (handlers read only the fields they know).
291
+ const mods = {
292
+ silent: params.silent,
293
+ protect: params.protect,
294
+ spoiler: params.spoiler,
295
+ thread_id: params.thread_id,
296
+ };
236
297
  // Thread chat_id through to every bridge call so heartbeat / dream
237
298
  // outbound (no ambient chat) gets routed by the explicit chat_id.
238
299
  // `createBridge` at src/core/tools/bridge.ts:29 reads
@@ -253,6 +314,7 @@ Examples:
253
314
  delay_seconds: params.delay_seconds,
254
315
  rows: params.buttons,
255
316
  reply_to_message_id: params.reply_to,
317
+ ...mods,
256
318
  chat_id,
257
319
  });
258
320
  }
@@ -261,12 +323,15 @@ Examples:
261
323
  text: params.text,
262
324
  rows: params.buttons,
263
325
  reply_to_message_id: params.reply_to,
326
+ ...mods,
264
327
  chat_id,
265
328
  });
266
329
  }
267
330
  return bridge("send_message", {
268
331
  text: params.text,
269
332
  reply_to_message_id: params.reply_to,
333
+ no_link_preview: params.no_link_preview,
334
+ ...mods,
270
335
  chat_id,
271
336
  });
272
337
  }
@@ -277,6 +342,7 @@ Examples:
277
342
  file_id: params.file_id,
278
343
  caption: params.caption,
279
344
  reply_to: params.reply_to,
345
+ ...mods,
280
346
  chat_id,
281
347
  });
282
348
  case "file":
@@ -286,6 +352,7 @@ Examples:
286
352
  file_id: params.file_id,
287
353
  caption: params.caption,
288
354
  reply_to: params.reply_to,
355
+ ...mods,
289
356
  chat_id,
290
357
  });
291
358
  case "video":
@@ -295,6 +362,7 @@ Examples:
295
362
  file_id: params.file_id,
296
363
  caption: params.caption,
297
364
  reply_to: params.reply_to,
365
+ ...mods,
298
366
  chat_id,
299
367
  });
300
368
  case "voice":
@@ -304,6 +372,7 @@ Examples:
304
372
  file_id: params.file_id,
305
373
  caption: params.caption,
306
374
  reply_to: params.reply_to,
375
+ ...mods,
307
376
  chat_id,
308
377
  });
309
378
  case "audio":
@@ -315,6 +384,7 @@ Examples:
315
384
  title: params.title,
316
385
  performer: params.performer,
317
386
  reply_to: params.reply_to,
387
+ ...mods,
318
388
  chat_id,
319
389
  });
320
390
  case "animation":
@@ -324,6 +394,7 @@ Examples:
324
394
  file_id: params.file_id,
325
395
  caption: params.caption,
326
396
  reply_to: params.reply_to,
397
+ ...mods,
327
398
  chat_id,
328
399
  });
329
400
  case "sticker":
@@ -333,6 +404,7 @@ Examples:
333
404
  emoji: params.emoji,
334
405
  set_name: params.set_name,
335
406
  reply_to: params.reply_to,
407
+ ...mods,
336
408
  chat_id,
337
409
  });
338
410
  case "poll":
@@ -344,6 +416,7 @@ Examples:
344
416
  explanation: params.explanation,
345
417
  type: params.correct_option_id !== undefined ? "quiz" : "regular",
346
418
  reply_to: params.reply_to,
419
+ ...mods,
347
420
  chat_id,
348
421
  });
349
422
  case "location":
@@ -351,6 +424,7 @@ Examples:
351
424
  latitude: params.latitude,
352
425
  longitude: params.longitude,
353
426
  reply_to: params.reply_to,
427
+ ...mods,
354
428
  chat_id,
355
429
  });
356
430
  case "contact":
@@ -359,12 +433,40 @@ Examples:
359
433
  first_name: params.first_name,
360
434
  last_name: params.last_name,
361
435
  reply_to: params.reply_to,
436
+ ...mods,
362
437
  chat_id,
363
438
  });
364
439
  case "dice":
365
440
  return bridge("send_dice", {
366
441
  emoji: params.emoji,
367
442
  reply_to: params.reply_to,
443
+ ...mods,
444
+ chat_id,
445
+ });
446
+ case "album":
447
+ return bridge("send_media_group", {
448
+ media: params.media,
449
+ reply_to: params.reply_to,
450
+ ...mods,
451
+ chat_id,
452
+ });
453
+ case "video_note":
454
+ return bridge("send_video_note", {
455
+ file_path: params.file_path,
456
+ url: params.url,
457
+ file_id: params.file_id,
458
+ reply_to: params.reply_to,
459
+ ...mods,
460
+ chat_id,
461
+ });
462
+ case "venue":
463
+ return bridge("send_venue", {
464
+ latitude: params.latitude,
465
+ longitude: params.longitude,
466
+ title: params.title,
467
+ address: params.address,
468
+ reply_to: params.reply_to,
469
+ ...mods,
368
470
  chat_id,
369
471
  });
370
472
  default:
@@ -479,8 +581,16 @@ Valid emoji: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉
479
581
  // ── edit_message ──────────────────────────────────────────────────────
480
582
  {
481
583
  name: "edit_message",
482
- description: "Edit a previously sent message.",
483
- schema: { message_id: snowflakeOrIdSchema, text: z.string() },
584
+ description:
585
+ "Edit a previously sent message. For a media message (photo/video/file), pass is_caption=true to edit its caption instead of message text.",
586
+ schema: {
587
+ message_id: snowflakeOrIdSchema,
588
+ text: z.string(),
589
+ is_caption: z
590
+ .boolean()
591
+ .optional()
592
+ .describe("Edit the media caption rather than message text (Telegram)"),
593
+ },
484
594
  execute: (params, bridge) => bridge("edit_message", params),
485
595
  frontends: ["telegram", "discord", "native"],
486
596
  tag: "messaging",
@@ -489,8 +599,15 @@ Valid emoji: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉
489
599
  // ── delete_message ────────────────────────────────────────────────────
490
600
  {
491
601
  name: "delete_message",
492
- description: "Delete a message.",
493
- schema: { message_id: snowflakeOrIdSchema },
602
+ description:
603
+ "Delete a message — or several at once via message_ids (Telegram; ids the bot can't delete are skipped).",
604
+ schema: {
605
+ message_id: snowflakeOrIdSchema.optional(),
606
+ message_ids: z
607
+ .array(snowflakeOrIdSchema)
608
+ .optional()
609
+ .describe("Bulk delete these message IDs (Telegram)"),
610
+ },
494
611
  execute: (params, bridge) => bridge("delete_message", params),
495
612
  frontends: ["telegram", "discord", "native"],
496
613
  tag: "messaging",
@@ -499,13 +616,42 @@ Valid emoji: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉
499
616
  // ── forward_message ───────────────────────────────────────────────────
500
617
  {
501
618
  name: "forward_message",
502
- description: "Forward a message within the chat.",
503
- schema: { message_id: snowflakeOrIdSchema },
619
+ description:
620
+ "Forward a message. Defaults to within the current chat; from_chat_id / to_chat_id forward across chats the bot is in, and message_ids forwards a batch (albums stay grouped) (Telegram).",
621
+ schema: {
622
+ message_id: snowflakeOrIdSchema.optional(),
623
+ message_ids: z
624
+ .array(snowflakeOrIdSchema)
625
+ .optional()
626
+ .describe("Forward these messages as a batch (Telegram)"),
627
+ from_chat_id: chatIdSchema
628
+ .optional()
629
+ .describe("Source chat (default: current chat)"),
630
+ to_chat_id: chatIdSchema
631
+ .optional()
632
+ .describe("Destination chat (default: current chat)"),
633
+ },
504
634
  execute: (params, bridge) => bridge("forward_message", params),
505
635
  frontends: ["telegram", "discord"],
506
636
  tag: "messaging",
507
637
  },
508
638
 
639
+ // ── copy_message ──────────────────────────────────────────────────────
640
+ {
641
+ name: "copy_message",
642
+ description:
643
+ "Repost a message without the 'forwarded from' header. Same cross-chat and batch semantics as forward_message (Telegram).",
644
+ schema: {
645
+ message_id: snowflakeOrIdSchema.optional(),
646
+ message_ids: z.array(snowflakeOrIdSchema).optional(),
647
+ from_chat_id: chatIdSchema.optional(),
648
+ to_chat_id: chatIdSchema.optional(),
649
+ },
650
+ execute: (params, bridge) => bridge("copy_message", params),
651
+ frontends: ["telegram"],
652
+ tag: "messaging",
653
+ },
654
+
509
655
  // ── pin_message ───────────────────────────────────────────────────────
510
656
  {
511
657
  name: "pin_message",
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Moderation tools — chat administration the platform grants admins: member
3
+ * bans/mutes/promotions, chat permissions and identity, invite links, join
4
+ * requests, and forum-topic management.
5
+ */
6
+
7
+ import { z } from "zod";
8
+ import type { ToolDefinition } from "./types.js";
9
+ import { chatIdSchema, snowflakeOrIdSchema } from "./schemas.js";
10
+
11
+ export const moderationTools: ToolDefinition[] = [
12
+ {
13
+ name: "moderate",
14
+ description: `Administer the chat (requires the bot to be an admin with matching rights; Telegram reports missing rights in the error).
15
+
16
+ Member ops:
17
+ moderate(op="ban", user_id=123) — ban; add minutes for a temp ban, delete_messages=true to also purge their messages
18
+ moderate(op="unban", user_id=123)
19
+ moderate(op="mute", user_id=123, minutes=60) — omit minutes to mute indefinitely
20
+ moderate(op="unmute", user_id=123)
21
+ moderate(op="promote", user_id=123) — grant standard admin rights; demote reverses
22
+ moderate(op="set_admin_title", user_id=123, title="Ops") — custom title for an admin the bot promoted
23
+
24
+ Chat ops:
25
+ moderate(op="set_permissions", permissions={"send_messages":true,"send_media":false}) — default member permissions
26
+ moderate(op="create_invite_link", expire_minutes=60, member_limit=5, title="beta testers")
27
+ moderate(op="revoke_invite_link", link="https://t.me/+...")
28
+ moderate(op="list_join_requests") — pending join requests seen since startup
29
+ moderate(op="approve_join_request", user_id=123) / decline_join_request
30
+ moderate(op="set_chat_photo", file_path="/path/logo.png") / delete_chat_photo
31
+ moderate(op="unpin_all")
32
+ moderate(op="leave_chat") — the bot leaves the chat
33
+
34
+ Forum topics (supergroups with topics):
35
+ moderate(op="create_topic", title="support")
36
+ moderate(op="edit_topic", thread_id=77, title="support-eu")
37
+ moderate(op="close_topic", thread_id=77) / reopen_topic / delete_topic`,
38
+ schema: {
39
+ op: z
40
+ .enum([
41
+ "ban",
42
+ "unban",
43
+ "mute",
44
+ "unmute",
45
+ "promote",
46
+ "demote",
47
+ "set_admin_title",
48
+ "set_permissions",
49
+ "create_invite_link",
50
+ "revoke_invite_link",
51
+ "list_join_requests",
52
+ "approve_join_request",
53
+ "decline_join_request",
54
+ "set_chat_photo",
55
+ "delete_chat_photo",
56
+ "unpin_all",
57
+ "leave_chat",
58
+ "create_topic",
59
+ "edit_topic",
60
+ "close_topic",
61
+ "reopen_topic",
62
+ "delete_topic",
63
+ ])
64
+ .describe("The moderation operation"),
65
+ user_id: snowflakeOrIdSchema
66
+ .optional()
67
+ .describe("Target user (member and join-request ops)"),
68
+ minutes: z
69
+ .number()
70
+ .optional()
71
+ .describe(
72
+ "Duration for ban/mute in minutes; omit for indefinite. Telegram treats <1min or >366d as forever.",
73
+ ),
74
+ delete_messages: z
75
+ .boolean()
76
+ .optional()
77
+ .describe("With op=ban: also delete the user's messages"),
78
+ title: z
79
+ .string()
80
+ .optional()
81
+ .describe(
82
+ "Admin custom title (set_admin_title), invite-link name (create_invite_link), or topic name (create_topic/edit_topic)",
83
+ ),
84
+ permissions: z
85
+ .record(z.string(), z.boolean())
86
+ .optional()
87
+ .describe(
88
+ 'Permission map for set_permissions, e.g. {"send_messages":true,"send_media":false,"invite_users":true}',
89
+ ),
90
+ expire_minutes: z
91
+ .number()
92
+ .optional()
93
+ .describe("Invite link lifetime in minutes (create_invite_link)"),
94
+ member_limit: z
95
+ .number()
96
+ .optional()
97
+ .describe("Max joins via the invite link (create_invite_link)"),
98
+ link: z
99
+ .string()
100
+ .optional()
101
+ .describe("Invite link to revoke (revoke_invite_link)"),
102
+ file_path: z
103
+ .string()
104
+ .optional()
105
+ .describe("Image path for set_chat_photo"),
106
+ thread_id: z
107
+ .number()
108
+ .optional()
109
+ .describe("Forum topic id (edit/close/reopen/delete_topic)"),
110
+ chat_id: chatIdSchema
111
+ .optional()
112
+ .describe("Target chat ID. Omit for the current chat."),
113
+ },
114
+ execute: (params, bridge) => bridge("moderate", params),
115
+ frontends: ["telegram"],
116
+ tag: "moderation",
117
+ },
118
+
119
+ {
120
+ name: "get_user_profile_photos",
121
+ description:
122
+ "Get a user's profile photos: total count plus file_ids (sendable/downloadable like any media).",
123
+ schema: {
124
+ user_id: snowflakeOrIdSchema,
125
+ limit: z.number().optional().describe("Max photos to return (default 5)"),
126
+ },
127
+ execute: (params, bridge) => bridge("get_user_profile_photos", params),
128
+ frontends: ["telegram"],
129
+ tag: "moderation",
130
+ },
131
+ ];
@@ -28,6 +28,7 @@ export type ToolTag =
28
28
  | "admin"
29
29
  | "models"
30
30
  | "mesh"
31
+ | "moderation"
31
32
  | "native";
32
33
 
33
34
  /** The bridge caller signature — injected into execute(). */
@@ -22,6 +22,7 @@ import type { ActionResult } from "../../../core/types.js";
22
22
  import { messagingHandlers, restoreScheduledMessages } from "./messaging.js";
23
23
  import { mediaHandlers } from "./media.js";
24
24
  import { chatInfoHandlers } from "./chat-info.js";
25
+ import { moderationHandlers } from "./moderation.js";
25
26
  import type { TelegramActionContext, TelegramActionHandlers } from "./types.js";
26
27
 
27
28
  export { sendText } from "./shared.js";
@@ -32,6 +33,7 @@ const handlers: TelegramActionHandlers = Object.assign(Object.create(null), {
32
33
  ...messagingHandlers,
33
34
  ...mediaHandlers,
34
35
  ...chatInfoHandlers,
36
+ ...moderationHandlers,
35
37
  });
36
38
 
37
39
  /**