@prom.codes/memory-mcp 0.11.0 → 0.11.2

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 +113 -14
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -89,6 +89,9 @@ function isNewerVersion(latest, current) {
89
89
  return true;
90
90
  return false;
91
91
  }
92
+ function cachedLatestIsStale(cachedLatest, minVersion) {
93
+ return minVersion !== void 0 && cachedLatest !== null && isNewerVersion(minVersion, cachedLatest);
94
+ }
92
95
  function cachePath(dir, name) {
93
96
  const safe = name.replace(/[^a-zA-Z0-9._-]+/g, "_");
94
97
  return join(dir, `.update-check-${safe}.json`);
@@ -175,7 +178,7 @@ async function checkForUpdate(options) {
175
178
  const now = Date.now();
176
179
  if (!force) {
177
180
  const cached = await readCache(file);
178
- if (cached !== null && now - cached.checkedAt < cacheTtlMs) {
181
+ if (cached !== null && now - cached.checkedAt < cacheTtlMs && !cachedLatestIsStale(cached.latest, version)) {
179
182
  const updateAvailable2 = cached.latest !== null && isNewerVersion(cached.latest, version);
180
183
  if (updateAvailable2)
181
184
  notify(log, name, version, cached.latest);
@@ -202,15 +205,16 @@ async function checkForUpdate(options) {
202
205
  return { ...base, latest, checked: true, updateAvailable };
203
206
  }
204
207
  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;
208
+ const { env = process.env, fetch: fetchImpl = globalThis.fetch, cacheDir = join(homedir(), ".prometheus"), cacheTtlMs = DEFAULT_TTL_MS, timeoutMs = DEFAULT_TIMEOUT_MS, minVersion } = options;
206
209
  const file = cachePath(cacheDir, name);
207
210
  const cached = await readCache(file);
208
211
  const now = Date.now();
209
- if (cached !== null && cached.latest !== null && now - cached.checkedAt < cacheTtlMs) {
212
+ const stale = cachedLatestIsStale(cached?.latest ?? null, minVersion);
213
+ if (cached !== null && cached.latest !== null && now - cached.checkedAt < cacheTtlMs && !stale) {
210
214
  return cached.latest;
211
215
  }
212
216
  if (OPT_OUT_RE.test(env.PROMETHEUS_NO_UPDATE_CHECK ?? "") || typeof fetchImpl !== "function") {
213
- return cached?.latest ?? null;
217
+ return stale ? null : cached?.latest ?? null;
214
218
  }
215
219
  const latest = await fetchLatest(name, fetchImpl, timeoutMs);
216
220
  if (latest !== null) {
@@ -218,7 +222,7 @@ async function getLatestVersion(name, options = {}) {
218
222
  await writeCache(file, { checkedAt: now, latest });
219
223
  return latest;
220
224
  }
221
- return cached?.latest ?? null;
225
+ return stale ? null : cached?.latest ?? null;
222
226
  }
223
227
  function notify(log, name, current, latest) {
224
228
  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.)
@@ -242,7 +246,8 @@ async function buildUpdateStatus(pkgName, currentVersion, options = {}) {
242
246
  ...options.env !== void 0 ? { env: options.env } : {},
243
247
  ...options.fetch !== void 0 ? { fetch: options.fetch } : {},
244
248
  ...options.cacheDir !== void 0 ? { cacheDir: options.cacheDir } : {},
245
- timeoutMs: options.timeoutMs ?? 1500
249
+ timeoutMs: options.timeoutMs ?? 1500,
250
+ minVersion: currentVersion
246
251
  });
247
252
  } catch {
248
253
  latest = null;
@@ -338,6 +343,57 @@ function startHeartbeat(options) {
338
343
  };
339
344
  }
340
345
 
346
+ // ../shared/dist/idle-watchdog.js
347
+ var DEFAULT_IDLE_EXIT_MS = 30 * 6e4;
348
+ var IDLE_CHECK_INTERVAL_MS = 6e4;
349
+ var IDLE_EXIT_ENV = "PROMETHEUS_IDLE_EXIT_MS";
350
+ function parseIdleExitMs(env) {
351
+ const raw = (env[IDLE_EXIT_ENV] ?? "").trim();
352
+ if (raw === "")
353
+ return void 0;
354
+ const n = Number(raw);
355
+ return Number.isFinite(n) && n >= 0 ? n : void 0;
356
+ }
357
+ function createIdleWatchdog(options) {
358
+ const env = options.env ?? process.env;
359
+ const idleMs = options.idleMs ?? parseIdleExitMs(env) ?? DEFAULT_IDLE_EXIT_MS;
360
+ const now = options.now ?? Date.now;
361
+ const checkIntervalMs = options.checkIntervalMs ?? Math.max(1e3, Math.min(IDLE_CHECK_INTERVAL_MS, idleMs));
362
+ if (!(idleMs > 0)) {
363
+ return { touch() {
364
+ }, stop() {
365
+ }, idleMs: 0 };
366
+ }
367
+ let lastActivity = now();
368
+ let stopped = false;
369
+ let fired = false;
370
+ const timer = setInterval(() => {
371
+ if (stopped || fired)
372
+ return;
373
+ const idleFor = now() - lastActivity;
374
+ if (idleFor >= idleMs) {
375
+ fired = true;
376
+ clearInterval(timer);
377
+ options.onIdle(`idle for ${Math.round(idleFor / 1e3)}s with no client activity (set ${IDLE_EXIT_ENV}=0 to disable)`);
378
+ }
379
+ }, checkIntervalMs);
380
+ timer.unref?.();
381
+ return {
382
+ idleMs,
383
+ touch() {
384
+ if (stopped)
385
+ return;
386
+ lastActivity = now();
387
+ },
388
+ stop() {
389
+ if (stopped)
390
+ return;
391
+ stopped = true;
392
+ clearInterval(timer);
393
+ }
394
+ };
395
+ }
396
+
341
397
  // dist/composition.js
342
398
  import { createHash } from "node:crypto";
343
399
  import { homedir as homedir4 } from "node:os";
@@ -1020,6 +1076,7 @@ var DEFAULT_BATCH4 = 100;
1020
1076
  var DEFAULT_BATCH_CHARS2 = 4e5;
1021
1077
  var DEFAULT_RETRIES4 = 4;
1022
1078
  var DEFAULT_BACKOFF4 = 250;
1079
+ var DEFAULT_TIMEOUT_MS2 = 15e3;
1023
1080
  function sleep4(ms, signal) {
1024
1081
  return new Promise((resolve3, reject) => {
1025
1082
  if (signal?.aborted === true) {
@@ -1052,6 +1109,7 @@ var PrometheusRerankProvider = class {
1052
1109
  #maxBatchChars;
1053
1110
  #maxRetries;
1054
1111
  #retryBaseMs;
1112
+ #timeoutMs;
1055
1113
  #fetch;
1056
1114
  #creditsUsed = 0;
1057
1115
  constructor(opts) {
@@ -1068,8 +1126,12 @@ var PrometheusRerankProvider = class {
1068
1126
  this.#url = `${(opts.baseUrl ?? DEFAULT_BASE2).replace(/\/+$/, "")}/rerank`;
1069
1127
  this.#batchSize = opts.batchSize ?? DEFAULT_BATCH4;
1070
1128
  this.#maxBatchChars = opts.maxBatchChars ?? DEFAULT_BATCH_CHARS2;
1129
+ if (opts.timeoutMs !== void 0 && (!Number.isInteger(opts.timeoutMs) || opts.timeoutMs < 0)) {
1130
+ throw new Error(`PrometheusRerankProvider: timeoutMs must be a non-negative integer (0 disables), got ${opts.timeoutMs}`);
1131
+ }
1071
1132
  this.#maxRetries = opts.maxRetries ?? DEFAULT_RETRIES4;
1072
1133
  this.#retryBaseMs = opts.retryBaseMs ?? DEFAULT_BACKOFF4;
1134
+ this.#timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
1073
1135
  this.#fetch = opts.fetch ?? fetch;
1074
1136
  }
1075
1137
  /** Cumulative credits charged across all rerank calls of this instance. */
@@ -1128,8 +1190,6 @@ var PrometheusRerankProvider = class {
1128
1190
  },
1129
1191
  body: JSON.stringify({ query, documents: batch.map((c) => c.text) })
1130
1192
  };
1131
- if (signal !== void 0)
1132
- init.signal = signal;
1133
1193
  const payload = await this.#requestJson(init, signal);
1134
1194
  if (payload?.ok !== true || !Array.isArray(payload.results)) {
1135
1195
  throw nonRetryable4("prometheus-rerank: malformed rerank response");
@@ -1161,8 +1221,25 @@ var PrometheusRerankProvider = class {
1161
1221
  let attempt = 0;
1162
1222
  let lastError = null;
1163
1223
  while (attempt <= this.#maxRetries) {
1224
+ const controller = new AbortController();
1225
+ let timedOut = false;
1226
+ let timer;
1227
+ if (this.#timeoutMs > 0) {
1228
+ timer = setTimeout(() => {
1229
+ timedOut = true;
1230
+ controller.abort();
1231
+ }, this.#timeoutMs);
1232
+ timer.unref?.();
1233
+ }
1234
+ const onParentAbort = () => controller.abort();
1235
+ if (signal !== void 0) {
1236
+ if (signal.aborted)
1237
+ controller.abort();
1238
+ else
1239
+ signal.addEventListener("abort", onParentAbort, { once: true });
1240
+ }
1164
1241
  try {
1165
- const res = await this.#fetch(this.#url, init);
1242
+ const res = await this.#fetch(this.#url, { ...init, signal: controller.signal });
1166
1243
  if (res.status >= 500 && res.status < 600) {
1167
1244
  lastError = new Error(`prometheus-rerank: HTTP ${res.status}`);
1168
1245
  attempt += 1;
@@ -1178,15 +1255,21 @@ var PrometheusRerankProvider = class {
1178
1255
  }
1179
1256
  return await res.json();
1180
1257
  } catch (err) {
1181
- if (err?.name === "AbortError")
1258
+ if (err?.name === "AbortError" && !timedOut)
1182
1259
  throw err;
1183
1260
  if (err?.nonRetryable === true)
1184
1261
  throw err;
1262
+ const retryErr = timedOut ? new Error(`prometheus-rerank: request timed out after ${this.#timeoutMs}ms`) : err;
1185
1263
  if (attempt >= this.#maxRetries)
1186
- throw err;
1187
- lastError = err;
1264
+ throw retryErr;
1265
+ lastError = retryErr;
1188
1266
  attempt += 1;
1189
1267
  await sleep4(this.#retryBaseMs * 2 ** (attempt - 1), signal);
1268
+ } finally {
1269
+ if (timer !== void 0)
1270
+ clearTimeout(timer);
1271
+ if (signal !== void 0)
1272
+ signal.removeEventListener("abort", onParentAbort);
1190
1273
  }
1191
1274
  }
1192
1275
  throw lastError instanceof Error ? lastError : new Error(`prometheus-rerank: exhausted ${this.#maxRetries} retries`);
@@ -3315,7 +3398,7 @@ ${f.value}`);
3315
3398
  embeddingsError = err instanceof Error ? err.message : String(err);
3316
3399
  }
3317
3400
  }
3318
- const update = await buildUpdateStatus("@prom.codes/memory-mcp", "0.11.0", { isDevBuild: false });
3401
+ const update = await buildUpdateStatus("@prom.codes/memory-mcp", "0.11.2", { isDevBuild: false });
3319
3402
  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).` : ""}`;
3320
3403
  return textResult({
3321
3404
  installed: true,
@@ -3351,7 +3434,7 @@ ${f.value}`);
3351
3434
  // dist/server.js
3352
3435
  var SERVER_IDENTITY = {
3353
3436
  name: "prometheus-memory-mcp",
3354
- version: "0.11.0",
3437
+ version: "0.11.2",
3355
3438
  title: "prom.codes Memory"
3356
3439
  };
3357
3440
  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.";
@@ -3385,6 +3468,7 @@ async function main() {
3385
3468
  registerTools(server, () => composedReady, {
3386
3469
  onToolCall: (tool) => heartbeat.update({ lastTool: tool, lastToolCallAt: Date.now() })
3387
3470
  });
3471
+ let watchdog = null;
3388
3472
  let shuttingDown = false;
3389
3473
  const shutdown = async (reason) => {
3390
3474
  if (shuttingDown)
@@ -3392,6 +3476,7 @@ async function main() {
3392
3476
  shuttingDown = true;
3393
3477
  process.stderr.write(`prometheus-memory-mcp: ${reason}, shutting down
3394
3478
  `);
3479
+ watchdog?.stop();
3395
3480
  heartbeat.stop();
3396
3481
  try {
3397
3482
  await server.close();
@@ -3406,6 +3491,18 @@ async function main() {
3406
3491
  process.stdin.once("end", () => void shutdown("stdin closed (client exited)"));
3407
3492
  process.stdin.once("close", () => void shutdown("stdin closed (client exited)"));
3408
3493
  server.server.onclose = () => void shutdown("transport closed (client exited)");
3494
+ watchdog = createIdleWatchdog({ onIdle: (reason) => void shutdown(reason), env });
3495
+ if (watchdog.idleMs > 0) {
3496
+ process.stderr.write(`prometheus-memory-mcp: idle self-exit armed (${Math.round(watchdog.idleMs / 6e4)} min of no client activity)
3497
+ `);
3498
+ }
3499
+ const armIdleWatch = () => {
3500
+ const prev = transport.onmessage?.bind(transport);
3501
+ transport.onmessage = ((...args) => {
3502
+ watchdog?.touch();
3503
+ prev?.(...args);
3504
+ });
3505
+ };
3409
3506
  const boot = (override, via) => {
3410
3507
  composed = composeFromEnv({
3411
3508
  env,
@@ -3440,6 +3537,7 @@ async function main() {
3440
3537
  if (eagerVia !== null) {
3441
3538
  boot(void 0, eagerVia);
3442
3539
  await server.connect(transport);
3540
+ armIdleWatch();
3443
3541
  return;
3444
3542
  }
3445
3543
  let booted = false;
@@ -3463,6 +3561,7 @@ async function main() {
3463
3561
  void resolveAndBoot();
3464
3562
  };
3465
3563
  await server.connect(transport);
3564
+ armIdleWatch();
3466
3565
  const t = setTimeout(() => void resolveAndBoot(), 5e3);
3467
3566
  t.unref?.();
3468
3567
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prom.codes/memory-mcp",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "prom.codes Memory — persistent, local-first agent memory as an MCP server.",
5
5
  "type": "module",
6
6
  "bin": {