@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
431 lines (368 loc) • 13.7 kB
text/typescript
import { ConfigManager } from '../../config/config-manager.js';
import { CodeContext } from './types.js';
import { promises as fs } from 'fs';
import path from 'path';
export class CodeMemory {
private configManager: ConfigManager;
private codeContextPath: string;
private codeContexts: Map<string, CodeContext>;
constructor(configManager: ConfigManager) {
this.configManager = configManager;
this.codeContextPath = '';
this.codeContexts = new Map();
}
async initialize(): Promise<void> {
const storageManager = this.configManager.getStorageManager();
const location = await storageManager.getStorageLocation();
this.codeContextPath = path.join(location.data, 'memory', 'code-contexts.json');
// Ensure directory exists
await fs.mkdir(path.dirname(this.codeContextPath), { recursive: true });
// Load existing code contexts
await this.loadCodeContexts();
}
async analyzeContext(options: {
filePath: string;
functionName?: string;
includeRelated?: boolean;
depth?: number;
}): Promise<CodeContext> {
const fullPath = path.resolve(options.filePath);
const contextKey = options.functionName ? `${fullPath}:${options.functionName}` : fullPath;
// Check if we have cached context
const cached = this.codeContexts.get(contextKey);
if (cached) {
return cached;
}
// Analyze the code file
const context = await this.performCodeAnalysis(options);
// Cache the context
this.codeContexts.set(contextKey, context);
await this.saveCodeContexts();
return context;
}
async updateCodeContext(filePath: string, context: CodeContext): Promise<void> {
this.codeContexts.set(filePath, context);
await this.saveCodeContexts();
}
async findRelatedCode(filePath: string): Promise<CodeContext[]> {
const targetContext = this.codeContexts.get(filePath);
if (!targetContext) return [];
const related: { context: CodeContext; score: number }[] = [];
for (const [path, context] of this.codeContexts) {
if (path === filePath) continue;
let score = 0;
// Check if files import each other
const hasImportRelation = context.dependencies.some(dep =>
dep.filePath === filePath
) || targetContext.dependencies.some(dep =>
dep.filePath === path.replace(/:[^:]*$/, '') // Remove function name if present
);
if (hasImportRelation) {
score += 0.5;
}
// Check for similar usage patterns
const sharedPatterns = targetContext.usagePatterns.filter(pattern =>
context.usagePatterns.includes(pattern)
);
score += sharedPatterns.length * 0.2;
// Check for related code references
const relatedCodeMatch = targetContext.relatedCode.some(related =>
related.file === context.filePath
);
if (relatedCodeMatch) {
score += 0.3;
}
if (score > 0.2) {
related.push({ context, score });
}
}
return related
.sort((a, b) => b.score - a.score)
.slice(0, 5)
.map(item => item.context);
}
async getCodeStats(): Promise<{
totalContexts: number;
fileTypes: Record<string, number>;
topPatterns: Array<{ pattern: string; count: number }>;
dependencyGraph: Record<string, string[]>;
}> {
const contexts = Array.from(this.codeContexts.values());
const fileTypes: Record<string, number> = {};
const patternCounts: Record<string, number> = {};
const dependencyGraph: Record<string, string[]> = {};
contexts.forEach(context => {
const ext = path.extname(context.filePath);
fileTypes[ext] = (fileTypes[ext] || 0) + 1;
context.usagePatterns.forEach(pattern => {
patternCounts[pattern] = (patternCounts[pattern] || 0) + 1;
});
const deps = context.dependencies
.filter(dep => dep.filePath)
.map(dep => dep.filePath!);
if (deps.length > 0) {
dependencyGraph[context.filePath] = deps;
}
});
const topPatterns = Object.entries(patternCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([pattern, count]) => ({ pattern, count }));
return {
totalContexts: contexts.length,
fileTypes,
topPatterns,
dependencyGraph,
};
}
private async performCodeAnalysis(options: {
filePath: string;
functionName?: string;
includeRelated?: boolean;
depth?: number;
}): Promise<CodeContext> {
try {
const content = await fs.readFile(options.filePath, 'utf-8');
const ext = path.extname(options.filePath);
let context: CodeContext;
switch (ext) {
case '.ts':
case '.tsx':
case '.js':
case '.jsx':
context = await this.analyzeJavaScriptTypeScript(content, options);
break;
case '.py':
context = await this.analyzePython(content, options);
break;
default:
context = await this.analyzeGeneric(content, options);
}
// Add related code if requested
if (options.includeRelated) {
context.relatedCode = await this.findRelatedCodeReferences(context, options.depth || 2);
}
return context;
} catch (error) {
// Return minimal context if analysis fails
return {
filePath: options.filePath,
functionName: options.functionName,
summary: `Failed to analyze: ${(error as Error).message}`,
dependencies: [],
usagePatterns: [],
relatedCode: [],
recommendations: ['File could not be analyzed - check file permissions and format'],
};
}
}
private async analyzeJavaScriptTypeScript(
content: string,
options: { filePath: string; functionName?: string }
): Promise<CodeContext> {
const lines = content.split('\n');
const dependencies: CodeContext['dependencies'] = [];
const usagePatterns: string[] = [];
const recommendations: string[] = [];
// Extract imports
const importRegex = /^import\s+(?:.*\s+from\s+)?['"]([^'"]+)['"];?/gm;
let match;
while ((match = importRegex.exec(content)) !== null) {
dependencies.push({
name: match[1],
type: 'import',
description: `Imported module: ${match[1]}`,
filePath: this.resolveImportPath(match[1], options.filePath),
});
}
// Extract function/class definitions
const functionRegex = /^(?:export\s+)?(?:async\s+)?function\s+(\w+)/gm;
while ((match = functionRegex.exec(content)) !== null) {
dependencies.push({
name: match[1],
type: 'function',
description: `Function: ${match[1]}`,
});
}
const classRegex = /^(?:export\s+)?class\s+(\w+)/gm;
while ((match = classRegex.exec(content)) !== null) {
dependencies.push({
name: match[1],
type: 'class',
description: `Class: ${match[1]}`,
});
}
// Detect usage patterns
if (content.includes('React')) usagePatterns.push('React component');
if (content.includes('express')) usagePatterns.push('Express.js server');
if (content.includes('async/await')) usagePatterns.push('Async/await pattern');
if (content.includes('jest') || content.includes('describe(')) usagePatterns.push('Jest testing');
if (content.includes('useState') || content.includes('useEffect')) usagePatterns.push('React hooks');
if (content.includes('try/catch')) usagePatterns.push('Error handling');
// Generate recommendations
if (!content.includes('use strict') && content.includes('function')) {
recommendations.push('Consider using strict mode');
}
if (content.includes('any') && options.filePath.endsWith('.ts')) {
recommendations.push('Avoid using "any" type in TypeScript');
}
if (content.includes('console.log')) {
recommendations.push('Remove console.log statements before production');
}
let summary = `${path.basename(options.filePath)} - `;
if (usagePatterns.length > 0) {
summary += usagePatterns[0];
} else {
summary += 'JavaScript/TypeScript file';
}
if (options.functionName) {
const functionMatch = content.match(new RegExp(`function\\s+${options.functionName}\\s*\\(`));
if (functionMatch) {
summary += ` (analyzing function: ${options.functionName})`;
}
}
return {
filePath: options.filePath,
functionName: options.functionName,
summary,
dependencies,
usagePatterns,
relatedCode: [],
recommendations,
};
}
private async analyzePython(
content: string,
options: { filePath: string; functionName?: string }
): Promise<CodeContext> {
const dependencies: CodeContext['dependencies'] = [];
const usagePatterns: string[] = [];
const recommendations: string[] = [];
// Extract imports
const importRegex = /^(?:from\s+(\S+)\s+)?import\s+(.+)$/gm;
let match;
while ((match = importRegex.exec(content)) !== null) {
const module = match[1] || match[2].split(',')[0].trim();
dependencies.push({
name: module,
type: 'import',
description: `Imported: ${module}`,
});
}
// Extract function definitions
const functionRegex = /^def\s+(\w+)\s*\(/gm;
while ((match = functionRegex.exec(content)) !== null) {
dependencies.push({
name: match[1],
type: 'function',
description: `Function: ${match[1]}`,
});
}
// Extract class definitions
const classRegex = /^class\s+(\w+)/gm;
while ((match = classRegex.exec(content)) !== null) {
dependencies.push({
name: match[1],
type: 'class',
description: `Class: ${match[1]}`,
});
}
// Detect patterns
if (content.includes('flask')) usagePatterns.push('Flask web application');
if (content.includes('django')) usagePatterns.push('Django web application');
if (content.includes('pandas')) usagePatterns.push('Data analysis with pandas');
if (content.includes('numpy')) usagePatterns.push('Numerical computing');
if (content.includes('async def')) usagePatterns.push('Async/await pattern');
if (content.includes('unittest') || content.includes('pytest')) usagePatterns.push('Unit testing');
return {
filePath: options.filePath,
functionName: options.functionName,
summary: `${path.basename(options.filePath)} - Python ${usagePatterns[0] || 'script'}`,
dependencies,
usagePatterns,
relatedCode: [],
recommendations,
};
}
private async analyzeGeneric(
content: string,
options: { filePath: string; functionName?: string }
): Promise<CodeContext> {
const ext = path.extname(options.filePath);
const dependencies: CodeContext['dependencies'] = [];
const usagePatterns: string[] = [];
// Basic analysis for any file type
const lineCount = content.split('\n').length;
const wordCount = content.split(/\s+/).length;
if (ext === '.md') {
usagePatterns.push('Markdown documentation');
} else if (ext === '.json') {
usagePatterns.push('JSON configuration');
} else if (ext === '.yml' || ext === '.yaml') {
usagePatterns.push('YAML configuration');
}
return {
filePath: options.filePath,
functionName: options.functionName,
summary: `${path.basename(options.filePath)} - ${lineCount} lines, ${wordCount} words`,
dependencies,
usagePatterns,
relatedCode: [],
recommendations: [],
};
}
private async findRelatedCodeReferences(
context: CodeContext,
depth: number
): Promise<CodeContext['relatedCode']> {
const related: CodeContext['relatedCode'] = [];
// Find files that import this file or are imported by this file
for (const dep of context.dependencies) {
if (dep.filePath && dep.type === 'import') {
try {
await fs.access(dep.filePath);
related.push({
file: dep.filePath,
description: `Imported dependency: ${dep.name}`,
relationship: 'imports',
});
} catch {
// File doesn't exist or can't be accessed
}
}
}
// Look for files that might import this file
if (depth > 1) {
// This would require scanning other files in the project
// For now, we'll keep it simple
}
return related.slice(0, 10); // Limit to 10 related files
}
private resolveImportPath(importPath: string, currentFile: string): string | undefined {
if (importPath.startsWith('.')) {
// Relative import
const currentDir = path.dirname(currentFile);
return path.resolve(currentDir, importPath);
}
// For node_modules or absolute imports, we'd need more sophisticated resolution
return undefined;
}
private async loadCodeContexts(): Promise<void> {
try {
const data = await fs.readFile(this.codeContextPath, 'utf-8');
const contextsArray: CodeContext[] = JSON.parse(data);
this.codeContexts.clear();
for (const context of contextsArray) {
const key = context.functionName ? `${context.filePath}:${context.functionName}` : context.filePath;
this.codeContexts.set(key, context);
}
} catch (error) {
// File doesn't exist or is invalid, start with empty contexts
this.codeContexts.clear();
}
}
private async saveCodeContexts(): Promise<void> {
const contextsArray = Array.from(this.codeContexts.values());
await fs.writeFile(this.codeContextPath, JSON.stringify(contextsArray, null, 2));
}
}