agent-messenger 2.38.1 → 2.39.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 (97) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/package.json +1 -1
  3. package/dist/src/platforms/discordbot/client-message.d.ts +15 -0
  4. package/dist/src/platforms/discordbot/client-message.d.ts.map +1 -0
  5. package/dist/src/platforms/discordbot/client-message.js +70 -0
  6. package/dist/src/platforms/discordbot/client-message.js.map +1 -0
  7. package/dist/src/platforms/discordbot/client-threads.d.ts +10 -0
  8. package/dist/src/platforms/discordbot/client-threads.d.ts.map +1 -0
  9. package/dist/src/platforms/discordbot/client-threads.js +31 -0
  10. package/dist/src/platforms/discordbot/client-threads.js.map +1 -0
  11. package/dist/src/platforms/discordbot/client.d.ts +14 -2
  12. package/dist/src/platforms/discordbot/client.d.ts.map +1 -1
  13. package/dist/src/platforms/discordbot/client.js +21 -28
  14. package/dist/src/platforms/discordbot/client.js.map +1 -1
  15. package/dist/src/platforms/discordbot/commands/file.d.ts +14 -10
  16. package/dist/src/platforms/discordbot/commands/file.d.ts.map +1 -1
  17. package/dist/src/platforms/discordbot/commands/file.js +58 -24
  18. package/dist/src/platforms/discordbot/commands/file.js.map +1 -1
  19. package/dist/src/platforms/discordbot/commands/message.d.ts +3 -9
  20. package/dist/src/platforms/discordbot/commands/message.d.ts.map +1 -1
  21. package/dist/src/platforms/discordbot/commands/message.js +9 -51
  22. package/dist/src/platforms/discordbot/commands/message.js.map +1 -1
  23. package/dist/src/platforms/discordbot/commands/reaction.d.ts +9 -0
  24. package/dist/src/platforms/discordbot/commands/reaction.d.ts.map +1 -1
  25. package/dist/src/platforms/discordbot/commands/reaction.js +39 -0
  26. package/dist/src/platforms/discordbot/commands/reaction.js.map +1 -1
  27. package/dist/src/platforms/discordbot/commands/shared.d.ts +36 -0
  28. package/dist/src/platforms/discordbot/commands/shared.d.ts.map +1 -1
  29. package/dist/src/platforms/discordbot/commands/shared.js +36 -0
  30. package/dist/src/platforms/discordbot/commands/shared.js.map +1 -1
  31. package/dist/src/platforms/discordbot/commands/thread.d.ts +14 -0
  32. package/dist/src/platforms/discordbot/commands/thread.d.ts.map +1 -1
  33. package/dist/src/platforms/discordbot/commands/thread.js +40 -0
  34. package/dist/src/platforms/discordbot/commands/thread.js.map +1 -1
  35. package/dist/src/platforms/discordbot/index.d.ts +1 -1
  36. package/dist/src/platforms/discordbot/index.d.ts.map +1 -1
  37. package/dist/src/platforms/discordbot/index.js.map +1 -1
  38. package/dist/src/platforms/discordbot/types.d.ts +68 -17
  39. package/dist/src/platforms/discordbot/types.d.ts.map +1 -1
  40. package/dist/src/platforms/discordbot/types.js +20 -16
  41. package/dist/src/platforms/discordbot/types.js.map +1 -1
  42. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  43. package/dist/src/platforms/kakaotalk/client.js +4 -32
  44. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  45. package/dist/src/platforms/kakaotalk/sync-state-store.d.ts +11 -0
  46. package/dist/src/platforms/kakaotalk/sync-state-store.d.ts.map +1 -0
  47. package/dist/src/platforms/kakaotalk/sync-state-store.js +89 -0
  48. package/dist/src/platforms/kakaotalk/sync-state-store.js.map +1 -0
  49. package/docs/content/docs/cli/discordbot.mdx +60 -27
  50. package/docs/content/docs/sdk/discordbot.mdx +101 -3
  51. package/e2e/README.md +3 -1
  52. package/e2e/config.ts +1 -1
  53. package/e2e/discordbot.e2e.test.ts +206 -11
  54. package/package.json +1 -1
  55. package/skills/agent-channeltalk/SKILL.md +1 -1
  56. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  57. package/skills/agent-discord/SKILL.md +1 -1
  58. package/skills/agent-discordbot/SKILL.md +57 -11
  59. package/skills/agent-discordbot/references/authentication.md +13 -11
  60. package/skills/agent-discordbot/references/common-patterns.md +36 -8
  61. package/skills/agent-imessage/SKILL.md +1 -1
  62. package/skills/agent-instagram/SKILL.md +1 -1
  63. package/skills/agent-kakaotalk/SKILL.md +1 -1
  64. package/skills/agent-line/SKILL.md +1 -1
  65. package/skills/agent-slack/SKILL.md +1 -1
  66. package/skills/agent-slackbot/SKILL.md +1 -1
  67. package/skills/agent-teams/SKILL.md +1 -1
  68. package/skills/agent-telegram/SKILL.md +1 -1
  69. package/skills/agent-telegrambot/SKILL.md +1 -1
  70. package/skills/agent-webex/SKILL.md +1 -1
  71. package/skills/agent-webexbot/SKILL.md +1 -1
  72. package/skills/agent-wechatbot/SKILL.md +1 -1
  73. package/skills/agent-whatsapp/SKILL.md +1 -1
  74. package/skills/agent-whatsappbot/SKILL.md +1 -1
  75. package/src/platforms/discordbot/client-message.ts +91 -0
  76. package/src/platforms/discordbot/client-threads.test.ts +177 -0
  77. package/src/platforms/discordbot/client-threads.ts +69 -0
  78. package/src/platforms/discordbot/client.test.ts +277 -1
  79. package/src/platforms/discordbot/client.ts +60 -40
  80. package/src/platforms/discordbot/commands/file.test.ts +242 -90
  81. package/src/platforms/discordbot/commands/file.ts +78 -36
  82. package/src/platforms/discordbot/commands/message.test.ts +183 -26
  83. package/src/platforms/discordbot/commands/message.ts +13 -61
  84. package/src/platforms/discordbot/commands/reaction.test.ts +73 -1
  85. package/src/platforms/discordbot/commands/reaction.ts +46 -0
  86. package/src/platforms/discordbot/commands/shared.test.ts +98 -0
  87. package/src/platforms/discordbot/commands/shared.ts +67 -0
  88. package/src/platforms/discordbot/commands/thread.test.ts +58 -1
  89. package/src/platforms/discordbot/commands/thread.ts +58 -0
  90. package/src/platforms/discordbot/index.ts +3 -0
  91. package/src/platforms/discordbot/types.test.ts +46 -0
  92. package/src/platforms/discordbot/types.ts +44 -19
  93. package/src/platforms/kakaotalk/client-listener-integration.test.ts +21 -3
  94. package/src/platforms/kakaotalk/client.test.ts +42 -4
  95. package/src/platforms/kakaotalk/client.ts +4 -39
  96. package/src/platforms/kakaotalk/sync-state-store.test.ts +197 -0
  97. package/src/platforms/kakaotalk/sync-state-store.ts +99 -0
@@ -63,7 +63,7 @@ Without this, the bot won't receive message content in large servers.
63
63
  agent-discordbot auth set <token>
64
64
 
65
65
  # Set with a custom bot identifier for multi-bot setups
66
- agent-discordbot auth set <token> --bot deploy --name "Deploy Bot"
66
+ agent-discordbot auth set <token> --bot deploy
67
67
 
68
68
  # Check bot auth status
69
69
  agent-discordbot auth status
@@ -87,8 +87,8 @@ Store multiple bot tokens and switch between them:
87
87
 
88
88
  ```bash
89
89
  # Add multiple bots
90
- agent-discordbot auth set deploy-bot-token --bot deploy --name "Deploy Bot"
91
- agent-discordbot auth set alert-bot-token --bot alert --name "Alert Bot"
90
+ agent-discordbot auth set deploy-bot-token --bot deploy
91
+ agent-discordbot auth set alert-bot-token --bot alert
92
92
 
93
93
  # List all stored bots
94
94
  agent-discordbot auth list
@@ -105,14 +105,15 @@ agent-discordbot auth remove deploy
105
105
 
106
106
  ### Permissions Reference
107
107
 
108
- | Permission | Used For |
109
- | -------------------- | ----------------------------------- |
110
- | Send Messages | Sending messages to channels |
111
- | Read Message History | Reading past messages in channels |
112
- | View Channels | Listing and accessing channels |
113
- | Add Reactions | Adding and removing emoji reactions |
114
- | Attach Files | Uploading files to channels |
115
- | Manage Threads | Creating and archiving threads |
108
+ | Permission | Used For |
109
+ | ------------------------ | ---------------------------------------------------------------------------- |
110
+ | Send Messages | Sending messages to channels |
111
+ | Send Messages in Threads | Posting into threads with `--thread` (`message send`, `file upload`) |
112
+ | Read Message History | Reading past messages, replying with `--reply`, and `thread list --archived` |
113
+ | View Channels | Listing and accessing channels |
114
+ | Add Reactions | Adding and removing emoji reactions |
115
+ | Attach Files | Uploading files to channels and threads |
116
+ | Manage Threads | Creating and archiving threads |
116
117
 
117
118
  ### Environment Variables (CI/CD)
118
119
 
@@ -159,8 +160,12 @@ agent-discordbot message send general "Hello world"
159
160
  # Reply to a message
160
161
  agent-discordbot message send general "On it!" --reply 9876543210987654321
161
162
 
162
- # Send a message into a thread
163
- agent-discordbot message send general "Hello world" --thread 9876543210987654321
163
+ # Send a message into a thread. --thread takes a thread ID or an active
164
+ # thread name in that channel. The message lands in the thread, so the
165
+ # response's channel_id is the thread ID, not the parent channel.
166
+ agent-discordbot thread list general # find the thread ID
167
+ agent-discordbot message send general "Hello" --thread 9876543210987654321
168
+ agent-discordbot message send general "Hello" --thread "Release notes"
164
169
 
165
170
  # List messages
166
171
  agent-discordbot message list <channel>
@@ -196,7 +201,6 @@ agent-discordbot channel info general
196
201
  ```bash
197
202
  # List server members
198
203
  agent-discordbot user list
199
- agent-discordbot user list --limit 50
200
204
 
201
205
  # Get user info
202
206
  agent-discordbot user info <user-id>
@@ -211,19 +215,39 @@ agent-discordbot reaction add general 9876543210987654321 thumbsup
211
215
 
212
216
  # Remove reaction
213
217
  agent-discordbot reaction remove <channel> <message-id> <emoji>
218
+
219
+ # List reactions on a message (emoji, count, and whether the bot reacted)
220
+ agent-discordbot reaction list <channel> <message-id>
214
221
  ```
215
222
 
216
223
  ### File Commands
217
224
 
218
225
  ```bash
219
- # Upload file to a channel
220
- agent-discordbot file upload <channel> <file-path>
226
+ # Upload one or more files (max 10) to a channel
227
+ agent-discordbot file upload <channel> <file-path...> [--text <text>] [--thread <id|name>] [--reply <message-id>] [--filename <name>]
221
228
  agent-discordbot file upload general ./report.pdf
229
+ agent-discordbot file upload general ./report.pdf ./chart.png --text "Weekly numbers"
230
+
231
+ # Upload into a thread (same --thread rules as message send)
232
+ agent-discordbot file upload general ./log.txt --thread "Release notes"
233
+
234
+ # Upload as a reply to a message
235
+ agent-discordbot file upload general ./fix.patch --reply 9876543210987654321
222
236
 
223
- # List files in channel
237
+ # Override the filename (single file only)
238
+ agent-discordbot file upload general ./tmp/abc123.txt --filename notes.txt
239
+
240
+ # List files in channel (attachments of the latest 100 messages, one request)
224
241
  agent-discordbot file list <channel>
242
+
243
+ # Get a single file by attachment ID (searches back through the latest 1,000 messages)
244
+ agent-discordbot file info <channel> <file-id>
225
245
  ```
226
246
 
247
+ `file list` reads a single page of channel history, so it stays fast on busy channels. `file info` pages further back with `before` cursors and stops at the first matching attachment; a file older than the latest 1,000 messages reports `File not found`.
248
+
249
+ The upload response includes `message_id`, `channel_id` (the thread ID when `--thread` is used), and a `files` array with each attachment's `id`, `filename`, `size`, and `url`. Discord enforces the per-file size limit; oversized uploads come back as an error instead of being rejected locally.
250
+
227
251
  ### Thread Commands
228
252
 
229
253
  ```bash
@@ -231,6 +255,13 @@ agent-discordbot file list <channel>
231
255
  agent-discordbot thread create <channel> <name>
232
256
  agent-discordbot thread create general "Discussion" --auto-archive-duration 1440
233
257
 
258
+ # List active threads in the server, or only those under one channel
259
+ agent-discordbot thread list
260
+ agent-discordbot thread list <channel>
261
+
262
+ # List archived threads (a channel is required; returns up to 100 with has_more)
263
+ agent-discordbot thread list <channel> --archived
264
+
234
265
  # Archive a thread
235
266
  agent-discordbot thread archive <thread-id>
236
267
  ```
@@ -269,16 +300,18 @@ With `--full`, returns comprehensive JSON with:
269
300
 
270
301
  ## Key Differences from agent-discord
271
302
 
272
- | Feature | agent-discord | agent-discordbot |
273
- | --------------------------- | ------------------------------- | ---------------------------- |
274
- | Token type | User token | Bot token |
275
- | Token source | Auto-extracted from desktop app | Manual from Developer Portal |
276
- | Message search | Yes | No |
277
- | DM commands | Yes | No |
278
- | Mention/Friend/Note/Profile | Yes | No |
279
- | Thread management | Yes | Yes |
280
- | Multi-bot support | N/A | Yes (`--bot` flag) |
281
- | CI/CD friendly | Requires desktop app | Yes |
303
+ | Feature | agent-discord | agent-discordbot |
304
+ | --------------------------- | ------------------------------- | ------------------------------------------ |
305
+ | Token type | User token | Bot token |
306
+ | Token source | Auto-extracted from desktop app | Manual from Developer Portal |
307
+ | Message search | Yes | No |
308
+ | DM commands | Yes | No |
309
+ | Mention/Friend/Note/Profile | Yes | No |
310
+ | Thread management | Yes | Yes (create, list, archive) |
311
+ | File upload | Yes | Yes (multi-file, into threads, as replies) |
312
+ | Reactions | Yes | Yes (add, remove, list) |
313
+ | Multi-bot support | N/A | Yes (`--bot` flag) |
314
+ | CI/CD friendly | Requires desktop app | Yes |
282
315
 
283
316
  ## Global Options
284
317
 
@@ -76,9 +76,10 @@ const sameId = await client.resolveChannel(guildId, '1234567890123456789')
76
76
  // Send a message to a channel
77
77
  const msg = await client.sendMessage(channelId, 'Hello world')
78
78
 
79
- // Send a message in a thread
79
+ // Send a message into a thread. The request is posted to the thread itself,
80
+ // so the returned channel_id is the thread ID, not channelId.
80
81
  const reply = await client.sendMessage(channelId, 'Reply', { thread_id: threadId })
81
- // → DiscordMessage { id, channel_id, author, content, timestamp }
82
+ // → DiscordMessage { id, channel_id: threadId, author, content, timestamp, attachments? }
82
83
 
83
84
  // Reply to a specific message
84
85
  const replyToMsg = await client.sendMessage(channelId, 'On it!', { reply_to: messageId })
@@ -101,6 +102,30 @@ const edited = await client.editMessage(channelId, messageId, 'Updated content')
101
102
  await client.deleteMessage(channelId, messageId)
102
103
  ```
103
104
 
105
+ #### createMessage
106
+
107
+ `createMessage` is the single entry point behind `sendMessage` and `uploadFile`. It takes text, files, a reply target, and a thread in one call and returns the created `DiscordMessage`, including its `attachments`.
108
+
109
+ ```typescript
110
+ const created = await client.createMessage(channelId, {
111
+ content: 'Build artifacts attached',
112
+ files: [{ path: '/tmp/report.pdf' }, { path: '/tmp/log.txt', filename: 'build-log.txt' }],
113
+ reply_to: messageId,
114
+ thread_id: threadId,
115
+ })
116
+ // → DiscordMessage { id, channel_id: threadId, content, attachments: DiscordFile[], ... }
117
+ ```
118
+
119
+ How the request is built:
120
+
121
+ - With no `files`, the message is sent as a plain JSON body (`content`, `message_reference`).
122
+ - With `files`, the client sends `multipart/form-data`: a `payload_json` part carrying `content`, `message_reference`, and an `attachments` list, plus one `files[n]` part per file. Files are read from disk with `node:fs/promises`.
123
+ - `thread_id` changes the target path to `/channels/{thread_id}/messages`. Discord's Create Message body has no thread field, so this is the only way a message lands in a thread. Posting to an archived thread by ID unarchives it unless the thread is locked.
124
+ - `reply_to` becomes `message_reference: { message_id }`.
125
+ - `filename` overrides the name Discord shows for that file. It's taken as a basename, so it can't contain `/` or `\`, and it can't be empty, `.`, or `..`.
126
+
127
+ Validation happens before any request goes out. An empty `content` with no files throws `empty_message`, more than 10 files throws `too_many_files`, and a bad `filename` throws `invalid_filename`. Discord enforces the per-file size limit server-side; when an upload exceeds it, the client surfaces Discord error `40005` (or HTTP 413) with a message that explains the cause.
128
+
104
129
  ### Reactions
105
130
 
106
131
  ```typescript
@@ -131,11 +156,34 @@ const user = await client.getUser(userId)
131
156
  const file = await client.uploadFile(channelId, '/path/to/report.pdf')
132
157
  // → DiscordFile { id, filename, size, url, content_type?, height?, width? }
133
158
 
134
- // List file attachments from recent messages in a channel
159
+ // Optional third parameter: caption, display name, reply target, thread
160
+ const inThread = await client.uploadFile(channelId, '/path/to/report.pdf', {
161
+ content: 'Weekly report',
162
+ filename: 'report-2026-09.pdf',
163
+ reply_to: messageId,
164
+ thread_id: threadId,
165
+ })
166
+ // → DiscordFile (the first attachment of the created message)
167
+
168
+ // Several files in one message: use createMessage and read the attachments back
169
+ const batch = await client.createMessage(channelId, {
170
+ content: 'Screenshots',
171
+ files: [{ path: '/tmp/a.png' }, { path: '/tmp/b.png' }, { path: '/tmp/c.png' }],
172
+ })
173
+ // → batch.attachments: DiscordFile[] (up to 10 per message)
174
+
175
+ // List file attachments from recent messages in a channel (one page, latest 100 messages)
135
176
  const files = await client.listFiles(channelId)
136
177
  // → DiscordFile[]
178
+
179
+ // Look up one attachment by ID; pages back with before cursors and stops at the first match
180
+ const found = await client.findFile(channelId, fileId)
181
+ // → DiscordFile | undefined
137
182
  ```
138
183
 
184
+ `listFiles` always makes exactly one request. `findFile` scans up to 10 pages (the latest 1,000 messages) and
185
+ returns `undefined` when the attachment is not in that range.
186
+
139
187
  ### Threads
140
188
 
141
189
  ```typescript
@@ -151,8 +199,25 @@ const configured = await client.createThread(channelId, 'Design Review', {
151
199
  await client.archiveThread(threadId)
152
200
  await client.archiveThread(threadId, false) // unarchive
153
201
  // → DiscordChannel
202
+
203
+ // List active threads in a server, optionally narrowed to one parent channel
204
+ const active = await client.listThreads(guildId)
205
+ const inChannel = await client.listThreads(guildId, { parentId: channelId })
206
+ // → DiscordThreadListResult { threads: DiscordChannel[] }
207
+
208
+ // List archived public threads of a channel (one page of up to 100)
209
+ const archived = await client.listThreads(guildId, { parentId: channelId, archived: true })
210
+ // → DiscordThreadListResult { threads, has_more? }
211
+
212
+ // Resolve a thread name (or ID) to a thread ID
213
+ const resolvedId = await client.resolveThread(guildId, channelId, 'bug-report-login')
214
+ const sameThreadId = await client.resolveThread(guildId, channelId, '1234567890123456789')
154
215
  ```
155
216
 
217
+ `listThreads` with `archived: true` needs `parentId`; without it the call throws `parent_required`. Active threads come from a single guild-wide request and are filtered by `parent_id` on the client.
218
+
219
+ `resolveThread` returns a numeric ID as-is without any lookup. A name (with or without a leading `#`) is matched against the active threads of that parent channel in one request. Two or more matches throw `thread_ambiguous` with the candidate IDs in the message; no match throws `thread_not_found`. Archived threads aren't searched by name. Find them with `listThreads(..., { archived: true })` and target them by ID.
220
+
156
221
  ## Real-Time Events
157
222
 
158
223
  `DiscordBotListener` connects to Discord's [Gateway WebSocket](https://discord.com/developers/docs/topics/gateway) for instant event streaming — messages, reactions, slash command interactions, member changes, and more. No webhook server required.
@@ -285,11 +350,13 @@ await manager.setCurrentServer('1234567890123456789', 'My Server')
285
350
 
286
351
  ```typescript
287
352
  import type {
353
+ DiscordAttachmentInput,
288
354
  DiscordBotConfig,
289
355
  DiscordBotCredentials,
290
356
  DiscordBotEntry,
291
357
  DiscordBotListenerEventMap,
292
358
  DiscordChannel,
359
+ DiscordCreateMessageOptions,
293
360
  DiscordFile,
294
361
  DiscordGatewayChannelEvent,
295
362
  DiscordGatewayEvent,
@@ -306,10 +373,34 @@ import type {
306
373
  DiscordGuild,
307
374
  DiscordMessage,
308
375
  DiscordReaction,
376
+ DiscordThreadListResult,
309
377
  DiscordUser,
310
378
  } from 'agent-messenger/discordbot'
311
379
  ```
312
380
 
381
+ Shapes used by `createMessage` and the thread helpers:
382
+
383
+ ```typescript
384
+ interface DiscordAttachmentInput {
385
+ path: string // local file path
386
+ filename?: string // display name override (basename only)
387
+ }
388
+
389
+ interface DiscordCreateMessageOptions {
390
+ content?: string
391
+ files?: DiscordAttachmentInput[] // at most 10
392
+ reply_to?: string // message ID to reply to
393
+ thread_id?: string // post into this thread instead of the channel
394
+ }
395
+
396
+ interface DiscordThreadListResult {
397
+ threads: DiscordChannel[]
398
+ has_more?: boolean // only set for archived listings
399
+ }
400
+ ```
401
+
402
+ `DiscordMessage` also carries optional `attachments` (`DiscordFile[]`), `reactions` (`DiscordReaction[]`), and `thread` (`DiscordChannel`, the thread started from that message). `DiscordReaction.me` reports whether the bot itself reacted. `DiscordMessageSchema` and `DiscordReactionSchema` validate these fields too.
403
+
313
404
  ### Zod Schemas
314
405
 
315
406
  Runtime-validated schemas for parsing API responses:
@@ -375,7 +466,14 @@ Error codes thrown by the client:
375
466
  - `rate_limited` — Discord 429 response; the client retries automatically with `Retry-After`, then surfaces this if retries are exhausted
376
467
  - `network_error` — Fetch failed (DNS, connection refused, timeout)
377
468
  - `channel_not_found` — `resolveChannel()` could not find the named channel in the current guild
469
+ - `empty_message` — `createMessage()` called with no content and no files
470
+ - `too_many_files` — `createMessage()` called with more than 10 files
471
+ - `invalid_filename` — A `filename` override was empty, `.`, `..`, or contained a path separator
472
+ - `parent_required` — `listThreads()` called with `archived: true` but no `parentId`
473
+ - `thread_ambiguous` — `resolveThread()` matched more than one active thread with that name; the message lists the IDs
474
+ - `thread_not_found` — `resolveThread()` found no active thread with that name in the parent channel
378
475
  - `no_attachments` — File upload returned 200 but the response had no attachments
476
+ - `40005` / `http_413` — Discord rejected an upload as too large for the server's limit; the message says so explicitly
379
477
  - `http_<status>` — Discord returned an unexpected status (e.g., `http_500`) without a structured `code` in the error body
380
478
  - Discord API error codes (numeric, as strings) — When Discord's response body includes a `code` field (e.g., `"50001"` for "Missing Access"), it is propagated as-is. See the [Discord JSON Error Codes reference](https://discord.com/developers/docs/topics/opcodes-and-status-codes#json-json-error-codes).
381
479
 
package/e2e/README.md CHANGED
@@ -231,7 +231,9 @@ When triggering manually, you can select which platform to test:
231
231
  | `message` | send, list, get, edit, delete, thread, replies |
232
232
  | `channel` | list, info |
233
233
  | `user` | list, info |
234
- | `reaction` | add, remove |
234
+ | `file` | upload, list, info |
235
+ | `thread` | create, list, archive |
236
+ | `reaction` | add, list, remove |
235
237
  | `server` | list, current |
236
238
 
237
239
  ### Webex Tests
package/e2e/config.ts CHANGED
@@ -80,7 +80,7 @@ export async function validateDiscordEnvironment() {
80
80
  export const DISCORDBOT_TEST_SERVER_ID = process.env.E2E_DISCORDBOT_SERVER_ID || '1467039439770357844'
81
81
  export const DISCORDBOT_TEST_SERVER_NAME = process.env.E2E_DISCORDBOT_SERVER_NAME || 'Agent Messenger'
82
82
  export const DISCORDBOT_TEST_CHANNEL_ID = process.env.E2E_DISCORDBOT_CHANNEL_ID || '1467062262996144162'
83
- export const DISCORDBOT_TEST_CHANNEL = 'e2e-test'
83
+ export const DISCORDBOT_TEST_CHANNEL = process.env.E2E_DISCORDBOT_CHANNEL_NAME || 'e2e-test'
84
84
 
85
85
  export async function validateDiscordBotEnvironment() {
86
86
  const { runCLI, parseJSON } = await import('./helpers')
@@ -1,4 +1,7 @@
1
1
  import { describe, it, expect, beforeAll, afterEach } from 'bun:test'
2
+ import { rm } from 'node:fs/promises'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
2
5
 
3
6
  import { DiscordBotClient } from '../src/platforms/discordbot/client'
4
7
  import { DiscordBotCredentialManager } from '../src/platforms/discordbot/credential-manager'
@@ -16,6 +19,7 @@ interface TrackedMessage {
16
19
  }
17
20
 
18
21
  let testMessages: TrackedMessage[] = []
22
+ let testThreads: string[] = []
19
23
  let cleanupClient: DiscordBotClient
20
24
 
21
25
  async function getClient(): Promise<DiscordBotClient> {
@@ -36,10 +40,29 @@ async function cleanupBotMessages(messages: TrackedMessage[]) {
36
40
  }
37
41
  }
38
42
 
43
+ async function cleanupBotThreads(threads: string[]) {
44
+ for (const threadId of threads) {
45
+ try {
46
+ await cleanupClient.archiveThread(threadId)
47
+ await waitForRateLimit(500)
48
+ } catch {
49
+ // best-effort cleanup
50
+ }
51
+ }
52
+ }
53
+
39
54
  function trackMessage(id: string, channelId: string = DISCORDBOT_TEST_CHANNEL_ID) {
40
55
  testMessages.push({ id, channelId })
41
56
  }
42
57
 
58
+ function trackThread(id: string) {
59
+ testThreads.push(id)
60
+ }
61
+
62
+ function untrackThread(id: string) {
63
+ testThreads = testThreads.filter((threadId) => threadId !== id)
64
+ }
65
+
43
66
  describe('DiscordBot E2E Tests', () => {
44
67
  beforeAll(async () => {
45
68
  await validateDiscordBotEnvironment()
@@ -51,6 +74,10 @@ describe('DiscordBot E2E Tests', () => {
51
74
  await cleanupBotMessages(testMessages)
52
75
  testMessages = []
53
76
  }
77
+ if (testThreads.length > 0) {
78
+ await cleanupBotThreads(testThreads)
79
+ testThreads = []
80
+ }
54
81
  await waitForRateLimit()
55
82
  })
56
83
 
@@ -161,12 +188,20 @@ describe('DiscordBot E2E Tests', () => {
161
188
  expect(result.exitCode).toBe(0)
162
189
  })
163
190
 
164
- it('message send with --thread creates reply', async () => {
191
+ it('message send with --thread creates reply in the thread', async () => {
165
192
  const testId = generateTestId()
166
- const sendResult = await runCLI('discordbot', ['message', 'send', DISCORDBOT_TEST_CHANNEL_ID, `Parent ${testId}`])
167
- const parent = parseJSON<{ id: string }>(sendResult.stdout)
168
- expect(parent?.id).toBeTruthy()
169
- if (parent?.id) trackMessage(parent.id)
193
+ const threadResult = await runCLI('discordbot', [
194
+ 'thread',
195
+ 'create',
196
+ DISCORDBOT_TEST_CHANNEL_ID,
197
+ `E2E Thread ${testId}`,
198
+ ])
199
+ expect(threadResult.exitCode).toBe(0)
200
+
201
+ const thread = parseJSON<{ thread: { id: string } }>(threadResult.stdout)
202
+ expect(thread?.thread?.id).toBeTruthy()
203
+ const threadId = thread!.thread.id
204
+ trackThread(threadId)
170
205
 
171
206
  await waitForRateLimit()
172
207
 
@@ -176,15 +211,14 @@ describe('DiscordBot E2E Tests', () => {
176
211
  DISCORDBOT_TEST_CHANNEL_ID,
177
212
  `Reply ${testId}`,
178
213
  '--thread',
179
- parent!.id,
214
+ threadId,
180
215
  ])
181
216
  expect(replyResult.exitCode).toBe(0)
182
217
 
183
- const reply = parseJSON<{ id: string; thread_id: string }>(replyResult.stdout)
218
+ const reply = parseJSON<{ id: string; channel_id: string }>(replyResult.stdout)
184
219
  expect(reply?.id).toBeTruthy()
185
-
186
- const replyChannel = reply?.thread_id || DISCORDBOT_TEST_CHANNEL_ID
187
- if (reply?.id) trackMessage(reply.id, replyChannel)
220
+ expect(reply?.channel_id).toBe(threadId)
221
+ if (reply?.id) trackMessage(reply.id, threadId)
188
222
  }, 30000)
189
223
 
190
224
  it('message replies gets thread messages', async () => {
@@ -201,6 +235,7 @@ describe('DiscordBot E2E Tests', () => {
201
235
  const thread = parseJSON<{ thread: { id: string } }>(threadResult.stdout)
202
236
  expect(thread?.thread?.id).toBeTruthy()
203
237
  const threadId = thread!.thread.id
238
+ trackThread(threadId)
204
239
 
205
240
  await waitForRateLimit()
206
241
 
@@ -222,9 +257,155 @@ describe('DiscordBot E2E Tests', () => {
222
257
 
223
258
  // cleanup: archive the thread
224
259
  await runCLI('discordbot', ['thread', 'archive', threadId])
260
+ untrackThread(threadId)
225
261
  })
226
262
  })
227
263
 
264
+ describe('file', () => {
265
+ it('uploads a file into a thread and round-trips its metadata', async () => {
266
+ const testId = generateTestId()
267
+ const filePath = join(tmpdir(), `e2e-${testId}.txt`)
268
+ await Bun.write(filePath, testId)
269
+
270
+ try {
271
+ const threadResult = await runCLI('discordbot', [
272
+ 'thread',
273
+ 'create',
274
+ DISCORDBOT_TEST_CHANNEL_ID,
275
+ `E2E Thread ${testId}`,
276
+ ])
277
+ expect(threadResult.exitCode).toBe(0)
278
+ const thread = parseJSON<{ thread: { id: string } }>(threadResult.stdout)
279
+ expect(thread?.thread?.id).toBeTruthy()
280
+ const threadId = thread!.thread.id
281
+ trackThread(threadId)
282
+
283
+ await waitForRateLimit()
284
+
285
+ const uploadResult = await runCLI('discordbot', [
286
+ 'file',
287
+ 'upload',
288
+ DISCORDBOT_TEST_CHANNEL_ID,
289
+ filePath,
290
+ '--text',
291
+ `File ${testId}`,
292
+ '--thread',
293
+ threadId,
294
+ ])
295
+ expect(uploadResult.exitCode).toBe(0)
296
+ const upload = parseJSON<{
297
+ channel_id: string
298
+ message_id: string
299
+ file: { id: string; filename: string }
300
+ files: Array<{ filename: string }>
301
+ }>(uploadResult.stdout)
302
+ expect(upload?.channel_id).toBe(threadId)
303
+ expect(upload?.message_id).toBeTruthy()
304
+ expect(upload?.file?.filename).toBe(`e2e-${testId}.txt`)
305
+ expect(upload?.files).toHaveLength(1)
306
+ trackMessage(upload!.message_id, threadId)
307
+
308
+ await waitForRateLimit()
309
+
310
+ const messageResult = await runCLI('discordbot', ['message', 'get', threadId, upload!.message_id])
311
+ expect(messageResult.exitCode).toBe(0)
312
+ const message = parseJSON<{
313
+ channel_id: string
314
+ content: string
315
+ attachments: Array<{ id: string; filename: string; size: number }>
316
+ }>(messageResult.stdout)
317
+ expect(message?.channel_id).toBe(threadId)
318
+ expect(message?.content).toBe(`File ${testId}`)
319
+ expect(message?.attachments).toHaveLength(1)
320
+ expect(message!.attachments[0].filename).toBe(`e2e-${testId}.txt`)
321
+ expect(message!.attachments[0].size).toBe(Buffer.byteLength(testId))
322
+
323
+ await waitForRateLimit()
324
+
325
+ const namedResult = await runCLI('discordbot', [
326
+ 'message',
327
+ 'send',
328
+ DISCORDBOT_TEST_CHANNEL_ID,
329
+ `Reply ${testId}`,
330
+ '--thread',
331
+ `E2E Thread ${testId}`,
332
+ ])
333
+ expect(namedResult.exitCode).toBe(0)
334
+ const named = parseJSON<{ id: string; channel_id: string }>(namedResult.stdout)
335
+ expect(named?.channel_id).toBe(threadId)
336
+ if (named?.id) trackMessage(named.id, threadId)
337
+
338
+ await waitForRateLimit()
339
+
340
+ const missingNameResult = await runCLI('discordbot', [
341
+ 'message',
342
+ 'send',
343
+ DISCORDBOT_TEST_CHANNEL_ID,
344
+ 'x',
345
+ '--thread',
346
+ `no-such-thread-${testId}`,
347
+ ])
348
+ expect(missingNameResult.exitCode).toBe(1)
349
+ const missingName = parseJSON<{ error: string }>(missingNameResult.stdout)
350
+ expect(missingName?.error).toContain('thread list')
351
+ expect(missingNameResult.stderr).toBe('')
352
+
353
+ await waitForRateLimit()
354
+
355
+ const activeListResult = await runCLI('discordbot', ['thread', 'list', DISCORDBOT_TEST_CHANNEL_ID])
356
+ expect(activeListResult.exitCode).toBe(0)
357
+ const activeList = parseJSON<{
358
+ threads: Array<{ id: string; parent_id?: string; archived?: boolean }>
359
+ }>(activeListResult.stdout)
360
+ expect(
361
+ activeList?.threads?.some(
362
+ (item) => item.id === threadId && item.parent_id === DISCORDBOT_TEST_CHANNEL_ID && item.archived === false,
363
+ ),
364
+ ).toBe(true)
365
+
366
+ await waitForRateLimit()
367
+
368
+ const infoResult = await runCLI('discordbot', ['file', 'info', threadId, upload!.file.id])
369
+ expect(infoResult.exitCode).toBe(0)
370
+ const info = parseJSON<{ id: string; filename: string }>(infoResult.stdout)
371
+ expect(info?.id).toBe(upload!.file.id)
372
+ expect(info?.filename).toBe(`e2e-${testId}.txt`)
373
+
374
+ const missingInfoResult = await runCLI('discordbot', [
375
+ 'file',
376
+ 'info',
377
+ DISCORDBOT_TEST_CHANNEL_ID,
378
+ '000000000000000000',
379
+ ])
380
+ expect(missingInfoResult.exitCode).toBe(0)
381
+ const missingInfo = parseJSON<{ error: string }>(missingInfoResult.stdout)
382
+ expect(missingInfo?.error).toContain('File not found')
383
+
384
+ await waitForRateLimit()
385
+
386
+ const parentMessagesResult = await runCLI('discordbot', [
387
+ 'message',
388
+ 'list',
389
+ DISCORDBOT_TEST_CHANNEL_ID,
390
+ '--limit',
391
+ '5',
392
+ ])
393
+ expect(parentMessagesResult.exitCode).toBe(0)
394
+ const parentMessages = parseJSON<{ messages: Array<{ content: string }> }>(parentMessagesResult.stdout)
395
+ expect(parentMessages?.messages?.some((item) => item.content === 'x')).toBe(false)
396
+
397
+ const archiveResult = await runCLI('discordbot', ['thread', 'archive', threadId])
398
+ expect(archiveResult.exitCode).toBe(0)
399
+ untrackThread(threadId)
400
+
401
+ // Discord's archived-thread index is eventually consistent; active listing above is the stable proof.
402
+ await waitForRateLimit(2000)
403
+ } finally {
404
+ await rm(filePath, { force: true })
405
+ }
406
+ }, 60000)
407
+ })
408
+
228
409
  describe('channel', () => {
229
410
  it('channel list returns channels array', async () => {
230
411
  const result = await runCLI('discordbot', ['channel', 'list'])
@@ -288,6 +469,11 @@ describe('DiscordBot E2E Tests', () => {
288
469
 
289
470
  await waitForRateLimit(2000)
290
471
 
472
+ const emptyListResult = await runCLI('discordbot', ['reaction', 'list', DISCORDBOT_TEST_CHANNEL_ID, sent!.id])
473
+ expect(emptyListResult.exitCode).toBe(0)
474
+ const emptyList = parseJSON<{ reactions: unknown[] }>(emptyListResult.stdout)
475
+ expect(emptyList?.reactions).toEqual([])
476
+
291
477
  // when: add reaction
292
478
  const addResult = await runCLI('discordbot', ['reaction', 'add', DISCORDBOT_TEST_CHANNEL_ID, sent!.id, '👍'])
293
479
  expect(addResult.exitCode).toBe(0)
@@ -297,6 +483,15 @@ describe('DiscordBot E2E Tests', () => {
297
483
 
298
484
  await waitForRateLimit(2000)
299
485
 
486
+ const listResult = await runCLI('discordbot', ['reaction', 'list', DISCORDBOT_TEST_CHANNEL_ID, sent!.id])
487
+ expect(listResult.exitCode).toBe(0)
488
+ const listed = parseJSON<{
489
+ reactions: Array<{ emoji: { name: string }; count: number; me: boolean }>
490
+ }>(listResult.stdout)
491
+ expect(listed?.reactions?.[0]?.emoji?.name).toBe('👍')
492
+ expect(listed?.reactions?.[0]?.count).toBe(1)
493
+ expect(listed?.reactions?.[0]?.me).toBe(true)
494
+
300
495
  // then: remove reaction
301
496
  const removeResult = await runCLI('discordbot', [
302
497
  'reaction',
@@ -309,7 +504,7 @@ describe('DiscordBot E2E Tests', () => {
309
504
 
310
505
  const removeData = parseJSON<{ success: boolean }>(removeResult.stdout)
311
506
  expect(removeData?.success).toBe(true)
312
- }, 15000)
507
+ }, 30000)
313
508
  })
314
509
 
315
510
  describe('server', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-messenger",
3
- "version": "2.38.1",
3
+ "version": "2.39.0",
4
4
  "description": "Multi-platform messaging CLI for AI agents (Slack, Discord, Teams, Webex, Telegram, Telegram Bot, WhatsApp, LINE, iMessage, Instagram, KakaoTalk, Channel Talk)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-channeltalk
3
3
  description: Interact with Channel Talk using extracted desktop app or browser credentials - read chats, send messages, search messages, manage groups
4
- version: 2.38.1
4
+ version: 2.39.0
5
5
  allowed-tools: Bash(agent-channeltalk:*)
6
6
  metadata:
7
7
  openclaw:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: agent-channeltalkbot
3
3
  description: Interact with Channel Talk workspaces using API credentials - send messages, read chats, manage groups and bots
4
- version: 2.38.1
4
+ version: 2.39.0
5
5
  allowed-tools: Bash(agent-channeltalkbot:*)
6
6
  metadata:
7
7
  openclaw: