"use strict"; /** * RuvBot - Self-learning AI Assistant with RuVector Backend * * Main entry point for the RuvBot framework. * Combines Clawdbot-style personal AI with RuVector's WASM vector operations. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.RuvBot = void 0; exports.createRuvBot = createRuvBot; exports.createRuvBotFromEnv = createRuvBotFromEnv; const eventemitter3_1 = require("eventemitter3"); const node_http_1 = require("node:http"); const pino_1 = __importDefault(require("pino")); const uuid_1 = require("uuid"); const BotConfig_js_1 = require("./core/BotConfig.js"); const BotState_js_1 = require("./core/BotState.js"); const errors_js_1 = require("./core/errors.js"); const index_js_1 = require("./integration/providers/index.js"); // ============================================================================ // RuvBot Main Class // ============================================================================ class RuvBot extends eventemitter3_1.EventEmitter { constructor(options = {}) { super(); this.agents = new Map(); this.sessions = new Map(); this.isRunning = false; this.llmProvider = null; this.httpServer = null; this.id = (0, uuid_1.v4)(); // Initialize configuration if (options.config) { this.configManager = new BotConfig_js_1.ConfigManager(options.config); } else { this.configManager = BotConfig_js_1.ConfigManager.fromEnv(); } // Validate configuration const validation = this.configManager.validate(); if (!validation.valid) { throw new errors_js_1.ConfigurationError(`Invalid configuration: ${validation.errors.join(', ')}`); } // Initialize logger const config = this.configManager.getConfig(); this.logger = (0, pino_1.default)({ level: config.logging.level, transport: config.logging.pretty ? { target: 'pino-pretty', options: { colorize: true } } : undefined, }); // Initialize state manager this.stateManager = new BotState_js_1.BotStateManager(); this.logger.info({ botId: this.id }, 'RuvBot instance created'); // Auto-start if requested if (options.autoStart) { this.start().catch((error) => { this.logger.error({ error }, 'Auto-start failed'); this.emit('error', error); }); } } // ========================================================================== // Lifecycle Methods // ========================================================================== /** * Start the bot and all configured services */ async start() { if (this.isRunning) { this.logger.warn('RuvBot is already running'); return; } this.logger.info('Starting RuvBot...'); this.stateManager.setStatus('starting'); try { const config = this.configManager.getConfig(); // Initialize core services await this.initializeServices(); // Start integrations await this.startIntegrations(config); // Start API server if enabled if (config.api.enabled) { await this.startApiServer(config); } // Mark as running this.isRunning = true; this.startTime = new Date(); this.stateManager.setStatus('running'); this.logger.info({ botId: this.id, name: config.name }, 'RuvBot started successfully'); this.emit('ready'); } catch (error) { this.stateManager.setStatus('error'); this.logger.error({ error }, 'Failed to start RuvBot'); throw new errors_js_1.InitializationError(`Failed to start RuvBot: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Stop the bot and cleanup resources */ async stop() { if (!this.isRunning) { this.logger.warn('RuvBot is not running'); return; } this.logger.info('Stopping RuvBot...'); this.stateManager.setStatus('stopping'); try { // Stop all agents for (const [agentId] of this.agents) { await this.stopAgent(agentId); } // End all sessions for (const [sessionId] of this.sessions) { await this.endSession(sessionId); } // Stop integrations await this.stopIntegrations(); // Stop API server await this.stopApiServer(); this.isRunning = false; this.stateManager.setStatus('stopped'); this.logger.info('RuvBot stopped successfully'); this.emit('shutdown'); } catch (error) { this.stateManager.setStatus('error'); this.logger.error({ error }, 'Error during shutdown'); throw error; } } // ========================================================================== // Agent Management // ========================================================================== /** * Spawn a new agent with the given configuration */ async spawnAgent(config) { const agentId = config.id || (0, uuid_1.v4)(); if (this.agents.has(agentId)) { throw new errors_js_1.RuvBotError(`Agent with ID ${agentId} already exists`, 'AGENT_EXISTS'); } const agent = { id: agentId, name: config.name, config, status: 'idle', createdAt: new Date(), lastActiveAt: new Date(), }; this.agents.set(agentId, agent); this.logger.info({ agentId, name: config.name }, 'Agent spawned'); this.emit('agent:spawn', agent); return agent; } /** * Stop an agent by ID */ async stopAgent(agentId) { const agent = this.agents.get(agentId); if (!agent) { throw new errors_js_1.RuvBotError(`Agent with ID ${agentId} not found`, 'AGENT_NOT_FOUND'); } // End all sessions for this agent for (const [sessionId, session] of this.sessions) { if (session.agentId === agentId) { await this.endSession(sessionId); } } this.agents.delete(agentId); this.logger.info({ agentId }, 'Agent stopped'); this.emit('agent:stop', agentId); } /** * Get an agent by ID */ getAgent(agentId) { return this.agents.get(agentId); } /** * List all active agents */ listAgents() { return Array.from(this.agents.values()); } // ========================================================================== // Session Management // ========================================================================== /** * Create a new session for an agent */ async createSession(agentId, options = {}) { const agent = this.agents.get(agentId); if (!agent) { throw new errors_js_1.RuvBotError(`Agent with ID ${agentId} not found`, 'AGENT_NOT_FOUND'); } const sessionId = (0, uuid_1.v4)(); const config = this.configManager.getConfig(); const session = { id: sessionId, agentId, userId: options.userId, channelId: options.channelId, platform: options.platform || 'api', messages: [], context: { topics: [], entities: [], }, metadata: options.metadata || {}, createdAt: new Date(), updatedAt: new Date(), expiresAt: new Date(Date.now() + config.session.defaultTTL), }; this.sessions.set(sessionId, session); this.logger.info({ sessionId, agentId }, 'Session created'); this.emit('session:create', session); return session; } /** * End a session by ID */ async endSession(sessionId) { const session = this.sessions.get(sessionId); if (!session) { throw new errors_js_1.RuvBotError(`Session with ID ${sessionId} not found`, 'SESSION_NOT_FOUND'); } this.sessions.delete(sessionId); this.logger.info({ sessionId }, 'Session ended'); this.emit('session:end', sessionId); } /** * Get a session by ID */ getSession(sessionId) { return this.sessions.get(sessionId); } /** * List all active sessions */ listSessions() { return Array.from(this.sessions.values()); } // ========================================================================== // Message Handling // ========================================================================== /** * Send a message to an agent in a session */ async chat(sessionId, content, options = {}) { const session = this.sessions.get(sessionId); if (!session) { throw new errors_js_1.RuvBotError(`Session with ID ${sessionId} not found`, 'SESSION_NOT_FOUND'); } const agent = this.agents.get(session.agentId); if (!agent) { throw new errors_js_1.RuvBotError(`Agent with ID ${session.agentId} not found`, 'AGENT_NOT_FOUND'); } // Create user message const userMessage = { id: (0, uuid_1.v4)(), sessionId, role: 'user', content, attachments: options.attachments, metadata: options.metadata, createdAt: new Date(), }; // Add to session session.messages.push(userMessage); session.updatedAt = new Date(); // Update agent status agent.status = 'processing'; agent.lastActiveAt = new Date(); this.logger.debug({ sessionId, messageId: userMessage.id }, 'User message received'); this.emit('message', userMessage, session); try { // Generate response (placeholder for LLM integration) const responseContent = await this.generateResponse(session, agent, content); // Create assistant message const assistantMessage = { id: (0, uuid_1.v4)(), sessionId, role: 'assistant', content: responseContent, createdAt: new Date(), }; session.messages.push(assistantMessage); session.updatedAt = new Date(); agent.status = 'idle'; this.logger.debug({ sessionId, messageId: assistantMessage.id }, 'Assistant response generated'); this.emit('message', assistantMessage, session); return assistantMessage; } catch (error) { agent.status = 'error'; throw error; } } // ========================================================================== // Status & Info // ========================================================================== /** * Get the current bot status */ getStatus() { const config = this.configManager.getConfig(); return { id: this.id, name: config.name, state: this.stateManager.getStatus(), isRunning: this.isRunning, uptime: this.startTime ? Date.now() - this.startTime.getTime() : undefined, agents: this.agents.size, sessions: this.sessions.size, }; } /** * Get the current configuration */ getConfig() { return this.configManager.getConfig(); } // ========================================================================== // Private Methods // ========================================================================== async initializeServices() { this.logger.debug('Initializing core services...'); const config = this.configManager.getConfig(); // Initialize LLM provider based on configuration const { provider, apiKey, model } = config.llm; // Check for available API keys in priority order const openrouterKey = process.env.OPENROUTER_API_KEY; const anthropicKey = process.env.ANTHROPIC_API_KEY || apiKey; const googleAIKey = process.env.GOOGLE_AI_API_KEY || process.env.GEMINI_API_KEY; if (openrouterKey) { // Use OpenRouter for Gemini 2.5 and other models this.llmProvider = (0, index_js_1.createOpenRouterProvider)({ apiKey: openrouterKey, model: model || 'google/gemini-2.5-pro-preview-05-06', siteName: 'RuvBot', }); this.logger.info({ provider: 'openrouter', model: model || 'google/gemini-2.5-pro-preview-05-06' }, 'LLM provider initialized'); } else if (googleAIKey) { // Use Google AI directly (Gemini 2.5) this.llmProvider = (0, index_js_1.createGoogleAIProvider)({ apiKey: googleAIKey, model: model || 'gemini-2.5-flash', }); this.logger.info({ provider: 'google-ai', model: model || 'gemini-2.5-flash' }, 'LLM provider initialized'); } else if (provider === 'anthropic' && anthropicKey) { this.llmProvider = (0, index_js_1.createAnthropicProvider)({ apiKey: anthropicKey, model: model || 'claude-3-5-sonnet-20241022', }); this.logger.info({ provider: 'anthropic', model }, 'LLM provider initialized'); } else if (anthropicKey) { // Fallback to Anthropic if only that key is available this.llmProvider = (0, index_js_1.createAnthropicProvider)({ apiKey: anthropicKey, model: model || 'claude-3-5-sonnet-20241022', }); this.logger.info({ provider: 'anthropic', model }, 'LLM provider initialized'); } else { this.logger.warn({}, 'No LLM API key found. Set GOOGLE_AI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY'); } // TODO: Initialize memory manager, skill registry, etc. } async startIntegrations(config) { this.logger.debug('Starting integrations...'); if (config.slack.enabled) { this.logger.info('Slack integration enabled'); // TODO: Initialize Slack adapter } if (config.discord.enabled) { this.logger.info('Discord integration enabled'); // TODO: Initialize Discord adapter } if (config.webhook.enabled) { this.logger.info('Webhook integration enabled'); // TODO: Initialize webhook handler } } async stopIntegrations() { this.logger.debug('Stopping integrations...'); // TODO: Stop all integration adapters } async startApiServer(config) { const port = config.api.port || 3000; const host = config.api.host || '0.0.0.0'; this.httpServer = (0, node_http_1.createServer)((req, res) => { this.handleApiRequest(req, res).catch((error) => { this.logger.error({ err: error }, 'Unhandled API request error'); if (!res.headersSent) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Internal server error' })); } }); }); return new Promise((resolve, reject) => { this.httpServer.on('error', (err) => { this.logger.error({ err, port, host }, 'API server failed to start'); reject(err); }); this.httpServer.listen(port, host, () => { this.logger.info({ port, host }, 'API server listening'); resolve(); }); }); } async stopApiServer() { if (!this.httpServer) return; return new Promise((resolve) => { this.httpServer.close(() => { this.logger.debug('API server stopped'); this.httpServer = null; resolve(); }); }); } async handleApiRequest(req, res) { const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`); const path = url.pathname; const method = req.method || 'GET'; // CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); if (method === 'OPTIONS') { res.writeHead(204); res.end(); return; } const json = (status, data) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(data)); }; // Health check if (path === '/health' || path === '/healthz') { json(200, { status: 'healthy', uptime: this.startTime ? Math.floor((Date.now() - this.startTime.getTime()) / 1000) : 0, timestamp: new Date().toISOString(), }); return; } // Readiness check if (path === '/ready' || path === '/readyz') { if (this.isRunning) { json(200, { status: 'ready' }); } else { json(503, { status: 'not ready' }); } return; } // Status if (path === '/api/status') { json(200, this.getStatus()); return; } // Chat endpoint if (path === '/api/chat' && method === 'POST') { const body = await this.parseRequestBody(req); const message = body?.message; const agentId = body?.agentId || 'default-agent'; if (!message) { json(400, { error: 'Missing "message" field' }); return; } // Create or reuse a session let sessionId = body?.sessionId; if (!sessionId || !this.sessions.has(sessionId)) { const session = await this.createSession(agentId); sessionId = session.id; } const response = await this.chat(sessionId, message); json(200, { sessionId, agentId, response }); return; } // List agents if (path === '/api/agents' && method === 'GET') { json(200, { agents: this.listAgents() }); return; } // List sessions if (path === '/api/sessions' && method === 'GET') { json(200, { sessions: this.listSessions() }); return; } // Root — simple landing page if (path === '/') { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(`