@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.
Files changed (3) hide show
  1. package/README.md +13 -7
  2. package/dist/bin.js +150 -6
  3. 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
- Then ask your agent to run `memory_setup` once per workspace it installs the
22
- memory protocol into your runtime rule files (CLAUDE.md, .cursor/rules,
23
- AGENTS.md) so the agent reads memory at session start and captures learnings at
24
- session end.
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) and `PROMETHEUS_MEMORY_REWRITE_PROVIDER` (HyDE). Temporal-intent
37
- ranking ("latest/earliest" queries) is **on by default**; disable with
38
- `PROMETHEUS_MEMORY_TEMPORAL=off`.
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 limited = query.limit !== void 0 ? resolved.slice(0, query.limit) : resolved;
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 reranked = input.rerank === false ? pool : await this.rerankPool(input.query, pool, finalLimit);
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. At the START of a session or task, call memory_read to recall facts, decisions and procedures from earlier sessions. When the user states a durable preference, decision or correction, store it with memory_write. Use memory_search for keyword recall when memory_read is not specific enough. At the END of a session, consolidate what was learned with memory_capture. Run memory_setup once per workspace to install the memory protocol into runtime rule files. Never store secrets, API keys or credentials \u2014 such writes are rejected.";
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prom.codes/memory-mcp",
3
- "version": "0.7.3",
3
+ "version": "0.9.0",
4
4
  "description": "prom.codes Memory — persistent, local-first agent memory as an MCP server.",
5
5
  "type": "module",
6
6
  "bin": {