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
JavaScript
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
};
}
}