UNPKG

lamplighter-mcp

Version:

An intelligent context engine for AI-assisted software development

224 lines (193 loc) 8.4 kB
import * as fs from 'fs/promises'; import * as path from 'path'; import dotenv from 'dotenv'; // Load environment variables dotenv.config(); const DEFAULT_CONTEXT_DIR = './lamplighter_context'; const SUMMARY_FILE_NAME = 'codebase_summary.md'; // Tech stack detection patterns const TECH_PATTERNS = { nodejs: ['package.json', 'node_modules'], typescript: ['tsconfig.json', '.ts', '.tsx'], python: ['requirements.txt', 'setup.py', '.py'], java: ['pom.xml', 'build.gradle', '.java'], rust: ['Cargo.toml', '.rs'], go: ['go.mod', '.go'], docker: ['Dockerfile', 'docker-compose.yml'], git: ['.git', '.gitignore'], } as const; // Directories to ignore const IGNORE_DIRS = new Set([ 'node_modules', 'dist', 'build', 'target', '.git', '__pycache__', 'venv', '.env' ]); export class CodebaseAnalyzer { private contextDir: string; private summaryPath: string; constructor() { this.contextDir = process.env.LAMPLIGHTER_CONTEXT_DIR || DEFAULT_CONTEXT_DIR; this.summaryPath = path.join(this.contextDir, SUMMARY_FILE_NAME); } /** * Analyzes the project structure and generates a summary */ async analyze(projectRoot: string): Promise<void> { const content: string[] = []; content.push('# Codebase Summary\n'); content.push(`*Generated at: ${new Date().toISOString()}*\n`); try { // Ensure context directory exists await fs.mkdir(this.contextDir, { recursive: true }); // Detect tech stack const techStack = await this.detectTechStack(projectRoot); content.push('## Tech Stack\n'); if (techStack.length > 0) { for (const tech of techStack) { content.push(tech); } } else { content.push('_(No specific technologies detected)_\n'); } content.push('\n'); // Analyze directory structure content.push('## Directory Structure\n'); content.push('```'); const dirStructure = await this.analyzeDirectory(projectRoot, 0); content.push(dirStructure || '_(Empty or unreadable)_'); content.push('```\n'); // Write the summary to file await fs.writeFile(this.summaryPath, content.join('\n'), 'utf-8'); console.log(`[CodebaseAnalyzer] Summary written to ${this.summaryPath}`); } catch (error) { console.error('[CodebaseAnalyzer] Error during analysis:', error); // Attempt to write an error state to the summary file content.push('\n## Analysis Error\n'); content.push('```'); content.push(`An error occurred during analysis: ${error instanceof Error ? error.message : String(error)}`); content.push('```'); try { await fs.writeFile(this.summaryPath, content.join('\n'), 'utf-8'); console.warn(`[CodebaseAnalyzer] Wrote error state to summary file: ${this.summaryPath}`); } catch (writeError) { console.error(`[CodebaseAnalyzer] Failed to write error state to summary file:`, writeError); } // Do not re-throw; allow the process to continue if possible, // but the summary file will indicate the failure. } } /** * Detects the primary technologies used in the project. */ private async detectTechStack(projectRoot: string): Promise<string[]> { const tech: Set<string> = new Set(); // Use a Set to prevent duplicates inherently const techReasons: Record<string, string> = {}; // Store reason for first detection const addTech = (name: string, reason: string) => { if (!tech.has(name)) { tech.add(name); techReasons[name] = reason; } }; const checks = [ { name: 'nodejs', file: 'package.json' }, { name: 'typescript', file: 'tsconfig.json' }, { name: 'python', file: 'requirements.txt' }, { name: 'python', file: 'setup.py' }, { name: 'java', file: 'pom.xml' }, { name: 'java', file: 'build.gradle' }, { name: 'rust', file: 'Cargo.toml' }, { name: 'golang', file: 'go.mod' }, { name: 'git', file: '.git' }, // Check for .git directory { name: 'git', file: '.gitignore' }, ]; for (const check of checks) { try { await fs.access(path.join(projectRoot, check.file)); addTech(check.name, check.file); } catch (error) { /* ignore */ } } // Fallback: Check for file extensions ONLY if the tech hasn't been added by primary file const fileExtensionChecks = [ { name: 'typescript', ext: '.ts' }, { name: 'typescript', ext: '.tsx' }, { name: 'python', ext: '.py' }, // Add more extension checks if needed ]; for (const extCheck of fileExtensionChecks) { if (!tech.has(extCheck.name)) { // Only check if tech not already detected try { const files = await this.findFilesByExtension(projectRoot, extCheck.ext); if (files.length > 0) { addTech(extCheck.name, `${extCheck.ext} files`); } } catch (error) { // Log this error as it might indicate deeper issues than just missing tech console.error(`[CodebaseAnalyzer] Error during fallback extension check for ${extCheck.ext}:`, error); } } } // Format output return Array.from(tech).map(name => `- **${name}** (detected from: ${techReasons[name]})`); } /** * Recursively analyzes directory structure */ private async analyzeDirectory(dir: string, depth: number): Promise<string> { const indent = ' '.repeat(depth); const result: string[] = []; try { const entries = await fs.readdir(dir, { withFileTypes: true }); const sortedEntries = entries.sort((a, b) => { // Directories first, then files if (a.isDirectory() && !b.isDirectory()) return -1; if (!a.isDirectory() && b.isDirectory()) return 1; return a.name.localeCompare(b.name); }); for (const entry of sortedEntries) { const fullPath = path.join(dir, entry.name); // Skip ignored directories if (entry.isDirectory() && IGNORE_DIRS.has(entry.name)) { continue; } if (entry.isDirectory()) { result.push(`${indent}📁 ${entry.name}/`); const subDirContent = await this.analyzeDirectory(fullPath, depth + 1); if (subDirContent) { result.push(subDirContent); } } else { result.push(`${indent}📄 ${entry.name}`); } } } catch (error) { console.error(`[CodebaseAnalyzer] Error analyzing directory ${dir}:`, error); } return result.join('\n'); } /** * Helper to find files with specific extension */ private async findFilesByExtension(dir: string, ext: string): Promise<string[]> { const result: string[] = []; try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory() && !IGNORE_DIRS.has(entry.name)) { const subDirFiles = await this.findFilesByExtension(fullPath, ext); result.push(...subDirFiles); } else if (entry.isFile() && entry.name.endsWith(ext)) { result.push(fullPath); } } } catch (error) { console.error(`[CodebaseAnalyzer] Error finding files with extension ${ext}:`, error); } return result; } }