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.

191 lines (190 loc) 7.99 kB
import { promises as fs } from 'fs'; import path from 'path'; import logger from '../../../logger.js'; export class CodemapCacheManager { static async findRecentCodemap(maxAgeMinutes, outputDir) { const baseOutputDir = outputDir || process.env.VIBE_CODER_OUTPUT_DIR || path.join(process.cwd(), 'VibeCoderOutput'); const codemapDir = path.join(baseOutputDir, 'code-map-generator'); const maxAgeMs = maxAgeMinutes * 60 * 1000; const now = Date.now(); try { await fs.access(codemapDir); const files = await fs.readdir(codemapDir); const codemapFiles = files .filter(f => f.endsWith('.md') && f.includes('code-map')) .map(f => ({ name: f, path: path.join(codemapDir, f), timestamp: this.extractTimestampFromFilename(f) })) .filter(f => f.timestamp !== null) .filter(f => (now - f.timestamp.getTime()) <= maxAgeMs) .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); if (codemapFiles.length > 0) { const latestCodemap = codemapFiles[0]; const ageMs = now - latestCodemap.timestamp.getTime(); logger.info({ path: latestCodemap.path, ageMinutes: Math.round(ageMs / (60 * 1000)), maxAgeMinutes, fileCount: codemapFiles.length }, 'Found recent codemap in cache'); const content = await this.readCodemapWithRetry(latestCodemap.path); return { content, path: latestCodemap.path, timestamp: latestCodemap.timestamp, fromCache: true }; } else { logger.debug({ codemapDir, maxAgeMinutes, totalFiles: files.length, codemapFiles: files.filter(f => f.endsWith('.md') && f.includes('code-map')).length }, 'No recent codemap found in cache'); } } catch (error) { if (error.code === 'ENOENT') { logger.debug({ codemapDir }, 'Codemap directory does not exist'); } else { logger.warn({ error: error instanceof Error ? error.message : 'Unknown error', codemapDir }, 'Failed to check for cached codemap'); } } return null; } static extractTimestampFromFilename(filename) { try { const match = filename.match(/^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)/); if (match) { const timestampStr = match[1]; const parts = timestampStr.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/); if (!parts) { logger.warn({ filename, timestampStr }, 'Invalid timestamp format in filename'); return null; } const [, year, month, day, hour, minute, second, millisecond] = parts; const isoString = `${year}-${month}-${day}T${hour}:${minute}:${second}.${millisecond}Z`; const date = new Date(isoString); if (isNaN(date.getTime())) { logger.warn({ filename, timestampStr, isoString }, 'Invalid timestamp in filename'); return null; } return date; } else { logger.debug({ filename }, 'No timestamp pattern found in filename'); return null; } } catch (error) { logger.warn({ filename, error: error instanceof Error ? error.message : 'Unknown error' }, 'Failed to extract timestamp from filename'); return null; } } static async readCodemapWithRetry(filePath, maxRetries = 3) { const retryDelayMs = 100; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { await fs.access(filePath, fs.constants.R_OK); const content = await fs.readFile(filePath, 'utf-8'); if (content.length > 0 && content.includes('# Code Map')) { logger.debug({ filePath, attempt, contentLength: content.length }, 'Successfully read cached codemap'); return content; } else { throw new Error('File appears to be incomplete or corrupted'); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.warn({ filePath, attempt, maxRetries, error: errorMessage }, 'Failed to read cached codemap file'); if (attempt === maxRetries) { throw new Error(`Failed to read codemap after ${maxRetries} attempts: ${errorMessage}`); } await new Promise(resolve => setTimeout(resolve, retryDelayMs * attempt)); } } throw new Error('Unexpected end of retry loop'); } static async getCacheStats(outputDir) { const baseOutputDir = outputDir || process.env.VIBE_CODER_OUTPUT_DIR || path.join(process.cwd(), 'VibeCoderOutput'); const codemapDir = path.join(baseOutputDir, 'code-map-generator'); try { const files = await fs.readdir(codemapDir); const codemapFiles = files.filter(f => f.endsWith('.md') && f.includes('code-map')); if (codemapFiles.length === 0) { return { totalCodemaps: 0, oldestTimestamp: null, newestTimestamp: null, totalSizeBytes: 0, averageAgeMinutes: 0 }; } const now = Date.now(); let totalSizeBytes = 0; let totalAgeMs = 0; let oldestTimestamp = null; let newestTimestamp = null; for (const file of codemapFiles) { const filePath = path.join(codemapDir, file); const stats = await fs.stat(filePath); const timestamp = this.extractTimestampFromFilename(file); totalSizeBytes += stats.size; if (timestamp) { const ageMs = now - timestamp.getTime(); totalAgeMs += ageMs; if (!oldestTimestamp || timestamp < oldestTimestamp) { oldestTimestamp = timestamp; } if (!newestTimestamp || timestamp > newestTimestamp) { newestTimestamp = timestamp; } } } return { totalCodemaps: codemapFiles.length, oldestTimestamp, newestTimestamp, totalSizeBytes, averageAgeMinutes: Math.round(totalAgeMs / (codemapFiles.length * 60 * 1000)) }; } catch (error) { logger.warn({ error: error instanceof Error ? error.message : 'Unknown error', codemapDir }, 'Failed to get cache statistics'); return { totalCodemaps: 0, oldestTimestamp: null, newestTimestamp: null, totalSizeBytes: 0, averageAgeMinutes: 0 }; } } }