@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 +5 -0
- package/dist/bot.js +111 -14
- package/package.json +1 -1
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
|
-
|
|
383
|
-
|
|
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
|
-
|
|
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",
|
|
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);
|