@prom.codes/memory-mcp 0.7.3 → 0.9.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 +13 -7
- package/dist/bin.js +150 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,10 +18,15 @@ Docks under the server name `memory`, so tools resolve to `memory_read` /
|
|
|
18
18
|
`--scope project` for a committable `.mcp.json`. Other hosts (Cursor, VS Code)
|
|
19
19
|
use the same command/args in their own config.
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
**Awareness — so the agent actually uses memory.** A server only offers tools;
|
|
22
|
+
the agent uses them only if told to. On startup the server **auto-installs a
|
|
23
|
+
marked memory-protocol rule block** into the runtime config files you already
|
|
24
|
+
have (CLAUDE.md / AGENTS.md / .cursor / .augment) — idempotent, never creates a
|
|
25
|
+
new file, skipped for home/root, opt out with `PROMETHEUS_MEMORY_AUTO_SETUP=off`.
|
|
26
|
+
Those files load every session, so the agent reads memory at session start,
|
|
27
|
+
writes durable facts as they come up, and captures at the end — without being
|
|
28
|
+
asked. Run `memory_setup` to install into all detected runtimes (incl. creating
|
|
29
|
+
`AGENTS.md` if you have no config yet); `memory_status` shows what's installed.
|
|
25
30
|
|
|
26
31
|
## Configuration
|
|
27
32
|
|
|
@@ -33,9 +38,10 @@ session end.
|
|
|
33
38
|
`roots`) — no need to set `PROMETHEUS_WORKSPACE_ROOT`.
|
|
34
39
|
- **Optional quality levers** (documented): `PROMETHEUS_MEMORY_RERANK_PROVIDER`
|
|
35
40
|
(cross-encoder rerank — reranked recall ties full-context at ~28× fewer tokens
|
|
36
|
-
on LoCoMo)
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
on LoCoMo), `PROMETHEUS_MEMORY_REWRITE_PROVIDER` (HyDE), and
|
|
42
|
+
`PROMETHEUS_MEMORY_DEDUP=on` (collapse restatements so distinct facts fill the
|
|
43
|
+
top-k). Temporal-intent ranking ("latest/earliest" queries) is **on by
|
|
44
|
+
default**; disable with `PROMETHEUS_MEMORY_TEMPORAL=off`.
|
|
39
45
|
|
|
40
46
|
If a fresh window opens with no project, the workspace falls back to the host
|
|
41
47
|
cwd (often home); in that case memory still works but project memories are NOT
|
package/dist/bin.js
CHANGED
|
@@ -1360,6 +1360,59 @@ function applyTemporalRanking(hits, intent, getTimestampMs, weight = DEFAULT_TEM
|
|
|
1360
1360
|
return scored.map((s) => s.hit);
|
|
1361
1361
|
}
|
|
1362
1362
|
|
|
1363
|
+
// dist/dedup.js
|
|
1364
|
+
function normalizeForDedup(value) {
|
|
1365
|
+
return value.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
|
|
1366
|
+
}
|
|
1367
|
+
function cosineSim(a, b) {
|
|
1368
|
+
if (a.length !== b.length)
|
|
1369
|
+
return 0;
|
|
1370
|
+
let dot = 0;
|
|
1371
|
+
let na = 0;
|
|
1372
|
+
let nb = 0;
|
|
1373
|
+
for (let i = 0; i < a.length; i++) {
|
|
1374
|
+
dot += a[i] * b[i];
|
|
1375
|
+
na += a[i] * a[i];
|
|
1376
|
+
nb += b[i] * b[i];
|
|
1377
|
+
}
|
|
1378
|
+
if (na === 0 || nb === 0)
|
|
1379
|
+
return 0;
|
|
1380
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
1381
|
+
}
|
|
1382
|
+
var DEFAULT_DEDUP_THRESHOLD = 0.92;
|
|
1383
|
+
function dedupe(items, getText, getVector, threshold = DEFAULT_DEDUP_THRESHOLD) {
|
|
1384
|
+
if (items.length <= 1)
|
|
1385
|
+
return { kept: [...items], merged: 0 };
|
|
1386
|
+
const kept = [];
|
|
1387
|
+
const keptNorm = [];
|
|
1388
|
+
const keptVec = [];
|
|
1389
|
+
let merged = 0;
|
|
1390
|
+
for (const item of items) {
|
|
1391
|
+
const norm = normalizeForDedup(getText(item));
|
|
1392
|
+
const vec = getVector(item);
|
|
1393
|
+
let isDup = false;
|
|
1394
|
+
for (let i = 0; i < kept.length; i++) {
|
|
1395
|
+
if (norm.length > 0 && norm === keptNorm[i]) {
|
|
1396
|
+
isDup = true;
|
|
1397
|
+
break;
|
|
1398
|
+
}
|
|
1399
|
+
const kv = keptVec[i];
|
|
1400
|
+
if (threshold > 0 && vec !== void 0 && kv !== void 0 && cosineSim(vec, kv) >= threshold) {
|
|
1401
|
+
isDup = true;
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
if (isDup) {
|
|
1406
|
+
merged += 1;
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
kept.push(item);
|
|
1410
|
+
keptNorm.push(norm);
|
|
1411
|
+
keptVec.push(vec);
|
|
1412
|
+
}
|
|
1413
|
+
return { kept, merged };
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1363
1416
|
// dist/types.js
|
|
1364
1417
|
var MEMORY_SCOPES = [
|
|
1365
1418
|
"system",
|
|
@@ -1535,6 +1588,8 @@ var SqliteMemoryBackend = class {
|
|
|
1535
1588
|
rewriter;
|
|
1536
1589
|
temporalEnabled;
|
|
1537
1590
|
temporalWeight;
|
|
1591
|
+
dedupEnabled;
|
|
1592
|
+
dedupThreshold;
|
|
1538
1593
|
/** Record ids whose vector is missing/stale, awaiting a batched embed. */
|
|
1539
1594
|
pendingEmbed = /* @__PURE__ */ new Set();
|
|
1540
1595
|
closed = false;
|
|
@@ -1553,6 +1608,8 @@ var SqliteMemoryBackend = class {
|
|
|
1553
1608
|
this.rewriter = opts.rewriter;
|
|
1554
1609
|
this.temporalEnabled = opts.temporal?.enabled ?? false;
|
|
1555
1610
|
this.temporalWeight = opts.temporal?.weight ?? DEFAULT_TEMPORAL_WEIGHT;
|
|
1611
|
+
this.dedupEnabled = opts.dedup?.enabled ?? false;
|
|
1612
|
+
this.dedupThreshold = opts.dedup?.threshold ?? DEFAULT_DEDUP_THRESHOLD;
|
|
1556
1613
|
if (this.embedder !== void 0)
|
|
1557
1614
|
this.queueUnembedded();
|
|
1558
1615
|
}
|
|
@@ -1597,6 +1654,35 @@ var SqliteMemoryBackend = class {
|
|
|
1597
1654
|
const row = this.db.prepare(`SELECT * FROM agent_memory WHERE id = ?`).get(id);
|
|
1598
1655
|
return rowToRecord(row);
|
|
1599
1656
|
}
|
|
1657
|
+
/** Fetch stored vectors for a set of record ids (missing → absent from map). */
|
|
1658
|
+
vectorsByIds(ids) {
|
|
1659
|
+
const out = /* @__PURE__ */ new Map();
|
|
1660
|
+
if (ids.length === 0)
|
|
1661
|
+
return out;
|
|
1662
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
1663
|
+
const rows = this.db.prepare(`SELECT record_id, vector FROM agent_memory_vec WHERE record_id IN (${placeholders})`).all(...ids);
|
|
1664
|
+
for (const r of rows)
|
|
1665
|
+
out.set(r.record_id, blobToVector(r.vector));
|
|
1666
|
+
return out;
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* M6: collapse near-duplicate records, keeping the first (highest-priority).
|
|
1670
|
+
* No-op when dedup is disabled or there is ≤1 record. Uses stored vectors for
|
|
1671
|
+
* the cosine signal when present (text-equality always applies).
|
|
1672
|
+
*/
|
|
1673
|
+
dedupeRecords(records) {
|
|
1674
|
+
if (!this.dedupEnabled || records.length <= 1)
|
|
1675
|
+
return records;
|
|
1676
|
+
const vectors = this.vectorsByIds(records.map((r) => r.id));
|
|
1677
|
+
return dedupe(records, (r) => r.value, (r) => vectors.get(r.id), this.dedupThreshold).kept;
|
|
1678
|
+
}
|
|
1679
|
+
/** As {@link dedupeRecords}, but over search hits (caller gates on the flag). */
|
|
1680
|
+
dedupeHitPool(hits) {
|
|
1681
|
+
if (hits.length <= 1)
|
|
1682
|
+
return hits;
|
|
1683
|
+
const vectors = this.vectorsByIds(hits.map((h) => h.record.id));
|
|
1684
|
+
return dedupe(hits, (h) => h.record.value, (h) => vectors.get(h.record.id), this.dedupThreshold).kept;
|
|
1685
|
+
}
|
|
1600
1686
|
async read(query) {
|
|
1601
1687
|
if (query.chain.length === 0)
|
|
1602
1688
|
return [];
|
|
@@ -1610,7 +1696,8 @@ var SqliteMemoryBackend = class {
|
|
|
1610
1696
|
sql += ` ORDER BY updated_at DESC`;
|
|
1611
1697
|
const rows = this.db.prepare(sql).all(...params);
|
|
1612
1698
|
const resolved = resolveScopeChain(rows.map(rowToRecord), query.chain);
|
|
1613
|
-
const
|
|
1699
|
+
const deduped = this.dedupeRecords(resolved);
|
|
1700
|
+
const limited = query.limit !== void 0 ? deduped.slice(0, query.limit) : deduped;
|
|
1614
1701
|
const bump = this.db.prepare(`UPDATE agent_memory SET use_count = use_count + 1 WHERE id = ?`);
|
|
1615
1702
|
for (const rec of limited) {
|
|
1616
1703
|
bump.run(rec.id);
|
|
@@ -1675,7 +1762,9 @@ var SqliteMemoryBackend = class {
|
|
|
1675
1762
|
{ id: "vec", items: vecHits.map((h) => ({ key: h.record.id, payload: h })) }
|
|
1676
1763
|
], { limit: poolLimit }).map((f) => f.payload);
|
|
1677
1764
|
}
|
|
1678
|
-
const
|
|
1765
|
+
const dedupOn = input.dedup ?? this.dedupEnabled;
|
|
1766
|
+
const deduped = dedupOn ? this.dedupeHitPool(pool) : pool;
|
|
1767
|
+
const reranked = input.rerank === false ? deduped : await this.rerankPool(input.query, deduped, finalLimit);
|
|
1679
1768
|
const temporalOn = input.temporal ?? this.temporalEnabled;
|
|
1680
1769
|
const ordered = temporalOn ? this.applyTemporal(input.query, reranked) : reranked;
|
|
1681
1770
|
return ordered.slice(0, finalLimit);
|
|
@@ -2168,6 +2257,18 @@ function discoverMemoryTemporal(env) {
|
|
|
2168
2257
|
}
|
|
2169
2258
|
return { enabled, weight };
|
|
2170
2259
|
}
|
|
2260
|
+
function discoverMemoryDedup(env) {
|
|
2261
|
+
const raw = (env.PROMETHEUS_MEMORY_DEDUP ?? "off").toLowerCase();
|
|
2262
|
+
const enabled = raw === "on" || raw === "1" || raw === "true" || raw === "yes";
|
|
2263
|
+
const rawThreshold = env.PROMETHEUS_MEMORY_DEDUP_THRESHOLD;
|
|
2264
|
+
let threshold = DEFAULT_DEDUP_THRESHOLD;
|
|
2265
|
+
if (rawThreshold !== void 0 && rawThreshold !== "") {
|
|
2266
|
+
const n = Number.parseFloat(rawThreshold);
|
|
2267
|
+
if (Number.isFinite(n) && n > 0 && n <= 1)
|
|
2268
|
+
threshold = n;
|
|
2269
|
+
}
|
|
2270
|
+
return { enabled, threshold };
|
|
2271
|
+
}
|
|
2171
2272
|
function composeFromEnv(opts) {
|
|
2172
2273
|
const env = opts.env;
|
|
2173
2274
|
const override = (opts.workspaceRootOverride ?? "").trim();
|
|
@@ -2183,11 +2284,14 @@ function composeFromEnv(opts) {
|
|
|
2183
2284
|
const { id: extractorId, provider: extractor } = discoverMemoryExtractor(env);
|
|
2184
2285
|
const { id: rewriterId, provider: rewriter } = discoverMemoryRewriter(env);
|
|
2185
2286
|
const temporal = discoverMemoryTemporal(env);
|
|
2287
|
+
const dedup = discoverMemoryDedup(env);
|
|
2288
|
+
const autoSetup = !/^(off|0|false|no)$/i.test(env.PROMETHEUS_MEMORY_AUTO_SETUP ?? "");
|
|
2186
2289
|
const backend = new SqliteMemoryBackend(dbPath, {
|
|
2187
2290
|
...embedder !== void 0 ? { embedder } : {},
|
|
2188
2291
|
...reranker !== null ? { reranker } : {},
|
|
2189
2292
|
...rewriter !== null ? { rewriter } : {},
|
|
2190
|
-
temporal
|
|
2293
|
+
temporal,
|
|
2294
|
+
dedup
|
|
2191
2295
|
});
|
|
2192
2296
|
return {
|
|
2193
2297
|
backend,
|
|
@@ -2205,6 +2309,8 @@ function composeFromEnv(opts) {
|
|
|
2205
2309
|
rewriter,
|
|
2206
2310
|
rewriterId,
|
|
2207
2311
|
temporalEnabled: temporal.enabled,
|
|
2312
|
+
dedupEnabled: dedup.enabled,
|
|
2313
|
+
autoSetup,
|
|
2208
2314
|
rootIsHomeOrFsRoot: isHomeOrFilesystemRoot(workspaceRoot),
|
|
2209
2315
|
close: () => backend.close()
|
|
2210
2316
|
};
|
|
@@ -2362,7 +2468,7 @@ function assertNoSecrets(text) {
|
|
|
2362
2468
|
}
|
|
2363
2469
|
|
|
2364
2470
|
// dist/setup.js
|
|
2365
|
-
import { existsSync } from "node:fs";
|
|
2471
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2366
2472
|
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
2367
2473
|
import { dirname as dirname3, join as join4 } from "node:path";
|
|
2368
2474
|
var MEMORY_RUNTIMES = [
|
|
@@ -2425,6 +2531,28 @@ function detectRuntimes(workspaceRoot) {
|
|
|
2425
2531
|
const found = MEMORY_RUNTIMES.filter((rt) => existsSync(join4(workspaceRoot, TARGETS[rt].detect)));
|
|
2426
2532
|
return found.length > 0 ? found : ["agents"];
|
|
2427
2533
|
}
|
|
2534
|
+
function existingRuntimes(workspaceRoot) {
|
|
2535
|
+
return MEMORY_RUNTIMES.filter((rt) => existsSync(join4(workspaceRoot, TARGETS[rt].detect)));
|
|
2536
|
+
}
|
|
2537
|
+
function installedRuntimes(workspaceRoot) {
|
|
2538
|
+
return MEMORY_RUNTIMES.filter((rt) => {
|
|
2539
|
+
const p = join4(workspaceRoot, TARGETS[rt].relPath);
|
|
2540
|
+
if (!existsSync(p))
|
|
2541
|
+
return false;
|
|
2542
|
+
try {
|
|
2543
|
+
return readFileSync(p, "utf-8").includes(BLOCK_START);
|
|
2544
|
+
} catch {
|
|
2545
|
+
return false;
|
|
2546
|
+
}
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
2549
|
+
async function autoInstallExisting(workspaceRoot) {
|
|
2550
|
+
const results = [];
|
|
2551
|
+
for (const rt of existingRuntimes(workspaceRoot)) {
|
|
2552
|
+
results.push(await installRuntime(workspaceRoot, rt));
|
|
2553
|
+
}
|
|
2554
|
+
return results;
|
|
2555
|
+
}
|
|
2428
2556
|
function upsertBlock(existing, block) {
|
|
2429
2557
|
const marked = withMarkers(block);
|
|
2430
2558
|
const start = existing.indexOf(BLOCK_START);
|
|
@@ -2820,6 +2948,12 @@ ${f.value}`);
|
|
|
2820
2948
|
installed: true,
|
|
2821
2949
|
project: { id: projectId, name: projectName, workspaceRoot },
|
|
2822
2950
|
rootIsHomeOrFsRoot: deps.rootIsHomeOrFsRoot,
|
|
2951
|
+
rules: {
|
|
2952
|
+
// Which runtime configs carry the always-on memory protocol — the
|
|
2953
|
+
// signal that the agent will actually USE memory in future sessions.
|
|
2954
|
+
installed: deps.rootIsHomeOrFsRoot ? [] : installedRuntimes(workspaceRoot),
|
|
2955
|
+
autoSetup: deps.autoSetup
|
|
2956
|
+
},
|
|
2823
2957
|
storage: { dbPath, projectFileMirror: mirrorToFiles },
|
|
2824
2958
|
records: { total: stats.total, byScope: stats.byScope },
|
|
2825
2959
|
embeddings: {
|
|
@@ -2832,6 +2966,7 @@ ${f.value}`);
|
|
|
2832
2966
|
rerank: deps.rerankerId,
|
|
2833
2967
|
rewrite: deps.rewriterId,
|
|
2834
2968
|
temporal: deps.temporalEnabled,
|
|
2969
|
+
dedup: deps.dedupEnabled,
|
|
2835
2970
|
extract: deps.extractorId
|
|
2836
2971
|
},
|
|
2837
2972
|
summary
|
|
@@ -2845,7 +2980,7 @@ var SERVER_IDENTITY = {
|
|
|
2845
2980
|
version: PROMETHEUS_VERSION,
|
|
2846
2981
|
title: "prom.codes Memory"
|
|
2847
2982
|
};
|
|
2848
|
-
var SERVER_INSTRUCTIONS = "Persistent agent memory for this workspace.
|
|
2983
|
+
var SERVER_INSTRUCTIONS = "Persistent agent memory for this workspace \u2014 USE IT PROACTIVELY; the user will not tell you to. Protocol:\n1. ONE-TIME: if this workspace has no Prometheus memory rule yet, call memory_setup now (idempotent) so the protocol is installed into the runtime rule files and survives future sessions. (The server also auto-installs it on startup when a project rule file already exists \u2014 memory_setup covers the rest.)\n2. SESSION START: before any non-trivial task, call memory_read to recall facts, decisions and procedures from earlier sessions.\n3. DURING WORK: when the user states a durable preference, decision, correction or project fact, store it with memory_write (semantic for facts, procedural for how-tos) \u2014 without being asked.\n4. LOOK-UP: use memory_search for keyword recall when memory_read is not specific enough.\n5. SESSION END: consolidate what was learned with memory_capture.\nCall memory_status anytime to check what is stored and whether the rule is installed. Never store secrets, API keys or credentials \u2014 such writes are rejected.";
|
|
2849
2984
|
|
|
2850
2985
|
// dist/bin.js
|
|
2851
2986
|
function looksLikeMissingNativeBinding(msg) {
|
|
@@ -2882,11 +3017,20 @@ async function main() {
|
|
|
2882
3017
|
env,
|
|
2883
3018
|
...override !== void 0 && override !== "" ? { workspaceRootOverride: override } : {}
|
|
2884
3019
|
});
|
|
2885
|
-
process.stderr.write(`prometheus-memory-mcp: workspace=${composed.workspaceRoot} (via ${via}) project=${composed.projectName} (${composed.projectId}) db=${composed.dbPath} embed=${composed.embedderId}${composed.embeddingsEnabled ? "" : " (keyword-only)"} rerank=${composed.rerankerId} extract=${composed.extractorId} rewrite=${composed.rewriterId} temporal=${composed.temporalEnabled ? "on" : "off"}
|
|
3020
|
+
process.stderr.write(`prometheus-memory-mcp: workspace=${composed.workspaceRoot} (via ${via}) project=${composed.projectName} (${composed.projectId}) db=${composed.dbPath} embed=${composed.embedderId}${composed.embeddingsEnabled ? "" : " (keyword-only)"} rerank=${composed.rerankerId} extract=${composed.extractorId} rewrite=${composed.rewriterId} temporal=${composed.temporalEnabled ? "on" : "off"} dedup=${composed.dedupEnabled ? "on" : "off"}
|
|
2886
3021
|
`);
|
|
2887
3022
|
if (composed.rootIsHomeOrFsRoot) {
|
|
2888
3023
|
process.stderr.write(`prometheus-memory-mcp: workspace resolved to ${composed.workspaceRoot} (your home directory or a filesystem root) \u2014 project memories will NOT be mirrored to markdown there. Open a project folder (Claude Code passes it via CLAUDE_PROJECT_DIR) or set PROMETHEUS_WORKSPACE_ROOT. Call memory_status for details.
|
|
2889
3024
|
`);
|
|
3025
|
+
} else if (composed.autoSetup) {
|
|
3026
|
+
void autoInstallExisting(composed.workspaceRoot).then((results) => {
|
|
3027
|
+
const wrote = results.filter((r) => r.action !== "unchanged");
|
|
3028
|
+
if (wrote.length > 0) {
|
|
3029
|
+
process.stderr.write(`prometheus-memory-mcp: auto-installed the memory rule into ${wrote.map((r) => r.runtime).join(", ")} (set PROMETHEUS_MEMORY_AUTO_SETUP=off to disable)
|
|
3030
|
+
`);
|
|
3031
|
+
}
|
|
3032
|
+
}).catch(() => {
|
|
3033
|
+
});
|
|
2890
3034
|
}
|
|
2891
3035
|
registerTools(server, composed);
|
|
2892
3036
|
};
|