UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

280 lines (279 loc) 10.6 kB
import fs from 'fs/promises'; import path from 'path'; import logger from '../../logger.js'; import { FileSearchService } from './file-search-engine.js'; export class FileReaderService { static instance; contentCache = new Map(); fileSearchService; constructor() { this.fileSearchService = FileSearchService.getInstance(); logger.debug('File reader service initialized'); } static getInstance() { if (!FileReaderService.instance) { FileReaderService.instance = new FileReaderService(); } return FileReaderService.instance; } async readFiles(filePaths, options = {}) { const startTime = Date.now(); const result = { files: [], errors: [], metrics: { totalFiles: filePaths.length, successCount: 0, errorCount: 0, totalSize: 0, readTime: 0, cacheHits: 0 } }; logger.debug({ fileCount: filePaths.length, options }, 'Reading multiple files'); for (const filePath of filePaths) { try { const fileContent = await this.readSingleFile(filePath, options); if (fileContent) { result.files.push(fileContent); result.metrics.successCount++; result.metrics.totalSize += fileContent.size; } } catch (error) { const errorInfo = this.categorizeError(error, filePath); result.errors.push(errorInfo); result.metrics.errorCount++; logger.debug({ filePath, error: errorInfo }, 'Failed to read file'); } } result.metrics.readTime = Date.now() - startTime; logger.info({ totalFiles: result.metrics.totalFiles, successCount: result.metrics.successCount, errorCount: result.metrics.errorCount, readTime: result.metrics.readTime }, 'File reading completed'); return result; } async readFilesByPattern(projectPath, pattern, options = {}) { logger.debug({ projectPath, pattern }, 'Reading files by pattern'); const searchResults = await this.fileSearchService.searchFiles(projectPath, { pattern, searchStrategy: 'fuzzy', maxResults: 100, cacheResults: true }); const filePaths = searchResults.map(result => result.filePath); return this.readFiles(filePaths, options); } async readFilesByGlob(projectPath, globPattern, options = {}) { logger.debug({ projectPath, globPattern }, 'Reading files by glob pattern'); const searchResults = await this.fileSearchService.searchFiles(projectPath, { glob: globPattern, searchStrategy: 'glob', maxResults: 200, cacheResults: true }); const filePaths = searchResults.map(result => result.filePath); return this.readFiles(filePaths, options); } async readSingleFile(filePath, options) { const cacheKey = this.generateCacheKey(filePath, options); if (options.cacheContent !== false && this.contentCache.has(cacheKey)) { const cached = this.contentCache.get(cacheKey); try { const stats = await fs.stat(filePath); if (stats.mtime.getTime() === cached.lastModified.getTime()) { logger.debug({ filePath }, 'Using cached file content'); return cached; } else { this.contentCache.delete(cacheKey); } } catch { this.contentCache.delete(cacheKey); return null; } } const fileContent = await this.readFromDisk(filePath, options); if (fileContent && options.cacheContent !== false) { this.contentCache.set(cacheKey, fileContent); if (this.contentCache.size > 1000) { const firstKey = this.contentCache.keys().next().value; if (firstKey) { this.contentCache.delete(firstKey); } } } return fileContent; } async readFromDisk(filePath, options) { const maxFileSize = options.maxFileSize || 10 * 1024 * 1024; const encoding = options.encoding || 'utf-8'; const stats = await fs.stat(filePath); if (stats.size > maxFileSize) { throw new Error(`File too large: ${stats.size} bytes (max: ${maxFileSize})`); } const extension = path.extname(filePath).toLowerCase(); const contentType = this.determineContentType(extension); if ((contentType === 'binary' || contentType === 'image') && !options.includeBinary) { throw new Error('Binary file excluded'); } let content; let fileEncoding; try { const buffer = await fs.readFile(filePath); if (contentType === 'binary' || contentType === 'image') { content = buffer.toString('base64'); fileEncoding = 'base64'; } else { content = buffer.toString(encoding); fileEncoding = encoding; } } catch (error) { throw new Error(`Failed to read file: ${error}`); } if (options.lineRange) { const lines = content.split('\n'); const [start, end] = options.lineRange; content = lines.slice(start - 1, end).join('\n'); } if (options.maxLines) { const lines = content.split('\n'); if (lines.length > options.maxLines) { content = lines.slice(0, options.maxLines).join('\n'); } } const fileContent = { filePath, content, size: stats.size, lastModified: stats.mtime, extension, contentType, encoding: fileEncoding, lineCount: content.split('\n').length, charCount: content.length }; return fileContent; } determineContentType(extension) { const textExtensions = new Set([ '.txt', '.md', '.js', '.ts', '.jsx', '.tsx', '.json', '.xml', '.html', '.htm', '.css', '.scss', '.sass', '.less', '.py', '.java', '.c', '.cpp', '.h', '.hpp', '.cs', '.php', '.rb', '.go', '.rs', '.swift', '.kt', '.scala', '.clj', '.hs', '.ml', '.fs', '.vb', '.sql', '.sh', '.bash', '.zsh', '.fish', '.ps1', '.bat', '.cmd', '.yaml', '.yml', '.toml', '.ini', '.cfg', '.conf', '.log', '.csv', '.tsv', '.gitignore', '.gitattributes', '.editorconfig', '.eslintrc', '.prettierrc' ]); const imageExtensions = new Set([ '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.webp', '.ico', '.tiff', '.tif' ]); const binaryExtensions = new Set([ '.exe', '.dll', '.so', '.dylib', '.bin', '.zip', '.tar', '.gz', '.rar', '.7z', '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.mp3', '.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv', '.wav', '.flac', '.ogg' ]); if (textExtensions.has(extension)) { return 'text'; } else if (imageExtensions.has(extension)) { return 'image'; } else if (binaryExtensions.has(extension)) { return 'binary'; } else { return 'unknown'; } } generateCacheKey(filePath, options) { const keyData = { filePath, encoding: options.encoding || 'utf-8', lineRange: options.lineRange, maxLines: options.maxLines, includeBinary: options.includeBinary }; return JSON.stringify(keyData); } categorizeError(error, filePath) { const errorMessage = error?.message || String(error); if (errorMessage.includes('ENOENT') || errorMessage.includes('not found')) { return { filePath, error: errorMessage, reason: 'not-found' }; } else if (errorMessage.includes('too large')) { return { filePath, error: errorMessage, reason: 'too-large' }; } else if (errorMessage.includes('Binary file excluded')) { return { filePath, error: errorMessage, reason: 'binary' }; } else if (errorMessage.includes('EACCES') || errorMessage.includes('permission')) { return { filePath, error: errorMessage, reason: 'permission' }; } else if (errorMessage.includes('encoding') || errorMessage.includes('decode')) { return { filePath, error: errorMessage, reason: 'encoding' }; } else { return { filePath, error: errorMessage, reason: 'unknown' }; } } clearCache(filePath) { if (filePath) { const keysToDelete = []; for (const key of this.contentCache.keys()) { if (key.includes(filePath)) { keysToDelete.push(key); } } keysToDelete.forEach(key => this.contentCache.delete(key)); logger.debug({ filePath, clearedEntries: keysToDelete.length }, 'File content cache cleared for file'); } else { const totalEntries = this.contentCache.size; this.contentCache.clear(); logger.info({ clearedEntries: totalEntries }, 'File content cache cleared completely'); } } getCacheStats() { const totalEntries = this.contentCache.size; let totalMemoryUsage = 0; for (const content of this.contentCache.values()) { totalMemoryUsage += content.content.length * 2; totalMemoryUsage += JSON.stringify(content).length * 2; } return { totalEntries, memoryUsage: totalMemoryUsage, averageFileSize: totalEntries > 0 ? totalMemoryUsage / totalEntries : 0 }; } }