Skip to main content
Glama
repo-map.ts11.7 kB
/** * Code Agent - Repository Mapping * AST-based repository structure analysis and context extraction */ import * as fs from 'fs/promises'; import * as path from 'path'; import { callLLM } from '../llm/index.js'; import { getModelsByLayer } from '../../config/models.js'; import { logger } from '../../logging/logger.js'; export interface RepoMap { rootPath: string; structure: FileNode; modules: ModuleInfo[]; dependencies: DependencyGraph; entryPoints: string[]; statistics: RepoStatistics; } export interface FileNode { name: string; path: string; type: 'file' | 'directory'; size?: number; language?: string; children?: FileNode[]; exports?: string[]; imports?: string[]; } export interface ModuleInfo { filePath: string; name: string; exports: ExportInfo[]; imports: ImportInfo[]; functions: FunctionInfo[]; classes: ClassInfo[]; complexity: number; } export interface ExportInfo { name: string; type: 'function' | 'class' | 'const' | 'type' | 'interface'; isDefault: boolean; } export interface ImportInfo { source: string; imports: string[]; isExternal: boolean; } export interface FunctionInfo { name: string; parameters: string[]; returnType?: string; async: boolean; lineCount: number; } export interface ClassInfo { name: string; methods: string[]; properties: string[]; extends?: string; } export interface DependencyGraph { nodes: Array<{ id: string; label: string; type: string }>; edges: Array<{ from: string; to: string; type: string }>; } export interface RepoStatistics { totalFiles: number; totalLines: number; languages: Record<string, number>; avgComplexity: number; largestFiles: Array<{ path: string; lines: number }>; } /** * Generate comprehensive repository map */ export async function generateRepoMap( rootPath: string, options: { excludePatterns?: string[]; maxDepth?: number; includeNodeModules?: boolean; } = {}, ): Promise<RepoMap> { logger.info('Generating repository map', { rootPath }); const excludePatterns = options.excludePatterns || [ 'node_modules', 'dist', 'build', '.git', 'coverage', ]; // Step 1: Build file tree const structure = await buildFileTree(rootPath, excludePatterns, 0, options.maxDepth || 10); // Step 2: Analyze modules const modules = await analyzeModules(structure); // Step 3: Build dependency graph const dependencies = buildDependencyGraph(modules); // Step 4: Identify entry points const entryPoints = identifyEntryPoints(structure, modules); // Step 5: Calculate statistics const statistics = calculateStatistics(structure, modules); return { rootPath, structure, modules, dependencies, entryPoints, statistics, }; } /** * Build file tree recursively */ async function buildFileTree( dirPath: string, excludePatterns: string[], depth: number, maxDepth: number, ): Promise<FileNode> { const stats = await fs.stat(dirPath); const name = path.basename(dirPath); if (excludePatterns.some((pattern) => name.includes(pattern))) { return { name, path: dirPath, type: 'directory', children: [], }; } if (stats.isFile()) { const ext = path.extname(name); const language = getLanguageFromExtension(ext); return { name, path: dirPath, type: 'file', size: stats.size, language, }; } if (depth >= maxDepth) { return { name, path: dirPath, type: 'directory', children: [], }; } const entries = await fs.readdir(dirPath); const children: FileNode[] = []; for (const entry of entries) { const fullPath = path.join(dirPath, entry); try { const child = await buildFileTree(fullPath, excludePatterns, depth + 1, maxDepth); children.push(child); } catch (error) { // Skip inaccessible files logger.debug(`Skipping ${fullPath}: ${error}`); } } return { name, path: dirPath, type: 'directory', children, }; } /** * Analyze TypeScript/JavaScript modules */ async function analyzeModules(structure: FileNode): Promise<ModuleInfo[]> { const modules: ModuleInfo[] = []; async function processNode(node: FileNode) { if (node.type === 'file' && (node.language === 'typescript' || node.language === 'javascript')) { try { const content = await fs.readFile(node.path, 'utf-8'); const moduleInfo = await analyzeModuleContent(node.path, content); modules.push(moduleInfo); } catch (error) { logger.debug(`Failed to analyze ${node.path}: ${error}`); } } if (node.children) { for (const child of node.children) { await processNode(child); } } } await processNode(structure); return modules; } /** * Analyze module content with LLM */ async function analyzeModuleContent(filePath: string, content: string): Promise<ModuleInfo> { // Simple regex-based analysis for performance const exports = extractExports(content); const imports = extractImports(content); const functions = extractFunctions(content); const classes = extractClasses(content); return { filePath, name: path.basename(filePath), exports, imports, functions, classes, complexity: calculateComplexity(content), }; } /** * Extract exports from code */ function extractExports(code: string): ExportInfo[] { const exports: ExportInfo[] = []; // Export default const defaultExport = /export\s+default\s+(function|class|const|interface|type)\s+(\w+)/g.exec( code, ); if (defaultExport) { exports.push({ name: defaultExport[2], type: defaultExport[1] as ExportInfo['type'], isDefault: true, }); } // Named exports const namedExports = code.matchAll( /export\s+(function|class|const|interface|type)\s+(\w+)/g, ); for (const match of namedExports) { exports.push({ name: match[2], type: match[1] as ExportInfo['type'], isDefault: false, }); } return exports; } /** * Extract imports from code */ function extractImports(code: string): ImportInfo[] { const imports: ImportInfo[] = []; const importStatements = code.matchAll(/import\s+.*?\s+from\s+['"](.+?)['"]/g); for (const match of importStatements) { const source = match[1]; const isExternal = !source.startsWith('.') && !source.startsWith('/'); // Extract imported names const importedNames = match[0].match(/import\s+\{(.+?)\}/); const names = importedNames ? importedNames[1].split(',').map((n) => n.trim()) : []; imports.push({ source, imports: names, isExternal, }); } return imports; } /** * Extract functions from code */ function extractFunctions(code: string): FunctionInfo[] { const functions: FunctionInfo[] = []; const functionMatches = code.matchAll( /(async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g, ); for (const match of functionMatches) { const isAsync = !!match[1]; const name = match[2]; const params = match[3] .split(',') .map((p) => p.trim()) .filter((p) => p); functions.push({ name, parameters: params, async: isAsync, lineCount: 0, // TODO: Calculate actual line count }); } return functions; } /** * Extract classes from code */ function extractClasses(code: string): ClassInfo[] { const classes: ClassInfo[] = []; const classMatches = code.matchAll(/class\s+(\w+)(?:\s+extends\s+(\w+))?/g); for (const match of classMatches) { classes.push({ name: match[1], methods: [], properties: [], extends: match[2], }); } return classes; } /** * Calculate cyclomatic complexity (simplified) */ function calculateComplexity(code: string): number { const keywords = ['if', 'else', 'for', 'while', 'switch', 'case', 'catch', '&&', '||']; let complexity = 1; for (const keyword of keywords) { const regex = new RegExp(`\\b${keyword}\\b`, 'g'); const matches = code.match(regex); if (matches) { complexity += matches.length; } } return complexity; } /** * Build dependency graph */ function buildDependencyGraph(modules: ModuleInfo[]): DependencyGraph { const nodes: DependencyGraph['nodes'] = []; const edges: DependencyGraph['edges'] = []; for (const module of modules) { nodes.push({ id: module.filePath, label: module.name, type: 'module', }); for (const imp of module.imports) { if (!imp.isExternal) { edges.push({ from: module.filePath, to: imp.source, type: 'import', }); } } } return { nodes, edges }; } /** * Identify entry points (main files) */ function identifyEntryPoints(structure: FileNode, modules: ModuleInfo[]): string[] { const entryPoints: string[] = []; // Common entry point names const entryNames = ['index.ts', 'main.ts', 'app.ts', 'server.ts']; function findEntries(node: FileNode) { if (node.type === 'file' && entryNames.includes(node.name)) { entryPoints.push(node.path); } if (node.children) { node.children.forEach(findEntries); } } findEntries(structure); return entryPoints; } /** * Calculate repository statistics */ function calculateStatistics(structure: FileNode, modules: ModuleInfo[]): RepoStatistics { let totalFiles = 0; let totalLines = 0; const languages: Record<string, number> = {}; const largestFiles: Array<{ path: string; lines: number }> = []; function countStats(node: FileNode) { if (node.type === 'file') { totalFiles++; if (node.language) { languages[node.language] = (languages[node.language] || 0) + 1; } } if (node.children) { node.children.forEach(countStats); } } countStats(structure); const avgComplexity = modules.reduce((sum, m) => sum + m.complexity, 0) / Math.max(modules.length, 1); return { totalFiles, totalLines, languages, avgComplexity, largestFiles, }; } /** * Get programming language from file extension */ function getLanguageFromExtension(ext: string): string | undefined { const mapping: Record<string, string> = { '.ts': 'typescript', '.tsx': 'typescript', '.js': 'javascript', '.jsx': 'javascript', '.py': 'python', '.java': 'java', '.go': 'go', '.rs': 'rust', '.cpp': 'cpp', '.c': 'c', }; return mapping[ext.toLowerCase()]; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/babasida246/ai-mcp-gateway'

If you have feedback or need assistance with the MCP directory API, please join our Discord server