local-agentic-ai-mem 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -0
- package/dist/commands/import.js +232 -0
- package/dist/commands/install.js +112 -0
- package/dist/commands/status.js +29 -0
- package/dist/commands/uninstall-legacy.js +159 -0
- package/dist/db.js +359 -0
- package/dist/index.js +58 -0
- package/dist/lib/compliance.js +169 -0
- package/dist/lib/compose.js +130 -0
- package/dist/lib/conventions.js +78 -0
- package/dist/lib/embed.js +47 -0
- package/dist/lib/extract.js +297 -0
- package/dist/lib/maintain.js +81 -0
- package/dist/lib/recall.js +152 -0
- package/dist/lib/redact.js +64 -0
- package/dist/lib/tiers.js +405 -0
- package/dist/mcp/server.js +238 -0
- package/package.json +42 -0
- package/templates/hooks/post-tool-use.mjs +29 -0
- package/templates/hooks/session-start.mjs +72 -0
- package/templates/hooks/stop.mjs +113 -0
- package/templates/hooks/user-prompt-submit.mjs +72 -0
package/dist/db.js
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DIMS = void 0;
|
|
7
|
+
exports.resolveDbPath = resolveDbPath;
|
|
8
|
+
exports.db = db;
|
|
9
|
+
exports.closeDb = closeDb;
|
|
10
|
+
exports.encodeVec = encodeVec;
|
|
11
|
+
exports.decodeVec = decodeVec;
|
|
12
|
+
exports.cosine = cosine;
|
|
13
|
+
exports.upsertProject = upsertProject;
|
|
14
|
+
exports.insertMemory = insertMemory;
|
|
15
|
+
exports.liveMemories = liveMemories;
|
|
16
|
+
exports.lexicalSearch = lexicalSearch;
|
|
17
|
+
exports.markRecalled = markRecalled;
|
|
18
|
+
exports.replaceFacts = replaceFacts;
|
|
19
|
+
exports.getFacts = getFacts;
|
|
20
|
+
exports.setMeta = setMeta;
|
|
21
|
+
exports.getMeta = getMeta;
|
|
22
|
+
exports.recordInjection = recordInjection;
|
|
23
|
+
exports.openInjections = openInjections;
|
|
24
|
+
exports.resolveInjection = resolveInjection;
|
|
25
|
+
exports.complianceReport = complianceReport;
|
|
26
|
+
/**
|
|
27
|
+
* Local storage. No server, no account, no network.
|
|
28
|
+
*
|
|
29
|
+
* Everything lives in one SQLite file on the machine that produced it. That is
|
|
30
|
+
* not a downgrade from the hosted version — at this scale the server was doing
|
|
31
|
+
* nothing a local file cannot. A median project's whole vector index is 375 KB
|
|
32
|
+
* and a brute-force cosine scan over 2,000 vectors takes 1.7ms, so pgvector,
|
|
33
|
+
* HNSW and the entire indexing apparatus were solving a problem that does not
|
|
34
|
+
* exist for a single user.
|
|
35
|
+
*
|
|
36
|
+
* Vectors are stored as raw float32 BLOBs and scanned in JS. FTS5 handles the
|
|
37
|
+
* lexical arm, replacing the tsvector column.
|
|
38
|
+
*/
|
|
39
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
40
|
+
const fs_1 = require("fs");
|
|
41
|
+
const os_1 = require("os");
|
|
42
|
+
const path_1 = require("path");
|
|
43
|
+
exports.DIMS = 384;
|
|
44
|
+
// ── where the database lives ─────────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* A repo containing `.agentic-memory/` keeps its memory with the code, so it
|
|
47
|
+
* travels through git to other machines and to teammates. Otherwise memory is
|
|
48
|
+
* global to the user. Both are local files; neither leaves the machine.
|
|
49
|
+
*/
|
|
50
|
+
function resolveDbPath(cwd = process.cwd()) {
|
|
51
|
+
const override = process.env.AGENTIC_MEMORY_DB;
|
|
52
|
+
if (override)
|
|
53
|
+
return override;
|
|
54
|
+
let dir = cwd;
|
|
55
|
+
for (let i = 0; i < 12; i++) {
|
|
56
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(dir, ".agentic-memory"))) {
|
|
57
|
+
return (0, path_1.join)(dir, ".agentic-memory", "memory.db");
|
|
58
|
+
}
|
|
59
|
+
const parent = (0, path_1.dirname)(dir);
|
|
60
|
+
if (parent === dir)
|
|
61
|
+
break;
|
|
62
|
+
dir = parent;
|
|
63
|
+
}
|
|
64
|
+
return (0, path_1.join)((0, os_1.homedir)(), ".agentic-memory", "memory.db");
|
|
65
|
+
}
|
|
66
|
+
// ── schema ───────────────────────────────────────────────────────────────────
|
|
67
|
+
const SCHEMA = `
|
|
68
|
+
PRAGMA journal_mode = WAL;
|
|
69
|
+
PRAGMA synchronous = NORMAL;
|
|
70
|
+
|
|
71
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
72
|
+
id INTEGER PRIMARY KEY,
|
|
73
|
+
slug TEXT NOT NULL UNIQUE,
|
|
74
|
+
root TEXT,
|
|
75
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
79
|
+
id INTEGER PRIMARY KEY,
|
|
80
|
+
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
81
|
+
prompt TEXT NOT NULL,
|
|
82
|
+
content TEXT,
|
|
83
|
+
decisions TEXT NOT NULL DEFAULT '[]',
|
|
84
|
+
files TEXT NOT NULL DEFAULT '[]',
|
|
85
|
+
tool_calls TEXT NOT NULL DEFAULT '[]',
|
|
86
|
+
git_commit TEXT,
|
|
87
|
+
anchor_sha TEXT,
|
|
88
|
+
confidence TEXT NOT NULL DEFAULT 'stated',
|
|
89
|
+
tier INTEGER NOT NULL DEFAULT 3,
|
|
90
|
+
started_at TEXT NOT NULL,
|
|
91
|
+
recall_count INTEGER NOT NULL DEFAULT 0,
|
|
92
|
+
last_recalled TEXT,
|
|
93
|
+
archived_at TEXT,
|
|
94
|
+
superseded_at TEXT,
|
|
95
|
+
superseded_by INTEGER,
|
|
96
|
+
session_id TEXT,
|
|
97
|
+
vec BLOB,
|
|
98
|
+
prompt_vec BLOB
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
CREATE INDEX IF NOT EXISTS memories_live_idx
|
|
102
|
+
ON memories (project_id, tier)
|
|
103
|
+
WHERE archived_at IS NULL AND superseded_at IS NULL;
|
|
104
|
+
CREATE INDEX IF NOT EXISTS memories_session_idx ON memories (session_id);
|
|
105
|
+
|
|
106
|
+
-- Lexical arm. Embeddings are weak on opaque tokens — SHAs, error codes,
|
|
107
|
+
-- symbol names — which is exactly what a developer searches for.
|
|
108
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
109
|
+
content, prompt, decisions, files,
|
|
110
|
+
content='memories', content_rowid='id', tokenize='porter unicode61'
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
114
|
+
INSERT INTO memories_fts(rowid, content, prompt, decisions, files)
|
|
115
|
+
VALUES (new.id, new.content, new.prompt, new.decisions, new.files);
|
|
116
|
+
END;
|
|
117
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
118
|
+
INSERT INTO memories_fts(memories_fts, rowid, content, prompt, decisions, files)
|
|
119
|
+
VALUES ('delete', old.id, old.content, old.prompt, old.decisions, old.files);
|
|
120
|
+
END;
|
|
121
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
122
|
+
INSERT INTO memories_fts(memories_fts, rowid, content, prompt, decisions, files)
|
|
123
|
+
VALUES ('delete', old.id, old.content, old.prompt, old.decisions, old.files);
|
|
124
|
+
INSERT INTO memories_fts(rowid, content, prompt, decisions, files)
|
|
125
|
+
VALUES (new.id, new.content, new.prompt, new.decisions, new.files);
|
|
126
|
+
END;
|
|
127
|
+
|
|
128
|
+
-- Tier 1. Small, injected whole, never searched.
|
|
129
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
130
|
+
id INTEGER PRIMARY KEY,
|
|
131
|
+
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
132
|
+
kind TEXT NOT NULL,
|
|
133
|
+
fact TEXT NOT NULL,
|
|
134
|
+
evidence TEXT,
|
|
135
|
+
files TEXT NOT NULL DEFAULT '[]',
|
|
136
|
+
observations INTEGER NOT NULL DEFAULT 1,
|
|
137
|
+
confidence TEXT NOT NULL DEFAULT 'stated',
|
|
138
|
+
last_confirmed TEXT NOT NULL DEFAULT (datetime('now')),
|
|
139
|
+
superseded_at TEXT,
|
|
140
|
+
UNIQUE (project_id, kind, fact)
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* What was injected, and what the session then did about it.
|
|
145
|
+
*
|
|
146
|
+
* Every other measurement in this package is about retrieval — whether the
|
|
147
|
+
* right memory was surfaced. This is the only one about use. They are
|
|
148
|
+
* different questions and only the second decides whether any of it helps.
|
|
149
|
+
*/
|
|
150
|
+
CREATE TABLE IF NOT EXISTS injections (
|
|
151
|
+
id INTEGER PRIMARY KEY,
|
|
152
|
+
session_id TEXT NOT NULL,
|
|
153
|
+
project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE,
|
|
154
|
+
kind TEXT NOT NULL,
|
|
155
|
+
at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
156
|
+
memory_ids TEXT NOT NULL DEFAULT '[]',
|
|
157
|
+
files TEXT NOT NULL DEFAULT '[]',
|
|
158
|
+
tokens TEXT NOT NULL DEFAULT '[]',
|
|
159
|
+
pitfall_files TEXT NOT NULL DEFAULT '[]',
|
|
160
|
+
resolved_at TEXT,
|
|
161
|
+
files_named INTEGER,
|
|
162
|
+
files_reread INTEGER,
|
|
163
|
+
tokens_total INTEGER,
|
|
164
|
+
tokens_echoed INTEGER,
|
|
165
|
+
pitfall_touched INTEGER
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
CREATE INDEX IF NOT EXISTS injections_open_idx ON injections (session_id);
|
|
169
|
+
|
|
170
|
+
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
171
|
+
`;
|
|
172
|
+
let _db = null;
|
|
173
|
+
function db(cwd) {
|
|
174
|
+
if (_db)
|
|
175
|
+
return _db;
|
|
176
|
+
const path = resolveDbPath(cwd);
|
|
177
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(path), { recursive: true });
|
|
178
|
+
_db = new better_sqlite3_1.default(path);
|
|
179
|
+
_db.exec(SCHEMA);
|
|
180
|
+
return _db;
|
|
181
|
+
}
|
|
182
|
+
function closeDb() {
|
|
183
|
+
_db?.close();
|
|
184
|
+
_db = null;
|
|
185
|
+
}
|
|
186
|
+
// ── vectors ──────────────────────────────────────────────────────────────────
|
|
187
|
+
function encodeVec(v) {
|
|
188
|
+
const f = v instanceof Float32Array ? v : Float32Array.from(v);
|
|
189
|
+
return Buffer.from(f.buffer, f.byteOffset, f.byteLength);
|
|
190
|
+
}
|
|
191
|
+
function decodeVec(b) {
|
|
192
|
+
if (!b || b.byteLength < exports.DIMS * 4)
|
|
193
|
+
return null;
|
|
194
|
+
// Copy rather than view: better-sqlite3 buffers are not guaranteed aligned.
|
|
195
|
+
const out = new Float32Array(exports.DIMS);
|
|
196
|
+
for (let i = 0; i < exports.DIMS; i++)
|
|
197
|
+
out[i] = b.readFloatLE(i * 4);
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
function cosine(a, b) {
|
|
201
|
+
let dot = 0, na = 0, nb = 0;
|
|
202
|
+
for (let i = 0; i < a.length; i++) {
|
|
203
|
+
dot += a[i] * b[i];
|
|
204
|
+
na += a[i] * a[i];
|
|
205
|
+
nb += b[i] * b[i];
|
|
206
|
+
}
|
|
207
|
+
const d = Math.sqrt(na) * Math.sqrt(nb);
|
|
208
|
+
return d === 0 ? 0 : dot / d;
|
|
209
|
+
}
|
|
210
|
+
// ── projects ─────────────────────────────────────────────────────────────────
|
|
211
|
+
function upsertProject(slug, root) {
|
|
212
|
+
const conn = db();
|
|
213
|
+
conn.prepare("INSERT OR IGNORE INTO projects (slug, root) VALUES (?, ?)").run(slug, root ?? null);
|
|
214
|
+
const row = conn.prepare("SELECT id FROM projects WHERE slug = ?").get(slug);
|
|
215
|
+
return row.id;
|
|
216
|
+
}
|
|
217
|
+
function insertMemory(m) {
|
|
218
|
+
const info = db()
|
|
219
|
+
.prepare(`INSERT INTO memories
|
|
220
|
+
(project_id, prompt, content, decisions, files, tool_calls, git_commit,
|
|
221
|
+
anchor_sha, confidence, tier, started_at, session_id, vec, prompt_vec)
|
|
222
|
+
VALUES (@project_id, @prompt, @content, @decisions, @files, @tool_calls, @git_commit,
|
|
223
|
+
@anchor_sha, @confidence, @tier, @started_at, @session_id, @vec, @prompt_vec)`)
|
|
224
|
+
.run({
|
|
225
|
+
project_id: m.projectId,
|
|
226
|
+
prompt: m.prompt,
|
|
227
|
+
content: m.content,
|
|
228
|
+
decisions: JSON.stringify(m.decisions),
|
|
229
|
+
files: JSON.stringify(m.files),
|
|
230
|
+
tool_calls: JSON.stringify(m.toolCalls),
|
|
231
|
+
git_commit: m.gitCommit,
|
|
232
|
+
anchor_sha: m.anchorSha,
|
|
233
|
+
confidence: m.confidence,
|
|
234
|
+
tier: m.tier,
|
|
235
|
+
started_at: m.startedAt,
|
|
236
|
+
session_id: m.sessionId,
|
|
237
|
+
vec: encodeVec(m.vec),
|
|
238
|
+
prompt_vec: encodeVec(m.promptVec),
|
|
239
|
+
});
|
|
240
|
+
return Number(info.lastInsertRowid);
|
|
241
|
+
}
|
|
242
|
+
/** Live memories, optionally restricted to a tier. Vectors included. */
|
|
243
|
+
function liveMemories(opts = {}) {
|
|
244
|
+
const where = ["m.archived_at IS NULL", "m.superseded_at IS NULL"];
|
|
245
|
+
const params = [];
|
|
246
|
+
if (opts.projectId !== undefined) {
|
|
247
|
+
where.push("m.project_id = ?");
|
|
248
|
+
params.push(opts.projectId);
|
|
249
|
+
}
|
|
250
|
+
if (opts.tier !== undefined) {
|
|
251
|
+
where.push("m.tier <= ?");
|
|
252
|
+
params.push(opts.tier);
|
|
253
|
+
}
|
|
254
|
+
return db()
|
|
255
|
+
.prepare(`SELECT m.*, p.slug AS project_slug
|
|
256
|
+
FROM memories m JOIN projects p ON p.id = m.project_id
|
|
257
|
+
WHERE ${where.join(" AND ")}`)
|
|
258
|
+
.all(...params);
|
|
259
|
+
}
|
|
260
|
+
/** FTS5 lexical search. Returns ids ranked by bm25. */
|
|
261
|
+
function lexicalSearch(query, limit) {
|
|
262
|
+
// FTS5 treats punctuation as syntax; quote each term so arbitrary user text
|
|
263
|
+
// cannot produce a syntax error.
|
|
264
|
+
const terms = query
|
|
265
|
+
.toLowerCase()
|
|
266
|
+
.split(/[^a-z0-9_./-]+/)
|
|
267
|
+
.filter((t) => t.length > 2)
|
|
268
|
+
.slice(0, 12)
|
|
269
|
+
.map((t) => `"${t.replace(/"/g, "")}"`);
|
|
270
|
+
if (terms.length === 0)
|
|
271
|
+
return [];
|
|
272
|
+
try {
|
|
273
|
+
const rows = db()
|
|
274
|
+
.prepare(`SELECT rowid FROM memories_fts WHERE memories_fts MATCH ?
|
|
275
|
+
ORDER BY bm25(memories_fts) LIMIT ?`)
|
|
276
|
+
.all(terms.join(" OR "), limit);
|
|
277
|
+
return rows.map((r) => r.rowid);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function markRecalled(ids) {
|
|
284
|
+
if (ids.length === 0)
|
|
285
|
+
return;
|
|
286
|
+
const stmt = db().prepare("UPDATE memories SET recall_count = recall_count + 1, last_recalled = datetime('now') WHERE id = ?");
|
|
287
|
+
const tx = db().transaction((list) => list.forEach((id) => stmt.run(id)));
|
|
288
|
+
tx(ids);
|
|
289
|
+
}
|
|
290
|
+
function replaceFacts(projectId, facts) {
|
|
291
|
+
const conn = db();
|
|
292
|
+
const tx = conn.transaction(() => {
|
|
293
|
+
// Retire rather than delete: a fact that stops being derivable usually
|
|
294
|
+
// means the code moved past it, and that history is worth keeping.
|
|
295
|
+
conn.prepare("UPDATE facts SET superseded_at = datetime('now') WHERE project_id = ?").run(projectId);
|
|
296
|
+
const stmt = conn.prepare(`INSERT INTO facts (project_id, kind, fact, evidence, files, observations, confidence, superseded_at, last_confirmed)
|
|
297
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, NULL, datetime('now'))
|
|
298
|
+
ON CONFLICT (project_id, kind, fact) DO UPDATE SET
|
|
299
|
+
evidence = excluded.evidence,
|
|
300
|
+
files = excluded.files,
|
|
301
|
+
observations = excluded.observations,
|
|
302
|
+
confidence = excluded.confidence,
|
|
303
|
+
superseded_at = NULL,
|
|
304
|
+
last_confirmed = datetime('now')`);
|
|
305
|
+
for (const f of facts) {
|
|
306
|
+
stmt.run(projectId, f.kind, f.fact, f.evidence, JSON.stringify(f.files), f.observations, f.confidence);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
tx();
|
|
310
|
+
}
|
|
311
|
+
function getFacts(projectId, limit = 40) {
|
|
312
|
+
return db()
|
|
313
|
+
.prepare(`SELECT kind, fact, evidence, files, observations, confidence
|
|
314
|
+
FROM facts WHERE project_id = ? AND superseded_at IS NULL
|
|
315
|
+
ORDER BY observations DESC LIMIT ?`)
|
|
316
|
+
.all(projectId, limit);
|
|
317
|
+
}
|
|
318
|
+
function setMeta(key, value) {
|
|
319
|
+
db().prepare("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = ?").run(key, value, value);
|
|
320
|
+
}
|
|
321
|
+
function getMeta(key) {
|
|
322
|
+
const row = db().prepare("SELECT value FROM meta WHERE key = ?").get(key);
|
|
323
|
+
return row?.value ?? null;
|
|
324
|
+
}
|
|
325
|
+
function recordInjection(r) {
|
|
326
|
+
const info = db()
|
|
327
|
+
.prepare(`INSERT INTO injections (session_id, project_id, kind, memory_ids, files, tokens, pitfall_files)
|
|
328
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
329
|
+
.run(r.sessionId, r.projectId, r.kind, JSON.stringify(r.memoryIds), JSON.stringify(r.files), JSON.stringify(r.tokens), JSON.stringify(r.pitfallFiles));
|
|
330
|
+
return Number(info.lastInsertRowid);
|
|
331
|
+
}
|
|
332
|
+
function openInjections(sessionId) {
|
|
333
|
+
return db()
|
|
334
|
+
.prepare("SELECT id, kind, files, tokens, pitfall_files FROM injections WHERE session_id = ? AND resolved_at IS NULL")
|
|
335
|
+
.all(sessionId);
|
|
336
|
+
}
|
|
337
|
+
function resolveInjection(id, o) {
|
|
338
|
+
db()
|
|
339
|
+
.prepare(`UPDATE injections
|
|
340
|
+
SET resolved_at = datetime('now'), files_named = ?, files_reread = ?,
|
|
341
|
+
tokens_total = ?, tokens_echoed = ?, pitfall_touched = ?
|
|
342
|
+
WHERE id = ?`)
|
|
343
|
+
.run(o.filesNamed, o.filesReread, o.tokensTotal, o.tokensEchoed, o.pitfallTouched, id);
|
|
344
|
+
}
|
|
345
|
+
function complianceReport() {
|
|
346
|
+
const row = db()
|
|
347
|
+
.prepare(`SELECT
|
|
348
|
+
count(*) AS resolved,
|
|
349
|
+
sum(CASE WHEN tokens_echoed > 0 THEN 1 ELSE 0 END) AS echoed,
|
|
350
|
+
sum(CASE WHEN files_named > 0 AND files_reread = 0 THEN 1 ELSE 0 END) AS avoided_reread,
|
|
351
|
+
sum(CASE WHEN files_named > 0 THEN 1 ELSE 0 END) AS had_files,
|
|
352
|
+
sum(pitfall_touched) AS pitfall_touched,
|
|
353
|
+
sum(tokens_echoed) AS tokens_echoed,
|
|
354
|
+
sum(tokens_total) AS tokens_total
|
|
355
|
+
FROM injections WHERE resolved_at IS NOT NULL`)
|
|
356
|
+
.get();
|
|
357
|
+
const open = db().prepare("SELECT count(*) n FROM injections WHERE resolved_at IS NULL").get().n;
|
|
358
|
+
return { ...row, open };
|
|
359
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
/**
|
|
5
|
+
* agentic-memory — local-first memory for Claude Code.
|
|
6
|
+
*
|
|
7
|
+
* No server, no account, no API key. One SQLite file on the machine that
|
|
8
|
+
* produced it.
|
|
9
|
+
*/
|
|
10
|
+
const install_1 = require("./commands/install");
|
|
11
|
+
const uninstall_legacy_1 = require("./commands/uninstall-legacy");
|
|
12
|
+
function help() {
|
|
13
|
+
console.log(`
|
|
14
|
+
agentic-memory — local-first memory for Claude Code
|
|
15
|
+
|
|
16
|
+
install [--project] install hooks + MCP; --project stores memory in
|
|
17
|
+
./.agentic-memory/ so it travels through git
|
|
18
|
+
mcp run the MCP server on stdio (Claude Code calls this)
|
|
19
|
+
import <dump.sql> import a MemoryOS pg_dump into the local database
|
|
20
|
+
status what is stored, and what is left of any old install
|
|
21
|
+
purge-legacy remove a previous MemoryOS install and stop
|
|
22
|
+
|
|
23
|
+
Everything is local. Nothing is uploaded.
|
|
24
|
+
`);
|
|
25
|
+
}
|
|
26
|
+
async function main() {
|
|
27
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
28
|
+
switch (cmd) {
|
|
29
|
+
case "install":
|
|
30
|
+
(0, install_1.install)({ projectLocal: rest.includes("--project") });
|
|
31
|
+
break;
|
|
32
|
+
case "mcp":
|
|
33
|
+
require("./mcp/server").serve();
|
|
34
|
+
break;
|
|
35
|
+
case "import":
|
|
36
|
+
await require("./commands/import").importDump(rest[0]);
|
|
37
|
+
break;
|
|
38
|
+
case "status":
|
|
39
|
+
require("./commands/status").status();
|
|
40
|
+
break;
|
|
41
|
+
case "purge-legacy": {
|
|
42
|
+
const r = (0, uninstall_legacy_1.removeLegacyInstall)();
|
|
43
|
+
console.log(` hooks removed ${r.hooksRemoved.length}`);
|
|
44
|
+
console.log(` MCP shim ${r.mcpRemoved ? "removed" : "none"}`);
|
|
45
|
+
console.log(` credential file ${r.credentialRemoved ? "removed" : "none"}`);
|
|
46
|
+
console.log(` settings entries ${r.settingsCleaned}`);
|
|
47
|
+
const left = (0, uninstall_legacy_1.findLegacyLeftovers)();
|
|
48
|
+
console.log(left.length ? ` still present:\n ${left.join("\n ")}` : " nothing left behind");
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
default:
|
|
52
|
+
help();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
main().catch((e) => {
|
|
56
|
+
console.error(e?.message ?? e);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Did the injected memory change anything?
|
|
4
|
+
*
|
|
5
|
+
* Everything else this package measures is retrieval — whether the right
|
|
6
|
+
* memory was surfaced. This is the only measurement of *use*, and it is the
|
|
7
|
+
* one that decides whether any of the rest is worth its complexity. A memory
|
|
8
|
+
* system that surfaces perfect context and is then ignored is theatre.
|
|
9
|
+
*
|
|
10
|
+
* Three signals, in ascending order of how much they prove:
|
|
11
|
+
*
|
|
12
|
+
* echo a distinctive token from the injected text appears in the
|
|
13
|
+
* assistant's output and could not have come from the prompt
|
|
14
|
+
* or from a file the session read. Weak — the model may
|
|
15
|
+
* paraphrase — so read it as a floor, not a rate.
|
|
16
|
+
*
|
|
17
|
+
* reread memory described a file and the session read it anyway. If
|
|
18
|
+
* that happens every time, the injection is not landing.
|
|
19
|
+
* Correlational: the session may have had another reason.
|
|
20
|
+
*
|
|
21
|
+
* pitfall touch memory warned that an approach failed, and the session
|
|
22
|
+
* edited the file that pitfall was about. The closest thing
|
|
23
|
+
* here to a counterfactual, and the only signal worth acting
|
|
24
|
+
* on individually. Flagged for review, never treated as proof.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.distinctiveTokens = distinctiveTokens;
|
|
28
|
+
exports.readTranscript = readTranscript;
|
|
29
|
+
exports.scoreInjection = scoreInjection;
|
|
30
|
+
exports.formatReport = formatReport;
|
|
31
|
+
/** Words too common to prove anything if they reappear. */
|
|
32
|
+
const STOPWORDS = new Set([
|
|
33
|
+
"about", "after", "again", "because", "before", "being", "between", "could",
|
|
34
|
+
"every", "first", "found", "important", "instead", "into", "issue", "later",
|
|
35
|
+
"needs", "other", "problem", "return", "should", "since", "still", "their",
|
|
36
|
+
"there", "these", "thing", "those", "through", "using", "value", "which",
|
|
37
|
+
"while", "would", "change", "changed", "changes", "check", "class", "config",
|
|
38
|
+
"error", "field", "files", "function", "import", "method", "number", "object",
|
|
39
|
+
"output", "point", "result", "server", "state", "string", "table", "there",
|
|
40
|
+
"update", "where", "write",
|
|
41
|
+
]);
|
|
42
|
+
/**
|
|
43
|
+
* Tokens distinctive enough that seeing them again is evidence rather than
|
|
44
|
+
* coincidence: long enough, not ordinary English, and absent from the prompt.
|
|
45
|
+
*/
|
|
46
|
+
function distinctiveTokens(injected, prompt, max = 25) {
|
|
47
|
+
const promptTokens = new Set(tokenize(prompt));
|
|
48
|
+
const out = [];
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
for (const t of tokenize(injected)) {
|
|
51
|
+
if (t.length < 5 || t.length > 40)
|
|
52
|
+
continue;
|
|
53
|
+
if (STOPWORDS.has(t))
|
|
54
|
+
continue;
|
|
55
|
+
if (promptTokens.has(t))
|
|
56
|
+
continue;
|
|
57
|
+
if (seen.has(t))
|
|
58
|
+
continue;
|
|
59
|
+
// Prefer things that look like identifiers or paths over prose.
|
|
60
|
+
const identifierish = /[._/-]/.test(t) || /\d/.test(t);
|
|
61
|
+
seen.add(t);
|
|
62
|
+
out.push(t);
|
|
63
|
+
if (identifierish && out.length >= max)
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
return out.slice(0, max);
|
|
67
|
+
}
|
|
68
|
+
function tokenize(s) {
|
|
69
|
+
return (s || "").toLowerCase().replace(/[^a-z0-9._/-]+/g, " ").split(/\s+/).filter(Boolean);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Pull the three things compliance needs out of a Claude Code transcript.
|
|
73
|
+
* Tolerates partial and malformed lines — a transcript being unparseable
|
|
74
|
+
* should cost a measurement, not a session.
|
|
75
|
+
*/
|
|
76
|
+
function readTranscript(jsonl) {
|
|
77
|
+
const read = new Set();
|
|
78
|
+
const edited = new Set();
|
|
79
|
+
const said = [];
|
|
80
|
+
for (const line of (jsonl || "").split("\n")) {
|
|
81
|
+
const t = line.trim();
|
|
82
|
+
if (!t.startsWith("{"))
|
|
83
|
+
continue;
|
|
84
|
+
let row;
|
|
85
|
+
try {
|
|
86
|
+
row = JSON.parse(t);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const blocks = row?.message?.content;
|
|
92
|
+
if (row?.type !== "assistant" || !Array.isArray(blocks))
|
|
93
|
+
continue;
|
|
94
|
+
for (const b of blocks) {
|
|
95
|
+
if (b?.type === "text" && typeof b.text === "string")
|
|
96
|
+
said.push(b.text);
|
|
97
|
+
else if (b?.type === "tool_use") {
|
|
98
|
+
const f = b.input?.file_path ?? b.input?.path;
|
|
99
|
+
if (typeof f !== "string")
|
|
100
|
+
continue;
|
|
101
|
+
if (b.name === "Read")
|
|
102
|
+
read.add(f);
|
|
103
|
+
else if (["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(b.name))
|
|
104
|
+
edited.add(f);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { read, edited, said: said.join("\n").toLowerCase() };
|
|
109
|
+
}
|
|
110
|
+
function scoreInjection(injected, t) {
|
|
111
|
+
const sameFile = (a, b) => a === b || a.endsWith("/" + b) || b.endsWith("/" + a);
|
|
112
|
+
const filesReread = injected.files.filter((f) => [...t.read].some((r) => sameFile(r, f))).length;
|
|
113
|
+
// A token only counts as echoed if it did not arrive by another route: if
|
|
114
|
+
// the session read a file whose path contains it, the model could have seen
|
|
115
|
+
// it there rather than in the memory.
|
|
116
|
+
const readPaths = [...t.read].join(" ").toLowerCase();
|
|
117
|
+
const tokensEchoed = injected.tokens.filter((tok) => t.said.includes(tok) && !readPaths.includes(tok)).length;
|
|
118
|
+
const pitfallTouched = injected.pitfallFiles.filter((f) => [...t.edited].some((e) => sameFile(e, f))).length;
|
|
119
|
+
return {
|
|
120
|
+
filesNamed: injected.files.length,
|
|
121
|
+
filesReread,
|
|
122
|
+
tokensTotal: injected.tokens.length,
|
|
123
|
+
tokensEchoed,
|
|
124
|
+
pitfallTouched,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** Render the report. Deliberately states what each number cannot prove. */
|
|
128
|
+
function formatReport(r) {
|
|
129
|
+
const n = Number(r.resolved) || 0;
|
|
130
|
+
if (n === 0) {
|
|
131
|
+
return "\n No resolved injections yet. Memory has to be injected and a\n session finished before there is anything to measure.\n";
|
|
132
|
+
}
|
|
133
|
+
const pct = (x, d = n) => (d ? ((Number(x) || 0) / d * 100).toFixed(1) + "%" : "—");
|
|
134
|
+
const hadFiles = Number(r.had_files) || 0;
|
|
135
|
+
const echoRate = (Number(r.echoed) || 0) / n;
|
|
136
|
+
const avoidRate = hadFiles ? (Number(r.avoided_reread) || 0) / hadFiles : 0;
|
|
137
|
+
// The decision this instrumentation exists to serve, printed with the
|
|
138
|
+
// numbers rather than written down somewhere that will be lost. Retrieval
|
|
139
|
+
// buys +2.8 coverage points over tier-1 facts alone and costs roughly 80%
|
|
140
|
+
// of this package; it is only worth keeping if memory is actually used.
|
|
141
|
+
const verdict = n < 30
|
|
142
|
+
? " Too few to judge. Come back after ~3 weeks of real sessions."
|
|
143
|
+
: echoRate < 0.15 && avoidRate < 0.25
|
|
144
|
+
? [
|
|
145
|
+
" READ AND IGNORED. Memory is being surfaced and not used.",
|
|
146
|
+
" Cut back to tier-1 facts plus save_memory, and delete recall.ts,",
|
|
147
|
+
" embed.ts and the vector/FTS machinery — retrieval is buying 2.8",
|
|
148
|
+
" coverage points for ~80% of this package.",
|
|
149
|
+
].join("\n")
|
|
150
|
+
: echoRate < 0.15
|
|
151
|
+
? " Weak. Memory rarely echoed, but re-reads are being avoided.\n Inconclusive — keep collecting, and check the pitfall list below."
|
|
152
|
+
: " BEING USED. Memory is landing often enough to keep the retrieval half.";
|
|
153
|
+
return [
|
|
154
|
+
"",
|
|
155
|
+
` injections measured ${n}${r.open ? ` (${r.open} still open)` : ""}`,
|
|
156
|
+
"",
|
|
157
|
+
` echoed a distinctive token ${pct(r.echoed)} of injections`,
|
|
158
|
+
` tokens echoed ${Number(r.tokens_echoed) || 0} of ${Number(r.tokens_total) || 0}`,
|
|
159
|
+
` avoided re-reading a file ${pct(r.avoided_reread, hadFiles)} of the ${hadFiles} that named files`,
|
|
160
|
+
` pitfall files edited ${Number(r.pitfall_touched) || 0} worth reviewing individually`,
|
|
161
|
+
"",
|
|
162
|
+
verdict,
|
|
163
|
+
"",
|
|
164
|
+
" Echo is a floor, not a rate — the model may use a memory and paraphrase",
|
|
165
|
+
" it. Avoided-reread is correlational. Only the pitfall count is close to",
|
|
166
|
+
" evidence, and it is a prompt to go look, not a verdict.",
|
|
167
|
+
"",
|
|
168
|
+
].join("\n");
|
|
169
|
+
}
|