aide-cli
Version:
AIDE - The companion control system for Claude Code with intelligent task management
223 lines (190 loc) • 7.67 kB
JavaScript
const fs = require('fs');
const path = require('path');
const os = require('os');
const { spawn } = require('child_process');
class AIDEDaemonClient {
constructor() {
this.daemonUrl = 'http://127.0.0.1:47742';
this.tokenFile = path.join(os.homedir(), '.aide', 'daemon-token');
this.daemonScript = path.join(__dirname, 'aide-daemon.js');
}
async getToken() {
try {
if (fs.existsSync(this.tokenFile)) {
return fs.readFileSync(this.tokenFile, 'utf8').trim();
}
} catch (e) {
// Token file doesn't exist
}
return null;
}
async isDaemonRunning() {
try {
const response = await fetch(`${this.daemonUrl}/health`);
return response.ok;
} catch (e) {
return false;
}
}
async startDaemon() {
return new Promise((resolve, reject) => {
const child = spawn('node', [this.daemonScript, 'start'], {
stdio: 'ignore',
detached: true
});
child.on('error', (error) => {
resolve(false);
});
// Detach the child process so it continues running
child.unref();
// Wait a moment and check if daemon is running
setTimeout(async () => {
const isRunning = await this.isDaemonRunning();
resolve(isRunning);
}, 1500);
});
}
async ensureDaemonRunning() {
const isRunning = await this.isDaemonRunning();
if (!isRunning) {
console.log('🔄 AIDE daemon not running, starting automatically...');
const started = await this.startDaemon();
if (!started) {
console.log('⚠️ Failed to auto-start daemon, using direct execution');
return false;
}
// Wait a moment for daemon to fully start
await new Promise(resolve => setTimeout(resolve, 1500));
// Verify it actually started
const verifyRunning = await this.isDaemonRunning();
if (!verifyRunning) {
console.log('⚠️ Daemon failed to start, using direct execution');
return false;
}
console.log('✅ AIDE daemon started automatically');
}
return true;
}
async executeCommand(command, args = [], cwd = null) {
try {
// Ensure daemon is running
const daemonStarted = await this.ensureDaemonRunning();
if (!daemonStarted) {
return await this.executeDirectly(command, args, cwd);
}
const token = await this.getToken();
if (!token) {
console.log('⚠️ No daemon token available, using direct execution');
return await this.executeDirectly(command, args, cwd);
}
const response = await fetch(`${this.daemonUrl}/execute`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
command,
args,
cwd: cwd || process.cwd()
}),
timeout: 10000 // 10 second timeout
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Command execution failed');
}
return await response.json();
} catch (error) {
// Check if it's a connection error (daemon died)
if (error.message.includes('ECONNREFUSED') || error.message.includes('fetch failed')) {
console.log('🔄 Daemon connection lost, attempting restart...');
// Try to restart daemon once
const restarted = await this.startDaemon();
if (restarted) {
console.log('✅ Daemon restarted, retrying command...');
try {
const token = await this.getToken();
const response = await fetch(`${this.daemonUrl}/execute`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
command,
args,
cwd: cwd || process.cwd()
})
});
if (response.ok) {
return await response.json();
}
} catch (retryError) {
// Fall through to direct execution
}
}
}
// Fallback to direct execution if daemon fails
console.log(`⚠️ Using direct execution: ${error.message}`);
return await this.executeDirectly(command, args, cwd);
}
}
async executeDirectly(command, args = [], cwd = null) {
return new Promise((resolve, reject) => {
const cmd = command;
const allArgs = args;
const child = spawn(cmd, allArgs, {
cwd: cwd || process.cwd(),
stdio: ['pipe', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
resolve({
exitCode: code,
stdout: stdout.trim(),
stderr: stderr.trim(),
success: code === 0
});
});
child.on('error', (error) => {
reject(error);
});
});
}
async stopDaemon() {
try {
const token = await this.getToken();
if (!token) {
console.log('❌ No daemon token found');
return false;
}
const response = await fetch(`${this.daemonUrl}/shutdown`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
console.log('✅ AIDE daemon stopped');
return true;
} else {
console.log('❌ Failed to stop daemon');
return false;
}
} catch (e) {
console.log('❌ AIDE daemon not running or failed to stop');
return false;
}
}
}
// Export both class name and alias for backward compatibility
module.exports = { DaemonClient: AIDEDaemonClient, AIDEDaemonClient };