/** * Markdown file–backed `MemoryRepository`. Each memory is a `.md` file with * YAML-ish frontmatter. Mirrors `apps/infrastructure/src/persistence/cms/memory_repo.rs`. */ import * as fs from "node:fs"; import * as path from "node:path"; import { type Memory, type MemoryRepository, memoryPath } from "@zesdex/domain"; import { randomUUID } from "node:crypto"; /** Escape newlines so they do not break line-oriented frontmatter. */ function escapeNewlines(s: string): string { return s.replace(/\n/g, "\\n"); } function unescapeNewlines(s: string): string { return s.replace(/\\n/g, "\n"); } function buildFrontmatter(memory: Memory): string { const outcome = memory.outcome ? `outcome: ${escapeNewlines(memory.outcome)}\n` : ""; const scope = memory.scope ? `scope: ${escapeNewlines(memory.scope)}\n` : ""; const before = memory.before_snippet ? `before: ${escapeNewlines(memory.before_snippet)}\n` : ""; const after = memory.after_snippet ? `after: ${escapeNewlines(memory.after_snippet)}\n` : ""; const prov = memory.provenances.length > 0 ? `provenances: ${memory.provenances.join(", ")}\n` : ""; return `name: ${memory.name}\ndescription: ${memory.description}\nkind: ${memory.kind}\ncreated_at: ${memory.created_at}\nupdated_at: ${memory.updated_at}\nlifecycle: ${memory.lifecycle}\n${outcome}${scope}${before}${after}${prov}`; } function parseFrontmatter(front: string): Record { const map: Record = {}; for (const line of front.split("\n")) { const idx = line.indexOf(":"); if (idx === -1) continue; map[line.slice(0, idx).trim()] = line.slice(idx + 1).trim(); } return map; } function parseMemory(content: string): Memory { let body = content; if (body.startsWith("---\n")) body = body.slice(4); const parts = body.split("\n---\n", 2); const frontPart = parts[0]; const mdPart = parts[1]; if (frontPart === undefined || mdPart === undefined) throw new Error("missing frontmatter"); const front = parseFrontmatter(frontPart); const md = mdPart.trim(); const prov = front.provenances ?? ""; return { name: front.name ?? "", description: front.description ?? "", content: md, kind: front.kind ?? "reference", created_at: Number(front.created_at) || 0, updated_at: Number(front.updated_at) || 0, outcome: front.outcome ? unescapeNewlines(front.outcome) : undefined, lifecycle: front.lifecycle ?? "new", scope: front.scope ? unescapeNewlines(front.scope) : undefined, before_snippet: front.before ? unescapeNewlines(front.before) : undefined, after_snippet: front.after ? unescapeNewlines(front.after) : undefined, provenances: prov === "" ? [] : prov.split(", "), }; } /** File-based `MemoryRepository` storing memories as `.md` files. */ export class MarkdownMemoryRepository implements MemoryRepository { async list(memoryDir: string): Promise { let entries: string[]; try { entries = fs.readdirSync(memoryDir); } catch { return []; } return entries .filter((n) => n.endsWith(".md") && n !== "MEMORY.md") .map((n) => n.slice(0, -3)); } async load(memoryDir: string, name: string): Promise { const file = memoryPath(memoryDir, name); const content = fs.readFileSync(file, "utf8"); try { return parseMemory(content); } catch (e) { throw new Error(`failed to parse memory '${name}': ${(e as Error).message}`); } } async save(memoryDir: string, memory: Memory): Promise { const file = memoryPath(memoryDir, memory.name); fs.mkdirSync(path.dirname(file), { recursive: true }); const content = `---\n${buildFrontmatter(memory)}---\n\n${memory.content}`; const tmp = path.join(path.dirname(file), `.${randomUUID()}.tmp`); const fd = fs.openSync(tmp, "w"); try { fs.writeFileSync(fd, content); fs.fsyncSync(fd); } finally { fs.closeSync(fd); } fs.renameSync(tmp, file); } async delete(memoryDir: string, name: string): Promise { const file = memoryPath(memoryDir, name); if (fs.existsSync(file)) fs.unlinkSync(file); } }