import { EventEmitter } from 'events'; import * as pty from 'node-pty'; import { SSHAgent } from './agents/ssh-agent'; import { BashAgent } from './agents/bash-agent'; export interface AgentOptions { workspacePath?: string; sessionId: string; // WebSocket session ID terminalSessionId?: string; // Terminal session ID for persistence vmHost?: string; vmPort?: number; vmUser?: string; sandboxed?: boolean; // Whether to run in sandboxed Docker container (default: true) } export interface Agent extends EventEmitter { id: string; type: string; status: string; startTime: number; initialize(): Promise; sendInput(input: string): Promise; stop(): Promise; resize?(cols: number, rows: number): Promise; } // Removed local ClaudeAgent - now using SSH connection to VM export class AgentManager extends EventEmitter { private agents: Map = new Map(); private agentTypes: Map = new Map(); constructor() { super(); // Increase max listeners to prevent warnings with multiple WebSocket connections this.setMaxListeners(100); } async initialize(): Promise { // Register available agent types this.registerAgentType('ssh', SSHAgent); this.registerAgentType('bash', BashAgent); // Future agent types can be registered here // this.registerAgentType('openai', OpenAIAgent); // this.registerAgentType('ollama', OllamaAgent); } registerAgentType(name: string, AgentClass: any): void { this.agentTypes.set(name, AgentClass); console.log(`Registered agent type: ${name}`); } async launchAgent(type: string, options: AgentOptions): Promise { const AgentClass = this.agentTypes.get(type); if (!AgentClass) { throw new Error(`Unknown agent type: ${type}`); } const agentId = `agent_${type}_${Date.now()}_${Math.random().toString(36).substring(7)}`; const agent = new AgentClass(agentId, options); try { await agent.initialize(); this.agents.set(agentId, agent); console.log(`Launched ${type} agent: ${agentId}`); // Forward agent events agent.on('output', (data: any) => { this.emit('agent_output', { agentId, data }); }); agent.on('error', (error: any) => { this.emit('agent_error', { agentId, error }); }); agent.on('exit', (code: number | null) => { this.handleAgentExit(agentId, code); }); agent.on('sessionId', (sessionId: string) => { this.emit('agent_sessionId', { agentId, sessionId }); }); agent.on('context-usage', (usage: any) => { this.emit('context_usage', { agentId, usage }); }); agent.on('command-complete', () => { this.emit('agent_command_complete', { agentId }); }); return agentId; } catch (error) { console.error(`Failed to launch ${type} agent:`, error); throw error; } } async sendToAgent(agentId: string, input: string): Promise { const agent = this.agents.get(agentId); if (!agent) { throw new Error(`Agent not found: ${agentId}`); } return agent.sendInput(input); } async stopAgent(agentId: string): Promise { const agent = this.agents.get(agentId); if (agent) { await agent.stop(); this.agents.delete(agentId); console.log(`Stopped agent: ${agentId}`); } } async stopAllAgents(): Promise { const stopPromises = Array.from(this.agents.keys()).map(agentId => this.stopAgent(agentId) ); await Promise.all(stopPromises); } getActiveAgents(): Array<{id: string, type: string, status: string, startTime: number}> { return Array.from(this.agents.entries()).map(([id, agent]) => ({ id, type: agent.type, status: agent.status, startTime: agent.startTime })); } // Get all agents as an object for monitoring purposes getAllAgents(): Record { const result: Record = {}; for (const [id, agent] of this.agents.entries()) { result[id] = { type: agent.type, status: agent.status, startTime: agent.startTime, sessionId: (agent as any).sessionId || 'unknown' }; } return result; } getAgent(agentId: string): Agent | undefined { return this.agents.get(agentId); } // Detach agent without stopping it (for persistence) detachAgent(agentId: string): void { const agent = this.agents.get(agentId); if (!agent) { return; } // Don't remove listeners from the agent itself - we'll reattach // Just note that it's detached console.log(`Detached agent: ${agentId}`); } // Reattach to an existing agent reattachAgent(agentId: string): boolean { const agent = this.agents.get(agentId); if (!agent) { return false; } // Don't re-add listeners - they're already there from initial launch // Detach doesn't remove them anymore, so we don't need to re-add console.log(`Reattached to agent: ${agentId}`); return true; } private handleAgentExit(agentId: string, code: number | null): void { console.log(`Agent ${agentId} exited with code ${code}`); this.agents.delete(agentId); this.emit('agent_exit', { agentId, code: code ?? 0 }); } }