@prom.codes/memory-mcp 0.15.2 → 0.16.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/dist/bin.js +151 -22
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -301,6 +301,20 @@ function isHomeOrFilesystemRoot(root) {
|
|
|
301
301
|
return false;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
// ../shared/dist/workspace-path.js
|
|
305
|
+
import { createHash } from "node:crypto";
|
|
306
|
+
import { resolve as resolve2 } from "node:path";
|
|
307
|
+
function canonicalizeWorkspacePath(p) {
|
|
308
|
+
const abs = resolve2(p);
|
|
309
|
+
return process.platform === "win32" ? abs.replace(/\\/g, "/").toLowerCase() : abs;
|
|
310
|
+
}
|
|
311
|
+
function stableWorkspaceId(root) {
|
|
312
|
+
return createHash("sha256").update(canonicalizeWorkspacePath(root)).digest("hex").slice(0, 16);
|
|
313
|
+
}
|
|
314
|
+
function legacyWorkspaceId(root) {
|
|
315
|
+
return createHash("sha256").update(resolve2(root)).digest("hex").slice(0, 16);
|
|
316
|
+
}
|
|
317
|
+
|
|
304
318
|
// ../shared/dist/heartbeat.js
|
|
305
319
|
import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
306
320
|
import { homedir as homedir3 } from "node:os";
|
|
@@ -471,9 +485,8 @@ function nativeBindingOption() {
|
|
|
471
485
|
}
|
|
472
486
|
|
|
473
487
|
// dist/composition.js
|
|
474
|
-
import { createHash } from "node:crypto";
|
|
475
488
|
import { homedir as homedir5 } from "node:os";
|
|
476
|
-
import { basename, join as join7, resolve as
|
|
489
|
+
import { basename, join as join7, resolve as resolve3 } from "node:path";
|
|
477
490
|
|
|
478
491
|
// ../embeddings-openai-compat/dist/index.js
|
|
479
492
|
var DEFAULT_BATCH = 96;
|
|
@@ -504,14 +517,14 @@ function parseRetryAfterMs(value, now = Date.now()) {
|
|
|
504
517
|
return delta > 0 ? delta : 0;
|
|
505
518
|
}
|
|
506
519
|
function sleep(ms, signal) {
|
|
507
|
-
return new Promise((
|
|
520
|
+
return new Promise((resolve4, reject) => {
|
|
508
521
|
if (signal?.aborted === true) {
|
|
509
522
|
reject(new Error("aborted"));
|
|
510
523
|
return;
|
|
511
524
|
}
|
|
512
525
|
const timer = setTimeout(() => {
|
|
513
526
|
signal?.removeEventListener("abort", onAbort);
|
|
514
|
-
|
|
527
|
+
resolve4();
|
|
515
528
|
}, ms);
|
|
516
529
|
const onAbort = () => {
|
|
517
530
|
clearTimeout(timer);
|
|
@@ -754,14 +767,14 @@ var DEFAULT_RETRIES2 = 4;
|
|
|
754
767
|
var DEFAULT_BACKOFF2 = 250;
|
|
755
768
|
var DEFAULT_TIMEOUT_MS2 = 15e3;
|
|
756
769
|
function sleep2(ms, signal) {
|
|
757
|
-
return new Promise((
|
|
770
|
+
return new Promise((resolve4, reject) => {
|
|
758
771
|
if (signal?.aborted === true) {
|
|
759
772
|
reject(new Error("aborted"));
|
|
760
773
|
return;
|
|
761
774
|
}
|
|
762
775
|
const timer = setTimeout(() => {
|
|
763
776
|
signal?.removeEventListener("abort", onAbort);
|
|
764
|
-
|
|
777
|
+
resolve4();
|
|
765
778
|
}, ms);
|
|
766
779
|
const onAbort = () => {
|
|
767
780
|
clearTimeout(timer);
|
|
@@ -1025,14 +1038,14 @@ function parseRetryAfterMs2(value, now = Date.now()) {
|
|
|
1025
1038
|
return delta > 0 ? delta : 0;
|
|
1026
1039
|
}
|
|
1027
1040
|
function sleep3(ms, signal) {
|
|
1028
|
-
return new Promise((
|
|
1041
|
+
return new Promise((resolve4, reject) => {
|
|
1029
1042
|
if (signal?.aborted === true) {
|
|
1030
1043
|
reject(new Error("aborted"));
|
|
1031
1044
|
return;
|
|
1032
1045
|
}
|
|
1033
1046
|
const timer = setTimeout(() => {
|
|
1034
1047
|
signal?.removeEventListener("abort", onAbort);
|
|
1035
|
-
|
|
1048
|
+
resolve4();
|
|
1036
1049
|
}, ms);
|
|
1037
1050
|
const onAbort = () => {
|
|
1038
1051
|
clearTimeout(timer);
|
|
@@ -1179,14 +1192,14 @@ var DEFAULT_RETRIES4 = 4;
|
|
|
1179
1192
|
var DEFAULT_BACKOFF4 = 250;
|
|
1180
1193
|
var DEFAULT_TIMEOUT_MS3 = 15e3;
|
|
1181
1194
|
function sleep4(ms, signal) {
|
|
1182
|
-
return new Promise((
|
|
1195
|
+
return new Promise((resolve4, reject) => {
|
|
1183
1196
|
if (signal?.aborted === true) {
|
|
1184
1197
|
reject(new Error("aborted"));
|
|
1185
1198
|
return;
|
|
1186
1199
|
}
|
|
1187
1200
|
const timer = setTimeout(() => {
|
|
1188
1201
|
signal?.removeEventListener("abort", onAbort);
|
|
1189
|
-
|
|
1202
|
+
resolve4();
|
|
1190
1203
|
}, ms);
|
|
1191
1204
|
const onAbort = () => {
|
|
1192
1205
|
clearTimeout(timer);
|
|
@@ -1405,14 +1418,14 @@ function parseRetryAfterMs3(value, now = Date.now()) {
|
|
|
1405
1418
|
return delta > 0 ? delta : 0;
|
|
1406
1419
|
}
|
|
1407
1420
|
function sleep5(ms, signal) {
|
|
1408
|
-
return new Promise((
|
|
1421
|
+
return new Promise((resolve4, reject) => {
|
|
1409
1422
|
if (signal?.aborted === true) {
|
|
1410
1423
|
reject(new Error("aborted"));
|
|
1411
1424
|
return;
|
|
1412
1425
|
}
|
|
1413
1426
|
const timer = setTimeout(() => {
|
|
1414
1427
|
signal?.removeEventListener("abort", onAbort);
|
|
1415
|
-
|
|
1428
|
+
resolve4();
|
|
1416
1429
|
}, ms);
|
|
1417
1430
|
const onAbort = () => {
|
|
1418
1431
|
clearTimeout(timer);
|
|
@@ -1806,7 +1819,7 @@ var OpenAICompatRewriter = class {
|
|
|
1806
1819
|
|
|
1807
1820
|
// dist/sqlite.js
|
|
1808
1821
|
import { randomUUID } from "node:crypto";
|
|
1809
|
-
import { mkdirSync as mkdirSync3, readdirSync as
|
|
1822
|
+
import { mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, rmSync as rmSync2 } from "node:fs";
|
|
1810
1823
|
import { dirname as dirname4, join as join6 } from "node:path";
|
|
1811
1824
|
import Database from "better-sqlite3";
|
|
1812
1825
|
|
|
@@ -1854,7 +1867,7 @@ function assertNoSecrets(text) {
|
|
|
1854
1867
|
}
|
|
1855
1868
|
|
|
1856
1869
|
// dist/recorder.js
|
|
1857
|
-
import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1870
|
+
import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, renameSync, rmdirSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1858
1871
|
import { homedir as homedir4 } from "node:os";
|
|
1859
1872
|
import { dirname as dirname3, join as join5 } from "node:path";
|
|
1860
1873
|
var SPOOL_VERSION = 1;
|
|
@@ -1890,6 +1903,46 @@ function recorderRoot(env = process.env) {
|
|
|
1890
1903
|
function sanitizeSegment(s) {
|
|
1891
1904
|
return s.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 128) || "_";
|
|
1892
1905
|
}
|
|
1906
|
+
function migrateRecorderSpoolDir(oldId, newId, env = process.env) {
|
|
1907
|
+
if (oldId === newId)
|
|
1908
|
+
return { renamedDir: false, movedFiles: 0 };
|
|
1909
|
+
const root = recorderRoot(env);
|
|
1910
|
+
const oldDir = join5(root, sanitizeSegment(oldId));
|
|
1911
|
+
const newDir = join5(root, sanitizeSegment(newId));
|
|
1912
|
+
if (!existsSync2(oldDir))
|
|
1913
|
+
return { renamedDir: false, movedFiles: 0 };
|
|
1914
|
+
if (!existsSync2(newDir)) {
|
|
1915
|
+
try {
|
|
1916
|
+
renameSync(oldDir, newDir);
|
|
1917
|
+
return { renamedDir: true, movedFiles: 0 };
|
|
1918
|
+
} catch {
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
mkdirSync2(newDir, { recursive: true });
|
|
1922
|
+
let moved = 0;
|
|
1923
|
+
let names;
|
|
1924
|
+
try {
|
|
1925
|
+
names = readdirSync2(oldDir);
|
|
1926
|
+
} catch {
|
|
1927
|
+
return { renamedDir: false, movedFiles: 0 };
|
|
1928
|
+
}
|
|
1929
|
+
for (const name of names) {
|
|
1930
|
+
const to = join5(newDir, name);
|
|
1931
|
+
if (existsSync2(to))
|
|
1932
|
+
continue;
|
|
1933
|
+
try {
|
|
1934
|
+
renameSync(join5(oldDir, name), to);
|
|
1935
|
+
moved++;
|
|
1936
|
+
} catch {
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
try {
|
|
1940
|
+
if (readdirSync2(oldDir).length === 0)
|
|
1941
|
+
rmdirSync(oldDir);
|
|
1942
|
+
} catch {
|
|
1943
|
+
}
|
|
1944
|
+
return { renamedDir: false, movedFiles: moved };
|
|
1945
|
+
}
|
|
1893
1946
|
function parseSpool(raw) {
|
|
1894
1947
|
const lines = raw.split("\n");
|
|
1895
1948
|
const events = [];
|
|
@@ -1921,7 +1974,7 @@ var RECORDER_HOOK_SCRIPT = String.raw`#!/usr/bin/env node
|
|
|
1921
1974
|
// never disturb a coding session. Managed by prom.codes (memory-mcp recorder_setup).
|
|
1922
1975
|
import { appendFileSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
1923
1976
|
import { homedir } from "node:os";
|
|
1924
|
-
import { join } from "node:path";
|
|
1977
|
+
import { join, resolve } from "node:path";
|
|
1925
1978
|
import { createHash } from "node:crypto";
|
|
1926
1979
|
|
|
1927
1980
|
const SPOOL_CAP = ${SPOOL_CAP_BYTES};
|
|
@@ -1944,7 +1997,11 @@ function sensitivePath(p) {
|
|
|
1944
1997
|
if (/(^|\/)(secrets?|credentials?)(\/|$)/.test(s)) return true;
|
|
1945
1998
|
return false;
|
|
1946
1999
|
}
|
|
1947
|
-
|
|
2000
|
+
// Canonical workspace id — MUST match @prometheus/shared stableWorkspaceId so
|
|
2001
|
+
// the spool key equals memory-mcp's projectIdFor (the ingest side). Inlined
|
|
2002
|
+
// because this standalone hook cannot import from the package.
|
|
2003
|
+
function canonicalizeWorkspacePath(p) { const abs = resolve(p); return process.platform === "win32" ? abs.replace(/\\/g, "/").toLowerCase() : abs; }
|
|
2004
|
+
function projectIdOf(root) { return createHash("sha256").update(canonicalizeWorkspacePath(root)).digest("hex").slice(0, 16); }
|
|
1948
2005
|
|
|
1949
2006
|
function dietTool(toolName, inp, result) {
|
|
1950
2007
|
const tool = toolName; inp = inp || {};
|
|
@@ -2604,6 +2661,61 @@ var SqliteMemoryBackend = class {
|
|
|
2604
2661
|
if (this.embedder !== void 0)
|
|
2605
2662
|
this.queueUnembedded();
|
|
2606
2663
|
}
|
|
2664
|
+
/**
|
|
2665
|
+
* Canonical-hash migration (spec 2026-07-19 §3.3): move every row keyed on the
|
|
2666
|
+
* PRE-flip `oldId` onto the canonical `newId`, in ONE transaction. No re-embed
|
|
2667
|
+
* (the vector is over `value`, which is unchanged) — the memory analogue of
|
|
2668
|
+
* context's "no reindex".
|
|
2669
|
+
*
|
|
2670
|
+
* - `agent_memory`: change `project_id`; for project-scoped rows the
|
|
2671
|
+
* `scope_id` (which equals the project id) moves too. The UNIQUE
|
|
2672
|
+
* `(scope, scope_id, type, key)` can then clash with a row already under
|
|
2673
|
+
* `newId` (the user used both path spellings). **Merge rule: newest
|
|
2674
|
+
* `updated_at` wins**; the loser is deleted (its vector + FTS entry GC'd by
|
|
2675
|
+
* the existing delete triggers). This mirrors `resolveScopeChain`'s own
|
|
2676
|
+
* newer-wins tie-break, so the survivor is the record recall already prefers.
|
|
2677
|
+
* - `agent_sessions` / `session_events`: `UPDATE OR IGNORE` the `project_id`;
|
|
2678
|
+
* a `(session_id, seq)` clash across two spellings is effectively impossible
|
|
2679
|
+
* (ids are unique per run under one spelling), and OR IGNORE leaves any
|
|
2680
|
+
* clash inert under the old id rather than throwing.
|
|
2681
|
+
*
|
|
2682
|
+
* Idempotent (afterwards no row has `project_id = oldId`) and a no-op when
|
|
2683
|
+
* `oldId === newId` (POSIX / already-canonical).
|
|
2684
|
+
*/
|
|
2685
|
+
migrateProjectIdentity(oldId, newId) {
|
|
2686
|
+
if (oldId === newId) {
|
|
2687
|
+
return { memoryRows: 0, memoryMerged: 0, sessions: 0, events: 0 };
|
|
2688
|
+
}
|
|
2689
|
+
const run = this.db.transaction(() => {
|
|
2690
|
+
const incoming = this.db.prepare(`SELECT * FROM agent_memory WHERE project_id = ?`).all(oldId);
|
|
2691
|
+
const findClash = this.db.prepare(`SELECT id, updated_at FROM agent_memory
|
|
2692
|
+
WHERE scope = ? AND scope_id = ? AND type = ? AND key = ? AND id != ?`);
|
|
2693
|
+
const del = this.db.prepare(`DELETE FROM agent_memory WHERE id = ?`);
|
|
2694
|
+
const upd = this.db.prepare(`UPDATE agent_memory SET project_id = ?, scope_id = ? WHERE id = ?`);
|
|
2695
|
+
let merged = 0;
|
|
2696
|
+
for (const row of incoming) {
|
|
2697
|
+
const newScopeId = row.scope === "project" && row.scope_id === oldId ? newId : row.scope_id;
|
|
2698
|
+
const clash = findClash.get(row.scope, newScopeId, row.type, row.key, row.id);
|
|
2699
|
+
if (clash !== void 0) {
|
|
2700
|
+
if (row.updated_at > clash.updated_at) {
|
|
2701
|
+
del.run(clash.id);
|
|
2702
|
+
this.pendingEmbed.delete(clash.id);
|
|
2703
|
+
upd.run(newId, newScopeId, row.id);
|
|
2704
|
+
} else {
|
|
2705
|
+
del.run(row.id);
|
|
2706
|
+
this.pendingEmbed.delete(row.id);
|
|
2707
|
+
}
|
|
2708
|
+
merged++;
|
|
2709
|
+
} else {
|
|
2710
|
+
upd.run(newId, newScopeId, row.id);
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
const sessions = this.db.prepare(`UPDATE OR IGNORE agent_sessions SET project_id = ? WHERE project_id = ?`).run(newId, oldId).changes;
|
|
2714
|
+
const events = this.db.prepare(`UPDATE OR IGNORE session_events SET project_id = ? WHERE project_id = ?`).run(newId, oldId).changes;
|
|
2715
|
+
return { memoryRows: incoming.length, memoryMerged: merged, sessions, events };
|
|
2716
|
+
});
|
|
2717
|
+
return run();
|
|
2718
|
+
}
|
|
2607
2719
|
/**
|
|
2608
2720
|
* Queue every record lacking a stored vector for a (re)embed. Run at
|
|
2609
2721
|
* open so a DB first built keyword-only — or one whose vectors were
|
|
@@ -3038,7 +3150,7 @@ ${h.record.value}`
|
|
|
3038
3150
|
let deletedSpools = 0;
|
|
3039
3151
|
let files;
|
|
3040
3152
|
try {
|
|
3041
|
-
files =
|
|
3153
|
+
files = readdirSync3(dir).filter((f) => f.endsWith(".jsonl"));
|
|
3042
3154
|
} catch {
|
|
3043
3155
|
return { sessions: 0, events: 0, deletedSpools: 0 };
|
|
3044
3156
|
}
|
|
@@ -3212,8 +3324,10 @@ ${h.record.value}`
|
|
|
3212
3324
|
|
|
3213
3325
|
// dist/composition.js
|
|
3214
3326
|
function projectIdFor(workspaceRoot) {
|
|
3215
|
-
|
|
3216
|
-
|
|
3327
|
+
return stableWorkspaceId(workspaceRoot);
|
|
3328
|
+
}
|
|
3329
|
+
function legacyProjectIdFor(workspaceRoot) {
|
|
3330
|
+
return legacyWorkspaceId(workspaceRoot);
|
|
3217
3331
|
}
|
|
3218
3332
|
function defaultMemoryDbPath() {
|
|
3219
3333
|
return join7(homedir5(), ".prometheus", "memory.db");
|
|
@@ -3463,7 +3577,7 @@ function composeFromEnv(opts) {
|
|
|
3463
3577
|
const override = (opts.workspaceRootOverride ?? "").trim();
|
|
3464
3578
|
const envRoot = (env.PROMETHEUS_WORKSPACE_ROOT ?? "").trim();
|
|
3465
3579
|
const claudeRoot = (env.CLAUDE_PROJECT_DIR ?? "").trim();
|
|
3466
|
-
const workspaceRoot =
|
|
3580
|
+
const workspaceRoot = resolve3(override !== "" ? override : envRoot !== "" ? envRoot : claudeRoot !== "" ? claudeRoot : process.cwd());
|
|
3467
3581
|
const projectId = projectIdFor(workspaceRoot);
|
|
3468
3582
|
const projectName = basename(workspaceRoot) || workspaceRoot;
|
|
3469
3583
|
const rawDbPath = env.PROMETHEUS_MEMORY_DB_PATH;
|
|
@@ -3482,6 +3596,21 @@ function composeFromEnv(opts) {
|
|
|
3482
3596
|
temporal,
|
|
3483
3597
|
dedup
|
|
3484
3598
|
});
|
|
3599
|
+
const legacyId = legacyProjectIdFor(workspaceRoot);
|
|
3600
|
+
const canonMigrationOff = /^(off|0|false|no)$/i.test((env.PROMETHEUS_CANON_MIGRATION ?? "").trim());
|
|
3601
|
+
if (legacyId !== projectId && !canonMigrationOff) {
|
|
3602
|
+
try {
|
|
3603
|
+
const m = backend.migrateProjectIdentity(legacyId, projectId);
|
|
3604
|
+
const spool = migrateRecorderSpoolDir(legacyId, projectId, env);
|
|
3605
|
+
if (m.memoryRows > 0 || m.sessions > 0 || m.events > 0 || spool.renamedDir || spool.movedFiles > 0) {
|
|
3606
|
+
process.stderr.write(`[prom.codes] canonical-hash migration: ${m.memoryRows} memory row(s) (${m.memoryMerged} merged), ${m.sessions} session(s), ${m.events} event(s), spool ${spool.renamedDir ? "dir renamed" : `${spool.movedFiles} file(s)`} ${legacyId} -> ${projectId}
|
|
3607
|
+
`);
|
|
3608
|
+
}
|
|
3609
|
+
} catch (err) {
|
|
3610
|
+
process.stderr.write(`[prom.codes] canonical-hash memory migration skipped (non-fatal): ${err.message}
|
|
3611
|
+
`);
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3485
3614
|
return {
|
|
3486
3615
|
backend,
|
|
3487
3616
|
recorder: backend,
|
|
@@ -4499,7 +4628,7 @@ ${f.value}`);
|
|
|
4499
4628
|
embeddingsError = err instanceof Error ? err.message : String(err);
|
|
4500
4629
|
}
|
|
4501
4630
|
}
|
|
4502
|
-
const update = await buildUpdateStatus("@prom.codes/memory-mcp", "0.
|
|
4631
|
+
const update = await buildUpdateStatus("@prom.codes/memory-mcp", "0.16.0", { isDevBuild: false });
|
|
4503
4632
|
let recorder;
|
|
4504
4633
|
try {
|
|
4505
4634
|
const scopes = ["project-local", "project", "user"];
|
|
@@ -4581,7 +4710,7 @@ ${f.value}`);
|
|
|
4581
4710
|
// dist/server.js
|
|
4582
4711
|
var SERVER_IDENTITY = {
|
|
4583
4712
|
name: "prometheus-memory-mcp",
|
|
4584
|
-
version: "0.
|
|
4713
|
+
version: "0.16.0",
|
|
4585
4714
|
title: "prom.codes Memory"
|
|
4586
4715
|
};
|
|
4587
4716
|
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.";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prom.codes/memory-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "prom.codes Memory — persistent, local-first agent memory as an MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
29
|
-
"better-sqlite3": "
|
|
29
|
+
"better-sqlite3": "12.10.0",
|
|
30
30
|
"zod": "^4.4.3"
|
|
31
31
|
},
|
|
32
32
|
"optionalDependencies": {
|