UNPKG

@hanzo/dev

Version:

Hanzo Dev - Meta AI development CLI that manages and runs all LLMs and CLI tools

435 lines (369 loc) 13 kB
import { EventEmitter } from 'events'; import { spawn, ChildProcess } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import chalk from 'chalk'; import { FileEditor } from './editor'; import { CodeActAgent } from './code-act-agent'; import { FunctionCallingSystem } from './function-calling'; import { MCPClient, MCPSession } from './mcp-client'; export interface WorkspacePane { id: string; type: 'shell' | 'editor' | 'browser' | 'planner' | 'output'; title: string; content?: string; active: boolean; } export interface ShellSession { id: string; process: ChildProcess; cwd: string; history: string[]; output: string; } export class UnifiedWorkspace extends EventEmitter { private panes: Map<string, WorkspacePane> = new Map(); private shellSessions: Map<string, ShellSession> = new Map(); private editor: FileEditor; private agent: CodeActAgent; private functionCalling: FunctionCallingSystem; private mcpClient: MCPClient; private activePane: string = ''; private browserUrl: string = ''; constructor() { super(); this.editor = new FileEditor(); this.agent = new CodeActAgent(); this.functionCalling = new FunctionCallingSystem(); this.mcpClient = new MCPClient(); // Initialize default panes this.initializeDefaultPanes(); } private initializeDefaultPanes(): void { // Shell pane this.createPane('shell', 'Shell', ''); // Editor pane this.createPane('editor', 'Editor', 'No file open'); // Browser pane this.createPane('browser', 'Browser', 'Browser: Ready'); // Planner pane this.createPane('planner', 'Planner', 'Task planner ready'); // Output pane this.createPane('output', 'Output', ''); // Set shell as active by default this.setActivePane('shell'); } private createPane(type: WorkspacePane['type'], title: string, content: string): void { const id = `${type}-${Date.now()}`; const pane: WorkspacePane = { id, type, title, content, active: false }; this.panes.set(id, pane); } setActivePane(type: WorkspacePane['type']): void { // Find pane by type for (const [id, pane] of this.panes) { if (pane.type === type) { this.activePane = id; pane.active = true; } else { pane.active = false; } } this.emit('pane-changed', type); } // Shell operations async executeShellCommand(command: string): Promise<void> { const shellPane = this.getPane('shell'); if (!shellPane) return; // Get or create shell session let session = this.getOrCreateShellSession(); // Add to history session.history.push(command); // Execute command this.appendToPane('shell', `\n$ ${command}\n`); try { const result = await this.functionCalling.callFunction({ id: Date.now().toString(), name: 'run_command', arguments: { command, cwd: session.cwd } }); if (result.result?.stdout) { this.appendToPane('shell', result.result.stdout); } if (result.result?.stderr) { this.appendToPane('shell', chalk.red(result.result.stderr)); } // Update cwd if cd command if (command.startsWith('cd ')) { const newDir = command.substring(3).trim(); session.cwd = path.resolve(session.cwd, newDir); } } catch (error) { this.appendToPane('shell', chalk.red(`Error: ${error}`)); } } private getOrCreateShellSession(): ShellSession { const sessionId = 'main'; if (!this.shellSessions.has(sessionId)) { const session: ShellSession = { id: sessionId, process: spawn('bash', [], { cwd: process.cwd() }), cwd: process.cwd(), history: [], output: '' }; this.shellSessions.set(sessionId, session); } return this.shellSessions.get(sessionId)!; } // Editor operations async openFile(filePath: string): Promise<void> { const result = await this.editor.execute({ command: 'view', path: filePath }); if (result.success) { this.updatePane('editor', result.content || ''); this.updatePaneTitle('editor', `Editor - ${path.basename(filePath)}`); this.setActivePane('editor'); } else { this.appendToPane('output', chalk.red(`Failed to open file: ${result.message}`)); } } async saveFile(filePath: string, content: string): Promise<void> { fs.writeFileSync(filePath, content); this.appendToPane('output', chalk.green(`✓ Saved ${filePath}`)); } // Browser operations async navigateBrowser(url: string): Promise<void> { this.browserUrl = url; this.updatePane('browser', `Browser: ${url}`); this.appendToPane('output', `Navigated to ${url}`); // In a real implementation, this would use a headless browser // For now, we'll just simulate try { const response = await fetch(url); const text = await response.text(); const preview = text.substring(0, 500) + '...'; this.updatePane('browser', `URL: ${url}\n\n${preview}`); } catch (error) { this.updatePane('browser', `Failed to load ${url}: ${error}`); } } // Planner operations async planTask(description: string): Promise<void> { this.updatePane('planner', `Planning: ${description}\n\nGenerating execution plan...`); this.setActivePane('planner'); // Use the agent to plan const plan = await this.generatePlan(description); let planContent = `Task: ${description}\n\nExecution Plan:\n`; plan.steps.forEach((step, i) => { const parallel = plan.parallelizable[i] ? ' [can run in parallel]' : ''; planContent += `${i + 1}. ${step}${parallel}\n`; }); this.updatePane('planner', planContent); } private async generatePlan(description: string): Promise<{ steps: string[]; parallelizable: boolean[]; }> { // Simplified planning logic const steps: string[] = []; const parallelizable: boolean[] = []; if (description.includes('debug')) { steps.push('Reproduce the issue'); parallelizable.push(false); steps.push('Analyze error logs'); parallelizable.push(false); steps.push('Identify root cause'); parallelizable.push(false); steps.push('Implement fix'); parallelizable.push(false); steps.push('Test the fix'); parallelizable.push(false); } else if (description.includes('feature')) { steps.push('Analyze requirements'); parallelizable.push(false); steps.push('Design implementation'); parallelizable.push(false); steps.push('Write tests'); parallelizable.push(true); steps.push('Implement feature'); parallelizable.push(true); steps.push('Run tests'); parallelizable.push(false); steps.push('Update documentation'); parallelizable.push(true); } else { steps.push('Analyze task'); parallelizable.push(false); steps.push('Execute task'); parallelizable.push(false); steps.push('Verify results'); parallelizable.push(false); } return { steps, parallelizable }; } // Execute planned task async executePlan(): Promise<void> { const plannerPane = this.getPane('planner'); if (!plannerPane || !plannerPane.content) return; // Extract task from planner const lines = plannerPane.content.split('\n'); const taskLine = lines.find(l => l.startsWith('Task:')); if (!taskLine) return; const task = taskLine.substring(5).trim(); this.appendToPane('output', chalk.cyan(`\nExecuting task: ${task}\n`)); // Execute using agent await this.agent.executeTask(task); } // Pane management private getPane(type: WorkspacePane['type']): WorkspacePane | undefined { for (const pane of this.panes.values()) { if (pane.type === type) return pane; } return undefined; } private updatePane(type: WorkspacePane['type'], content: string): void { const pane = this.getPane(type); if (pane) { pane.content = content; this.emit('pane-updated', type, content); } } private appendToPane(type: WorkspacePane['type'], content: string): void { const pane = this.getPane(type); if (pane) { pane.content = (pane.content || '') + content; this.emit('pane-updated', type, pane.content); } } private updatePaneTitle(type: WorkspacePane['type'], title: string): void { const pane = this.getPane(type); if (pane) { pane.title = title; this.emit('pane-title-updated', type, title); } } // Display workspace (simplified for CLI) displayWorkspace(): void { console.clear(); console.log(chalk.bold.cyan('╔══════════════════════════════════════════════════════════════╗')); console.log(chalk.bold.cyan('║ 🚀 Hanzo Dev Workspace ║')); console.log(chalk.bold.cyan('╚══════════════════════════════════════════════════════════════╝')); console.log(); // Display pane tabs const tabs: string[] = []; for (const pane of this.panes.values()) { const isActive = pane.id === this.activePane; const tab = isActive ? chalk.bold.yellow(`[${pane.title}]`) : chalk.gray(`[${pane.title}]`); tabs.push(tab); } console.log(tabs.join(' ')); console.log(chalk.gray('─'.repeat(64))); // Display active pane content const activePane = this.panes.get(this.activePane); if (activePane && activePane.content) { const lines = activePane.content.split('\n'); const maxLines = 20; const displayLines = lines.slice(-maxLines); console.log(displayLines.join('\n')); } console.log(chalk.gray('─'.repeat(64))); } // Cleanup async cleanup(): Promise<void> { // Close shell sessions for (const session of this.shellSessions.values()) { session.process.kill(); } // Disconnect MCP sessions const sessions = this.mcpClient.getAllSessions(); for (const session of sessions) { await this.mcpClient.disconnect(session.id); } } } // Interactive workspace session export class WorkspaceSession { private workspace: UnifiedWorkspace; private running: boolean = true; constructor() { this.workspace = new UnifiedWorkspace(); } async start(): Promise<void> { console.log(chalk.bold.cyan('\n🎯 Starting Unified Workspace...\n')); // Set up event listeners this.workspace.on('pane-updated', () => { if (this.running) { this.workspace.displayWorkspace(); } }); // Initial display this.workspace.displayWorkspace(); // Start interactive loop const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); console.log(chalk.gray('\nCommands: shell <cmd>, edit <file>, browse <url>, plan <task>, execute, switch <pane>, exit\n')); const prompt = () => { rl.question(chalk.green('workspace> '), async (input) => { if (!this.running) return; const [cmd, ...args] = input.trim().split(' '); const arg = args.join(' '); try { switch (cmd) { case 'shell': case 'sh': await this.workspace.executeShellCommand(arg); break; case 'edit': case 'e': await this.workspace.openFile(arg); break; case 'browse': case 'b': await this.workspace.navigateBrowser(arg); break; case 'plan': case 'p': await this.workspace.planTask(arg); break; case 'execute': case 'x': await this.workspace.executePlan(); break; case 'switch': case 's': this.workspace.setActivePane(arg as any); this.workspace.displayWorkspace(); break; case 'exit': case 'quit': this.running = false; await this.workspace.cleanup(); rl.close(); return; default: console.log(chalk.red(`Unknown command: ${cmd}`)); } } catch (error) { console.log(chalk.red(`Error: ${error}`)); } if (this.running) { prompt(); } }); }; prompt(); } }