lamplighter-mcp
Version:
An intelligent context engine for AI-assisted software development
224 lines (193 loc) • 8.4 kB
text/typescript
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;
}
}