agents / memory.js
"use strict";
/**
* Persistent memory — JSON file stored in the workspace.
*
* Each memory entry has a key, content, optional tags, and timestamps.
* Search is keyword-based (fast, no external deps, works offline).
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.memorySave = memorySave;
exports.memoryRecall = memoryRecall;
exports.memoryList = memoryList;
exports.memoryDelete = memoryDelete;
const promises_1 = require("fs/promises");
const path_1 = require("path");
function memoryPath(workspace) {
return (0, path_1.join)(workspace, ".agent_memory", "memory.json");
}
async function load(workspace) {
try {
return JSON.parse(await (0, promises_1.readFile)(memoryPath(workspace), "utf-8"));
}
catch {
return { version: 1, entries: [] };
}
}
async function save(workspace, store) {
const p = memoryPath(workspace);
await (0, promises_1.mkdir)((0, path_1.dirname)(p), { recursive: true });
await (0, promises_1.writeFile)(p, JSON.stringify(store, null, 2), "utf-8");
}
function uid() {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
}
function score(entry, query) {
const q = query.toLowerCase();
const haystack = `${entry.key} ${entry.content} ${entry.tags.join(" ")}`.toLowerCase();
// Count how many query words appear in the haystack
const words = q.split(/\s+/).filter(Boolean);
const hits = words.filter(w => haystack.includes(w)).length;
return words.length > 0 ? hits / words.length : 0;
}
// ── Public API ────────────────────────────────────────────────────────────
async function memorySave(workspace, key, content, tags) {
const store = await load(workspace);
const existing = store.entries.find(e => e.key.toLowerCase() === key.toLowerCase());
const now = new Date().toISOString();
if (existing) {
existing.content = content;
existing.tags = tags;
existing.updated = now;
await save(workspace, store);
return `Updated memory: "${key}" (id: ${existing.id})`;
}
const entry = { id: uid(), key, content, tags, created: now, updated: now };
store.entries.push(entry);
await save(workspace, store);
return `Saved memory: "${key}" (id: ${entry.id})`;
}
async function memoryRecall(workspace, query, limit, tags) {
const store = await load(workspace);
let results = store.entries;
// Filter by tags if specified
if (tags.length > 0) {
results = results.filter(e => tags.some(t => e.tags.map(x => x.toLowerCase()).includes(t.toLowerCase())));
}
// Score and sort
const scored = results
.map(e => ({ entry: e, score: score(e, query) }))
.filter(x => query.trim() === "" || x.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, limit);
if (scored.length === 0)
return "No memories found matching the query.";
return JSON.stringify(scored.map(({ entry, score: s }) => ({
id: entry.id,
key: entry.key,
content: entry.content,
tags: entry.tags,
relevance: Math.round(s * 100) + "%",
updated: entry.updated,
})), null, 2);
}
async function memoryList(workspace, tags) {
const store = await load(workspace);
let results = store.entries;
if (tags.length > 0) {
results = results.filter(e => tags.some(t => e.tags.map(x => x.toLowerCase()).includes(t.toLowerCase())));
}
if (results.length === 0)
return "Memory is empty.";
return JSON.stringify(results.map(e => ({ id: e.id, key: e.key, tags: e.tags, updated: e.updated })), null, 2);
}
async function memoryDelete(workspace, idOrKey) {
const store = await load(workspace);
const before = store.entries.length;
store.entries = store.entries.filter(e => e.id !== idOrKey && e.key.toLowerCase() !== idOrKey.toLowerCase());
if (store.entries.length === before)
return `No memory found with id or key: "${idOrKey}"`;
await save(workspace, store);
return `Deleted ${before - store.entries.length} memory entry.`;
}