lamplighter-mcp
Version:
An intelligent context engine for AI-assisted software development
245 lines (244 loc) • 9.91 kB
JavaScript
;
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;