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

170 lines 6.31 kB
/** * RLM result cache filesystem store. * * Layout: * .aiwg/working/rlm-cache/{hash}/ * ├── result.json * ├── manifest.json * └── metadata.json * * @implements #1203 * @see .aiwg/architecture/adr-rlm-index-features-impl-plan.md */ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from 'node:fs'; import { join } from 'node:path'; const CACHE_ROOT_DEFAULT = '.aiwg/working/rlm-cache'; const HASH_RE = /^[0-9a-f]{64}$/; /** Resolve cache root, allowing tests to override. */ export function resolveCacheRoot(cwd = process.cwd()) { return join(cwd, CACHE_ROOT_DEFAULT); } /** Compute the directory for a single entry. */ function entryDir(root, hash) { if (!HASH_RE.test(hash)) { throw new Error(`Invalid cache hash: ${hash}`); } return join(root, hash); } /** Check whether a cache entry exists. */ export function has(root, hash) { return existsSync(join(entryDir(root, hash), 'result.json')); } /** Read a full entry from disk. Throws on missing. */ export function get(root, hash) { const dir = entryDir(root, hash); if (!existsSync(dir)) { throw new Error(`Cache miss: ${hash}`); } const result = JSON.parse(readFileSync(join(dir, 'result.json'), 'utf-8')); const manifest = JSON.parse(readFileSync(join(dir, 'manifest.json'), 'utf-8')); const metadata = JSON.parse(readFileSync(join(dir, 'metadata.json'), 'utf-8')); return { hash, result, manifest, metadata }; } /** Write an entry, atomically per-file. Returns the entry directory. */ export function put(root, entry) { const dir = entryDir(root, entry.hash); mkdirSync(dir, { recursive: true }); writeFileSync(join(dir, 'result.json'), JSON.stringify(entry.result, null, 2), 'utf-8'); writeFileSync(join(dir, 'manifest.json'), JSON.stringify(entry.manifest, null, 2), 'utf-8'); writeFileSync(join(dir, 'metadata.json'), JSON.stringify(entry.metadata, null, 2), 'utf-8'); return dir; } /** List all entries (compact summary form). */ export function list(root, now = new Date()) { if (!existsSync(root)) return []; const out = []; for (const name of readdirSync(root)) { if (!HASH_RE.test(name)) continue; const metaPath = join(root, name, 'metadata.json'); if (!existsSync(metaPath)) continue; let meta; try { meta = JSON.parse(readFileSync(metaPath, 'utf-8')); } catch { continue; } const manifestPath = join(root, name, 'manifest.json'); let inputCount = 0; if (existsSync(manifestPath)) { try { const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')); inputCount = manifest.inputs.length; } catch { /* ignore */ } } const ageMs = now.getTime() - new Date(meta.createdAt).getTime(); const ageDays = Math.max(0, Math.floor(ageMs / 86_400_000)); out.push({ hash: name, model: meta.model, query: meta.query.length > 80 ? meta.query.slice(0, 77) + '...' : meta.query, createdAt: meta.createdAt, ageDays, inputCount, costUsd: typeof meta.costUsd === 'number' ? meta.costUsd : null, }); } return out.sort((a, b) => a.createdAt.localeCompare(b.createdAt)); } /** Aggregate statistics. */ export function stats(root, now = new Date()) { const entries = list(root, now); if (entries.length === 0) { return { totalEntries: 0, totalSizeKb: 0, oldestAgeDays: null, newestAgeDays: null, totalCostUsd: 0 }; } let totalBytes = 0; for (const e of entries) { const dir = entryDir(root, e.hash); for (const f of ['result.json', 'manifest.json', 'metadata.json']) { const p = join(dir, f); if (existsSync(p)) totalBytes += statSync(p).size; } } const ages = entries.map((e) => e.ageDays); const totalCostUsd = entries.reduce((sum, e) => sum + (e.costUsd ?? 0), 0); return { totalEntries: entries.length, totalSizeKb: Math.round(totalBytes / 1024), oldestAgeDays: Math.max(...ages), newestAgeDays: Math.min(...ages), totalCostUsd, }; } /** Remove entries matching the given options. */ export function evict(root, opts, now = new Date()) { if (!existsSync(root)) { return { evictedCount: 0, evictedBytes: 0, hashes: [] }; } const targets = []; if (opts.hash) { if (existsSync(entryDir(root, opts.hash))) targets.push(opts.hash); } else if (typeof opts.olderThanDays === 'number') { const cutoff = now.getTime() - opts.olderThanDays * 86_400_000; for (const e of list(root, now)) { if (new Date(e.createdAt).getTime() < cutoff) targets.push(e.hash); } } else { throw new Error('evict() requires either {hash} or {olderThanDays}'); } let evictedBytes = 0; for (const h of targets) { const dir = entryDir(root, h); if (existsSync(dir)) { for (const f of ['result.json', 'manifest.json', 'metadata.json']) { const p = join(dir, f); if (existsSync(p)) evictedBytes += statSync(p).size; } rmSync(dir, { recursive: true, force: true }); } } return { evictedCount: targets.length, evictedBytes, hashes: targets }; } /** Wipe the entire cache. Caller is responsible for confirmation. */ export function clear(root) { if (!existsSync(root)) { return { evictedCount: 0, evictedBytes: 0, hashes: [] }; } const before = list(root); let evictedBytes = 0; for (const e of before) { const dir = entryDir(root, e.hash); for (const f of ['result.json', 'manifest.json', 'metadata.json']) { const p = join(dir, f); if (existsSync(p)) evictedBytes += statSync(p).size; } } rmSync(root, { recursive: true, force: true }); return { evictedCount: before.length, evictedBytes, hashes: before.map((e) => e.hash) }; } //# sourceMappingURL=store.js.map