UNPKG

typeref-mcp

Version:

TypeScript type inference and symbol navigation MCP server for Claude Code

164 lines 7.14 kB
import * as fs from 'fs/promises'; import * as path from 'path'; export class DiskCache { logger; CACHE_VERSION = '1.0.0'; CACHE_DIR = '.typeref'; constructor(logger) { this.logger = logger; } getCacheFilePath(projectPath) { const projectHash = this.hashPath(projectPath); return path.join(projectPath, this.CACHE_DIR, `${projectHash}.json`); } getMetadataFilePath(projectPath) { const projectHash = this.hashPath(projectPath); return path.join(projectPath, this.CACHE_DIR, `${projectHash}.meta.json`); } hashPath(projectPath) { return Buffer.from(projectPath).toString('base64').replace(/[/+=]/g, '_').substring(0, 16); } async getFileHashes(projectPath) { const fileHashes = new Map(); try { const files = await this.findTypeScriptFiles(projectPath); for (const filePath of files) { try { const stats = await fs.stat(filePath); fileHashes.set(filePath, stats.mtime.toISOString()); } catch (error) { this.logger.debug(`Could not stat file ${filePath}: ${error}`); } } } catch (error) { this.logger.warn(`Failed to get file hashes for ${projectPath}: ${error}`); } return fileHashes; } async findTypeScriptFiles(projectPath) { const files = []; const excludeDirs = ['node_modules', 'dist', 'build', '.git', 'coverage']; const scanDir = async (dirPath) => { try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { if (!excludeDirs.includes(entry.name)) { await scanDir(fullPath); } } else if (entry.isFile() && /\.(ts|tsx)$/.test(entry.name)) { files.push(fullPath); } } } catch (error) { this.logger.debug(`Could not scan directory ${dirPath}: ${error}`); } }; await scanDir(projectPath); return files; } async isCacheValid(projectPath) { try { const metadataPath = this.getMetadataFilePath(projectPath); const cacheFilePath = this.getCacheFilePath(projectPath); await fs.access(metadataPath); await fs.access(cacheFilePath); const metadataContent = await fs.readFile(metadataPath, 'utf8'); const metadata = JSON.parse(metadataContent); if (metadata.version !== this.CACHE_VERSION) { this.logger.info(`Cache version mismatch for ${projectPath}, invalidating`); return false; } const currentFileHashes = await this.getFileHashes(projectPath); const cachedFileHashes = new Map(Object.entries(metadata.fileHashes)); if (currentFileHashes.size !== cachedFileHashes.size) { this.logger.info(`File count changed for ${projectPath} (${cachedFileHashes.size}${currentFileHashes.size}), invalidating cache`); return false; } for (const [filePath, currentHash] of currentFileHashes) { const cachedHash = cachedFileHashes.get(filePath); if (!cachedHash || cachedHash !== currentHash) { this.logger.info(`File modified: ${filePath}, invalidating cache`); return false; } } this.logger.info(`Cache is valid for ${projectPath} (${currentFileHashes.size} files)`); return true; } catch (error) { this.logger.debug(`Cache validation failed for ${projectPath}: ${error}`); return false; } } async loadProjectIndex(projectPath) { try { const cacheFilePath = this.getCacheFilePath(projectPath); const content = await fs.readFile(cacheFilePath, 'utf8'); const data = JSON.parse(content); const index = { projectPath: data.projectPath, symbols: new Map(Object.entries(data.symbols)), types: new Map(Object.entries(data.types)), modules: new Map(Object.entries(data.modules)), dependencies: new Map(Object.entries(data.dependencies)), lastIndexed: new Date(data.lastIndexed), }; this.logger.info(`Loaded cached index for ${projectPath}: ${index.symbols.size} symbols, ${index.types.size} types`); return index; } catch (error) { this.logger.debug(`Failed to load cached index for ${projectPath}: ${error}`); return null; } } async saveProjectIndex(index) { try { const projectPath = index.projectPath; const cacheDir = path.join(projectPath, this.CACHE_DIR); await fs.mkdir(cacheDir, { recursive: true }); const cacheFilePath = this.getCacheFilePath(projectPath); const metadataPath = this.getMetadataFilePath(projectPath); const serializable = { projectPath: index.projectPath, symbols: Object.fromEntries(index.symbols), types: Object.fromEntries(index.types), modules: Object.fromEntries(index.modules), dependencies: Object.fromEntries(index.dependencies), lastIndexed: index.lastIndexed.toISOString(), }; await fs.writeFile(cacheFilePath, JSON.stringify(serializable, null, 2)); const fileHashes = await this.getFileHashes(projectPath); const metadata = { projectPath, lastIndexed: index.lastIndexed, fileCount: fileHashes.size, fileHashes, version: this.CACHE_VERSION, }; await fs.writeFile(metadataPath, JSON.stringify({ ...metadata, fileHashes: Object.fromEntries(metadata.fileHashes) }, null, 2)); this.logger.info(`Saved index cache for ${projectPath}: ${index.symbols.size} symbols, ${index.types.size} types`); } catch (error) { this.logger.error(`Failed to save index cache for ${index.projectPath}: ${error}`); } } async clearProjectCache(projectPath) { try { const cacheDir = path.join(projectPath, this.CACHE_DIR); await fs.rm(cacheDir, { recursive: true, force: true }); this.logger.info(`Cleared cache for ${projectPath}`); } catch (error) { this.logger.debug(`Failed to clear cache for ${projectPath}: ${error}`); } } } //# sourceMappingURL=DiskCache.js.map