@prom.codes/memory-mcp 0.5.0 → 0.6.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 +268 -49
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -4,10 +4,189 @@
4
4
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
 
7
+ // ../shared/dist/types.js
8
+ var GRAMMAR_LANGUAGE_IDS = [
9
+ "typescript",
10
+ "tsx",
11
+ "javascript",
12
+ "python",
13
+ "php",
14
+ "go",
15
+ "rust",
16
+ "java",
17
+ "csharp",
18
+ "c",
19
+ "cpp",
20
+ "ruby",
21
+ "kotlin",
22
+ "html"
23
+ ];
24
+ var DOCUMENT_LANGUAGE_IDS = [
25
+ "markdown",
26
+ "text",
27
+ "json",
28
+ "yaml",
29
+ "toml"
30
+ ];
31
+ var LANGUAGE_IDS = [
32
+ ...GRAMMAR_LANGUAGE_IDS,
33
+ ...DOCUMENT_LANGUAGE_IDS
34
+ ];
35
+
36
+ // ../shared/dist/update-check.js
37
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
38
+ import { homedir } from "node:os";
39
+ import { join } from "node:path";
40
+ import { fileURLToPath } from "node:url";
41
+ async function packageIdentity(binImportMetaUrl) {
42
+ try {
43
+ const binPath = fileURLToPath(binImportMetaUrl);
44
+ const pkgPath = join(binPath, "..", "..", "package.json");
45
+ const raw = await readFile(pkgPath, "utf8");
46
+ const parsed = JSON.parse(raw);
47
+ if (typeof parsed.name !== "string" || typeof parsed.version !== "string") {
48
+ return null;
49
+ }
50
+ return { name: parsed.name, version: parsed.version };
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ async function maybeNotifyUpdate(binImportMetaUrl, env = process.env) {
56
+ try {
57
+ const id = await packageIdentity(binImportMetaUrl);
58
+ if (id === null)
59
+ return;
60
+ await checkForUpdate({ name: id.name, version: id.version, env });
61
+ } catch {
62
+ }
63
+ }
64
+ var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
65
+ var DEFAULT_TIMEOUT_MS = 1500;
66
+ var OPT_OUT_RE = /^(1|true|yes|on)$/i;
67
+ function parseSemver(v) {
68
+ const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(v.trim());
69
+ if (m === null)
70
+ return null;
71
+ return {
72
+ core: [Number(m[1]), Number(m[2]), Number(m[3])],
73
+ pre: m[4] ?? null
74
+ };
75
+ }
76
+ function isNewerVersion(latest, current) {
77
+ const a = parseSemver(latest);
78
+ const b = parseSemver(current);
79
+ if (a === null || b === null)
80
+ return false;
81
+ for (let i = 0; i < 3; i++) {
82
+ if (a.core[i] > b.core[i])
83
+ return true;
84
+ if (a.core[i] < b.core[i])
85
+ return false;
86
+ }
87
+ if (a.pre === null && b.pre !== null)
88
+ return true;
89
+ return false;
90
+ }
91
+ function cachePath(dir, name) {
92
+ const safe = name.replace(/[^a-zA-Z0-9._-]+/g, "_");
93
+ return join(dir, `.update-check-${safe}.json`);
94
+ }
95
+ async function readCache(path2) {
96
+ try {
97
+ const raw = await readFile(path2, "utf8");
98
+ const parsed = JSON.parse(raw);
99
+ if (typeof parsed.checkedAt !== "number")
100
+ return null;
101
+ return {
102
+ checkedAt: parsed.checkedAt,
103
+ latest: typeof parsed.latest === "string" ? parsed.latest : null
104
+ };
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+ async function writeCache(path2, data) {
110
+ try {
111
+ await writeFile(path2, JSON.stringify(data), "utf8");
112
+ } catch {
113
+ }
114
+ }
115
+ async function fetchLatest(name, fetchImpl, timeoutMs) {
116
+ const controller = new AbortController();
117
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
118
+ timer.unref?.();
119
+ try {
120
+ const url = `https://registry.npmjs.org/${name.replace("/", "%2F")}/latest`;
121
+ const res = await fetchImpl(url, {
122
+ signal: controller.signal,
123
+ headers: { accept: "application/vnd.npm.install-v1+json" }
124
+ });
125
+ if (!res.ok)
126
+ return null;
127
+ const body = await res.json();
128
+ return typeof body.version === "string" ? body.version : null;
129
+ } catch {
130
+ return null;
131
+ } finally {
132
+ clearTimeout(timer);
133
+ }
134
+ }
135
+ async function checkForUpdate(options) {
136
+ const { name, version, env = process.env, log = (line) => process.stderr.write(line), fetch: fetchImpl = globalThis.fetch, cacheDir = join(homedir(), ".prometheus"), cacheTtlMs = DEFAULT_TTL_MS, timeoutMs = DEFAULT_TIMEOUT_MS, force = false } = options;
137
+ const base = {
138
+ latest: null,
139
+ current: version
140
+ };
141
+ if (OPT_OUT_RE.test(env.PROMETHEUS_NO_UPDATE_CHECK ?? "")) {
142
+ return { ...base, checked: false, updateAvailable: false, reason: "opted-out" };
143
+ }
144
+ if (parseSemver(version) === null || version === "0.0.0") {
145
+ return { ...base, checked: false, updateAvailable: false, reason: "invalid-version" };
146
+ }
147
+ if (typeof fetchImpl !== "function") {
148
+ return { ...base, checked: false, updateAvailable: false, reason: "error" };
149
+ }
150
+ const file = cachePath(cacheDir, name);
151
+ const now = Date.now();
152
+ if (!force) {
153
+ const cached = await readCache(file);
154
+ if (cached !== null && now - cached.checkedAt < cacheTtlMs) {
155
+ const updateAvailable2 = cached.latest !== null && isNewerVersion(cached.latest, version);
156
+ if (updateAvailable2)
157
+ notify(log, name, version, cached.latest);
158
+ return {
159
+ ...base,
160
+ latest: cached.latest,
161
+ checked: false,
162
+ updateAvailable: updateAvailable2,
163
+ reason: "throttled"
164
+ };
165
+ }
166
+ }
167
+ await mkdir(cacheDir, { recursive: true }).catch(() => void 0);
168
+ const latest = await fetchLatest(name, fetchImpl, timeoutMs);
169
+ await writeCache(file, { checkedAt: now, latest });
170
+ if (latest === null) {
171
+ return { ...base, checked: true, updateAvailable: false, reason: "error" };
172
+ }
173
+ const updateAvailable = isNewerVersion(latest, version);
174
+ if (updateAvailable)
175
+ notify(log, name, version, latest);
176
+ return { ...base, latest, checked: true, updateAvailable };
177
+ }
178
+ function notify(log, name, current, latest) {
179
+ 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
+ `);
181
+ }
182
+
183
+ // ../shared/dist/index.js
184
+ var PROMETHEUS_VERSION = "0.1.0";
185
+
7
186
  // dist/composition.js
8
187
  import { createHash } from "node:crypto";
9
- import { homedir } from "node:os";
10
- import { basename, join, resolve } from "node:path";
188
+ import { homedir as homedir2 } from "node:os";
189
+ import { basename, join as join2, resolve } from "node:path";
11
190
 
12
191
  // ../embeddings-openai-compat/dist/index.js
13
192
  var DEFAULT_BATCH = 96;
@@ -1128,6 +1307,45 @@ function reciprocalRankFusion(lists, options = {}) {
1128
1307
  return options.limit !== void 0 && options.limit >= 0 ? merged.slice(0, options.limit) : merged;
1129
1308
  }
1130
1309
 
1310
+ // dist/temporal.js
1311
+ var RRF_K = 60;
1312
+ var DEFAULT_TEMPORAL_WEIGHT = 2;
1313
+ var RECENT_RE = /\b(latest|most[\s-]?recent|recently|recent|current|currently|now|nowadays|today|newest|up[\s-]?to[\s-]?date|so far)\b/i;
1314
+ var EARLIEST_RE = /\b(earliest|oldest|first|initial|initially|original|originally|at the (?:very )?(?:start|beginning)|back then)\b/i;
1315
+ function detectTemporalIntent(query) {
1316
+ if (query === "")
1317
+ return null;
1318
+ const recent = RECENT_RE.test(query);
1319
+ const earliest = EARLIEST_RE.test(query);
1320
+ if (recent === earliest)
1321
+ return null;
1322
+ return { direction: recent ? "recent" : "earliest" };
1323
+ }
1324
+ function parseTimestamp(iso) {
1325
+ if (iso === null || iso === void 0 || iso === "")
1326
+ return null;
1327
+ const ms = Date.parse(iso);
1328
+ return Number.isFinite(ms) ? ms : null;
1329
+ }
1330
+ function applyTemporalRanking(hits, intent, getTimestampMs, weight = DEFAULT_TEMPORAL_WEIGHT) {
1331
+ if (hits.length <= 1 || weight <= 0)
1332
+ return [...hits];
1333
+ const stamped = hits.map((hit, index) => ({ hit, index, ts: getTimestampMs(hit) })).filter((x) => x.ts !== null);
1334
+ if (stamped.length <= 1)
1335
+ return [...hits];
1336
+ const byTime = [...stamped].sort((a, b) => intent.direction === "recent" ? b.ts - a.ts : a.ts - b.ts);
1337
+ const tempRank = /* @__PURE__ */ new Map();
1338
+ byTime.forEach((x, rank) => tempRank.set(x.index, rank));
1339
+ const scored = hits.map((hit, index) => {
1340
+ const posScore = 1 / (RRF_K + index);
1341
+ const tr = tempRank.get(index);
1342
+ const tempScore = tr === void 0 ? 0 : 1 / (RRF_K + tr);
1343
+ return { hit, index, score: posScore + weight * tempScore };
1344
+ });
1345
+ scored.sort((a, b) => b.score - a.score || a.index - b.index);
1346
+ return scored.map((s) => s.hit);
1347
+ }
1348
+
1131
1349
  // dist/types.js
1132
1350
  var MEMORY_SCOPES = [
1133
1351
  "system",
@@ -1301,6 +1519,8 @@ var SqliteMemoryBackend = class {
1301
1519
  embedder;
1302
1520
  reranker;
1303
1521
  rewriter;
1522
+ temporalEnabled;
1523
+ temporalWeight;
1304
1524
  /** Record ids whose vector is missing/stale, awaiting a batched embed. */
1305
1525
  pendingEmbed = /* @__PURE__ */ new Set();
1306
1526
  closed = false;
@@ -1317,6 +1537,8 @@ var SqliteMemoryBackend = class {
1317
1537
  this.embedder = opts.embedder;
1318
1538
  this.reranker = opts.reranker;
1319
1539
  this.rewriter = opts.rewriter;
1540
+ this.temporalEnabled = opts.temporal?.enabled ?? false;
1541
+ this.temporalWeight = opts.temporal?.weight ?? DEFAULT_TEMPORAL_WEIGHT;
1320
1542
  if (this.embedder !== void 0)
1321
1543
  this.queueUnembedded();
1322
1544
  }
@@ -1440,7 +1662,20 @@ var SqliteMemoryBackend = class {
1440
1662
  ], { limit: poolLimit }).map((f) => f.payload);
1441
1663
  }
1442
1664
  const reranked = input.rerank === false ? pool : await this.rerankPool(input.query, pool, finalLimit);
1443
- return reranked.slice(0, finalLimit);
1665
+ const temporalOn = input.temporal ?? this.temporalEnabled;
1666
+ const ordered = temporalOn ? this.applyTemporal(input.query, reranked) : reranked;
1667
+ return ordered.slice(0, finalLimit);
1668
+ }
1669
+ /**
1670
+ * M5: blend the current order with a timestamp order when the query expresses
1671
+ * a clear temporal direction. Uses `updatedAt` (falls back to `createdAt`) as
1672
+ * the record's time. No-op when no intent is detected.
1673
+ */
1674
+ applyTemporal(query, hits) {
1675
+ const intent = detectTemporalIntent(query);
1676
+ if (intent === null)
1677
+ return hits;
1678
+ return applyTemporalRanking(hits, intent, (h) => parseTimestamp(h.record.updatedAt) ?? parseTimestamp(h.record.createdAt), this.temporalWeight);
1444
1679
  }
1445
1680
  /**
1446
1681
  * M4: rewrite the query for the retrieval channels (HyDE concat). Returns the
@@ -1695,7 +1930,7 @@ function projectIdFor(workspaceRoot) {
1695
1930
  return createHash("sha256").update(abs).digest("hex").slice(0, 16);
1696
1931
  }
1697
1932
  function defaultMemoryDbPath() {
1698
- return join(homedir(), ".prometheus", "memory.db");
1933
+ return join2(homedir2(), ".prometheus", "memory.db");
1699
1934
  }
1700
1935
  function intEnv(env, name, def) {
1701
1936
  const raw = env[name];
@@ -1897,6 +2132,18 @@ function discoverMemoryRewriter(env) {
1897
2132
  }
1898
2133
  throw new Error(`unknown PROMETHEUS_MEMORY_REWRITE_PROVIDER="${forced}" (expected "none", "mistral", "openai", or "generic")`);
1899
2134
  }
2135
+ function discoverMemoryTemporal(env) {
2136
+ const raw = (env.PROMETHEUS_MEMORY_TEMPORAL ?? "off").toLowerCase();
2137
+ const enabled = raw === "on" || raw === "1" || raw === "true" || raw === "yes";
2138
+ const rawWeight = env.PROMETHEUS_MEMORY_TEMPORAL_WEIGHT;
2139
+ let weight = DEFAULT_TEMPORAL_WEIGHT;
2140
+ if (rawWeight !== void 0 && rawWeight !== "") {
2141
+ const n = Number.parseFloat(rawWeight);
2142
+ if (Number.isFinite(n) && n >= 0)
2143
+ weight = n;
2144
+ }
2145
+ return { enabled, weight };
2146
+ }
1900
2147
  function composeFromEnv(opts) {
1901
2148
  const env = opts.env;
1902
2149
  const override = (opts.workspaceRootOverride ?? "").trim();
@@ -1911,10 +2158,12 @@ function composeFromEnv(opts) {
1911
2158
  const { id: rerankerId, provider: reranker } = discoverMemoryReranker(env);
1912
2159
  const { id: extractorId, provider: extractor } = discoverMemoryExtractor(env);
1913
2160
  const { id: rewriterId, provider: rewriter } = discoverMemoryRewriter(env);
2161
+ const temporal = discoverMemoryTemporal(env);
1914
2162
  const backend = new SqliteMemoryBackend(dbPath, {
1915
2163
  ...embedder !== void 0 ? { embedder } : {},
1916
2164
  ...reranker !== null ? { reranker } : {},
1917
- ...rewriter !== null ? { rewriter } : {}
2165
+ ...rewriter !== null ? { rewriter } : {},
2166
+ temporal
1918
2167
  });
1919
2168
  return {
1920
2169
  backend,
@@ -1930,12 +2179,13 @@ function composeFromEnv(opts) {
1930
2179
  extractorId,
1931
2180
  rewriter,
1932
2181
  rewriterId,
2182
+ temporalEnabled: temporal.enabled,
1933
2183
  close: () => backend.close()
1934
2184
  };
1935
2185
  }
1936
2186
 
1937
2187
  // dist/roots.js
1938
- import { fileURLToPath } from "node:url";
2188
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1939
2189
  async function rootFromClient(server, timeoutMs = 2500) {
1940
2190
  let supportsRoots = false;
1941
2191
  try {
@@ -1956,7 +2206,7 @@ async function rootFromClient(server, timeoutMs = 2500) {
1956
2206
  const uri = typeof r?.uri === "string" ? r.uri : "";
1957
2207
  if (uri.startsWith("file://")) {
1958
2208
  try {
1959
- return fileURLToPath(uri);
2209
+ return fileURLToPath2(uri);
1960
2210
  } catch {
1961
2211
  }
1962
2212
  }
@@ -1967,38 +2217,6 @@ async function rootFromClient(server, timeoutMs = 2500) {
1967
2217
  // dist/server.js
1968
2218
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1969
2219
 
1970
- // ../shared/dist/types.js
1971
- var GRAMMAR_LANGUAGE_IDS = [
1972
- "typescript",
1973
- "tsx",
1974
- "javascript",
1975
- "python",
1976
- "php",
1977
- "go",
1978
- "rust",
1979
- "java",
1980
- "csharp",
1981
- "c",
1982
- "cpp",
1983
- "ruby",
1984
- "kotlin",
1985
- "html"
1986
- ];
1987
- var DOCUMENT_LANGUAGE_IDS = [
1988
- "markdown",
1989
- "text",
1990
- "json",
1991
- "yaml",
1992
- "toml"
1993
- ];
1994
- var LANGUAGE_IDS = [
1995
- ...GRAMMAR_LANGUAGE_IDS,
1996
- ...DOCUMENT_LANGUAGE_IDS
1997
- ];
1998
-
1999
- // ../shared/dist/index.js
2000
- var PROMETHEUS_VERSION = "0.1.0";
2001
-
2002
2220
  // dist/tools.js
2003
2221
  import { z } from "zod";
2004
2222
 
@@ -2119,8 +2337,8 @@ function assertNoSecrets(text) {
2119
2337
 
2120
2338
  // dist/setup.js
2121
2339
  import { existsSync } from "node:fs";
2122
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
2123
- import { dirname as dirname2, join as join3 } from "node:path";
2340
+ 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";
2124
2342
  var MEMORY_RUNTIMES = [
2125
2343
  "claude-code",
2126
2344
  "cursor",
@@ -2164,13 +2382,13 @@ alwaysApply: true
2164
2382
  var TARGETS = {
2165
2383
  "claude-code": { relPath: "CLAUDE.md", mode: "block", detect: "CLAUDE.md" },
2166
2384
  cursor: {
2167
- relPath: join3(".cursor", "rules", "prometheus-memory.mdc"),
2385
+ relPath: join4(".cursor", "rules", "prometheus-memory.mdc"),
2168
2386
  mode: "file",
2169
2387
  fileContent: CURSOR_FRONTMATTER + withMarkers(RULE_BLOCK) + "\n",
2170
2388
  detect: ".cursor"
2171
2389
  },
2172
2390
  augment: {
2173
- relPath: join3(".augment", "rules", "prometheus-memory.md"),
2391
+ relPath: join4(".augment", "rules", "prometheus-memory.md"),
2174
2392
  mode: "file",
2175
2393
  fileContent: withMarkers(RULE_BLOCK) + "\n",
2176
2394
  detect: ".augment"
@@ -2178,7 +2396,7 @@ var TARGETS = {
2178
2396
  agents: { relPath: "AGENTS.md", mode: "block", detect: "AGENTS.md" }
2179
2397
  };
2180
2398
  function detectRuntimes(workspaceRoot) {
2181
- const found = MEMORY_RUNTIMES.filter((rt) => existsSync(join3(workspaceRoot, TARGETS[rt].detect)));
2399
+ const found = MEMORY_RUNTIMES.filter((rt) => existsSync(join4(workspaceRoot, TARGETS[rt].detect)));
2182
2400
  return found.length > 0 ? found : ["agents"];
2183
2401
  }
2184
2402
  function upsertBlock(existing, block) {
@@ -2193,15 +2411,15 @@ function upsertBlock(existing, block) {
2193
2411
  }
2194
2412
  async function installRuntime(workspaceRoot, runtime) {
2195
2413
  const target = TARGETS[runtime];
2196
- const absPath = join3(workspaceRoot, target.relPath);
2414
+ const absPath = join4(workspaceRoot, target.relPath);
2197
2415
  const exists = existsSync(absPath);
2198
- const before = exists ? await readFile2(absPath, "utf-8") : "";
2416
+ const before = exists ? await readFile3(absPath, "utf-8") : "";
2199
2417
  const after = target.mode === "file" ? target.fileContent : upsertBlock(before, RULE_BLOCK);
2200
2418
  if (exists && before === after) {
2201
2419
  return { runtime, path: absPath, action: "unchanged" };
2202
2420
  }
2203
- await mkdir2(dirname2(absPath), { recursive: true });
2204
- await writeFile2(absPath, after, "utf-8");
2421
+ await mkdir3(dirname2(absPath), { recursive: true });
2422
+ await writeFile3(absPath, after, "utf-8");
2205
2423
  return { runtime, path: absPath, action: exists ? "updated" : "created" };
2206
2424
  }
2207
2425
 
@@ -2563,6 +2781,7 @@ async function main() {
2563
2781
  const explicitRoot = (env.PROMETHEUS_WORKSPACE_ROOT ?? "").trim();
2564
2782
  const claudeRoot = (env.CLAUDE_PROJECT_DIR ?? "").trim();
2565
2783
  const eagerVia = explicitRoot !== "" ? "PROMETHEUS_WORKSPACE_ROOT" : claudeRoot !== "" ? "CLAUDE_PROJECT_DIR" : null;
2784
+ void maybeNotifyUpdate(import.meta.url, env);
2566
2785
  const transport = new StdioServerTransport();
2567
2786
  const server = new McpServer2(SERVER_IDENTITY, {
2568
2787
  capabilities: { tools: {} },
@@ -2587,7 +2806,7 @@ async function main() {
2587
2806
  env,
2588
2807
  ...override !== void 0 && override !== "" ? { workspaceRootOverride: override } : {}
2589
2808
  });
2590
- 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}
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"}
2591
2810
  `);
2592
2811
  registerTools(server, composed);
2593
2812
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prom.codes/memory-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "prom.codes Memory — persistent, local-first agent memory as an MCP server.",
5
5
  "type": "module",
6
6
  "bin": {