UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

431 lines (368 loc) 13.7 kB
import { ConfigManager } from '../../config/config-manager.js'; import { CodeContext } from './types.js'; import { promises as fs } from 'fs'; import path from 'path'; export class CodeMemory { private configManager: ConfigManager; private codeContextPath: string; private codeContexts: Map<string, CodeContext>; constructor(configManager: ConfigManager) { this.configManager = configManager; this.codeContextPath = ''; this.codeContexts = new Map(); } async initialize(): Promise<void> { const storageManager = this.configManager.getStorageManager(); const location = await storageManager.getStorageLocation(); this.codeContextPath = path.join(location.data, 'memory', 'code-contexts.json'); // Ensure directory exists await fs.mkdir(path.dirname(this.codeContextPath), { recursive: true }); // Load existing code contexts await this.loadCodeContexts(); } async analyzeContext(options: { filePath: string; functionName?: string; includeRelated?: boolean; depth?: number; }): Promise<CodeContext> { const fullPath = path.resolve(options.filePath); const contextKey = options.functionName ? `${fullPath}:${options.functionName}` : fullPath; // Check if we have cached context const cached = this.codeContexts.get(contextKey); if (cached) { return cached; } // Analyze the code file const context = await this.performCodeAnalysis(options); // Cache the context this.codeContexts.set(contextKey, context); await this.saveCodeContexts(); return context; } async updateCodeContext(filePath: string, context: CodeContext): Promise<void> { this.codeContexts.set(filePath, context); await this.saveCodeContexts(); } async findRelatedCode(filePath: string): Promise<CodeContext[]> { const targetContext = this.codeContexts.get(filePath); if (!targetContext) return []; const related: { context: CodeContext; score: number }[] = []; for (const [path, context] of this.codeContexts) { if (path === filePath) continue; let score = 0; // Check if files import each other const hasImportRelation = context.dependencies.some(dep => dep.filePath === filePath ) || targetContext.dependencies.some(dep => dep.filePath === path.replace(/:[^:]*$/, '') // Remove function name if present ); if (hasImportRelation) { score += 0.5; } // Check for similar usage patterns const sharedPatterns = targetContext.usagePatterns.filter(pattern => context.usagePatterns.includes(pattern) ); score += sharedPatterns.length * 0.2; // Check for related code references const relatedCodeMatch = targetContext.relatedCode.some(related => related.file === context.filePath ); if (relatedCodeMatch) { score += 0.3; } if (score > 0.2) { related.push({ context, score }); } } return related .sort((a, b) => b.score - a.score) .slice(0, 5) .map(item => item.context); } async getCodeStats(): Promise<{ totalContexts: number; fileTypes: Record<string, number>; topPatterns: Array<{ pattern: string; count: number }>; dependencyGraph: Record<string, string[]>; }> { const contexts = Array.from(this.codeContexts.values()); const fileTypes: Record<string, number> = {}; const patternCounts: Record<string, number> = {}; const dependencyGraph: Record<string, string[]> = {}; contexts.forEach(context => { const ext = path.extname(context.filePath); fileTypes[ext] = (fileTypes[ext] || 0) + 1; context.usagePatterns.forEach(pattern => { patternCounts[pattern] = (patternCounts[pattern] || 0) + 1; }); const deps = context.dependencies .filter(dep => dep.filePath) .map(dep => dep.filePath!); if (deps.length > 0) { dependencyGraph[context.filePath] = deps; } }); const topPatterns = Object.entries(patternCounts) .sort(([, a], [, b]) => b - a) .slice(0, 10) .map(([pattern, count]) => ({ pattern, count })); return { totalContexts: contexts.length, fileTypes, topPatterns, dependencyGraph, }; } private async performCodeAnalysis(options: { filePath: string; functionName?: string; includeRelated?: boolean; depth?: number; }): Promise<CodeContext> { try { const content = await fs.readFile(options.filePath, 'utf-8'); const ext = path.extname(options.filePath); let context: CodeContext; switch (ext) { case '.ts': case '.tsx': case '.js': case '.jsx': context = await this.analyzeJavaScriptTypeScript(content, options); break; case '.py': context = await this.analyzePython(content, options); break; default: context = await this.analyzeGeneric(content, options); } // Add related code if requested if (options.includeRelated) { context.relatedCode = await this.findRelatedCodeReferences(context, options.depth || 2); } return context; } catch (error) { // Return minimal context if analysis fails return { filePath: options.filePath, functionName: options.functionName, summary: `Failed to analyze: ${(error as Error).message}`, dependencies: [], usagePatterns: [], relatedCode: [], recommendations: ['File could not be analyzed - check file permissions and format'], }; } } private async analyzeJavaScriptTypeScript( content: string, options: { filePath: string; functionName?: string } ): Promise<CodeContext> { const lines = content.split('\n'); const dependencies: CodeContext['dependencies'] = []; const usagePatterns: string[] = []; const recommendations: string[] = []; // Extract imports const importRegex = /^import\s+(?:.*\s+from\s+)?['"]([^'"]+)['"];?/gm; let match; while ((match = importRegex.exec(content)) !== null) { dependencies.push({ name: match[1], type: 'import', description: `Imported module: ${match[1]}`, filePath: this.resolveImportPath(match[1], options.filePath), }); } // Extract function/class definitions const functionRegex = /^(?:export\s+)?(?:async\s+)?function\s+(\w+)/gm; while ((match = functionRegex.exec(content)) !== null) { dependencies.push({ name: match[1], type: 'function', description: `Function: ${match[1]}`, }); } const classRegex = /^(?:export\s+)?class\s+(\w+)/gm; while ((match = classRegex.exec(content)) !== null) { dependencies.push({ name: match[1], type: 'class', description: `Class: ${match[1]}`, }); } // Detect usage patterns if (content.includes('React')) usagePatterns.push('React component'); if (content.includes('express')) usagePatterns.push('Express.js server'); if (content.includes('async/await')) usagePatterns.push('Async/await pattern'); if (content.includes('jest') || content.includes('describe(')) usagePatterns.push('Jest testing'); if (content.includes('useState') || content.includes('useEffect')) usagePatterns.push('React hooks'); if (content.includes('try/catch')) usagePatterns.push('Error handling'); // Generate recommendations if (!content.includes('use strict') && content.includes('function')) { recommendations.push('Consider using strict mode'); } if (content.includes('any') && options.filePath.endsWith('.ts')) { recommendations.push('Avoid using "any" type in TypeScript'); } if (content.includes('console.log')) { recommendations.push('Remove console.log statements before production'); } let summary = `${path.basename(options.filePath)} - `; if (usagePatterns.length > 0) { summary += usagePatterns[0]; } else { summary += 'JavaScript/TypeScript file'; } if (options.functionName) { const functionMatch = content.match(new RegExp(`function\\s+${options.functionName}\\s*\\(`)); if (functionMatch) { summary += ` (analyzing function: ${options.functionName})`; } } return { filePath: options.filePath, functionName: options.functionName, summary, dependencies, usagePatterns, relatedCode: [], recommendations, }; } private async analyzePython( content: string, options: { filePath: string; functionName?: string } ): Promise<CodeContext> { const dependencies: CodeContext['dependencies'] = []; const usagePatterns: string[] = []; const recommendations: string[] = []; // Extract imports const importRegex = /^(?:from\s+(\S+)\s+)?import\s+(.+)$/gm; let match; while ((match = importRegex.exec(content)) !== null) { const module = match[1] || match[2].split(',')[0].trim(); dependencies.push({ name: module, type: 'import', description: `Imported: ${module}`, }); } // Extract function definitions const functionRegex = /^def\s+(\w+)\s*\(/gm; while ((match = functionRegex.exec(content)) !== null) { dependencies.push({ name: match[1], type: 'function', description: `Function: ${match[1]}`, }); } // Extract class definitions const classRegex = /^class\s+(\w+)/gm; while ((match = classRegex.exec(content)) !== null) { dependencies.push({ name: match[1], type: 'class', description: `Class: ${match[1]}`, }); } // Detect patterns if (content.includes('flask')) usagePatterns.push('Flask web application'); if (content.includes('django')) usagePatterns.push('Django web application'); if (content.includes('pandas')) usagePatterns.push('Data analysis with pandas'); if (content.includes('numpy')) usagePatterns.push('Numerical computing'); if (content.includes('async def')) usagePatterns.push('Async/await pattern'); if (content.includes('unittest') || content.includes('pytest')) usagePatterns.push('Unit testing'); return { filePath: options.filePath, functionName: options.functionName, summary: `${path.basename(options.filePath)} - Python ${usagePatterns[0] || 'script'}`, dependencies, usagePatterns, relatedCode: [], recommendations, }; } private async analyzeGeneric( content: string, options: { filePath: string; functionName?: string } ): Promise<CodeContext> { const ext = path.extname(options.filePath); const dependencies: CodeContext['dependencies'] = []; const usagePatterns: string[] = []; // Basic analysis for any file type const lineCount = content.split('\n').length; const wordCount = content.split(/\s+/).length; if (ext === '.md') { usagePatterns.push('Markdown documentation'); } else if (ext === '.json') { usagePatterns.push('JSON configuration'); } else if (ext === '.yml' || ext === '.yaml') { usagePatterns.push('YAML configuration'); } return { filePath: options.filePath, functionName: options.functionName, summary: `${path.basename(options.filePath)} - ${lineCount} lines, ${wordCount} words`, dependencies, usagePatterns, relatedCode: [], recommendations: [], }; } private async findRelatedCodeReferences( context: CodeContext, depth: number ): Promise<CodeContext['relatedCode']> { const related: CodeContext['relatedCode'] = []; // Find files that import this file or are imported by this file for (const dep of context.dependencies) { if (dep.filePath && dep.type === 'import') { try { await fs.access(dep.filePath); related.push({ file: dep.filePath, description: `Imported dependency: ${dep.name}`, relationship: 'imports', }); } catch { // File doesn't exist or can't be accessed } } } // Look for files that might import this file if (depth > 1) { // This would require scanning other files in the project // For now, we'll keep it simple } return related.slice(0, 10); // Limit to 10 related files } private resolveImportPath(importPath: string, currentFile: string): string | undefined { if (importPath.startsWith('.')) { // Relative import const currentDir = path.dirname(currentFile); return path.resolve(currentDir, importPath); } // For node_modules or absolute imports, we'd need more sophisticated resolution return undefined; } private async loadCodeContexts(): Promise<void> { try { const data = await fs.readFile(this.codeContextPath, 'utf-8'); const contextsArray: CodeContext[] = JSON.parse(data); this.codeContexts.clear(); for (const context of contextsArray) { const key = context.functionName ? `${context.filePath}:${context.functionName}` : context.filePath; this.codeContexts.set(key, context); } } catch (error) { // File doesn't exist or is invalid, start with empty contexts this.codeContexts.clear(); } } private async saveCodeContexts(): Promise<void> { const contextsArray = Array.from(this.codeContexts.values()); await fs.writeFile(this.codeContextPath, JSON.stringify(contextsArray, null, 2)); } }