Project Files
src / memory / store.ts
import { randomUUID } from "crypto";
import { existsSync } from "fs";
import fs from "fs/promises";
import path from "path";
export interface MemoryRecord {
id: string;
timestamp: string;
kind: string;
content: string;
meta: Record<string, unknown>;
}
export class MemoryStore {
private readonly folder: string;
private readonly file: string;
constructor(private readonly root: string) {
this.folder = path.join(root, ".forge-memory");
this.file = path.join(this.folder, "memory.jsonl");
}
private async ensureReady() {
await fs.mkdir(this.folder, { recursive: true });
}
async add(
kind: string,
content: string,
meta: Record<string, unknown> = {}
) {
await this.ensureReady();
const record: MemoryRecord = {
id: randomUUID(),
timestamp: new Date().toISOString(),
kind,
content,
meta,
};
await fs.appendFile(this.file, JSON.stringify(record) + "\n", "utf8");
return record;
}
async list(limit = 100): Promise<MemoryRecord[]> {
if (!existsSync(this.file)) {
return [];
}
const raw = await fs.readFile(this.file, "utf8");
return raw
.split(/\r?\n/)
.filter(Boolean)
.map((line) => JSON.parse(line) as MemoryRecord)
.slice(-limit)
.reverse();
}
async search(query: string, limit = 8): Promise<MemoryRecord[]> {
const items = await this.list(1000);
if (!query.trim()) {
return items.slice(0, limit);
}
const terms = query
.toLowerCase()
.split(/\s+/)
.filter(Boolean);
const scored = items
.map((record) => {
const blob = JSON.stringify(record).toLowerCase();
let score = 0;
for (const term of terms) {
if (blob.includes(term)) score += 1;
}
return { record, score };
})
.filter((entry) => entry.score > 0)
.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
return b.record.timestamp.localeCompare(a.record.timestamp);
});
return scored.slice(0, limit).map((entry) => entry.record);
}
async summary(limit = 8) {
const items = await this.list(limit);
if (!items.length) {
return "";
}
return items
.map((record, index) => {
const meta =
record.meta && Object.keys(record.meta).length > 0
? ` | meta=${JSON.stringify(record.meta)}`
: "";
return `${index + 1}. [${record.kind}] ${record.content}${meta}`;
})
.join("\n");
}
}
export async function readRecentMemorySummary(root: string, query: string) {
const memory = new MemoryStore(root);
const items = await memory.search(query, 8);
if (!items.length) {
return "";
}
return items
.map((record, index) => `${index + 1}. [${record.kind}] ${record.content}`)
.join("\n");
}