UNPKG

jay-code

Version:

Streamlined AI CLI orchestration engine with mathematical rigor and enterprise-grade reliability

909 lines (810 loc) • 31.8 kB
/** * Simple orchestrator implementation for Node.js compatibility */ import { EventEmitter } from 'events'; import express from 'express'; import { WebSocketServer } from 'ws'; import { createServer } from 'http'; import { spawn } from 'child_process'; import path from 'path'; import { fileURLToPath } from 'url'; import cors from 'cors'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Simple in-memory stores const agents = new Map(); const tasks = new Map(); const memory = new Map(); // Event bus const eventBus = new EventEmitter(); // Component status const componentStatus = { eventBus: false, orchestrator: false, memoryManager: false, terminalPool: false, mcpServer: false, coordinationManager: false, webUI: false, }; // Simple MCP server function startMCPServer(port: number) { console.log(`🌐 Starting MCP server on port ${port}...`); // In a real implementation, this would start the actual MCP server componentStatus.mcpServer = true; return true; } // Enhanced web UI with console interface function startWebUI(host: string, port: number) { const app = express(); const server = createServer(app); const wss = new WebSocketServer({ server }); // Add CORS middleware for cross-origin support app.use( cors({ origin: '*', methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: true, }), ); // Global error handler middleware app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { console.error('Global error handler:', err); res.status(err.status || 500).json({ error: err.message || 'Internal server error', timestamp: new Date().toISOString(), }); }); // Request logging middleware app.use((req, res, next) => { console.log(`${new Date().toISOString()} ${req.method} ${req.path}`); next(); }); // Store CLI output history and active connections const outputHistory: string[] = []; const activeConnections: Set<any> = new Set(); // CLI output capture system let cliProcess: any = null; const consoleHTML = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Jay-Code Console</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Consolas', 'Monaco', 'Courier New', monospace; background: #0d1117; color: #c9d1d9; height: 100vh; display: flex; flex-direction: column; overflow: hidden; } .header { background: #161b22; border-bottom: 1px solid #21262d; padding: 10px 20px; display: flex; justify-content: space-between; align-items: center; } .title { font-weight: bold; color: #58a6ff; } .connection-status { font-size: 12px; color: #7c3aed; } .console-container { flex: 1; display: flex; flex-direction: column; overflow: hidden; } .console-output { flex: 1; overflow-y: auto; padding: 10px; background: #0d1117; font-size: 13px; line-height: 1.4; white-space: pre-wrap; word-wrap: break-word; } .console-input { background: #161b22; border: none; border-top: 1px solid #21262d; padding: 10px; color: #c9d1d9; font-family: inherit; font-size: 13px; outline: none; } .console-input:focus { background: #21262d; } .prompt { color: #58a6ff; font-weight: bold; } .error { color: #ff7b72; } .success { color: #3fb950; } .warning { color: #ffa657; } .info { color: #79c0ff; } .dim { color: #8b949e; } .scrollbar { scrollbar-width: thin; scrollbar-color: #21262d #0d1117; } .scrollbar::-webkit-scrollbar { width: 8px; } .scrollbar::-webkit-scrollbar-track { background: #0d1117; } .scrollbar::-webkit-scrollbar-thumb { background: #21262d; border-radius: 4px; } .scrollbar::-webkit-scrollbar-thumb:hover { background: #30363d; } .system-status { display: flex; gap: 15px; font-size: 11px; } .status-item { display: flex; align-items: center; gap: 5px; } .status-dot { width: 6px; height: 6px; border-radius: 50%; background: #3fb950; } .status-dot.inactive { background: #f85149; } </style> </head> <body> <div class="header"> <div class="title">🧠 Jay-Code Console</div> <div class="system-status"> <div class="status-item"> <div class="status-dot" id="ws-status"></div> <span id="ws-text">Connecting...</span> </div> <div class="status-item"> <div class="status-dot" id="cli-status"></div> <span id="cli-text">CLI Ready</span> </div> </div> </div> <div class="console-container"> <div class="console-output scrollbar" id="output"></div> <input type="text" class="console-input" id="input" placeholder="Enter jay-code command..." autocomplete="off"> </div> <script> const output = document.getElementById('output'); const input = document.getElementById('input'); const wsStatus = document.getElementById('ws-status'); const wsText = document.getElementById('ws-text'); const cliStatus = document.getElementById('cli-status'); const cliText = document.getElementById('cli-text'); let ws = null; let commandHistory = []; let historyIndex = -1; let reconnectAttempts = 0; let reconnectTimer = null; let isReconnecting = false; const MAX_RECONNECT_ATTEMPTS = 10; const BASE_RECONNECT_DELAY = 1000; function getReconnectDelay() { // Exponential backoff with jitter const exponentialDelay = Math.min(BASE_RECONNECT_DELAY * Math.pow(2, reconnectAttempts), 30000); const jitter = Math.random() * 0.3 * exponentialDelay; return exponentialDelay + jitter; } function connect() { if (isReconnecting || (ws && ws.readyState === WebSocket.CONNECTING)) { console.log('Already connecting, skipping duplicate attempt'); return; } isReconnecting = true; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = \`\${protocol}//\${window.location.host}\`; try { console.log(\`Attempting WebSocket connection to \${wsUrl}\`); ws = new WebSocket(wsUrl); ws.onopen = () => { console.log('WebSocket connected successfully'); wsStatus.classList.remove('inactive'); wsText.textContent = 'Connected'; reconnectAttempts = 0; isReconnecting = false; if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } appendOutput('\n<span class="success">šŸ”— Connected to Jay-Code Console</span>\n'); appendOutput('<span class="info">Type "help" for available commands or use any jay-code command</span>\n\n'); }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); handleMessage(data); } catch (error) { console.error('Failed to parse WebSocket message:', error); appendOutput(\`\n<span class="error">āŒ Invalid message received: \${(error instanceof Error ? error.message : String(error))}</span>\n\`); } }; ws.onclose = (event) => { console.log(\`WebSocket closed: code=\${event.code}, reason=\${event.reason}\`); wsStatus.classList.add('inactive'); wsText.textContent = 'Disconnected'; isReconnecting = false; if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { reconnectAttempts++; const delay = getReconnectDelay(); appendOutput(\`\n<span class="error">šŸ”— Connection lost. Reconnecting in \${Math.round(delay/1000)}s... (attempt \${reconnectAttempts}/\${MAX_RECONNECT_ATTEMPTS})</span>\n\`); reconnectTimer = setTimeout(() => { reconnectTimer = null; connect(); }, delay); } else { appendOutput(\`\n<span class="error">āŒ Failed to reconnect after \${MAX_RECONNECT_ATTEMPTS} attempts. Please refresh the page.</span>\n\`); wsText.textContent = 'Failed to connect'; } }; ws.onerror = (error) => { console.error('WebSocket error:', error); appendOutput(\`\n<span class="error">āŒ WebSocket error: \${(error instanceof Error ? error.message : String(error)) || 'Connection failed'}</span>\n\`); isReconnecting = false; }; } catch (error) { console.error('Failed to create WebSocket:', error); appendOutput(\`\n<span class="error">āŒ Failed to create WebSocket connection: \${(error instanceof Error ? error.message : String(error))}</span>\n\`); isReconnecting = false; // Try reconnect if not exceeded max attempts if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { reconnectAttempts++; const delay = getReconnectDelay(); reconnectTimer = setTimeout(() => { reconnectTimer = null; connect(); }, delay); } } } function handleMessage(data) { switch (data.type) { case 'output': appendOutput(data.data); break; case 'error': appendOutput('<span class="error">' + data.data + '</span>'); break; case 'command_complete': appendOutput('\n<span class="prompt">jay-code> </span>'); break; case 'status': updateStatus(data.data); break; } } function appendOutput(text) { output.innerHTML += text; output.scrollTop = output.scrollHeight; } function updateStatus(status) { // Update CLI status based on server response if (status.cliActive) { cliStatus.classList.remove('inactive'); cliText.textContent = 'CLI Active'; } else { cliStatus.classList.add('inactive'); cliText.textContent = 'CLI Inactive'; } } function sendCommand(command) { if (ws && ws.readyState === WebSocket.OPEN) { appendOutput('<span class="prompt">jay-code> </span>' + command + '\n'); ws.send(JSON.stringify({ type: 'command', data: command })); // Add to history if (command.trim() && commandHistory[commandHistory.length - 1] !== command) { commandHistory.push(command); if (commandHistory.length > 100) { commandHistory.shift(); } } historyIndex = commandHistory.length; } } // Input handling input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { const command = input.value.trim(); if (command) { sendCommand(command); input.value = ''; } } else if (e.key === 'ArrowUp') { e.preventDefault(); if (historyIndex > 0) { historyIndex--; input.value = commandHistory[historyIndex] || ''; } } else if (e.key === 'ArrowDown') { e.preventDefault(); if (historyIndex < commandHistory.length - 1) { historyIndex++; input.value = commandHistory[historyIndex] || ''; } else { historyIndex = commandHistory.length; input.value = ''; } } else if (e.key === 'Tab') { e.preventDefault(); // Basic tab completion for common commands const value = input.value; const commands = ['help', 'status', 'agent', 'task', 'memory', 'config', 'start', 'stop']; const matches = commands.filter(cmd => cmd.startsWith(value)); if (matches.length === 1) { input.value = matches[0] + ' '; } } }); // Focus input on page load window.addEventListener('load', () => { input.focus(); connect(); }); // Implement heartbeat to detect stale connections setInterval(() => { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() })); } }, 30000); // Ping every 30 seconds // Handle page visibility changes document.addEventListener('visibilitychange', () => { if (!document.hidden && ws && ws.readyState !== WebSocket.OPEN) { console.log('Page became visible, checking connection...'); reconnectAttempts = 0; // Reset attempts when page becomes visible connect(); } }); // Keep input focused document.addEventListener('click', () => { input.focus(); }); </script> </body> </html> `; app.get('/', (req, res) => { res.send(consoleHTML); }); // API endpoints app.get('/api/status', (req, res) => { res.json({ components: componentStatus, metrics: { agents: agents.size, tasks: tasks.size, memory: memory.size, connectedClients: activeConnections.size, }, }); }); app.get('/api/history', (req, res) => { const limit = parseInt(req.query.limit as string) || 100; res.json({ history: outputHistory.slice(-limit), total: outputHistory.length, }); }); app.post('/api/command', express.json(), (req, res) => { const { command } = req.body; if (!command) { res.status(400).json({ error: 'Command is required' }); return; } // Execute command and return immediately // Output will be sent via WebSocket try { broadcastToClients({ type: 'output', data: `<span class="prompt">API> </span>${command}\\n`, }); executeCliCommand(command, null); res.json({ success: true, message: 'Command executed' }); } catch (error) { res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } }); app.get('/api/agents', (req, res) => { const agentList = Array.from(agents.entries()).map(([id, agent]) => ({ id, ...agent, })); res.json(agentList); }); app.get('/api/tasks', (req, res) => { const taskList = Array.from(tasks.entries()).map(([id, task]) => ({ id, ...task, })); res.json(taskList); }); app.get('/api/memory', (req, res) => { const memoryList = Array.from(memory.entries()).map(([key, value]) => ({ key, value, type: typeof value, size: JSON.stringify(value).length, })); res.json(memoryList); }); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime(), components: componentStatus, }); }); // WebSocket for real-time CLI interaction wss.on('connection', (ws, req) => { const clientIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress; console.log(`šŸ”Œ WebSocket client connected from ${clientIp}`); activeConnections.add(ws); // Send initial status and history ws.send( JSON.stringify({ type: 'status', data: { ...componentStatus, cliActive: true }, }), ); // Send recent output history outputHistory.slice(-50).forEach((line) => { ws.send( JSON.stringify({ type: 'output', data: line, }), ); }); // Handle incoming commands ws.on('message', (message) => { try { const data = JSON.parse(message.toString()); console.log(`Received command from client: ${data.type}`); if (data.type === 'command') { handleCliCommand(data.data, ws); } else if (data.type === 'ping') { // Handle ping/pong for connection keepalive ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() })); } } catch (error) { console.error('Failed to handle WebSocket message:', error); ws.send( JSON.stringify({ type: 'error', data: `Invalid message format: ${error instanceof Error ? error.message : String(error)}`, timestamp: new Date().toISOString(), }), ); } }); ws.on('close', () => { console.log('šŸ”Œ WebSocket client disconnected'); activeConnections.delete(ws); }); ws.on('error', (error) => { console.error('WebSocket client error:', error); // Send detailed error information to client before closing try { ws.send( JSON.stringify({ type: 'error', data: `Server WebSocket error: ${(error instanceof Error ? error.message : String(error)) || 'Unknown error'}`, timestamp: new Date().toISOString(), }), ); } catch (sendError) { console.error('Failed to send error to client:', sendError); } activeConnections.delete(ws); }); }); // Helper function to send response to specific client or broadcast function sendResponse(ws: any, data: any) { if (ws) { ws.send(JSON.stringify(data)); } else { broadcastToClients(data); } } // CLI command execution handler function handleCliCommand(command: string, ws: any) { try { // Add timestamp and format output const timestamp = new Date().toLocaleTimeString(); const logEntry = `[${timestamp}] Executing: ${command}`; outputHistory.push(logEntry); // Broadcast to all connected clients broadcastToClients({ type: 'output', data: `<span class="dim">[${timestamp}]</span> <span class="info">Executing:</span> ${command}\\n`, }); // Execute the command executeCliCommand(command, ws); } catch (error) { const errorMsg = `Error executing command: ${error instanceof Error ? error.message : String(error)}`; outputHistory.push(errorMsg); sendResponse(ws, { type: 'error', data: errorMsg, }); } } // Execute CLI commands and capture output function executeCliCommand(command: string, ws: any) { // Handle built-in commands first if (command === 'help') { const helpText = `<span class="success">Available Commands:</span> • <span class="info">help</span> - Show this help message • <span class="info">status</span> - Show system status • <span class="info">agent list</span> - List all agents • <span class="info">agent spawn [type]</span> - Spawn a new agent • <span class="info">task list</span> - List all tasks • <span class="info">memory list</span> - List memory entries • <span class="info">config show</span> - Show configuration • <span class="info">clear</span> - Clear console • <span class="info">version</span> - Show version information <span class="warning">Note:</span> This is a web console interface for jay-code CLI commands. `; sendResponse(ws, { type: 'output', data: helpText, }); sendResponse(ws, { type: 'command_complete' }); return; } if (command === 'clear') { sendResponse(ws, { type: 'output', data: '\\x1b[2J\\x1b[H', // ANSI clear screen }); sendResponse(ws, { type: 'command_complete' }); return; } if (command === 'status') { const statusText = `<span class="success">System Status:</span> • Event Bus: <span class="${componentStatus.eventBus ? 'success' : 'error'}">${componentStatus.eventBus ? 'Active' : 'Inactive'}</span> • Orchestrator: <span class="${componentStatus.orchestrator ? 'success' : 'error'}">${componentStatus.orchestrator ? 'Active' : 'Inactive'}</span> • Memory Manager: <span class="${componentStatus.memoryManager ? 'success' : 'error'}">${componentStatus.memoryManager ? 'Active' : 'Inactive'}</span> • Terminal Pool: <span class="${componentStatus.terminalPool ? 'success' : 'error'}">${componentStatus.terminalPool ? 'Active' : 'Inactive'}</span> • MCP Server: <span class="${componentStatus.mcpServer ? 'success' : 'error'}">${componentStatus.mcpServer ? 'Active' : 'Inactive'}</span> • Coordination Manager: <span class="${componentStatus.coordinationManager ? 'success' : 'error'}">${componentStatus.coordinationManager ? 'Active' : 'Inactive'}</span> • Web UI: <span class="${componentStatus.webUI ? 'success' : 'error'}">${componentStatus.webUI ? 'Active' : 'Inactive'}</span> <span class="info">Metrics:</span> • Active Agents: ${agents.size} • Pending Tasks: ${tasks.size} • Memory Entries: ${memory.size} `; sendResponse(ws, { type: 'output', data: statusText, }); sendResponse(ws, { type: 'command_complete' }); return; } // For other commands, spawn a subprocess const args = command.split(' '); const cmd = args[0]; const cmdArgs = args.slice(1); // Determine the correct jay-code executable path const rootDir = path.resolve(__dirname, '../..'); const cliPath = path.join(rootDir, 'bin', 'jay-code'); // Spawn the command const child = spawn('node', [path.join(rootDir, 'src/cli/simple-cli.js'), ...cmdArgs], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, JAY_CODE_WEB_MODE: 'true' }, }); // Handle stdout child.stdout.on('data', (data) => { const output = data.toString(); outputHistory.push(output); // Convert ANSI colors to HTML spans const htmlOutput = convertAnsiToHtml(output); broadcastToClients({ type: 'output', data: htmlOutput, }); }); // Handle stderr child.stderr.on('data', (data) => { const error = data.toString(); outputHistory.push(error); broadcastToClients({ type: 'error', data: convertAnsiToHtml(error), }); }); // Handle process exit child.on('close', (code) => { const exitMsg = code === 0 ? `<span class="success">Command completed successfully</span>` : `<span class="error">Command failed with exit code ${code}</span>`; broadcastToClients({ type: 'output', data: `\\n${exitMsg}\\n`, }); sendResponse(ws, { type: 'command_complete' }); }); child.on('error', (error) => { const errorMsg = `<span class="error">Failed to execute command: ${error instanceof Error ? error.message : String(error)}</span>`; outputHistory.push(errorMsg); sendResponse(ws, { type: 'error', data: errorMsg, }); sendResponse(ws, { type: 'command_complete' }); }); } // Broadcast message to all connected clients function broadcastToClients(message: any) { const messageStr = JSON.stringify(message); activeConnections.forEach((client) => { if (client.readyState === 1) { // WebSocket.OPEN client.send(messageStr); } }); } // Convert ANSI escape codes to HTML function convertAnsiToHtml(text: string): string { return text .replace(/\x1b\[0m/g, '</span>') .replace(/\x1b\[1m/g, '<span style="font-weight: bold;">') .replace(/\x1b\[31m/g, '<span class="error">') .replace(/\x1b\[32m/g, '<span class="success">') .replace(/\x1b\[33m/g, '<span class="warning">') .replace(/\x1b\[34m/g, '<span class="info">') .replace(/\x1b\[35m/g, '<span style="color: #d946ef;">') .replace(/\x1b\[36m/g, '<span style="color: #06b6d4;">') .replace(/\x1b\[37m/g, '<span class="dim">') .replace(/\x1b\[90m/g, '<span class="dim">') .replace(/\x1b\[[0-9;]*m/g, '') // Remove any remaining ANSI codes .replace(/\n/g, '\\n') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/&lt;span/g, '<span') .replace(/span&gt;/g, 'span>'); } return new Promise((resolve, reject) => { server.on('error', (err: any) => { if (err.code === 'EADDRINUSE') { console.error(`\nāŒ Port ${port} is already in use`); console.log(`šŸ’” Try a different port: jay-code start --ui --port ${port + 1}`); console.log(`šŸ’” Or stop the process using port ${port}: lsof -ti:${port} | xargs kill -9`); componentStatus.webUI = false; reject(err); } else { console.error('āŒ Web UI server error:', err.message, err.stack); reject(err); } }); server.listen(port, host, () => { console.log(`🌐 Web UI available at http://${host}:${port}`); componentStatus.webUI = true; resolve(server); }); }); } // Start all components export async function startOrchestrator(options: any) { console.log('\nšŸš€ Starting orchestration components...\n'); // Start Event Bus console.log('⚔ Starting Event Bus...'); componentStatus.eventBus = true; eventBus.emit('system:start'); console.log('āœ… Event Bus started'); // Start Orchestrator Engine console.log('🧠 Starting Orchestrator Engine...'); componentStatus.orchestrator = true; console.log('āœ… Orchestrator Engine started'); // Start Memory Manager console.log('šŸ’¾ Starting Memory Manager...'); componentStatus.memoryManager = true; console.log('āœ… Memory Manager started'); // Start Terminal Pool console.log('šŸ–„ļø Starting Terminal Pool...'); componentStatus.terminalPool = true; console.log('āœ… Terminal Pool started'); // Start MCP Server const mcpPort = options.mcpPort || 3001; startMCPServer(mcpPort); console.log('āœ… MCP Server started'); // Start Coordination Manager console.log('šŸ”„ Starting Coordination Manager...'); componentStatus.coordinationManager = true; console.log('āœ… Coordination Manager started'); // Start Web UI if requested if (options.ui && !options.noUi) { const host = options.host || 'localhost'; const port = options.port || 3000; try { await startWebUI(host, port); } catch (err: any) { if (err.code === 'EADDRINUSE') { console.log('\nāš ļø Web UI could not start due to port conflict'); console.log(' Orchestrator is running without Web UI'); } else { console.error('\nāš ļø Web UI failed to start:', err.message); } } } console.log('\nāœ… All components started successfully!'); console.log('\nšŸ“Š System Status:'); console.log(' • Event Bus: Active'); console.log(' • Orchestrator: Active'); console.log(' • Memory Manager: Active'); console.log(' • Terminal Pool: Active'); console.log(' • MCP Server: Active'); console.log(' • Coordination Manager: Active'); if (options.ui && !options.noUi) { console.log( ` • Web UI: Active at http://${options.host || 'localhost'}:${options.port || 3000}`, ); } console.log('\nšŸ’” Use "jay-code status" to check system status'); console.log('šŸ’” Use "jay-code stop" to stop the orchestrator'); // Keep the process running if (!options.daemon) { console.log('\nšŸ“Œ Press Ctrl+C to stop the orchestrator...\n'); // Handle graceful shutdown process.on('SIGINT', () => { console.log('\n\nšŸ›‘ Shutting down orchestrator...'); process.exit(0); }); } } // Export component status for other commands export function getComponentStatus() { return componentStatus; } // Export stores for other commands export function getStores() { return { agents, tasks, memory }; }