aide-cli
Version:
AIDE - The companion control system for Claude Code with intelligent task management
386 lines (329 loc) • 12.1 kB
JavaScript
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;