@waifucave/discord-waifus 1.5.154

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 (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +230 -0
  3. package/THIRD_PARTY_NOTICES.md +26 -0
  4. package/assets/ocr/eng.traineddata +0 -0
  5. package/bin/waifus.mjs +36 -0
  6. package/dist/api/errors.d.ts +9 -0
  7. package/dist/api/errors.js +23 -0
  8. package/dist/api/errors.js.map +1 -0
  9. package/dist/api/server.d.ts +19 -0
  10. package/dist/api/server.js +1290 -0
  11. package/dist/api/server.js.map +1 -0
  12. package/dist/backend/logger.d.ts +20 -0
  13. package/dist/backend/logger.js +65 -0
  14. package/dist/backend/logger.js.map +1 -0
  15. package/dist/backend/migrations.d.ts +4 -0
  16. package/dist/backend/migrations.js +521 -0
  17. package/dist/backend/migrations.js.map +1 -0
  18. package/dist/backend/redaction.d.ts +1 -0
  19. package/dist/backend/redaction.js +22 -0
  20. package/dist/backend/redaction.js.map +1 -0
  21. package/dist/backend/runtime.d.ts +30 -0
  22. package/dist/backend/runtime.js +49 -0
  23. package/dist/backend/runtime.js.map +1 -0
  24. package/dist/backend/server.d.ts +18 -0
  25. package/dist/backend/server.js +581 -0
  26. package/dist/backend/server.js.map +1 -0
  27. package/dist/cli/commands.d.ts +35 -0
  28. package/dist/cli/commands.js +510 -0
  29. package/dist/cli/commands.js.map +1 -0
  30. package/dist/cli/main.d.ts +1 -0
  31. package/dist/cli/main.js +12 -0
  32. package/dist/cli/main.js.map +1 -0
  33. package/dist/cli/parser.d.ts +10 -0
  34. package/dist/cli/parser.js +64 -0
  35. package/dist/cli/parser.js.map +1 -0
  36. package/dist/config/appConfig.d.ts +3 -0
  37. package/dist/config/appConfig.js +27 -0
  38. package/dist/config/appConfig.js.map +1 -0
  39. package/dist/config/layout.d.ts +13 -0
  40. package/dist/config/layout.js +173 -0
  41. package/dist/config/layout.js.map +1 -0
  42. package/dist/config/paths.d.ts +5 -0
  43. package/dist/config/paths.js +20 -0
  44. package/dist/config/paths.js.map +1 -0
  45. package/dist/config/prebuiltWaifus.d.ts +3 -0
  46. package/dist/config/prebuiltWaifus.js +106 -0
  47. package/dist/config/prebuiltWaifus.js.map +1 -0
  48. package/dist/discord/client.d.ts +394 -0
  49. package/dist/discord/client.js +1196 -0
  50. package/dist/discord/client.js.map +1 -0
  51. package/dist/discord/contextBuilder.d.ts +15 -0
  52. package/dist/discord/contextBuilder.js +18 -0
  53. package/dist/discord/contextBuilder.js.map +1 -0
  54. package/dist/discord/intents.d.ts +5 -0
  55. package/dist/discord/intents.js +21 -0
  56. package/dist/discord/intents.js.map +1 -0
  57. package/dist/discord/memberCache.d.ts +12 -0
  58. package/dist/discord/memberCache.js +55 -0
  59. package/dist/discord/memberCache.js.map +1 -0
  60. package/dist/discord/normalization.d.ts +34 -0
  61. package/dist/discord/normalization.js +330 -0
  62. package/dist/discord/normalization.js.map +1 -0
  63. package/dist/discord/rateLimit.d.ts +9 -0
  64. package/dist/discord/rateLimit.js +11 -0
  65. package/dist/discord/rateLimit.js.map +1 -0
  66. package/dist/orchestration/context.d.ts +85 -0
  67. package/dist/orchestration/context.js +200 -0
  68. package/dist/orchestration/context.js.map +1 -0
  69. package/dist/orchestration/decisions.d.ts +52 -0
  70. package/dist/orchestration/decisions.js +61 -0
  71. package/dist/orchestration/decisions.js.map +1 -0
  72. package/dist/orchestration/messageSplit.d.ts +6 -0
  73. package/dist/orchestration/messageSplit.js +118 -0
  74. package/dist/orchestration/messageSplit.js.map +1 -0
  75. package/dist/orchestration/ocr.d.ts +43 -0
  76. package/dist/orchestration/ocr.js +504 -0
  77. package/dist/orchestration/ocr.js.map +1 -0
  78. package/dist/orchestration/ocrPackages.d.ts +24 -0
  79. package/dist/orchestration/ocrPackages.js +85 -0
  80. package/dist/orchestration/ocrPackages.js.map +1 -0
  81. package/dist/orchestration/promptBlocks.d.ts +28 -0
  82. package/dist/orchestration/promptBlocks.js +232 -0
  83. package/dist/orchestration/promptBlocks.js.map +1 -0
  84. package/dist/orchestration/replyQuote.d.ts +7 -0
  85. package/dist/orchestration/replyQuote.js +198 -0
  86. package/dist/orchestration/replyQuote.js.map +1 -0
  87. package/dist/orchestration/reviewer.d.ts +5 -0
  88. package/dist/orchestration/reviewer.js +5 -0
  89. package/dist/orchestration/reviewer.js.map +1 -0
  90. package/dist/orchestration/runtime.d.ts +152 -0
  91. package/dist/orchestration/runtime.js +3535 -0
  92. package/dist/orchestration/runtime.js.map +1 -0
  93. package/dist/orchestration/session.d.ts +30 -0
  94. package/dist/orchestration/session.js +41 -0
  95. package/dist/orchestration/session.js.map +1 -0
  96. package/dist/orchestration/stageManager.d.ts +42 -0
  97. package/dist/orchestration/stageManager.js +50 -0
  98. package/dist/orchestration/stageManager.js.map +1 -0
  99. package/dist/providers/catalog.d.ts +6 -0
  100. package/dist/providers/catalog.js +212 -0
  101. package/dist/providers/catalog.js.map +1 -0
  102. package/dist/providers/pipelines.d.ts +13 -0
  103. package/dist/providers/pipelines.js +2388 -0
  104. package/dist/providers/pipelines.js.map +1 -0
  105. package/dist/providers/types.d.ts +91 -0
  106. package/dist/providers/types.js +2 -0
  107. package/dist/providers/types.js.map +1 -0
  108. package/dist/shared/queryLog.d.ts +39 -0
  109. package/dist/shared/queryLog.js +80 -0
  110. package/dist/shared/queryLog.js.map +1 -0
  111. package/dist/shared/schemas/common.d.ts +21 -0
  112. package/dist/shared/schemas/common.js +27 -0
  113. package/dist/shared/schemas/common.js.map +1 -0
  114. package/dist/shared/schemas/config.d.ts +57 -0
  115. package/dist/shared/schemas/config.js +49 -0
  116. package/dist/shared/schemas/config.js.map +1 -0
  117. package/dist/shared/schemas/domain.d.ts +825 -0
  118. package/dist/shared/schemas/domain.js +444 -0
  119. package/dist/shared/schemas/domain.js.map +1 -0
  120. package/dist/storage/atomic.d.ts +6 -0
  121. package/dist/storage/atomic.js +60 -0
  122. package/dist/storage/atomic.js.map +1 -0
  123. package/dist/storage/errors.d.ts +12 -0
  124. package/dist/storage/errors.js +23 -0
  125. package/dist/storage/errors.js.map +1 -0
  126. package/dist/storage/locks.d.ts +9 -0
  127. package/dist/storage/locks.js +42 -0
  128. package/dist/storage/locks.js.map +1 -0
  129. package/dist/storage/storageService.d.ts +25 -0
  130. package/dist/storage/storageService.js +66 -0
  131. package/dist/storage/storageService.js.map +1 -0
  132. package/dist-frontend/assets/index-B2bB3_2r.js +39 -0
  133. package/dist-frontend/assets/index-B2bB3_2r.js.map +1 -0
  134. package/dist-frontend/assets/index-Dj1L8Px-.css +1 -0
  135. package/dist-frontend/index.html +14 -0
  136. package/docs/api.md +162 -0
  137. package/docs/old-orchestrator-prompt.md +123 -0
  138. package/package.json +81 -0
@@ -0,0 +1,1196 @@
1
+ import { ApplicationCommandOptionType, Client, Events, GatewayIntentBits, MessageFlags, Partials, PermissionFlagsBits } from "discord.js";
2
+ import { buildNormalizedDiscordContext } from "./contextBuilder.js";
3
+ import { unresolvedMentionIdsByType } from "./memberCache.js";
4
+ import { denormalizeModelContentForDiscord } from "./normalization.js";
5
+ export class DiscordJsGateway {
6
+ options;
7
+ clients = new Map();
8
+ listeners = new Set();
9
+ reviewListeners = new Set();
10
+ clearListeners = new Set();
11
+ runListeners = new Set();
12
+ runWaifuAutocompleteListeners = new Set();
13
+ stopListeners = new Set();
14
+ memoriesListeners = new Set();
15
+ printListeners = new Set();
16
+ printWaifuAutocompleteListeners = new Set();
17
+ debugListeners = new Set();
18
+ recentMentionRefreshes = new Map();
19
+ constructor(options) {
20
+ this.options = options;
21
+ }
22
+ async connect() {
23
+ const bots = [this.options.orchestrator, ...(this.options.waifus ?? [])].filter((bot) => Boolean(bot.token));
24
+ await Promise.all(bots.map(async (bot) => {
25
+ const client = new Client({
26
+ intents: [
27
+ GatewayIntentBits.Guilds,
28
+ GatewayIntentBits.GuildMessages,
29
+ GatewayIntentBits.GuildMessageReactions,
30
+ GatewayIntentBits.MessageContent
31
+ ],
32
+ partials: [Partials.Channel, Partials.Message, Partials.Reaction]
33
+ });
34
+ this.attachClientRuntimeHandlers(client, bot.id);
35
+ if (bot.id === this.options.orchestrator.id) {
36
+ client.on("messageCreate", (message) => {
37
+ if (!message.guildId)
38
+ return;
39
+ const event = {
40
+ guildId: message.guildId,
41
+ channelId: message.channelId,
42
+ messageId: message.id,
43
+ authorId: message.author.id,
44
+ authorDisplayName: message.member?.displayName ?? message.author.globalName ?? message.author.username,
45
+ authorBot: message.author.bot
46
+ };
47
+ this.options.logger?.info("Discord gateway received message", {
48
+ guildId: event.guildId,
49
+ channelId: event.channelId,
50
+ messageId: event.messageId,
51
+ authorBot: event.authorBot
52
+ });
53
+ for (const listener of this.listeners) {
54
+ this.dispatchListener("Discord message listener failed", listener, event, {
55
+ guildId: event.guildId,
56
+ channelId: event.channelId,
57
+ messageId: event.messageId
58
+ });
59
+ }
60
+ });
61
+ client.on(Events.InteractionCreate, (interaction) => {
62
+ if (interaction.isAutocomplete()) {
63
+ if (interaction.commandName === RUN_COMMAND_NAME) {
64
+ this.runBackground("Discord run autocomplete interaction failed", {
65
+ commandName: interaction.commandName,
66
+ guildId: interaction.guildId,
67
+ channelId: interaction.channelId
68
+ }, () => this.handleRunAutocompleteInteraction(interaction));
69
+ }
70
+ else if (interaction.commandName === PRINT_COMMAND_NAME) {
71
+ this.runBackground("Discord print autocomplete interaction failed", {
72
+ commandName: interaction.commandName,
73
+ guildId: interaction.guildId,
74
+ channelId: interaction.channelId
75
+ }, () => this.handlePrintAutocompleteInteraction(interaction));
76
+ }
77
+ return;
78
+ }
79
+ if (!interaction.isChatInputCommand())
80
+ return;
81
+ if (interaction.commandName === REVIEW_COMMAND_NAME) {
82
+ this.runBackground("Discord review interaction failed", interactionLogContext(interaction), () => this.handleReviewInteraction(interaction));
83
+ }
84
+ else if (interaction.commandName === CLEAR_COMMAND_NAME) {
85
+ this.runBackground("Discord clear interaction failed", interactionLogContext(interaction), () => this.handleClearInteraction(interaction));
86
+ }
87
+ else if (interaction.commandName === RUN_COMMAND_NAME) {
88
+ this.runBackground("Discord run interaction failed", interactionLogContext(interaction), () => this.handleRunInteraction(interaction));
89
+ }
90
+ else if (interaction.commandName === STOP_COMMAND_NAME) {
91
+ this.runBackground("Discord stop interaction failed", interactionLogContext(interaction), () => this.handleStopInteraction(interaction));
92
+ }
93
+ else if (interaction.commandName === MEMORIES_COMMAND_NAME) {
94
+ this.runBackground("Discord memories interaction failed", interactionLogContext(interaction), () => this.handleMemoriesInteraction(interaction));
95
+ }
96
+ else if (interaction.commandName === PRINT_COMMAND_NAME) {
97
+ this.runBackground("Discord print interaction failed", interactionLogContext(interaction), () => this.handlePrintInteraction(interaction));
98
+ }
99
+ else if (interaction.commandName === DEBUG_COMMAND_NAME) {
100
+ this.runBackground("Discord debug interaction failed", interactionLogContext(interaction), () => this.handleDebugInteraction(interaction));
101
+ }
102
+ });
103
+ }
104
+ await loginAndWaitUntilReady(client, bot.token);
105
+ this.clients.set(bot.id, client);
106
+ if (bot.id === this.options.orchestrator.id) {
107
+ await registerOrchestratorCommands(client, this.options.logger);
108
+ }
109
+ this.options.logger?.info("Discord bot connected", {
110
+ botId: bot.id,
111
+ displayName: bot.displayName,
112
+ kind: bot.id === this.options.orchestrator.id ? "orchestrator" : "waifu"
113
+ });
114
+ }));
115
+ return {
116
+ connected: this.clients.size > 0,
117
+ orchestratorConnected: this.clients.has(this.options.orchestrator.id),
118
+ waifuBotCount: (this.options.waifus ?? []).filter((bot) => this.clients.has(bot.id)).length,
119
+ warnings: this.clients.size > 0
120
+ ? []
121
+ : ["No enabled Discord bot tokens are configured."]
122
+ };
123
+ }
124
+ async disconnect() {
125
+ await Promise.all([...this.clients.values()].map((client) => client.destroy()));
126
+ this.clients.clear();
127
+ }
128
+ onMessage(listener) {
129
+ this.listeners.add(listener);
130
+ return () => this.listeners.delete(listener);
131
+ }
132
+ onReviewCommand(listener) {
133
+ this.reviewListeners.add(listener);
134
+ return () => this.reviewListeners.delete(listener);
135
+ }
136
+ onClearCommand(listener) {
137
+ this.clearListeners.add(listener);
138
+ return () => this.clearListeners.delete(listener);
139
+ }
140
+ onRunCommand(listener) {
141
+ this.runListeners.add(listener);
142
+ return () => this.runListeners.delete(listener);
143
+ }
144
+ onRunWaifuAutocomplete(listener) {
145
+ this.runWaifuAutocompleteListeners.add(listener);
146
+ return () => this.runWaifuAutocompleteListeners.delete(listener);
147
+ }
148
+ onStopCommand(listener) {
149
+ this.stopListeners.add(listener);
150
+ return () => this.stopListeners.delete(listener);
151
+ }
152
+ onMemoriesCommand(listener) {
153
+ this.memoriesListeners.add(listener);
154
+ return () => this.memoriesListeners.delete(listener);
155
+ }
156
+ onPrintCommand(listener) {
157
+ this.printListeners.add(listener);
158
+ return () => this.printListeners.delete(listener);
159
+ }
160
+ onPrintWaifuAutocomplete(listener) {
161
+ this.printWaifuAutocompleteListeners.add(listener);
162
+ return () => this.printWaifuAutocompleteListeners.delete(listener);
163
+ }
164
+ onDebugCommand(listener) {
165
+ this.debugListeners.add(listener);
166
+ return () => this.debugListeners.delete(listener);
167
+ }
168
+ async listGuilds() {
169
+ const client = this.orchestratorClient();
170
+ await client.guilds.fetch();
171
+ return client.guilds.cache.map((guild) => ({
172
+ guildId: guild.id,
173
+ name: guild.name
174
+ }));
175
+ }
176
+ async fetchChannelMetadata(input) {
177
+ const client = this.orchestratorClient();
178
+ const [guild, channel] = await Promise.all([
179
+ client.guilds.fetch(input.guildId),
180
+ client.channels.fetch(input.channelId)
181
+ ]);
182
+ if (!channel) {
183
+ throw new Error(`Discord channel ${input.channelId} is not accessible to the orchestrator bot.`);
184
+ }
185
+ const channelGuildId = "guildId" in channel && typeof channel.guildId === "string" ? channel.guildId : undefined;
186
+ if (channelGuildId && channelGuildId !== input.guildId) {
187
+ throw new Error(`Discord channel ${input.channelId} does not belong to guild ${input.guildId}.`);
188
+ }
189
+ return {
190
+ guildId: guild.id,
191
+ guildName: guild.name,
192
+ channelId: channel.id,
193
+ channelName: "name" in channel && typeof channel.name === "string" ? channel.name : undefined
194
+ };
195
+ }
196
+ async fetchFreshContext(input) {
197
+ throwIfAborted(input.signal);
198
+ const client = this.orchestratorClient();
199
+ let cache = await this.cacheForGuild(input.guildId);
200
+ const channel = await client.channels.fetch(input.channelId);
201
+ if (!channel || !("messages" in channel)) {
202
+ return [];
203
+ }
204
+ const collection = await channel.messages.fetch({ limit: input.limit });
205
+ const rawMessages = [...collection.values()].sort((a, b) => a.createdTimestamp - b.createdTimestamp);
206
+ const mentionedRoles = uniqueMentionedRoles(rawMessages, cache.roles ?? []);
207
+ const roleCacheForResolution = mergeRoleCaches(cache.roles ?? [], mentionedRoles);
208
+ const missingMentionIds = unresolvedMentionIdsByType(rawMessages.map((message) => message.content), cache.members ?? [], roleCacheForResolution);
209
+ if ((missingMentionIds.userIds.length > 0 || missingMentionIds.roleIds.length > 0) && this.shouldRefreshMentions(input.guildId)) {
210
+ this.options.logger?.info("Refreshing Discord mention caches for unresolved mentions", {
211
+ guildId: input.guildId,
212
+ channelId: input.channelId,
213
+ userMentionCount: missingMentionIds.userIds.length,
214
+ roleMentionCount: missingMentionIds.roleIds.length,
215
+ mentionIds: [...missingMentionIds.userIds, ...missingMentionIds.roleIds].slice(0, 5)
216
+ });
217
+ try {
218
+ const [refreshedMembers, refreshedRoles] = await Promise.all([
219
+ missingMentionIds.userIds.length > 0 ? this.options.refreshMembersForGuild?.(input.guildId) : undefined,
220
+ missingMentionIds.roleIds.length > 0 ? this.options.refreshRolesForGuild?.(input.guildId) : undefined
221
+ ]);
222
+ cache = {
223
+ ...cache,
224
+ ...refreshedMembers,
225
+ ...refreshedRoles
226
+ };
227
+ }
228
+ catch (error) {
229
+ this.options.logger?.warn("Discord mention cache refresh for unresolved mentions failed", {
230
+ guildId: input.guildId,
231
+ channelId: input.channelId,
232
+ message: error instanceof Error ? error.message : String(error)
233
+ });
234
+ }
235
+ }
236
+ const messages = await Promise.all(rawMessages.map(async (message) => {
237
+ const [replyTo, reactions] = await Promise.all([
238
+ message.reference?.messageId
239
+ ? resolveReplyReference(message).catch(() => ({ messageId: message.reference?.messageId ?? "" }))
240
+ : undefined,
241
+ summarizeReactions(message.reactions.cache.values(), cache)
242
+ ]);
243
+ return {
244
+ id: message.id,
245
+ channelId: message.channelId,
246
+ guildId: message.guildId ?? input.guildId,
247
+ authorId: message.author.id,
248
+ authorName: message.author.username,
249
+ authorDisplayName: message.member?.displayName ?? message.author.globalName ?? message.author.username,
250
+ authorBot: message.author.bot,
251
+ content: message.content,
252
+ createdAt: message.createdAt,
253
+ images: collectImageAttachments(message),
254
+ replyTo,
255
+ reactions
256
+ };
257
+ }));
258
+ return buildNormalizedDiscordContext(messages, { ...cache, roles: mergeRoleCaches(cache.roles ?? [], mentionedRoles) }, {
259
+ orchestratorAuthorIds: botAuthorIds([this.options.orchestrator]),
260
+ waifuAuthorIds: botAuthorIds(this.options.waifus ?? [])
261
+ }).messages;
262
+ }
263
+ async sendWaifuMessage(input) {
264
+ const client = input.senderBotId ? this.clients.get(input.senderBotId) : this.orchestratorClient();
265
+ if (!client) {
266
+ throw new Error(input.senderBotId
267
+ ? `Waifu Discord bot ${input.senderBotId} is not connected.`
268
+ : "Orchestrator Discord client is not connected.");
269
+ }
270
+ const cache = await this.cacheForGuild(input.guildId);
271
+ const channel = await client.channels.fetch(input.channelId);
272
+ if (!channel || !("send" in channel)) {
273
+ throw new Error(`Discord channel ${input.channelId} is not sendable.`);
274
+ }
275
+ const denormalized = denormalizeModelContentForDiscord(input.content, {
276
+ ...cache,
277
+ activeAuthorIds: input.allowedUserMentionIds
278
+ });
279
+ const replyToMessageId = sanitizeReplyTarget(input.replyToMessageId);
280
+ if (input.replyToMessageId && !replyToMessageId) {
281
+ this.options.logger?.warn("Dropping non-snowflake replyToMessageId", {
282
+ guildId: input.guildId,
283
+ channelId: input.channelId,
284
+ value: input.replyToMessageId
285
+ });
286
+ }
287
+ const sent = await channel.send({
288
+ content: denormalized.content,
289
+ allowedMentions: denormalized.allowedMentions,
290
+ reply: replyToMessageId ? { messageReference: replyToMessageId } : undefined
291
+ });
292
+ return { messageId: sent.id };
293
+ }
294
+ async sendTyping(input) {
295
+ const client = input.senderBotId ? this.clients.get(input.senderBotId) : this.orchestratorClient();
296
+ if (!client)
297
+ return;
298
+ try {
299
+ const channel = await client.channels.fetch(input.channelId);
300
+ if (channel && "sendTyping" in channel) {
301
+ await channel.sendTyping();
302
+ }
303
+ }
304
+ catch (error) {
305
+ this.options.logger?.warn("Failed to send typing indicator", {
306
+ guildId: input.guildId,
307
+ channelId: input.channelId,
308
+ senderBotId: input.senderBotId,
309
+ message: error instanceof Error ? error.message : String(error)
310
+ });
311
+ }
312
+ }
313
+ async validateDebugChannel(input) {
314
+ const channel = await this.orchestratorClient().channels.fetch(input.channelId);
315
+ if (!channel) {
316
+ throw new Error(`Discord channel ${input.channelId} is not accessible to the orchestrator bot.`);
317
+ }
318
+ if (input.requireMessages && !("messages" in channel)) {
319
+ throw new Error(`Discord channel ${input.channelId} is not readable by the orchestrator bot.`);
320
+ }
321
+ if (input.requireSend && !("send" in channel)) {
322
+ throw new Error(`Discord channel ${input.channelId} is not sendable by the orchestrator bot.`);
323
+ }
324
+ return {
325
+ channelId: input.channelId,
326
+ guildId: "guildId" in channel && typeof channel.guildId === "string" ? channel.guildId : undefined,
327
+ name: "name" in channel && typeof channel.name === "string" ? channel.name : undefined
328
+ };
329
+ }
330
+ async sendDebugMessage(input) {
331
+ const channel = await this.orchestratorClient().channels.fetch(input.channelId);
332
+ if (!channel || !("send" in channel)) {
333
+ throw new Error(`Discord channel ${input.channelId} is not sendable by the orchestrator bot.`);
334
+ }
335
+ const sent = await channel.send({
336
+ content: input.content,
337
+ allowedMentions: { parse: [], users: [], roles: [], repliedUser: false }
338
+ });
339
+ return { messageId: sent.id };
340
+ }
341
+ async deleteMessages(input) {
342
+ const result = {
343
+ deletedMessageIds: [],
344
+ failedMessageIds: []
345
+ };
346
+ if (input.messageIds.length === 0)
347
+ return result;
348
+ const orchestrator = this.orchestratorClient();
349
+ const orchestratorChannel = await orchestrator.channels.fetch(input.channelId);
350
+ if (!orchestratorChannel || !("messages" in orchestratorChannel)) {
351
+ throw new Error(`Discord channel ${input.channelId} is not message-manageable.`);
352
+ }
353
+ const remaining = new Set(input.messageIds);
354
+ const supportsBulkDelete = "bulkDelete" in orchestratorChannel &&
355
+ typeof orchestratorChannel.bulkDelete === "function";
356
+ if (input.messageIds.length >= 2 && supportsBulkDelete) {
357
+ try {
358
+ const bulkChannel = orchestratorChannel;
359
+ const bulkDeleted = await bulkChannel.bulkDelete(input.messageIds, true);
360
+ for (const id of bulkDeleted.keys()) {
361
+ if (remaining.has(id)) {
362
+ result.deletedMessageIds.push(id);
363
+ remaining.delete(id);
364
+ }
365
+ }
366
+ }
367
+ catch (error) {
368
+ const message = error instanceof Error ? error.message : String(error);
369
+ this.options.logger?.warn("Discord bulk delete failed, falling back to per-message deletes", {
370
+ guildId: input.guildId,
371
+ channelId: input.channelId,
372
+ messageCount: input.messageIds.length,
373
+ message
374
+ });
375
+ }
376
+ }
377
+ if (remaining.size === 0)
378
+ return result;
379
+ const channelByBotId = new Map();
380
+ channelByBotId.set(this.options.orchestrator.id, orchestratorChannel);
381
+ for (const messageId of input.messageIds) {
382
+ if (!remaining.has(messageId))
383
+ continue;
384
+ const messageAuthorId = input.authorIdByMessageId?.[messageId] ?? input.authorId;
385
+ const client = this.clientForAuthor(messageAuthorId);
386
+ const botId = this.botIdForClient(client);
387
+ let channel = botId ? channelByBotId.get(botId) : undefined;
388
+ try {
389
+ if (!channel) {
390
+ const fetched = await client.channels.fetch(input.channelId);
391
+ if (!fetched || !("messages" in fetched)) {
392
+ throw new Error(`Discord channel ${input.channelId} is not message-manageable from this bot.`);
393
+ }
394
+ channel = fetched;
395
+ if (botId)
396
+ channelByBotId.set(botId, fetched);
397
+ }
398
+ await channel.messages.delete(messageId);
399
+ result.deletedMessageIds.push(messageId);
400
+ }
401
+ catch (error) {
402
+ const message = error instanceof Error ? error.message : String(error);
403
+ result.failedMessageIds.push({ messageId, message });
404
+ this.options.logger?.warn("Failed to delete Discord message", {
405
+ guildId: input.guildId,
406
+ channelId: input.channelId,
407
+ messageId,
408
+ authorId: messageAuthorId,
409
+ deletingBotId: botId,
410
+ message
411
+ });
412
+ }
413
+ }
414
+ return result;
415
+ }
416
+ async deleteAllMessages(input) {
417
+ const result = {
418
+ scannedMessageCount: 0,
419
+ deletedCount: 0,
420
+ failedCount: 0,
421
+ failedMessageIds: []
422
+ };
423
+ const orchestrator = this.orchestratorClient();
424
+ const fetchedChannel = await orchestrator.channels.fetch(input.channelId);
425
+ if (!fetchedChannel || !("messages" in fetchedChannel)) {
426
+ throw new Error(`Discord channel ${input.channelId} is not message-manageable.`);
427
+ }
428
+ const channel = fetchedChannel;
429
+ let before;
430
+ while (true) {
431
+ const page = await channel.messages.fetch(before ? { limit: 100, before } : { limit: 100 });
432
+ const messages = [...page.values()];
433
+ if (messages.length === 0)
434
+ break;
435
+ result.scannedMessageCount += messages.length;
436
+ await this.deleteFetchedMessages({
437
+ guildId: input.guildId,
438
+ channelId: input.channelId,
439
+ channel,
440
+ messages,
441
+ result
442
+ });
443
+ const nextBefore = oldestMessageId(messages);
444
+ if (!nextBefore || nextBefore === before)
445
+ break;
446
+ before = nextBefore;
447
+ }
448
+ return result;
449
+ }
450
+ async deleteFetchedMessages(input) {
451
+ const remaining = new Set(input.messages.map((message) => message.id));
452
+ const bulkIds = input.messages
453
+ .filter((message) => isBulkDeletableMessage(message))
454
+ .map((message) => message.id);
455
+ if (bulkIds.length >= 2 && input.channel.bulkDelete) {
456
+ for (const chunk of chunks(bulkIds, DISCORD_BULK_DELETE_MAX_MESSAGES)) {
457
+ if (chunk.length < 2)
458
+ continue;
459
+ try {
460
+ const bulkDeleted = await input.channel.bulkDelete(chunk, true);
461
+ for (const id of bulkDeleted.keys()) {
462
+ if (remaining.delete(id)) {
463
+ input.result.deletedCount += 1;
464
+ }
465
+ }
466
+ }
467
+ catch (error) {
468
+ this.options.logger?.warn("Discord bulk delete failed during full channel clear", {
469
+ guildId: input.guildId,
470
+ channelId: input.channelId,
471
+ messageCount: chunk.length,
472
+ message: error instanceof Error ? error.message : String(error)
473
+ });
474
+ }
475
+ }
476
+ }
477
+ for (const message of input.messages) {
478
+ if (!remaining.has(message.id))
479
+ continue;
480
+ try {
481
+ await input.channel.messages.delete(message.id);
482
+ remaining.delete(message.id);
483
+ input.result.deletedCount += 1;
484
+ }
485
+ catch (error) {
486
+ const errorMessage = error instanceof Error ? error.message : String(error);
487
+ remaining.delete(message.id);
488
+ input.result.failedCount += 1;
489
+ input.result.failedMessageIds.push({ messageId: message.id, message: errorMessage });
490
+ this.options.logger?.warn("Failed to delete Discord message during full channel clear", {
491
+ guildId: input.guildId,
492
+ channelId: input.channelId,
493
+ messageId: message.id,
494
+ message: errorMessage
495
+ });
496
+ }
497
+ }
498
+ }
499
+ async cacheForGuild(guildId) {
500
+ return this.options.cacheForGuild?.(guildId) ?? {
501
+ members: this.options.members,
502
+ emojis: this.options.emojis
503
+ };
504
+ }
505
+ orchestratorClient() {
506
+ const client = this.clients.get(this.options.orchestrator.id);
507
+ if (!client) {
508
+ throw new Error("Orchestrator Discord client is not connected.");
509
+ }
510
+ return client;
511
+ }
512
+ clientForAuthor(authorId) {
513
+ if (authorId) {
514
+ const bots = [this.options.orchestrator, ...(this.options.waifus ?? [])];
515
+ const bot = bots.find((candidate) => candidate.id === authorId || candidate.applicationId === authorId);
516
+ const client = bot ? this.clients.get(bot.id) : undefined;
517
+ if (client) {
518
+ return client;
519
+ }
520
+ }
521
+ return this.orchestratorClient();
522
+ }
523
+ botIdForClient(client) {
524
+ for (const [botId, candidate] of this.clients) {
525
+ if (candidate === client)
526
+ return botId;
527
+ }
528
+ return undefined;
529
+ }
530
+ shouldRefreshMentions(guildId) {
531
+ if (!this.options.refreshMembersForGuild)
532
+ return false;
533
+ const now = Date.now();
534
+ const previous = this.recentMentionRefreshes.get(guildId) ?? 0;
535
+ if (now - previous < MENTION_REFRESH_COOLDOWN_MS) {
536
+ return false;
537
+ }
538
+ this.recentMentionRefreshes.set(guildId, now);
539
+ return true;
540
+ }
541
+ attachClientRuntimeHandlers(client, botId) {
542
+ client.on(Events.Error, (error) => {
543
+ this.reportRuntimeIssue({
544
+ source: "client-error",
545
+ botId,
546
+ message: errorMessage(error),
547
+ error
548
+ });
549
+ });
550
+ client.on(Events.ShardError, (error, shardId) => {
551
+ this.reportRuntimeIssue({
552
+ source: "shard-error",
553
+ botId,
554
+ shardId,
555
+ message: errorMessage(error),
556
+ error
557
+ });
558
+ });
559
+ client.on(Events.ShardDisconnect, (event, shardId) => {
560
+ const code = closeEventCode(event);
561
+ const reason = closeEventReason(event);
562
+ this.reportRuntimeIssue({
563
+ source: "shard-disconnect",
564
+ botId,
565
+ shardId,
566
+ code,
567
+ wasClean: closeEventWasClean(event),
568
+ message: `Discord shard disconnected${code === undefined ? "" : ` with code ${code}`}${reason ? `: ${reason}` : "."}`
569
+ });
570
+ });
571
+ client.on(Events.Invalidated, () => {
572
+ this.reportRuntimeIssue({
573
+ source: "invalidated",
574
+ botId,
575
+ message: "Discord gateway session was invalidated."
576
+ });
577
+ });
578
+ client.on(Events.Warn, (message) => {
579
+ this.reportRuntimeIssue({
580
+ source: "warn",
581
+ botId,
582
+ message
583
+ });
584
+ });
585
+ }
586
+ reportRuntimeIssue(issue) {
587
+ const context = runtimeIssueLogContext(issue);
588
+ if (issue.source === "warn") {
589
+ this.options.logger?.warn("Discord gateway warning", context);
590
+ }
591
+ else {
592
+ this.options.logger?.error("Discord gateway runtime issue", context);
593
+ }
594
+ if (!this.options.onRuntimeIssue)
595
+ return;
596
+ this.runBackground("Discord runtime issue callback failed", context, () => this.options.onRuntimeIssue?.(issue));
597
+ }
598
+ async callListener(label, listener, event, context) {
599
+ try {
600
+ await listener(event);
601
+ }
602
+ catch (error) {
603
+ this.options.logger?.error(label, {
604
+ ...context,
605
+ message: error instanceof Error ? error.message : String(error)
606
+ });
607
+ }
608
+ }
609
+ dispatchListener(label, listener, event, context) {
610
+ void this.callListener(label, listener, event, context);
611
+ }
612
+ runBackground(label, context, task) {
613
+ void Promise.resolve()
614
+ .then(task)
615
+ .catch((error) => {
616
+ this.options.logger?.error(label, {
617
+ ...context,
618
+ message: error instanceof Error ? error.message : String(error)
619
+ });
620
+ });
621
+ }
622
+ async handleReviewInteraction(interaction) {
623
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
624
+ if (!interaction.guildId || !interaction.channelId) {
625
+ await interaction.editReply("/review can only be used in a server channel.");
626
+ return;
627
+ }
628
+ const event = {
629
+ guildId: interaction.guildId,
630
+ channelId: interaction.channelId,
631
+ userId: interaction.user.id,
632
+ respond: async (content) => {
633
+ await interaction.editReply(content);
634
+ }
635
+ };
636
+ for (const listener of this.reviewListeners) {
637
+ this.dispatchListener("Discord review command listener failed", listener, event, slashCommandLogContext(event));
638
+ }
639
+ }
640
+ async handleClearInteraction(interaction) {
641
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
642
+ if (!interaction.guildId || !interaction.channelId) {
643
+ await interaction.editReply("/clear can only be used in a server channel.");
644
+ return;
645
+ }
646
+ const event = {
647
+ guildId: interaction.guildId,
648
+ channelId: interaction.channelId,
649
+ userId: interaction.user.id,
650
+ count: interaction.options.getInteger(CLEAR_COUNT_OPTION_NAME) ?? undefined,
651
+ type: parseClearType(interaction.options.getString(CLEAR_TYPE_OPTION_NAME)),
652
+ respond: async (content) => {
653
+ await interaction.editReply(content);
654
+ }
655
+ };
656
+ for (const listener of this.clearListeners) {
657
+ this.dispatchListener("Discord clear command listener failed", listener, event, slashCommandLogContext(event));
658
+ }
659
+ }
660
+ async handleRunInteraction(interaction) {
661
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
662
+ if (!interaction.guildId || !interaction.channelId) {
663
+ await interaction.editReply("/run can only be used in a server channel.");
664
+ return;
665
+ }
666
+ const event = {
667
+ guildId: interaction.guildId,
668
+ channelId: interaction.channelId,
669
+ userId: interaction.user.id,
670
+ waifuId: sanitizeRunString(interaction.options.getString(RUN_WAIFU_OPTION_NAME)),
671
+ sceneDirection: sanitizeRunString(interaction.options.getString(RUN_SCENE_DIRECTION_OPTION_NAME)),
672
+ respond: async (content) => {
673
+ await interaction.editReply(content);
674
+ }
675
+ };
676
+ for (const listener of this.runListeners) {
677
+ this.dispatchListener("Discord run command listener failed", listener, event, slashCommandLogContext(event));
678
+ }
679
+ }
680
+ async handleRunAutocompleteInteraction(interaction) {
681
+ const focused = interaction.options.getFocused(true);
682
+ if (focused.name !== RUN_WAIFU_OPTION_NAME) {
683
+ await interaction.respond([]);
684
+ return;
685
+ }
686
+ const event = {
687
+ guildId: interaction.guildId ?? undefined,
688
+ channelId: interaction.channelId ?? undefined,
689
+ focusedValue: typeof focused.value === "string" ? focused.value : String(focused.value ?? ""),
690
+ respond: async (choices) => {
691
+ await interaction.respond(choices.slice(0, MAX_AUTOCOMPLETE_CHOICES));
692
+ }
693
+ };
694
+ if (this.runWaifuAutocompleteListeners.size === 0) {
695
+ await interaction.respond([]);
696
+ return;
697
+ }
698
+ await Promise.all([...this.runWaifuAutocompleteListeners].map((listener) => this.callListener("Discord run autocomplete listener failed", listener, event, {
699
+ guildId: event.guildId,
700
+ channelId: event.channelId,
701
+ focusedValue: event.focusedValue
702
+ })));
703
+ }
704
+ async handleStopInteraction(interaction) {
705
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
706
+ if (!interaction.guildId || !interaction.channelId) {
707
+ await interaction.editReply("/stop can only be used in a server channel.");
708
+ return;
709
+ }
710
+ const event = {
711
+ guildId: interaction.guildId,
712
+ channelId: interaction.channelId,
713
+ userId: interaction.user.id,
714
+ respond: async (content) => {
715
+ await interaction.editReply(content);
716
+ }
717
+ };
718
+ for (const listener of this.stopListeners) {
719
+ this.dispatchListener("Discord stop command listener failed", listener, event, slashCommandLogContext(event));
720
+ }
721
+ }
722
+ async handleMemoriesInteraction(interaction) {
723
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
724
+ if (!interaction.guildId || !interaction.channelId) {
725
+ await interaction.editReply("/memories can only be used in a server channel.");
726
+ return;
727
+ }
728
+ const event = {
729
+ guildId: interaction.guildId,
730
+ channelId: interaction.channelId,
731
+ userId: interaction.user.id,
732
+ respond: async (content) => {
733
+ await interaction.editReply(content);
734
+ }
735
+ };
736
+ for (const listener of this.memoriesListeners) {
737
+ this.dispatchListener("Discord memories command listener failed", listener, event, slashCommandLogContext(event));
738
+ }
739
+ }
740
+ async handlePrintInteraction(interaction) {
741
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
742
+ if (!interaction.guildId || !interaction.channelId) {
743
+ await interaction.editReply("/print can only be used in a server channel.");
744
+ return;
745
+ }
746
+ const event = {
747
+ guildId: interaction.guildId,
748
+ channelId: interaction.channelId,
749
+ userId: interaction.user.id,
750
+ type: parsePrintCommandType(interaction.options.getString(PRINT_TYPE_OPTION_NAME)),
751
+ waifuId: sanitizeRunString(interaction.options.getString(PRINT_WAIFU_OPTION_NAME)),
752
+ respond: async (content) => {
753
+ await interaction.editReply(content);
754
+ }
755
+ };
756
+ for (const listener of this.printListeners) {
757
+ this.dispatchListener("Discord print command listener failed", listener, event, {
758
+ ...slashCommandLogContext(event),
759
+ type: event.type,
760
+ waifuId: event.waifuId
761
+ });
762
+ }
763
+ }
764
+ async handlePrintAutocompleteInteraction(interaction) {
765
+ const focused = interaction.options.getFocused(true);
766
+ if (focused.name !== PRINT_WAIFU_OPTION_NAME) {
767
+ await interaction.respond([]);
768
+ return;
769
+ }
770
+ const event = {
771
+ guildId: interaction.guildId ?? undefined,
772
+ channelId: interaction.channelId ?? undefined,
773
+ focusedValue: typeof focused.value === "string" ? focused.value : String(focused.value ?? ""),
774
+ respond: async (choices) => {
775
+ await interaction.respond(choices.slice(0, MAX_AUTOCOMPLETE_CHOICES));
776
+ }
777
+ };
778
+ if (this.printWaifuAutocompleteListeners.size === 0) {
779
+ await interaction.respond([]);
780
+ return;
781
+ }
782
+ await Promise.all([...this.printWaifuAutocompleteListeners].map((listener) => this.callListener("Discord print autocomplete listener failed", listener, event, {
783
+ guildId: event.guildId,
784
+ channelId: event.channelId,
785
+ focusedValue: event.focusedValue
786
+ })));
787
+ }
788
+ async handleDebugInteraction(interaction) {
789
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
790
+ if (!interaction.guildId || !interaction.channelId) {
791
+ await interaction.editReply("/console can only be used in a server channel.");
792
+ return;
793
+ }
794
+ const event = {
795
+ guildId: interaction.guildId,
796
+ channelId: interaction.channelId,
797
+ userId: interaction.user.id,
798
+ type: parseDebugCommandType(interaction.options.getString(DEBUG_TYPE_OPTION_NAME)),
799
+ sourceChannelId: sanitizeRunString(interaction.options.getString(DEBUG_CHANNEL_ID_OPTION_NAME)),
800
+ respond: async (content) => {
801
+ await interaction.editReply(content);
802
+ }
803
+ };
804
+ for (const listener of this.debugListeners) {
805
+ this.dispatchListener("Discord debug command listener failed", listener, event, {
806
+ ...slashCommandLogContext(event),
807
+ type: event.type,
808
+ sourceChannelId: event.sourceChannelId
809
+ });
810
+ }
811
+ }
812
+ }
813
+ export class DiscordGatewayNotConfigured {
814
+ async connect() {
815
+ return {
816
+ connected: false,
817
+ orchestratorConnected: false,
818
+ waifuBotCount: 0,
819
+ warnings: ["Discord tokens are not configured yet."]
820
+ };
821
+ }
822
+ async disconnect() { }
823
+ async fetchFreshContext() {
824
+ return [];
825
+ }
826
+ async sendWaifuMessage() {
827
+ throw new Error("Discord sending is not configured yet.");
828
+ }
829
+ async sendTyping() { }
830
+ }
831
+ async function loginAndWaitUntilReady(client, token) {
832
+ if (client.isReady())
833
+ return;
834
+ await Promise.all([
835
+ new Promise((resolve) => {
836
+ client.once(Events.ClientReady, () => resolve());
837
+ }),
838
+ client.login(token)
839
+ ]);
840
+ }
841
+ function botAuthorIds(bots) {
842
+ return bots.flatMap((bot) => [bot.id, bot.applicationId].filter((id) => Boolean(id)));
843
+ }
844
+ function collectImageAttachments(message) {
845
+ if (!message.attachments)
846
+ return undefined;
847
+ const images = [];
848
+ for (const attachment of message.attachments.values()) {
849
+ if (!attachment.url)
850
+ continue;
851
+ const contentType = attachment.contentType ?? undefined;
852
+ if (isImageAttachment(contentType, attachment.name ?? undefined)) {
853
+ images.push({
854
+ url: attachment.url,
855
+ contentType,
856
+ contentLengthBytes: attachment.size ?? undefined
857
+ });
858
+ }
859
+ }
860
+ return images.length ? images : undefined;
861
+ }
862
+ function isImageAttachment(contentType, name) {
863
+ if (contentType)
864
+ return contentType.startsWith("image/");
865
+ return /\.(png|jpe?g|webp|gif)$/i.test(name ?? "");
866
+ }
867
+ async function resolveReplyReference(message) {
868
+ if (!message.reference?.messageId)
869
+ return undefined;
870
+ if (!message.fetchReference) {
871
+ return { messageId: message.reference.messageId };
872
+ }
873
+ const referenced = await message.fetchReference();
874
+ return {
875
+ messageId: message.reference.messageId,
876
+ authorName: referenced.member?.displayName ?? referenced.author.globalName ?? referenced.author.username,
877
+ contentPreview: referenced.content.slice(0, 160)
878
+ };
879
+ }
880
+ async function summarizeReactions(reactions, cache) {
881
+ const membersById = new Map((cache.members ?? []).map((member) => [member.userId, member]));
882
+ const emojisById = new Map((cache.emojis ?? []).map((emoji) => [emoji.id, emoji]));
883
+ return Promise.all([...reactions].map(async (reaction) => {
884
+ let users;
885
+ if (reaction.users) {
886
+ try {
887
+ const fetched = await reaction.users.fetch({ limit: 20 });
888
+ users = [...fetched.values()].map((user) => {
889
+ const member = membersById.get(user.id);
890
+ return member?.guildDisplayName ?? member?.globalDisplayName ?? user.globalName ?? user.username;
891
+ });
892
+ }
893
+ catch {
894
+ users = undefined;
895
+ }
896
+ }
897
+ return {
898
+ emoji: modelVisibleReactionEmoji(reaction.emoji, emojisById),
899
+ count: reaction.count ?? 0,
900
+ users
901
+ };
902
+ }));
903
+ }
904
+ function modelVisibleReactionEmoji(emoji, emojisById) {
905
+ if (emoji.id) {
906
+ const cached = emojisById.get(emoji.id);
907
+ if (cached) {
908
+ return `<${cached.animated ? "a" : ""}:${cached.name}:>`;
909
+ }
910
+ return emoji.name ? `:${emoji.name}:` : emoji.identifier;
911
+ }
912
+ return emoji.name ?? emoji.identifier;
913
+ }
914
+ function uniqueMentionedRoles(messages, cachedRoles) {
915
+ const cachedById = new Map(cachedRoles.map((role) => [role.id, role]));
916
+ const rolesById = new Map();
917
+ for (const message of messages) {
918
+ for (const role of message.mentions?.roles?.values() ?? []) {
919
+ rolesById.set(role.id, cachedById.get(role.id) ?? {
920
+ id: role.id,
921
+ name: role.name,
922
+ color: 0,
923
+ hoist: false,
924
+ mentionable: false,
925
+ managed: false,
926
+ fetchedAt: new Date().toISOString()
927
+ });
928
+ }
929
+ }
930
+ return [...rolesById.values()];
931
+ }
932
+ function mergeRoleCaches(primary, fallback) {
933
+ const rolesById = new Map(fallback.map((role) => [role.id, role]));
934
+ for (const role of primary) {
935
+ rolesById.set(role.id, role);
936
+ }
937
+ return [...rolesById.values()];
938
+ }
939
+ function throwIfAborted(signal) {
940
+ if (signal?.aborted) {
941
+ throw signal.reason instanceof Error ? signal.reason : new Error("Discord request aborted.");
942
+ }
943
+ }
944
+ const SNOWFLAKE_PATTERN = /^\d{17,20}$/;
945
+ const MENTION_REFRESH_COOLDOWN_MS = 5 * 60 * 1000;
946
+ const REVIEW_COMMAND_NAME = "review";
947
+ const CLEAR_COMMAND_NAME = "clear";
948
+ const RUN_COMMAND_NAME = "run";
949
+ const STOP_COMMAND_NAME = "stop";
950
+ const MEMORIES_COMMAND_NAME = "memories";
951
+ const PRINT_COMMAND_NAME = "print";
952
+ const DEBUG_COMMAND_NAME = "console";
953
+ const LEGACY_DEBUG_COMMAND_NAMES = ["debug"];
954
+ const CLEAR_COUNT_OPTION_NAME = "count";
955
+ const CLEAR_TYPE_OPTION_NAME = "type";
956
+ const PRINT_TYPE_OPTION_NAME = "type";
957
+ const PRINT_WAIFU_OPTION_NAME = "waifu";
958
+ const DEBUG_TYPE_OPTION_NAME = "type";
959
+ const DEBUG_CHANNEL_ID_OPTION_NAME = "channel_id";
960
+ const RUN_WAIFU_OPTION_NAME = "waifu";
961
+ const RUN_SCENE_DIRECTION_OPTION_NAME = "scene_direction";
962
+ const MAX_CLEAR_COUNT = 100;
963
+ const MAX_AUTOCOMPLETE_CHOICES = 25;
964
+ const DISCORD_BULK_DELETE_MAX_MESSAGES = 100;
965
+ const DISCORD_BULK_DELETE_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
966
+ const ADMIN_COMMAND_PERMISSIONS = PermissionFlagsBits.Administrator;
967
+ function sanitizeReplyTarget(value) {
968
+ return value && SNOWFLAKE_PATTERN.test(value) ? value : undefined;
969
+ }
970
+ function sanitizeRunString(value) {
971
+ const trimmed = value?.trim();
972
+ return trimmed || undefined;
973
+ }
974
+ function parseDebugCommandType(value) {
975
+ return value === "set" || value === "unset" || value === "print" ? value : undefined;
976
+ }
977
+ function parsePrintCommandType(value) {
978
+ return value === "system_prompt" || value === "memories" || value === "personality" ? value : undefined;
979
+ }
980
+ function errorMessage(error) {
981
+ return error instanceof Error ? error.message : String(error);
982
+ }
983
+ function closeEventCode(event) {
984
+ return typeof event.code === "string" || typeof event.code === "number" ? event.code : undefined;
985
+ }
986
+ function closeEventReason(event) {
987
+ return typeof event.reason === "string" && event.reason.trim() ? event.reason.trim() : undefined;
988
+ }
989
+ function closeEventWasClean(event) {
990
+ return typeof event.wasClean === "boolean" ? event.wasClean : undefined;
991
+ }
992
+ function runtimeIssueLogContext(issue) {
993
+ return {
994
+ source: issue.source,
995
+ botId: issue.botId,
996
+ shardId: issue.shardId,
997
+ code: issue.code,
998
+ wasClean: issue.wasClean,
999
+ message: issue.message
1000
+ };
1001
+ }
1002
+ function interactionLogContext(interaction) {
1003
+ return {
1004
+ commandName: interaction.commandName,
1005
+ guildId: interaction.guildId,
1006
+ channelId: interaction.channelId,
1007
+ userId: interaction.user.id
1008
+ };
1009
+ }
1010
+ function slashCommandLogContext(event) {
1011
+ return {
1012
+ guildId: event.guildId,
1013
+ channelId: event.channelId,
1014
+ userId: event.userId,
1015
+ commandMessageId: event.commandMessageId
1016
+ };
1017
+ }
1018
+ function isBulkDeletableMessage(message) {
1019
+ return message.createdTimestamp === undefined || Date.now() - message.createdTimestamp < DISCORD_BULK_DELETE_MAX_AGE_MS;
1020
+ }
1021
+ function oldestMessageId(messages) {
1022
+ if (messages.length === 0)
1023
+ return undefined;
1024
+ if (messages.some((message) => typeof message.createdTimestamp !== "number")) {
1025
+ return messages[messages.length - 1]?.id;
1026
+ }
1027
+ let oldest = messages[0];
1028
+ for (const message of messages.slice(1)) {
1029
+ if ((message.createdTimestamp ?? 0) < (oldest.createdTimestamp ?? 0)) {
1030
+ oldest = message;
1031
+ }
1032
+ }
1033
+ return oldest.id;
1034
+ }
1035
+ function chunks(values, size) {
1036
+ const result = [];
1037
+ for (let index = 0; index < values.length; index += size) {
1038
+ result.push(values.slice(index, index + size));
1039
+ }
1040
+ return result;
1041
+ }
1042
+ async function registerOrchestratorCommands(client, logger) {
1043
+ try {
1044
+ const manager = client.application?.commands;
1045
+ if (!manager)
1046
+ return;
1047
+ const payloads = buildOrchestratorCommandPayloads();
1048
+ const commands = await manager.fetch();
1049
+ for (const payload of payloads) {
1050
+ const existing = commands.find((command) => command.name === payload.name);
1051
+ if (existing) {
1052
+ await manager.edit(existing.id, payload);
1053
+ }
1054
+ else {
1055
+ await manager.create(payload);
1056
+ }
1057
+ }
1058
+ const payloadNames = new Set(payloads.map((payload) => payload.name));
1059
+ for (const legacyName of LEGACY_DEBUG_COMMAND_NAMES) {
1060
+ if (payloadNames.has(legacyName))
1061
+ continue;
1062
+ const existing = commands.find((command) => command.name === legacyName);
1063
+ if (existing) {
1064
+ await manager.delete(existing.id);
1065
+ }
1066
+ }
1067
+ }
1068
+ catch (error) {
1069
+ logger?.warn("Failed to register orchestrator slash commands", {
1070
+ message: error instanceof Error ? error.message : String(error)
1071
+ });
1072
+ }
1073
+ }
1074
+ export function buildOrchestratorCommandPayloads() {
1075
+ return [
1076
+ {
1077
+ name: REVIEW_COMMAND_NAME,
1078
+ description: "Review and remove the latest waifu message if it leaked hallucinated internals.",
1079
+ defaultMemberPermissions: ADMIN_COMMAND_PERMISSIONS
1080
+ },
1081
+ {
1082
+ name: CLEAR_COMMAND_NAME,
1083
+ description: "Delete channel messages without running reviewer judgment.",
1084
+ defaultMemberPermissions: ADMIN_COMMAND_PERMISSIONS,
1085
+ options: [
1086
+ {
1087
+ type: ApplicationCommandOptionType.String,
1088
+ name: CLEAR_TYPE_OPTION_NAME,
1089
+ description: "Which messages to clear.",
1090
+ required: true,
1091
+ choices: [
1092
+ { name: "waifus", value: "waifus" },
1093
+ { name: "users", value: "users" },
1094
+ { name: "both", value: "both" },
1095
+ { name: "everything", value: "everything" }
1096
+ ]
1097
+ },
1098
+ {
1099
+ type: ApplicationCommandOptionType.Integer,
1100
+ name: CLEAR_COUNT_OPTION_NAME,
1101
+ description: "How many latest messages to delete for waifus, users, or both.",
1102
+ required: false,
1103
+ min_value: 1,
1104
+ max_value: MAX_CLEAR_COUNT
1105
+ }
1106
+ ]
1107
+ },
1108
+ {
1109
+ name: RUN_COMMAND_NAME,
1110
+ description: "Run the orchestrator in this channel immediately if the runtime is idle.",
1111
+ options: [
1112
+ {
1113
+ type: ApplicationCommandOptionType.String,
1114
+ name: RUN_WAIFU_OPTION_NAME,
1115
+ description: "Optional waifu id or display name to speak first.",
1116
+ required: false,
1117
+ autocomplete: true
1118
+ },
1119
+ {
1120
+ type: ApplicationCommandOptionType.String,
1121
+ name: RUN_SCENE_DIRECTION_OPTION_NAME,
1122
+ description: "Optional private scene direction for the selected waifu.",
1123
+ required: false
1124
+ }
1125
+ ]
1126
+ },
1127
+ {
1128
+ name: STOP_COMMAND_NAME,
1129
+ description: "Stop the current orchestrator and waifu work in this channel.",
1130
+ defaultMemberPermissions: ADMIN_COMMAND_PERMISSIONS
1131
+ },
1132
+ {
1133
+ name: MEMORIES_COMMAND_NAME,
1134
+ description: "Run the stage manager for this channel and update guild memories.",
1135
+ defaultMemberPermissions: ADMIN_COMMAND_PERMISSIONS
1136
+ },
1137
+ {
1138
+ name: PRINT_COMMAND_NAME,
1139
+ description: "Print a configured waifu field for this server.",
1140
+ defaultMemberPermissions: ADMIN_COMMAND_PERMISSIONS,
1141
+ options: [
1142
+ {
1143
+ type: ApplicationCommandOptionType.String,
1144
+ name: PRINT_TYPE_OPTION_NAME,
1145
+ description: "Which waifu field to print.",
1146
+ required: true,
1147
+ choices: [
1148
+ { name: "system prompt", value: "system_prompt" },
1149
+ { name: "memories", value: "memories" },
1150
+ { name: "personality", value: "personality" }
1151
+ ]
1152
+ },
1153
+ {
1154
+ type: ApplicationCommandOptionType.String,
1155
+ name: PRINT_WAIFU_OPTION_NAME,
1156
+ description: "Waifu to inspect from this server.",
1157
+ required: true,
1158
+ autocomplete: true
1159
+ }
1160
+ ]
1161
+ },
1162
+ {
1163
+ name: DEBUG_COMMAND_NAME,
1164
+ description: "Route debug logs for a source channel.",
1165
+ defaultMemberPermissions: ADMIN_COMMAND_PERMISSIONS,
1166
+ options: [
1167
+ {
1168
+ type: ApplicationCommandOptionType.String,
1169
+ name: DEBUG_TYPE_OPTION_NAME,
1170
+ description: "Whether to set or unset debug output.",
1171
+ required: true,
1172
+ choices: [
1173
+ { name: "set", value: "set" },
1174
+ { name: "unset", value: "unset" }
1175
+ ]
1176
+ },
1177
+ {
1178
+ type: ApplicationCommandOptionType.String,
1179
+ name: DEBUG_CHANNEL_ID_OPTION_NAME,
1180
+ description: "Source channel ID for set/unset.",
1181
+ required: false
1182
+ }
1183
+ ]
1184
+ }
1185
+ ];
1186
+ }
1187
+ function parseClearType(value) {
1188
+ if (value === "waifus" || value === "users" || value === "both" || value === "everything") {
1189
+ return value;
1190
+ }
1191
+ if (value === "all") {
1192
+ return "both";
1193
+ }
1194
+ return undefined;
1195
+ }
1196
+ //# sourceMappingURL=client.js.map