agent-messenger 2.15.0 → 2.16.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 (85) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +1 -1
  3. package/dist/package.json +1 -1
  4. package/dist/src/platforms/kakaotalk/attachment-router.d.ts +25 -0
  5. package/dist/src/platforms/kakaotalk/attachment-router.d.ts.map +1 -0
  6. package/dist/src/platforms/kakaotalk/attachment-router.js +29 -0
  7. package/dist/src/platforms/kakaotalk/attachment-router.js.map +1 -0
  8. package/dist/src/platforms/kakaotalk/client.d.ts +14 -1
  9. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  10. package/dist/src/platforms/kakaotalk/client.js +216 -0
  11. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  12. package/dist/src/platforms/kakaotalk/commands/message.d.ts.map +1 -1
  13. package/dist/src/platforms/kakaotalk/commands/message.js +49 -0
  14. package/dist/src/platforms/kakaotalk/commands/message.js.map +1 -1
  15. package/dist/src/platforms/kakaotalk/image-meta.d.ts +7 -0
  16. package/dist/src/platforms/kakaotalk/image-meta.d.ts.map +1 -0
  17. package/dist/src/platforms/kakaotalk/image-meta.js +153 -0
  18. package/dist/src/platforms/kakaotalk/image-meta.js.map +1 -0
  19. package/dist/src/platforms/kakaotalk/index.d.ts +6 -2
  20. package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
  21. package/dist/src/platforms/kakaotalk/index.js +4 -1
  22. package/dist/src/platforms/kakaotalk/index.js.map +1 -1
  23. package/dist/src/platforms/kakaotalk/media-upload.d.ts +3 -0
  24. package/dist/src/platforms/kakaotalk/media-upload.d.ts.map +1 -0
  25. package/dist/src/platforms/kakaotalk/media-upload.js +44 -0
  26. package/dist/src/platforms/kakaotalk/media-upload.js.map +1 -0
  27. package/dist/src/platforms/kakaotalk/protocol/connection.d.ts +1 -0
  28. package/dist/src/platforms/kakaotalk/protocol/connection.d.ts.map +1 -1
  29. package/dist/src/platforms/kakaotalk/protocol/connection.js +11 -0
  30. package/dist/src/platforms/kakaotalk/protocol/connection.js.map +1 -1
  31. package/dist/src/platforms/kakaotalk/protocol/media-uploader.d.ts +25 -0
  32. package/dist/src/platforms/kakaotalk/protocol/media-uploader.d.ts.map +1 -0
  33. package/dist/src/platforms/kakaotalk/protocol/media-uploader.js +99 -0
  34. package/dist/src/platforms/kakaotalk/protocol/media-uploader.js.map +1 -0
  35. package/dist/src/platforms/kakaotalk/protocol/session.d.ts +6 -0
  36. package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
  37. package/dist/src/platforms/kakaotalk/protocol/session.js +61 -0
  38. package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
  39. package/dist/src/platforms/kakaotalk/types.d.ts +44 -0
  40. package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
  41. package/dist/src/platforms/kakaotalk/types.js +9 -0
  42. package/dist/src/platforms/kakaotalk/types.js.map +1 -1
  43. package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
  44. package/dist/src/platforms/slack/commands/auth.js +6 -2
  45. package/dist/src/platforms/slack/commands/auth.js.map +1 -1
  46. package/dist/src/platforms/slack/ensure-auth.d.ts +8 -2
  47. package/dist/src/platforms/slack/ensure-auth.d.ts.map +1 -1
  48. package/dist/src/platforms/slack/ensure-auth.js +67 -10
  49. package/dist/src/platforms/slack/ensure-auth.js.map +1 -1
  50. package/docs/content/docs/cli/kakaotalk.mdx +47 -2
  51. package/docs/content/docs/sdk/kakaotalk.mdx +32 -0
  52. package/package.json +1 -1
  53. package/skills/agent-channeltalk/SKILL.md +1 -1
  54. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  55. package/skills/agent-discord/SKILL.md +1 -1
  56. package/skills/agent-discordbot/SKILL.md +1 -1
  57. package/skills/agent-instagram/SKILL.md +1 -1
  58. package/skills/agent-kakaotalk/SKILL.md +62 -4
  59. package/skills/agent-kakaotalk/references/common-patterns.md +50 -11
  60. package/skills/agent-line/SKILL.md +1 -1
  61. package/skills/agent-slack/SKILL.md +1 -1
  62. package/skills/agent-slackbot/SKILL.md +1 -1
  63. package/skills/agent-teams/SKILL.md +1 -1
  64. package/skills/agent-telegram/SKILL.md +1 -1
  65. package/skills/agent-telegrambot/SKILL.md +1 -1
  66. package/skills/agent-webex/SKILL.md +1 -1
  67. package/skills/agent-wechatbot/SKILL.md +1 -1
  68. package/skills/agent-whatsapp/SKILL.md +1 -1
  69. package/skills/agent-whatsappbot/SKILL.md +1 -1
  70. package/src/platforms/kakaotalk/attachment-router.test.ts +102 -0
  71. package/src/platforms/kakaotalk/attachment-router.ts +50 -0
  72. package/src/platforms/kakaotalk/client.ts +315 -8
  73. package/src/platforms/kakaotalk/commands/message.ts +66 -0
  74. package/src/platforms/kakaotalk/image-meta.test.ts +90 -0
  75. package/src/platforms/kakaotalk/image-meta.ts +176 -0
  76. package/src/platforms/kakaotalk/index.ts +7 -0
  77. package/src/platforms/kakaotalk/media-upload.ts +44 -0
  78. package/src/platforms/kakaotalk/protocol/connection.ts +11 -0
  79. package/src/platforms/kakaotalk/protocol/media-uploader.ts +129 -0
  80. package/src/platforms/kakaotalk/protocol/session.ts +67 -0
  81. package/src/platforms/kakaotalk/types.ts +57 -0
  82. package/src/platforms/slack/commands/auth.ts +6 -4
  83. package/src/platforms/slack/ensure-auth.test.ts +223 -1
  84. package/src/platforms/slack/ensure-auth.ts +80 -11
  85. package/src/platforms/telegrambot/cli.ts +0 -0
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-kakaotalk
3
3
  description: Interact with KakaoTalk - send messages, read chats, manage conversations
4
- version: 2.15.0
4
+ version: 2.16.0
5
5
  allowed-tools: Bash(agent-kakaotalk:*)
6
6
  metadata:
7
7
  openclaw:
@@ -335,6 +335,20 @@ agent-kakaotalk message list <chat-id> --pretty
335
335
  agent-kakaotalk message send <chat-id> "Hello world"
336
336
  agent-kakaotalk message send <chat-id> "Hello world" --pretty
337
337
 
338
+ # Send a file (auto-routes by MIME: photo / video / audio / generic file)
339
+ agent-kakaotalk message upload <chat-id> ./photo.jpg
340
+ agent-kakaotalk message upload <chat-id> ./clip.mp4
341
+ agent-kakaotalk message upload <chat-id> ./voice.m4a
342
+ agent-kakaotalk message upload <chat-id> ./report.pdf
343
+
344
+ # Send a multi-photo gallery (2+ images in one message)
345
+ agent-kakaotalk message upload <chat-id> ./img1.jpg ./img2.jpg ./img3.jpg
346
+
347
+ # Force a specific kind (override auto-routing)
348
+ agent-kakaotalk message upload <chat-id> ./clip.mp4 --as file # send as generic file, not video
349
+ agent-kakaotalk message upload <chat-id> ./image --as photo # extension-less file
350
+ agent-kakaotalk message upload <chat-id> ./data.bin --mime application/octet-stream
351
+
338
352
  # Mark messages as read up to a given log ID
339
353
  agent-kakaotalk message mark-read <chat-id> <log-id>
340
354
  agent-kakaotalk message mark-read <chat-id> <log-id> --pretty
@@ -343,9 +357,49 @@ agent-kakaotalk message mark-read <chat-id> <log-id> --link-id <li> # open cha
343
357
  # Use a specific account
344
358
  agent-kakaotalk message list <chat-id> --account <account-id>
345
359
  agent-kakaotalk message send <chat-id> "Hello" --account <account-id>
360
+ agent-kakaotalk message upload <chat-id> ./photo.jpg --account <account-id>
346
361
  agent-kakaotalk message mark-read <chat-id> <log-id> --account <account-id>
347
362
  ```
348
363
 
364
+ #### Sending Attachments
365
+
366
+ `message upload` sends photos, videos, audio, and arbitrary files to a chat. The CLI sniffs the MIME type from the filename and dispatches to the matching KakaoTalk message type, so the common case is a single command and a file path:
367
+
368
+ ```bash
369
+ agent-kakaotalk message upload <chat-id> ./report.pdf
370
+ ```
371
+
372
+ Routing rules:
373
+
374
+ | Filename / MIME | Sent as | KakaoTalk renders it as |
375
+ | --- | --- | --- |
376
+ | `image/*` (`.jpg`, `.png`, `.gif`, `.webp`) | photo | Inline image with tap-to-zoom |
377
+ | `video/*` (`.mp4`, `.mov`, `.webm`) | video | Inline player with play button |
378
+ | `audio/*` (`.m4a`, `.mp3`, `.wav`, `.ogg`) | audio | Voice-message bubble with waveform |
379
+ | anything else | file | Generic file attachment with download icon |
380
+
381
+ Multi-photo galleries (one message that contains several images): pass 2+ files and the CLI uses the gallery flow automatically.
382
+
383
+ ```bash
384
+ agent-kakaotalk message upload <chat-id> ./img1.jpg ./img2.jpg ./img3.jpg
385
+ ```
386
+
387
+ Override the auto-routing with `--as <photo|video|audio|file|multi>` when you need explicit control — for example, to send an `.mp4` as a generic downloadable file instead of an inline video. Use `--mime <type>` to override MIME detection (handy for extension-less files or when the caller knows better than the filename).
388
+
389
+ Output (JSON by default; `--pretty` pretty-prints the same JSON):
390
+
391
+ ```json
392
+ { "success": true, "status_code": 0, "chat_id": "9876543210", "log_id": "3846830417126748160", "sent_at": 1779509936 }
393
+ ```
394
+
395
+ The process exits non-zero when `success` is `false`.
396
+
397
+ Notes:
398
+ - All attachment kinds use the same SHIP / POST / COMPLETE LOCO pipeline (and MSHIP / MPOST / FORWARD for multi-photo). The server registers the chatlog itself once the upload finishes; no follow-up text message is needed.
399
+ - KakaoTalk caps single-message attachment sizes server-side (currently ~300MB for files, ~20MB per image in multi-photo). The CLI surfaces the server's status code on rejection.
400
+ - Filenames are preserved on the recipient side for `file` kind, used as a display label for `audio`, and ignored for `photo` / `video` (the client renders the bytes directly).
401
+ - Each upload opens one fresh TCP+LOCO connection per attachment. Multi-photo opens N connections in parallel.
402
+
349
403
  #### Mark as Read
350
404
 
351
405
  `message mark-read` sends a LOCO `NOTIREAD` packet to advance the server-side read watermark for a chat. The watermark is a `log_id` — typically the latest message's `log_id` from `message list` — not a timestamp.
@@ -368,7 +422,7 @@ Notes:
368
422
 
369
423
  Each message includes:
370
424
  - `log_id` — unique message identifier
371
- - `type` — message type (1 = text, 2 = photo, 12 = sticker, 20 = animated sticker, etc.)
425
+ - `type` — message type (1 = text, 2 = photo, 3 = video, 5 = audio, 12 = sticker, 18 = file, 20 = animated sticker, 27 = multi-photo, etc.)
372
426
  - `author_id` — sender's user ID
373
427
  - `author_name` — sender's nickname when known from the chat list (otherwise `null`; only the room's "display members" are cached)
374
428
  - `message` — message text content (empty string for non-text messages like stickers)
@@ -526,6 +580,11 @@ try {
526
580
  const chatId = chats[0].chat_id
527
581
  const result = await client.sendMessage(chatId, 'Hello from SDK!')
528
582
 
583
+ // Send a file (photo / video / audio / file auto-routed by MIME). Pass
584
+ // an array to send several at once — all-image arrays become a gallery.
585
+ const photo = await Bun.file('./photo.jpg').bytes()
586
+ await client.sendAttachment(chatId, photo, 'photo.jpg')
587
+
529
588
  // Read messages
530
589
  const messages = await client.getMessages(chatId, { count: 50 })
531
590
  } finally {
@@ -541,14 +600,13 @@ See the [KakaoTalk SDK documentation](https://agent-messenger.dev/docs/sdk/kakao
541
600
  ## Limitations
542
601
 
543
602
  - Auto-extraction of email/password from the desktop app is **macOS and Windows only** (KakaoTalk desktop is not available on Linux). Linux users must pass `--email` and `--password` (or `--password-file`) explicitly — the LOCO protocol, login flow, and all messaging features work on Linux.
544
- - No file upload or download
545
603
  - No channel/chat room creation or management
546
604
  - No friend list management
547
605
  - No reactions or emoji
548
606
  - No message editing or deletion
549
607
  - No open chat (오픈채팅) browsing or joining
550
608
  - No search across chats
551
- - Read-only for rich content: photos, stickers, files, and other non-text messages are exposed via the `attachment` field on each message, but sending them is not supported
609
+ - Stickers / emoticons cannot be sent (inbound stickers expose pack/path metadata, but the sticker store requires desktop-app purchase flows the SDK does not replicate). Photos, videos, audio, and arbitrary files can both be received and sent — see [`message upload`](#message-commands) and [`KakaoTalkClient.sendAttachment`](#sdk-programmatic-usage).
552
610
  - Chat IDs are numeric and not human-readable — use `chat list` to discover them
553
611
 
554
612
  ## Troubleshooting
@@ -55,9 +55,48 @@ agent-kakaotalk message send "$TARGET_CHAT" "Hey Alice!"
55
55
 
56
56
  **When to use**: First time interacting with a chat, or when the user references a chat by name.
57
57
 
58
- > Note: `display_name` joins the chat's member nicknames. For the user-set room title (matching the KakaoTalk app), see [Pattern 9](#pattern-9-resolve-canonical-room-titles).
58
+ > Note: `display_name` joins the chat's member nicknames. For the user-set room title (matching the KakaoTalk app), see [Pattern 10](#pattern-10-resolve-canonical-room-titles).
59
59
 
60
- ## Pattern 3: Monitor Chat for New Messages
60
+ ## Pattern 3: Send Files, Photos, Videos, and Audio
61
+
62
+ **Use case**: Upload an attachment to a chat (photo, video, voice, generic file, or multi-photo gallery)
63
+
64
+ ```bash
65
+ #!/bin/bash
66
+
67
+ CHAT_ID="9876543210"
68
+
69
+ # Single file — MIME is sniffed from the filename and routed to the right kind
70
+ agent-kakaotalk message upload "$CHAT_ID" ./photo.jpg # → photo (inline preview)
71
+ agent-kakaotalk message upload "$CHAT_ID" ./clip.mp4 # → video (inline player)
72
+ agent-kakaotalk message upload "$CHAT_ID" ./voice.m4a # → audio (voice bubble)
73
+ agent-kakaotalk message upload "$CHAT_ID" ./report.pdf # → file (download icon)
74
+
75
+ # Multi-photo gallery — pass 2+ files and the CLI uses the gallery flow
76
+ agent-kakaotalk message upload "$CHAT_ID" ./img1.jpg ./img2.jpg ./img3.jpg
77
+
78
+ # Force a specific kind to override auto-routing
79
+ agent-kakaotalk message upload "$CHAT_ID" ./clip.mp4 --as file # send as generic file, not inline video
80
+
81
+ # Override MIME detection for extension-less files
82
+ agent-kakaotalk message upload "$CHAT_ID" ./data.bin --mime application/octet-stream
83
+
84
+ # With error handling
85
+ RESULT=$(agent-kakaotalk message upload "$CHAT_ID" ./photo.jpg)
86
+ SUCCESS=$(echo "$RESULT" | jq -r '.success')
87
+ if [ "$SUCCESS" = "true" ]; then
88
+ echo "Uploaded as log_id $(echo "$RESULT" | jq -r '.log_id')"
89
+ else
90
+ echo "Upload failed: $(echo "$RESULT" | jq -r '.status_code')"
91
+ exit 1
92
+ fi
93
+ ```
94
+
95
+ **When to use**: Sending screenshots, generated reports, deployment artifacts, voice notes, or photo bundles to a chat.
96
+
97
+ **Routing rules**: `image/*` → photo, `video/*` → video, `audio/*` → audio (voice memo), everything else → generic file. The full SHIP / POST / COMPLETE LOCO pipeline is handled internally — no separate "send text after upload" step is needed. KakaoTalk caps single-message attachment sizes server-side; the CLI surfaces the server's status code on rejection.
98
+
99
+ ## Pattern 4: Monitor Chat for New Messages
61
100
 
62
101
  **Use case**: Watch a chat room and respond to new messages
63
102
 
@@ -91,7 +130,7 @@ done
91
130
 
92
131
  **Limitations**: Polling-based, not real-time. Each poll establishes a LOCO connection, so use reasonable intervals (10s+).
93
132
 
94
- ## Pattern 4: Read Recent Chat History
133
+ ## Pattern 5: Read Recent Chat History
95
134
 
96
135
  **Use case**: Catch up on what happened in a chat
97
136
 
@@ -113,7 +152,7 @@ echo "$MESSAGES" | jq -r '.[] | "\(.author_id): \(.message // "[non-text]")"'
113
152
 
114
153
  **When to use**: Context gathering, summarizing conversations, catching up on missed messages.
115
154
 
116
- ## Pattern 5: Fetch More Messages
155
+ ## Pattern 6: Fetch More Messages
117
156
 
118
157
  **Use case**: Read more messages than the default 20
119
158
 
@@ -148,7 +187,7 @@ NEW_MESSAGES=$(agent-kakaotalk message list "$CHAT_ID" --from "$LAST_SEEN")
148
187
 
149
188
  **Pagination details**: The CLI now prefers KakaoTalk's `MCHATLOGS` flow for history reads, fetching message batches from the requested `--from` point and returning the last N messages after deduplication and ascending sort. If that path cannot provide results, it falls back to `CHATONROOM` + `SYNCMSG` for compatibility. As a safety net, both paths are capped at 50 internal pages. A warning is printed to stderr only when that cap is actually hit and the returned history may be incomplete.
150
189
 
151
- ## Pattern 6: Multi-Chat Broadcast
190
+ ## Pattern 7: Multi-Chat Broadcast
152
191
 
153
192
  **Use case**: Send the same message to multiple chats
154
193
 
@@ -176,7 +215,7 @@ done
176
215
 
177
216
  **When to use**: Announcements, notifications across multiple chats.
178
217
 
179
- ## Pattern 7: Multi-Account Operations
218
+ ## Pattern 8: Multi-Account Operations
180
219
 
181
220
  **Use case**: Manage and operate across multiple KakaoTalk accounts
182
221
 
@@ -201,7 +240,7 @@ agent-kakaotalk auth status --account 1111111111
201
240
 
202
241
  **When to use**: Managing multiple KakaoTalk identities, sending messages as different accounts, or checking status across accounts.
203
242
 
204
- ## Pattern 8: Unread Message Summary
243
+ ## Pattern 9: Unread Message Summary
205
244
 
206
245
  **Use case**: Check which chats have unread messages
207
246
 
@@ -223,7 +262,7 @@ echo "$UNREAD" | jq -r '.[] | " \(.display_name // "Unknown") — \(.unread_cou
223
262
 
224
263
  **When to use**: Morning catch-up, checking for urgent messages, triage.
225
264
 
226
- ## Pattern 9: Resolve Canonical Room Titles
265
+ ## Pattern 10: Resolve Canonical Room Titles
227
266
 
228
267
  **Use case**: Show user-set room names (matching the official KakaoTalk app) instead of comma-joined member nicknames
229
268
 
@@ -269,7 +308,7 @@ const chats = await client.getChats({ resolveTitles: true })
269
308
  const title = await client.getChatTitle('9876543210')
270
309
  ```
271
310
 
272
- ## Pattern 10: Error Handling and Retry
311
+ ## Pattern 11: Error Handling and Retry
273
312
 
274
313
  **Use case**: Robust message sending with retries
275
314
 
@@ -454,9 +493,9 @@ The `deviceType` determines the LOCO protocol identity: `'tablet'` sends `os: 'a
454
493
 
455
494
  ### Auto-Reconnect
456
495
 
457
- `getChats`, `getMessages`, and `sendMessage` automatically reconnect once when the LOCO session dies (e.g. the KakaoTalk desktop app reclaims the session or the network drops). The reconnect is transparent — callers don't need to handle session-drop errors.
496
+ `getChats`, `getMessages`, `sendMessage`, and `sendAttachment` (plus the underlying typed helpers `sendPhoto` / `sendVideo` / `sendAudio` / `sendFile` / `sendMultiPhoto`) automatically reconnect once when the LOCO session dies (e.g. the KakaoTalk desktop app reclaims the session or the network drops). The reconnect is transparent — callers don't need to handle session-drop errors.
458
497
 
459
- Reconnect only triggers on actual session death. Operation-level errors (invalid chat ID, server rejection, etc.) are thrown immediately without retry, so side effects like `sendMessage` are never duplicated.
498
+ Reconnect only triggers on actual session death. Operation-level errors (invalid chat ID, server rejection, etc.) are thrown immediately without retry, so side effects like `sendMessage` or `sendAttachment` are never duplicated.
460
499
 
461
500
  ## See Also
462
501
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-line
3
3
  description: Interact with LINE - send messages, read chats, manage conversations
4
- version: 2.15.0
4
+ version: 2.16.0
5
5
  allowed-tools: Bash(agent-line:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-slack
3
3
  description: Interact with Slack workspaces - send messages, read channels, manage reactions
4
- version: 2.15.0
4
+ version: 2.16.0
5
5
  allowed-tools: Bash(agent-slack:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-slackbot
3
3
  description: Interact with Slack workspaces using bot tokens - send messages, read channels, manage reactions
4
- version: 2.15.0
4
+ version: 2.16.0
5
5
  allowed-tools: Bash(agent-slackbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-teams
3
3
  description: Interact with Microsoft Teams - send messages, read channels, manage reactions
4
- version: 2.15.0
4
+ version: 2.16.0
5
5
  allowed-tools: Bash(agent-teams:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-telegram
3
3
  description: Interact with Telegram through TDLib - authenticate, inspect chats, and send messages
4
- version: 2.15.0
4
+ version: 2.16.0
5
5
  allowed-tools: Bash(agent-telegram:*)
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-telegrambot
3
3
  description: Interact with Telegram using bot tokens - send messages, read chats, manage reactions
4
- version: 2.15.0
4
+ version: 2.16.0
5
5
  allowed-tools: Bash(agent-telegrambot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-webex
3
3
  description: Interact with Cisco Webex - send messages, read spaces, manage memberships
4
- version: 2.15.0
4
+ version: 2.16.0
5
5
  allowed-tools: Bash(agent-webex:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-wechatbot
3
3
  description: Interact with WeChat Official Account using API credentials - send messages, manage templates, list followers
4
- version: 2.15.0
4
+ version: 2.16.0
5
5
  allowed-tools: Bash(agent-wechatbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-whatsapp
3
3
  description: Interact with WhatsApp - send messages, read chats, manage conversations
4
- version: 2.15.0
4
+ version: 2.16.0
5
5
  allowed-tools: Bash(agent-whatsapp:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-whatsappbot
3
3
  description: Interact with WhatsApp using Cloud API credentials - send messages, manage templates
4
- version: 2.15.0
4
+ version: 2.16.0
5
5
  allowed-tools: Bash(agent-whatsappbot:*)
6
6
  metadata:
7
7
  openclaw:
@@ -0,0 +1,102 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+
3
+ import { planAttachments, resolveAttachment } from './attachment-router'
4
+
5
+ const bytes = new Uint8Array([0])
6
+
7
+ describe('resolveAttachment', () => {
8
+ it('classifies image MIME as photo', () => {
9
+ expect(resolveAttachment({ data: bytes, filename: 'x.jpg' }).kind).toBe('photo')
10
+ expect(resolveAttachment({ data: bytes, filename: 'x.png', mime: 'image/png' }).kind).toBe('photo')
11
+ })
12
+
13
+ it('classifies video MIME as video', () => {
14
+ expect(resolveAttachment({ data: bytes, filename: 'x.mp4' }).kind).toBe('video')
15
+ })
16
+
17
+ it('classifies audio MIME as audio', () => {
18
+ expect(resolveAttachment({ data: bytes, filename: 'x.m4a' }).kind).toBe('audio')
19
+ })
20
+
21
+ it('classifies everything else as file', () => {
22
+ expect(resolveAttachment({ data: bytes, filename: 'x.pdf' }).kind).toBe('file')
23
+ expect(resolveAttachment({ data: bytes, filename: 'x.zip' }).kind).toBe('file')
24
+ expect(resolveAttachment({ data: bytes, filename: 'x.unknown-ext' }).kind).toBe('file')
25
+ })
26
+
27
+ it('uses explicit mime override over filename inference', () => {
28
+ const r = resolveAttachment({ data: bytes, filename: 'x.pdf', mime: 'image/png' })
29
+ expect(r.kind).toBe('photo')
30
+ expect(r.mime).toBe('image/png')
31
+ })
32
+
33
+ it('routes upper-case and mixed-case MIME overrides the same as lower-case', () => {
34
+ expect(resolveAttachment({ data: bytes, filename: 'x.bin', mime: 'IMAGE/JPEG' }).kind).toBe('photo')
35
+ expect(resolveAttachment({ data: bytes, filename: 'x.bin', mime: 'Video/MP4' }).kind).toBe('video')
36
+ expect(resolveAttachment({ data: bytes, filename: 'x.bin', mime: 'Audio/MPEG' }).kind).toBe('audio')
37
+ expect(resolveAttachment({ data: bytes, filename: 'x.bin', mime: 'IMAGE/PNG' }).mime).toBe('image/png')
38
+ })
39
+
40
+ it('preserves data and filename verbatim', () => {
41
+ const r = resolveAttachment({ data: bytes, filename: 'cat picture.jpg' })
42
+ expect(r.data).toBe(bytes)
43
+ expect(r.filename).toBe('cat picture.jpg')
44
+ })
45
+ })
46
+
47
+ describe('planAttachments', () => {
48
+ it('throws on empty array', () => {
49
+ expect(() => planAttachments([])).toThrow(/empty/i)
50
+ })
51
+
52
+ it('returns single for a one-element array', () => {
53
+ const plan = planAttachments([{ data: bytes, filename: 'x.jpg' }])
54
+ expect(plan.kind).toBe('single')
55
+ if (plan.kind !== 'single') return
56
+ expect(plan.resolved.kind).toBe('photo')
57
+ })
58
+
59
+ it('returns single (not multiphoto) for a one-photo array', () => {
60
+ const plan = planAttachments([{ data: bytes, filename: 'a.png' }])
61
+ expect(plan.kind).toBe('single')
62
+ })
63
+
64
+ it('returns multiphoto when every item resolves to photo', () => {
65
+ const plan = planAttachments([
66
+ { data: bytes, filename: 'a.jpg' },
67
+ { data: bytes, filename: 'b.png' },
68
+ { data: bytes, filename: 'c.webp' },
69
+ ])
70
+ expect(plan.kind).toBe('multiphoto')
71
+ if (plan.kind !== 'multiphoto') return
72
+ expect(plan.items.length).toBe(3)
73
+ })
74
+
75
+ it('returns sequential for mixed kinds (image + file)', () => {
76
+ const plan = planAttachments([
77
+ { data: bytes, filename: 'photo.jpg' },
78
+ { data: bytes, filename: 'spec.pdf' },
79
+ ])
80
+ expect(plan.kind).toBe('sequential')
81
+ if (plan.kind !== 'sequential') return
82
+ expect(plan.resolved.map((r) => r.kind)).toEqual(['photo', 'file'])
83
+ })
84
+
85
+ it('returns sequential for all-video (multiphoto is image-only)', () => {
86
+ const plan = planAttachments([
87
+ { data: bytes, filename: 'a.mp4' },
88
+ { data: bytes, filename: 'b.mp4' },
89
+ ])
90
+ expect(plan.kind).toBe('sequential')
91
+ if (plan.kind !== 'sequential') return
92
+ expect(plan.resolved.every((r) => r.kind === 'video')).toBe(true)
93
+ })
94
+
95
+ it('honors explicit mime overrides when classifying for the photo gate', () => {
96
+ const plan = planAttachments([
97
+ { data: bytes, filename: 'a.pdf', mime: 'image/jpeg' },
98
+ { data: bytes, filename: 'b.bin', mime: 'image/png' },
99
+ ])
100
+ expect(plan.kind).toBe('multiphoto')
101
+ })
102
+ })
@@ -0,0 +1,50 @@
1
+ import { guessMimeFromFilename } from './media-upload'
2
+
3
+ export type AttachmentInput = {
4
+ data: Uint8Array | Buffer
5
+ filename: string
6
+ mime?: string
7
+ }
8
+
9
+ export type SingleAttachmentKind = 'photo' | 'video' | 'audio' | 'file'
10
+
11
+ export type ResolvedAttachment = {
12
+ kind: SingleAttachmentKind
13
+ mime: string
14
+ data: Uint8Array | Buffer
15
+ filename: string
16
+ }
17
+
18
+ export type AttachmentPlan =
19
+ | { kind: 'single'; resolved: ResolvedAttachment }
20
+ | { kind: 'multiphoto'; items: readonly AttachmentInput[] }
21
+ | { kind: 'sequential'; resolved: readonly ResolvedAttachment[] }
22
+
23
+ export function resolveAttachment(input: AttachmentInput): ResolvedAttachment {
24
+ // MIME types are case-insensitive per RFC 2045 §5.1; normalize so an `Image/JPEG`
25
+ // override still routes to `photo` instead of falling through to `file`.
26
+ const mime = (input.mime ?? guessMimeFromFilename(input.filename)).toLowerCase()
27
+ const kind: SingleAttachmentKind = mime.startsWith('image/')
28
+ ? 'photo'
29
+ : mime.startsWith('video/')
30
+ ? 'video'
31
+ : mime.startsWith('audio/')
32
+ ? 'audio'
33
+ : 'file'
34
+ return { kind, mime, data: input.data, filename: input.filename }
35
+ }
36
+
37
+ export function planAttachments(items: readonly AttachmentInput[]): AttachmentPlan {
38
+ if (items.length === 0) {
39
+ throw new Error('sendAttachment received an empty attachments array')
40
+ }
41
+ if (items.length === 1) {
42
+ return { kind: 'single', resolved: resolveAttachment(items[0]!) }
43
+ }
44
+ const resolved = items.map(resolveAttachment)
45
+ // MULTIPHOTO (message_type 27) is image-only by KakaoTalk's wire protocol.
46
+ if (resolved.every((r) => r.kind === 'photo')) {
47
+ return { kind: 'multiphoto', items: items.slice() }
48
+ }
49
+ return { kind: 'sequential', resolved }
50
+ }