Skip to main content
Glama
codemap-cache.ts•9.48 kB
/** * Codemap cache management utilities for Context Curator * Provides intelligent caching of recently generated codemaps to optimize workflow performance */ import { promises as fs } from 'fs'; import path from 'path'; import logger from '../../../logger.js'; /** * Result of codemap cache lookup */ export interface CodemapCacheResult { /** Codemap file content */ content: string; /** Full path to the codemap file */ path: string; /** Timestamp when the codemap was generated */ timestamp: Date; /** Whether this result came from cache */ fromCache: boolean; } /** * Codemap cache manager for intelligent caching */ export class CodemapCacheManager { /** * Find the most recent codemap within the specified age limit * @param maxAgeMinutes Maximum age of cached codemap in minutes * @param outputDir Base output directory (defaults to VIBE_CODER_OUTPUT_DIR) * @returns CodemapCacheResult if found, null otherwise */ static async findRecentCodemap( maxAgeMinutes: number, outputDir?: string ): Promise<CodemapCacheResult | null> { 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 { // Check if codemap directory exists await fs.access(codemapDir); // Read directory contents const files = await fs.readdir(codemapDir); // Filter and process codemap files 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'); // Read the codemap content with retry logic 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 as NodeJS.ErrnoException).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; } /** * Extract timestamp from codemap filename * Expected format: YYYY-MM-DDTHH-mm-ss-sssZ-code-map.md * @param filename The codemap filename * @returns Date object if parsing successful, null otherwise */ static extractTimestampFromFilename(filename: string): Date | null { try { // Match ISO timestamp pattern in filename const match = filename.match(/^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)/); if (match) { // Convert filename format back to ISO string // From: 2025-06-08T20-16-46-608Z // To: 2025-06-08T20:16:46.608Z const timestampStr = match[1]; // Parse the timestamp components manually for better control 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; // Create ISO string manually const isoString = `${year}-${month}-${day}T${hour}:${minute}:${second}.${millisecond}Z`; const date = new Date(isoString); // Validate the parsed date 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; } } /** * Read codemap file with retry logic for concurrent access handling * @param filePath Path to the codemap file * @param maxRetries Maximum number of retry attempts * @returns File content as string */ private static async readCodemapWithRetry( filePath: string, maxRetries: number = 3 ): Promise<string> { const retryDelayMs = 100; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { // Check file accessibility first await fs.access(filePath, fs.constants.R_OK); // Read file content const content = await fs.readFile(filePath, 'utf-8'); // Basic validation - ensure file is not empty and contains expected content 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}`); } // Wait before retry with exponential backoff await new Promise(resolve => setTimeout(resolve, retryDelayMs * attempt)); } } throw new Error('Unexpected end of retry loop'); } /** * Get cache statistics for debugging and monitoring * @param outputDir Base output directory (defaults to VIBE_CODER_OUTPUT_DIR) * @returns Cache statistics object */ static async getCacheStats(outputDir?: string): Promise<{ totalCodemaps: number; oldestTimestamp: Date | null; newestTimestamp: Date | null; totalSizeBytes: number; averageAgeMinutes: number; }> { 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: Date | null = null; let newestTimestamp: Date | null = 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 }; } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/freshtechbro/vibe-coder-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server