@prom.codes/memory-mcp 0.7.2 → 0.8.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 -6
  2. package/dist/bin.js +210 -25
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -33,14 +33,21 @@ session end.
33
33
  `roots`) — no need to set `PROMETHEUS_WORKSPACE_ROOT`.
34
34
  - **Optional quality levers** (documented): `PROMETHEUS_MEMORY_RERANK_PROVIDER`
35
35
  (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`.
36
+ on LoCoMo), `PROMETHEUS_MEMORY_REWRITE_PROVIDER` (HyDE), and
37
+ `PROMETHEUS_MEMORY_DEDUP=on` (collapse restatements so distinct facts fill the
38
+ top-k). Temporal-intent ranking ("latest/earliest" queries) is **on by
39
+ default**; disable with `PROMETHEUS_MEMORY_TEMPORAL=off`.
40
+
41
+ If a fresh window opens with no project, the workspace falls back to the host
42
+ cwd (often home); in that case memory still works but project memories are NOT
43
+ mirrored to markdown there and `memory_setup` refuses to write rule files into
44
+ your home dir. Open a project folder so memories scope correctly.
39
45
 
40
46
  Tools (docked as `memory`): `memory_read`, `memory_write`, `memory_capture`,
41
- `memory_search`, `memory_list`, `memory_delete`, `memory_setup`. Secrets are
42
- rejected on every write. Your memories never leave your machine (only short
43
- query/record text transits when embeddings are enabled).
47
+ `memory_search`, `memory_list`, `memory_delete`, `memory_setup`,
48
+ `memory_status` (health check: which folder, how many records, does the key
49
+ work?). Secrets are rejected on every write. Your memories never leave your
50
+ machine (only short query/record text transits when embeddings are enabled).
44
51
 
45
52
  ## Native modules
46
53
 
package/dist/bin.js CHANGED
@@ -180,13 +180,27 @@ function notify(log, name, current, latest) {
180
180
  `);
181
181
  }
182
182
 
183
+ // ../shared/dist/workspace-root.js
184
+ import { homedir as homedir2 } from "node:os";
185
+ import { dirname, resolve } from "node:path";
186
+ function isHomeOrFilesystemRoot(root) {
187
+ const abs = resolve(root);
188
+ if (abs === "")
189
+ return true;
190
+ if (abs === resolve(homedir2()))
191
+ return true;
192
+ if (dirname(abs) === abs)
193
+ return true;
194
+ return false;
195
+ }
196
+
183
197
  // ../shared/dist/index.js
184
198
  var PROMETHEUS_VERSION = "0.1.0";
185
199
 
186
200
  // dist/composition.js
187
201
  import { createHash } from "node:crypto";
188
- import { homedir as homedir2 } from "node:os";
189
- import { basename, join as join2, resolve } from "node:path";
202
+ import { homedir as homedir3 } from "node:os";
203
+ import { basename, join as join2, resolve as resolve2 } from "node:path";
190
204
 
191
205
  // ../embeddings-openai-compat/dist/index.js
192
206
  var DEFAULT_BATCH = 96;
@@ -217,14 +231,14 @@ function parseRetryAfterMs(value, now = Date.now()) {
217
231
  return delta > 0 ? delta : 0;
218
232
  }
219
233
  function sleep(ms, signal) {
220
- return new Promise((resolve2, reject) => {
234
+ return new Promise((resolve3, reject) => {
221
235
  if (signal?.aborted === true) {
222
236
  reject(new Error("aborted"));
223
237
  return;
224
238
  }
225
239
  const timer = setTimeout(() => {
226
240
  signal?.removeEventListener("abort", onAbort);
227
- resolve2();
241
+ resolve3();
228
242
  }, ms);
229
243
  const onAbort = () => {
230
244
  clearTimeout(timer);
@@ -466,14 +480,14 @@ var DEFAULT_BATCH_CHARS = 4e5;
466
480
  var DEFAULT_RETRIES2 = 4;
467
481
  var DEFAULT_BACKOFF2 = 250;
468
482
  function sleep2(ms, signal) {
469
- return new Promise((resolve2, reject) => {
483
+ return new Promise((resolve3, reject) => {
470
484
  if (signal?.aborted === true) {
471
485
  reject(new Error("aborted"));
472
486
  return;
473
487
  }
474
488
  const timer = setTimeout(() => {
475
489
  signal?.removeEventListener("abort", onAbort);
476
- resolve2();
490
+ resolve3();
477
491
  }, ms);
478
492
  const onAbort = () => {
479
493
  clearTimeout(timer);
@@ -713,14 +727,14 @@ function parseRetryAfterMs2(value, now = Date.now()) {
713
727
  return delta > 0 ? delta : 0;
714
728
  }
715
729
  function sleep3(ms, signal) {
716
- return new Promise((resolve2, reject) => {
730
+ return new Promise((resolve3, reject) => {
717
731
  if (signal?.aborted === true) {
718
732
  reject(new Error("aborted"));
719
733
  return;
720
734
  }
721
735
  const timer = setTimeout(() => {
722
736
  signal?.removeEventListener("abort", onAbort);
723
- resolve2();
737
+ resolve3();
724
738
  }, ms);
725
739
  const onAbort = () => {
726
740
  clearTimeout(timer);
@@ -885,14 +899,14 @@ function parseRetryAfterMs3(value, now = Date.now()) {
885
899
  return delta > 0 ? delta : 0;
886
900
  }
887
901
  function sleep4(ms, signal) {
888
- return new Promise((resolve2, reject) => {
902
+ return new Promise((resolve3, reject) => {
889
903
  if (signal?.aborted === true) {
890
904
  reject(new Error("aborted"));
891
905
  return;
892
906
  }
893
907
  const timer = setTimeout(() => {
894
908
  signal?.removeEventListener("abort", onAbort);
895
- resolve2();
909
+ resolve3();
896
910
  }, ms);
897
911
  const onAbort = () => {
898
912
  clearTimeout(timer);
@@ -1254,7 +1268,7 @@ var OpenAICompatRewriter = class {
1254
1268
  // dist/sqlite.js
1255
1269
  import { randomUUID } from "node:crypto";
1256
1270
  import { mkdirSync } from "node:fs";
1257
- import { dirname } from "node:path";
1271
+ import { dirname as dirname2 } from "node:path";
1258
1272
  import Database from "better-sqlite3";
1259
1273
 
1260
1274
  // dist/rrf.js
@@ -1346,6 +1360,59 @@ function applyTemporalRanking(hits, intent, getTimestampMs, weight = DEFAULT_TEM
1346
1360
  return scored.map((s) => s.hit);
1347
1361
  }
1348
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
+
1349
1416
  // dist/types.js
1350
1417
  var MEMORY_SCOPES = [
1351
1418
  "system",
@@ -1521,12 +1588,14 @@ var SqliteMemoryBackend = class {
1521
1588
  rewriter;
1522
1589
  temporalEnabled;
1523
1590
  temporalWeight;
1591
+ dedupEnabled;
1592
+ dedupThreshold;
1524
1593
  /** Record ids whose vector is missing/stale, awaiting a batched embed. */
1525
1594
  pendingEmbed = /* @__PURE__ */ new Set();
1526
1595
  closed = false;
1527
1596
  constructor(dbPath, opts = {}) {
1528
1597
  if (dbPath !== ":memory:") {
1529
- mkdirSync(dirname(dbPath), { recursive: true });
1598
+ mkdirSync(dirname2(dbPath), { recursive: true });
1530
1599
  }
1531
1600
  this.db = new Database(dbPath);
1532
1601
  this.db.pragma("journal_mode = WAL");
@@ -1539,6 +1608,8 @@ var SqliteMemoryBackend = class {
1539
1608
  this.rewriter = opts.rewriter;
1540
1609
  this.temporalEnabled = opts.temporal?.enabled ?? false;
1541
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;
1542
1613
  if (this.embedder !== void 0)
1543
1614
  this.queueUnembedded();
1544
1615
  }
@@ -1583,6 +1654,35 @@ var SqliteMemoryBackend = class {
1583
1654
  const row = this.db.prepare(`SELECT * FROM agent_memory WHERE id = ?`).get(id);
1584
1655
  return rowToRecord(row);
1585
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
+ }
1586
1686
  async read(query) {
1587
1687
  if (query.chain.length === 0)
1588
1688
  return [];
@@ -1596,7 +1696,8 @@ var SqliteMemoryBackend = class {
1596
1696
  sql += ` ORDER BY updated_at DESC`;
1597
1697
  const rows = this.db.prepare(sql).all(...params);
1598
1698
  const resolved = resolveScopeChain(rows.map(rowToRecord), query.chain);
1599
- 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;
1600
1701
  const bump = this.db.prepare(`UPDATE agent_memory SET use_count = use_count + 1 WHERE id = ?`);
1601
1702
  for (const rec of limited) {
1602
1703
  bump.run(rec.id);
@@ -1661,7 +1762,9 @@ var SqliteMemoryBackend = class {
1661
1762
  { id: "vec", items: vecHits.map((h) => ({ key: h.record.id, payload: h })) }
1662
1763
  ], { limit: poolLimit }).map((f) => f.payload);
1663
1764
  }
1664
- 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);
1665
1768
  const temporalOn = input.temporal ?? this.temporalEnabled;
1666
1769
  const ordered = temporalOn ? this.applyTemporal(input.query, reranked) : reranked;
1667
1770
  return ordered.slice(0, finalLimit);
@@ -1870,6 +1973,16 @@ ${h.record.value}`
1870
1973
  this.audit("delete", input, result.changes > 0 ? "removed" : "no-op");
1871
1974
  return result.changes > 0;
1872
1975
  }
1976
+ async stats(projectId) {
1977
+ const rows = this.db.prepare(`SELECT scope, COUNT(*) AS n FROM agent_memory WHERE project_id = ? GROUP BY scope`).all(projectId);
1978
+ let total = 0;
1979
+ const byScope = {};
1980
+ for (const r of rows) {
1981
+ byScope[r.scope] = r.n;
1982
+ total += r.n;
1983
+ }
1984
+ return { total, byScope };
1985
+ }
1873
1986
  async consolidate(input) {
1874
1987
  const written = [];
1875
1988
  if (input.plan || input.outcome) {
@@ -1926,11 +2039,11 @@ ${h.record.value}`
1926
2039
 
1927
2040
  // dist/composition.js
1928
2041
  function projectIdFor(workspaceRoot) {
1929
- const abs = resolve(workspaceRoot);
2042
+ const abs = resolve2(workspaceRoot);
1930
2043
  return createHash("sha256").update(abs).digest("hex").slice(0, 16);
1931
2044
  }
1932
2045
  function defaultMemoryDbPath() {
1933
- return join2(homedir2(), ".prometheus", "memory.db");
2046
+ return join2(homedir3(), ".prometheus", "memory.db");
1934
2047
  }
1935
2048
  function intEnv(env, name, def) {
1936
2049
  const raw = env[name];
@@ -2144,12 +2257,24 @@ function discoverMemoryTemporal(env) {
2144
2257
  }
2145
2258
  return { enabled, weight };
2146
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
+ }
2147
2272
  function composeFromEnv(opts) {
2148
2273
  const env = opts.env;
2149
2274
  const override = (opts.workspaceRootOverride ?? "").trim();
2150
2275
  const envRoot = (env.PROMETHEUS_WORKSPACE_ROOT ?? "").trim();
2151
2276
  const claudeRoot = (env.CLAUDE_PROJECT_DIR ?? "").trim();
2152
- const workspaceRoot = resolve(override !== "" ? override : envRoot !== "" ? envRoot : claudeRoot !== "" ? claudeRoot : process.cwd());
2277
+ const workspaceRoot = resolve2(override !== "" ? override : envRoot !== "" ? envRoot : claudeRoot !== "" ? claudeRoot : process.cwd());
2153
2278
  const projectId = projectIdFor(workspaceRoot);
2154
2279
  const projectName = basename(workspaceRoot) || workspaceRoot;
2155
2280
  const rawDbPath = env.PROMETHEUS_MEMORY_DB_PATH;
@@ -2159,11 +2284,13 @@ function composeFromEnv(opts) {
2159
2284
  const { id: extractorId, provider: extractor } = discoverMemoryExtractor(env);
2160
2285
  const { id: rewriterId, provider: rewriter } = discoverMemoryRewriter(env);
2161
2286
  const temporal = discoverMemoryTemporal(env);
2287
+ const dedup = discoverMemoryDedup(env);
2162
2288
  const backend = new SqliteMemoryBackend(dbPath, {
2163
2289
  ...embedder !== void 0 ? { embedder } : {},
2164
2290
  ...reranker !== null ? { reranker } : {},
2165
2291
  ...rewriter !== null ? { rewriter } : {},
2166
- temporal
2292
+ temporal,
2293
+ dedup
2167
2294
  });
2168
2295
  return {
2169
2296
  backend,
@@ -2173,6 +2300,7 @@ function composeFromEnv(opts) {
2173
2300
  dbPath,
2174
2301
  embeddingsEnabled: embedder !== void 0,
2175
2302
  embedderId,
2303
+ embedder,
2176
2304
  reranker,
2177
2305
  rerankerId,
2178
2306
  extractor,
@@ -2180,6 +2308,8 @@ function composeFromEnv(opts) {
2180
2308
  rewriter,
2181
2309
  rewriterId,
2182
2310
  temporalEnabled: temporal.enabled,
2311
+ dedupEnabled: dedup.enabled,
2312
+ rootIsHomeOrFsRoot: isHomeOrFilesystemRoot(workspaceRoot),
2183
2313
  close: () => backend.close()
2184
2314
  };
2185
2315
  }
@@ -2338,7 +2468,7 @@ function assertNoSecrets(text) {
2338
2468
  // dist/setup.js
2339
2469
  import { existsSync } from "node:fs";
2340
2470
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
2341
- import { dirname as dirname2, join as join4 } from "node:path";
2471
+ import { dirname as dirname3, join as join4 } from "node:path";
2342
2472
  var MEMORY_RUNTIMES = [
2343
2473
  "claude-code",
2344
2474
  "cursor",
@@ -2418,7 +2548,7 @@ async function installRuntime(workspaceRoot, runtime) {
2418
2548
  if (exists && before === after) {
2419
2549
  return { runtime, path: absPath, action: "unchanged" };
2420
2550
  }
2421
- await mkdir3(dirname2(absPath), { recursive: true });
2551
+ await mkdir3(dirname3(absPath), { recursive: true });
2422
2552
  await writeFile3(absPath, after, "utf-8");
2423
2553
  return { runtime, path: absPath, action: exists ? "updated" : "created" };
2424
2554
  }
@@ -2576,15 +2706,17 @@ var runtimeEnum = z.enum(MEMORY_RUNTIMES);
2576
2706
  var setupInput = {
2577
2707
  runtimes: z.array(runtimeEnum).min(1).optional()
2578
2708
  };
2709
+ var emptyInput = {};
2579
2710
  function registerTools(server, deps) {
2580
2711
  const { backend, workspaceRoot, projectId, projectName, dbPath, extractor } = deps;
2712
+ const mirrorToFiles = !deps.rootIsHomeOrFsRoot;
2581
2713
  server.registerTool("read", {
2582
2714
  title: "Recall agent memory",
2583
2715
  description: "Read agent memory for this project along the scope chain (project \u2192 workspace \u2192 tenant \u2192 system; narrowest scope wins). Syncs `.prometheus/memories/*.md` first, then returns the resolved records plus a prompt-ready `woven` markdown block (token-capped). Call this at the START of a session or task to recall what earlier sessions learned.",
2584
2716
  inputSchema: readInput
2585
2717
  }, async (args) => {
2586
2718
  const limit = clampLimit(args.limit, DEFAULT_READ_LIMIT);
2587
- const synced = await syncProjectFiles(backend, { projectId, workspaceRoot });
2719
+ const synced = mirrorToFiles ? await syncProjectFiles(backend, { projectId, workspaceRoot }) : 0;
2588
2720
  const records = await backend.read({
2589
2721
  chain: defaultScopeChain(projectId),
2590
2722
  types: args.types,
@@ -2620,7 +2752,7 @@ ${args.value}`);
2620
2752
  source: "user"
2621
2753
  });
2622
2754
  let projectFile = null;
2623
- if (scope === "project" && args.type === "semantic") {
2755
+ if (mirrorToFiles && scope === "project" && args.type === "semantic") {
2624
2756
  projectFile = await writeProjectFile(workspaceRoot, args.key, args.value);
2625
2757
  }
2626
2758
  return textResult({ record: recordToJson(record), projectFile });
@@ -2694,7 +2826,8 @@ ${f.value}`);
2694
2826
  inputSchema: searchInput
2695
2827
  }, async (args) => {
2696
2828
  const limit = clampLimit(args.limit, 20);
2697
- await syncProjectFiles(backend, { projectId, workspaceRoot });
2829
+ if (mirrorToFiles)
2830
+ await syncProjectFiles(backend, { projectId, workspaceRoot });
2698
2831
  const hits = await backend.search({
2699
2832
  chain: defaultScopeChain(projectId),
2700
2833
  query: args.query,
@@ -2744,7 +2877,7 @@ ${f.value}`);
2744
2877
  key: args.key
2745
2878
  });
2746
2879
  let fileRemoved = false;
2747
- if (scope === "project" && args.type === "semantic") {
2880
+ if (mirrorToFiles && scope === "project" && args.type === "semantic") {
2748
2881
  fileRemoved = await deleteProjectFile(workspaceRoot, args.key);
2749
2882
  }
2750
2883
  return textResult({ removed, fileRemoved });
@@ -2754,6 +2887,13 @@ ${f.value}`);
2754
2887
  description: "Idempotently install the Prometheus memory-protocol rule block into agent runtime configs in this workspace: CLAUDE.md (claude-code), .cursor/rules/prometheus-memory.mdc (cursor), .augment/rules/prometheus-memory.md (augment), AGENTS.md (agents). Without `runtimes` it auto-detects which runtimes are present (fallback: agents). Only the marked block is written \u2014 existing content is never touched. Re-running updates the block in place.",
2755
2888
  inputSchema: setupInput
2756
2889
  }, async (args) => {
2890
+ if (!mirrorToFiles) {
2891
+ return textResult({
2892
+ workspaceRoot,
2893
+ installed: false,
2894
+ reason: "Workspace resolved to your home directory or a filesystem root \u2014 refusing to write rule files there. Open your project folder (Claude Code passes it via CLAUDE_PROJECT_DIR) or set PROMETHEUS_WORKSPACE_ROOT, then retry."
2895
+ });
2896
+ }
2757
2897
  const runtimes = args.runtimes ?? detectRuntimes(workspaceRoot);
2758
2898
  const results = [];
2759
2899
  for (const runtime of runtimes) {
@@ -2761,6 +2901,47 @@ ${f.value}`);
2761
2901
  }
2762
2902
  return textResult({ workspaceRoot, results });
2763
2903
  });
2904
+ server.registerTool("status", {
2905
+ title: "Memory status / health check",
2906
+ description: "Health check for this project's agent memory. Reports the resolved workspace root, project id, DB path, how many records are stored (total + by scope), the embedding provider with a zero-cost key-reachability probe, and which quality levers are active (rerank / rewrite / temporal). CALL THIS to confirm where memory is stored, how much is there, and whether the API key works.",
2907
+ inputSchema: emptyInput
2908
+ }, async () => {
2909
+ const stats = await backend.stats(projectId);
2910
+ let embeddingsReachable = null;
2911
+ let embeddingsError = null;
2912
+ const resolveIdentity = deps.embedder?.resolveIdentity;
2913
+ if (deps.embeddingsEnabled && typeof resolveIdentity === "function") {
2914
+ try {
2915
+ await resolveIdentity.call(deps.embedder);
2916
+ embeddingsReachable = true;
2917
+ } catch (err) {
2918
+ embeddingsReachable = false;
2919
+ embeddingsError = err instanceof Error ? err.message : String(err);
2920
+ }
2921
+ }
2922
+ const summary = deps.rootIsHomeOrFsRoot ? `Memory at ${dbPath}: ${stats.total} records, but the workspace resolved to ${workspaceRoot} (home/root) \u2014 open a project folder so memories scope and mirror correctly.` : `Memory at ${dbPath}: ${stats.total} records for project "${projectName}".`;
2923
+ return textResult({
2924
+ installed: true,
2925
+ project: { id: projectId, name: projectName, workspaceRoot },
2926
+ rootIsHomeOrFsRoot: deps.rootIsHomeOrFsRoot,
2927
+ storage: { dbPath, projectFileMirror: mirrorToFiles },
2928
+ records: { total: stats.total, byScope: stats.byScope },
2929
+ embeddings: {
2930
+ enabled: deps.embeddingsEnabled,
2931
+ provider: deps.embedderId,
2932
+ reachable: embeddingsReachable,
2933
+ ...embeddingsError !== null ? { error: embeddingsError } : {}
2934
+ },
2935
+ levers: {
2936
+ rerank: deps.rerankerId,
2937
+ rewrite: deps.rewriterId,
2938
+ temporal: deps.temporalEnabled,
2939
+ dedup: deps.dedupEnabled,
2940
+ extract: deps.extractorId
2941
+ },
2942
+ summary
2943
+ });
2944
+ });
2764
2945
  }
2765
2946
 
2766
2947
  // dist/server.js
@@ -2806,8 +2987,12 @@ async function main() {
2806
2987
  env,
2807
2988
  ...override !== void 0 && override !== "" ? { workspaceRootOverride: override } : {}
2808
2989
  });
2809
- 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"}
2990
+ 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"}
2991
+ `);
2992
+ if (composed.rootIsHomeOrFsRoot) {
2993
+ 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.
2810
2994
  `);
2995
+ }
2811
2996
  registerTools(server, composed);
2812
2997
  };
2813
2998
  if (eagerVia !== null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prom.codes/memory-mcp",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "prom.codes Memory — persistent, local-first agent memory as an MCP server.",
5
5
  "type": "module",
6
6
  "bin": {