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
JavaScript
/**
* 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