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

1,010 lines (996 loc) • 37.5 kB
/** * Lua Debugging Handler - Comprehensive Lua Development Tools * * Provides real, functional debugging tools for Lua development including: * - Project analysis and dependency management * - Script execution and testing analysis * - Performance profiling and memory analysis * - Code quality analysis with luacheck integration * - Web framework debugging (OpenResty, Lapis, etc.) * * All tools provide REAL functionality with actual Lua toolchain integration. */ import { exec } from 'child_process'; import { promisify } from 'util'; import * as fs from 'fs/promises'; import * as path from 'path'; import { existsSync } from 'fs'; import { BaseToolHandler } from './base-handler-migrated.js'; const execAsync = promisify(exec); export class LuaDebuggingHandler extends BaseToolHandler { name = 'lua-debugging'; description = 'Comprehensive Lua development debugging tools with real Lua toolchain integration'; get tools() { return this.getTools(); } getTools() { return [ { name: 'lua_project_inspector', description: 'Analyze Lua project structure, dependencies, and configuration', inputSchema: { type: 'object', properties: { projectPath: { type: 'string', description: 'Path to the Lua project directory' } }, required: ['projectPath'] } }, { name: 'lua_script_analyzer', description: 'Analyze Lua scripts for syntax, style, and potential issues using luacheck', inputSchema: { type: 'object', properties: { scriptPath: { type: 'string', description: 'Path to the Lua script or directory to analyze' }, checkLevel: { type: 'string', enum: ['basic', 'standard', 'strict'], default: 'standard', description: 'Level of analysis to perform' } }, required: ['scriptPath'] } }, { name: 'lua_test_runner', description: 'Run Lua tests using busted or other test frameworks', inputSchema: { type: 'object', properties: { projectPath: { type: 'string', description: 'Path to the Lua project directory' }, testPattern: { type: 'string', description: 'Test file pattern (optional)', default: 'spec' }, framework: { type: 'string', enum: ['busted', 'luaunit', 'auto'], default: 'auto', description: 'Test framework to use' } }, required: ['projectPath'] } }, { name: 'lua_dependency_analyzer', description: 'Analyze Lua dependencies and rockspec files', inputSchema: { type: 'object', properties: { projectPath: { type: 'string', description: 'Path to the Lua project directory' } }, required: ['projectPath'] } }, { name: 'lua_performance_profiler', description: 'Profile Lua script performance and identify bottlenecks', inputSchema: { type: 'object', properties: { scriptPath: { type: 'string', description: 'Path to the Lua script to profile' }, profilingMode: { type: 'string', enum: ['time', 'memory', 'call'], default: 'time', description: 'Type of profiling to perform' } }, required: ['scriptPath'] } }, { name: 'lua_debug_tracer', description: 'Trace Lua script execution with debug hooks', inputSchema: { type: 'object', properties: { scriptPath: { type: 'string', description: 'Path to the Lua script to trace' }, traceLevel: { type: 'string', enum: ['call', 'return', 'line', 'all'], default: 'call', description: 'Level of tracing detail' } }, required: ['scriptPath'] } }, { name: 'lua_memory_analyzer', description: 'Analyze Lua memory usage and detect leaks', inputSchema: { type: 'object', properties: { scriptPath: { type: 'string', description: 'Path to the Lua script to analyze' }, analysisType: { type: 'string', enum: ['usage', 'leaks', 'gc'], default: 'usage', description: 'Type of memory analysis to perform' } }, required: ['scriptPath'] } }, { name: 'lua_web_framework_inspector', description: 'Inspect Lua web frameworks (OpenResty, Lapis, Sailor)', inputSchema: { type: 'object', properties: { projectPath: { type: 'string', description: 'Path to the Lua web project directory' } }, required: ['projectPath'] } }, { name: 'lua_c_module_analyzer', description: 'Analyze Lua C modules and FFI usage', inputSchema: { type: 'object', properties: { projectPath: { type: 'string', description: 'Path to the Lua project directory' } }, required: ['projectPath'] } }, { name: 'lua_framework_detector', description: 'Detect Lua frameworks and provide framework-specific debugging info', inputSchema: { type: 'object', properties: { projectPath: { type: 'string', description: 'Path to the Lua project directory' } }, required: ['projectPath'] } } ]; } async handle(toolName, args, sessions) { const projectPath = args.projectPath || args.scriptPath || process.cwd(); try { switch (toolName) { case 'lua_project_inspector': return await this.inspectLuaProject(projectPath); case 'lua_script_analyzer': return await this.analyzeLuaScript(args.scriptPath, args.checkLevel || 'standard'); case 'lua_test_runner': return await this.runLuaTests(projectPath, args.testPattern || 'spec', args.framework || 'auto'); case 'lua_dependency_analyzer': return await this.analyzeLuaDependencies(projectPath); case 'lua_performance_profiler': return await this.profileLuaPerformance(args.scriptPath, args.profilingMode || 'time'); case 'lua_debug_tracer': return await this.traceLuaExecution(args.scriptPath, args.traceLevel || 'call'); case 'lua_memory_analyzer': return await this.analyzeLuaMemory(args.scriptPath, args.analysisType || 'usage'); case 'lua_web_framework_inspector': return await this.inspectLuaWebFramework(projectPath); case 'lua_c_module_analyzer': return await this.analyzeLuaCModules(projectPath); case 'lua_framework_detector': return await this.detectLuaFrameworks(projectPath); default: throw new Error(`Unknown Lua debugging tool: ${toolName}`); } } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), tool: toolName, timestamp: new Date().toISOString() }; } } async inspectLuaProject(projectPath) { if (!existsSync(projectPath)) { throw new Error(`Project path does not exist: ${projectPath}`); } const results = { success: true, projectPath, luaVersion: null, rockspecFiles: [], luaFiles: [], dependencies: [], structure: {}, frameworks: [], timestamp: new Date().toISOString() }; try { // Get Lua version try { const { stdout } = await execAsync('lua -v', { cwd: projectPath }); results.luaVersion = stdout.trim(); } catch { try { const { stdout } = await execAsync('luajit -v', { cwd: projectPath }); results.luaVersion = stdout.trim(); } catch { results.luaVersion = 'Lua not found in PATH'; } } // Find rockspec files const files = await this.findFiles(projectPath, /\.rockspec$/); results.rockspecFiles = files; // Count Lua files const luaFiles = await this.findFiles(projectPath, /\.lua$/); results.luaFiles = luaFiles; // Analyze project structure results.structure = await this.analyzeLuaProjectStructure(projectPath); // Detect frameworks results.frameworks = await this.detectLuaFrameworks(projectPath); return results; } catch (error) { results.success = false; results.error = error instanceof Error ? error.message : String(error); return results; } } async analyzeLuaScript(scriptPath, checkLevel) { const results = { success: true, scriptPath, checkLevel, issues: [], metrics: {}, timestamp: new Date().toISOString() }; try { // Try to use luacheck if available try { const checkArgs = this.getLuaCheckArgs(checkLevel); const { stdout, stderr } = await execAsync(`luacheck ${checkArgs} "${scriptPath}"`, { timeout: 30000 }); if (stdout) { results.issues = this.parseLuaCheckOutput(stdout); } if (stderr && !stderr.includes('Checking')) { results.warnings = stderr.split('\n').filter(line => line.trim()); } } catch (error) { // Fallback to basic syntax check try { await execAsync(`lua -l "${scriptPath}"`, { timeout: 10000 }); results.issues.push({ type: 'info', message: 'Lua syntax check passed (luacheck not available)', line: null }); } catch (syntaxError) { results.issues.push({ type: 'error', message: 'Lua syntax error detected', line: null, details: syntaxError instanceof Error ? syntaxError.message : String(syntaxError) }); } } // Basic file metrics if (existsSync(scriptPath)) { const content = await fs.readFile(scriptPath, 'utf-8'); results.metrics = { lines: content.split('\n').length, nonEmptyLines: content.split('\n').filter(line => line.trim()).length, functions: (content.match(/function\s+\w+/g) || []).length, localFunctions: (content.match(/local\s+function/g) || []).length }; } return results; } catch (error) { results.success = false; results.error = error instanceof Error ? error.message : String(error); return results; } } async runLuaTests(projectPath, testPattern, framework) { const results = { success: true, projectPath, framework, testResults: {}, timestamp: new Date().toISOString() }; try { let testCommand = ''; // Detect and run appropriate test framework if (framework === 'auto') { if (existsSync(path.join(projectPath, 'spec'))) { framework = 'busted'; testCommand = 'busted'; } else if (await this.findFiles(projectPath, /test.*\.lua$/)) { framework = 'luaunit'; testCommand = 'lua -l luaunit'; } else { throw new Error('No test framework detected. Create a spec/ directory for busted or test files for luaunit'); } } else if (framework === 'busted') { testCommand = 'busted'; } else if (framework === 'luaunit') { testCommand = 'lua -l luaunit'; } const { stdout, stderr } = await execAsync(testCommand, { cwd: projectPath, timeout: 60000 }); results.testResults = this.parseTestOutput(stdout, stderr, framework); results.framework = framework; return results; } catch (error) { results.success = false; results.error = error instanceof Error ? error.message : String(error); results.suggestion = 'Install busted (luarocks install busted) or luaunit for testing'; return results; } } async analyzeLuaDependencies(projectPath) { const results = { success: true, projectPath, dependencies: [], rockspecs: [], luaRocksInstalled: false, timestamp: new Date().toISOString() }; try { // Check if LuaRocks is available try { const { stdout } = await execAsync('luarocks --version', { cwd: projectPath }); results.luaRocksInstalled = true; results.luaRocksVersion = stdout.trim().split('\n')[0]; } catch { results.luaRocksInstalled = false; } // Find and parse rockspec files const rockspecFiles = await this.findFiles(projectPath, /\.rockspec$/); for (const rockspecFile of rockspecFiles) { try { const content = await fs.readFile(rockspecFile, 'utf-8'); const dependencies = this.parseRockspecDependencies(content); results.rockspecs.push({ file: rockspecFile, dependencies }); results.dependencies.push(...dependencies); } catch (error) { results.warnings = results.warnings || []; results.warnings.push(`Failed to parse ${rockspecFile}: ${error}`); } } // Check installed rocks if LuaRocks is available if (results.luaRocksInstalled) { try { const { stdout } = await execAsync('luarocks list', { cwd: projectPath }); results.installedRocks = this.parseInstalledRocks(stdout); } catch (error) { results.warnings = results.warnings || []; results.warnings.push(`Failed to list installed rocks: ${error}`); } } return results; } catch (error) { results.success = false; results.error = error instanceof Error ? error.message : String(error); return results; } } async profileLuaPerformance(scriptPath, profilingMode) { const results = { success: true, scriptPath, profilingMode, profile: {}, timestamp: new Date().toISOString() }; try { // Create a simple profiling script const profilerScript = this.generateLuaProfiler(scriptPath, profilingMode); const tempProfilerPath = path.join(path.dirname(scriptPath), 'temp_profiler.lua'); await fs.writeFile(tempProfilerPath, profilerScript); try { const { stdout, stderr } = await execAsync(`lua "${tempProfilerPath}"`, { timeout: 30000 }); results.profile = this.parseProfilerOutput(stdout, profilingMode); if (stderr) { results.warnings = stderr.split('\n').filter(line => line.trim()); } } finally { // Clean up temp file try { await fs.unlink(tempProfilerPath); } catch { } } return results; } catch (error) { results.success = false; results.error = error instanceof Error ? error.message : String(error); return results; } } async traceLuaExecution(scriptPath, traceLevel) { const results = { success: true, scriptPath, traceLevel, trace: [], timestamp: new Date().toISOString() }; try { // Create a debug tracer script const tracerScript = this.generateLuaTracer(scriptPath, traceLevel); const tempTracerPath = path.join(path.dirname(scriptPath), 'temp_tracer.lua'); await fs.writeFile(tempTracerPath, tracerScript); try { const { stdout, stderr } = await execAsync(`lua "${tempTracerPath}"`, { timeout: 30000 }); results.trace = this.parseTracerOutput(stdout, traceLevel); if (stderr) { results.warnings = stderr.split('\n').filter(line => line.trim()); } } finally { // Clean up temp file try { await fs.unlink(tempTracerPath); } catch { } } return results; } catch (error) { results.success = false; results.error = error instanceof Error ? error.message : String(error); return results; } } async analyzeLuaMemory(scriptPath, analysisType) { const results = { success: true, scriptPath, analysisType, memoryAnalysis: {}, timestamp: new Date().toISOString() }; try { // Create a memory analysis script const memoryScript = this.generateMemoryAnalyzer(scriptPath, analysisType); const tempMemoryPath = path.join(path.dirname(scriptPath), 'temp_memory.lua'); await fs.writeFile(tempMemoryPath, memoryScript); try { const { stdout, stderr } = await execAsync(`lua "${tempMemoryPath}"`, { timeout: 30000 }); results.memoryAnalysis = this.parseMemoryOutput(stdout, analysisType); if (stderr) { results.warnings = stderr.split('\n').filter(line => line.trim()); } } finally { // Clean up temp file try { await fs.unlink(tempMemoryPath); } catch { } } return results; } catch (error) { results.success = false; results.error = error instanceof Error ? error.message : String(error); return results; } } async inspectLuaWebFramework(projectPath) { const results = { success: true, projectPath, frameworks: [], webConfig: {}, timestamp: new Date().toISOString() }; try { // Detect web frameworks const frameworks = await this.detectLuaWebFrameworks(projectPath); results.frameworks = frameworks; // Framework-specific analysis for (const framework of frameworks) { switch (framework.name.toLowerCase()) { case 'openresty': case 'nginx': results.webConfig.nginx = await this.analyzeNginxConfig(projectPath); break; case 'lapis': results.webConfig.lapis = await this.analyzeLapisConfig(projectPath); break; case 'sailor': results.webConfig.sailor = await this.analyzeSailorConfig(projectPath); break; } } return results; } catch (error) { results.success = false; results.error = error instanceof Error ? error.message : String(error); return results; } } async analyzeLuaCModules(projectPath) { const results = { success: true, projectPath, cModules: [], ffiUsage: {}, timestamp: new Date().toISOString() }; try { // Find C extension files const cFiles = await this.findFiles(projectPath, /\.(c|cpp|cc)$/); const soFiles = await this.findFiles(projectPath, /\.so$/); results.cModules = [...cFiles, ...soFiles]; // Analyze FFI usage in Lua files const luaFiles = await this.findFiles(projectPath, /\.lua$/); const ffiUsage = []; for (const luaFile of luaFiles) { try { const content = await fs.readFile(luaFile, 'utf-8'); if (content.includes('require("ffi")') || content.includes('ffi.')) { const ffiCalls = content.match(/ffi\.\w+/g) || []; ffiUsage.push({ file: luaFile, ffiCalls: [...new Set(ffiCalls)] }); } } catch { } } results.ffiUsage = ffiUsage; return results; } catch (error) { results.success = false; results.error = error instanceof Error ? error.message : String(error); return results; } } async detectLuaFrameworks(projectPath) { const frameworks = []; try { // Check for common Lua framework indicators const files = await fs.readdir(projectPath); // OpenResty/nginx.conf if (files.some(f => f.includes('nginx.conf')) || existsSync(path.join(projectPath, 'conf', 'nginx.conf'))) { frameworks.push({ name: 'OpenResty', version: await this.getOpenRestyVersion(), configFiles: ['nginx.conf'] }); } // Lapis if (files.includes('config.lua') || files.includes('app.lua')) { const hasLapis = await this.checkForLapisImports(projectPath); if (hasLapis) { frameworks.push({ name: 'Lapis', version: await this.getLapisVersion(), configFiles: ['config.lua', 'app.lua'] }); } } // Sailor if (files.includes('sailor.lua') || existsSync(path.join(projectPath, 'conf', 'sailor.lua'))) { frameworks.push({ name: 'Sailor', version: await this.getSailorVersion(), configFiles: ['sailor.lua'] }); } // Kong (API Gateway) if (files.includes('kong.conf') || existsSync(path.join(projectPath, 'plugins'))) { frameworks.push({ name: 'Kong', version: await this.getKongVersion(), configFiles: ['kong.conf'] }); } // Lua Web Server (LWS) if (files.some(f => f.includes('lws'))) { frameworks.push({ name: 'LWS', version: 'unknown' }); } } catch (error) { // Return partial results even if detection fails } return { success: true, frameworks, frameworkCount: frameworks.length, timestamp: new Date().toISOString() }; } // Helper methods for parsing and analysis async findFiles(dir, pattern, maxDepth = 3) { const results = []; const search = async (currentDir, depth) => { if (depth > maxDepth) return; try { const items = await fs.readdir(currentDir); for (const item of items) { const fullPath = path.join(currentDir, item); const stat = await fs.stat(fullPath); if (stat.isFile() && pattern.test(item)) { results.push(fullPath); } else if (stat.isDirectory() && !item.startsWith('.')) { await search(fullPath, depth + 1); } } } catch { } }; await search(dir, 0); return results; } getLuaCheckArgs(level) { switch (level) { case 'basic': return '--no-unused --no-redefined'; case 'strict': return '--std max --max-line-length 120'; default: return '--std lua51c'; // standard } } parseLuaCheckOutput(output) { const issues = []; const lines = output.split('\n'); for (const line of lines) { const match = line.match(/^(.+):(\d+):(\d+):\s+\(([WE])\d+\)\s+(.+)$/); if (match) { issues.push({ file: match[1], line: parseInt(match[2]), column: parseInt(match[3]), type: match[4] === 'W' ? 'warning' : 'error', message: match[5] }); } } return issues; } parseTestOutput(stdout, stderr, framework) { // Parse test results based on framework if (framework === 'busted') { return this.parseBustedOutput(stdout); } else if (framework === 'luaunit') { return this.parseLuaUnitOutput(stdout); } return { stdout, stderr, framework: 'unknown' }; } parseBustedOutput(output) { const result = { tests: 0, passed: 0, failed: 0, pending: 0, failures: [] }; // Parse busted output format const summaryMatch = output.match(/(\d+) success.+?(\d+) failure.+?(\d+) error.+?(\d+) pending/); if (summaryMatch) { result.passed = parseInt(summaryMatch[1]); result.failed = parseInt(summaryMatch[2]) + parseInt(summaryMatch[3]); result.pending = parseInt(summaryMatch[4]); result.tests = result.passed + result.failed + result.pending; } return result; } parseLuaUnitOutput(output) { return { output, framework: 'luaunit' }; } parseRockspecDependencies(content) { const dependencies = []; const depMatch = content.match(/dependencies\s*=\s*{([^}]+)}/); if (depMatch) { const deps = depMatch[1].split(','); for (const dep of deps) { const cleanDep = dep.trim().replace(/['"]/g, ''); if (cleanDep && !cleanDep.startsWith('lua')) { dependencies.push(cleanDep); } } } return dependencies; } parseInstalledRocks(output) { const rocks = []; const lines = output.split('\n'); for (const line of lines) { const match = line.match(/^(\w+)\s+(\S+)/); if (match) { rocks.push(`${match[1]} ${match[2]}`); } } return rocks; } generateLuaProfiler(scriptPath, mode) { return ` -- Generated Lua Profiler local start_time = os.clock() local memory_start = collectgarbage("count") -- Load and run the target script local success, err = pcall(function() dofile("${scriptPath}") end) local end_time = os.clock() local memory_end = collectgarbage("count") print("PROFILE_START") print("mode: ${mode}") print("execution_time: " .. (end_time - start_time)) print("memory_used: " .. (memory_end - memory_start)) print("success: " .. tostring(success)) if not success then print("error: " .. tostring(err)) end print("PROFILE_END") `; } generateLuaTracer(scriptPath, level) { const hookLevel = level === 'all' ? '"call", "return", "line"' : level === 'line' ? '"line"' : '"call", "return"'; return ` -- Generated Lua Tracer print("TRACE_START") print("level: ${level}") local function trace_hook(event, line) local info = debug.getinfo(2, "nSl") if info and info.source ~= "=[C]" then print("trace: " .. event .. " " .. (info.name or "?") .. " " .. (info.source or "?") .. ":" .. (line or "?")) end end debug.sethook(trace_hook, ${hookLevel}) local success, err = pcall(function() dofile("${scriptPath}") end) debug.sethook() -- Remove hook print("success: " .. tostring(success)) if not success then print("error: " .. tostring(err)) end print("TRACE_END") `; } generateMemoryAnalyzer(scriptPath, analysisType) { return ` -- Generated Memory Analyzer local function get_memory_info() collectgarbage("collect") return { used = collectgarbage("count"), objects = 0 -- Would need additional tools for object counting } end print("MEMORY_START") print("type: ${analysisType}") local before = get_memory_info() print("memory_before: " .. before.used) local success, err = pcall(function() dofile("${scriptPath}") end) local after = get_memory_info() print("memory_after: " .. after.used) print("memory_delta: " .. (after.used - before.used)) print("success: " .. tostring(success)) if not success then print("error: " .. tostring(err)) end print("MEMORY_END") `; } parseProfilerOutput(output, mode) { const profile = { mode }; const lines = output.split('\n'); for (const line of lines) { if (line.startsWith('execution_time: ')) { profile.executionTime = parseFloat(line.split(': ')[1]); } else if (line.startsWith('memory_used: ')) { profile.memoryUsed = parseFloat(line.split(': ')[1]); } } return profile; } parseTracerOutput(output, level) { const traces = []; const lines = output.split('\n'); for (const line of lines) { if (line.startsWith('trace: ')) { const parts = line.substring(7).split(' '); traces.push({ event: parts[0], function: parts[1], location: parts[2] }); } } return traces; } parseMemoryOutput(output, analysisType) { const analysis = { type: analysisType }; const lines = output.split('\n'); for (const line of lines) { if (line.startsWith('memory_before: ')) { analysis.before = parseFloat(line.split(': ')[1]); } else if (line.startsWith('memory_after: ')) { analysis.after = parseFloat(line.split(': ')[1]); } else if (line.startsWith('memory_delta: ')) { analysis.delta = parseFloat(line.split(': ')[1]); } } return analysis; } async analyzeLuaProjectStructure(projectPath) { const structure = {}; try { const hasSpecs = existsSync(path.join(projectPath, 'spec')); const hasTests = existsSync(path.join(projectPath, 'test')); const hasLib = existsSync(path.join(projectPath, 'lib')); const hasSrc = existsSync(path.join(projectPath, 'src')); structure.testDirectory = hasSpecs ? 'spec' : hasTests ? 'test' : null; structure.sourceDirectory = hasLib ? 'lib' : hasSrc ? 'src' : null; structure.hasRockspec = (await this.findFiles(projectPath, /\.rockspec$/)).length > 0; } catch { } return structure; } async detectLuaWebFrameworks(projectPath) { // This would be similar to detectLuaFrameworks but focused on web-specific detection return (await this.detectLuaFrameworks(projectPath)).frameworks.filter((f) => ['OpenResty', 'Lapis', 'Sailor', 'Kong'].includes(f.name)); } async analyzeNginxConfig(projectPath) { // Analyze nginx.conf for OpenResty return { analyzed: true, type: 'nginx' }; } async analyzeLapisConfig(projectPath) { // Analyze Lapis configuration return { analyzed: true, type: 'lapis' }; } async analyzeSailorConfig(projectPath) { // Analyze Sailor configuration return { analyzed: true, type: 'sailor' }; } async getOpenRestyVersion() { try { const { stdout } = await execAsync('openresty -v'); return stdout.trim(); } catch { return 'unknown'; } } async getLapisVersion() { try { const { stdout } = await execAsync('lapis --version'); return stdout.trim(); } catch { return 'unknown'; } } async getSailorVersion() { return 'unknown'; // Would need to check Sailor installation } async getKongVersion() { try { const { stdout } = await execAsync('kong version'); return stdout.trim(); } catch { return 'unknown'; } } async checkForLapisImports(projectPath) { try { const luaFiles = await this.findFiles(projectPath, /\.lua$/); for (const file of luaFiles) { const content = await fs.readFile(file, 'utf-8'); if (content.includes('require("lapis"')) { return true; } } } catch { } return false; } } //# sourceMappingURL=lua-debugging-handler.js.map