@copilotkit/channels-discord 0.0.2

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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +308 -0
  3. package/dist/__tests__/ephemeral.test.d.ts +2 -0
  4. package/dist/__tests__/ephemeral.test.d.ts.map +1 -0
  5. package/dist/__tests__/ephemeral.test.js +26 -0
  6. package/dist/__tests__/intents.test.d.ts +2 -0
  7. package/dist/__tests__/intents.test.d.ts.map +1 -0
  8. package/dist/__tests__/intents.test.js +8 -0
  9. package/dist/__tests__/modal-submit.test.d.ts +2 -0
  10. package/dist/__tests__/modal-submit.test.d.ts.map +1 -0
  11. package/dist/__tests__/modal-submit.test.js +26 -0
  12. package/dist/__tests__/reactions.test.d.ts +2 -0
  13. package/dist/__tests__/reactions.test.d.ts.map +1 -0
  14. package/dist/__tests__/reactions.test.js +87 -0
  15. package/dist/adapter.d.ts +118 -0
  16. package/dist/adapter.d.ts.map +1 -0
  17. package/dist/adapter.js +567 -0
  18. package/dist/adapter.test.d.ts +2 -0
  19. package/dist/adapter.test.d.ts.map +1 -0
  20. package/dist/adapter.test.js +343 -0
  21. package/dist/auto-close-streaming.d.ts +61 -0
  22. package/dist/auto-close-streaming.d.ts.map +1 -0
  23. package/dist/auto-close-streaming.js +276 -0
  24. package/dist/auto-close-streaming.test.d.ts +2 -0
  25. package/dist/auto-close-streaming.test.d.ts.map +1 -0
  26. package/dist/auto-close-streaming.test.js +149 -0
  27. package/dist/built-in-context.d.ts +20 -0
  28. package/dist/built-in-context.d.ts.map +1 -0
  29. package/dist/built-in-context.js +30 -0
  30. package/dist/built-in-context.test.d.ts +2 -0
  31. package/dist/built-in-context.test.d.ts.map +1 -0
  32. package/dist/built-in-context.test.js +21 -0
  33. package/dist/built-in-tools.d.ts +4 -0
  34. package/dist/built-in-tools.d.ts.map +1 -0
  35. package/dist/built-in-tools.js +27 -0
  36. package/dist/built-in-tools.test.d.ts +2 -0
  37. package/dist/built-in-tools.test.d.ts.map +1 -0
  38. package/dist/built-in-tools.test.js +30 -0
  39. package/dist/chunked-message-stream.d.ts +94 -0
  40. package/dist/chunked-message-stream.d.ts.map +1 -0
  41. package/dist/chunked-message-stream.js +203 -0
  42. package/dist/chunked-message-stream.test.d.ts +2 -0
  43. package/dist/chunked-message-stream.test.d.ts.map +1 -0
  44. package/dist/chunked-message-stream.test.js +384 -0
  45. package/dist/commands.d.ts +29 -0
  46. package/dist/commands.d.ts.map +1 -0
  47. package/dist/commands.js +193 -0
  48. package/dist/commands.test.d.ts +2 -0
  49. package/dist/commands.test.d.ts.map +1 -0
  50. package/dist/commands.test.js +281 -0
  51. package/dist/conversation-store.d.ts +45 -0
  52. package/dist/conversation-store.d.ts.map +1 -0
  53. package/dist/conversation-store.js +78 -0
  54. package/dist/conversation-store.test.d.ts +2 -0
  55. package/dist/conversation-store.test.d.ts.map +1 -0
  56. package/dist/conversation-store.test.js +106 -0
  57. package/dist/discord-listener.d.ts +88 -0
  58. package/dist/discord-listener.d.ts.map +1 -0
  59. package/dist/discord-listener.js +118 -0
  60. package/dist/discord-listener.test.d.ts +2 -0
  61. package/dist/discord-listener.test.d.ts.map +1 -0
  62. package/dist/discord-listener.test.js +361 -0
  63. package/dist/download-files.d.ts +94 -0
  64. package/dist/download-files.d.ts.map +1 -0
  65. package/dist/download-files.js +145 -0
  66. package/dist/download-files.test.d.ts +2 -0
  67. package/dist/download-files.test.d.ts.map +1 -0
  68. package/dist/download-files.test.js +181 -0
  69. package/dist/event-renderer.d.ts +36 -0
  70. package/dist/event-renderer.d.ts.map +1 -0
  71. package/dist/event-renderer.js +270 -0
  72. package/dist/event-renderer.test.d.ts +2 -0
  73. package/dist/event-renderer.test.d.ts.map +1 -0
  74. package/dist/event-renderer.test.js +160 -0
  75. package/dist/index.d.ts +24 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +17 -0
  78. package/dist/interaction.d.ts +11 -0
  79. package/dist/interaction.d.ts.map +1 -0
  80. package/dist/interaction.js +138 -0
  81. package/dist/interaction.test.d.ts +2 -0
  82. package/dist/interaction.test.d.ts.map +1 -0
  83. package/dist/interaction.test.js +120 -0
  84. package/dist/markdown.d.ts +7 -0
  85. package/dist/markdown.d.ts.map +1 -0
  86. package/dist/markdown.js +66 -0
  87. package/dist/markdown.test.d.ts +2 -0
  88. package/dist/markdown.test.d.ts.map +1 -0
  89. package/dist/markdown.test.js +47 -0
  90. package/dist/message-stream.d.ts +45 -0
  91. package/dist/message-stream.d.ts.map +1 -0
  92. package/dist/message-stream.js +76 -0
  93. package/dist/message-stream.test.d.ts +2 -0
  94. package/dist/message-stream.test.d.ts.map +1 -0
  95. package/dist/message-stream.test.js +162 -0
  96. package/dist/pending-interactions.d.ts +26 -0
  97. package/dist/pending-interactions.d.ts.map +1 -0
  98. package/dist/pending-interactions.js +50 -0
  99. package/dist/pending-interactions.test.d.ts +2 -0
  100. package/dist/pending-interactions.test.d.ts.map +1 -0
  101. package/dist/pending-interactions.test.js +73 -0
  102. package/dist/render/budget.d.ts +28 -0
  103. package/dist/render/budget.d.ts.map +1 -0
  104. package/dist/render/budget.js +49 -0
  105. package/dist/render/budget.test.d.ts +2 -0
  106. package/dist/render/budget.test.d.ts.map +1 -0
  107. package/dist/render/budget.test.js +59 -0
  108. package/dist/render/components-v2.d.ts +16 -0
  109. package/dist/render/components-v2.d.ts.map +1 -0
  110. package/dist/render/components-v2.js +396 -0
  111. package/dist/render/components-v2.test.d.ts +2 -0
  112. package/dist/render/components-v2.test.d.ts.map +1 -0
  113. package/dist/render/components-v2.test.js +395 -0
  114. package/dist/render/modal.d.ts +12 -0
  115. package/dist/render/modal.d.ts.map +1 -0
  116. package/dist/render/modal.js +44 -0
  117. package/dist/types.d.ts +16 -0
  118. package/dist/types.d.ts.map +1 -0
  119. package/dist/types.js +4 -0
  120. package/dist/types.test.d.ts +2 -0
  121. package/dist/types.test.d.ts.map +1 -0
  122. package/dist/types.test.js +12 -0
  123. package/package.json +56 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) Atai Barkai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,308 @@
1
+ # @copilotkit/channels-discord
2
+
3
+ The **Discord `PlatformAdapter`** for [`@copilotkit/channels`](../channels). It connects a
4
+ Discord application to any AG-UI agent: ingress via discord.js (Gateway), egress
5
+ as Components V2 rendered from the `@copilotkit/channels-ui` JSX vocabulary, plus text
6
+ streaming, opaque-id interactions, and HITL.
7
+
8
+ You write your UI as JSX once (`@copilotkit/channels-ui`) and drive the bot with
9
+ `@copilotkit/channels`; this package is the only one that talks to Discord.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ pnpm add @copilotkit/channels-discord @copilotkit/channels @copilotkit/channels-ui
15
+ ```
16
+
17
+ ## Quickstart
18
+
19
+ ```ts
20
+ import { createBot } from "@copilotkit/channels";
21
+ import {
22
+ discord,
23
+ defaultDiscordTools,
24
+ defaultDiscordContext,
25
+ } from "@copilotkit/channels-discord";
26
+
27
+ const bot = createBot({
28
+ adapters: [
29
+ discord({
30
+ botToken: process.env.DISCORD_BOT_TOKEN!, // Bot token — Gateway + REST
31
+ appId: process.env.DISCORD_APP_ID!, // Application ID for command registration
32
+ guildId: process.env.DISCORD_GUILD_ID, // Optional: instant guild-scoped dev commands
33
+ }),
34
+ ],
35
+ agent: (threadId) => makeAgent(threadId),
36
+ tools: [...defaultDiscordTools, ...appTools], // lookup_discord_user + your tools
37
+ context: [...defaultDiscordContext, ...appContext], // tagging/formatting/thread guidance
38
+ commands: [
39
+ {
40
+ name: "triage",
41
+ description: "Summarize the thread and propose issues.",
42
+ async handler({ thread, text }) {
43
+ await thread.runAgent({ prompt: `Triage: ${text}` });
44
+ },
45
+ },
46
+ ],
47
+ });
48
+
49
+ bot.onMention(({ thread }) => thread.runAgent());
50
+
51
+ await bot.start();
52
+ ```
53
+
54
+ `discord(opts)` returns a `DiscordAdapter`. The adapter connects via the
55
+ Discord Gateway (WebSocket) — no public URL required. The listener pre-filters
56
+ ingress to the turns the bot should answer (@-mentions in guild channels and
57
+ DMs), so a single `onMention` handler covers most use cases.
58
+
59
+ ### Required env
60
+
61
+ | Var | Purpose |
62
+ | ------------------- | ------------------------------------------------------------------------ |
63
+ | `DISCORD_BOT_TOKEN` | Bot token for Gateway login and REST calls. |
64
+ | `DISCORD_APP_ID` | Application ID used when registering slash commands. |
65
+ | `DISCORD_GUILD_ID` | _(Optional)_ Guild ID for instant per-guild command registration in dev. |
66
+
67
+ > **Global commands** (no `guildId`) propagate across Discord in ~1 hour.
68
+ > **Guild-scoped commands** (with `guildId`) register instantly — use them
69
+ > during development and switch to global for production.
70
+
71
+ ### Privileged intents
72
+
73
+ The adapter requests **two** privileged gateway intents — `MessageContent` and
74
+ `GuildMembers`. Both must be enabled in the
75
+ [Discord Developer Portal](https://discord.com/developers/applications)
76
+ (your application → Bot → Privileged Gateway Intents) or Gateway login is
77
+ rejected.
78
+
79
+ > - **Message Content Intent** — without it the Gateway delivers messages with
80
+ > an empty `content` string and the bot cannot read what users write.
81
+ > - **Server Members Intent** (`GuildMembers`) — backs member search, which
82
+ > powers `lookup_discord_user` / `thread.lookupUser`. Without it those
83
+ > lookups fail and login is rejected.
84
+
85
+ ### Reaction intent and partials
86
+
87
+ The adapter also requests `GuildMessageReactions` and enables the
88
+ `Partials.Message` and `Partials.Reaction` partials:
89
+
90
+ > - **`GuildMessageReactions`** — a **non-privileged** gateway intent; no
91
+ > toggle is needed in the Discord Developer Portal (unlike `MessageContent`
92
+ > and `GuildMembers`). Required to receive reaction events in guild channels.
93
+ > - **`DirectMessageReactions`** — also non-privileged; required to receive
94
+ > reaction events in DMs. Discord.js v14 treats guild and DM reactions as
95
+ > separate intents — both are needed if the bot operates in DMs.
96
+ > - **`Partials.Message` + `Partials.Reaction`** — Discord only includes full
97
+ > message objects in the reaction event payload when the message is already
98
+ > in the client's in-memory cache. For any message that was sent before the
99
+ > bot started (or was evicted from cache), the payload arrives as a _partial_.
100
+ > Enabling these two partials lets the listener fetch the full object on
101
+ > demand via `reaction.fetch()` / `reaction.message.fetch()`, so reactions
102
+ > on older messages are not silently dropped.
103
+
104
+ ## What it provides
105
+
106
+ ### JSX → Components V2 rendering
107
+
108
+ `renderDiscordMessage(ir)` translates the `@copilotkit/channels-ui` vocabulary to a
109
+ ready-to-send Discord Components V2 payload (`{ components, flags }`) with the
110
+ `IS_COMPONENTS_V2` flag (`MessageFlags.IsComponentsV2`) set. It builds on
111
+ `renderComponents(ir)`, the lower-level building block, which returns a bare
112
+ `ContainerBuilder` with no flag. The entire message is wrapped in a single
113
+ `Container`; child nodes map as follows:
114
+
115
+ | bot-ui element | Discord output |
116
+ | ---------------------- | ------------------------------------------------------- |
117
+ | `Message` | `Container` (accent color → `setAccentColor`) |
118
+ | `Header` | `TextDisplay` with `# ` prefix |
119
+ | `Section` / `Markdown` | `TextDisplay` |
120
+ | `Fields` | `TextDisplay` — each field as a **bold-label** line |
121
+ | `Context` | `TextDisplay` — each part as a `-# subtext` line |
122
+ | `Actions` | One or more `ActionRow`s |
123
+ | `Button` | `Button` (custom_id = minted opaque `ck:` id) |
124
+ | `Select` | `StringSelect` (custom_id = minted opaque `ck:` id) |
125
+ | `Image` | `MediaGallery` with a single item |
126
+ | `Divider` | `Separator` |
127
+ | `Table` | `TextDisplay` — fenced code block via `discordMarkdown` |
128
+
129
+ ### Per-element budget
130
+
131
+ Discord caps every element. The renderer degrades by truncate-with-overflow /
132
+ clamp — it never silently drops content. Limits live in `DISCORD_LIMITS`:
133
+
134
+ | Limit | Value | Element |
135
+ | ---------------------- | ----- | ------------------------------------- |
136
+ | `componentsPerMessage` | 40 | total (nested) components per message |
137
+ | `actionRows` | 5 | action rows per message |
138
+ | `buttonsPerRow` | 5 | buttons per action row |
139
+ | `selectOptions` | 25 | options per string select |
140
+ | `textDisplayChars` | 2000 | chars per TextDisplay |
141
+ | `buttonLabel` | 80 | button label chars |
142
+ | `customId` | 100 | `custom_id` chars |
143
+ | `headerText` | 256 | header line chars (`# ` TextDisplay) |
144
+
145
+ ### Streaming
146
+
147
+ `thread.stream(...)` posts a plain-text placeholder and edits it in place via
148
+ `ChunkedMessageStream`: throttled `message.edit` calls at ~1100 ms intervals,
149
+ 2000-char chunking, mid-stream bracket auto-close, and Markdown →
150
+ Discord-flavored Markdown translation so the in-flight message always renders.
151
+
152
+ ### Interactions (ack-first)
153
+
154
+ Every `interactionCreate` event (button click, string select) is immediately
155
+ acknowledged with `deferUpdate` (within the **≤3s** deadline,
156
+ `ackDeadlineMs = 3000`). `decodeInteraction` then extracts the opaque minted
157
+ id (`ck:…`) from the `custom_id`, and hands an `InteractionEvent` to the
158
+ engine. The token carries only the opaque id — no props or secrets. Unrelated
159
+ clicks decode to events the bot harmlessly ignores.
160
+
161
+ ### Human-in-the-loop
162
+
163
+ Use `thread.awaitChoice(<Picker .../>)` to post an interactive message and
164
+ block until a click resolves it; the resolved value is the clicked control's
165
+ value. Agent interrupts (`on_interrupt`) are captured by the run renderer and
166
+ dispatched to your `onInterrupt` handler, which posts a picker; the click
167
+ resumes the agent via `thread.resume(value)`.
168
+
169
+ ### Modals
170
+
171
+ `openModal(view)` opens a Discord modal in response to a button click or slash
172
+ command. The call must happen **before any other response** (within Discord's
173
+ 3-second acknowledgement window) — open the modal first, then do long-running
174
+ work in a follow-up message.
175
+
176
+ > **Validation re-open is not supported on Discord.** When a user submits a
177
+ > modal, a `bot.onModalSubmit` handler may return `{ errors }`, but Discord has
178
+ > no API to re-open the same modal with per-field validation errors (unlike
179
+ > Slack's `response_action: "errors"` mechanic). The `{ errors }` result is
180
+ > ignored by the adapter — the modal is acknowledged with `deferUpdate`
181
+ > regardless. Validate inputs before calling `openModal`, or post a follow-up
182
+ > message to report any submission errors.
183
+ >
184
+ > Only `TextInput` fields are supported in Discord modals. `ModalSelect` and
185
+ > `RadioButtons` elements are rejected at render time with a `ModalRenderError`
186
+ > (which `openModal` surfaces as `{ ok: false, error }`).
187
+
188
+ ### Ephemeral messages
189
+
190
+ Discord ephemeral messages are interaction-scoped — Discord only supports them
191
+ as the initial response to a button click or slash command, and they cannot be
192
+ sent outside that 3-second window. For this reason `supportsEphemeral` is
193
+ advertised as `false`.
194
+
195
+ `thread.postEphemeral(user, ui, { fallbackToDM: true })` works around this by
196
+ sending the message as a **DM** to the target user when native ephemeral is
197
+ unavailable. The result carries `{ ok: true, usedFallback: true }` so callers
198
+ can detect that a DM was used instead of an in-channel ephemeral.
199
+
200
+ With `{ fallbackToDM: false }` and no live interaction, `postEphemeral` returns
201
+ `null` (the documented no-fallback sentinel).
202
+
203
+ > **Future enhancement:** native interaction-ephemeral follow-up (calling
204
+ > `interaction.followUp({ ephemeral: true, … })` within the ack window) is a
205
+ > planned addition. Once plumbed, `supportsEphemeral` will be upgraded to `true`
206
+ > for interaction contexts and `usedFallback` will return `false`.
207
+
208
+ ### Typing indicator and reactions
209
+
210
+ The adapter supports both Discord-native capabilities:
211
+
212
+ - **Typing indicator** — `channel.sendTyping()` is called at the start of each
213
+ run, giving users immediate feedback.
214
+ - **Reactions** — `supportsReactions: true` is advertised; the engine can add
215
+ emoji reactions during processing.
216
+
217
+ ### Sender-profile resolution
218
+
219
+ The adapter resolves each turn's Discord user id to a `PlatformUser`
220
+ (`{ id, name?, handle? }`), cached per id. Note that Discord bots cannot read
221
+ user email addresses — `PlatformUser.email` is always `undefined` on this
222
+ platform. Inbound file attachments can be downloaded and delivered to the agent
223
+ as multimodal content parts (`buildFileContentParts`); a tool can post a file
224
+ back out via `thread.postFile(...)`.
225
+
226
+ ### Built-ins
227
+
228
+ - `defaultDiscordTools` — ships `lookup_discord_user` so the agent can resolve
229
+ a name/handle to a `<@USERID>` mention. Spread into `tools`.
230
+ - `defaultDiscordContext` — tagging procedure, Discord Markdown formatting
231
+ guidance, and the Discord channel/thread conversation model. Spread into
232
+ `context`.
233
+
234
+ ## Tool context
235
+
236
+ Tools receive the single shared `BotToolContext` from `@copilotkit/channels`
237
+ (`{ thread, message?, user?, signal?, platform }`) and reach Discord power
238
+ only through capability-gated `thread` methods, which this adapter backs:
239
+
240
+ - `thread.getMessages()` — the current channel's recent messages (via
241
+ `channel.messages.fetch`), each a `ThreadMessage` (`{ user?, text, ts?,
242
+ isBot? }`).
243
+ - `thread.lookupUser(query)` — resolve a name/handle to a `PlatformUser` by
244
+ searching guild members.
245
+ - `thread.postFile({ bytes, filename, title?, altText? })` — upload a file
246
+ into the channel as an attachment.
247
+
248
+ This keeps tools portable: define them with `defineBotTool({...})` and they
249
+ work against any adapter that advertises the same capabilities.
250
+
251
+ ## Slash commands
252
+
253
+ Slash commands are registered up front on `bot.start()` via `registerCommands`.
254
+ When `guildId` is set they register to that guild instantly; without it they
255
+ register globally and take ~1 hour to propagate. Register handlers with
256
+ `bot.onCommand`:
257
+
258
+ ```ts
259
+ bot.onCommand({
260
+ name: "triage",
261
+ description: "Summarize the thread and propose issues.",
262
+ options: {
263
+ // Optional JSON Schema for native Discord slash-command options.
264
+ // Generates typed Discord option descriptors via jsonSchemaToDiscordOptions.
265
+ },
266
+ async handler({ thread, text, user, rawOptions }) {
267
+ await thread.runAgent({ prompt: `Triage: ${text}` });
268
+ },
269
+ });
270
+ ```
271
+
272
+ Unlike Slack, commands are registered programmatically — there is no manifest
273
+ file. Discord delivers native structured option values via `rawOptions` when the
274
+ command's `options` schema is provided; args also arrive flattened as free text
275
+ in `ctx.text`.
276
+
277
+ > **App setup** (OAuth scopes, bot permissions, invite URL) is done via the
278
+ > Discord Developer Portal and the OAuth2 invite flow. A complete wiring example
279
+ > lives in [`examples/slack`](../../examples/slack) — one bot app that runs
280
+ > Slack and/or Discord depending on which secrets you set.
281
+
282
+ ## What's NOT in v1
283
+
284
+ - **Modal limitations** — text-input modals are supported (up to 5 fields).
285
+ `ModalSelect` and `RadioButtons` elements are rejected at render time.
286
+ Validation re-open (`response_action: "errors"`) is not supported — Discord
287
+ has no API for it; submit errors should be posted as a follow-up message
288
+ instead.
289
+ - OAuth / multi-guild install (single bot token only)
290
+ - Durable (Redis/DB) `ActionStore` — in-memory only; actions expire on restart
291
+ - Proactive posting (bot replies only to turns it's part of)
292
+ - Auto-sharding (single `Client` instance)
293
+ - Native interaction-ephemeral (`supportsEphemeral` is `false`; use
294
+ `thread.postEphemeral(user, ui, { fallbackToDM: true })` for a DM-based
295
+ workaround)
296
+
297
+ ## Exports
298
+
299
+ `discord`, `DiscordAdapter`, `DiscordAdapterOptions`; `DiscordConversationStore`;
300
+ `attachDiscordListener`, `ListenerConfig`, `ClientLike`, `IncomingCommandRaw`;
301
+ `createRunRenderer`, `ChannelLike`; `decodeInteraction`; `conversationKeyOf`,
302
+ `ReplyTarget`, `IncomingTurn`; `renderComponents`, `renderDiscordMessage`,
303
+ `DISCORD_LIMITS`; `discordMarkdown`; `MessageStream`, `MessageStreamConfig`;
304
+ `ChunkedMessageStream`, `ChunkedMessageStreamConfig`; `autoCloseOpenMarkdown`;
305
+ `registerCommands`, `jsonSchemaToDiscordOptions`; `buildFileContentParts`,
306
+ `DiscordAttachmentRef`, `AgentContentPart`, `FileDeliveryConfig`;
307
+ `defaultDiscordContext`, `discordTaggingContext`, `discordFormattingContext`,
308
+ `discordConversationModelContext`; `lookupDiscordUserTool`, `defaultDiscordTools`.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ephemeral.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ephemeral.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/ephemeral.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,26 @@
1
+ // packages/channels-discord/src/__tests__/ephemeral.test.ts
2
+ import { it, expect, vi } from "vitest";
3
+ import { DiscordAdapter } from "../adapter.js";
4
+ function makeAdapter() {
5
+ const send = vi.fn().mockResolvedValue({ id: "dm1" });
6
+ const client = {
7
+ users: {
8
+ fetch: vi.fn().mockResolvedValue({
9
+ createDM: vi.fn().mockResolvedValue({ id: "dm-channel-1", send }),
10
+ }),
11
+ },
12
+ };
13
+ const adapter = new DiscordAdapter({ botToken: "t", appId: "app" }, { client: client });
14
+ return { adapter, send, client };
15
+ }
16
+ it("DM-falls-back when fallbackToDM=true (usedFallback=true)", async () => {
17
+ const { adapter, send } = makeAdapter();
18
+ const res = await adapter.postEphemeral({ channelId: "C1" }, { id: "U1" }, [{ type: "text", props: { value: "hi" } }], { fallbackToDM: true });
19
+ expect(res).toMatchObject({ ok: true, usedFallback: true });
20
+ expect(send).toHaveBeenCalled();
21
+ });
22
+ it("returns null when fallbackToDM=false and no live interaction", async () => {
23
+ const { adapter } = makeAdapter();
24
+ const res = await adapter.postEphemeral({ channelId: "C1" }, { id: "U1" }, [{ type: "text", props: { value: "hi" } }], { fallbackToDM: false });
25
+ expect(res).toBeNull();
26
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=intents.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intents.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/intents.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,8 @@
1
+ import { it, expect } from "vitest";
2
+ import { GatewayIntentBits, Partials } from "discord.js";
3
+ import { DISCORD_DEFAULT_INTENTS, DISCORD_DEFAULT_PARTIALS, } from "../adapter.js";
4
+ it("requests the reaction intent and partials", () => {
5
+ expect(DISCORD_DEFAULT_INTENTS).toContain(GatewayIntentBits.GuildMessageReactions);
6
+ expect(DISCORD_DEFAULT_PARTIALS).toContain(Partials.Message);
7
+ expect(DISCORD_DEFAULT_PARTIALS).toContain(Partials.Reaction);
8
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=modal-submit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modal-submit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/modal-submit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,26 @@
1
+ // packages/channels-discord/src/__tests__/modal-submit.test.ts
2
+ import { it, expect } from "vitest";
3
+ import { decodeModalSubmit } from "../interaction.js";
4
+ it("decodes a modal submission's text fields by custom_id", () => {
5
+ const interaction = {
6
+ customId: "triage",
7
+ channelId: "C1",
8
+ guildId: "G1",
9
+ user: { id: "U1", username: "ada" },
10
+ fields: {
11
+ fields: new Map([
12
+ ["summary", { customId: "summary", value: "boom" }],
13
+ ["detail", { customId: "detail", value: "ctx" }],
14
+ ]),
15
+ },
16
+ };
17
+ const evt = decodeModalSubmit(interaction);
18
+ expect(evt).toMatchObject({
19
+ callbackId: "triage",
20
+ values: { summary: "boom", detail: "ctx" },
21
+ user: { id: "U1", name: "ada" },
22
+ conversationKey: "C1",
23
+ replyTarget: { channelId: "C1", guildId: "G1" },
24
+ platform: "discord",
25
+ });
26
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=reactions.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactions.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/reactions.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { emoji } from "@copilotkit/channels-ui";
3
+ import { decodeReaction } from "../interaction.js";
4
+ import { DiscordAdapter } from "../adapter.js";
5
+ it("decodes a unicode reaction add", () => {
6
+ const evt = decodeReaction({
7
+ emoji: { name: "👍", id: null },
8
+ message: { id: "m1", channelId: "C1", guildId: "G1" },
9
+ }, { id: "U1", username: "ada", bot: false }, true);
10
+ expect(evt).toMatchObject({
11
+ rawEmoji: "👍",
12
+ added: true,
13
+ user: { id: "U1", name: "ada" },
14
+ conversationKey: "C1",
15
+ messageId: "m1",
16
+ replyTarget: { channelId: "C1", guildId: "G1" },
17
+ });
18
+ });
19
+ it("encodes a custom emoji as name:id passthrough", () => {
20
+ const evt = decodeReaction({
21
+ emoji: { name: "blob", id: "999" },
22
+ message: { id: "m2", channelId: "C2" },
23
+ }, { id: "U2", bot: false }, false);
24
+ expect(evt.rawEmoji).toBe("blob:999");
25
+ expect(evt.added).toBe(false);
26
+ });
27
+ describe("addReaction / removeReaction egress", () => {
28
+ function makeAdapter() {
29
+ const botId = "BOT1";
30
+ const react = vi.fn().mockResolvedValue(undefined);
31
+ const removeUser = vi.fn().mockResolvedValue(undefined);
32
+ const msg = {
33
+ react,
34
+ reactions: {
35
+ cache: new Map([
36
+ ["👍", { users: { remove: removeUser } }],
37
+ ["custom", { users: { remove: removeUser } }],
38
+ // Discord keys hearts by the BARE codepoint (no trailing U+FE0F).
39
+ ["❤", { users: { remove: removeUser } }],
40
+ ]),
41
+ },
42
+ };
43
+ const client = {
44
+ user: { id: botId },
45
+ channels: {
46
+ fetch: vi.fn().mockResolvedValue({
47
+ id: "C1",
48
+ send: vi.fn(),
49
+ messages: {
50
+ fetch: vi.fn().mockResolvedValue(msg),
51
+ },
52
+ }),
53
+ },
54
+ };
55
+ const adapter = new DiscordAdapter({ botToken: "t", appId: "app" }, { client: client });
56
+ return { adapter, react, removeUser, botId, msg };
57
+ }
58
+ const target = { channelId: "C1" };
59
+ const messageRef = { id: "M1", channelId: "C1" };
60
+ it("addReaction resolves thumbs_up to 👍 and calls msg.react", async () => {
61
+ const { adapter, react } = makeAdapter();
62
+ const res = await adapter.addReaction(target, messageRef, "thumbs_up");
63
+ expect(res).toEqual({ ok: true });
64
+ expect(react).toHaveBeenCalledWith("👍");
65
+ });
66
+ it("addReaction passes unknown emoji through raw", async () => {
67
+ const { adapter, react } = makeAdapter();
68
+ const res = await adapter.addReaction(target, messageRef, "some_custom_emoji");
69
+ expect(res).toEqual({ ok: true });
70
+ expect(react).toHaveBeenCalledWith("some_custom_emoji");
71
+ });
72
+ it("removeReaction removes the bot's own id via reactions.cache.get(token)?.users.remove", async () => {
73
+ const { adapter, removeUser, botId } = makeAdapter();
74
+ const res = await adapter.removeReaction(target, messageRef, "thumbs_up");
75
+ expect(res).toEqual({ ok: true });
76
+ expect(removeUser).toHaveBeenCalledWith(botId);
77
+ });
78
+ it("removeReaction finds a reaction cached under the bare (VS16-stripped) codepoint", async () => {
79
+ // `emoji.heart` resolves to the qualified "❤️" (U+2764 U+FE0F), but Discord
80
+ // keys the cache by the bare "❤" (U+2764). A VS16-tolerant lookup must still
81
+ // find the reaction and remove the bot's own id.
82
+ const { adapter, removeUser, botId } = makeAdapter();
83
+ const res = await adapter.removeReaction(target, messageRef, emoji.heart);
84
+ expect(res).toEqual({ ok: true });
85
+ expect(removeUser).toHaveBeenCalledWith(botId);
86
+ });
87
+ });
@@ -0,0 +1,118 @@
1
+ import { Client, GatewayIntentBits, Partials } from "discord.js";
2
+ import type { PlatformAdapter, SurfaceCapabilities, IngressSink, InteractionEvent, RunRenderer, ReplyTarget as BotReplyTarget, ConversationStore, MessageRef, PlatformUser, UserQuery, CommandSpec, NativePayload } from "@copilotkit/channels";
3
+ import type { BotNode, ThreadMessage, EmojiValue, EphemeralResult } from "@copilotkit/channels-ui";
4
+ import type { RestLike } from "./commands.js";
5
+ export interface DiscordAdapterOptions {
6
+ botToken: string;
7
+ appId: string;
8
+ /** When set, slash commands register to this guild instantly (dev); else global. */
9
+ guildId?: string;
10
+ interruptEventNames?: ReadonlySet<string>;
11
+ }
12
+ /**
13
+ * Default Gateway intents for the bot.
14
+ *
15
+ * - `Guilds`, `GuildMessages`, `MessageContent`, `DirectMessages` — message ingress.
16
+ * - `GuildMembers` — privileged; backs `lookup_discord_user` / `thread.lookupUser`.
17
+ * Must be toggled on in the Discord Developer Portal (Bot → Privileged Gateway Intents).
18
+ * - `GuildMessageReactions` — non-privileged; no portal toggle needed.
19
+ * Required to receive reaction add/remove events in guild channels.
20
+ * - `DirectMessageReactions` — non-privileged; required to receive reaction
21
+ * add/remove events in DMs. Distinct from `GuildMessageReactions` in discord.js v14.
22
+ */
23
+ export declare const DISCORD_DEFAULT_INTENTS: readonly [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.DirectMessages, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.DirectMessageReactions];
24
+ /**
25
+ * Default partials for the bot.
26
+ *
27
+ * - `Channel` — needed to receive DMs.
28
+ * - `Message` + `Reaction` — required to receive reactions on uncached messages.
29
+ * Discord only delivers partial reaction events when the reacted-to message is not
30
+ * in the client's cache; without these partials those events are silently dropped.
31
+ */
32
+ export declare const DISCORD_DEFAULT_PARTIALS: readonly [Partials.Channel, Partials.Message, Partials.Reaction];
33
+ export declare class DiscordAdapter implements PlatformAdapter {
34
+ private readonly opts;
35
+ readonly platform = "discord";
36
+ readonly capabilities: SurfaceCapabilities;
37
+ readonly ackDeadlineMs = 3000;
38
+ private readonly client;
39
+ private readonly rest;
40
+ private readonly store;
41
+ private botUserId;
42
+ private isReady;
43
+ private pendingCommands;
44
+ private readonly userCache;
45
+ /**
46
+ * Tracks live component interactions so a handler can open a modal (which
47
+ * must be the interaction's INITIAL response, within ~3s) before the adapter
48
+ * auto-defers. Constructed in `start()`. Used by `openModal` (Task D4).
49
+ */
50
+ private pending;
51
+ /**
52
+ * Tracks live slash-command interactions (a command's initial response is a
53
+ * reply, not a component update, so it needs its own registry). `openModal`
54
+ * consults this too, so a command handler can `showModal` before the adapter
55
+ * auto-defers. Constructed in `start()`.
56
+ */
57
+ private commandPending;
58
+ constructor(opts: DiscordAdapterOptions, injected?: {
59
+ client?: Client;
60
+ rest?: RestLike;
61
+ });
62
+ registerCommands(commands: readonly CommandSpec[]): void;
63
+ /**
64
+ * Publish the registered commands. Guards against an empty list: an empty
65
+ * `setMyCommands`/`PUT` CLEARS all of the bot's commands, so a race where
66
+ * `ready` fires before commands are stashed must not wipe them.
67
+ */
68
+ private publishCommands;
69
+ start(sink: IngressSink): Promise<void>;
70
+ stop(): Promise<void>;
71
+ render(ir: BotNode[]): import("discord.js").ContainerBuilder;
72
+ post(target: BotReplyTarget, ir: BotNode[]): Promise<MessageRef>;
73
+ update(ref: MessageRef, ir: BotNode[]): Promise<void>;
74
+ stream(target: BotReplyTarget, chunks: AsyncIterable<string>): Promise<MessageRef>;
75
+ delete(ref: MessageRef): Promise<void>;
76
+ createRunRenderer(target: BotReplyTarget): RunRenderer;
77
+ decodeInteraction(raw: unknown): InteractionEvent | undefined;
78
+ lookupUser(q: UserQuery): Promise<PlatformUser | undefined>;
79
+ get conversationStore(): ConversationStore;
80
+ getMessages(target: BotReplyTarget): Promise<ThreadMessage[]>;
81
+ postFile(target: BotReplyTarget, args: {
82
+ bytes: Uint8Array;
83
+ filename: string;
84
+ title?: string;
85
+ altText?: string;
86
+ }): Promise<{
87
+ ok: boolean;
88
+ fileId?: string;
89
+ error?: string;
90
+ }>;
91
+ addReaction(target: BotReplyTarget, messageRef: MessageRef, emoji: EmojiValue): Promise<{
92
+ ok: boolean;
93
+ error?: string;
94
+ }>;
95
+ removeReaction(target: BotReplyTarget, messageRef: MessageRef, emoji: EmojiValue): Promise<{
96
+ ok: boolean;
97
+ error?: string;
98
+ }>;
99
+ postEphemeral(_target: BotReplyTarget, user: PlatformUser | string, ir: BotNode[], opts: {
100
+ fallbackToDM: boolean;
101
+ }): Promise<EphemeralResult | null>;
102
+ renderModal(ir: BotNode[]): NativePayload;
103
+ openModal(_target: BotReplyTarget, triggerId: string, ir: BotNode[]): Promise<{
104
+ ok: boolean;
105
+ error?: string;
106
+ }>;
107
+ resolveUser(userId: string): Promise<PlatformUser>;
108
+ private channelIdOf;
109
+ private fetchSendable;
110
+ /**
111
+ * Fetch the channel's recent messages OLDEST→NEWEST, normalized for the
112
+ * conversation store's history reconstruction. Best-effort: on any fetch
113
+ * error returns [] (bot-slack's fetchHistory does the same).
114
+ */
115
+ private fetchHistory;
116
+ }
117
+ export declare function discord(opts: DiscordAdapterOptions): DiscordAdapter;
118
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}