diff --git a/npm/packages/ruvbot/src/RuvBot.ts b/npm/packages/ruvbot/src/RuvBot.ts index aff1685a..8fdd0c1f 100644 --- a/npm/packages/ruvbot/src/RuvBot.ts +++ b/npm/packages/ruvbot/src/RuvBot.ts @@ -6,6 +6,7 @@ */ import { EventEmitter } from 'eventemitter3'; +import { createServer, type Server, type IncomingMessage, type ServerResponse } from 'node:http'; import pino from 'pino'; import { v4 as uuidv4 } from 'uuid'; @@ -71,6 +72,7 @@ export class RuvBot extends EventEmitter { private isRunning: boolean = false; private startTime?: Date; private llmProvider: LLMProvider | null = null; + private httpServer: Server | null = null; constructor(options: RuvBotOptions = {}) { super(); @@ -539,16 +541,151 @@ export class RuvBot extends EventEmitter { } private async startApiServer(config: BotConfig): Promise { - this.logger.info( - { port: config.api.port, host: config.api.host }, - 'Starting API server...' - ); - // TODO: Initialize Fastify server + const port = config.api.port || 3000; + const host = config.api.host || '0.0.0.0'; + + this.httpServer = 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(); + }); + }); } private async stopApiServer(): Promise { - this.logger.debug('Stopping API server...'); - // TODO: Stop Fastify server + if (!this.httpServer) return; + + return new Promise((resolve) => { + this.httpServer!.close(() => { + this.logger.debug('API server stopped'); + this.httpServer = null; + resolve(); + }); + }); + } + + private async handleApiRequest(req: IncomingMessage, res: ServerResponse): Promise { + 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: number, data: unknown) => { + 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 as string; + const agentId = (body?.agentId as string) || 'default-agent'; + + if (!message) { + json(400, { error: 'Missing "message" field' }); + return; + } + + // Create or reuse a session + let sessionId = body?.sessionId as string; + 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(`RuvBot + +

RuvBot

Enterprise-grade AI Assistant

API Status
`); + return; + } + + // 404 + json(404, { error: 'Not found' }); + } + + private parseRequestBody(req: IncomingMessage): Promise | null> { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + req.on('data', (chunk: Buffer) => chunks.push(chunk)); + req.on('end', () => { + if (chunks.length === 0) { resolve(null); return; } + try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf-8'))); } + catch { reject(new Error('Invalid JSON')); } + }); + req.on('error', reject); + }); } private async generateResponse(