import { Client, Events, GatewayIntentBits, Partials, type Message, } from "discord.js"; import { createSharedDeps, describeModel } from "./agent.js"; import { LaneManager } from "./lanes.js"; import { PresenceIndicator, idlePresence } from "./presence.js"; import { Scheduler } from "./scheduler.js"; import { TypingIndicator } from "./typing.js"; import { chunkForDiscord } from "./util.js"; const TOKEN = process.env.DISCORD_BOT_TOKEN; const ALLOWED = (process.env.DISCORD_ALLOWED_USER_IDS ?? "") .split(",") .map((s) => s.trim()) .filter(Boolean); if (!TOKEN) { console.error("DISCORD_BOT_TOKEN not set. Copy .env.example to .env and fill it in."); process.exit(1); } if (ALLOWED.length === 0) { console.error( "DISCORD_ALLOWED_USER_IDS not set. Refusing to start — would respond to anyone." ); process.exit(1); } function extractContent(msg: Message, mentioned: boolean): string { const raw = mentioned ? msg.content.replace(/<@!?\d+>/g, "") : msg.content; return raw.trim(); } async function main() { console.log(`disclaw starting (${describeModel()}), logging in…`); const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.DirectMessages, ], partials: [Partials.Channel, Partials.Message], presence: idlePresence(), }); const presence = new PresenceIndicator(client); const typing = new TypingIndicator(client); let scheduler: Scheduler | null = null; const deps = await createSharedDeps({ onSchedulesChanged: () => scheduler?.scheduleReload(), }); const lanes = new LaneManager(deps, client); scheduler = new Scheduler({ deps, client, schedulesDir: deps.schedulesDir, presence, }); client.once(Events.ClientReady, async (c) => { console.log(`disclaw bot ready as ${c.user.tag} (${describeModel()})`); console.log(`allowed user IDs: ${ALLOWED.join(", ")}`); presence.refresh(); await scheduler!.start(); }); client.on(Events.MessageCreate, async (msg) => { if (msg.author.bot) return; if (!ALLOWED.includes(msg.author.id)) return; const isDm = msg.channel.isDMBased(); const mentioned = client.user ? msg.mentions.has(client.user.id) : false; if (!isDm && !mentioned) return; const content = extractContent(msg, mentioned); if (!content) return; const channel = msg.channel; await typing.start(msg.channelId); try { const result = await presence.track( () => lanes.send(msg.channelId, content, { userId: msg.author.id }) ); if (result.steered) { await msg.react("⏳").catch(() => {}); } else if (!("send" in channel)) { console.error("channel has no send method", channel); } else { const parts = chunkForDiscord(result.output); if (parts.length === 0) { await channel.send("(empty response)"); } else { for (const part of parts) { await channel.send(part); } } } } catch (err) { console.error("disclaw bot error:", err); if ("send" in channel) { channel.send("⚠️ error — check logs").catch(() => {}); } } finally { typing.stop(msg.channelId); } }); await client.login(TOKEN); const shutdown = () => { console.log("\nshutting down…"); scheduler?.stop(); client.destroy(); lanes.dispose(); process.exit(0); }; process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown); } main().catch((err) => { console.error(err); process.exit(1); });