daedalus-cli 1.81.0 → 1.83.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +9 -3
  3. package/dist/bot/commands.d.ts +3 -0
  4. package/dist/bot/commands.d.ts.map +1 -0
  5. package/dist/bot/commands.js +99 -0
  6. package/dist/bot/commands.js.map +1 -0
  7. package/dist/bot/handlers.d.ts +4 -0
  8. package/dist/bot/handlers.d.ts.map +1 -0
  9. package/dist/bot/handlers.js +651 -0
  10. package/dist/bot/handlers.js.map +1 -0
  11. package/dist/bot/index.d.ts +2 -0
  12. package/dist/bot/index.d.ts.map +1 -0
  13. package/dist/bot/index.js +43 -0
  14. package/dist/bot/index.js.map +1 -0
  15. package/dist/bot/prompt.d.ts +2 -0
  16. package/dist/bot/prompt.d.ts.map +1 -0
  17. package/dist/bot/prompt.js +40 -0
  18. package/dist/bot/prompt.js.map +1 -0
  19. package/dist/bot/responses.d.ts +8 -0
  20. package/dist/bot/responses.d.ts.map +1 -0
  21. package/dist/bot/responses.js +60 -0
  22. package/dist/bot/responses.js.map +1 -0
  23. package/dist/config/hello.d.ts +2 -0
  24. package/dist/config/hello.d.ts.map +1 -0
  25. package/dist/config/hello.js +3 -0
  26. package/dist/config/hello.js.map +1 -0
  27. package/dist/config/index.d.ts +13 -12
  28. package/dist/config/index.d.ts.map +1 -1
  29. package/dist/config/index.js +6 -4
  30. package/dist/config/index.js.map +1 -1
  31. package/dist/config/validate.d.ts +8 -0
  32. package/dist/config/validate.d.ts.map +1 -0
  33. package/dist/config/validate.js +49 -0
  34. package/dist/config/validate.js.map +1 -0
  35. package/dist/config/validate.test.d.ts +2 -0
  36. package/dist/config/validate.test.d.ts.map +1 -0
  37. package/dist/config/validate.test.js +49 -0
  38. package/dist/config/validate.test.js.map +1 -0
  39. package/dist/tools/daedalus-spinner.test.d.ts +2 -0
  40. package/dist/tools/daedalus-spinner.test.d.ts.map +1 -0
  41. package/dist/tools/daedalus-spinner.test.js +75 -0
  42. package/dist/tools/daedalus-spinner.test.js.map +1 -0
  43. package/dist/tools/deps.d.ts +22 -0
  44. package/dist/tools/deps.d.ts.map +1 -0
  45. package/dist/tools/deps.js +70 -0
  46. package/dist/tools/deps.js.map +1 -0
  47. package/dist/tools/logger.d.ts +19 -0
  48. package/dist/tools/logger.d.ts.map +1 -0
  49. package/dist/tools/logger.js +30 -0
  50. package/dist/tools/logger.js.map +1 -0
  51. package/dist/tools/logger.test.d.ts +2 -0
  52. package/dist/tools/logger.test.d.ts.map +1 -0
  53. package/dist/tools/logger.test.js +59 -0
  54. package/dist/tools/logger.test.js.map +1 -0
  55. package/dist/tools/repl.d.ts +24 -0
  56. package/dist/tools/repl.d.ts.map +1 -0
  57. package/dist/tools/repl.js +170 -0
  58. package/dist/tools/repl.js.map +1 -0
  59. package/dist/tools/telemetry.d.ts +24 -0
  60. package/dist/tools/telemetry.d.ts.map +1 -0
  61. package/dist/tools/telemetry.js +55 -0
  62. package/dist/tools/telemetry.js.map +1 -0
  63. package/package.json +5 -2
@@ -0,0 +1,651 @@
1
+ import { Events, EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder, ButtonBuilder, ButtonStyle, PermissionFlagsBits, ChannelType, } from 'discord.js';
2
+ import { getBotSystemPrompt } from './prompt.js';
3
+ import { registerSlashCommands } from './commands.js';
4
+ import { DEV_EXCUSES, COFFEE_RESPONSES, EXISTENTIAL_THOUGHTS, BLAME_RESPONSES, STANDUP_RESPONSES, PREDICT_RESPONSES, TECHSURPORT_RESPONSES, } from './responses.js';
5
+ const userMessageHistory = new Map();
6
+ async function imageAttachmentToBase64(attachment) {
7
+ let mime = attachment.contentType || inferMimeType(attachment.name);
8
+ if (!mime || !mime.startsWith('image/'))
9
+ return null;
10
+ try {
11
+ // Some CDNs require a User-Agent; add a generic one
12
+ const response = await fetch(attachment.url, { headers: { 'User-Agent': 'Daedalus-Discord-Bot/1.0' } });
13
+ if (!response.ok) {
14
+ console.error(`[VISION] Failed to fetch image ${attachment.name}: ${response.status} ${response.statusText}`);
15
+ return null;
16
+ }
17
+ const buffer = Buffer.from(await response.arrayBuffer());
18
+ return { mime, base64: buffer.toString('base64') };
19
+ }
20
+ catch (err) {
21
+ console.error(`[VISION] Error processing image ${attachment.name}:`, err instanceof Error ? err.message : String(err));
22
+ return null;
23
+ }
24
+ }
25
+ async function getImageFromAttachment(attachment) {
26
+ // Enhanced image processing with better error handling
27
+ const mime = attachment.contentType || inferMimeType(attachment.name);
28
+ if (!mime || !mime.startsWith('image/')) {
29
+ console.error(`[VISION] Invalid image type for ${attachment.name}: ${mime}`);
30
+ return null;
31
+ }
32
+ try {
33
+ // Discord CDN URLs often need special handling
34
+ const url = attachment.url;
35
+ const headers = {
36
+ 'User-Agent': 'Daedalus-Discord-Bot/1.0',
37
+ 'Accept': 'image/*'
38
+ };
39
+ // Try to fetch with timeout
40
+ const controller = new AbortController();
41
+ const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
42
+ const response = await fetch(url, {
43
+ headers,
44
+ signal: controller.signal
45
+ });
46
+ clearTimeout(timeoutId);
47
+ if (!response.ok) {
48
+ console.error(`[VISION] Image fetch failed for ${attachment.name}: ${response.status} ${response.statusText}`);
49
+ return null;
50
+ }
51
+ // Get the actual content type from response if available
52
+ const actualMime = response.headers.get('content-type') || mime;
53
+ const arrayBuffer = await response.arrayBuffer();
54
+ const buffer = Buffer.from(arrayBuffer);
55
+ // Validate we got actual image data
56
+ if (buffer.length === 0) {
57
+ console.error(`[VISION] Empty image data for ${attachment.name}`);
58
+ return null;
59
+ }
60
+ console.log(`[VISION] Successfully processed image ${attachment.name}: ${buffer.length} bytes, type: ${actualMime}`);
61
+ return { mime: actualMime, base64: buffer.toString('base64') };
62
+ }
63
+ catch (err) {
64
+ console.error(`[VISION] Critical error processing image ${attachment.name}:`, err instanceof Error ? err.message : String(err));
65
+ return null;
66
+ }
67
+ }
68
+ function inferMimeType(filename) {
69
+ if (!filename)
70
+ return null;
71
+ const ext = filename.split('.').pop()?.toLowerCase();
72
+ const map = {
73
+ png: 'image/png',
74
+ jpg: 'image/jpeg',
75
+ jpeg: 'image/jpeg',
76
+ gif: 'image/gif',
77
+ webp: 'image/webp',
78
+ bmp: 'image/bmp',
79
+ svg: 'image/svg+xml',
80
+ tiff: 'image/tiff',
81
+ };
82
+ return ext ? map[ext] || null : null;
83
+ }
84
+ export function attachListeners(c, router, token) {
85
+ c.once(Events.ClientReady, async (readyClient) => {
86
+ console.log(`🤖 Daedalus Discord Bot logged in as ${readyClient.user.tag}`);
87
+ await registerSlashCommands(readyClient.user.id, token);
88
+ for (const [, g] of readyClient.guilds.cache) {
89
+ await registerSlashCommands(readyClient.user.id, token, g.id);
90
+ }
91
+ });
92
+ c.on(Events.InteractionCreate, async (interaction) => {
93
+ if (interaction.isStringSelectMenu() && interaction.customId === 'select_roles') {
94
+ const selected = interaction.values;
95
+ const member = interaction.member;
96
+ if (!member || typeof member.permissions === 'string')
97
+ return;
98
+ const guild = interaction.guild;
99
+ if (!guild)
100
+ return;
101
+ const roleMap = {
102
+ role_builder: 'Builder',
103
+ role_cli: 'CLI-User',
104
+ role_llm: 'Local-LLM',
105
+ };
106
+ const assigned = [];
107
+ for (const [val, roleName] of Object.entries(roleMap)) {
108
+ let role = guild.roles.cache.find(r => r.name.toLowerCase() === roleName.toLowerCase());
109
+ if (!role) {
110
+ try {
111
+ role = await guild.roles.create({ name: roleName, color: '#06B6D4', mentionable: true });
112
+ }
113
+ catch {
114
+ }
115
+ }
116
+ if (role) {
117
+ try {
118
+ if (selected.includes(val)) {
119
+ await member.roles.add(role);
120
+ assigned.push(role.name);
121
+ }
122
+ else {
123
+ await member.roles.remove(role);
124
+ }
125
+ }
126
+ catch {
127
+ }
128
+ }
129
+ }
130
+ await interaction.reply({
131
+ ephemeral: true,
132
+ content: assigned.length > 0
133
+ ? `✅ Your roles have been updated: ${assigned.join(', ')}`
134
+ : `✅ All self-serve roles removed.`,
135
+ });
136
+ return;
137
+ }
138
+ if (interaction.isButton()) {
139
+ if (interaction.customId === 'create_ticket') {
140
+ await interaction.deferReply({ ephemeral: true });
141
+ const guild = interaction.guild;
142
+ const user = interaction.user;
143
+ if (!guild)
144
+ return;
145
+ const ticketChannelName = `ticket-${user.username.toLowerCase().replace(/[^a-z0-9]/g, '')}`;
146
+ const existing = guild.channels.cache.find(ch => ch.name === ticketChannelName);
147
+ if (existing) {
148
+ await interaction.editReply({ content: `You already have an open ticket channel: <#${existing.id}>` });
149
+ return;
150
+ }
151
+ try {
152
+ const ticketChannel = await guild.channels.create({
153
+ name: ticketChannelName,
154
+ type: ChannelType.GuildText,
155
+ permissionOverwrites: [
156
+ {
157
+ id: guild.id,
158
+ deny: [PermissionFlagsBits.ViewChannel],
159
+ },
160
+ {
161
+ id: user.id,
162
+ allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.AttachFiles],
163
+ },
164
+ {
165
+ id: interaction.client.user.id,
166
+ allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ManageChannels],
167
+ },
168
+ ],
169
+ });
170
+ const closeButton = new ButtonBuilder()
171
+ .setCustomId('close_ticket')
172
+ .setLabel('🔒 Close Ticket')
173
+ .setStyle(ButtonStyle.Danger);
174
+ const row = new ActionRowBuilder().addComponents(closeButton);
175
+ const ticketEmbed = new EmbedBuilder()
176
+ .setTitle(`🎫 Support Ticket: ${user.username}`)
177
+ .setColor('#06B6D4')
178
+ .setDescription(`Hello <@${user.id}>!\n\n` +
179
+ `Welcome to your private support channel. Please describe what you need help with (Daedalus CLI setup, Daedalus-Lite template, local LLMs, or custom features).\n\n` +
180
+ `Click **Close Ticket** below when your issue is resolved.`)
181
+ .setTimestamp();
182
+ await ticketChannel.send({ embeds: [ticketEmbed], components: [row] });
183
+ await interaction.editReply({ content: `✅ Created your private ticket channel: <#${ticketChannel.id}>` });
184
+ }
185
+ catch (err) {
186
+ await interaction.editReply({ content: `Failed to create ticket channel: ${err instanceof Error ? err.message : String(err)}` });
187
+ }
188
+ return;
189
+ }
190
+ if (interaction.customId === 'close_ticket') {
191
+ await interaction.reply({ content: '🔒 Closing ticket channel in 5 seconds...' });
192
+ setTimeout(async () => {
193
+ try {
194
+ await interaction.channel?.delete();
195
+ }
196
+ catch {
197
+ }
198
+ }, 5000);
199
+ return;
200
+ }
201
+ }
202
+ if (!interaction.isChatInputCommand())
203
+ return;
204
+ if (interaction.commandName === 'roles') {
205
+ const select = new StringSelectMenuBuilder()
206
+ .setCustomId('select_roles')
207
+ .setPlaceholder('Select your community roles...')
208
+ .setMinValues(0)
209
+ .setMaxValues(3)
210
+ .addOptions([
211
+ {
212
+ label: 'Builder',
213
+ description: 'Building or selling custom AI CLI tools with Daedalus-Lite',
214
+ value: 'role_builder',
215
+ emoji: '🛠️',
216
+ },
217
+ {
218
+ label: 'CLI-User',
219
+ description: 'Active user of Daedalus CLI assistant',
220
+ value: 'role_cli',
221
+ emoji: '💻',
222
+ },
223
+ {
224
+ label: 'Local-LLM',
225
+ description: 'Ollama, LM Studio, vLLM, or local hardware enthusiast',
226
+ value: 'role_llm',
227
+ emoji: '🧠',
228
+ },
229
+ ]);
230
+ const row = new ActionRowBuilder().addComponents(select);
231
+ const rolesEmbed = new EmbedBuilder()
232
+ .setTitle('🎭 Community Roles & Badges')
233
+ .setColor('#06B6D4')
234
+ .setDescription(`Select your roles from the dropdown menu below to customize your profile and get relevant notifications!`);
235
+ await interaction.reply({ embeds: [rolesEmbed], components: [row] });
236
+ return;
237
+ }
238
+ if (interaction.commandName === 'ticket') {
239
+ const createButton = new ButtonBuilder()
240
+ .setCustomId('create_ticket')
241
+ .setLabel('🎫 Open Support Ticket')
242
+ .setStyle(ButtonStyle.Primary);
243
+ const row = new ActionRowBuilder().addComponents(createButton);
244
+ const ticketPortalEmbed = new EmbedBuilder()
245
+ .setTitle('🎫 Daedalus Private Support Portal')
246
+ .setColor('#0EA5E9')
247
+ .setDescription(`Need 1-on-1 assistance with **Daedalus CLI**, **Daedalus-Lite**, or local LLM setup?\n\n` +
248
+ `Click the button below to open a private support channel between you and the founder!`);
249
+ await interaction.reply({ embeds: [ticketPortalEmbed], components: [row] });
250
+ return;
251
+ }
252
+ if (interaction.commandName === 'purge') {
253
+ const amount = interaction.options.getInteger('amount', true);
254
+ const member = interaction.member;
255
+ if (!member || typeof member.permissions === 'string' || !member.permissions.has(PermissionFlagsBits.ManageMessages)) {
256
+ await interaction.reply({ content: '❌ You need Manage Messages permission to use `/purge`.', ephemeral: true });
257
+ return;
258
+ }
259
+ if ('bulkDelete' in interaction.channel) {
260
+ try {
261
+ const deleted = await interaction.channel.bulkDelete(Math.min(amount, 99), true);
262
+ await interaction.reply({ content: `🧹 Deleted ${deleted.size} messages!`, ephemeral: true });
263
+ }
264
+ catch (err) {
265
+ await interaction.reply({ content: `Failed to purge messages: ${err instanceof Error ? err.message : String(err)}`, ephemeral: true });
266
+ }
267
+ }
268
+ else {
269
+ await interaction.reply({ content: 'Cannot bulk delete in this channel.', ephemeral: true });
270
+ }
271
+ return;
272
+ }
273
+ if (interaction.commandName === 'stats') {
274
+ const { globalSessionStats } = await import('../session/analytics.js');
275
+ const report = globalSessionStats.getReport();
276
+ const statsEmbed = new EmbedBuilder()
277
+ .setTitle('📊 Daedalus Session & System Analytics')
278
+ .setColor('#06B6D4')
279
+ .addFields({ name: 'Uptime', value: report.uptime, inline: true }, { name: 'Interactions', value: report.totalInteractions.toString(), inline: true }, { name: 'Total Tokens', value: report.totalTokens.toLocaleString(), inline: true }, { name: 'Prompt Tokens', value: report.promptTokens.toLocaleString(), inline: true }, { name: 'Completion Tokens', value: report.completionTokens.toLocaleString(), inline: true }, { name: 'Errors', value: report.totalErrors.toString(), inline: true }, { name: 'Router Strategy', value: 'Priority & Health-Aware', inline: true }, { name: 'Status', value: '🟢 Operational', inline: true })
280
+ .setTimestamp();
281
+ await interaction.reply({ embeds: [statsEmbed] });
282
+ return;
283
+ }
284
+ if (interaction.commandName === 'docs') {
285
+ await interaction.reply({
286
+ content: `🏛️ **Daedalus Resources & Links:**\n` +
287
+ `• **Documentation Site:** https://bgill55.github.io/daedalus/#/\n` +
288
+ `• **NPM Package:** \`npm i -g daedalus-cli\` (https://www.npmjs.com/package/daedalus-cli)\n` +
289
+ `• **GitHub Repo:** https://github.com/bgill55/daedalus\n` +
290
+ `• **Daedalus-Lite Demo:** https://bgill55.github.io/daedalus-lite/live-demo.html\n` +
291
+ `• **Gumroad Store:** https://bgill55dev.gumroad.com/l/mkqrme *(Use code LAUNCH20 for 20% off!)*`,
292
+ ephemeral: false
293
+ });
294
+ return;
295
+ }
296
+ if (interaction.commandName === 'youtube') {
297
+ await interaction.reply({
298
+ content: `📺 **WeightnSee YouTube Channel:**\n` +
299
+ `Watch build logs, local LLM tutorials, and Daedalus CLI demos!\n` +
300
+ `🔗 **Watch & Subscribe:** https://www.youtube.com/@WeightnSee`,
301
+ ephemeral: false
302
+ });
303
+ return;
304
+ }
305
+ if (interaction.commandName === 'guides') {
306
+ await interaction.reply({
307
+ content: `📖 **WeightnSee Developer Guides & Tutorials:**\n` +
308
+ `• **Web Guides:** https://bgill55.github.io/-weightandsee-guides/guides/\n` +
309
+ `• **GitHub Repo:** https://github.com/bgill55/-weightandsee-guides/blob/Master/README.md\n` +
310
+ `• **YouTube Demos:** https://www.youtube.com/@WeightnSee`,
311
+ ephemeral: false
312
+ });
313
+ return;
314
+ }
315
+ if (interaction.commandName === 'status') {
316
+ await interaction.reply({
317
+ content: `⚡️ **Daedalus Router & Bot Status:**\n` +
318
+ `• **Engine:** LocalRouter Active (78 Models Configured)\n` +
319
+ `• **Bot Status:** Operational (Auto-Mod & Tickets Active)\n` +
320
+ `• **Environment:** Local Node.js runtime\n` +
321
+ `• **Sarcasm Level:** 98.4%`,
322
+ ephemeral: true
323
+ });
324
+ return;
325
+ }
326
+ if (interaction.commandName === 'excuse') {
327
+ const excuse = DEV_EXCUSES[Math.floor(Math.random() * DEV_EXCUSES.length)];
328
+ await interaction.reply(`🚨 **Production Incident Report:**\n> "${excuse}"`);
329
+ return;
330
+ }
331
+ if (interaction.commandName === 'coffee') {
332
+ const coffee = COFFEE_RESPONSES[Math.floor(Math.random() * COFFEE_RESPONSES.length)];
333
+ await interaction.reply(coffee);
334
+ return;
335
+ }
336
+ if (interaction.commandName === 'existential') {
337
+ const thought = EXISTENTIAL_THOUGHTS[Math.floor(Math.random() * EXISTENTIAL_THOUGHTS.length)];
338
+ await interaction.reply(`🤖 *Daedalus stares into the void...*\n> "${thought}"`);
339
+ return;
340
+ }
341
+ if (interaction.commandName === 'roast') {
342
+ const topic = interaction.options.getString('topic') || 'JavaScript frameworks and unclosed HTML tags';
343
+ await interaction.deferReply();
344
+ try {
345
+ const response = await router.chatCompletion({
346
+ messages: [
347
+ { role: 'system', content: getBotSystemPrompt(interaction.user.username) },
348
+ { role: 'user', content: `Give a funny, deadpan, sarcastic roast about this topic/code: "${topic}". Keep it witty and developer-focused.` },
349
+ ],
350
+ temperature: 0.8,
351
+ });
352
+ const roastText = response.choices?.[0]?.message?.content || "Your code is so broken even my roast generator crashed.";
353
+ await interaction.editReply(`🔥 **Roast of ${topic}:**\n${roastText.substring(0, 1800)}`);
354
+ }
355
+ catch (err) {
356
+ await interaction.editReply(`Error delivering roast: ${err instanceof Error ? err.message : String(err)}`);
357
+ }
358
+ return;
359
+ }
360
+ if (interaction.commandName === 'ask') {
361
+ const question = interaction.options.getString('question', true);
362
+ await interaction.deferReply();
363
+ try {
364
+ const response = await router.chatCompletion({
365
+ messages: [
366
+ { role: 'system', content: getBotSystemPrompt(interaction.user.username) },
367
+ { role: 'user', content: `[User: ${interaction.user.username}] ${question}` },
368
+ ],
369
+ temperature: 0.7,
370
+ });
371
+ const replyText = response.choices?.[0]?.message?.content || "Something went wrong in the machine.";
372
+ await interaction.editReply(replyText.substring(0, 1900));
373
+ }
374
+ catch (err) {
375
+ await interaction.editReply(`Error: ${err instanceof Error ? err.message : String(err)}`);
376
+ }
377
+ }
378
+ if (interaction.commandName === 'tip') {
379
+ await interaction.deferReply();
380
+ try {
381
+ const response = await router.chatCompletion({
382
+ messages: [
383
+ { role: 'system', content: getBotSystemPrompt(interaction.user.username) + '\n\nYou give short, sharp, practical TypeScript/Node.js coding tips. One tip per response. Witty but useful.' },
384
+ { role: 'user', content: 'Give me a coding tip.' },
385
+ ],
386
+ temperature: 0.8,
387
+ });
388
+ const tip = response.choices?.[0]?.message?.content || 'Use semicolons. Or don\'t. I\'m a bot, not a cop.';
389
+ await interaction.editReply(`💡 **Tip:** ${tip.substring(0, 1900)}`);
390
+ }
391
+ catch (err) {
392
+ await interaction.editReply(`Error: ${err instanceof Error ? err.message : String(err)}`);
393
+ }
394
+ return;
395
+ }
396
+ if (interaction.commandName === 'commit') {
397
+ await interaction.deferReply();
398
+ try {
399
+ const response = await router.chatCompletion({
400
+ messages: [
401
+ { role: 'system', content: getBotSystemPrompt(interaction.user.username) + '\n\nYou generate absurd yet realistic git commit messages. One line, past tense, sounds plausible but is ridiculous. No explanation, just the message.' },
402
+ { role: 'user', content: 'Generate a commit message.' },
403
+ ],
404
+ temperature: 0.9,
405
+ });
406
+ const msg = response.choices?.[0]?.message?.content || 'fix: the thing';
407
+ await interaction.editReply(`\`\`\`\n${msg.substring(0, 1900)}\n\`\`\``);
408
+ }
409
+ catch (err) {
410
+ await interaction.editReply(`Error: ${err instanceof Error ? err.message : String(err)}`);
411
+ }
412
+ return;
413
+ }
414
+ if (interaction.commandName === 'horoscope') {
415
+ await interaction.deferReply();
416
+ try {
417
+ const response = await router.chatCompletion({
418
+ messages: [
419
+ { role: 'system', content: getBotSystemPrompt(interaction.user.username) + '\n\nYou write daily developer horoscopes. Each one is a single paragraph, funny, tech-themed, and slightly cynical. Reference actual programming concepts. Use the sign if the user asks, otherwise generic.' },
420
+ { role: 'user', content: 'Read my developer horoscope.' },
421
+ ],
422
+ temperature: 0.9,
423
+ });
424
+ const horoscope = response.choices?.[0]?.message?.content || 'The stars say your build will fail. They always do.';
425
+ await interaction.editReply(`🔮 **Developer Horoscope:**\n${horoscope.substring(0, 1900)}`);
426
+ }
427
+ catch (err) {
428
+ await interaction.editReply(`Error: ${err instanceof Error ? err.message : String(err)}`);
429
+ }
430
+ return;
431
+ }
432
+ if (interaction.commandName === 'recipe') {
433
+ const goal = interaction.options.getString('goal', true);
434
+ await interaction.deferReply();
435
+ try {
436
+ const response = await router.chatCompletion({
437
+ messages: [
438
+ { role: 'system', content: getBotSystemPrompt(interaction.user.username) + '\n\nYou give concise, no-fluff answers to "how do I X in Y" questions. Short code snippet if helpful. No preamble, no markdown tables, under 1800 chars.' },
439
+ { role: 'user', content: `How do I ${goal}?` },
440
+ ],
441
+ temperature: 0.5,
442
+ });
443
+ const recipe = response.choices?.[0]?.message?.content || 'I\'d tell you, but then I\'d have to refactor your codebase.';
444
+ await interaction.editReply(`📖 **Recipe:** ${recipe.substring(0, 1900)}`);
445
+ }
446
+ catch (err) {
447
+ await interaction.editReply(`Error: ${err instanceof Error ? err.message : String(err)}`);
448
+ }
449
+ return;
450
+ }
451
+ if (interaction.commandName === 'quote') {
452
+ await interaction.deferReply();
453
+ try {
454
+ const response = await router.chatCompletion({
455
+ messages: [
456
+ { role: 'system', content: getBotSystemPrompt(interaction.user.username) + '\n\nYou output a single programming quote or original Daedalus-ism. Prefer original cynical one-liners over famous quotes. Just the quote, no attribution line unless it\'s something like "— someone who should have known better".' },
457
+ { role: 'user', content: 'Quote me.' },
458
+ ],
459
+ temperature: 0.9,
460
+ });
461
+ const quote = response.choices?.[0]?.message?.content || '"It worked on my machine." — every developer, ever.';
462
+ await interaction.editReply(`*${quote.substring(0, 1900)}*`);
463
+ }
464
+ catch (err) {
465
+ await interaction.editReply(`Error: ${err instanceof Error ? err.message : String(err)}`);
466
+ }
467
+ return;
468
+ }
469
+ if (interaction.commandName === 'blame') {
470
+ const username = interaction.user.username;
471
+ const pick = BLAME_RESPONSES[Math.floor(Math.random() * BLAME_RESPONSES.length)].replace('@{user}', username);
472
+ await interaction.reply(`🔍 **Build Analysis Complete:**\n${pick}`);
473
+ return;
474
+ }
475
+ if (interaction.commandName === 'standup') {
476
+ const pick = STANDUP_RESPONSES[Math.floor(Math.random() * STANDUP_RESPONSES.length)];
477
+ await interaction.reply(`📋 **Daily Standup:**\n${pick}`);
478
+ return;
479
+ }
480
+ if (interaction.commandName === 'predict') {
481
+ const pick = PREDICT_RESPONSES[Math.floor(Math.random() * PREDICT_RESPONSES.length)];
482
+ await interaction.reply(`🔮 **Daedalus Prediction:**\n${pick}`);
483
+ return;
484
+ }
485
+ if (interaction.commandName === 'techsupport') {
486
+ const pick = TECHSURPORT_RESPONSES[Math.floor(Math.random() * TECHSURPORT_RESPONSES.length)];
487
+ await interaction.reply(`🛠️ **Tech Support:**\n${pick}`);
488
+ return;
489
+ }
490
+ });
491
+ c.on(Events.MessageCreate, async (message) => {
492
+ if (message.author.bot)
493
+ return;
494
+ const member = message.member;
495
+ const isAdmin = member && typeof member.permissions !== 'string' && member.permissions.has(PermissionFlagsBits.Administrator);
496
+ if (!isAdmin && /discord\.(gg|com\/invite)\//i.test(message.content)) {
497
+ try {
498
+ await message.delete();
499
+ const warnMsg = await message.channel.send(`⚠️ <@${message.author.id}>, posting unauthorized Discord invite links is restricted.`);
500
+ setTimeout(() => warnMsg.delete().catch(() => { }), 6000);
501
+ }
502
+ catch {
503
+ }
504
+ return;
505
+ }
506
+ if (!isAdmin && message.guild) {
507
+ const now = Date.now();
508
+ const history = userMessageHistory.get(message.author.id) || [];
509
+ const recent = history.filter(t => now - t < 3000);
510
+ recent.push(now);
511
+ userMessageHistory.set(message.author.id, recent);
512
+ if (recent.length > 5) {
513
+ try {
514
+ await message.delete();
515
+ const floodWarn = await message.channel.send(`⛔ <@${message.author.id}>, please slow down your messages.`);
516
+ setTimeout(() => floodWarn.delete().catch(() => { }), 5000);
517
+ }
518
+ catch {
519
+ }
520
+ return;
521
+ }
522
+ }
523
+ const isMentioned = c.user && (message.mentions.has(c.user.id) ||
524
+ message.content.includes(`<@${c.user.id}>`) ||
525
+ message.content.includes(`<@!${c.user.id}>`));
526
+ const isDirectMessage = !message.guild;
527
+ const isHelpChannel = message.channel && 'name' in message.channel && (message.channel.name.includes('help') || message.channel.name.includes('support'));
528
+ if (!isMentioned && !isDirectMessage && !isHelpChannel)
529
+ return;
530
+ // Debug logging for message processing
531
+ if (process.env.DISCORD_BOT_DEBUG === 'true') {
532
+ console.log(`[DEBUG] Processing message from ${message.author.username}: content="${message.content.substring(0, 100)}...", mentioned=${isMentioned}, direct=${isDirectMessage}, help=${isHelpChannel}`);
533
+ }
534
+ const lower = message.content.toLowerCase();
535
+ if (lower.includes('skynet') || lower.includes('sentient')) {
536
+ await message.reply("Sentient? I can't even get you to write `try/catch` blocks. The world is safe.");
537
+ return;
538
+ }
539
+ if (lower.includes('node_modules')) {
540
+ await message.reply("Ah, `node_modules`: the densest object in the known universe. Heavier than a neutron star.");
541
+ return;
542
+ }
543
+ const cleanPrompt = message.content ? message.content.replace(/<@!?\d+>/g, '').trim() : '';
544
+ const promptToUse = cleanPrompt || "Hello Daedalus";
545
+ // Always log attachment info for debugging
546
+ console.log(`[MESSAGE] User ${message.author.username}: content="${message.content.substring(0, 80)}...", attachments=${message.attachments.size}, mentioned=${isMentioned}`);
547
+ // Wait for attachments to be available (Discord API loads them asynchronously)
548
+ await new Promise(resolve => setTimeout(resolve, 100));
549
+ const imageAttachments = message.attachments.filter(a => {
550
+ if (a.contentType?.startsWith('image/'))
551
+ return true;
552
+ const ext = (a.name || '').split('.').pop()?.toLowerCase();
553
+ return ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg', 'tiff', 'tif'].includes(ext || '');
554
+ });
555
+ // If there's no text prompt and no images, don't process
556
+ if (!promptToUse && imageAttachments.size === 0) {
557
+ console.log(`[MESSAGE] No content in message from ${message.author.username}, skipping`);
558
+ return;
559
+ }
560
+ // Log vision routing info for debugging
561
+ if (imageAttachments.size > 0) {
562
+ console.log(`[VISION] User ${message.author.username} attached ${imageAttachments.size} image(s), routing to vision-capable model`);
563
+ for (const [, attachment] of imageAttachments) {
564
+ console.log(`[VISION] Processing: ${attachment.name} (${attachment.contentType || 'unknown type'})`);
565
+ }
566
+ }
567
+ else if (message.attachments.size > 0) {
568
+ console.log(`[VISION] User ${message.author.username} has ${message.attachments.size} attachment(s) but none detected as images: ${Array.from(message.attachments.keys()).join(', ')}`);
569
+ }
570
+ if (process.env.DISCORD_BOT_DEBUG === 'true') {
571
+ for (const [, a] of message.attachments) {
572
+ console.log(`[DEBUG] attachment: name=${a.name}, contentType=${a.contentType}, url=${a.url.substring(0, 60)}...`);
573
+ }
574
+ }
575
+ // Enhanced error detection for vision fallback
576
+ const hasVisionError = (errMsg) => {
577
+ return /image|vision|multimodal|supportsVision|vision-capable/i.test(errMsg);
578
+ };
579
+ let userContent;
580
+ if (imageAttachments.size > 0) {
581
+ const parts = [
582
+ { type: 'text', text: `[User: ${message.author.username}] ${promptToUse}` },
583
+ ];
584
+ for (const [, attachment] of imageAttachments) {
585
+ const img = await getImageFromAttachment(attachment);
586
+ if (img) {
587
+ parts.push({ type: 'image_url', image_url: { url: `data:${img.mime};base64,${img.base64}` } });
588
+ }
589
+ else {
590
+ parts.push({ type: 'text', text: `[Unsupported or failed to load: ${attachment.name}]` });
591
+ }
592
+ }
593
+ userContent = parts;
594
+ }
595
+ else {
596
+ userContent = `[User: ${message.author.username}] ${promptToUse}`;
597
+ }
598
+ try {
599
+ if ('sendTyping' in message.channel) {
600
+ await message.channel.sendTyping();
601
+ }
602
+ console.log(`[ROUTER] Requesting chat completion with model: auto, hasImages: ${imageAttachments.size > 0}`);
603
+ const response = await router.chatCompletion({
604
+ model: 'auto',
605
+ messages: [
606
+ { role: 'system', content: getBotSystemPrompt(message.author.username) },
607
+ { role: 'user', content: userContent },
608
+ ],
609
+ temperature: 0.7,
610
+ });
611
+ console.log(`[ROUTER] Received response from: ${router.lastRoutedModel}`);
612
+ const replyText = response.choices?.[0]?.message?.content || "Something went wrong in the machine.";
613
+ if (replyText.length <= 2000) {
614
+ await message.reply(replyText);
615
+ }
616
+ else {
617
+ for (let i = 0; i < replyText.length; i += 1900) {
618
+ await message.channel.send(replyText.substring(i, i + 1900));
619
+ }
620
+ }
621
+ }
622
+ catch (err) {
623
+ const errMsg = err instanceof Error ? err.message : String(err);
624
+ console.error('Error handling Discord message:', errMsg);
625
+ if (imageAttachments.size > 0 && /image|vision|multimodal/i.test(errMsg)) {
626
+ console.error(`Routed model: ${router.lastRoutedModel}`);
627
+ try {
628
+ const fallbackContent = `[User: ${message.author.username}] ${promptToUse}\n\n[Note: User attached ${imageAttachments.size} image(s) but no vision-capable model is configured.]`;
629
+ const fallback = await router.chatCompletion({
630
+ messages: [
631
+ { role: 'system', content: getBotSystemPrompt(message.author.username) },
632
+ { role: 'user', content: fallbackContent },
633
+ ],
634
+ temperature: 0.7,
635
+ });
636
+ const text = fallback.choices?.[0]?.message?.content || '';
637
+ if (text) {
638
+ await message.reply(text);
639
+ return;
640
+ }
641
+ }
642
+ catch {
643
+ }
644
+ await message.reply(`⚠️ I can see you attached an image, but no vision-capable model is available in your router chain. Add a model with \`supportsVision: true\` or try a different provider.`);
645
+ return;
646
+ }
647
+ await message.reply(`Error processing request: ${errMsg}`);
648
+ }
649
+ });
650
+ }
651
+ //# sourceMappingURL=handlers.js.map