// Bun's timer loop drifts so nextTime - Date.now() can go negative inside // @discordjs/voice's audio cycle, which breaks audio playback entirely. // Clamp all setTimeout delays to >= 0 before any voice code is imported. // See https://github.com/oven-sh/bun/issues/11313 if (!(globalThis as Record<string, unknown>).__bunTimeoutClamp) { const origSetTimeout = globalThis.setTimeout.bind(globalThis); globalThis.setTimeout = ((handler: TimerHandler, timeout = 0, ...args: unknown[]) => origSetTimeout(handler, Math.max(0, timeout), ...args) ) as typeof setTimeout; (globalThis as Record<string, unknown>).__bunTimeoutClamp = true; }
import path from "node:path"; import { fileURLToPath } from "node:url"; import { appConfig, ensureRuntimeEnv } from "./config.ts"; import { createDashboardServer } from "./dashboard.ts"; import { ClankerBot } from "./bot.ts"; import { DiscoveryService } from "./services/discovery.ts"; import { GifService } from "./services/gif.ts"; import { LLMService } from "./llm.ts"; import { MemoryManager } from "./memory/memoryManager.ts"; import { WebSearchService } from "./services/search.ts"; import { Store } from "./store/store.ts"; import { VideoContextService } from "./video/videoContextService.ts"; import { BrowserManager } from "./services/BrowserManager.ts"; import { PublicHttpsEntrypoint } from "./services/publicHttpsEntrypoint.ts"; import { ScreenShareSessionManager } from "./services/screenShareSessionManager.ts"; import { RuntimeActionLogger } from "./services/runtimeActionLogger.ts";
export async function main() { ensureRuntimeEnv();
const dbPath = path.resolve(process.cwd(), "data", "clanker.db"); const memoryFilePath = path.resolve(process.cwd(), "memory", "MEMORY.md");
const store = new Store(dbPath); store.init(); const runtimeActionLogger = new RuntimeActionLogger({ enabled: appConfig.runtimeStructuredLogsEnabled, writeToStdout: appConfig.runtimeStructuredLogsStdout, logFilePath: appConfig.runtimeStructuredLogsFilePath }); runtimeActionLogger.attachToStore(store);
const llm = new LLMService({ appConfig, store }); const discovery = new DiscoveryService({ store }); const gifs = new GifService({ appConfig, store }); const search = new WebSearchService({ appConfig, store }); const video = new VideoContextService({ store, llm }); const memory = new MemoryManager({ store, llm, memoryFilePath }); await Promise.all([ memory.refreshMemoryMarkdown(), llm.warmup() ]); const browserManager = new BrowserManager({ maxConcurrentSessions: 2, sessionTimeoutMs: 300_000 });
const bot = new ClankerBot({ appConfig, store, llm, memory, discovery, search, gifs, video, browserManager }); const publicHttpsEntrypoint = new PublicHttpsEntrypoint({ appConfig, store }); const screenShareSessionManager = new ScreenShareSessionManager({ appConfig, store, bot, publicHttpsEntrypoint }); bot.attachScreenShareSessionManager(screenShareSessionManager); const dashboard = createDashboardServer({ appConfig, store, bot, memory, publicHttpsEntrypoint, screenShareSessionManager });
await bot.start(); await publicHttpsEntrypoint.start();
let closing = false;
const shutdown = async (signal) => {
if (closing) {
console.warn(Received ${signal} during shutdown. Forcing immediate exit.);
process.exit(1);
return;
}
closing = true;
const forceTimer = setTimeout(() => {
console.error("Shutdown timed out after 10s. Forcing exit.");
process.exit(1);
}, 10_000);
forceTimer.unref();
console.log(`Shutting down (${signal})...`);
try {
await bot.stop();
} catch {
// ignore
}
try {
await publicHttpsEntrypoint.stop();
} catch {
// ignore
}
await closeHttpServerWithTimeout(dashboard.server, 4_000);
if (typeof llm.close === "function") {
llm.close();
}
runtimeActionLogger.close();
store.close();
process.exit(0);
};
process.on("SIGINT", () => shutdown("SIGINT")); process.on("SIGTERM", () => shutdown("SIGTERM")); }
export function isDirectExecution(argv = process.argv) { const entry = String(argv?.[1] || "").trim(); if (!entry) return false; return path.resolve(entry) === fileURLToPath(import.meta.url); }
export async function runCli() { try { await main(); } catch (error) { console.error("Fatal startup error:", error); process.exit(1); } }
if (isDirectExecution()) { void runCli(); }
async function closeHttpServerWithTimeout(server, timeoutMs = 4_000) { if (!server || typeof server.close !== "function") return;
await new Promise((resolve) => { let resolved = false; const finish = () => { if (resolved) return; resolved = true; clearTimeout(timer); resolve(undefined); };
const timer = setTimeout(() => {
try {
if (typeof server.closeIdleConnections === "function") {
server.closeIdleConnections();
}
} catch {
// ignore
}
try {
if (typeof server.closeAllConnections === "function") {
server.closeAllConnections();
}
} catch {
// ignore
}
finish();
}, Math.max(100, Number(timeoutMs) || 4_000));
try {
server.close(() => {
finish();
});
} catch {
finish();
}
}); }
