@prom.codes/memory-mcp 0.10.2 → 0.11.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 +3 -2
  2. package/dist/bin.js +212 -32
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -51,8 +51,9 @@ your home dir. Open a project folder so memories scope correctly.
51
51
  Tools (docked as `memory`): `memory_read`, `memory_write`, `memory_capture`,
52
52
  `memory_search`, `memory_list`, `memory_delete`, `memory_setup`,
53
53
  `memory_status` (health check: which folder, how many records, does the key
54
- work?). Secrets are rejected on every write. Your memories never leave your
55
- machine (only short query/record text transits when embeddings are enabled).
54
+ work, is a newer version published?). Secrets are rejected on every write.
55
+ Your memories never leave your machine (only short query/record text
56
+ transits when embeddings are enabled).
56
57
 
57
58
  ## Native modules
58
59
 
package/dist/bin.js CHANGED
@@ -34,10 +34,11 @@ var LANGUAGE_IDS = [
34
34
  ];
35
35
 
36
36
  // ../shared/dist/update-check.js
37
- import { mkdir, readFile, writeFile } from "node:fs/promises";
37
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
38
38
  import { homedir } from "node:os";
39
39
  import { join } from "node:path";
40
40
  import { fileURLToPath } from "node:url";
41
+ var UPGRADE_COMMAND = "npm install -g @prom.codes/context-mcp @prom.codes/memory-mcp @prom.codes/saver --ignore-scripts=false --foreground-scripts";
41
42
  async function packageIdentity(binImportMetaUrl) {
42
43
  try {
43
44
  const binPath = fileURLToPath(binImportMetaUrl);
@@ -92,6 +93,29 @@ function cachePath(dir, name) {
92
93
  const safe = name.replace(/[^a-zA-Z0-9._-]+/g, "_");
93
94
  return join(dir, `.update-check-${safe}.json`);
94
95
  }
96
+ function availabilityMarkerPath(dir, name) {
97
+ const safe = name.replace(/[^a-zA-Z0-9._-]+/g, "_");
98
+ return join(dir, `.update-available-${safe}.json`);
99
+ }
100
+ async function syncAvailabilityMarker(dir, name, current, latest, updateAvailable) {
101
+ const file = availabilityMarkerPath(dir, name);
102
+ try {
103
+ if (updateAvailable && latest !== null) {
104
+ const marker = {
105
+ name,
106
+ current,
107
+ latest,
108
+ command: UPGRADE_COMMAND,
109
+ notedAt: Date.now()
110
+ };
111
+ await mkdir(dir, { recursive: true }).catch(() => void 0);
112
+ await writeFile(file, JSON.stringify(marker), "utf8");
113
+ } else if (latest !== null) {
114
+ await rm(file, { force: true });
115
+ }
116
+ } catch {
117
+ }
118
+ }
95
119
  async function readCache(path2) {
96
120
  try {
97
121
  const raw = await readFile(path2, "utf8");
@@ -120,7 +144,7 @@ async function fetchLatest(name, fetchImpl, timeoutMs) {
120
144
  const url = `https://registry.npmjs.org/${name.replace("/", "%2F")}/latest`;
121
145
  const res = await fetchImpl(url, {
122
146
  signal: controller.signal,
123
- headers: { accept: "application/vnd.npm.install-v1+json" }
147
+ headers: { accept: "application/json" }
124
148
  });
125
149
  if (!res.ok)
126
150
  return null;
@@ -155,6 +179,7 @@ async function checkForUpdate(options) {
155
179
  const updateAvailable2 = cached.latest !== null && isNewerVersion(cached.latest, version);
156
180
  if (updateAvailable2)
157
181
  notify(log, name, version, cached.latest);
182
+ await syncAvailabilityMarker(cacheDir, name, version, cached.latest, updateAvailable2);
158
183
  return {
159
184
  ...base,
160
185
  latest: cached.latest,
@@ -173,13 +198,62 @@ async function checkForUpdate(options) {
173
198
  const updateAvailable = isNewerVersion(latest, version);
174
199
  if (updateAvailable)
175
200
  notify(log, name, version, latest);
201
+ await syncAvailabilityMarker(cacheDir, name, version, latest, updateAvailable);
176
202
  return { ...base, latest, checked: true, updateAvailable };
177
203
  }
204
+ async function getLatestVersion(name, options = {}) {
205
+ const { env = process.env, fetch: fetchImpl = globalThis.fetch, cacheDir = join(homedir(), ".prometheus"), cacheTtlMs = DEFAULT_TTL_MS, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
206
+ const file = cachePath(cacheDir, name);
207
+ const cached = await readCache(file);
208
+ const now = Date.now();
209
+ if (cached !== null && cached.latest !== null && now - cached.checkedAt < cacheTtlMs) {
210
+ return cached.latest;
211
+ }
212
+ if (OPT_OUT_RE.test(env.PROMETHEUS_NO_UPDATE_CHECK ?? "") || typeof fetchImpl !== "function") {
213
+ return cached?.latest ?? null;
214
+ }
215
+ const latest = await fetchLatest(name, fetchImpl, timeoutMs);
216
+ if (latest !== null) {
217
+ await mkdir(cacheDir, { recursive: true }).catch(() => void 0);
218
+ await writeCache(file, { checkedAt: now, latest });
219
+ return latest;
220
+ }
221
+ return cached?.latest ?? null;
222
+ }
178
223
  function notify(log, name, current, latest) {
179
224
  log(`${name}: a newer version (${latest}) is available \u2014 you are on ${current}. npx users get it automatically on the next restart; for a global install run \`npm update -g ${name}\`. (Set PROMETHEUS_NO_UPDATE_CHECK=1 to silence.)
180
225
  `);
181
226
  }
182
227
 
228
+ // ../shared/dist/update-info.js
229
+ async function buildUpdateStatus(pkgName, currentVersion, options = {}) {
230
+ const base = { current: currentVersion, command: UPGRADE_COMMAND };
231
+ if (options.isDevBuild === true) {
232
+ return {
233
+ ...base,
234
+ latest: null,
235
+ updateAvailable: null,
236
+ note: "dev build (workspace) \u2014 version comparison skipped"
237
+ };
238
+ }
239
+ let latest = null;
240
+ try {
241
+ latest = await getLatestVersion(pkgName, {
242
+ ...options.env !== void 0 ? { env: options.env } : {},
243
+ ...options.fetch !== void 0 ? { fetch: options.fetch } : {},
244
+ ...options.cacheDir !== void 0 ? { cacheDir: options.cacheDir } : {},
245
+ timeoutMs: options.timeoutMs ?? 1500
246
+ });
247
+ } catch {
248
+ latest = null;
249
+ }
250
+ return {
251
+ ...base,
252
+ latest,
253
+ updateAvailable: latest !== null ? isNewerVersion(latest, currentVersion) : null
254
+ };
255
+ }
256
+
183
257
  // ../shared/dist/workspace-root.js
184
258
  import { homedir as homedir2 } from "node:os";
185
259
  import { dirname, resolve } from "node:path";
@@ -194,10 +268,80 @@ function isHomeOrFilesystemRoot(root) {
194
268
  return false;
195
269
  }
196
270
 
271
+ // ../shared/dist/heartbeat.js
272
+ import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
273
+ import { homedir as homedir3 } from "node:os";
274
+ import { join as join2 } from "node:path";
275
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 6e4;
276
+ var STALE_AFTER_MS = 5 * 6e4;
277
+ function defaultStatusDir(env = process.env) {
278
+ const override = (env.PROMETHEUS_STATUS_DIR ?? "").trim();
279
+ if (override !== "")
280
+ return override;
281
+ return join2(homedir3(), ".prometheus", "status");
282
+ }
283
+ function startHeartbeat(options) {
284
+ const dir = options.dir ?? defaultStatusDir(options.env ?? process.env);
285
+ const file = join2(dir, `${options.server}-${process.pid}.json`);
286
+ const intervalMs = options.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
287
+ let record = {
288
+ server: options.server,
289
+ pid: process.pid,
290
+ version: options.version,
291
+ startedAt: Date.now(),
292
+ updatedAt: Date.now(),
293
+ workspaceRoot: options.workspaceRoot ?? null
294
+ };
295
+ let stopped = false;
296
+ const persist = () => {
297
+ if (stopped)
298
+ return;
299
+ try {
300
+ mkdirSync(dir, { recursive: true });
301
+ writeFileSync(file, JSON.stringify(record), "utf8");
302
+ } catch {
303
+ }
304
+ };
305
+ const remove = () => {
306
+ try {
307
+ rmSync(file, { force: true });
308
+ } catch {
309
+ }
310
+ };
311
+ persist();
312
+ const timer = setInterval(() => {
313
+ record = { ...record, updatedAt: Date.now() };
314
+ persist();
315
+ }, intervalMs);
316
+ timer.unref?.();
317
+ const onExit = () => {
318
+ stopped = true;
319
+ remove();
320
+ };
321
+ process.once("exit", onExit);
322
+ return {
323
+ file,
324
+ update(patch) {
325
+ if (stopped)
326
+ return;
327
+ record = { ...record, ...patch, updatedAt: Date.now() };
328
+ persist();
329
+ },
330
+ stop() {
331
+ if (stopped)
332
+ return;
333
+ stopped = true;
334
+ clearInterval(timer);
335
+ process.removeListener("exit", onExit);
336
+ remove();
337
+ }
338
+ };
339
+ }
340
+
197
341
  // dist/composition.js
198
342
  import { createHash } from "node:crypto";
199
- import { homedir as homedir3 } from "node:os";
200
- import { basename, join as join2, resolve as resolve2 } from "node:path";
343
+ import { homedir as homedir4 } from "node:os";
344
+ import { basename, join as join3, resolve as resolve2 } from "node:path";
201
345
 
202
346
  // ../embeddings-openai-compat/dist/index.js
203
347
  var DEFAULT_BATCH = 96;
@@ -1445,7 +1589,7 @@ var OpenAICompatRewriter = class {
1445
1589
 
1446
1590
  // dist/sqlite.js
1447
1591
  import { randomUUID } from "node:crypto";
1448
- import { mkdirSync } from "node:fs";
1592
+ import { mkdirSync as mkdirSync2 } from "node:fs";
1449
1593
  import { dirname as dirname2 } from "node:path";
1450
1594
  import Database from "better-sqlite3";
1451
1595
 
@@ -1773,7 +1917,7 @@ var SqliteMemoryBackend = class {
1773
1917
  closed = false;
1774
1918
  constructor(dbPath, opts = {}) {
1775
1919
  if (dbPath !== ":memory:") {
1776
- mkdirSync(dirname2(dbPath), { recursive: true });
1920
+ mkdirSync2(dirname2(dbPath), { recursive: true });
1777
1921
  }
1778
1922
  this.db = new Database(dbPath);
1779
1923
  this.db.pragma("journal_mode = WAL");
@@ -2226,7 +2370,7 @@ function projectIdFor(workspaceRoot) {
2226
2370
  return createHash("sha256").update(abs).digest("hex").slice(0, 16);
2227
2371
  }
2228
2372
  function defaultMemoryDbPath() {
2229
- return join2(homedir3(), ".prometheus", "memory.db");
2373
+ return join3(homedir4(), ".prometheus", "memory.db");
2230
2374
  }
2231
2375
  function intEnv(env, name, def) {
2232
2376
  const raw = env[name];
@@ -2667,9 +2811,9 @@ function assertNoSecrets(text) {
2667
2811
  }
2668
2812
 
2669
2813
  // dist/setup.js
2670
- import { existsSync, readFileSync } from "node:fs";
2814
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
2671
2815
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
2672
- import { dirname as dirname3, join as join4 } from "node:path";
2816
+ import { dirname as dirname3, join as join5 } from "node:path";
2673
2817
  var MEMORY_RUNTIMES = [
2674
2818
  "claude-code",
2675
2819
  "cursor",
@@ -2713,13 +2857,13 @@ alwaysApply: true
2713
2857
  var TARGETS = {
2714
2858
  "claude-code": { relPath: "CLAUDE.md", mode: "block", detect: "CLAUDE.md" },
2715
2859
  cursor: {
2716
- relPath: join4(".cursor", "rules", "prometheus-memory.mdc"),
2860
+ relPath: join5(".cursor", "rules", "prometheus-memory.mdc"),
2717
2861
  mode: "file",
2718
2862
  fileContent: CURSOR_FRONTMATTER + withMarkers(RULE_BLOCK) + "\n",
2719
2863
  detect: ".cursor"
2720
2864
  },
2721
2865
  augment: {
2722
- relPath: join4(".augment", "rules", "prometheus-memory.md"),
2866
+ relPath: join5(".augment", "rules", "prometheus-memory.md"),
2723
2867
  mode: "file",
2724
2868
  fileContent: withMarkers(RULE_BLOCK) + "\n",
2725
2869
  detect: ".augment"
@@ -2727,19 +2871,19 @@ var TARGETS = {
2727
2871
  agents: { relPath: "AGENTS.md", mode: "block", detect: "AGENTS.md" }
2728
2872
  };
2729
2873
  function detectRuntimes(workspaceRoot) {
2730
- const found = MEMORY_RUNTIMES.filter((rt) => existsSync(join4(workspaceRoot, TARGETS[rt].detect)));
2874
+ const found = MEMORY_RUNTIMES.filter((rt) => existsSync(join5(workspaceRoot, TARGETS[rt].detect)));
2731
2875
  return found.length > 0 ? found : ["agents"];
2732
2876
  }
2733
2877
  function existingRuntimes(workspaceRoot) {
2734
- return MEMORY_RUNTIMES.filter((rt) => existsSync(join4(workspaceRoot, TARGETS[rt].detect)));
2878
+ return MEMORY_RUNTIMES.filter((rt) => existsSync(join5(workspaceRoot, TARGETS[rt].detect)));
2735
2879
  }
2736
2880
  function installedRuntimes(workspaceRoot) {
2737
2881
  return MEMORY_RUNTIMES.filter((rt) => {
2738
- const p = join4(workspaceRoot, TARGETS[rt].relPath);
2882
+ const p = join5(workspaceRoot, TARGETS[rt].relPath);
2739
2883
  if (!existsSync(p))
2740
2884
  return false;
2741
2885
  try {
2742
- return readFileSync(p, "utf-8").includes(BLOCK_START);
2886
+ return readFileSync2(p, "utf-8").includes(BLOCK_START);
2743
2887
  } catch {
2744
2888
  return false;
2745
2889
  }
@@ -2764,7 +2908,7 @@ function upsertBlock(existing, block) {
2764
2908
  }
2765
2909
  async function installRuntime(workspaceRoot, runtime) {
2766
2910
  const target = TARGETS[runtime];
2767
- const absPath = join4(workspaceRoot, target.relPath);
2911
+ const absPath = join5(workspaceRoot, target.relPath);
2768
2912
  const exists = existsSync(absPath);
2769
2913
  const before = exists ? await readFile3(absPath, "utf-8") : "";
2770
2914
  const after = target.mode === "file" ? target.fileContent : upsertBlock(before, RULE_BLOCK);
@@ -2930,9 +3074,19 @@ var setupInput = {
2930
3074
  runtimes: z.array(runtimeEnum).min(1).optional()
2931
3075
  };
2932
3076
  var emptyInput = {};
2933
- function registerTools(server, source) {
3077
+ function registerTools(server, source, hooks = {}) {
2934
3078
  const ready = typeof source === "function" ? source : () => Promise.resolve(source);
2935
- server.registerTool("read", {
3079
+ const onToolCall = hooks.onToolCall;
3080
+ const reg = ((name, meta, handler) => server.registerTool(name, meta, (...callArgs) => {
3081
+ if (onToolCall !== void 0) {
3082
+ try {
3083
+ onToolCall(name);
3084
+ } catch {
3085
+ }
3086
+ }
3087
+ return handler(...callArgs);
3088
+ }));
3089
+ reg("read", {
2936
3090
  title: "Recall agent memory",
2937
3091
  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.",
2938
3092
  inputSchema: readInput
@@ -2955,7 +3109,7 @@ function registerTools(server, source) {
2955
3109
  records: records.map(recordToJson)
2956
3110
  });
2957
3111
  });
2958
- server.registerTool("write", {
3112
+ reg("write", {
2959
3113
  title: "Store agent memory",
2960
3114
  description: "Upsert one memory record (identity: scope+type+key). Use type `semantic` for durable facts, `procedural` for how-to knowledge, `episodic` for session events, `working` for short-lived notes. Default scope `project` also mirrors the value to `.prometheus/memories/<key>.md` (git-versioned, human-editable). Values matching the secret deny-list are rejected. Call this whenever the user states a durable preference, decision, or correction worth remembering.",
2961
3115
  inputSchema: writeInput
@@ -2985,7 +3139,7 @@ ${args.value}`);
2985
3139
  }
2986
3140
  return textResult({ record: recordToJson(record), projectFile });
2987
3141
  });
2988
- server.registerTool("capture", {
3142
+ reg("capture", {
2989
3143
  title: "Consolidate session learnings",
2990
3144
  description: "Session-end consolidation: `plan`/`outcome` become one episodic record (key = sessionId), `facts` become semantic upserts, `procedures` become procedural upserts. Secret-bearing payloads are rejected. Call this at the END of a session to persist what was learned.",
2991
3145
  inputSchema: captureInput
@@ -3049,7 +3203,7 @@ ${f.value}`);
3049
3203
  });
3050
3204
  return textResult({ written: written.map(recordToJson), extracted: extractedCount });
3051
3205
  });
3052
- server.registerTool("search", {
3206
+ reg("search", {
3053
3207
  title: "Search agent memory",
3054
3208
  description: "Full-text search (FTS5) over memory keys and values within this project's scope chain, ranked by relevance. Returns matching records plus a highlighted snippet per hit. Use this when memory_read's recall is not specific enough. Does not bump useCount.",
3055
3209
  inputSchema: searchInput
@@ -3075,7 +3229,7 @@ ${f.value}`);
3075
3229
  }))
3076
3230
  });
3077
3231
  });
3078
- server.registerTool("list", {
3232
+ reg("list", {
3079
3233
  title: "List stored memory (admin)",
3080
3234
  description: "Flat listing of this project's memory records without scope resolution \u2014 inspection/debug surface. Optional filters: scope, type, keyContains (case-insensitive substring).",
3081
3235
  inputSchema: listInput
@@ -3096,7 +3250,7 @@ ${f.value}`);
3096
3250
  records: records.map(recordToJson)
3097
3251
  });
3098
3252
  });
3099
- server.registerTool("delete", {
3253
+ reg("delete", {
3100
3254
  title: "Delete stored memory",
3101
3255
  description: "Delete one memory record by identity (scope+type+key). For project-scoped semantic records the mirrored `.prometheus/memories/<key>.md` file is removed as well. Returns whether a record/file was actually removed.",
3102
3256
  inputSchema: deleteInput
@@ -3118,7 +3272,7 @@ ${f.value}`);
3118
3272
  }
3119
3273
  return textResult({ removed, fileRemoved });
3120
3274
  });
3121
- server.registerTool("setup", {
3275
+ reg("setup", {
3122
3276
  title: "Install memory rules into runtime configs",
3123
3277
  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.",
3124
3278
  inputSchema: setupInput
@@ -3140,7 +3294,7 @@ ${f.value}`);
3140
3294
  }
3141
3295
  return textResult({ workspaceRoot, results });
3142
3296
  });
3143
- server.registerTool("status", {
3297
+ reg("status", {
3144
3298
  title: "Memory status / health check",
3145
3299
  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.",
3146
3300
  inputSchema: emptyInput
@@ -3161,7 +3315,8 @@ ${f.value}`);
3161
3315
  embeddingsError = err instanceof Error ? err.message : String(err);
3162
3316
  }
3163
3317
  }
3164
- 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}".`;
3318
+ const update = await buildUpdateStatus("@prom.codes/memory-mcp", "0.11.0", { isDevBuild: false });
3319
+ 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}".${update.updateAvailable === true ? ` Update available: ${update.current} \u2192 ${update.latest} (ask me to run update via the context server's update_servers tool).` : ""}`;
3165
3320
  return textResult({
3166
3321
  installed: true,
3167
3322
  project: { id: projectId, name: projectName, workspaceRoot },
@@ -3187,6 +3342,7 @@ ${f.value}`);
3187
3342
  dedup: deps.dedupEnabled,
3188
3343
  extract: deps.extractorId
3189
3344
  },
3345
+ update,
3190
3346
  summary
3191
3347
  });
3192
3348
  });
@@ -3195,7 +3351,7 @@ ${f.value}`);
3195
3351
  // dist/server.js
3196
3352
  var SERVER_IDENTITY = {
3197
3353
  name: "prometheus-memory-mcp",
3198
- version: "0.10.2",
3354
+ version: "0.11.0",
3199
3355
  title: "prom.codes Memory"
3200
3356
  };
3201
3357
  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.";
@@ -3211,6 +3367,11 @@ async function main() {
3211
3367
  const claudeRoot = (env.CLAUDE_PROJECT_DIR ?? "").trim();
3212
3368
  const eagerVia = explicitRoot !== "" ? "PROMETHEUS_WORKSPACE_ROOT" : claudeRoot !== "" ? "CLAUDE_PROJECT_DIR" : null;
3213
3369
  void maybeNotifyUpdate(import.meta.url, env);
3370
+ const heartbeat = startHeartbeat({
3371
+ server: "memory",
3372
+ version: SERVER_IDENTITY.version,
3373
+ env
3374
+ });
3214
3375
  const transport = new StdioServerTransport();
3215
3376
  const server = new McpServer2(SERVER_IDENTITY, {
3216
3377
  capabilities: { tools: {} },
@@ -3221,10 +3382,17 @@ async function main() {
3221
3382
  const composedReady = new Promise((res) => {
3222
3383
  composedResolve = res;
3223
3384
  });
3224
- registerTools(server, () => composedReady);
3225
- const shutdown = async (signal) => {
3226
- process.stderr.write(`prometheus-memory-mcp: received ${signal}, shutting down
3385
+ registerTools(server, () => composedReady, {
3386
+ onToolCall: (tool) => heartbeat.update({ lastTool: tool, lastToolCallAt: Date.now() })
3387
+ });
3388
+ let shuttingDown = false;
3389
+ const shutdown = async (reason) => {
3390
+ if (shuttingDown)
3391
+ return;
3392
+ shuttingDown = true;
3393
+ process.stderr.write(`prometheus-memory-mcp: ${reason}, shutting down
3227
3394
  `);
3395
+ heartbeat.stop();
3228
3396
  try {
3229
3397
  await server.close();
3230
3398
  } finally {
@@ -3233,8 +3401,11 @@ async function main() {
3233
3401
  process.exit(0);
3234
3402
  }
3235
3403
  };
3236
- process.once("SIGINT", shutdown);
3237
- process.once("SIGTERM", shutdown);
3404
+ process.once("SIGINT", () => void shutdown("received SIGINT"));
3405
+ process.once("SIGTERM", () => void shutdown("received SIGTERM"));
3406
+ process.stdin.once("end", () => void shutdown("stdin closed (client exited)"));
3407
+ process.stdin.once("close", () => void shutdown("stdin closed (client exited)"));
3408
+ server.server.onclose = () => void shutdown("transport closed (client exited)");
3238
3409
  const boot = (override, via) => {
3239
3410
  composed = composeFromEnv({
3240
3411
  env,
@@ -3242,6 +3413,15 @@ async function main() {
3242
3413
  });
3243
3414
  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"}
3244
3415
  `);
3416
+ heartbeat.update({
3417
+ workspaceRoot: composed.workspaceRoot,
3418
+ extra: {
3419
+ dbPath: composed.dbPath,
3420
+ projectId: composed.projectId,
3421
+ projectName: composed.projectName,
3422
+ embed: composed.embedderId
3423
+ }
3424
+ });
3245
3425
  if (composed.rootIsHomeOrFsRoot) {
3246
3426
  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.
3247
3427
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prom.codes/memory-mcp",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "description": "prom.codes Memory — persistent, local-first agent memory as an MCP server.",
5
5
  "type": "module",
6
6
  "bin": {