UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

227 lines 6.86 kB
/** * Cache manager for research API responses * * @module research/cache/manager */ import { promises as fs } from 'fs'; import { join, dirname } from 'path'; import { createHash } from 'crypto'; import { ResearchError, ResearchErrorCode, } from '../types.js'; /** * Cache manager for file-based caching */ export class CacheManager { config; constructor(config) { this.config = { cacheDir: '.aiwg/research/cache', defaultTtl: 86400, // 24 hours ttlByEndpoint: { 'semantic-scholar': 604800, // 7 days 'crossref': 604800, // 7 days 'arxiv': 2592000, // 30 days (preprints don't change) 'unpaywall': 86400, // 24 hours }, ...config, }; } /** * Get cached data */ async get(key) { try { const filePath = this.getFilePath(key); const data = await fs.readFile(filePath, 'utf-8'); const entry = JSON.parse(data); // Check if expired if (this.isExpired(entry)) { await this.delete(key); return null; } return entry.data; } catch (error) { if (error.code === 'ENOENT') { return null; // Cache miss } throw new ResearchError(ResearchErrorCode.RF_400, 'Cache read error', error); } } /** * Set cached data */ async set(key, data, endpoint) { try { const ttl = endpoint ? this.config.ttlByEndpoint?.[endpoint] || this.config.defaultTtl : this.config.defaultTtl; const entry = { data, cachedAt: new Date().toISOString(), ttl, key, }; const filePath = this.getFilePath(key); await this.ensureDir(dirname(filePath)); await fs.writeFile(filePath, JSON.stringify(entry, null, 2), 'utf-8'); } catch (error) { throw new ResearchError(ResearchErrorCode.RF_401, 'Cache write error', error); } } /** * Delete cached data */ async delete(key) { try { const filePath = this.getFilePath(key); await fs.unlink(filePath); } catch (error) { if (error.code !== 'ENOENT') { throw new ResearchError(ResearchErrorCode.RF_402, 'Cache deletion error', error); } } } /** * Clear all cache */ async clear() { try { const files = await this.listCacheFiles(); await Promise.all(files.map((file) => fs.unlink(file))); } catch (error) { throw new ResearchError(ResearchErrorCode.RF_402, 'Cache clear error', error); } } /** * Clear expired entries */ async clearExpired() { try { const files = await this.listCacheFiles(); let cleared = 0; for (const file of files) { try { const data = await fs.readFile(file, 'utf-8'); const entry = JSON.parse(data); if (this.isExpired(entry)) { await fs.unlink(file); cleared++; } } catch { // Skip invalid files } } return cleared; } catch (error) { throw new ResearchError(ResearchErrorCode.RF_402, 'Cache cleanup error', error); } } /** * Get cache statistics */ async getStats() { try { const files = await this.listCacheFiles(); let expiredEntries = 0; let sizeBytes = 0; for (const file of files) { try { const stats = await fs.stat(file); sizeBytes += stats.size; const data = await fs.readFile(file, 'utf-8'); const entry = JSON.parse(data); if (this.isExpired(entry)) { expiredEntries++; } } catch { // Skip invalid files } } return { totalEntries: files.length, expiredEntries, sizeBytes, }; } catch (error) { throw new ResearchError(ResearchErrorCode.RF_400, 'Cache stats error', error); } } /** * Generate cache key from input */ generateKey(endpoint, params) { // Sort keys to ensure consistent hashing const sortedKeys = Object.keys(params).sort(); const sortedParams = {}; for (const key of sortedKeys) { sortedParams[key] = params[key]; } const normalized = JSON.stringify({ endpoint, params: sortedParams }); return createHash('sha256').update(normalized).digest('hex'); } /** * Check if cache entry is expired */ isExpired(entry) { const cachedTime = new Date(entry.cachedAt).getTime(); const expiryTime = cachedTime + entry.ttl * 1000; return Date.now() > expiryTime; } /** * Get file path for cache key */ getFilePath(key) { // Use first 2 chars for subdirectory to avoid too many files in one dir const subdir = key.substring(0, 2); return join(this.config.cacheDir, subdir, `${key}.json`); } /** * Ensure directory exists */ async ensureDir(dir) { try { await fs.mkdir(dir, { recursive: true }); } catch (error) { if (error.code !== 'EEXIST') { throw error; } } } /** * List all cache files */ async listCacheFiles() { const files = []; try { const entries = await fs.readdir(this.config.cacheDir, { withFileTypes: true, }); for (const entry of entries) { const fullPath = join(this.config.cacheDir, entry.name); if (entry.isDirectory()) { const subFiles = await fs.readdir(fullPath); for (const file of subFiles) { if (file.endsWith('.json')) { files.push(join(fullPath, file)); } } } } } catch (error) { if (error.code !== 'ENOENT') { throw error; } } return files; } } //# sourceMappingURL=manager.js.map