@gonzih/cc-discord 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bot.d.ts CHANGED
@@ -3,6 +3,10 @@
3
3
  * One ClaudeProcess per channel (or channel:thread) — sessions are isolated per channel.
4
4
  */
5
5
  import { Redis } from "ioredis";
6
+ /** Returns true if the attachment name/contentType indicates an audio file. */
7
+ export declare function isAudioAttachment(name: string, contentType: string): boolean;
8
+ /** Build the prompt text for a file/document attachment, optionally with caption. */
9
+ export declare function buildAttachmentPrompt(caption: string, fileName: string, filePath: string): string;
6
10
  /** Prepend [DayOfWeek HH:MM] username: so Claude knows when the message was received and from whom. */
7
11
  export declare function stampPrompt(text: string, username?: string, now?: Date): string;
8
12
  export interface DiscordBotOptions {
@@ -56,6 +60,7 @@ export declare class CcDiscordBot {
56
60
  private handleMessage;
57
61
  private handleVoice;
58
62
  private handleImage;
63
+ private handleDocument;
59
64
  private getOrCreateSession;
60
65
  private onClaudeMessage;
61
66
  private flushSession;
package/dist/bot.js CHANGED
@@ -3,8 +3,8 @@
3
3
  * One ClaudeProcess per channel (or channel:thread) — sessions are isolated per channel.
4
4
  */
5
5
  import { Client, GatewayIntentBits, REST, Routes, SlashCommandBuilder, EmbedBuilder, AttachmentBuilder, Events, ChannelType, } from "discord.js";
6
- import { existsSync, createWriteStream } from "fs";
7
- import { basename } from "path";
6
+ import { existsSync, createWriteStream, mkdirSync } from "fs";
7
+ import { resolve, basename, join } from "path";
8
8
  import https from "https";
9
9
  import http from "http";
10
10
  import { ClaudeProcess, extractText } from "./claude.js";
@@ -42,6 +42,20 @@ const FLUSH_DELAY_MS = 800;
42
42
  // Discord typing indicator: re-send every 9s (indicator expires after ~10s)
43
43
  const TYPING_INTERVAL_MS = 9000;
44
44
  const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
45
+ /** Returns true if the attachment name/contentType indicates an audio file. */
46
+ export function isAudioAttachment(name, contentType) {
47
+ const n = name.toLowerCase();
48
+ const ct = contentType.toLowerCase();
49
+ return (ct.startsWith("audio/") ||
50
+ n.endsWith(".ogg") || n.endsWith(".mp3") || n.endsWith(".m4a") ||
51
+ n.endsWith(".wav") || n.endsWith(".webm") ||
52
+ ct.includes("ogg") || ct.includes("mpeg") || ct.includes("mp4a"));
53
+ }
54
+ /** Build the prompt text for a file/document attachment, optionally with caption. */
55
+ export function buildAttachmentPrompt(caption, fileName, filePath) {
56
+ const ref = `[${fileName}](${filePath})`;
57
+ return caption ? `${caption}\n\nATTACHMENTS: ${ref}` : `ATTACHMENTS: ${ref}`;
58
+ }
45
59
  /** Prepend [DayOfWeek HH:MM] username: so Claude knows when the message was received and from whom. */
46
60
  export function stampPrompt(text, username, now = new Date()) {
47
61
  const day = DAYS[now.getDay()];
@@ -294,12 +308,7 @@ export class CcDiscordBot {
294
308
  // Store snowflake mapping for cron reverse-lookup
295
309
  this.storeSnowflake(effectiveChannelId);
296
310
  // Check for voice/audio attachments
297
- const audioAttachment = msg.attachments.find((att) => {
298
- const name = att.name?.toLowerCase() ?? "";
299
- const ct = att.contentType?.toLowerCase() ?? "";
300
- return (name.endsWith(".ogg") || name.endsWith(".mp3") || name.endsWith(".m4a") ||
301
- ct.includes("ogg") || ct.includes("mpeg") || ct.includes("mp4a"));
302
- });
311
+ const audioAttachment = msg.attachments.find((att) => isAudioAttachment(att.name ?? "", att.contentType ?? ""));
303
312
  if (audioAttachment) {
304
313
  await this.handleVoice(msg, effectiveChannelId, audioAttachment.url, audioAttachment.name ?? "audio.ogg");
305
314
  return;
@@ -313,6 +322,12 @@ export class CcDiscordBot {
313
322
  await this.handleImage(msg, effectiveChannelId, imageAttachment.url, imageAttachment.contentType ?? "image/jpeg");
314
323
  return;
315
324
  }
325
+ // Other file/document attachments
326
+ const docAttachment = msg.attachments.first();
327
+ if (docAttachment) {
328
+ await this.handleDocument(msg, effectiveChannelId, docAttachment.url, docAttachment.name ?? "file");
329
+ return;
330
+ }
316
331
  let text = msg.content.trim();
317
332
  if (!text)
318
333
  return;
@@ -379,12 +394,29 @@ export class CcDiscordBot {
379
394
  await channel.send("Could not transcribe voice message.").catch(() => { });
380
395
  return;
381
396
  }
382
- const session = this.getOrCreateSession(channelId, channel);
383
- session.currentPrompt = transcript;
397
+ // Combine transcript with caption text if present
398
+ const caption = msg.content.trim().replace(/<@!?\d+>/g, "").trim();
399
+ const fullText = caption ? `${caption}\n\n${transcript}` : transcript;
384
400
  const voiceUsername = msg.member?.displayName ?? msg.author.username;
385
- session.claude.sendPrompt(stampPrompt(transcript, voiceUsername, msg.createdAt));
401
+ const prompt = stampPrompt(fullText, voiceUsername, msg.createdAt);
402
+ // Meta-agent routing
403
+ const mappedNs = this.channelNamespaceMap.get(channelId);
404
+ if (mappedNs && this.redis) {
405
+ this.writeChatMessage("user", "discord", fullText, channelId, mappedNs.namespace);
406
+ this.opts.registerRoutedChannelId?.(mappedNs.namespace, channelId);
407
+ try {
408
+ await routeToMetaAgent(mappedNs.namespace, prompt, this.redis);
409
+ }
410
+ catch (err) {
411
+ await channel.send(`Failed to route to ${mappedNs.namespace}: ${err.message}`).catch(() => { });
412
+ }
413
+ return;
414
+ }
415
+ const session = this.getOrCreateSession(channelId, channel);
416
+ session.currentPrompt = fullText;
417
+ session.claude.sendPrompt(prompt);
386
418
  this.startTyping(channelId, channel, session);
387
- this.writeChatMessage("user", "discord", transcript, channelId);
419
+ this.writeChatMessage("user", "discord", fullText, channelId);
388
420
  }
389
421
  catch (err) {
390
422
  const errMsg = err.message;
@@ -404,18 +436,83 @@ export class CcDiscordBot {
404
436
  async handleImage(msg, channelId, imageUrl, contentType) {
405
437
  const channel = msg.channel;
406
438
  await channel.sendTyping().catch(() => { });
439
+ const caption = msg.content.trim().replace(/<@!?\d+>/g, "").trim();
440
+ const imgUsername = msg.member?.displayName ?? msg.author.username;
407
441
  try {
442
+ // Meta-agent routing: save to disk and send as ATTACHMENTS path reference
443
+ const mappedNs = this.channelNamespaceMap.get(channelId);
444
+ if (mappedNs && this.redis) {
445
+ const ext = contentType.split("/")[1]?.replace("jpeg", "jpg") ?? "jpg";
446
+ const fileName = `image_${crypto.randomUUID()}.${ext}`;
447
+ const uploadsDir = resolve(this.opts.cwd ?? process.cwd(), ".cc-discord", "uploads");
448
+ mkdirSync(uploadsDir, { recursive: true });
449
+ const dest = join(uploadsDir, fileName);
450
+ await downloadFile(imageUrl, dest);
451
+ const fullText = buildAttachmentPrompt(caption, fileName, dest);
452
+ const prompt = stampPrompt(fullText, imgUsername, msg.createdAt);
453
+ this.writeChatMessage("user", "discord", fullText, channelId, mappedNs.namespace);
454
+ this.opts.registerRoutedChannelId?.(mappedNs.namespace, channelId);
455
+ try {
456
+ await routeToMetaAgent(mappedNs.namespace, prompt, this.redis);
457
+ }
458
+ catch (err) {
459
+ await channel.send(`Failed to route to ${mappedNs.namespace}: ${err.message}`).catch(() => { });
460
+ }
461
+ return;
462
+ }
463
+ // Local Claude session: send as base64
408
464
  const base64Data = await fetchAsBase64(imageUrl);
409
- const caption = msg.content.trim() || "";
410
- const imgUsername = msg.member?.displayName ?? msg.author.username;
411
465
  const session = this.getOrCreateSession(channelId, channel);
412
466
  session.claude.sendImage(base64Data, contentType, stampPrompt(caption, imgUsername, msg.createdAt));
413
467
  this.startTyping(channelId, channel, session);
468
+ this.writeChatMessage("user", "discord", caption || "[image]", channelId);
414
469
  }
415
470
  catch (err) {
416
471
  await channel.send(`Failed to process image: ${err.message}`).catch(() => { });
417
472
  }
418
473
  }
474
+ async handleDocument(msg, channelId, fileUrl, fileName) {
475
+ const channel = msg.channel;
476
+ await channel.sendTyping().catch(() => { });
477
+ const uploadsDir = resolve(this.opts.cwd ?? process.cwd(), ".cc-discord", "uploads");
478
+ mkdirSync(uploadsDir, { recursive: true });
479
+ const dest = join(uploadsDir, fileName);
480
+ try {
481
+ await downloadFile(fileUrl, dest);
482
+ }
483
+ catch (err) {
484
+ await channel.send(`Failed to download file: ${err.message}`).catch(() => { });
485
+ return;
486
+ }
487
+ const caption = msg.content.trim().replace(/<@!?\d+>/g, "").trim();
488
+ const fullText = buildAttachmentPrompt(caption, fileName, dest);
489
+ const username = msg.member?.displayName ?? msg.author.username;
490
+ const prompt = stampPrompt(fullText, username, msg.createdAt);
491
+ // Meta-agent routing
492
+ const mappedNs = this.channelNamespaceMap.get(channelId);
493
+ if (mappedNs && this.redis) {
494
+ this.writeChatMessage("user", "discord", fullText, channelId, mappedNs.namespace);
495
+ this.opts.registerRoutedChannelId?.(mappedNs.namespace, channelId);
496
+ try {
497
+ await routeToMetaAgent(mappedNs.namespace, prompt, this.redis);
498
+ }
499
+ catch (err) {
500
+ await channel.send(`Failed to route to ${mappedNs.namespace}: ${err.message}`).catch(() => { });
501
+ }
502
+ return;
503
+ }
504
+ // Local Claude session
505
+ const session = this.getOrCreateSession(channelId, channel);
506
+ try {
507
+ session.currentPrompt = fullText;
508
+ session.claude.sendPrompt(prompt);
509
+ this.startTyping(channelId, channel, session);
510
+ this.writeChatMessage("user", "discord", fullText, channelId);
511
+ }
512
+ catch (err) {
513
+ await channel.send(`Failed to process file: ${err.message}`).catch(() => { });
514
+ }
515
+ }
419
516
  getOrCreateSession(channelId, channel) {
420
517
  const key = this.sessionKey(channelId);
421
518
  let session = this.sessions.get(key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-discord",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Claude Code Discord bot — chat with Claude Code via Discord",
5
5
  "type": "module",
6
6
  "bin": {