UNPKG

lamplighter-mcp

Version:

An intelligent context engine for AI-assisted software development

245 lines (244 loc) 9.91 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CodebaseAnalyzer = void 0; const fs = __importStar(require("fs/promises")); const path = __importStar(require("path")); const dotenv_1 = __importDefault(require("dotenv")); // Load environment variables dotenv_1.default.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'], }; // Directories to ignore const IGNORE_DIRS = new Set([ 'node_modules', 'dist', 'build', 'target', '.git', '__pycache__', 'venv', '.env' ]); class CodebaseAnalyzer { contextDir; summaryPath; 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) { const content = []; 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. */ async detectTechStack(projectRoot) { const tech = new Set(); // Use a Set to prevent duplicates inherently const techReasons = {}; // Store reason for first detection const addTech = (name, reason) => { 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 */ async analyzeDirectory(dir, depth) { const indent = ' '.repeat(depth); const result = []; 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 */ async findFilesByExtension(dir, ext) { const result = []; 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; } } exports.CodebaseAnalyzer = CodebaseAnalyzer;