Files
zesdex/apps/packages/infrastructure/src/persistence/cms/memory_repo.ts
T
asepharyanaandClaude Opus 5 a2c7e5d44c feat(rewrite): tambah infrastructure — LLM client + full file persistence
Sub-fase 3a & 3e infrastructure TypeScript (@zesdex/infrastructure):
- llm/provider.ts: LlmClient (OpenAI/Anthropic-compatible fetch + SSE,
  retry 10x exponential backoff + jitter, abort on 401/402/403,
  stream quota-aware tool-call delta accumulation)
- resolveApiKey: settings.api_keys → app_config.env_var → inline default
- utils: writeJsonAtomic (tmp+rename+fsync), slugify, writeOsc52,
  truncateChars, buildWorkspaceTree, runCommand
- persistence/cms: JsonSettingsRepository, JsonAppConfigRepository
  (Claude credential auto-detection dari ~/.claude/settings.json + env),
  JsonConversationRepository, JsonlEditLogRepository (append NDJSON),
  MarkdownMemoryRepository (YAML-ish frontmatter), FileRewindBlobRepository
- persistence/iam: FileSystemSessionRepository, FileSystemSessionLockRepository,
  FileSystemOAuthRepository
- Workspace @zesdex/infrastructure tsc clean, semua test domain+application
  tetap hijau (38 pass)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 22:50:38 +07:00

107 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<string, string> {
const map: Record<string, string> = {};
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<string[]> {
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<Memory> {
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<void> {
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<void> {
const file = memoryPath(memoryDir, name);
if (fs.existsSync(file)) fs.unlinkSync(file);
}
}