@opichi/smartcode
Version:
Universal code intelligence MCP server - analyze any codebase with TypeScript excellence and multi-language support
478 lines • 18 kB
JavaScript
/**
* Code Indexer - Builds and caches project "table of contents"
* Provides AI with instant overview of all important code structures
*/
import * as fs from 'fs/promises';
import * as path from 'path';
import { TypeScriptAnalyzer } from './typescript.js';
// Debug logging function
function debugLog(message, data) {
if (!process.env.MCP_DEBUG)
return;
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] INDEXER: ${message}${data ? ': ' + JSON.stringify(data, null, 2) : ''}\n`;
try {
require('fs').appendFileSync(path.join(process.cwd(), '.mcp-debug.log'), logEntry);
}
catch (error) {
// Fallback to stderr if file logging fails
console.error(`[${timestamp}] INDEXER: ${message}`, data || '');
}
}
export class CodeIndexer {
projectRoot;
cacheDir;
indexFile;
isProcessing = false;
requestQueue = [];
constructor(projectRoot) {
this.projectRoot = projectRoot;
// Use stable cache directory name for consistent caching
this.cacheDir = path.join(projectRoot, '.mcp-cache');
this.indexFile = path.join(this.cacheDir, 'code-index.json');
}
/**
* Get the code index, building it if necessary
*/
async getIndex() {
debugLog('getIndex called');
return this.withLock(async () => {
debugLog('Inside withLock');
try {
debugLog('Checking for cached index');
// Check if cache exists and is fresh
const cached = await this.loadCachedIndex();
debugLog('Cached index loaded', { hasCached: !!cached });
if (cached && await this.isCacheFresh(cached)) {
debugLog('Using cached index');
return cached;
}
debugLog('Cache is stale or missing, building fresh index');
}
catch (error) {
debugLog('Error loading cache', { error: error instanceof Error ? error.message : String(error) });
// Cache doesn't exist or is invalid, build new one
}
// Build fresh index
debugLog('Starting buildIndex');
return await this.buildIndex();
});
}
/**
* Force rebuild the index
*/
async rebuildIndex() {
return this.withLock(async () => {
return await this.buildIndex();
});
}
/**
* Concurrency protection wrapper
*/
async withLock(operation) {
if (this.isProcessing) {
return new Promise((resolve, reject) => {
this.requestQueue.push(async () => {
try {
resolve(await operation());
}
catch (error) {
reject(error);
}
});
});
}
this.isProcessing = true;
try {
return await operation();
}
finally {
this.isProcessing = false;
this.processQueue();
}
}
/**
* Process queued requests
*/
processQueue() {
if (this.requestQueue.length > 0) {
const nextRequest = this.requestQueue.shift();
debugLog('Processing next request in queue');
nextRequest();
}
}
/**
* Get lightweight project overview
*/
async getProjectOverview() {
debugLog('getProjectOverview called');
const index = await this.getIndex();
debugLog('Index retrieved for overview');
// Get directory structure (top level)
const sourceFiles = await this.findSourceFiles();
const directories = [...new Set(sourceFiles
.map(file => path.relative(this.projectRoot, path.dirname(file)))
.filter(dir => dir && !dir.startsWith('.'))
.map(dir => dir.split('/')[0]))].sort();
// Identify key files
const keyFiles = sourceFiles
.map(file => path.relative(this.projectRoot, file))
.filter(file => {
const basename = path.basename(file).toLowerCase();
return basename.includes('app') ||
basename.includes('main') ||
basename.includes('index') ||
basename.includes('route') ||
basename.includes('config');
})
.slice(0, 5);
// Identify entry points
const entryPoints = sourceFiles
.map(file => path.relative(this.projectRoot, file))
.filter(file => {
const basename = path.basename(file).toLowerCase();
return basename.startsWith('main') ||
basename.startsWith('index') ||
basename.startsWith('app') ||
basename.includes('server');
});
const exportedFunctions = index.functions.filter(f => f.isExported).length;
const exportedTypes = index.types.filter(t => t.isExported).length;
return {
summary: `Project with ${exportedFunctions} exported functions, ${exportedTypes} types, ${index.components.length} components, ${index.routes.length} API routes`,
structure: directories,
keyFiles,
entryPoints,
stats: {
functions: index.functions.length,
types: index.types.length,
components: index.components.length,
routes: index.routes.length,
constants: index.constants.length
}
};
}
/**
* Get functions in a specific file
*/
async getFunctionsByFile(filePath) {
const index = await this.getIndex();
return index.functions.filter(func => func.file === filePath || func.file.endsWith(filePath));
}
/**
* Get types matching a pattern
*/
async getTypesByPattern(pattern) {
const index = await this.getIndex();
const regex = new RegExp(pattern, 'i');
return index.types.filter(type => regex.test(type.name));
}
/**
* Search functions by name or documentation
*/
async searchFunctions(query) {
const index = await this.getIndex();
const lowerQuery = query.toLowerCase();
return index.functions.filter(func => func.name.toLowerCase().includes(lowerQuery) ||
(func.documentation && func.documentation.toLowerCase().includes(lowerQuery)) ||
func.signature.toLowerCase().includes(lowerQuery)).slice(0, 20); // Limit results
}
/**
* Get components by file or name pattern
*/
async getComponentsByPattern(pattern) {
const index = await this.getIndex();
const regex = new RegExp(pattern, 'i');
return index.components.filter(comp => regex.test(comp.name) || regex.test(comp.file));
}
/**
* Get routes by method or path pattern
*/
async getRoutesByPattern(pattern) {
const index = await this.getIndex();
const regex = new RegExp(pattern, 'i');
return index.routes.filter(route => regex.test(route.method) ||
regex.test(route.path) ||
regex.test(route.handler));
}
/**
* Load cached index from disk
*/
async loadCachedIndex() {
try {
const data = await fs.readFile(this.indexFile, 'utf-8');
return JSON.parse(data);
}
catch (error) {
return null;
}
}
/**
* Check if cached index is still fresh
*/
async isCacheFresh(cached) {
try {
const cacheTime = new Date(cached.lastUpdated);
// Get all source files and check their timestamps
const analyzer = new TypeScriptAnalyzer(this.projectRoot);
const sourceFiles = await this.findSourceFiles();
for (const file of sourceFiles) {
const stats = await fs.stat(file);
if (stats.mtime > cacheTime) {
return false; // File is newer than cache
}
}
return true; // Cache is fresh
}
catch (error) {
return false; // Assume stale on error
}
}
/**
* Build complete code index
*/
async buildIndex() {
debugLog('buildIndex starting');
console.error('🔍 Building code index...');
debugLog('Creating TypeScript analyzer');
const analyzer = new TypeScriptAnalyzer(this.projectRoot);
debugLog('Finding source files');
const sourceFiles = await this.findSourceFiles();
debugLog('Source files found', { count: sourceFiles.length, files: sourceFiles.slice(0, 10) });
const index = {
lastUpdated: new Date().toISOString(),
projectPath: this.projectRoot,
routes: [],
functions: [],
types: [],
components: [],
constants: [],
exports: []
};
debugLog('Index structure initialized');
// Analyze each source file
debugLog('Starting file analysis loop');
for (let i = 0; i < sourceFiles.length; i++) {
const file = sourceFiles[i];
debugLog(`Analyzing file ${i + 1}/${sourceFiles.length}`, { file });
try {
const relativePath = path.relative(this.projectRoot, file);
debugLog('Calling analyzer.analyzeFile', { relativePath });
const structure = await analyzer.analyzeFile(relativePath);
debugLog('File analysis completed', { relativePath, hasStructure: !!structure });
if (structure) {
debugLog('Extracting from structure');
this.extractFromStructure(structure, index);
debugLog('Structure extraction completed');
}
}
catch (error) {
// Skip files that can't be analyzed
debugLog('File analysis error', { file, error: error instanceof Error ? error.message : String(error) });
console.error(`⚠️ Could not analyze ${file}: ${error}`);
}
}
// Extract routes from code patterns
await this.extractRoutes(analyzer, index);
// Save to cache
await this.saveIndex(index);
console.error(`✅ Index built: ${index.functions.length} functions, ${index.types.length} types, ${index.routes.length} routes`);
return index;
}
/**
* Extract information from file structure
*/
extractFromStructure(structure, index) {
// Extract functions
structure.functions.forEach(func => {
index.functions.push({
name: func.name,
file: structure.file,
line: func.line,
signature: this.buildFunctionSignature(func),
isExported: func.isExported,
isAsync: func.isAsync,
documentation: func.documentation
});
});
// Extract types (interfaces, types, classes)
structure.interfaces.forEach(int => {
index.types.push({
name: int.name,
file: structure.file,
line: int.line,
kind: 'interface',
isExported: int.isExported,
documentation: int.documentation,
properties: int.properties.map(p => p.name)
});
});
structure.types.forEach(type => {
index.types.push({
name: type.name,
file: structure.file,
line: type.line,
kind: 'type',
isExported: type.isExported,
documentation: type.documentation
});
});
structure.classes.forEach(cls => {
index.types.push({
name: cls.name,
file: structure.file,
line: cls.line,
kind: 'class',
isExported: cls.isExported,
documentation: cls.documentation,
properties: cls.properties.map(p => p.name)
});
});
// Extract constants and variables
structure.variables.forEach(variable => {
if (variable.isConst || variable.isExported) {
index.constants.push({
name: variable.name,
file: structure.file,
line: variable.line,
value: variable.value,
type: variable.type,
isExported: variable.isExported
});
}
});
// Check for React components
if (structure.file.match(/\.(tsx|jsx)$/)) {
structure.functions.forEach(func => {
if (func.name.match(/^[A-Z]/) && func.isExported) {
// Likely a React component
index.components.push({
name: func.name,
file: structure.file,
line: func.line,
isExported: func.isExported,
documentation: func.documentation
});
}
});
}
// Extract all exports
structure.exports.forEach(exp => {
index.exports.push({
name: exp.name,
file: structure.file,
line: 0, // TODO: Get actual line from exports
kind: exp.kind,
isDefault: exp.isDefault
});
});
}
/**
* Extract API routes from code patterns
*/
async extractRoutes(analyzer, index) {
const routePatterns = [
{ pattern: /app\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/, framework: 'express' },
{ pattern: /router\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/, framework: 'express' },
{ pattern: /export\s+async\s+function\s+(GET|POST|PUT|DELETE|PATCH)/, framework: 'nextjs' },
{ pattern: /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`]([^'"`]+)['"`]/, framework: 'decorator' }
];
for (const { pattern, framework } of routePatterns) {
try {
const matches = await analyzer.searchCode(pattern.source, {
regex: true,
fileTypes: ['.ts', '.js', '.tsx', '.jsx']
});
matches.forEach(match => {
const regexMatch = match.content.match(pattern);
if (regexMatch) {
const method = regexMatch[1]?.toUpperCase() || 'GET';
const routePath = regexMatch[2] || '';
index.routes.push({
method,
path: routePath,
handler: this.extractHandlerName(match.content),
file: match.file,
line: match.line
});
}
});
}
catch (error) {
// Continue if pattern search fails
}
}
}
/**
* Extract handler function name from route code
*/
extractHandlerName(code) {
// Try to extract function name from various patterns
const patterns = [
/function\s+(\w+)/,
/const\s+(\w+)\s*=/,
/(\w+)\s*=>/,
/\.(\w+)\s*\(/
];
for (const pattern of patterns) {
const match = code.match(pattern);
if (match) {
return match[1];
}
}
return 'anonymous';
}
/**
* Build function signature string
*/
buildFunctionSignature(func) {
const params = func.parameters?.map((p) => `${p.name}${p.isOptional ? '?' : ''}${p.type ? `: ${p.type}` : ''}`).join(', ') || '';
const returnType = func.returnType ? `: ${func.returnType}` : '';
const asyncPrefix = func.isAsync ? 'async ' : '';
return `${asyncPrefix}function ${func.name}(${params})${returnType}`;
}
/**
* Find all source files to analyze
*/
async findSourceFiles() {
const files = [];
const extensions = ['.ts', '.tsx', '.js', '.jsx'];
const walkDir = async (dir) => {
if (dir.includes('node_modules') || dir.includes('.git') || dir.includes('.mcp-cache'))
return;
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
await walkDir(fullPath);
}
else if (extensions.some(ext => entry.name.endsWith(ext))) {
files.push(fullPath);
}
}
}
catch (error) {
// Ignore permission errors
}
};
await walkDir(this.projectRoot);
return files;
}
/**
* Save index to cache
*/
async saveIndex(index) {
try {
await fs.mkdir(this.cacheDir, { recursive: true });
await fs.writeFile(this.indexFile, JSON.stringify(index, null, 2));
// Add .gitignore to cache directory
const gitignorePath = path.join(this.cacheDir, '.gitignore');
await fs.writeFile(gitignorePath, '*\n!.gitignore\n');
}
catch (error) {
console.error('Failed to save index cache:', error);
}
}
}
//# sourceMappingURL=indexer.js.map