@prom.codes/memory-mcp 0.15.2 → 0.17.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 (2) hide show
  1. package/dist/bin.js +171 -22
  2. package/package.json +2 -2
package/dist/bin.js CHANGED
@@ -301,6 +301,20 @@ function isHomeOrFilesystemRoot(root) {
301
301
  return false;
302
302
  }
303
303
 
304
+ // ../shared/dist/workspace-path.js
305
+ import { createHash } from "node:crypto";
306
+ import { resolve as resolve2 } from "node:path";
307
+ function canonicalizeWorkspacePath(p) {
308
+ const abs = resolve2(p);
309
+ return process.platform === "win32" ? abs.replace(/\\/g, "/").toLowerCase() : abs;
310
+ }
311
+ function stableWorkspaceId(root) {
312
+ return createHash("sha256").update(canonicalizeWorkspacePath(root)).digest("hex").slice(0, 16);
313
+ }
314
+ function legacyWorkspaceId(root) {
315
+ return createHash("sha256").update(resolve2(root)).digest("hex").slice(0, 16);
316
+ }
317
+
304
318
  // ../shared/dist/heartbeat.js
305
319
  import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
306
320
  import { homedir as homedir3 } from "node:os";
@@ -471,9 +485,8 @@ function nativeBindingOption() {
471
485
  }
472
486
 
473
487
  // dist/composition.js
474
- import { createHash } from "node:crypto";
475
488
  import { homedir as homedir5 } from "node:os";
476
- import { basename, join as join7, resolve as resolve2 } from "node:path";
489
+ import { basename, join as join7, resolve as resolve3 } from "node:path";
477
490
 
478
491
  // ../embeddings-openai-compat/dist/index.js
479
492
  var DEFAULT_BATCH = 96;
@@ -504,14 +517,14 @@ function parseRetryAfterMs(value, now = Date.now()) {
504
517
  return delta > 0 ? delta : 0;
505
518
  }
506
519
  function sleep(ms, signal) {
507
- return new Promise((resolve3, reject) => {
520
+ return new Promise((resolve4, reject) => {
508
521
  if (signal?.aborted === true) {
509
522
  reject(new Error("aborted"));
510
523
  return;
511
524
  }
512
525
  const timer = setTimeout(() => {
513
526
  signal?.removeEventListener("abort", onAbort);
514
- resolve3();
527
+ resolve4();
515
528
  }, ms);
516
529
  const onAbort = () => {
517
530
  clearTimeout(timer);
@@ -754,14 +767,14 @@ var DEFAULT_RETRIES2 = 4;
754
767
  var DEFAULT_BACKOFF2 = 250;
755
768
  var DEFAULT_TIMEOUT_MS2 = 15e3;
756
769
  function sleep2(ms, signal) {
757
- return new Promise((resolve3, reject) => {
770
+ return new Promise((resolve4, reject) => {
758
771
  if (signal?.aborted === true) {
759
772
  reject(new Error("aborted"));
760
773
  return;
761
774
  }
762
775
  const timer = setTimeout(() => {
763
776
  signal?.removeEventListener("abort", onAbort);
764
- resolve3();
777
+ resolve4();
765
778
  }, ms);
766
779
  const onAbort = () => {
767
780
  clearTimeout(timer);
@@ -1025,14 +1038,14 @@ function parseRetryAfterMs2(value, now = Date.now()) {
1025
1038
  return delta > 0 ? delta : 0;
1026
1039
  }
1027
1040
  function sleep3(ms, signal) {
1028
- return new Promise((resolve3, reject) => {
1041
+ return new Promise((resolve4, reject) => {
1029
1042
  if (signal?.aborted === true) {
1030
1043
  reject(new Error("aborted"));
1031
1044
  return;
1032
1045
  }
1033
1046
  const timer = setTimeout(() => {
1034
1047
  signal?.removeEventListener("abort", onAbort);
1035
- resolve3();
1048
+ resolve4();
1036
1049
  }, ms);
1037
1050
  const onAbort = () => {
1038
1051
  clearTimeout(timer);
@@ -1179,14 +1192,14 @@ var DEFAULT_RETRIES4 = 4;
1179
1192
  var DEFAULT_BACKOFF4 = 250;
1180
1193
  var DEFAULT_TIMEOUT_MS3 = 15e3;
1181
1194
  function sleep4(ms, signal) {
1182
- return new Promise((resolve3, reject) => {
1195
+ return new Promise((resolve4, reject) => {
1183
1196
  if (signal?.aborted === true) {
1184
1197
  reject(new Error("aborted"));
1185
1198
  return;
1186
1199
  }
1187
1200
  const timer = setTimeout(() => {
1188
1201
  signal?.removeEventListener("abort", onAbort);
1189
- resolve3();
1202
+ resolve4();
1190
1203
  }, ms);
1191
1204
  const onAbort = () => {
1192
1205
  clearTimeout(timer);
@@ -1405,14 +1418,14 @@ function parseRetryAfterMs3(value, now = Date.now()) {
1405
1418
  return delta > 0 ? delta : 0;
1406
1419
  }
1407
1420
  function sleep5(ms, signal) {
1408
- return new Promise((resolve3, reject) => {
1421
+ return new Promise((resolve4, reject) => {
1409
1422
  if (signal?.aborted === true) {
1410
1423
  reject(new Error("aborted"));
1411
1424
  return;
1412
1425
  }
1413
1426
  const timer = setTimeout(() => {
1414
1427
  signal?.removeEventListener("abort", onAbort);
1415
- resolve3();
1428
+ resolve4();
1416
1429
  }, ms);
1417
1430
  const onAbort = () => {
1418
1431
  clearTimeout(timer);
@@ -1806,7 +1819,7 @@ var OpenAICompatRewriter = class {
1806
1819
 
1807
1820
  // dist/sqlite.js
1808
1821
  import { randomUUID } from "node:crypto";
1809
- import { mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, rmSync as rmSync2 } from "node:fs";
1822
+ import { mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, rmSync as rmSync2 } from "node:fs";
1810
1823
  import { dirname as dirname4, join as join6 } from "node:path";
1811
1824
  import Database from "better-sqlite3";
1812
1825
 
@@ -1854,7 +1867,7 @@ function assertNoSecrets(text) {
1854
1867
  }
1855
1868
 
1856
1869
  // dist/recorder.js
1857
- import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
1870
+ import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, renameSync, rmdirSync, writeFileSync as writeFileSync2 } from "node:fs";
1858
1871
  import { homedir as homedir4 } from "node:os";
1859
1872
  import { dirname as dirname3, join as join5 } from "node:path";
1860
1873
  var SPOOL_VERSION = 1;
@@ -1890,6 +1903,46 @@ function recorderRoot(env = process.env) {
1890
1903
  function sanitizeSegment(s) {
1891
1904
  return s.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 128) || "_";
1892
1905
  }
1906
+ function migrateRecorderSpoolDir(oldId, newId, env = process.env) {
1907
+ if (oldId === newId)
1908
+ return { renamedDir: false, movedFiles: 0 };
1909
+ const root = recorderRoot(env);
1910
+ const oldDir = join5(root, sanitizeSegment(oldId));
1911
+ const newDir = join5(root, sanitizeSegment(newId));
1912
+ if (!existsSync2(oldDir))
1913
+ return { renamedDir: false, movedFiles: 0 };
1914
+ if (!existsSync2(newDir)) {
1915
+ try {
1916
+ renameSync(oldDir, newDir);
1917
+ return { renamedDir: true, movedFiles: 0 };
1918
+ } catch {
1919
+ }
1920
+ }
1921
+ mkdirSync2(newDir, { recursive: true });
1922
+ let moved = 0;
1923
+ let names;
1924
+ try {
1925
+ names = readdirSync2(oldDir);
1926
+ } catch {
1927
+ return { renamedDir: false, movedFiles: 0 };
1928
+ }
1929
+ for (const name of names) {
1930
+ const to = join5(newDir, name);
1931
+ if (existsSync2(to))
1932
+ continue;
1933
+ try {
1934
+ renameSync(join5(oldDir, name), to);
1935
+ moved++;
1936
+ } catch {
1937
+ }
1938
+ }
1939
+ try {
1940
+ if (readdirSync2(oldDir).length === 0)
1941
+ rmdirSync(oldDir);
1942
+ } catch {
1943
+ }
1944
+ return { renamedDir: false, movedFiles: moved };
1945
+ }
1893
1946
  function parseSpool(raw) {
1894
1947
  const lines = raw.split("\n");
1895
1948
  const events = [];
@@ -1921,7 +1974,7 @@ var RECORDER_HOOK_SCRIPT = String.raw`#!/usr/bin/env node
1921
1974
  // never disturb a coding session. Managed by prom.codes (memory-mcp recorder_setup).
1922
1975
  import { appendFileSync, mkdirSync, readFileSync, statSync } from "node:fs";
1923
1976
  import { homedir } from "node:os";
1924
- import { join } from "node:path";
1977
+ import { join, resolve } from "node:path";
1925
1978
  import { createHash } from "node:crypto";
1926
1979
 
1927
1980
  const SPOOL_CAP = ${SPOOL_CAP_BYTES};
@@ -1944,7 +1997,11 @@ function sensitivePath(p) {
1944
1997
  if (/(^|\/)(secrets?|credentials?)(\/|$)/.test(s)) return true;
1945
1998
  return false;
1946
1999
  }
1947
- function projectIdOf(root) { return createHash("sha256").update(String(root)).digest("hex").slice(0, 16); }
2000
+ // Canonical workspace id MUST match @prometheus/shared stableWorkspaceId so
2001
+ // the spool key equals memory-mcp's projectIdFor (the ingest side). Inlined
2002
+ // because this standalone hook cannot import from the package.
2003
+ function canonicalizeWorkspacePath(p) { const abs = resolve(p); return process.platform === "win32" ? abs.replace(/\\/g, "/").toLowerCase() : abs; }
2004
+ function projectIdOf(root) { return createHash("sha256").update(canonicalizeWorkspacePath(root)).digest("hex").slice(0, 16); }
1948
2005
 
1949
2006
  function dietTool(toolName, inp, result) {
1950
2007
  const tool = toolName; inp = inp || {};
@@ -2604,6 +2661,61 @@ var SqliteMemoryBackend = class {
2604
2661
  if (this.embedder !== void 0)
2605
2662
  this.queueUnembedded();
2606
2663
  }
2664
+ /**
2665
+ * Canonical-hash migration (spec 2026-07-19 §3.3): move every row keyed on the
2666
+ * PRE-flip `oldId` onto the canonical `newId`, in ONE transaction. No re-embed
2667
+ * (the vector is over `value`, which is unchanged) — the memory analogue of
2668
+ * context's "no reindex".
2669
+ *
2670
+ * - `agent_memory`: change `project_id`; for project-scoped rows the
2671
+ * `scope_id` (which equals the project id) moves too. The UNIQUE
2672
+ * `(scope, scope_id, type, key)` can then clash with a row already under
2673
+ * `newId` (the user used both path spellings). **Merge rule: newest
2674
+ * `updated_at` wins**; the loser is deleted (its vector + FTS entry GC'd by
2675
+ * the existing delete triggers). This mirrors `resolveScopeChain`'s own
2676
+ * newer-wins tie-break, so the survivor is the record recall already prefers.
2677
+ * - `agent_sessions` / `session_events`: `UPDATE OR IGNORE` the `project_id`;
2678
+ * a `(session_id, seq)` clash across two spellings is effectively impossible
2679
+ * (ids are unique per run under one spelling), and OR IGNORE leaves any
2680
+ * clash inert under the old id rather than throwing.
2681
+ *
2682
+ * Idempotent (afterwards no row has `project_id = oldId`) and a no-op when
2683
+ * `oldId === newId` (POSIX / already-canonical).
2684
+ */
2685
+ migrateProjectIdentity(oldId, newId) {
2686
+ if (oldId === newId) {
2687
+ return { memoryRows: 0, memoryMerged: 0, sessions: 0, events: 0 };
2688
+ }
2689
+ const run = this.db.transaction(() => {
2690
+ const incoming = this.db.prepare(`SELECT * FROM agent_memory WHERE project_id = ?`).all(oldId);
2691
+ const findClash = this.db.prepare(`SELECT id, updated_at FROM agent_memory
2692
+ WHERE scope = ? AND scope_id = ? AND type = ? AND key = ? AND id != ?`);
2693
+ const del = this.db.prepare(`DELETE FROM agent_memory WHERE id = ?`);
2694
+ const upd = this.db.prepare(`UPDATE agent_memory SET project_id = ?, scope_id = ? WHERE id = ?`);
2695
+ let merged = 0;
2696
+ for (const row of incoming) {
2697
+ const newScopeId = row.scope === "project" && row.scope_id === oldId ? newId : row.scope_id;
2698
+ const clash = findClash.get(row.scope, newScopeId, row.type, row.key, row.id);
2699
+ if (clash !== void 0) {
2700
+ if (row.updated_at > clash.updated_at) {
2701
+ del.run(clash.id);
2702
+ this.pendingEmbed.delete(clash.id);
2703
+ upd.run(newId, newScopeId, row.id);
2704
+ } else {
2705
+ del.run(row.id);
2706
+ this.pendingEmbed.delete(row.id);
2707
+ }
2708
+ merged++;
2709
+ } else {
2710
+ upd.run(newId, newScopeId, row.id);
2711
+ }
2712
+ }
2713
+ const sessions = this.db.prepare(`UPDATE OR IGNORE agent_sessions SET project_id = ? WHERE project_id = ?`).run(newId, oldId).changes;
2714
+ const events = this.db.prepare(`UPDATE OR IGNORE session_events SET project_id = ? WHERE project_id = ?`).run(newId, oldId).changes;
2715
+ return { memoryRows: incoming.length, memoryMerged: merged, sessions, events };
2716
+ });
2717
+ return run();
2718
+ }
2607
2719
  /**
2608
2720
  * Queue every record lacking a stored vector for a (re)embed. Run at
2609
2721
  * open so a DB first built keyword-only — or one whose vectors were
@@ -3038,7 +3150,7 @@ ${h.record.value}`
3038
3150
  let deletedSpools = 0;
3039
3151
  let files;
3040
3152
  try {
3041
- files = readdirSync2(dir).filter((f) => f.endsWith(".jsonl"));
3153
+ files = readdirSync3(dir).filter((f) => f.endsWith(".jsonl"));
3042
3154
  } catch {
3043
3155
  return { sessions: 0, events: 0, deletedSpools: 0 };
3044
3156
  }
@@ -3212,8 +3324,10 @@ ${h.record.value}`
3212
3324
 
3213
3325
  // dist/composition.js
3214
3326
  function projectIdFor(workspaceRoot) {
3215
- const abs = resolve2(workspaceRoot);
3216
- return createHash("sha256").update(abs).digest("hex").slice(0, 16);
3327
+ return stableWorkspaceId(workspaceRoot);
3328
+ }
3329
+ function legacyProjectIdFor(workspaceRoot) {
3330
+ return legacyWorkspaceId(workspaceRoot);
3217
3331
  }
3218
3332
  function defaultMemoryDbPath() {
3219
3333
  return join7(homedir5(), ".prometheus", "memory.db");
@@ -3463,7 +3577,7 @@ function composeFromEnv(opts) {
3463
3577
  const override = (opts.workspaceRootOverride ?? "").trim();
3464
3578
  const envRoot = (env.PROMETHEUS_WORKSPACE_ROOT ?? "").trim();
3465
3579
  const claudeRoot = (env.CLAUDE_PROJECT_DIR ?? "").trim();
3466
- const workspaceRoot = resolve2(override !== "" ? override : envRoot !== "" ? envRoot : claudeRoot !== "" ? claudeRoot : process.cwd());
3580
+ const workspaceRoot = resolve3(override !== "" ? override : envRoot !== "" ? envRoot : claudeRoot !== "" ? claudeRoot : process.cwd());
3467
3581
  const projectId = projectIdFor(workspaceRoot);
3468
3582
  const projectName = basename(workspaceRoot) || workspaceRoot;
3469
3583
  const rawDbPath = env.PROMETHEUS_MEMORY_DB_PATH;
@@ -3482,6 +3596,21 @@ function composeFromEnv(opts) {
3482
3596
  temporal,
3483
3597
  dedup
3484
3598
  });
3599
+ const legacyId = legacyProjectIdFor(workspaceRoot);
3600
+ const canonMigrationOff = /^(off|0|false|no)$/i.test((env.PROMETHEUS_CANON_MIGRATION ?? "").trim());
3601
+ if (legacyId !== projectId && !canonMigrationOff) {
3602
+ try {
3603
+ const m = backend.migrateProjectIdentity(legacyId, projectId);
3604
+ const spool = migrateRecorderSpoolDir(legacyId, projectId, env);
3605
+ if (m.memoryRows > 0 || m.sessions > 0 || m.events > 0 || spool.renamedDir || spool.movedFiles > 0) {
3606
+ process.stderr.write(`[prom.codes] canonical-hash migration: ${m.memoryRows} memory row(s) (${m.memoryMerged} merged), ${m.sessions} session(s), ${m.events} event(s), spool ${spool.renamedDir ? "dir renamed" : `${spool.movedFiles} file(s)`} ${legacyId} -> ${projectId}
3607
+ `);
3608
+ }
3609
+ } catch (err) {
3610
+ process.stderr.write(`[prom.codes] canonical-hash memory migration skipped (non-fatal): ${err.message}
3611
+ `);
3612
+ }
3613
+ }
3485
3614
  return {
3486
3615
  backend,
3487
3616
  recorder: backend,
@@ -4150,6 +4279,21 @@ var curateInput = {
4150
4279
  limit: z.number().int().positive().max(MAX_LIMIT).optional()
4151
4280
  };
4152
4281
  var emptyInput = {};
4282
+ var sharedVerdictCache = /* @__PURE__ */ new Map();
4283
+ function memorySource(workspaceRoot, rootIsHomeOrFsRoot) {
4284
+ if (rootIsHomeOrFsRoot)
4285
+ return { backend: "local-sqlite", projectMemoryShared: false };
4286
+ let shared = sharedVerdictCache.get(workspaceRoot);
4287
+ if (shared === void 0) {
4288
+ try {
4289
+ shared = memoriesShared(workspaceRoot);
4290
+ } catch {
4291
+ shared = false;
4292
+ }
4293
+ sharedVerdictCache.set(workspaceRoot, shared);
4294
+ }
4295
+ return { backend: "local-sqlite", projectMemoryShared: shared };
4296
+ }
4153
4297
  function registerTools(server, source, hooks = {}) {
4154
4298
  const ready = typeof source === "function" ? source : () => Promise.resolve(source);
4155
4299
  const onToolCall = hooks.onToolCall;
@@ -4187,6 +4331,9 @@ function registerTools(server, source, hooks = {}) {
4187
4331
  projectFilesSynced: sync.synced,
4188
4332
  projectFilesPruned: sync.pruned,
4189
4333
  ...sync.skipped.length > 0 ? { skippedFiles: sync.skipped } : {},
4334
+ // Provenance (ask 5): memory is local-sqlite; `projectMemoryShared` says
4335
+ // whether these project memories are git-shared with the team.
4336
+ source: memorySource(workspaceRoot, deps.rootIsHomeOrFsRoot),
4190
4337
  woven: weave(records),
4191
4338
  recordsShown: bounded.shown,
4192
4339
  recordsFetched: bounded.fetched,
@@ -4327,6 +4474,8 @@ ${f.value}`);
4327
4474
  projectId,
4328
4475
  query: args.query,
4329
4476
  sources,
4477
+ // Provenance (ask 5): local-sqlite backend + git-shared project flag.
4478
+ source: memorySource(workspaceRoot, deps.rootIsHomeOrFsRoot),
4330
4479
  hits: hits.map((h) => ({ snippet: h.snippet, record: recordToJson(h.record) })),
4331
4480
  sessions: sessionHits
4332
4481
  });
@@ -4499,7 +4648,7 @@ ${f.value}`);
4499
4648
  embeddingsError = err instanceof Error ? err.message : String(err);
4500
4649
  }
4501
4650
  }
4502
- const update = await buildUpdateStatus("@prom.codes/memory-mcp", "0.15.2", { isDevBuild: false });
4651
+ const update = await buildUpdateStatus("@prom.codes/memory-mcp", "0.17.0", { isDevBuild: false });
4503
4652
  let recorder;
4504
4653
  try {
4505
4654
  const scopes = ["project-local", "project", "user"];
@@ -4581,7 +4730,7 @@ ${f.value}`);
4581
4730
  // dist/server.js
4582
4731
  var SERVER_IDENTITY = {
4583
4732
  name: "prometheus-memory-mcp",
4584
- version: "0.15.2",
4733
+ version: "0.17.0",
4585
4734
  title: "prom.codes Memory"
4586
4735
  };
4587
4736
  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.";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prom.codes/memory-mcp",
3
- "version": "0.15.2",
3
+ "version": "0.17.0",
4
4
  "description": "prom.codes Memory — persistent, local-first agent memory as an MCP server.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@modelcontextprotocol/sdk": "^1.29.0",
29
- "better-sqlite3": "^12.10.0",
29
+ "better-sqlite3": "12.10.0",
30
30
  "zod": "^4.4.3"
31
31
  },
32
32
  "optionalDependencies": {