Initial commit: Discord-Claude Gateway with event-driven agent runtime

This commit is contained in:
2026-02-22 00:31:25 -05:00
commit 77d7c74909
58 changed files with 11772 additions and 0 deletions

165
src/discord-bot.ts Normal file
View File

@@ -0,0 +1,165 @@
import {
Client,
GatewayIntentBits,
REST,
Routes,
SlashCommandBuilder,
type ChatInputCommandInteraction,
type Message,
type TextChannel,
} from "discord.js";
export interface Prompt {
text: string;
channelId: string;
userId: string;
guildId: string | null;
}
export function shouldIgnoreMessage(message: { author: { bot: boolean } }): boolean {
return message.author.bot;
}
export function extractPromptFromMention(content: string, botId: string): string {
return content.replace(new RegExp(`<@!?${botId}>`, "g"), "").trim();
}
export class DiscordBot {
private client: Client;
private promptHandler: ((prompt: Prompt) => void) | null = null;
private resetHandler: ((channelId: string) => void) | null = null;
constructor() {
this.client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
}
async start(token: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.client.once("ready", () => {
const user = this.client.user;
console.log(`Bot logged in as ${user?.tag ?? "unknown"}`);
console.log(`Connected to ${this.client.guilds.cache.size} guild(s)`);
this.setupMessageHandler();
this.setupInteractionHandler();
resolve();
});
this.client.once("error", reject);
this.client.login(token).catch(reject);
});
}
async registerCommands(): Promise<void> {
const clientId = this.client.user?.id;
if (!clientId) {
throw new Error("Bot must be started before registering commands");
}
const claudeCommand = new SlashCommandBuilder()
.setName("claude")
.setDescription("Send a prompt to Claude")
.addStringOption((option) =>
option
.setName("prompt")
.setDescription("The prompt to send to Claude")
.setRequired(true)
);
const claudeResetCommand = new SlashCommandBuilder()
.setName("claude-reset")
.setDescription("Reset the conversation session in this channel");
const commands = [claudeCommand.toJSON(), claudeResetCommand.toJSON()];
const rest = new REST({ version: "10" }).setToken(this.client.token!);
await rest.put(Routes.applicationCommands(clientId), { body: commands });
console.log("Registered /claude and /claude-reset slash commands");
}
async sendMessage(channelId: string, content: string): Promise<void> {
try {
const channel = await this.client.channels.fetch(channelId);
if (channel && "send" in channel) {
await (channel as TextChannel).send(content);
}
} catch (error) {
console.error(
`Failed to send message to channel ${channelId} (content length: ${content.length}):`,
error
);
}
}
async sendTyping(channelId: string): Promise<void> {
try {
const channel = await this.client.channels.fetch(channelId);
if (channel && "sendTyping" in channel) {
await (channel as TextChannel).sendTyping();
}
} catch {
// Typing indicator failures are non-critical
}
}
async destroy(): Promise<void> {
await this.client.destroy();
}
onPrompt(handler: (prompt: Prompt) => void): void {
this.promptHandler = handler;
}
onReset(handler: (channelId: string) => void): void {
this.resetHandler = handler;
}
private setupMessageHandler(): void {
this.client.on("messageCreate", (message: Message) => {
if (shouldIgnoreMessage(message)) return;
const botUser = this.client.user;
if (!botUser) return;
if (!message.mentions.has(botUser)) return;
const text = extractPromptFromMention(message.content, botUser.id);
if (!text) return;
this.promptHandler?.({
text,
channelId: message.channelId,
userId: message.author.id,
guildId: message.guildId,
});
});
}
private setupInteractionHandler(): void {
this.client.on("interactionCreate", async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const command = interaction as ChatInputCommandInteraction;
if (command.commandName === "claude") {
const promptText = command.options.getString("prompt", true);
await command.deferReply();
this.promptHandler?.({
text: promptText,
channelId: command.channelId,
userId: command.user.id,
guildId: command.guildId,
});
} else if (command.commandName === "claude-reset") {
this.resetHandler?.(command.channelId);
await command.reply({ content: "Session reset. Your next message will start a new conversation.", ephemeral: true });
}
});
}
}