/** * Application configuration * Centralizes all environment variables and configuration settings */ interface BotConfig { discord: { token: string; clientId: string; clientSecret: string; }; ai: { openRouterApiKey: string; model: string; classificationModel: string; classificationFallbackModels: string[]; maxTokens: number; temperature: number; }; replicate: { apiKey: string; }; fal: { apiKey: string; }; klipy: { apiKey: string; }; elevenlabs: { apiKey: string; voiceId: string; modelId: string; }; bot: { /** Chance of Joel responding without being mentioned (0-1) */ freeWillChance: number; /** Chance of using memories for responses (0-1) */ memoryChance: number; /** Minimum time between random user mentions (ms) */ mentionCooldown: number; /** Chance of mentioning a random user (0-1) */ mentionProbability: number; /** Enable random-time spontaneous mention scheduler */ spontaneousSchedulerEnabled: boolean; /** Minimum delay between spontaneous posts (ms) */ spontaneousSchedulerMinIntervalMs: number; /** Maximum delay between spontaneous posts (ms) */ spontaneousSchedulerMaxIntervalMs: number; }; web: { port: number; baseUrl: string; sessionSecret: string; }; } function getEnvOrThrow(key: string): string { const value = Bun.env[key]; if (!value) { throw new Error(`Missing required environment variable: ${key}`); } return value; } function getEnvOrDefault(key: string, defaultValue: string): string { return Bun.env[key] ?? defaultValue; } function getFirstEnvOrDefault(keys: string[], defaultValue: string): string { for (const key of keys) { const value = Bun.env[key]; if (value !== undefined) { return value; } } return defaultValue; } function getBooleanEnvOrDefault(key: string, defaultValue: boolean): boolean { const raw = Bun.env[key]; if (raw === undefined) { return defaultValue; } const normalized = raw.toLowerCase(); return normalized === "1" || normalized === "true" || normalized === "yes"; } function getCsvEnvOrDefault(key: string, defaultValues: string[]): string[] { const raw = Bun.env[key]; if (!raw) { return defaultValues; } return raw .split(",") .map((value) => value.trim()) .filter((value) => value.length > 0); } export const config: BotConfig = { discord: { token: getEnvOrThrow("DISCORD_TOKEN"), clientId: getEnvOrThrow("DISCORD_CLIENT_ID"), clientSecret: getEnvOrThrow("DISCORD_CLIENT_SECRET"), }, ai: { openRouterApiKey: getEnvOrThrow("OPENROUTER_API_KEY"), model: getEnvOrDefault( "AI_MODEL", "x-ai/grok-4.1-fast" ), classificationModel: getEnvOrDefault( "AI_CLASSIFICATION_MODEL", "google/gemma-3-12b-it:free" ), classificationFallbackModels: getCsvEnvOrDefault("AI_CLASSIFICATION_FALLBACK_MODELS", [ "meta-llama/llama-3.3-70b-instruct:free", "qwen/qwen-2.5-7b-instruct", ]), maxTokens: parseInt(getEnvOrDefault("AI_MAX_TOKENS", "500")), temperature: parseFloat(getEnvOrDefault("AI_TEMPERATURE", "1.2")), }, replicate: { apiKey: getFirstEnvOrDefault(["REPLICATE_API_KEY", "REPLICATE_API_TOKEN"], ""), }, fal: { apiKey: getEnvOrDefault("FAL_KEY", ""), }, klipy: { apiKey: getEnvOrDefault("KLIPY_API_KEY", ""), }, elevenlabs: { apiKey: getEnvOrDefault("ELEVENLABS_API_KEY", ""), voiceId: getEnvOrDefault("ELEVENLABS_VOICE_ID", ""), modelId: getEnvOrDefault("ELEVENLABS_MODEL", "eleven_multilingual_v2"), }, bot: { freeWillChance: 0.02, memoryChance: 0.3, mentionCooldown: 24 * 60 * 60 * 1000, // 24 hours mentionProbability: 0.001, spontaneousSchedulerEnabled: getBooleanEnvOrDefault("BOT_SPONTANEOUS_SCHEDULER_ENABLED", true), spontaneousSchedulerMinIntervalMs: parseInt(getEnvOrDefault("BOT_SPONTANEOUS_MIN_INTERVAL_MS", String(2 * 24 * 60 * 60 * 1000))), // 2 days spontaneousSchedulerMaxIntervalMs: parseInt(getEnvOrDefault("BOT_SPONTANEOUS_MAX_INTERVAL_MS", String(7 * 24 * 60 * 60 * 1000))), // 7 days }, web: { port: parseInt(getEnvOrDefault("WEB_PORT", "3000")), baseUrl: getEnvOrDefault("WEB_BASE_URL", "http://localhost:3000"), sessionSecret: getEnvOrDefault("SESSION_SECRET", crypto.randomUUID()), }, };