/** * SOUL File Reader — reads/writes .soul.md files from agent-core/soul/ * with 30s TTL cache and backup support. */ import fs from 'fs/promises' import path from 'path' const AGENT_CORE_PATH = process.env.AGENT_CORE_PATH || path.join(process.cwd(), 'agent-core') const SOUL_DIR = path.join(AGENT_CORE_PATH, 'soul') const BACKUPS_DIR = path.join(SOUL_DIR, '.backups') // 30s TTL cache const cache = new Map() const CACHE_TTL = 30_000 function getSoulFilePath(agentId: string): string { // Prevent path traversal const safe = agentId.replace(/[^a-z0-9-]/g, '') return path.join(SOUL_DIR, `${safe}.soul.md`) } /** * Read a SOUL file with 30s cache */ export async function readSoulFile(agentId: string): Promise { const cached = cache.get(agentId) if (cached && Date.now() - cached.timestamp < CACHE_TTL) { return cached.content } try { const filePath = getSoulFilePath(agentId) const content = await fs.readFile(filePath, 'utf-8') cache.set(agentId, { content, timestamp: Date.now() }) return content } catch { return null } } /** * Write a SOUL file — creates backup first, then writes new content */ export async function writeSoulFile(agentId: string, content: string): Promise { const filePath = getSoulFilePath(agentId) // Ensure backups dir exists await fs.mkdir(BACKUPS_DIR, { recursive: true }) // Create backup of current file try { const current = await fs.readFile(filePath, 'utf-8') const backupName = `${agentId}-${Date.now()}.soul.md` await fs.writeFile(path.join(BACKUPS_DIR, backupName), current, 'utf-8') } catch { // No existing file to backup } // Write new content await fs.writeFile(filePath, content, 'utf-8') // Invalidate cache cache.delete(agentId) } /** * List backup versions for an agent */ export async function listSoulBackups(agentId: string): Promise> { try { const files = await fs.readdir(BACKUPS_DIR) const prefix = `${agentId}-` const backups: Array<{ filename: string; timestamp: number; size: number }> = [] for (const file of files) { if (!file.startsWith(prefix) || !file.endsWith('.soul.md')) continue const tsStr = file.slice(prefix.length, -'.soul.md'.length) const ts = parseInt(tsStr, 10) if (isNaN(ts)) continue const stat = await fs.stat(path.join(BACKUPS_DIR, file)) backups.push({ filename: file, timestamp: ts, size: stat.size }) } return backups.sort((a, b) => b.timestamp - a.timestamp) } catch { return [] } } /** * Read a specific backup file */ export async function readSoulBackup(filename: string): Promise { try { // Prevent path traversal const safe = path.basename(filename) return await fs.readFile(path.join(BACKUPS_DIR, safe), 'utf-8') } catch { return null } } /** * Check if SOUL file exists */ export async function soulFileExists(agentId: string): Promise { try { await fs.access(getSoulFilePath(agentId)) return true } catch { return false } } /** * Get SOUL file stats */ export async function getSoulFileStats(agentId: string): Promise<{ size: number; createdAt: string; updatedAt: string } | null> { try { const stat = await fs.stat(getSoulFilePath(agentId)) return { size: stat.size, createdAt: stat.birthtime.toISOString(), updatedAt: stat.mtime.toISOString(), } } catch { return null } }