UNPKG

@opichi/smartcode

Version:

Universal code intelligence MCP server - analyze any codebase with TypeScript excellence and multi-language support

478 lines 18 kB
/** * Code Indexer - Builds and caches project "table of contents" * Provides AI with instant overview of all important code structures */ import * as fs from 'fs/promises'; import * as path from 'path'; import { TypeScriptAnalyzer } from './typescript.js'; // Debug logging function function debugLog(message, data) { if (!process.env.MCP_DEBUG) return; const timestamp = new Date().toISOString(); const logEntry = `[${timestamp}] INDEXER: ${message}${data ? ': ' + JSON.stringify(data, null, 2) : ''}\n`; try { require('fs').appendFileSync(path.join(process.cwd(), '.mcp-debug.log'), logEntry); } catch (error) { // Fallback to stderr if file logging fails console.error(`[${timestamp}] INDEXER: ${message}`, data || ''); } } export class CodeIndexer { projectRoot; cacheDir; indexFile; isProcessing = false; requestQueue = []; constructor(projectRoot) { this.projectRoot = projectRoot; // Use stable cache directory name for consistent caching this.cacheDir = path.join(projectRoot, '.mcp-cache'); this.indexFile = path.join(this.cacheDir, 'code-index.json'); } /** * Get the code index, building it if necessary */ async getIndex() { debugLog('getIndex called'); return this.withLock(async () => { debugLog('Inside withLock'); try { debugLog('Checking for cached index'); // Check if cache exists and is fresh const cached = await this.loadCachedIndex(); debugLog('Cached index loaded', { hasCached: !!cached }); if (cached && await this.isCacheFresh(cached)) { debugLog('Using cached index'); return cached; } debugLog('Cache is stale or missing, building fresh index'); } catch (error) { debugLog('Error loading cache', { error: error instanceof Error ? error.message : String(error) }); // Cache doesn't exist or is invalid, build new one } // Build fresh index debugLog('Starting buildIndex'); return await this.buildIndex(); }); } /** * Force rebuild the index */ async rebuildIndex() { return this.withLock(async () => { return await this.buildIndex(); }); } /** * Concurrency protection wrapper */ async withLock(operation) { if (this.isProcessing) { return new Promise((resolve, reject) => { this.requestQueue.push(async () => { try { resolve(await operation()); } catch (error) { reject(error); } }); }); } this.isProcessing = true; try { return await operation(); } finally { this.isProcessing = false; this.processQueue(); } } /** * Process queued requests */ processQueue() { if (this.requestQueue.length > 0) { const nextRequest = this.requestQueue.shift(); debugLog('Processing next request in queue'); nextRequest(); } } /** * Get lightweight project overview */ async getProjectOverview() { debugLog('getProjectOverview called'); const index = await this.getIndex(); debugLog('Index retrieved for overview'); // Get directory structure (top level) const sourceFiles = await this.findSourceFiles(); const directories = [...new Set(sourceFiles .map(file => path.relative(this.projectRoot, path.dirname(file))) .filter(dir => dir && !dir.startsWith('.')) .map(dir => dir.split('/')[0]))].sort(); // Identify key files const keyFiles = sourceFiles .map(file => path.relative(this.projectRoot, file)) .filter(file => { const basename = path.basename(file).toLowerCase(); return basename.includes('app') || basename.includes('main') || basename.includes('index') || basename.includes('route') || basename.includes('config'); }) .slice(0, 5); // Identify entry points const entryPoints = sourceFiles .map(file => path.relative(this.projectRoot, file)) .filter(file => { const basename = path.basename(file).toLowerCase(); return basename.startsWith('main') || basename.startsWith('index') || basename.startsWith('app') || basename.includes('server'); }); const exportedFunctions = index.functions.filter(f => f.isExported).length; const exportedTypes = index.types.filter(t => t.isExported).length; return { summary: `Project with ${exportedFunctions} exported functions, ${exportedTypes} types, ${index.components.length} components, ${index.routes.length} API routes`, structure: directories, keyFiles, entryPoints, stats: { functions: index.functions.length, types: index.types.length, components: index.components.length, routes: index.routes.length, constants: index.constants.length } }; } /** * Get functions in a specific file */ async getFunctionsByFile(filePath) { const index = await this.getIndex(); return index.functions.filter(func => func.file === filePath || func.file.endsWith(filePath)); } /** * Get types matching a pattern */ async getTypesByPattern(pattern) { const index = await this.getIndex(); const regex = new RegExp(pattern, 'i'); return index.types.filter(type => regex.test(type.name)); } /** * Search functions by name or documentation */ async searchFunctions(query) { const index = await this.getIndex(); const lowerQuery = query.toLowerCase(); return index.functions.filter(func => func.name.toLowerCase().includes(lowerQuery) || (func.documentation && func.documentation.toLowerCase().includes(lowerQuery)) || func.signature.toLowerCase().includes(lowerQuery)).slice(0, 20); // Limit results } /** * Get components by file or name pattern */ async getComponentsByPattern(pattern) { const index = await this.getIndex(); const regex = new RegExp(pattern, 'i'); return index.components.filter(comp => regex.test(comp.name) || regex.test(comp.file)); } /** * Get routes by method or path pattern */ async getRoutesByPattern(pattern) { const index = await this.getIndex(); const regex = new RegExp(pattern, 'i'); return index.routes.filter(route => regex.test(route.method) || regex.test(route.path) || regex.test(route.handler)); } /** * Load cached index from disk */ async loadCachedIndex() { try { const data = await fs.readFile(this.indexFile, 'utf-8'); return JSON.parse(data); } catch (error) { return null; } } /** * Check if cached index is still fresh */ async isCacheFresh(cached) { try { const cacheTime = new Date(cached.lastUpdated); // Get all source files and check their timestamps const analyzer = new TypeScriptAnalyzer(this.projectRoot); const sourceFiles = await this.findSourceFiles(); for (const file of sourceFiles) { const stats = await fs.stat(file); if (stats.mtime > cacheTime) { return false; // File is newer than cache } } return true; // Cache is fresh } catch (error) { return false; // Assume stale on error } } /** * Build complete code index */ async buildIndex() { debugLog('buildIndex starting'); console.error('🔍 Building code index...'); debugLog('Creating TypeScript analyzer'); const analyzer = new TypeScriptAnalyzer(this.projectRoot); debugLog('Finding source files'); const sourceFiles = await this.findSourceFiles(); debugLog('Source files found', { count: sourceFiles.length, files: sourceFiles.slice(0, 10) }); const index = { lastUpdated: new Date().toISOString(), projectPath: this.projectRoot, routes: [], functions: [], types: [], components: [], constants: [], exports: [] }; debugLog('Index structure initialized'); // Analyze each source file debugLog('Starting file analysis loop'); for (let i = 0; i < sourceFiles.length; i++) { const file = sourceFiles[i]; debugLog(`Analyzing file ${i + 1}/${sourceFiles.length}`, { file }); try { const relativePath = path.relative(this.projectRoot, file); debugLog('Calling analyzer.analyzeFile', { relativePath }); const structure = await analyzer.analyzeFile(relativePath); debugLog('File analysis completed', { relativePath, hasStructure: !!structure }); if (structure) { debugLog('Extracting from structure'); this.extractFromStructure(structure, index); debugLog('Structure extraction completed'); } } catch (error) { // Skip files that can't be analyzed debugLog('File analysis error', { file, error: error instanceof Error ? error.message : String(error) }); console.error(`⚠️ Could not analyze ${file}: ${error}`); } } // Extract routes from code patterns await this.extractRoutes(analyzer, index); // Save to cache await this.saveIndex(index); console.error(`✅ Index built: ${index.functions.length} functions, ${index.types.length} types, ${index.routes.length} routes`); return index; } /** * Extract information from file structure */ extractFromStructure(structure, index) { // Extract functions structure.functions.forEach(func => { index.functions.push({ name: func.name, file: structure.file, line: func.line, signature: this.buildFunctionSignature(func), isExported: func.isExported, isAsync: func.isAsync, documentation: func.documentation }); }); // Extract types (interfaces, types, classes) structure.interfaces.forEach(int => { index.types.push({ name: int.name, file: structure.file, line: int.line, kind: 'interface', isExported: int.isExported, documentation: int.documentation, properties: int.properties.map(p => p.name) }); }); structure.types.forEach(type => { index.types.push({ name: type.name, file: structure.file, line: type.line, kind: 'type', isExported: type.isExported, documentation: type.documentation }); }); structure.classes.forEach(cls => { index.types.push({ name: cls.name, file: structure.file, line: cls.line, kind: 'class', isExported: cls.isExported, documentation: cls.documentation, properties: cls.properties.map(p => p.name) }); }); // Extract constants and variables structure.variables.forEach(variable => { if (variable.isConst || variable.isExported) { index.constants.push({ name: variable.name, file: structure.file, line: variable.line, value: variable.value, type: variable.type, isExported: variable.isExported }); } }); // Check for React components if (structure.file.match(/\.(tsx|jsx)$/)) { structure.functions.forEach(func => { if (func.name.match(/^[A-Z]/) && func.isExported) { // Likely a React component index.components.push({ name: func.name, file: structure.file, line: func.line, isExported: func.isExported, documentation: func.documentation }); } }); } // Extract all exports structure.exports.forEach(exp => { index.exports.push({ name: exp.name, file: structure.file, line: 0, // TODO: Get actual line from exports kind: exp.kind, isDefault: exp.isDefault }); }); } /** * Extract API routes from code patterns */ async extractRoutes(analyzer, index) { const routePatterns = [ { pattern: /app\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/, framework: 'express' }, { pattern: /router\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/, framework: 'express' }, { pattern: /export\s+async\s+function\s+(GET|POST|PUT|DELETE|PATCH)/, framework: 'nextjs' }, { pattern: /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`]([^'"`]+)['"`]/, framework: 'decorator' } ]; for (const { pattern, framework } of routePatterns) { try { const matches = await analyzer.searchCode(pattern.source, { regex: true, fileTypes: ['.ts', '.js', '.tsx', '.jsx'] }); matches.forEach(match => { const regexMatch = match.content.match(pattern); if (regexMatch) { const method = regexMatch[1]?.toUpperCase() || 'GET'; const routePath = regexMatch[2] || ''; index.routes.push({ method, path: routePath, handler: this.extractHandlerName(match.content), file: match.file, line: match.line }); } }); } catch (error) { // Continue if pattern search fails } } } /** * Extract handler function name from route code */ extractHandlerName(code) { // Try to extract function name from various patterns const patterns = [ /function\s+(\w+)/, /const\s+(\w+)\s*=/, /(\w+)\s*=>/, /\.(\w+)\s*\(/ ]; for (const pattern of patterns) { const match = code.match(pattern); if (match) { return match[1]; } } return 'anonymous'; } /** * Build function signature string */ buildFunctionSignature(func) { const params = func.parameters?.map((p) => `${p.name}${p.isOptional ? '?' : ''}${p.type ? `: ${p.type}` : ''}`).join(', ') || ''; const returnType = func.returnType ? `: ${func.returnType}` : ''; const asyncPrefix = func.isAsync ? 'async ' : ''; return `${asyncPrefix}function ${func.name}(${params})${returnType}`; } /** * Find all source files to analyze */ async findSourceFiles() { const files = []; const extensions = ['.ts', '.tsx', '.js', '.jsx']; const walkDir = async (dir) => { if (dir.includes('node_modules') || dir.includes('.git') || dir.includes('.mcp-cache')) return; try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { await walkDir(fullPath); } else if (extensions.some(ext => entry.name.endsWith(ext))) { files.push(fullPath); } } } catch (error) { // Ignore permission errors } }; await walkDir(this.projectRoot); return files; } /** * Save index to cache */ async saveIndex(index) { try { await fs.mkdir(this.cacheDir, { recursive: true }); await fs.writeFile(this.indexFile, JSON.stringify(index, null, 2)); // Add .gitignore to cache directory const gitignorePath = path.join(this.cacheDir, '.gitignore'); await fs.writeFile(gitignorePath, '*\n!.gitignore\n'); } catch (error) { console.error('Failed to save index cache:', error); } } } //# sourceMappingURL=indexer.js.map