@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.
- package/README.md +13 -6
- package/dist/bin.js +210 -25
- 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)
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
-
|
|
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) {
|