UNPKG

aide-cli

Version:

AIDE - The companion control system for Claude Code with intelligent task management

386 lines (329 loc) 12.1 kB
#!/usr/bin/env node const express = require('express'); const { spawn } = require('child_process'); const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const os = require('os'); class AIDEDaemon { constructor() { this.app = express(); this.port = 47742; this.tokenFile = path.join(os.homedir(), '.aide', 'daemon-token'); this.pidFile = path.join(os.homedir(), '.aide', 'daemon.pid'); this.logFile = path.join(os.homedir(), '.aide', 'daemon.log'); this.token = this.loadOrCreateToken(); this.setupMiddleware(); this.setupRoutes(); this.setupErrorHandling(); } loadOrCreateToken() { try { if (fs.existsSync(this.tokenFile)) { return fs.readFileSync(this.tokenFile, 'utf8').trim(); } } catch (e) { // Token file doesn't exist or can't be read } // Generate new token const token = crypto.randomBytes(32).toString('hex'); try { const aideDir = path.dirname(this.tokenFile); if (!fs.existsSync(aideDir)) { fs.mkdirSync(aideDir, { recursive: true }); } fs.writeFileSync(this.tokenFile, token); fs.chmodSync(this.tokenFile, 0o600); // Owner only } catch (e) { console.error('Failed to save daemon token:', e.message); } return token; } setupMiddleware() { this.app.use(express.json()); this.app.use((req, res, next) => { // Log request this.log(`${req.method} ${req.path} from ${req.ip}`); next(); }); // Authentication middleware this.app.use((req, res, next) => { if (req.path === '/health' || req.path === '/status') { return next(); } const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'Missing or invalid authorization header' }); } const providedToken = authHeader.slice(7); if (providedToken !== this.token) { return res.status(401).json({ error: 'Invalid token' }); } next(); }); } setupRoutes() { // Health check this.app.get('/health', (req, res) => { res.json({ status: 'healthy', pid: process.pid, uptime: process.uptime(), timestamp: new Date().toISOString() }); }); // Status with token (for client verification) this.app.get('/status', (req, res) => { res.json({ status: 'running', pid: process.pid, port: this.port, token: this.token, uptime: process.uptime() }); }); // Execute AIDE command this.app.post('/execute', async (req, res) => { try { const { command, args = [], cwd } = req.body; if (!command) { return res.status(400).json({ error: 'Command is required' }); } // Validate command is python3 and script is allowed if (command !== 'python3') { return res.status(403).json({ error: 'Only python3 commands allowed' }); } const allowedCommands = [ 'aide_init.py', 'aide_track.py', 'aide_status.py', 'aide_adapt.py', 'aide_i18n.py' ]; const scriptPath = args[0]; const scriptName = path.basename(scriptPath); if (!allowedCommands.includes(scriptName)) { return res.status(403).json({ error: 'Script not allowed' }); } const result = await this.executeCommand(command, args, cwd); res.json(result); } catch (error) { this.log(`Execution error: ${error.message}`); res.status(500).json({ error: 'Execution failed', message: error.message }); } }); // Shutdown daemon this.app.post('/shutdown', (req, res) => { this.log('Shutdown requested'); res.json({ message: 'Daemon shutting down' }); setTimeout(() => { this.cleanup(); process.exit(0); }, 100); }); } setupErrorHandling() { this.app.use((error, req, res, next) => { this.log(`Unhandled error: ${error.message}`); res.status(500).json({ error: 'Internal server error' }); }); this.app.use((req, res) => { res.status(404).json({ error: 'Endpoint not found' }); }); } async executeCommand(command, args, cwd) { return new Promise((resolve, reject) => { const cmd = command; const allArgs = args; this.log(`Executing: ${cmd} ${allArgs.join(' ')}`); const child = spawn(cmd, allArgs, { cwd: cwd || process.cwd(), stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env } }); let stdout = ''; let stderr = ''; child.stdout.on('data', (data) => { stdout += data.toString(); }); child.stderr.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { const result = { exitCode: code, stdout: stdout.trim(), stderr: stderr.trim(), success: code === 0 }; this.log(`Command completed with exit code: ${code}`); resolve(result); }); child.on('error', (error) => { this.log(`Command error: ${error.message}`); reject(error); }); // Timeout after 30 seconds const timeout = setTimeout(() => { child.kill('SIGTERM'); reject(new Error('Command timeout')); }, 30000); child.on('close', () => { clearTimeout(timeout); }); }); } log(message) { const timestamp = new Date().toISOString(); const logEntry = `${timestamp} - ${message}\n`; console.log(logEntry.trim()); try { fs.appendFileSync(this.logFile, logEntry); } catch (e) { // Ignore log file errors } } savePid() { try { fs.writeFileSync(this.pidFile, process.pid.toString()); } catch (e) { this.log(`Failed to save PID: ${e.message}`); } } cleanup() { try { if (fs.existsSync(this.pidFile)) { fs.unlinkSync(this.pidFile); } } catch (e) { this.log(`Cleanup error: ${e.message}`); } } start() { // Check if already running if (this.isAlreadyRunning()) { console.log('AIDE daemon is already running'); return false; } this.server = this.app.listen(this.port, '127.0.0.1', () => { this.log(`AIDE Daemon started on port ${this.port}`); this.log(`Token: ${this.token}`); this.savePid(); }); // Graceful shutdown process.on('SIGTERM', () => { this.log('SIGTERM received, shutting down'); this.cleanup(); process.exit(0); }); process.on('SIGINT', () => { this.log('SIGINT received, shutting down'); this.cleanup(); process.exit(0); }); return true; } isAlreadyRunning() { try { if (!fs.existsSync(this.pidFile)) { return false; } const pid = parseInt(fs.readFileSync(this.pidFile, 'utf8')); // Check if process is still running try { process.kill(pid, 0); return true; // Process exists } catch (e) { // Process doesn't exist, remove stale PID file fs.unlinkSync(this.pidFile); return false; } } catch (e) { return false; } } static async getToken() { const tokenFile = path.join(os.homedir(), '.aide', 'daemon-token'); try { if (fs.existsSync(tokenFile)) { return fs.readFileSync(tokenFile, 'utf8').trim(); } } catch (e) { // Token file doesn't exist } return null; } static async isRunning() { try { const response = await fetch('http://127.0.0.1:47742/health'); return response.ok; } catch (e) { return false; } } } // CLI handling if (require.main === module) { const command = process.argv[2]; if (command === 'start') { // Check if already running const daemon = new AIDEDaemon(); if (daemon.isAlreadyRunning()) { console.log('ℹ️ AIDE Daemon already running'); process.exit(0); } // Fork a background process const { spawn } = require('child_process'); const child = spawn(process.execPath, [__filename, 'run'], { detached: true, stdio: 'ignore' }); child.unref(); // Wait a moment to check if it started successfully setTimeout(async () => { const isRunning = await AIDEDaemon.isRunning(); if (isRunning) { console.log('✅ AIDE Daemon started successfully'); } else { console.log('❌ Failed to start AIDE Daemon'); } }, 1000); } else if (command === 'run') { // This is the actual daemon process running in background const daemon = new AIDEDaemon(); daemon.start(); } else if (command === 'stop') { // Stop daemon by sending shutdown request AIDEDaemon.getToken().then(token => { if (!token) { console.log('❌ No daemon token found'); return; } fetch('http://127.0.0.1:47742/shutdown', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }).then(() => { console.log('✅ AIDE Daemon stopped'); }).catch(() => { console.log('❌ Failed to stop daemon (may not be running)'); }); }); } else if (command === 'status') { AIDEDaemon.isRunning().then(running => { if (running) { console.log('✅ AIDE Daemon is running'); } else { console.log('❌ AIDE Daemon is not running'); } }); } else { console.log('Usage: node aide-daemon.js [start|stop|status]'); } } module.exports = AIDEDaemon;