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.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/package.json +1 -1
- package/dist/src/platforms/discordbot/client-message.d.ts +15 -0
- package/dist/src/platforms/discordbot/client-message.d.ts.map +1 -0
- package/dist/src/platforms/discordbot/client-message.js +70 -0
- package/dist/src/platforms/discordbot/client-message.js.map +1 -0
- package/dist/src/platforms/discordbot/client-threads.d.ts +10 -0
- package/dist/src/platforms/discordbot/client-threads.d.ts.map +1 -0
- package/dist/src/platforms/discordbot/client-threads.js +31 -0
- package/dist/src/platforms/discordbot/client-threads.js.map +1 -0
- package/dist/src/platforms/discordbot/client.d.ts +14 -2
- package/dist/src/platforms/discordbot/client.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/client.js +21 -28
- package/dist/src/platforms/discordbot/client.js.map +1 -1
- package/dist/src/platforms/discordbot/commands/file.d.ts +14 -10
- package/dist/src/platforms/discordbot/commands/file.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/commands/file.js +58 -24
- package/dist/src/platforms/discordbot/commands/file.js.map +1 -1
- package/dist/src/platforms/discordbot/commands/message.d.ts +3 -9
- package/dist/src/platforms/discordbot/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/commands/message.js +9 -51
- package/dist/src/platforms/discordbot/commands/message.js.map +1 -1
- package/dist/src/platforms/discordbot/commands/reaction.d.ts +9 -0
- package/dist/src/platforms/discordbot/commands/reaction.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/commands/reaction.js +39 -0
- package/dist/src/platforms/discordbot/commands/reaction.js.map +1 -1
- package/dist/src/platforms/discordbot/commands/shared.d.ts +36 -0
- package/dist/src/platforms/discordbot/commands/shared.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/commands/shared.js +36 -0
- package/dist/src/platforms/discordbot/commands/shared.js.map +1 -1
- package/dist/src/platforms/discordbot/commands/thread.d.ts +14 -0
- package/dist/src/platforms/discordbot/commands/thread.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/commands/thread.js +40 -0
- package/dist/src/platforms/discordbot/commands/thread.js.map +1 -1
- package/dist/src/platforms/discordbot/index.d.ts +1 -1
- package/dist/src/platforms/discordbot/index.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/index.js.map +1 -1
- package/dist/src/platforms/discordbot/types.d.ts +68 -17
- package/dist/src/platforms/discordbot/types.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/types.js +20 -16
- package/dist/src/platforms/discordbot/types.js.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +4 -32
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/sync-state-store.d.ts +11 -0
- package/dist/src/platforms/kakaotalk/sync-state-store.d.ts.map +1 -0
- package/dist/src/platforms/kakaotalk/sync-state-store.js +89 -0
- package/dist/src/platforms/kakaotalk/sync-state-store.js.map +1 -0
- package/docs/content/docs/cli/discordbot.mdx +60 -27
- package/docs/content/docs/sdk/discordbot.mdx +101 -3
- package/e2e/README.md +3 -1
- package/e2e/config.ts +1 -1
- package/e2e/discordbot.e2e.test.ts +206 -11
- package/package.json +1 -1
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +57 -11
- package/skills/agent-discordbot/references/authentication.md +13 -11
- package/skills/agent-discordbot/references/common-patterns.md +36 -8
- package/skills/agent-imessage/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-telegrambot/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-webexbot/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/discordbot/client-message.ts +91 -0
- package/src/platforms/discordbot/client-threads.test.ts +177 -0
- package/src/platforms/discordbot/client-threads.ts +69 -0
- package/src/platforms/discordbot/client.test.ts +277 -1
- package/src/platforms/discordbot/client.ts +60 -40
- package/src/platforms/discordbot/commands/file.test.ts +242 -90
- package/src/platforms/discordbot/commands/file.ts +78 -36
- package/src/platforms/discordbot/commands/message.test.ts +183 -26
- package/src/platforms/discordbot/commands/message.ts +13 -61
- package/src/platforms/discordbot/commands/reaction.test.ts +73 -1
- package/src/platforms/discordbot/commands/reaction.ts +46 -0
- package/src/platforms/discordbot/commands/shared.test.ts +98 -0
- package/src/platforms/discordbot/commands/shared.ts +67 -0
- package/src/platforms/discordbot/commands/thread.test.ts +58 -1
- package/src/platforms/discordbot/commands/thread.ts +58 -0
- package/src/platforms/discordbot/index.ts +3 -0
- package/src/platforms/discordbot/types.test.ts +46 -0
- package/src/platforms/discordbot/types.ts +44 -19
- package/src/platforms/kakaotalk/client-listener-integration.test.ts +21 -3
- package/src/platforms/kakaotalk/client.test.ts +42 -4
- package/src/platforms/kakaotalk/client.ts +4 -39
- package/src/platforms/kakaotalk/sync-state-store.test.ts +197 -0
- 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
|
|
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
|
|
91
|
-
agent-discordbot auth set alert-bot-token --bot alert
|
|
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
|
|
109
|
-
|
|
|
110
|
-
| Send Messages
|
|
111
|
-
|
|
|
112
|
-
|
|
|
113
|
-
|
|
|
114
|
-
|
|
|
115
|
-
|
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
|
281
|
-
|
|
|
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
|
|
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
|
-
//
|
|
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
|
-
| `
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
214
|
+
threadId,
|
|
180
215
|
])
|
|
181
216
|
expect(replyResult.exitCode).toBe(0)
|
|
182
217
|
|
|
183
|
-
const reply = parseJSON<{ id: string;
|
|
218
|
+
const reply = parseJSON<{ id: string; channel_id: string }>(replyResult.stdout)
|
|
184
219
|
expect(reply?.id).toBeTruthy()
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
},
|
|
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.
|
|
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.
|
|
4
|
+
version: 2.39.0
|
|
5
5
|
allowed-tools: Bash(agent-channeltalk:*)
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|