kimaki 0.4.76 → 0.4.78

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 (162) hide show
  1. package/dist/adapter-rest-boundary.test.js +34 -0
  2. package/dist/agent-model.e2e.test.js +2 -20
  3. package/dist/cli.js +50 -13
  4. package/dist/commands/channel-ref.js +16 -0
  5. package/dist/commands/diff.js +20 -85
  6. package/dist/commands/merge-worktree.js +5 -17
  7. package/dist/commands/new-worktree.js +5 -9
  8. package/dist/commands/permissions.js +77 -11
  9. package/dist/commands/resume.js +5 -9
  10. package/dist/commands/screenshare.js +295 -0
  11. package/dist/commands/session.js +6 -17
  12. package/dist/critique-utils.js +95 -0
  13. package/dist/diff-patch-plugin.js +314 -0
  14. package/dist/discord-bot.js +19 -14
  15. package/dist/discord-js-import-boundary.test.js +62 -0
  16. package/dist/discord-utils.js +44 -0
  17. package/dist/event-stream-real-capture.e2e.test.js +2 -20
  18. package/dist/gateway-proxy.e2e.test.js +2 -5
  19. package/dist/generated/cloudflare/browser.js +17 -0
  20. package/dist/generated/cloudflare/client.js +34 -0
  21. package/dist/generated/cloudflare/commonInputTypes.js +10 -0
  22. package/dist/generated/cloudflare/enums.js +48 -0
  23. package/dist/generated/cloudflare/internal/class.js +47 -0
  24. package/dist/generated/cloudflare/internal/prismaNamespace.js +252 -0
  25. package/dist/generated/cloudflare/internal/prismaNamespaceBrowser.js +222 -0
  26. package/dist/generated/cloudflare/internal/query_compiler_fast_bg.js +135 -0
  27. package/dist/generated/cloudflare/models/bot_api_keys.js +1 -0
  28. package/dist/generated/cloudflare/models/bot_tokens.js +1 -0
  29. package/dist/generated/cloudflare/models/channel_agents.js +1 -0
  30. package/dist/generated/cloudflare/models/channel_directories.js +1 -0
  31. package/dist/generated/cloudflare/models/channel_mention_mode.js +1 -0
  32. package/dist/generated/cloudflare/models/channel_models.js +1 -0
  33. package/dist/generated/cloudflare/models/channel_verbosity.js +1 -0
  34. package/dist/generated/cloudflare/models/channel_worktrees.js +1 -0
  35. package/dist/generated/cloudflare/models/forum_sync_configs.js +1 -0
  36. package/dist/generated/cloudflare/models/global_models.js +1 -0
  37. package/dist/generated/cloudflare/models/ipc_requests.js +1 -0
  38. package/dist/generated/cloudflare/models/part_messages.js +1 -0
  39. package/dist/generated/cloudflare/models/scheduled_tasks.js +1 -0
  40. package/dist/generated/cloudflare/models/session_agents.js +1 -0
  41. package/dist/generated/cloudflare/models/session_events.js +1 -0
  42. package/dist/generated/cloudflare/models/session_models.js +1 -0
  43. package/dist/generated/cloudflare/models/session_start_sources.js +1 -0
  44. package/dist/generated/cloudflare/models/thread_sessions.js +1 -0
  45. package/dist/generated/cloudflare/models/thread_worktrees.js +1 -0
  46. package/dist/generated/cloudflare/models.js +1 -0
  47. package/dist/generated/node/browser.js +17 -0
  48. package/dist/generated/node/client.js +37 -0
  49. package/dist/generated/node/commonInputTypes.js +10 -0
  50. package/dist/generated/node/enums.js +48 -0
  51. package/dist/generated/node/internal/class.js +49 -0
  52. package/dist/generated/node/internal/prismaNamespace.js +252 -0
  53. package/dist/generated/node/internal/prismaNamespaceBrowser.js +222 -0
  54. package/dist/generated/node/models/bot_api_keys.js +1 -0
  55. package/dist/generated/node/models/bot_tokens.js +1 -0
  56. package/dist/generated/node/models/channel_agents.js +1 -0
  57. package/dist/generated/node/models/channel_directories.js +1 -0
  58. package/dist/generated/node/models/channel_mention_mode.js +1 -0
  59. package/dist/generated/node/models/channel_models.js +1 -0
  60. package/dist/generated/node/models/channel_verbosity.js +1 -0
  61. package/dist/generated/node/models/channel_worktrees.js +1 -0
  62. package/dist/generated/node/models/forum_sync_configs.js +1 -0
  63. package/dist/generated/node/models/global_models.js +1 -0
  64. package/dist/generated/node/models/ipc_requests.js +1 -0
  65. package/dist/generated/node/models/part_messages.js +1 -0
  66. package/dist/generated/node/models/scheduled_tasks.js +1 -0
  67. package/dist/generated/node/models/session_agents.js +1 -0
  68. package/dist/generated/node/models/session_events.js +1 -0
  69. package/dist/generated/node/models/session_models.js +1 -0
  70. package/dist/generated/node/models/session_start_sources.js +1 -0
  71. package/dist/generated/node/models/thread_sessions.js +1 -0
  72. package/dist/generated/node/models/thread_worktrees.js +1 -0
  73. package/dist/generated/node/models.js +1 -0
  74. package/dist/interaction-handler.js +10 -0
  75. package/dist/kimaki-digital-twin.e2e.test.js +2 -20
  76. package/dist/message-flags-boundary.test.js +54 -0
  77. package/dist/message-formatting.js +3 -62
  78. package/dist/onboarding-tutorial-plugin.js +1 -1
  79. package/dist/opencode-command.js +129 -0
  80. package/dist/opencode-command.test.js +48 -0
  81. package/dist/opencode-interrupt-plugin.js +19 -1
  82. package/dist/opencode-interrupt-plugin.test.js +0 -5
  83. package/dist/opencode-plugin-loading.e2e.test.js +9 -20
  84. package/dist/opencode-plugin.js +4 -4
  85. package/dist/opencode.js +150 -27
  86. package/dist/patch-text-parser.js +97 -0
  87. package/dist/platform/components-v2.js +20 -0
  88. package/dist/platform/discord-adapter.js +1440 -0
  89. package/dist/platform/discord-routes.js +31 -0
  90. package/dist/platform/message-flags.js +8 -0
  91. package/dist/platform/platform-value.js +41 -0
  92. package/dist/platform/slack-adapter.js +872 -0
  93. package/dist/platform/slack-markdown.js +169 -0
  94. package/dist/platform/types.js +4 -0
  95. package/dist/queue-advanced-e2e-setup.js +265 -0
  96. package/dist/queue-advanced-footer.e2e.test.js +173 -0
  97. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  98. package/dist/queue-advanced-permissions-typing.e2e.test.js +73 -1
  99. package/dist/runtime-lifecycle.e2e.test.js +2 -20
  100. package/dist/session-handler/event-stream-state.js +5 -0
  101. package/dist/session-handler/event-stream-state.test.js +6 -2
  102. package/dist/session-handler/thread-session-runtime.js +32 -2
  103. package/dist/system-message.js +26 -23
  104. package/dist/test-utils.js +16 -0
  105. package/dist/thread-message-queue.e2e.test.js +2 -20
  106. package/dist/utils.js +3 -1
  107. package/dist/voice-message.e2e.test.js +2 -20
  108. package/dist/voice.js +122 -9
  109. package/dist/voice.test.js +17 -2
  110. package/dist/websockify.js +69 -0
  111. package/dist/worktree-lifecycle.e2e.test.js +308 -0
  112. package/package.json +4 -2
  113. package/skills/critique/SKILL.md +17 -0
  114. package/skills/egaki/SKILL.md +35 -0
  115. package/skills/event-sourcing-state/SKILL.md +252 -0
  116. package/skills/goke/SKILL.md +1 -0
  117. package/skills/npm-package/SKILL.md +21 -2
  118. package/skills/playwriter/SKILL.md +1 -1
  119. package/skills/x-articles/SKILL.md +554 -0
  120. package/src/agent-model.e2e.test.ts +4 -19
  121. package/src/cli.ts +60 -13
  122. package/src/commands/diff.ts +25 -99
  123. package/src/commands/merge-worktree.ts +5 -21
  124. package/src/commands/new-worktree.ts +5 -11
  125. package/src/commands/permissions.ts +100 -15
  126. package/src/commands/resume.ts +5 -12
  127. package/src/commands/screenshare.ts +354 -0
  128. package/src/commands/session.ts +6 -23
  129. package/src/critique-utils.ts +139 -0
  130. package/src/discord-bot.ts +20 -15
  131. package/src/discord-utils.ts +53 -0
  132. package/src/event-stream-real-capture.e2e.test.ts +4 -20
  133. package/src/gateway-proxy.e2e.test.ts +2 -5
  134. package/src/interaction-handler.ts +15 -0
  135. package/src/kimaki-digital-twin.e2e.test.ts +2 -21
  136. package/src/message-formatting.ts +3 -68
  137. package/src/onboarding-tutorial-plugin.ts +1 -1
  138. package/src/opencode-command.test.ts +70 -0
  139. package/src/opencode-command.ts +188 -0
  140. package/src/opencode-interrupt-plugin.test.ts +0 -5
  141. package/src/opencode-interrupt-plugin.ts +34 -1
  142. package/src/opencode-plugin-loading.e2e.test.ts +25 -35
  143. package/src/opencode-plugin.ts +5 -4
  144. package/src/opencode.ts +199 -32
  145. package/src/patch-text-parser.ts +107 -0
  146. package/src/queue-advanced-e2e-setup.ts +273 -0
  147. package/src/queue-advanced-footer.e2e.test.ts +211 -0
  148. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  149. package/src/queue-advanced-permissions-typing.e2e.test.ts +92 -0
  150. package/src/runtime-lifecycle.e2e.test.ts +4 -19
  151. package/src/session-handler/event-stream-state.test.ts +6 -2
  152. package/src/session-handler/event-stream-state.ts +5 -0
  153. package/src/session-handler/thread-session-runtime.ts +45 -2
  154. package/src/system-message.ts +26 -23
  155. package/src/test-utils.ts +17 -0
  156. package/src/thread-message-queue.e2e.test.ts +2 -20
  157. package/src/utils.ts +3 -1
  158. package/src/voice-message.e2e.test.ts +3 -20
  159. package/src/voice.test.ts +26 -2
  160. package/src/voice.ts +147 -9
  161. package/src/websockify.ts +101 -0
  162. package/src/worktree-lifecycle.e2e.test.ts +391 -0
@@ -0,0 +1,1440 @@
1
+ // Discord adapter for Kimaki's platform interface.
2
+ // Owns discord.js client lifecycle, inbound event normalization, thread/message
3
+ // operations, and Discord-specific markdown splitting/rendering.
4
+ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, Client, Events, GatewayIntentBits, MessageFlags, GuildMember, ModalBuilder, FileUploadBuilder, LabelBuilder, Partials, PermissionsBitField, Routes, StringSelectMenuBuilder, TextInputBuilder, TextInputStyle, ThreadChannel, } from 'discord.js';
5
+ import { getDiscordRestApiUrl } from '../discord-urls.js';
6
+ import { escapeBackticksInCodeBlocks, splitMarkdownForDiscord, } from '../discord-utils.js';
7
+ import { splitTablesFromMarkdown } from '../format-tables.js';
8
+ import { limitHeadingDepth } from '../limit-heading-depth.js';
9
+ import { unnestCodeBlocksFromLists } from '../unnest-code-blocks.js';
10
+ import { isTextMimeType } from '../message-formatting.js';
11
+ import { createLogger, LogPrefix } from '../logger.js';
12
+ import { processImage } from '../image-utils.js';
13
+ import { FetchError } from '../errors.js';
14
+ import * as errore from 'errore';
15
+ import { processVoiceAttachment } from '../voice-handler.js';
16
+ import { handleHtmlActionButton } from '../html-actions.js';
17
+ // ── Permission helpers (discord.js-specific, used by adapter + voice-handler) ──
18
+ /**
19
+ * Check if a guild member has permission to use the Kimaki bot.
20
+ * Returns true for: server owner, Administrator, Manage Server, or "Kimaki" role.
21
+ * Returns false if member has the "no-kimaki" role (overrides all).
22
+ */
23
+ export function hasKimakiBotPermission(member, guild) {
24
+ if (!member) {
25
+ return false;
26
+ }
27
+ if (hasRoleByName(member, 'no-kimaki', guild)) {
28
+ return false;
29
+ }
30
+ const memberPermissions = member instanceof GuildMember
31
+ ? member.permissions
32
+ : new PermissionsBitField(BigInt(member.permissions));
33
+ const ownerId = member instanceof GuildMember ? member.guild.ownerId : guild?.ownerId;
34
+ const memberId = member instanceof GuildMember ? member.id : member.user.id;
35
+ const isOwner = ownerId ? memberId === ownerId : false;
36
+ const isAdmin = memberPermissions.has(PermissionsBitField.Flags.Administrator);
37
+ const canManageServer = memberPermissions.has(PermissionsBitField.Flags.ManageGuild);
38
+ const hasKimakiRole = hasRoleByName(member, 'kimaki', guild);
39
+ return isOwner || isAdmin || canManageServer || hasKimakiRole;
40
+ }
41
+ function hasRoleByName(member, roleName, guild) {
42
+ const target = roleName.toLowerCase();
43
+ if (member instanceof GuildMember) {
44
+ return member.roles.cache.some((role) => role.name.toLowerCase() === target);
45
+ }
46
+ if (!guild) {
47
+ return false;
48
+ }
49
+ const roleIds = Array.isArray(member.roles) ? member.roles : [];
50
+ for (const roleId of roleIds) {
51
+ const role = guild.roles.cache.get(roleId);
52
+ if (role?.name.toLowerCase() === target) {
53
+ return true;
54
+ }
55
+ }
56
+ return false;
57
+ }
58
+ /**
59
+ * Check if the member has the "no-kimaki" role that blocks bot access.
60
+ */
61
+ export function hasNoKimakiRole(member) {
62
+ if (!member?.roles?.cache) {
63
+ return false;
64
+ }
65
+ return member.roles.cache.some((role) => role.name.toLowerCase() === 'no-kimaki');
66
+ }
67
+ // ── Discord message content helpers (discord.js-specific) ──────────────
68
+ const contentLogger = createLogger(LogPrefix.FORMATTING);
69
+ function resolveMentions(message) {
70
+ let content = message.content || '';
71
+ for (const [userId, user] of message.mentions.users) {
72
+ const member = message.guild?.members.cache.get(userId);
73
+ const displayName = member?.displayName || user.displayName || user.username;
74
+ content = content.replace(new RegExp(`<@!?${userId}>`, 'g'), `@${displayName}`);
75
+ }
76
+ for (const [roleId, role] of message.mentions.roles) {
77
+ content = content.replace(new RegExp(`<@&${roleId}>`, 'g'), `@${role.name}`);
78
+ }
79
+ for (const [channelId, channel] of message.mentions.channels) {
80
+ const name = 'name' in channel ? channel.name : channelId;
81
+ content = content.replace(new RegExp(`<#${channelId}>`, 'g'), `#${name}`);
82
+ }
83
+ return content;
84
+ }
85
+ async function getTextAttachments(message) {
86
+ const textAttachments = Array.from(message.attachments.values()).filter((attachment) => isTextMimeType(attachment.contentType));
87
+ if (textAttachments.length === 0) {
88
+ return '';
89
+ }
90
+ const textContents = await Promise.all(textAttachments.map(async (attachment) => {
91
+ const response = await errore.tryAsync({
92
+ try: () => fetch(attachment.url),
93
+ catch: (e) => new FetchError({ url: attachment.url, cause: e }),
94
+ });
95
+ if (response instanceof Error) {
96
+ return `<attachment filename="${attachment.name}" error="${response.message}" />`;
97
+ }
98
+ if (!response.ok) {
99
+ return `<attachment filename="${attachment.name}" error="Failed to fetch: ${response.status}" />`;
100
+ }
101
+ const text = await response.text();
102
+ return `<attachment filename="${attachment.name}" mime="${attachment.contentType}">\n${text}\n</attachment>`;
103
+ }));
104
+ return textContents.join('\n\n');
105
+ }
106
+ async function getFileAttachments(message) {
107
+ const fileAttachments = Array.from(message.attachments.values()).filter((attachment) => {
108
+ const contentType = attachment.contentType || '';
109
+ return (contentType.startsWith('image/') || contentType === 'application/pdf');
110
+ });
111
+ if (fileAttachments.length === 0) {
112
+ return [];
113
+ }
114
+ const results = await Promise.all(fileAttachments.map(async (attachment) => {
115
+ const response = await errore.tryAsync({
116
+ try: () => fetch(attachment.url),
117
+ catch: (e) => new FetchError({ url: attachment.url, cause: e }),
118
+ });
119
+ if (response instanceof Error) {
120
+ contentLogger.error(`Error downloading attachment ${attachment.name}:`, response.message);
121
+ return null;
122
+ }
123
+ if (!response.ok) {
124
+ contentLogger.error(`Failed to fetch attachment ${attachment.name}: ${response.status}`);
125
+ return null;
126
+ }
127
+ const rawBuffer = Buffer.from(await response.arrayBuffer());
128
+ const originalMime = attachment.contentType || 'application/octet-stream';
129
+ const { buffer, mime } = await processImage(rawBuffer, originalMime);
130
+ const base64 = buffer.toString('base64');
131
+ const dataUrl = `data:${mime};base64,${base64}`;
132
+ contentLogger.log(`Attachment ${attachment.name}: ${rawBuffer.length} → ${buffer.length} bytes, ${mime}`);
133
+ return {
134
+ type: 'file',
135
+ mime,
136
+ filename: attachment.name,
137
+ url: dataUrl,
138
+ sourceUrl: attachment.url,
139
+ };
140
+ }));
141
+ return results.filter(isTruthy);
142
+ }
143
+ const discordMessageStore = new Map();
144
+ const discordGuildStore = new Map();
145
+ function getDiscordMessageStoreKey({ channelId, messageId, }) {
146
+ return `${channelId}:${messageId}`;
147
+ }
148
+ function buildInteractionUiMessage(message) {
149
+ return {
150
+ content: message.markdown,
151
+ flags: message.flags,
152
+ components: buildMessageComponents({
153
+ buttons: message.buttons,
154
+ selectMenu: message.selectMenu,
155
+ selectMenus: message.selectMenus,
156
+ }),
157
+ };
158
+ }
159
+ function buildButtonStyle(style) {
160
+ if (style === 'primary') {
161
+ return ButtonStyle.Primary;
162
+ }
163
+ if (style === 'success') {
164
+ return ButtonStyle.Success;
165
+ }
166
+ if (style === 'danger') {
167
+ return ButtonStyle.Danger;
168
+ }
169
+ if (style === 'link') {
170
+ return ButtonStyle.Link;
171
+ }
172
+ return ButtonStyle.Secondary;
173
+ }
174
+ function isTruthy(value) {
175
+ return Boolean(value);
176
+ }
177
+ function buildMessageComponents({ buttons, selectMenu, selectMenus, }) {
178
+ const components = [];
179
+ if (buttons && buttons.length > 0) {
180
+ const row = new ActionRowBuilder().addComponents(...buttons.map((button) => {
181
+ const style = button.style === 'link' ? 'secondary' : button.style;
182
+ return new ButtonBuilder()
183
+ .setCustomId(button.id)
184
+ .setLabel(button.label)
185
+ .setStyle(buildButtonStyle(style));
186
+ }));
187
+ components.push(row);
188
+ }
189
+ const allSelectMenus = [selectMenu, ...(selectMenus || [])].filter(isTruthy);
190
+ for (const currentSelectMenu of allSelectMenus) {
191
+ const row = new ActionRowBuilder().addComponents(new StringSelectMenuBuilder()
192
+ .setCustomId(currentSelectMenu.id)
193
+ .setPlaceholder(currentSelectMenu.placeholder || 'Select an option')
194
+ .addOptions(currentSelectMenu.options)
195
+ .setMinValues(currentSelectMenu.minValues ?? 1)
196
+ .setMaxValues(currentSelectMenu.maxValues ?? 1));
197
+ components.push(row);
198
+ }
199
+ return components;
200
+ }
201
+ function wrapUser({ user, displayName, }) {
202
+ return {
203
+ id: user.id,
204
+ username: user.username,
205
+ displayName: displayName || user.displayName || user.username,
206
+ globalName: user.displayName || undefined,
207
+ bot: user.bot,
208
+ };
209
+ }
210
+ function wrapMessage(message) {
211
+ discordMessageStore.set(getDiscordMessageStoreKey({ channelId: message.channelId, messageId: message.id }), message);
212
+ return {
213
+ id: message.id,
214
+ content: message.content,
215
+ channelId: message.channelId,
216
+ author: wrapUser({ user: message.author, displayName: message.member?.displayName }),
217
+ attachments: message.attachments,
218
+ embeds: message.embeds.map((embed) => {
219
+ return {
220
+ data: {
221
+ footer: {
222
+ text: embed.footer?.text,
223
+ },
224
+ },
225
+ };
226
+ }),
227
+ };
228
+ }
229
+ function wrapThread(thread) {
230
+ return {
231
+ id: thread.id,
232
+ name: thread.name,
233
+ kind: 'thread',
234
+ type: 'thread',
235
+ parentId: thread.parentId,
236
+ guildId: thread.guildId,
237
+ createdTimestamp: thread.createdTimestamp ?? null,
238
+ isThread() {
239
+ return true;
240
+ },
241
+ };
242
+ }
243
+ function wrapChannel(channel) {
244
+ if (channel instanceof ThreadChannel) {
245
+ return {
246
+ id: channel.id,
247
+ name: channel.name,
248
+ kind: 'thread',
249
+ type: 'thread',
250
+ parentId: channel.parentId,
251
+ guildId: channel.guildId,
252
+ createdTimestamp: channel.createdTimestamp ?? null,
253
+ isThread() {
254
+ return true;
255
+ },
256
+ };
257
+ }
258
+ return {
259
+ id: channel.id,
260
+ name: channel.name,
261
+ kind: 'text',
262
+ type: 'text',
263
+ parentId: null,
264
+ guildId: channel.guildId,
265
+ topic: channel.topic,
266
+ isThread() {
267
+ return false;
268
+ },
269
+ };
270
+ }
271
+ function wrapOtherChannel({ id, guildId, name, parentId, }) {
272
+ return {
273
+ id,
274
+ name,
275
+ kind: 'other',
276
+ type: 'other',
277
+ parentId: parentId || null,
278
+ guildId: guildId || null,
279
+ isThread() {
280
+ return false;
281
+ },
282
+ };
283
+ }
284
+ function wrapOptionalChannel(channel) {
285
+ if (!channel) {
286
+ return null;
287
+ }
288
+ if (channel.type === ChannelType.GuildText) {
289
+ return wrapChannel(channel);
290
+ }
291
+ if (channel.type === ChannelType.PublicThread ||
292
+ channel.type === ChannelType.PrivateThread ||
293
+ channel.type === ChannelType.AnnouncementThread) {
294
+ return wrapChannel(channel);
295
+ }
296
+ return wrapOtherChannel({
297
+ id: channel.id,
298
+ guildId: 'guildId' in channel ? channel.guildId || null : null,
299
+ name: 'name' in channel ? channel.name || undefined : undefined,
300
+ parentId: 'parentId' in channel ? channel.parentId : null,
301
+ });
302
+ }
303
+ function getInteractionDisplayName({ member, user, }) {
304
+ if (member instanceof Object && 'displayName' in member && typeof member.displayName === 'string') {
305
+ return member.displayName;
306
+ }
307
+ return user.displayName || user.username;
308
+ }
309
+ function getInteractionAccess({ member, guild, }) {
310
+ const canUseKimaki = member && 'permissions' in member && !Array.isArray(member.roles)
311
+ ? hasKimakiBotPermission(member, guild)
312
+ : false;
313
+ const isBlocked = member instanceof GuildMember
314
+ ? hasNoKimakiRole(member)
315
+ : false;
316
+ return {
317
+ canUseKimaki,
318
+ isBlocked,
319
+ };
320
+ }
321
+ function wrapServer({ guild, }) {
322
+ if (!guild) {
323
+ return null;
324
+ }
325
+ discordGuildStore.set(guild.id, guild);
326
+ return {
327
+ id: guild.id,
328
+ name: guild.name,
329
+ };
330
+ }
331
+ function wrapGuildMemberSummary(member) {
332
+ return {
333
+ id: member.user.id,
334
+ username: member.user.username,
335
+ globalName: member.user.globalName || undefined,
336
+ nick: member.nickname || undefined,
337
+ };
338
+ }
339
+ function normalizeDiscordEmbeds(embeds) {
340
+ if (!embeds || embeds.length === 0) {
341
+ return undefined;
342
+ }
343
+ return embeds.map((embed) => {
344
+ const footerText = embed.footer?.text;
345
+ return {
346
+ ...(embed.color !== undefined ? { color: embed.color } : {}),
347
+ ...(footerText ? { footer: { text: footerText } } : {}),
348
+ };
349
+ });
350
+ }
351
+ function createCommandOptions({ interaction, }) {
352
+ return {
353
+ getString(name, required) {
354
+ return interaction.options.getString(name, required ?? false) || '';
355
+ },
356
+ getFocused(withMeta) {
357
+ if (withMeta && 'respond' in interaction) {
358
+ const focused = interaction.options.getFocused(true);
359
+ return {
360
+ name: focused.name,
361
+ value: String(focused.value),
362
+ };
363
+ }
364
+ if ('respond' in interaction) {
365
+ return String(interaction.options.getFocused());
366
+ }
367
+ return '';
368
+ },
369
+ };
370
+ }
371
+ function normalizeReplyOptions(options) {
372
+ if (typeof options === 'string') {
373
+ return options;
374
+ }
375
+ return options;
376
+ }
377
+ function normalizeEditReplyOptions(options) {
378
+ if (typeof options === 'string') {
379
+ return options;
380
+ }
381
+ return options;
382
+ }
383
+ function normalizeUpdateOptions(options) {
384
+ if (typeof options === 'string') {
385
+ return options;
386
+ }
387
+ return options;
388
+ }
389
+ function normalizeDeferReplyOptions(options) {
390
+ if (!options) {
391
+ return undefined;
392
+ }
393
+ if (options.flags !== undefined) {
394
+ return { flags: options.flags };
395
+ }
396
+ if (options.ephemeral !== undefined) {
397
+ return { flags: options.ephemeral ? MessageFlags.Ephemeral : undefined };
398
+ }
399
+ return undefined;
400
+ }
401
+ function createModalFields({ interaction, }) {
402
+ return {
403
+ getTextInputValue(id) {
404
+ return interaction.fields.getTextInputValue(id);
405
+ },
406
+ getFiles(id) {
407
+ const field = interaction.fields.fields.get(id);
408
+ if (!field || field.type !== 19) {
409
+ return [];
410
+ }
411
+ const attachments = field.attachments;
412
+ if (!attachments) {
413
+ return [];
414
+ }
415
+ return [...attachments.values()].map((attachment) => {
416
+ return wrapUploadedFile({ attachment });
417
+ });
418
+ },
419
+ };
420
+ }
421
+ function wrapUploadedFile({ attachment }) {
422
+ return {
423
+ id: attachment.id,
424
+ url: attachment.url,
425
+ name: attachment.name,
426
+ contentType: attachment.contentType,
427
+ };
428
+ }
429
+ function buildDiscordModal(modal) {
430
+ const built = new ModalBuilder().setCustomId(modal.id).setTitle(modal.title);
431
+ for (const input of modal.inputs) {
432
+ if (input.type === 'text') {
433
+ const row = new ActionRowBuilder().addComponents(new TextInputBuilder()
434
+ .setCustomId(input.id)
435
+ .setLabel(input.label)
436
+ .setPlaceholder(input.placeholder || '')
437
+ .setRequired(input.required ?? true)
438
+ .setStyle(input.style === 'paragraph'
439
+ ? TextInputStyle.Paragraph
440
+ : TextInputStyle.Short));
441
+ built.addComponents(row);
442
+ continue;
443
+ }
444
+ const fileUpload = new FileUploadBuilder()
445
+ .setCustomId(input.id)
446
+ .setMinValues(input.minFiles ?? 1)
447
+ .setMaxValues(input.maxFiles ?? 1);
448
+ const label = new LabelBuilder()
449
+ .setLabel(input.label)
450
+ .setDescription(input.description || '')
451
+ .setFileUploadComponent(fileUpload);
452
+ built.addLabelComponents(label);
453
+ }
454
+ return built;
455
+ }
456
+ export async function createDiscordClient() {
457
+ const restApiUrl = getDiscordRestApiUrl();
458
+ return new Client({
459
+ intents: [
460
+ GatewayIntentBits.Guilds,
461
+ GatewayIntentBits.GuildMessages,
462
+ GatewayIntentBits.MessageContent,
463
+ GatewayIntentBits.GuildVoiceStates,
464
+ ],
465
+ partials: [
466
+ Partials.Channel,
467
+ Partials.Message,
468
+ Partials.User,
469
+ Partials.ThreadMember,
470
+ ],
471
+ rest: { api: restApiUrl },
472
+ });
473
+ }
474
+ export class DiscordAdapter {
475
+ name = 'discord';
476
+ admin = {
477
+ listGuilds: async () => {
478
+ const cacheValues = [...this.client.guilds.cache.values()];
479
+ const guilds = cacheValues.length > 0
480
+ ? cacheValues
481
+ : [...(await this.client.guilds.fetch()).values()];
482
+ const hydratedGuilds = await Promise.all(guilds.map(async (guildRef) => {
483
+ const guild = await this.client.guilds.fetch(guildRef.id);
484
+ discordGuildStore.set(guild.id, guild);
485
+ const summary = {
486
+ id: guild.id,
487
+ name: guild.name,
488
+ memberCount: guild.memberCount,
489
+ ownerId: guild.ownerId,
490
+ };
491
+ return summary;
492
+ }));
493
+ return hydratedGuilds;
494
+ },
495
+ resolveGuild: async ({ guildId, fromChannelId, fallbackFirst }) => {
496
+ if (guildId) {
497
+ const guild = await this.client.guilds.fetch(guildId).catch(() => {
498
+ return null;
499
+ });
500
+ if (guild) {
501
+ discordGuildStore.set(guild.id, guild);
502
+ return {
503
+ id: guild.id,
504
+ name: guild.name,
505
+ memberCount: guild.memberCount,
506
+ ownerId: guild.ownerId,
507
+ };
508
+ }
509
+ }
510
+ if (fromChannelId) {
511
+ const channel = await this.client.channels.fetch(fromChannelId).catch(() => {
512
+ return null;
513
+ });
514
+ if (channel && 'guild' in channel && channel.guild) {
515
+ const guild = channel.guild;
516
+ discordGuildStore.set(guild.id, guild);
517
+ return {
518
+ id: guild.id,
519
+ name: guild.name,
520
+ memberCount: guild.memberCount,
521
+ ownerId: guild.ownerId,
522
+ };
523
+ }
524
+ }
525
+ if (!fallbackFirst) {
526
+ return null;
527
+ }
528
+ const firstGuild = await this.admin.listGuilds().then((guilds) => {
529
+ return guilds[0] || null;
530
+ });
531
+ return firstGuild;
532
+ },
533
+ registerCommands: async ({ appId, guildIds, commands, commandNamesForLegacyCleanup, allowLegacyGlobalCleanup, }) => {
534
+ const results = await Promise.allSettled(guildIds.map(async (guildId) => {
535
+ const route = Routes.applicationGuildCommands(appId, guildId);
536
+ const result = await this.client.rest.put(route, {
537
+ body: commands,
538
+ });
539
+ const registeredCount = Array.isArray(result) ? result.length : 0;
540
+ return {
541
+ guildId,
542
+ registeredCount,
543
+ };
544
+ }));
545
+ const failedGuilds = results.flatMap((result) => {
546
+ if (result.status === 'fulfilled') {
547
+ return [];
548
+ }
549
+ return [result.reason instanceof Error ? result.reason : new Error(String(result.reason))];
550
+ });
551
+ if (failedGuilds.length > 0) {
552
+ throw new Error(`Failed to register slash commands for ${failedGuilds.length} guild(s)`);
553
+ }
554
+ if (allowLegacyGlobalCleanup) {
555
+ const globalRoute = Routes.applicationCommands(appId);
556
+ const response = await this.client.rest.get(globalRoute);
557
+ if (Array.isArray(response)) {
558
+ const commandNames = new Set(commandNamesForLegacyCleanup);
559
+ const legacyCommands = response.filter((command) => {
560
+ if (!command || typeof command !== 'object') {
561
+ return false;
562
+ }
563
+ const commandName = 'name' in command ? command.name : undefined;
564
+ const commandId = 'id' in command ? command.id : undefined;
565
+ if (typeof commandName !== 'string' || typeof commandId !== 'string') {
566
+ return false;
567
+ }
568
+ return commandNames.has(commandName);
569
+ });
570
+ await Promise.allSettled(legacyCommands.map(async (command) => {
571
+ await this.client.rest.delete(Routes.applicationCommand(appId, command.id));
572
+ }));
573
+ }
574
+ }
575
+ const firstFulfilled = results.find((result) => {
576
+ return result.status === 'fulfilled';
577
+ });
578
+ return {
579
+ registeredGuildCount: results.length,
580
+ registeredCommandCount: firstFulfilled && firstFulfilled.status === 'fulfilled'
581
+ ? firstFulfilled.value.registeredCount
582
+ : commands.length,
583
+ };
584
+ },
585
+ ensureGuildAccessPolicy: async ({ guildId }) => {
586
+ const guild = await this.client.guilds.fetch(guildId);
587
+ discordGuildStore.set(guild.id, guild);
588
+ const roles = await guild.roles.fetch();
589
+ const existingRole = roles.find((role) => {
590
+ return role.name.toLowerCase() === 'kimaki';
591
+ });
592
+ if (existingRole) {
593
+ if (existingRole.position > 1) {
594
+ await existingRole.setPosition(1);
595
+ }
596
+ return;
597
+ }
598
+ await guild.roles.create({
599
+ name: 'Kimaki',
600
+ position: 1,
601
+ reason: 'Kimaki bot permission role - assign to users who can start sessions, send messages in threads, and use voice features',
602
+ });
603
+ },
604
+ listChannels: async ({ guildId }) => {
605
+ const guild = await this.client.guilds.fetch(guildId);
606
+ discordGuildStore.set(guild.id, guild);
607
+ await guild.channels.fetch();
608
+ return [...guild.channels.cache.values()].map((channel) => {
609
+ if (channel.type === ChannelType.GuildText) {
610
+ return wrapChannel(channel);
611
+ }
612
+ if (channel.type === ChannelType.PublicThread ||
613
+ channel.type === ChannelType.PrivateThread ||
614
+ channel.type === ChannelType.AnnouncementThread) {
615
+ return wrapChannel(channel);
616
+ }
617
+ return wrapOtherChannel({
618
+ id: channel.id,
619
+ guildId,
620
+ name: 'name' in channel ? channel.name : undefined,
621
+ parentId: 'parentId' in channel ? channel.parentId : null,
622
+ });
623
+ });
624
+ },
625
+ createCategory: async ({ guildId, name }) => {
626
+ const guild = await this.client.guilds.fetch(guildId);
627
+ discordGuildStore.set(guild.id, guild);
628
+ const created = await guild.channels.create({
629
+ name,
630
+ type: ChannelType.GuildCategory,
631
+ });
632
+ return wrapOtherChannel({ id: created.id, guildId, name: created.name });
633
+ },
634
+ createTextChannel: async ({ guildId, name, parentId, topic }) => {
635
+ const guild = await this.client.guilds.fetch(guildId);
636
+ discordGuildStore.set(guild.id, guild);
637
+ const created = await guild.channels.create({
638
+ name,
639
+ type: ChannelType.GuildText,
640
+ parent: parentId,
641
+ topic,
642
+ });
643
+ if (created.type !== ChannelType.GuildText) {
644
+ throw new Error(`Created non-text channel for ${name}`);
645
+ }
646
+ return wrapChannel(created);
647
+ },
648
+ createVoiceChannel: async ({ guildId, name, parentId }) => {
649
+ const guild = await this.client.guilds.fetch(guildId);
650
+ discordGuildStore.set(guild.id, guild);
651
+ const created = await guild.channels.create({
652
+ name,
653
+ type: ChannelType.GuildVoice,
654
+ parent: parentId,
655
+ });
656
+ return wrapOtherChannel({
657
+ id: created.id,
658
+ guildId,
659
+ name: created.name,
660
+ parentId: created.parentId,
661
+ });
662
+ },
663
+ fetchChannel: async ({ guildId, channelId }) => {
664
+ const guild = await this.client.guilds.fetch(guildId);
665
+ discordGuildStore.set(guild.id, guild);
666
+ const channel = guild.channels.cache.get(channelId) || (await guild.channels.fetch(channelId));
667
+ if (!channel) {
668
+ return null;
669
+ }
670
+ if (channel.type === ChannelType.GuildText) {
671
+ return wrapChannel(channel);
672
+ }
673
+ if (channel.type === ChannelType.PublicThread ||
674
+ channel.type === ChannelType.PrivateThread ||
675
+ channel.type === ChannelType.AnnouncementThread) {
676
+ return wrapChannel(channel);
677
+ }
678
+ return wrapOtherChannel({
679
+ id: channel.id,
680
+ guildId,
681
+ name: 'name' in channel ? channel.name : undefined,
682
+ parentId: 'parentId' in channel ? channel.parentId : null,
683
+ });
684
+ },
685
+ fetchChannelById: async ({ channelId }) => {
686
+ const channel = await this.client.channels.fetch(channelId);
687
+ if (!channel) {
688
+ return null;
689
+ }
690
+ if (channel.type === ChannelType.GuildText) {
691
+ return wrapChannel(channel);
692
+ }
693
+ if (channel.type === ChannelType.PublicThread ||
694
+ channel.type === ChannelType.PrivateThread ||
695
+ channel.type === ChannelType.AnnouncementThread) {
696
+ return wrapChannel(channel);
697
+ }
698
+ return wrapOtherChannel({
699
+ id: channel.id,
700
+ guildId: 'guildId' in channel ? channel.guildId || null : null,
701
+ name: 'name' in channel && typeof channel.name === 'string'
702
+ ? channel.name
703
+ : undefined,
704
+ parentId: 'parentId' in channel ? channel.parentId : null,
705
+ });
706
+ },
707
+ deleteChannel: async ({ guildId, channelId, reason }) => {
708
+ const guild = await this.client.guilds.fetch(guildId);
709
+ discordGuildStore.set(guild.id, guild);
710
+ const channel = await guild.channels.fetch(channelId);
711
+ if (!channel) {
712
+ return 'missing';
713
+ }
714
+ await channel.delete(reason);
715
+ return 'deleted';
716
+ },
717
+ listGuildMembers: async ({ guildId, limit }) => {
718
+ const guild = await this.client.guilds.fetch(guildId);
719
+ discordGuildStore.set(guild.id, guild);
720
+ const members = await guild.members.list({ limit: limit || 20 });
721
+ return [...members.values()].map((member) => {
722
+ return wrapGuildMemberSummary(member);
723
+ });
724
+ },
725
+ searchGuildMembers: async ({ guildId, query, limit }) => {
726
+ const guild = await this.client.guilds.fetch(guildId);
727
+ discordGuildStore.set(guild.id, guild);
728
+ const members = await guild.members.search({ query, limit: limit || 10 });
729
+ return [...members.values()].map((member) => {
730
+ return wrapGuildMemberSummary(member);
731
+ });
732
+ },
733
+ fetchCommandDescription: async ({ guildId, commandId }) => {
734
+ const guild = await this.client.guilds.fetch(guildId);
735
+ discordGuildStore.set(guild.id, guild);
736
+ const command = await guild.commands.fetch(commandId);
737
+ return command?.description;
738
+ },
739
+ };
740
+ client;
741
+ content = {
742
+ resolveMentions: async (message) => {
743
+ const discordMessage = this.getStoredMessage(message);
744
+ return resolveMentions(discordMessage);
745
+ },
746
+ getTextAttachments: async (message) => {
747
+ const discordMessage = this.getStoredMessage(message);
748
+ return getTextAttachments(discordMessage);
749
+ },
750
+ getFileAttachments: async (message) => {
751
+ const discordMessage = this.getStoredMessage(message);
752
+ return getFileAttachments(discordMessage);
753
+ },
754
+ };
755
+ permissions = {
756
+ getMessageAccess: async (message) => {
757
+ const discordMessage = this.getStoredMessage(message);
758
+ if (hasNoKimakiRole(discordMessage.member)) {
759
+ return 'blocked';
760
+ }
761
+ if (!hasKimakiBotPermission(discordMessage.member)) {
762
+ return 'denied';
763
+ }
764
+ return 'allowed';
765
+ },
766
+ };
767
+ voice = {
768
+ processAttachment: async (input) => {
769
+ return processVoiceAttachment({
770
+ adapter: this,
771
+ message: input.message,
772
+ thread: input.thread,
773
+ projectDirectory: input.projectDirectory,
774
+ isNewThread: input.isNewThread,
775
+ appId: input.appId,
776
+ currentSessionContext: input.currentSessionContext,
777
+ lastSessionContext: input.lastSessionContext,
778
+ });
779
+ },
780
+ };
781
+ constructor({ client }) {
782
+ this.client = client;
783
+ }
784
+ async login(token) {
785
+ await this.client.login(token);
786
+ }
787
+ destroy() {
788
+ this.client.destroy();
789
+ }
790
+ async getTargetChannel(target) {
791
+ const targetId = target.threadId || target.channelId;
792
+ const cached = this.client.channels.cache.get(targetId);
793
+ const channel = cached || (await this.client.channels.fetch(targetId));
794
+ if (!channel) {
795
+ throw new Error(`Channel not found: ${targetId}`);
796
+ }
797
+ if (channel.type === ChannelType.GuildText) {
798
+ return channel;
799
+ }
800
+ if (channel.type === ChannelType.PublicThread ||
801
+ channel.type === ChannelType.PrivateThread ||
802
+ channel.type === ChannelType.AnnouncementThread) {
803
+ return channel;
804
+ }
805
+ throw new Error(`Channel ${targetId} is not a sendable text target`);
806
+ }
807
+ async getMessageTarget(target, messageId) {
808
+ const channel = await this.getTargetChannel(target);
809
+ return channel.messages.fetch(messageId);
810
+ }
811
+ getStoredMessage(message) {
812
+ const stored = discordMessageStore.get(getDiscordMessageStoreKey({ channelId: message.channelId, messageId: message.id }));
813
+ if (!stored) {
814
+ throw new Error(`Discord message is not available in adapter cache: ${message.id}`);
815
+ }
816
+ return stored;
817
+ }
818
+ async getThreadChannel({ threadId, parentId, }) {
819
+ const channel = await this.getTargetChannel({
820
+ channelId: parentId || threadId,
821
+ threadId,
822
+ });
823
+ if (!(channel instanceof ThreadChannel)) {
824
+ throw new Error(`Channel ${threadId} is not a thread target`);
825
+ }
826
+ return channel;
827
+ }
828
+ createConversation(target) {
829
+ return {
830
+ target,
831
+ send: async (message) => {
832
+ const channel = await this.getTargetChannel(target);
833
+ if (message.files && message.files.length > 0) {
834
+ const sent = await channel.send({
835
+ content: message.markdown.slice(0, 2000),
836
+ flags: message.flags,
837
+ embeds: normalizeDiscordEmbeds(message.embeds),
838
+ files: message.files.map((file) => {
839
+ return {
840
+ attachment: Buffer.from(file.data),
841
+ name: file.filename,
842
+ contentType: file.contentType,
843
+ };
844
+ }),
845
+ reply: message.replyToMessageId
846
+ ? { messageReference: message.replyToMessageId }
847
+ : undefined,
848
+ });
849
+ return { id: sent.id };
850
+ }
851
+ if (message.buttons || message.selectMenu || message.selectMenus) {
852
+ const sent = await channel.send({
853
+ content: message.markdown.slice(0, 2000),
854
+ flags: message.flags,
855
+ embeds: normalizeDiscordEmbeds(message.embeds),
856
+ components: buildMessageComponents({
857
+ buttons: message.buttons,
858
+ selectMenu: message.selectMenu,
859
+ selectMenus: message.selectMenus,
860
+ }),
861
+ reply: message.replyToMessageId
862
+ ? { messageReference: message.replyToMessageId }
863
+ : undefined,
864
+ });
865
+ return { id: sent.id };
866
+ }
867
+ return this.sendMarkdownSegments(channel, message);
868
+ },
869
+ update: async (messageId, message) => {
870
+ const targetMessage = await this.getMessageTarget(target, messageId);
871
+ await targetMessage.edit({
872
+ content: message.markdown,
873
+ flags: message.flags,
874
+ embeds: normalizeDiscordEmbeds(message.embeds),
875
+ components: message.buttons || message.selectMenu || message.selectMenus
876
+ ? buildMessageComponents({
877
+ buttons: message.buttons,
878
+ selectMenu: message.selectMenu,
879
+ selectMenus: message.selectMenus,
880
+ })
881
+ : [],
882
+ });
883
+ },
884
+ delete: async (messageId) => {
885
+ const targetMessage = await this.getMessageTarget(target, messageId);
886
+ await targetMessage.delete();
887
+ },
888
+ message: async (messageId) => {
889
+ const message = await this.getMessageTarget(target, messageId);
890
+ return {
891
+ data: wrapMessage(message),
892
+ startThread: async ({ name, autoArchiveDuration, reason }) => {
893
+ const thread = await message.startThread({
894
+ name,
895
+ autoArchiveDuration,
896
+ reason,
897
+ });
898
+ return {
899
+ thread: wrapThread(thread),
900
+ target: {
901
+ channelId: thread.parentId || thread.id,
902
+ threadId: thread.id,
903
+ },
904
+ };
905
+ },
906
+ };
907
+ },
908
+ startTyping: async () => {
909
+ const channel = await this.getTargetChannel(target);
910
+ await channel.sendTyping();
911
+ },
912
+ };
913
+ }
914
+ conversation(target) {
915
+ return this.createConversation(target);
916
+ }
917
+ async channel(channelId) {
918
+ const cached = this.client.channels.cache.get(channelId);
919
+ const channel = cached || (await this.client.channels.fetch(channelId));
920
+ if (!channel) {
921
+ return null;
922
+ }
923
+ if (channel.type === ChannelType.GuildText) {
924
+ const data = wrapChannel(channel);
925
+ return {
926
+ data,
927
+ conversation: () => {
928
+ return this.createConversation({ channelId: data.id });
929
+ },
930
+ };
931
+ }
932
+ if (channel.type === ChannelType.PublicThread ||
933
+ channel.type === ChannelType.PrivateThread ||
934
+ channel.type === ChannelType.AnnouncementThread) {
935
+ const data = wrapChannel(channel);
936
+ return {
937
+ data,
938
+ conversation: () => {
939
+ return this.createConversation({
940
+ channelId: data.parentId || data.id,
941
+ threadId: data.id,
942
+ });
943
+ },
944
+ };
945
+ }
946
+ return {
947
+ data: {
948
+ id: channel.id,
949
+ kind: 'other',
950
+ type: 'other',
951
+ parentId: null,
952
+ guildId: 'guildId' in channel ? channel.guildId || null : null,
953
+ isThread() {
954
+ return false;
955
+ },
956
+ },
957
+ conversation: () => {
958
+ return this.createConversation({ channelId: channel.id });
959
+ },
960
+ };
961
+ }
962
+ async thread({ threadId, parentId, }) {
963
+ const thread = await this.getThreadChannel({ threadId, parentId }).catch(() => {
964
+ return null;
965
+ });
966
+ if (!thread) {
967
+ return null;
968
+ }
969
+ const data = wrapThread(thread);
970
+ return {
971
+ data,
972
+ conversation: () => {
973
+ return this.createConversation({
974
+ channelId: data.parentId || data.id,
975
+ threadId: data.id,
976
+ });
977
+ },
978
+ message: async (messageId) => {
979
+ return this.createConversation({
980
+ channelId: data.parentId || data.id,
981
+ threadId: data.id,
982
+ }).message(messageId);
983
+ },
984
+ starterMessage: async () => {
985
+ const starter = await thread.fetchStarterMessage().catch(() => {
986
+ return null;
987
+ });
988
+ if (!starter) {
989
+ return null;
990
+ }
991
+ return wrapMessage(starter);
992
+ },
993
+ rename: async (name) => {
994
+ await thread.setName(name);
995
+ },
996
+ archive: async () => {
997
+ await thread.setArchived(true);
998
+ },
999
+ addMember: async (userId) => {
1000
+ await thread.members.add(userId);
1001
+ },
1002
+ addStarterReaction: async (emoji) => {
1003
+ await this.client.rest.put(Routes.channelMessageOwnReaction(data.parentId || data.id, data.id, encodeURIComponent(emoji)));
1004
+ },
1005
+ reference: () => {
1006
+ return `<#${data.id}>`;
1007
+ },
1008
+ };
1009
+ }
1010
+ async sendMarkdownSegments(channel, message) {
1011
+ const segments = splitTablesFromMarkdown(message.markdown);
1012
+ const baseFlags = message.flags ?? 0;
1013
+ let firstMessage;
1014
+ let shouldReply = true;
1015
+ for (const segment of segments) {
1016
+ if (segment.type === 'components') {
1017
+ const sent = await channel.send({
1018
+ components: segment.components,
1019
+ flags: MessageFlags.IsComponentsV2 | baseFlags,
1020
+ embeds: firstMessage
1021
+ ? undefined
1022
+ : normalizeDiscordEmbeds(message.embeds),
1023
+ reply: shouldReply && message.replyToMessageId
1024
+ ? { messageReference: message.replyToMessageId }
1025
+ : undefined,
1026
+ });
1027
+ if (!firstMessage) {
1028
+ firstMessage = sent;
1029
+ }
1030
+ shouldReply = false;
1031
+ continue;
1032
+ }
1033
+ let text = segment.text;
1034
+ text = unnestCodeBlocksFromLists(text);
1035
+ text = limitHeadingDepth(text);
1036
+ text = escapeBackticksInCodeBlocks(text);
1037
+ if (!text.trim()) {
1038
+ continue;
1039
+ }
1040
+ const chunks = splitMarkdownForDiscord({
1041
+ content: text,
1042
+ maxLength: 2000,
1043
+ });
1044
+ for (let chunk of chunks) {
1045
+ if (!chunk) {
1046
+ continue;
1047
+ }
1048
+ if (chunk.length > 2000) {
1049
+ chunk = chunk.slice(0, 1996) + '...';
1050
+ }
1051
+ const sent = await channel.send({
1052
+ content: chunk,
1053
+ flags: baseFlags,
1054
+ embeds: firstMessage
1055
+ ? undefined
1056
+ : normalizeDiscordEmbeds(message.embeds),
1057
+ reply: shouldReply && message.replyToMessageId
1058
+ ? { messageReference: message.replyToMessageId }
1059
+ : undefined,
1060
+ });
1061
+ if (!firstMessage) {
1062
+ firstMessage = sent;
1063
+ }
1064
+ shouldReply = false;
1065
+ }
1066
+ }
1067
+ if (!firstMessage) {
1068
+ const sent = await channel.send({
1069
+ content: '\u200b',
1070
+ flags: baseFlags,
1071
+ embeds: normalizeDiscordEmbeds(message.embeds),
1072
+ reply: message.replyToMessageId
1073
+ ? { messageReference: message.replyToMessageId }
1074
+ : undefined,
1075
+ });
1076
+ firstMessage = sent;
1077
+ }
1078
+ return { id: firstMessage.id };
1079
+ }
1080
+ onReady(handler) {
1081
+ this.client.on(Events.ClientReady, () => {
1082
+ void handler();
1083
+ });
1084
+ }
1085
+ onMessage(handler) {
1086
+ this.client.on(Events.MessageCreate, async (message) => {
1087
+ const normalizedMessage = message.partial
1088
+ ? await message.fetch().catch(() => {
1089
+ return null;
1090
+ })
1091
+ : message;
1092
+ if (!normalizedMessage) {
1093
+ return;
1094
+ }
1095
+ const isThread = normalizedMessage.channel.isThread();
1096
+ const channelId = (() => {
1097
+ if (normalizedMessage.channel.isThread()) {
1098
+ return normalizedMessage.channel.parentId || normalizedMessage.channel.id;
1099
+ }
1100
+ return normalizedMessage.channel.id;
1101
+ })();
1102
+ const target = isThread
1103
+ ? {
1104
+ channelId,
1105
+ threadId: normalizedMessage.channel.id,
1106
+ }
1107
+ : { channelId };
1108
+ const isMention = Boolean(this.client.user && normalizedMessage.mentions.has(this.client.user.id));
1109
+ const conversation = this.createConversation(target);
1110
+ void handler({
1111
+ message: wrapMessage(normalizedMessage),
1112
+ thread: isThread
1113
+ ? wrapThread(normalizedMessage.channel)
1114
+ : undefined,
1115
+ conversation,
1116
+ kind: isThread ? 'thread' : 'channel',
1117
+ isMention,
1118
+ isSelf: Boolean(this.client.user && normalizedMessage.author.id === this.client.user.id),
1119
+ });
1120
+ });
1121
+ }
1122
+ onThreadCreate(handler) {
1123
+ this.client.on(Events.ThreadCreate, (thread, newlyCreated) => {
1124
+ const threadHandle = {
1125
+ data: wrapThread(thread),
1126
+ conversation: () => {
1127
+ return this.createConversation({
1128
+ channelId: thread.parentId || thread.id,
1129
+ threadId: thread.id,
1130
+ });
1131
+ },
1132
+ message: async (messageId) => {
1133
+ return this.createConversation({
1134
+ channelId: thread.parentId || thread.id,
1135
+ threadId: thread.id,
1136
+ }).message(messageId);
1137
+ },
1138
+ starterMessage: async () => {
1139
+ const starter = await thread.fetchStarterMessage().catch(() => {
1140
+ return null;
1141
+ });
1142
+ if (!starter) {
1143
+ return null;
1144
+ }
1145
+ return wrapMessage(starter);
1146
+ },
1147
+ rename: async (name) => {
1148
+ await thread.setName(name);
1149
+ },
1150
+ archive: async () => {
1151
+ await thread.setArchived(true);
1152
+ },
1153
+ addMember: async (userId) => {
1154
+ await thread.members.add(userId);
1155
+ },
1156
+ addStarterReaction: async (emoji) => {
1157
+ await this.client.rest.put(Routes.channelMessageOwnReaction(thread.parentId || thread.id, thread.id, encodeURIComponent(emoji)));
1158
+ },
1159
+ reference: () => {
1160
+ return `<#${thread.id}>`;
1161
+ },
1162
+ };
1163
+ void handler({
1164
+ thread: threadHandle.data,
1165
+ threadHandle,
1166
+ conversation: threadHandle.conversation(),
1167
+ newlyCreated,
1168
+ });
1169
+ });
1170
+ }
1171
+ onThreadDelete(handler) {
1172
+ this.client.on(Events.ThreadDelete, (thread) => {
1173
+ handler(thread.id);
1174
+ });
1175
+ }
1176
+ wrapCommandEvent({ interaction, appId, }) {
1177
+ return {
1178
+ appId,
1179
+ commandName: interaction.commandName,
1180
+ commandId: interaction.commandId,
1181
+ commandDescription: interaction.command?.description,
1182
+ channel: wrapOptionalChannel(interaction.channel),
1183
+ channelId: interaction.channelId,
1184
+ guild: wrapServer({ guild: interaction.guild }),
1185
+ guildId: interaction.guildId,
1186
+ user: wrapUser({
1187
+ user: interaction.user,
1188
+ displayName: getInteractionDisplayName({ member: interaction.member, user: interaction.user }),
1189
+ }),
1190
+ access: getInteractionAccess({ member: interaction.member, guild: interaction.guild }),
1191
+ options: createCommandOptions({ interaction }),
1192
+ deferred: interaction.deferred,
1193
+ replied: interaction.replied,
1194
+ botUserName: interaction.client.user?.username,
1195
+ reply: async (options) => {
1196
+ await interaction.reply(normalizeReplyOptions(options));
1197
+ },
1198
+ replyUi: async (message) => {
1199
+ await interaction.reply(buildInteractionUiMessage(message));
1200
+ },
1201
+ deferReply: async (options) => {
1202
+ await interaction.deferReply(normalizeDeferReplyOptions(options));
1203
+ },
1204
+ editReply: async (options) => {
1205
+ await interaction.editReply(normalizeEditReplyOptions(options));
1206
+ },
1207
+ editUiReply: async (message) => {
1208
+ await interaction.editReply(buildInteractionUiMessage(message));
1209
+ },
1210
+ showModal: async (modal) => {
1211
+ await interaction.showModal(buildDiscordModal(modal));
1212
+ },
1213
+ };
1214
+ }
1215
+ wrapAutocompleteEvent({ interaction, appId, }) {
1216
+ return {
1217
+ appId,
1218
+ commandName: interaction.commandName,
1219
+ channel: wrapOptionalChannel(interaction.channel),
1220
+ channelId: interaction.channelId,
1221
+ guild: wrapServer({ guild: interaction.guild }),
1222
+ guildId: interaction.guildId,
1223
+ user: wrapUser({
1224
+ user: interaction.user,
1225
+ displayName: getInteractionDisplayName({ member: interaction.member, user: interaction.user }),
1226
+ }),
1227
+ botUserName: interaction.client.user?.username,
1228
+ options: createCommandOptions({ interaction }),
1229
+ respond: async (options) => {
1230
+ await interaction.respond(options);
1231
+ },
1232
+ };
1233
+ }
1234
+ wrapButtonEvent({ interaction, appId, }) {
1235
+ const event = {
1236
+ appId,
1237
+ customId: interaction.customId,
1238
+ channel: wrapOptionalChannel(interaction.channel),
1239
+ channelId: interaction.channelId,
1240
+ guild: wrapServer({ guild: interaction.guild }),
1241
+ guildId: interaction.guildId,
1242
+ user: wrapUser({
1243
+ user: interaction.user,
1244
+ displayName: getInteractionDisplayName({ member: interaction.member, user: interaction.user }),
1245
+ }),
1246
+ access: getInteractionAccess({ member: interaction.member, guild: interaction.guild }),
1247
+ message: wrapMessage(interaction.message),
1248
+ deferred: interaction.deferred,
1249
+ replied: interaction.replied,
1250
+ botUserName: interaction.client.user?.username,
1251
+ reply: async (options) => {
1252
+ await interaction.reply(normalizeReplyOptions(options));
1253
+ },
1254
+ replyUi: async (message) => {
1255
+ await interaction.reply(buildInteractionUiMessage(message));
1256
+ },
1257
+ deferReply: async (options) => {
1258
+ await interaction.deferReply(normalizeDeferReplyOptions(options));
1259
+ },
1260
+ editReply: async (options) => {
1261
+ await interaction.editReply(normalizeEditReplyOptions(options));
1262
+ },
1263
+ editUiReply: async (message) => {
1264
+ await interaction.editReply(buildInteractionUiMessage(message));
1265
+ },
1266
+ followUp: async (options) => {
1267
+ await interaction.followUp(normalizeReplyOptions(options));
1268
+ },
1269
+ deferUpdate: async (options) => {
1270
+ await interaction.deferUpdate(options);
1271
+ },
1272
+ update: async (options) => {
1273
+ await interaction.update(normalizeUpdateOptions(options));
1274
+ },
1275
+ updateUi: async (message) => {
1276
+ await interaction.update(buildInteractionUiMessage(message));
1277
+ },
1278
+ showModal: async (modal) => {
1279
+ await interaction.showModal(buildDiscordModal(modal));
1280
+ },
1281
+ // Self-referential: passes the wrapped event to handleHtmlActionButton
1282
+ runHtmlAction: async () => {
1283
+ if (!interaction.customId.startsWith('html_action:')) {
1284
+ return false;
1285
+ }
1286
+ await handleHtmlActionButton(event);
1287
+ return true;
1288
+ },
1289
+ };
1290
+ return event;
1291
+ }
1292
+ wrapSelectMenuEvent({ interaction, appId, }) {
1293
+ return {
1294
+ appId,
1295
+ customId: interaction.customId,
1296
+ channel: wrapOptionalChannel(interaction.channel),
1297
+ channelId: interaction.channelId,
1298
+ guild: wrapServer({ guild: interaction.guild }),
1299
+ guildId: interaction.guildId,
1300
+ user: wrapUser({
1301
+ user: interaction.user,
1302
+ displayName: getInteractionDisplayName({ member: interaction.member, user: interaction.user }),
1303
+ }),
1304
+ access: getInteractionAccess({ member: interaction.member, guild: interaction.guild }),
1305
+ message: wrapMessage(interaction.message),
1306
+ values: [...interaction.values],
1307
+ deferred: interaction.deferred,
1308
+ replied: interaction.replied,
1309
+ botUserName: interaction.client.user?.username,
1310
+ reply: async (options) => {
1311
+ await interaction.reply(normalizeReplyOptions(options));
1312
+ },
1313
+ replyUi: async (message) => {
1314
+ await interaction.reply(buildInteractionUiMessage(message));
1315
+ },
1316
+ deferReply: async (options) => {
1317
+ await interaction.deferReply(normalizeDeferReplyOptions(options));
1318
+ },
1319
+ editReply: async (options) => {
1320
+ await interaction.editReply(normalizeEditReplyOptions(options));
1321
+ },
1322
+ editUiReply: async (message) => {
1323
+ await interaction.editReply(buildInteractionUiMessage(message));
1324
+ },
1325
+ deferUpdate: async (options) => {
1326
+ await interaction.deferUpdate(options);
1327
+ },
1328
+ update: async (options) => {
1329
+ await interaction.update(normalizeUpdateOptions(options));
1330
+ },
1331
+ updateUi: async (message) => {
1332
+ await interaction.update(buildInteractionUiMessage(message));
1333
+ },
1334
+ showModal: async (modal) => {
1335
+ await interaction.showModal(buildDiscordModal(modal));
1336
+ },
1337
+ };
1338
+ }
1339
+ wrapModalSubmitEvent({ interaction, appId, }) {
1340
+ return {
1341
+ appId,
1342
+ customId: interaction.customId,
1343
+ channel: wrapOptionalChannel(interaction.channel),
1344
+ channelId: interaction.channelId,
1345
+ guild: wrapServer({ guild: interaction.guild }),
1346
+ guildId: interaction.guildId,
1347
+ user: wrapUser({
1348
+ user: interaction.user,
1349
+ displayName: getInteractionDisplayName({ member: interaction.member, user: interaction.user }),
1350
+ }),
1351
+ access: getInteractionAccess({ member: interaction.member, guild: interaction.guild }),
1352
+ fields: createModalFields({ interaction }),
1353
+ deferred: interaction.deferred,
1354
+ replied: interaction.replied,
1355
+ botUserName: interaction.client.user?.username,
1356
+ reply: async (options) => {
1357
+ await interaction.reply(normalizeReplyOptions(options));
1358
+ },
1359
+ replyUi: async (message) => {
1360
+ await interaction.reply(buildInteractionUiMessage(message));
1361
+ },
1362
+ deferReply: async (options) => {
1363
+ await interaction.deferReply(normalizeDeferReplyOptions(options));
1364
+ },
1365
+ editReply: async (options) => {
1366
+ await interaction.editReply(normalizeEditReplyOptions(options));
1367
+ },
1368
+ editUiReply: async (message) => {
1369
+ await interaction.editReply(buildInteractionUiMessage(message));
1370
+ },
1371
+ };
1372
+ }
1373
+ onCommand(handler) {
1374
+ this.client.on(Events.InteractionCreate, (interaction) => {
1375
+ if (!interaction.isChatInputCommand()) {
1376
+ return;
1377
+ }
1378
+ const appId = this.client.application?.id;
1379
+ if (!appId) {
1380
+ return;
1381
+ }
1382
+ void handler(this.wrapCommandEvent({ interaction, appId }));
1383
+ });
1384
+ }
1385
+ onAutocomplete(handler) {
1386
+ this.client.on(Events.InteractionCreate, (interaction) => {
1387
+ if (!interaction.isAutocomplete()) {
1388
+ return;
1389
+ }
1390
+ const appId = this.client.application?.id;
1391
+ if (!appId) {
1392
+ return;
1393
+ }
1394
+ void handler(this.wrapAutocompleteEvent({ interaction, appId }));
1395
+ });
1396
+ }
1397
+ onButton(handler) {
1398
+ this.client.on(Events.InteractionCreate, (interaction) => {
1399
+ if (!interaction.isButton()) {
1400
+ return;
1401
+ }
1402
+ const appId = this.client.application?.id;
1403
+ if (!appId) {
1404
+ return;
1405
+ }
1406
+ void handler(this.wrapButtonEvent({ interaction, appId }));
1407
+ });
1408
+ }
1409
+ onSelectMenu(handler) {
1410
+ this.client.on(Events.InteractionCreate, (interaction) => {
1411
+ if (!interaction.isStringSelectMenu()) {
1412
+ return;
1413
+ }
1414
+ const appId = this.client.application?.id;
1415
+ if (!appId) {
1416
+ return;
1417
+ }
1418
+ void handler(this.wrapSelectMenuEvent({ interaction, appId }));
1419
+ });
1420
+ }
1421
+ onModalSubmit(handler) {
1422
+ this.client.on(Events.InteractionCreate, (interaction) => {
1423
+ if (!interaction.isModalSubmit()) {
1424
+ return;
1425
+ }
1426
+ const appId = this.client.application?.id;
1427
+ if (!appId) {
1428
+ return;
1429
+ }
1430
+ void handler(this.wrapModalSubmitEvent({ interaction, appId }));
1431
+ });
1432
+ }
1433
+ onError(handler) {
1434
+ this.client.on(Events.Error, handler);
1435
+ }
1436
+ }
1437
+ export async function createDiscordAdapter({ client, } = {}) {
1438
+ const resolvedClient = client || (await createDiscordClient());
1439
+ return new DiscordAdapter({ client: resolvedClient });
1440
+ }