UNPKG

context-engine-mcp

Version:

Context engine MCP server for comprehensive project analysis and multi-file editing

463 lines 17.4 kB
import fs from 'fs/promises'; import path from 'path'; import crypto from 'crypto'; import { glob } from 'glob'; import { configManager } from '../config/index.js'; import logger, { PerformanceLogger, logMemoryUsage } from '../utils/logger.js'; import { validatePath, validateProjectPath, validateFileSize, isPathWithinDirectory } from '../utils/validation.js'; import { FileNotFoundError, ProcessingError, handleAsyncError } from '../utils/errors.js'; import { fileCache } from './cache-manager.js'; export class FileManager { static instance; maxFileSize; supportedExtensions; constructor() { this.maxFileSize = configManager.get('maxFileSize'); this.supportedExtensions = new Set([ '.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.py', '.pyw', '.java', '.cs', '.cpp', '.cxx', '.cc', '.c++', '.c', '.h', '.hpp', '.php', '.rb', '.go', '.rs', '.vue', '.svelte', '.html', '.htm', '.css', '.scss', '.sass', '.less', '.json', '.yml', '.yaml', '.md', '.txt', '.xml', '.sql' ]); } static getInstance() { if (!FileManager.instance) { FileManager.instance = new FileManager(); } return FileManager.instance; } /** * Safely read a file with validation and caching */ async readFile(filePath, useCache = true) { const perf = new PerformanceLogger(`readFile: ${filePath}`); try { // Validate path const normalizedPath = validatePath(filePath); const absolutePath = path.resolve(normalizedPath); // Check cache first if (useCache) { const cached = fileCache.get(absolutePath); if (cached) { perf.end({ cacheHit: true }); return cached; } } // Check if file exists and get stats const stats = await fs.stat(absolutePath).catch(() => { throw new FileNotFoundError(absolutePath); }); // Validate file size validateFileSize(stats.size, this.maxFileSize, absolutePath); // Read file content const content = await handleAsyncError(fs.readFile(absolutePath, 'utf-8'), { operation: 'read file', filePath: absolutePath }); // Cache the content if requested if (useCache) { const hash = this.generateFileHash(content); fileCache.set(absolutePath, content); logger.debug('File cached', { filePath: absolutePath, size: content.length, hash: hash.substring(0, 8) }); } perf.end({ fileSize: stats.size, cached: false }); return content; } catch (error) { perf.end({ error: true }); throw error; } } /** * Safely write a file with backup creation */ async writeFile(filePath, content, options = {}) { const perf = new PerformanceLogger(`writeFile: ${filePath}`); const { createBackup = true, ensureDirectory = true, encoding = 'utf-8' } = options; try { // Validate path const normalizedPath = validatePath(filePath); const absolutePath = path.resolve(normalizedPath); // Validate content size const contentSize = Buffer.byteLength(content, encoding); validateFileSize(contentSize, this.maxFileSize, absolutePath); // Ensure directory exists if (ensureDirectory) { await this.ensureDirectory(path.dirname(absolutePath)); } // Create backup if file exists and backup is requested if (createBackup) { await this.createBackup(absolutePath); } // Write file await handleAsyncError(fs.writeFile(absolutePath, content, encoding), { operation: 'write file', filePath: absolutePath }); // Update cache fileCache.set(absolutePath, content); perf.end({ fileSize: contentSize, backedUp: createBackup }); logger.info('File written successfully', { filePath: absolutePath, size: contentSize }); } catch (error) { perf.end({ error: true }); throw error; } } /** * Safely delete a file with backup creation */ async deleteFile(filePath, createBackup = true) { const perf = new PerformanceLogger(`deleteFile: ${filePath}`); try { // Validate path const normalizedPath = validatePath(filePath); const absolutePath = path.resolve(normalizedPath); // Check if file exists const exists = await this.fileExists(absolutePath); if (!exists) { logger.warn('Attempted to delete non-existent file', { filePath: absolutePath }); return; } // Create backup if requested if (createBackup) { await this.createBackup(absolutePath); } // Delete file await handleAsyncError(fs.unlink(absolutePath), { operation: 'delete file', filePath: absolutePath }); // Remove from cache fileCache.delete(absolutePath); perf.end({ backedUp: createBackup }); logger.info('File deleted successfully', { filePath: absolutePath, backedUp: createBackup }); } catch (error) { perf.end({ error: true }); throw error; } } /** * Get file information with caching */ async getFileInfo(filePath) { const perf = new PerformanceLogger(`getFileInfo: ${filePath}`); try { // Validate path const normalizedPath = validatePath(filePath); const absolutePath = path.resolve(normalizedPath); // Check cache first const cacheKey = `fileInfo:${absolutePath}`; const cached = fileCache.get(cacheKey); if (cached) { perf.end({ cacheHit: true }); return cached; } // Get file stats const stats = await fs.stat(absolutePath).catch(() => null); if (!stats) { perf.end({ exists: false }); return null; } // Validate file size if (stats.size > this.maxFileSize) { logger.warn('File too large for processing', { filePath: absolutePath, size: stats.size, maxSize: this.maxFileSize }); return null; } // Read content if file is supported const extension = path.extname(absolutePath).toLowerCase(); let content = ''; let structure = { functions: [], classes: [], exports: [], imports: [], variables: [], comments: [] }; if (this.supportedExtensions.has(extension) && stats.isFile()) { try { content = await this.readFile(absolutePath, false); const hash = this.generateFileHash(content); // Create file info const fileInfo = { path: path.relative(process.cwd(), absolutePath), absolutePath, language: this.detectLanguage(absolutePath), size: stats.size, lines: content.split('\n').length, hash, lastModified: stats.mtime, dependencies: [], // Will be filled by language analyzer structure, // Will be filled by language analyzer content: content.length > configManager.get('maxContentLength') ? content.substring(0, configManager.get('maxContentLength')) + '\n... [truncated]' : content }; // Cache file info fileCache.set(cacheKey, fileInfo, 3600000); // 1 hour TTL perf.end({ fileSize: stats.size, cached: false }); return fileInfo; } catch (error) { logger.error('Error processing file info', { filePath: absolutePath, error: error instanceof Error ? error.message : String(error) }); return null; } } perf.end({ supported: false }); return null; } catch (error) { perf.end({ error: true }); throw error; } } /** * Find files matching patterns with exclusions */ async findFiles(searchPath, patterns = ['**/*'], ignorePatterns = []) { const perf = new PerformanceLogger(`findFiles: ${searchPath}`); try { // Validate and resolve search path const normalizedPath = validateProjectPath(searchPath); // Combine patterns with ignore patterns const allIgnorePatterns = [ ...configManager.get('ignorePatterns'), ...ignorePatterns ]; const globResults = await handleAsyncError(glob(patterns, { cwd: normalizedPath, ignore: allIgnorePatterns, absolute: true, dot: false }), { operation: 'find files', searchPath: normalizedPath }); // Convert glob results to strings - glob returns string[] const files = Array.isArray(globResults) ? globResults : [globResults]; // Filter files by supported extensions and size const filteredFiles = []; for (const file of files) { try { const stats = await fs.stat(file); // Skip directories if (!stats.isFile()) continue; // Skip files that are too large if (stats.size > this.maxFileSize) { logger.debug('Skipping large file', { filePath: file, size: stats.size }); continue; } // Check if extension is supported const extension = path.extname(file).toLowerCase(); if (this.supportedExtensions.has(extension) || extension === '') { filteredFiles.push(file); } } catch (error) { logger.debug('Error checking file', { filePath: file, error: error instanceof Error ? error.message : String(error) }); continue; } } perf.end({ totalFiles: files.length, filteredFiles: filteredFiles.length }); logMemoryUsage('find files'); return filteredFiles; } catch (error) { perf.end({ error: true }); throw error; } } /** * Check if a file exists */ async fileExists(filePath) { try { const normalizedPath = validatePath(filePath); const absolutePath = path.resolve(normalizedPath); await fs.access(absolutePath); return true; } catch { return false; } } /** * Create a backup of a file */ async createBackup(filePath) { try { const exists = await this.fileExists(filePath); if (!exists) return; const backupDir = path.join(path.dirname(filePath), configManager.get('backupDirectory'), new Date().toISOString().split('T')[0] // Today's date ); await this.ensureDirectory(backupDir); const fileName = path.basename(filePath); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupPath = path.join(backupDir, `${fileName}.${timestamp}.backup`); const content = await fs.readFile(filePath, 'utf-8'); await fs.writeFile(backupPath, content, 'utf-8'); logger.debug('Backup created', { originalFile: filePath, backupFile: backupPath }); } catch (error) { logger.error('Failed to create backup', { filePath, error: error instanceof Error ? error.message : String(error) }); // Don't throw error for backup failures to avoid blocking the operation } } /** * Ensure directory exists */ async ensureDirectory(dirPath) { try { await fs.mkdir(dirPath, { recursive: true }); } catch (error) { throw new ProcessingError('directory creation', `Failed to create directory: ${dirPath}`, { dirPath, error: error instanceof Error ? error.message : String(error) }); } } /** * Generate MD5 hash of file content */ generateFileHash(content) { return crypto.createHash('md5').update(content).digest('hex'); } /** * Detect programming language from file extension */ detectLanguage(filePath) { const languageMap = { '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript', '.ts': 'typescript', '.tsx': 'typescript', '.py': 'python', '.pyw': 'python', '.java': 'java', '.cs': 'csharp', '.cpp': 'cpp', '.cxx': 'cpp', '.cc': 'cpp', '.c++': 'cpp', '.c': 'c', '.h': 'c', '.hpp': 'cpp', '.php': 'php', '.rb': 'ruby', '.go': 'go', '.rs': 'rust', '.vue': 'vue', '.svelte': 'svelte', }; const ext = path.extname(filePath).toLowerCase(); return languageMap[ext] || 'text'; } /** * Validate that a path is within project boundaries */ validateProjectBoundaries(filePath, projectRoot) { try { const normalizedFile = path.resolve(filePath); const normalizedRoot = path.resolve(projectRoot); return isPathWithinDirectory(normalizedFile, normalizedRoot); } catch { return false; } } /** * Get disk usage statistics for a directory */ async getDiskUsage(dirPath) { const perf = new PerformanceLogger(`getDiskUsage: ${dirPath}`); try { const normalizedPath = validateProjectPath(dirPath); const files = await this.findFiles(normalizedPath); let totalSize = 0; let largestFile = null; const filesByExtension = {}; for (const file of files) { try { const stats = await fs.stat(file); const size = stats.size; totalSize += size; // Track largest file if (!largestFile || size > largestFile.size) { largestFile = { path: file, size }; } // Track by extension const ext = path.extname(file).toLowerCase() || '.txt'; if (!filesByExtension[ext]) { filesByExtension[ext] = { count: 0, size: 0 }; } filesByExtension[ext].count++; filesByExtension[ext].size += size; } catch (error) { logger.debug('Error getting file stats', { filePath: file, error: error instanceof Error ? error.message : String(error) }); } } const result = { totalFiles: files.length, totalSize, largestFile, filesByExtension }; perf.end(result); return result; } catch (error) { perf.end({ error: true }); throw error; } } /** * Clear all caches */ clearCache() { fileCache.clear(); logger.info('File manager cache cleared'); } /** * Get cache statistics */ getCacheStats() { return fileCache.getStats(); } } // Export singleton instance export const fileManager = FileManager.getInstance(); //# sourceMappingURL=file-manager.js.map