spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: src/memory/store.ts4 * Description: Project memory store — .khaelor/MEMORY.md with event-anchored provenance comments (v2 design §5).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910/** Relative path of the project memory file. */11export const MEMORY_FILE = ".khaelor/MEMORY.md";1213export type MemoryConfidence = "high" | "medium" | "low";1415export interface MemoryProvenance {16 session: string;17 /** Anchor into the session log — the tool_use id of the remember call. */18 tool: string;19 confidence: MemoryConfidence;20 /** ISO date (YYYY-MM-DD). */21 date: string;22}2324export interface MemoryEntry {25 section: string;26 text: string;27 provenance: MemoryProvenance | null;28}2930const HEADER = "# Project memory\n\n> Maintained by KHAELOR. Every entry is anchored to the session/event that produced it.\n";3132const PROVENANCE_RE =33 /<!--\s*khaelor:\s*session=(\S+)\s+tool=(\S+)\s+confidence=(high|medium|low)\s+date=(\S+)\s*-->/;3435/** Canonical section title casing: "conventions" → "Conventions". */36function sectionTitle(section: string): string {37 const trimmed = section.trim();38 if (trimmed.length === 0) return "Notes";39 return trimmed.charAt(0).toUpperCase() + trimmed.slice(1);40}4142/** Parse MEMORY.md into entries. Tolerant: unknown lines are ignored. */43export function parseMemory(content: string): MemoryEntry[] {44 const entries: MemoryEntry[] = [];45 let section = "Notes";46 let currentText: string[] = [];47 let currentProvenance: MemoryProvenance | null = null;4849 const flush = (): void => {50 const text = currentText.join("\n").trim();51 if (text.length > 0) entries.push({ section, text, provenance: currentProvenance });52 currentText = [];53 currentProvenance = null;54 };5556 for (const line of content.split("\n")) {57 const heading = /^##\s+(.+)$/.exec(line);58 if (heading !== null) {59 flush();60 section = (heading[1] as string).trim();61 continue;62 }63 const provenance = PROVENANCE_RE.exec(line);64 if (provenance !== null) {65 currentProvenance = {66 session: provenance[1] as string,67 tool: provenance[2] as string,68 confidence: provenance[3] as MemoryConfidence,69 date: provenance[4] as string,70 };71 flush();72 continue;73 }74 if (/^-\s+/.test(line) && currentText.length > 0) flush();75 if (line.trim().length > 0 && !line.startsWith("#") && !line.startsWith(">")) {76 currentText.push(line);77 }78 }79 flush();80 return entries;81}8283function formatProvenance(p: MemoryProvenance): string {84 return ` <!-- khaelor: session=${p.session} tool=${p.tool} confidence=${p.confidence} date=${p.date} -->`;85}8687/** Append one entry to existing MEMORY.md content, creating the section when needed. */88export function appendMemoryEntry(89 existing: string | null,90 entry: { section: string; text: string; provenance: MemoryProvenance },91): string {92 const title = sectionTitle(entry.section);93 const bullet = entry.text.startsWith("- ") ? entry.text : `- ${entry.text}`;94 const block = `${bullet}\n${formatProvenance(entry.provenance)}\n`;9596 const base = existing !== null && existing.trim().length > 0 ? existing : HEADER;97 const lines = base.split("\n");98 const headingLine = `## ${title}`;99 const headingIndex = lines.findIndex((line) => line.trim() === headingLine);100101 if (headingIndex === -1) {102 const trimmed = base.replace(/\n+$/, "");103 return `${trimmed}\n\n${headingLine}\n${block}`;104 }105106 // Insert at the end of the section (before the next heading or EOF).107 let insertAt = lines.length;108 for (let i = headingIndex + 1; i < lines.length; i++) {109 if (/^##\s+/.test(lines[i] as string)) {110 insertAt = i;111 break;112 }113 }114 while (insertAt > headingIndex + 1 && (lines[insertAt - 1] as string).trim().length === 0) {115 insertAt -= 1;116 }117 const out = [...lines.slice(0, insertAt), ...block.replace(/\n$/, "").split("\n"), ...lines.slice(insertAt)];118 return out.join("\n");119}120121/**122 * Hygiene report: low-confidence entries are purge candidates the agent may123 * propose to drop during /compact (v2 §5.4).124 */125export function purgeCandidates(entries: readonly MemoryEntry[]): MemoryEntry[] {126 return entries.filter((entry) => entry.provenance?.confidence === "low");127}128