UNPKG

ai-debug-local-mcp

Version:

🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh

428 lines • 15.9 kB
/** * VimAutomationHandler - Advanced Vim Automation for AI-Debug * * This handler incorporates sophisticated vim interaction capabilities * extracted from the CC-Vim vim plugin for automated testing and debugging. * * Based on analysis of vim_plugin_OLD/claude_realtime_v2.vim (638 lines) * Key capabilities ported: pane management, file tree interaction, * layout control, and real-time job monitoring. */ import { spawn } from 'child_process'; import { promises as fs } from 'fs'; export class VimAutomationHandler { activeSessions = new Map(); socketBaseDir = '/tmp'; /** * Create a new CC-Vim session with automated testing capabilities * Based on s:OpenClaudeLayout() from the vim plugin */ async createCCVimSession(options) { const sessionId = `ccvim_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const socketPath = `${this.socketBaseDir}/nvim_CC_VIM_${options.socketSuffix || sessionId}`; // Clean up any existing socket try { await fs.unlink(socketPath); } catch (e) { // Socket doesn't exist, that's fine } const session = { socketPath, sessionId, workingDirectory: options.workingDirectory, isActive: false }; this.activeSessions.set(sessionId, session); // Launch neovim with CC-Vim configuration // Based on the compiled cc-vim-ide script analysis const nvimArgs = [ '--listen', socketPath, '-c', 'set rtp+=/Users/og/src/cc-vim', '-c', 'lua require("cc-vim").setup()', '-c', 'CCVimLayout', options.initialFile || '.' ]; const nvimProcess = spawn('nvim', nvimArgs, { cwd: options.workingDirectory, stdio: 'pipe', detached: true }); session.process = nvimProcess; session.isActive = true; // Give neovim time to start and create socket await this.waitForSocket(socketPath, 5000); return { sessionId, socketPath }; } /** * Wait for neovim socket to become available */ async waitForSocket(socketPath, timeoutMs) { const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { try { await fs.access(socketPath); return; } catch (e) { await new Promise(resolve => setTimeout(resolve, 100)); } } throw new Error(`Socket ${socketPath} not available after ${timeoutMs}ms`); } /** * Execute a vim command via socket connection * Based on the bridge communication patterns from the vim plugin */ async executeVimCommand(sessionId, command) { const session = this.activeSessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } return new Promise((resolve, reject) => { const nvim = spawn('nvim', ['--server', session.socketPath, '--remote-send', `<Esc>:${command}<CR>`], { stdio: ['pipe', 'pipe', 'pipe'] }); let output = ''; let error = ''; nvim.stdout.on('data', (data) => { output += data.toString(); }); nvim.stderr.on('data', (data) => { error += data.toString(); }); nvim.on('close', (code) => { if (code === 0) { resolve(output); } else { reject(new Error(`Vim command failed: ${error}`)); } }); // Timeout after 10 seconds setTimeout(() => { nvim.kill(); reject(new Error('Vim command timeout')); }, 10000); }); } /** * Get current layout information - ULTRA FAST Lua version (1-2ms vs 200ms) * Uses direct Lua execution instead of VimScript */ async getLayoutInfo(sessionId) { const session = this.activeSessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } // Use ultra-fast Lua automation module const luaCode = ` local automation = require('cc-vim.automation') return vim.fn.json_encode(automation.get_layout_info()) `; const result = await this.executeVimExpression(sessionId, `luaeval('${luaCode.replace(/'/g, "\\'")}')`); return JSON.parse(result); } /** * Execute Lua automation function directly (ULTRA FAST - 1-3ms) */ async executeLuaAutomation(sessionId, functionName, ...args) { const session = this.activeSessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } const argsStr = args.length > 0 ? `, ${args.map(arg => JSON.stringify(arg)).join(', ')}` : ''; const luaCode = ` local automation = require('cc-vim.automation') return vim.fn.json_encode(automation.${functionName}(${argsStr})) `; const result = await this.executeVimExpression(sessionId, `luaeval('${luaCode.replace(/'/g, "\\'")}')`); try { return JSON.parse(result); } catch (e) { // If not JSON, return raw result return result; } } /** * Execute a vim expression and return the result */ async executeVimExpression(sessionId, expression) { const session = this.activeSessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } return new Promise((resolve, reject) => { const nvim = spawn('nvim', ['--server', session.socketPath, '--remote-expr', expression], { stdio: ['pipe', 'pipe', 'pipe'] }); let output = ''; let error = ''; nvim.stdout.on('data', (data) => { output += data.toString(); }); nvim.stderr.on('data', (data) => { error += data.toString(); }); nvim.on('close', (code) => { if (code === 0) { resolve(output.trim()); } else { reject(new Error(`Vim expression failed: ${error}`)); } }); setTimeout(() => { nvim.kill(); reject(new Error('Vim expression timeout')); }, 10000); }); } /** * Focus a specific pane (tree, editor, or claude) - ULTRA FAST Lua version (1ms vs 200ms) */ async focusPane(sessionId, pane) { const result = await this.executeLuaAutomation(sessionId, 'focus_pane', pane); if (!result.success) { throw new Error(`Failed to focus pane ${pane}: ${result.error}`); } } /** * Refresh the file tree - ULTRA FAST Lua version (2-5ms vs 500ms) */ async refreshFileTree(sessionId) { const result = await this.executeLuaAutomation(sessionId, 'refresh_file_tree'); if (!result.success) { throw new Error(`Failed to refresh file tree: ${result.error || 'Unknown error'}`); } } /** * Open a file from the tree * Based on s:OpenFileFromTree() logic */ async openFileFromTree(sessionId, filename) { // First focus the tree await this.focusPane(sessionId, 'tree'); // Find the line with the filename and simulate Enter press const searchAndOpen = ` call search('${filename}') call feedkeys("\\<CR>") `; await this.executeVimCommand(sessionId, searchAndOpen); } /** * Start a Claude edit session * Based on s:LaunchClaudeEdit() from vim plugin */ async startClaudeEdit(sessionId, prompt) { const claudeCommand = `ClaudeEdit ${prompt.replace(/"/g, '\\"')}`; await this.executeVimCommand(sessionId, claudeCommand); } /** * Comprehensive CC-Vim functionality test * This combines all the testing capabilities we need for automated validation */ async testCCVimFunctionality(sessionId) { const result = { success: false, layoutCreated: false, panesFunctional: false, claudeIntegration: false, fileTreeWorking: false, errors: [], diagnostics: { socketConnection: false, luaModuleLoaded: false, commandsAvailable: false, themeDetected: 'unknown' } }; try { // Test 1: Socket connection const session = this.activeSessions.get(sessionId); if (!session) { throw new Error('Session not found'); } try { await fs.access(session.socketPath); result.diagnostics.socketConnection = true; } catch (e) { result.errors.push('Socket connection failed'); return result; } // Test 2: Layout creation and pane detection try { const layoutInfo = await this.getLayoutInfo(sessionId); result.layoutCreated = layoutInfo.layoutActive; // Check for required panes const hasTreePane = layoutInfo.panes.some(p => p.type === 'tree'); const hasEditorPane = layoutInfo.panes.some(p => p.type === 'editor'); const hasClaudePane = layoutInfo.panes.some(p => p.type === 'claude'); result.panesFunctional = hasTreePane && hasEditorPane && hasClaudePane; if (!result.panesFunctional) { result.errors.push(`Missing panes - Tree: ${hasTreePane}, Editor: ${hasEditorPane}, Claude: ${hasClaudePane}`); } } catch (e) { result.errors.push(`Layout test failed: ${e instanceof Error ? e.message : String(e)}`); } // Test 3: Lua module loading try { const luaTest = await this.executeVimExpression(sessionId, 'type(require("cc-vim"))'); result.diagnostics.luaModuleLoaded = luaTest === 'table'; } catch (e) { result.errors.push('Lua module loading failed'); } // Test 4: Command availability try { await this.executeVimCommand(sessionId, 'help CCVimLayout'); result.diagnostics.commandsAvailable = true; } catch (e) { result.errors.push('CC-Vim commands not available'); } // Test 5: File tree functionality try { await this.refreshFileTree(sessionId); await this.focusPane(sessionId, 'tree'); result.fileTreeWorking = true; } catch (e) { result.errors.push(`File tree test failed: ${e instanceof Error ? e.message : String(e)}`); } // Test 6: Theme detection try { const themeVar = await this.executeVimExpression(sessionId, 'get(g:, "CC_VIM_DETECTED_THEME", "unknown")'); result.diagnostics.themeDetected = themeVar.replace(/['"]/g, ''); } catch (e) { result.diagnostics.themeDetected = 'detection_failed'; } // Test 7: Claude integration (basic test) try { await this.focusPane(sessionId, 'claude'); result.claudeIntegration = true; } catch (e) { result.errors.push(`Claude integration test failed: ${e instanceof Error ? e.message : String(e)}`); } // Calculate overall success result.success = result.layoutCreated && result.panesFunctional && result.fileTreeWorking && result.diagnostics.socketConnection && result.diagnostics.luaModuleLoaded; } catch (error) { result.errors.push(`Test execution failed: ${error instanceof Error ? error.message : String(error)}`); } return result; } /** * Get comprehensive session diagnostics * Similar to the AI-Debug session diagnostics but for CC-Vim */ async getSessionDiagnostics(sessionId) { const session = this.activeSessions.get(sessionId) || null; let socketStatus = 'missing'; let processStatus = 'unknown'; if (!session) { return { session, socketStatus, processStatus, layoutInfo: null, recommendations: ['Session not found - create new session'] }; } // Check socket status const recommendations = []; let layoutInfo = null; try { await fs.access(session.socketPath); socketStatus = 'active'; } catch (e) { socketStatus = 'missing'; recommendations.push('Socket missing - restart neovim session'); } // Check process status if (session.process) { processStatus = session.process.killed ? 'stopped' : 'running'; } // Get layout info if socket is active if (socketStatus === 'active') { try { layoutInfo = await this.getLayoutInfo(sessionId); if (!layoutInfo.layoutActive) { recommendations.push('Layout not active - run :CCVimLayout'); } if (layoutInfo.panes.length < 3) { recommendations.push('Missing panes - check layout creation'); } } catch (e) { recommendations.push('Layout info unavailable - check vim communication'); } } return { session, socketStatus, processStatus, layoutInfo, recommendations }; } /** * Close a CC-Vim session and clean up resources * Based on s:CloseClaudeLayout() from vim plugin */ async closeSession(sessionId) { const session = this.activeSessions.get(sessionId); if (!session) { return; } // Try to close layout gracefully try { await this.executeVimCommand(sessionId, 'ClaudeClose'); } catch (e) { // If graceful close fails, force quit try { await this.executeVimCommand(sessionId, 'qa!'); } catch (e2) { // Force kill the process if (session.process && !session.process.killed) { session.process.kill('SIGTERM'); } } } // Clean up socket file try { await fs.unlink(session.socketPath); } catch (e) { // Socket already cleaned up } this.activeSessions.delete(sessionId); } /** * Get all active sessions */ getActiveSessions() { return Array.from(this.activeSessions.values()); } /** * Clean up all sessions (emergency cleanup) */ async cleanup() { const sessions = Array.from(this.activeSessions.keys()); await Promise.all(sessions.map(sessionId => this.closeSession(sessionId))); } } export const vimAutomationHandler = new VimAutomationHandler(); //# sourceMappingURL=vim-automation-handler.js.map