@prom.codes/memory-mcp 0.15.1 → 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 +224 -29
- 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
|
|
@@ -2688,7 +2800,8 @@ var SqliteMemoryBackend = class {
|
|
|
2688
2800
|
const rows = this.db.prepare(sql).all(...params);
|
|
2689
2801
|
const resolved = resolveScopeChain(rows.map(rowToRecord), query.chain);
|
|
2690
2802
|
const deduped = this.dedupeRecords(resolved);
|
|
2691
|
-
const
|
|
2803
|
+
const offset = query.offset ?? 0;
|
|
2804
|
+
const limited = query.limit !== void 0 ? deduped.slice(offset, offset + query.limit) : deduped.slice(offset);
|
|
2692
2805
|
const bump = this.db.prepare(`UPDATE agent_memory SET use_count = use_count + 1 WHERE id = ?`);
|
|
2693
2806
|
for (const rec of limited) {
|
|
2694
2807
|
bump.run(rec.id);
|
|
@@ -3037,7 +3150,7 @@ ${h.record.value}`
|
|
|
3037
3150
|
let deletedSpools = 0;
|
|
3038
3151
|
let files;
|
|
3039
3152
|
try {
|
|
3040
|
-
files =
|
|
3153
|
+
files = readdirSync3(dir).filter((f) => f.endsWith(".jsonl"));
|
|
3041
3154
|
} catch {
|
|
3042
3155
|
return { sessions: 0, events: 0, deletedSpools: 0 };
|
|
3043
3156
|
}
|
|
@@ -3211,8 +3324,10 @@ ${h.record.value}`
|
|
|
3211
3324
|
|
|
3212
3325
|
// dist/composition.js
|
|
3213
3326
|
function projectIdFor(workspaceRoot) {
|
|
3214
|
-
|
|
3215
|
-
|
|
3327
|
+
return stableWorkspaceId(workspaceRoot);
|
|
3328
|
+
}
|
|
3329
|
+
function legacyProjectIdFor(workspaceRoot) {
|
|
3330
|
+
return legacyWorkspaceId(workspaceRoot);
|
|
3216
3331
|
}
|
|
3217
3332
|
function defaultMemoryDbPath() {
|
|
3218
3333
|
return join7(homedir5(), ".prometheus", "memory.db");
|
|
@@ -3462,7 +3577,7 @@ function composeFromEnv(opts) {
|
|
|
3462
3577
|
const override = (opts.workspaceRootOverride ?? "").trim();
|
|
3463
3578
|
const envRoot = (env.PROMETHEUS_WORKSPACE_ROOT ?? "").trim();
|
|
3464
3579
|
const claudeRoot = (env.CLAUDE_PROJECT_DIR ?? "").trim();
|
|
3465
|
-
const workspaceRoot =
|
|
3580
|
+
const workspaceRoot = resolve3(override !== "" ? override : envRoot !== "" ? envRoot : claudeRoot !== "" ? claudeRoot : process.cwd());
|
|
3466
3581
|
const projectId = projectIdFor(workspaceRoot);
|
|
3467
3582
|
const projectName = basename(workspaceRoot) || workspaceRoot;
|
|
3468
3583
|
const rawDbPath = env.PROMETHEUS_MEMORY_DB_PATH;
|
|
@@ -3481,6 +3596,21 @@ function composeFromEnv(opts) {
|
|
|
3481
3596
|
temporal,
|
|
3482
3597
|
dedup
|
|
3483
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
|
+
}
|
|
3484
3614
|
return {
|
|
3485
3615
|
backend,
|
|
3486
3616
|
recorder: backend,
|
|
@@ -3984,8 +4114,10 @@ function weave(records, options = {}) {
|
|
|
3984
4114
|
|
|
3985
4115
|
// dist/tools.js
|
|
3986
4116
|
var MAX_LIMIT = 100;
|
|
3987
|
-
var DEFAULT_READ_LIMIT =
|
|
4117
|
+
var DEFAULT_READ_LIMIT = 25;
|
|
3988
4118
|
var MAX_VALUE_CHARS = 64 * 1024;
|
|
4119
|
+
var RECORDS_TOKEN_BUDGET = 5e3;
|
|
4120
|
+
var MAX_VALUE_CHARS_IN_VIEW = 4e3;
|
|
3989
4121
|
function textResult(payload) {
|
|
3990
4122
|
return {
|
|
3991
4123
|
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
@@ -4009,6 +4141,52 @@ function recordToJson(rec) {
|
|
|
4009
4141
|
updatedAt: rec.updatedAt
|
|
4010
4142
|
};
|
|
4011
4143
|
}
|
|
4144
|
+
function recordToJsonPreview(rec, maxValueChars) {
|
|
4145
|
+
const view = recordToJson(rec);
|
|
4146
|
+
if (rec.value.length > maxValueChars) {
|
|
4147
|
+
const omitted = rec.value.length - maxValueChars;
|
|
4148
|
+
view.value = `${rec.value.slice(0, maxValueChars)}
|
|
4149
|
+
\u2026[+${omitted} chars omitted \u2014 use \`search\` for the full value]`;
|
|
4150
|
+
view.valueTruncated = true;
|
|
4151
|
+
view.valueChars = rec.value.length;
|
|
4152
|
+
}
|
|
4153
|
+
return view;
|
|
4154
|
+
}
|
|
4155
|
+
function boundRecordsForResult(records) {
|
|
4156
|
+
const views = [];
|
|
4157
|
+
let valuesPreviewed = 0;
|
|
4158
|
+
let tokens = 0;
|
|
4159
|
+
let overBudget = false;
|
|
4160
|
+
for (const rec of records) {
|
|
4161
|
+
const view = recordToJsonPreview(rec, MAX_VALUE_CHARS_IN_VIEW);
|
|
4162
|
+
if (view.valueTruncated === true)
|
|
4163
|
+
valuesPreviewed += 1;
|
|
4164
|
+
const cost = estimateTokens(JSON.stringify(view));
|
|
4165
|
+
if (views.length > 0 && tokens + cost > RECORDS_TOKEN_BUDGET) {
|
|
4166
|
+
overBudget = true;
|
|
4167
|
+
break;
|
|
4168
|
+
}
|
|
4169
|
+
views.push(view);
|
|
4170
|
+
tokens += cost;
|
|
4171
|
+
}
|
|
4172
|
+
return { views, shown: views.length, fetched: records.length, valuesPreviewed, overBudget };
|
|
4173
|
+
}
|
|
4174
|
+
function boundedRecordsNote(b, opts) {
|
|
4175
|
+
if (!b.overBudget && b.valuesPreviewed === 0 && b.fetched < opts.limit)
|
|
4176
|
+
return void 0;
|
|
4177
|
+
const parts = [];
|
|
4178
|
+
if (b.overBudget) {
|
|
4179
|
+
parts.push(`Showing ${b.shown} of ${b.fetched} fetched records \u2014 the result was trimmed to stay under the token budget. The \`woven\` block already carries the highest-value facts within its own cap.`);
|
|
4180
|
+
}
|
|
4181
|
+
if (b.valuesPreviewed > 0) {
|
|
4182
|
+
parts.push(`${b.valuesPreviewed} value(s) previewed to ${MAX_VALUE_CHARS_IN_VIEW} chars.`);
|
|
4183
|
+
}
|
|
4184
|
+
if (b.fetched >= opts.limit) {
|
|
4185
|
+
parts.push(`More records may exist beyond limit=${opts.limit}.`);
|
|
4186
|
+
}
|
|
4187
|
+
parts.push(`To see more: narrow with \`types\`, page with \`offset\` (e.g. offset=${(opts.offset ?? 0) + b.shown}), raise \`limit\`, or use \`search\` for specific facts.`);
|
|
4188
|
+
return parts.join(" ");
|
|
4189
|
+
}
|
|
4012
4190
|
function clampLimit(limit, def) {
|
|
4013
4191
|
if (limit === void 0)
|
|
4014
4192
|
return def;
|
|
@@ -4026,7 +4204,9 @@ var scopeEnum = z.enum(MEMORY_SCOPES);
|
|
|
4026
4204
|
var typeEnum = z.enum(MEMORY_TYPES);
|
|
4027
4205
|
var readInput = {
|
|
4028
4206
|
types: z.array(typeEnum).min(1).optional(),
|
|
4029
|
-
limit: z.number().int().positive().max(MAX_LIMIT).optional()
|
|
4207
|
+
limit: z.number().int().positive().max(MAX_LIMIT).optional(),
|
|
4208
|
+
/** Skip the first N resolved records — page through large memory sets. */
|
|
4209
|
+
offset: z.number().int().nonnegative().optional()
|
|
4030
4210
|
};
|
|
4031
4211
|
var writeInput = {
|
|
4032
4212
|
scope: scopeEnum.optional(),
|
|
@@ -4113,19 +4293,23 @@ function registerTools(server, source, hooks = {}) {
|
|
|
4113
4293
|
}));
|
|
4114
4294
|
reg("read", {
|
|
4115
4295
|
title: "Recall agent memory",
|
|
4116
|
-
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.",
|
|
4296
|
+
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). The `records` array is bounded to a token budget; a `note` explains how to page (`offset`) or narrow (`types`, `search`) when trimmed. Call this at the START of a session or task to recall what earlier sessions learned.",
|
|
4117
4297
|
inputSchema: readInput
|
|
4118
4298
|
}, async (args) => {
|
|
4119
4299
|
const deps = await ready();
|
|
4120
4300
|
const { backend, workspaceRoot, projectId, projectName } = deps;
|
|
4121
4301
|
const mirrorToFiles = !deps.rootIsHomeOrFsRoot;
|
|
4122
4302
|
const limit = clampLimit(args.limit, DEFAULT_READ_LIMIT);
|
|
4303
|
+
const offset = args.offset ?? 0;
|
|
4123
4304
|
const sync = mirrorToFiles ? await syncProjectFiles(backend, { projectId, workspaceRoot }) : { synced: 0, pruned: 0, skipped: [] };
|
|
4124
4305
|
const records = await backend.read({
|
|
4125
4306
|
chain: defaultScopeChain(projectId),
|
|
4126
4307
|
types: args.types,
|
|
4127
|
-
limit
|
|
4308
|
+
limit,
|
|
4309
|
+
offset
|
|
4128
4310
|
});
|
|
4311
|
+
const bounded = boundRecordsForResult(records);
|
|
4312
|
+
const note = boundedRecordsNote(bounded, { limit, offset });
|
|
4129
4313
|
return textResult({
|
|
4130
4314
|
projectId,
|
|
4131
4315
|
projectName,
|
|
@@ -4133,7 +4317,12 @@ function registerTools(server, source, hooks = {}) {
|
|
|
4133
4317
|
projectFilesPruned: sync.pruned,
|
|
4134
4318
|
...sync.skipped.length > 0 ? { skippedFiles: sync.skipped } : {},
|
|
4135
4319
|
woven: weave(records),
|
|
4136
|
-
|
|
4320
|
+
recordsShown: bounded.shown,
|
|
4321
|
+
recordsFetched: bounded.fetched,
|
|
4322
|
+
limit,
|
|
4323
|
+
offset,
|
|
4324
|
+
...note !== void 0 ? { note } : {},
|
|
4325
|
+
records: bounded.views
|
|
4137
4326
|
});
|
|
4138
4327
|
});
|
|
4139
4328
|
reg("write", {
|
|
@@ -4285,11 +4474,17 @@ ${f.value}`);
|
|
|
4285
4474
|
keyContains: args.keyContains,
|
|
4286
4475
|
limit
|
|
4287
4476
|
});
|
|
4477
|
+
const bounded = boundRecordsForResult(records);
|
|
4478
|
+
const note = boundedRecordsNote(bounded, { limit });
|
|
4288
4479
|
return textResult({
|
|
4289
4480
|
projectId,
|
|
4290
4481
|
projectName,
|
|
4291
4482
|
dbPath,
|
|
4292
|
-
|
|
4483
|
+
recordsShown: bounded.shown,
|
|
4484
|
+
recordsFetched: bounded.fetched,
|
|
4485
|
+
limit,
|
|
4486
|
+
...note !== void 0 ? { note } : {},
|
|
4487
|
+
records: bounded.views
|
|
4293
4488
|
});
|
|
4294
4489
|
});
|
|
4295
4490
|
reg("delete", {
|
|
@@ -4433,7 +4628,7 @@ ${f.value}`);
|
|
|
4433
4628
|
embeddingsError = err instanceof Error ? err.message : String(err);
|
|
4434
4629
|
}
|
|
4435
4630
|
}
|
|
4436
|
-
const update = await buildUpdateStatus("@prom.codes/memory-mcp", "0.
|
|
4631
|
+
const update = await buildUpdateStatus("@prom.codes/memory-mcp", "0.16.0", { isDevBuild: false });
|
|
4437
4632
|
let recorder;
|
|
4438
4633
|
try {
|
|
4439
4634
|
const scopes = ["project-local", "project", "user"];
|
|
@@ -4515,7 +4710,7 @@ ${f.value}`);
|
|
|
4515
4710
|
// dist/server.js
|
|
4516
4711
|
var SERVER_IDENTITY = {
|
|
4517
4712
|
name: "prometheus-memory-mcp",
|
|
4518
|
-
version: "0.
|
|
4713
|
+
version: "0.16.0",
|
|
4519
4714
|
title: "prom.codes Memory"
|
|
4520
4715
|
};
|
|
4521
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": {
|