@prom.codes/memory-mcp 0.3.2 → 0.4.1
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 +210 -5
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -891,6 +891,114 @@ function requireApiKey(env) {
|
|
|
891
891
|
return key;
|
|
892
892
|
}
|
|
893
893
|
|
|
894
|
+
// dist/extraction.js
|
|
895
|
+
var SYSTEM_PROMPT = 'You extract durable, atomic facts from a coding agent\'s session notes for long-term project memory. Output ONLY a JSON array of objects {"key":..., "value":...}. Each fact must be ONE self-contained statement that will be useful in a FUTURE session: a decision, a convention, a preference, a stable configuration, or a learned fact about the project. `key` is a short kebab-case slug; `value` is the full fact in one sentence. DROP transient step-by-step narration, anything true only for this one session, and anything obvious. Never invent facts not supported by the notes. If there is nothing durable, output []. Output at most 12 facts.';
|
|
896
|
+
function parseExtraction(raw, maxFacts = 12, maxValueChars = 2e3) {
|
|
897
|
+
const match = raw.match(/\[[\s\S]*\]/);
|
|
898
|
+
if (!match)
|
|
899
|
+
return [];
|
|
900
|
+
let parsed;
|
|
901
|
+
try {
|
|
902
|
+
parsed = JSON.parse(match[0]);
|
|
903
|
+
} catch {
|
|
904
|
+
return [];
|
|
905
|
+
}
|
|
906
|
+
if (!Array.isArray(parsed))
|
|
907
|
+
return [];
|
|
908
|
+
const out = [];
|
|
909
|
+
const seen = /* @__PURE__ */ new Set();
|
|
910
|
+
for (const item of parsed) {
|
|
911
|
+
if (out.length >= maxFacts)
|
|
912
|
+
break;
|
|
913
|
+
if (typeof item !== "object" || item === null)
|
|
914
|
+
continue;
|
|
915
|
+
const rec = item;
|
|
916
|
+
const rawKey = typeof rec.key === "string" ? rec.key : "";
|
|
917
|
+
const value = typeof rec.value === "string" ? rec.value.trim() : "";
|
|
918
|
+
if (value === "")
|
|
919
|
+
continue;
|
|
920
|
+
const key = slugify(rawKey) || slugify(value).slice(0, 48);
|
|
921
|
+
if (key === "" || seen.has(key))
|
|
922
|
+
continue;
|
|
923
|
+
seen.add(key);
|
|
924
|
+
const fact = { key, value: value.slice(0, maxValueChars) };
|
|
925
|
+
out.push(typeof rec.confidence === "number" && rec.confidence >= 0 && rec.confidence <= 1 ? { ...fact, confidence: rec.confidence } : fact);
|
|
926
|
+
}
|
|
927
|
+
return out;
|
|
928
|
+
}
|
|
929
|
+
function slugify(s) {
|
|
930
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
931
|
+
}
|
|
932
|
+
var OpenAICompatExtractor = class {
|
|
933
|
+
name;
|
|
934
|
+
model;
|
|
935
|
+
#url;
|
|
936
|
+
#apiKey;
|
|
937
|
+
#maxRetries;
|
|
938
|
+
#retryBaseMs;
|
|
939
|
+
#temperature;
|
|
940
|
+
#fetch;
|
|
941
|
+
constructor(opts) {
|
|
942
|
+
this.name = opts.name;
|
|
943
|
+
this.model = opts.model;
|
|
944
|
+
this.#url = `${opts.baseUrl.replace(/\/+$/, "")}/chat/completions`;
|
|
945
|
+
this.#apiKey = opts.apiKey;
|
|
946
|
+
this.#maxRetries = opts.maxRetries ?? 3;
|
|
947
|
+
this.#retryBaseMs = opts.retryBaseMs ?? 500;
|
|
948
|
+
this.#temperature = opts.temperature ?? 0;
|
|
949
|
+
this.#fetch = opts.fetchImpl ?? fetch;
|
|
950
|
+
}
|
|
951
|
+
async extract(text, opts) {
|
|
952
|
+
const trimmed = text.trim();
|
|
953
|
+
if (trimmed === "")
|
|
954
|
+
return [];
|
|
955
|
+
const body = JSON.stringify({
|
|
956
|
+
model: this.model,
|
|
957
|
+
temperature: this.#temperature,
|
|
958
|
+
messages: [
|
|
959
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
960
|
+
{ role: "user", content: `Session notes:
|
|
961
|
+
|
|
962
|
+
${trimmed}` }
|
|
963
|
+
]
|
|
964
|
+
});
|
|
965
|
+
const headers = { "content-type": "application/json" };
|
|
966
|
+
if (this.#apiKey !== void 0 && this.#apiKey !== "") {
|
|
967
|
+
headers.authorization = `Bearer ${this.#apiKey}`;
|
|
968
|
+
}
|
|
969
|
+
let lastErr;
|
|
970
|
+
for (let attempt = 0; attempt <= this.#maxRetries; attempt++) {
|
|
971
|
+
try {
|
|
972
|
+
const res = await this.#fetch(this.#url, {
|
|
973
|
+
method: "POST",
|
|
974
|
+
headers,
|
|
975
|
+
body,
|
|
976
|
+
...opts?.signal ? { signal: opts.signal } : {}
|
|
977
|
+
});
|
|
978
|
+
if (res.status === 429 || res.status >= 500) {
|
|
979
|
+
lastErr = new Error(`extractor HTTP ${res.status}`);
|
|
980
|
+
} else if (!res.ok) {
|
|
981
|
+
return [];
|
|
982
|
+
} else {
|
|
983
|
+
const json = await res.json();
|
|
984
|
+
const content = json.choices?.[0]?.message?.content ?? "";
|
|
985
|
+
return parseExtraction(content);
|
|
986
|
+
}
|
|
987
|
+
} catch (err) {
|
|
988
|
+
lastErr = err;
|
|
989
|
+
}
|
|
990
|
+
if (attempt < this.#maxRetries) {
|
|
991
|
+
await delay(this.#retryBaseMs * 2 ** attempt);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
void lastErr;
|
|
995
|
+
return [];
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
function delay(ms) {
|
|
999
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1000
|
+
}
|
|
1001
|
+
|
|
894
1002
|
// dist/sqlite.js
|
|
895
1003
|
import { randomUUID } from "node:crypto";
|
|
896
1004
|
import { mkdirSync } from "node:fs";
|
|
@@ -1255,7 +1363,7 @@ var SqliteMemoryBackend = class {
|
|
|
1255
1363
|
{ id: "vec", items: vecHits.map((h) => ({ key: h.record.id, payload: h })) }
|
|
1256
1364
|
], { limit: poolLimit }).map((f) => f.payload);
|
|
1257
1365
|
}
|
|
1258
|
-
const reranked = await this.rerankPool(input.query, pool, finalLimit);
|
|
1366
|
+
const reranked = input.rerank === false ? pool : await this.rerankPool(input.query, pool, finalLimit);
|
|
1259
1367
|
return reranked.slice(0, finalLimit);
|
|
1260
1368
|
}
|
|
1261
1369
|
/**
|
|
@@ -1593,6 +1701,57 @@ function discoverMemoryReranker(env) {
|
|
|
1593
1701
|
}
|
|
1594
1702
|
throw new Error(`unknown PROMETHEUS_MEMORY_RERANK_PROVIDER="${forced}" (expected "none", "voyage", or "bge")`);
|
|
1595
1703
|
}
|
|
1704
|
+
function discoverMemoryExtractor(env) {
|
|
1705
|
+
const forced = (env.PROMETHEUS_MEMORY_EXTRACT_PROVIDER ?? "none").toLowerCase();
|
|
1706
|
+
if (forced === "" || forced === "none")
|
|
1707
|
+
return { id: "none", provider: null };
|
|
1708
|
+
if (forced === "mistral") {
|
|
1709
|
+
const apiKey = env.MISTRAL_API_KEY;
|
|
1710
|
+
if (apiKey === void 0 || apiKey === "") {
|
|
1711
|
+
throw new Error('PROMETHEUS_MEMORY_EXTRACT_PROVIDER="mistral" requires MISTRAL_API_KEY.');
|
|
1712
|
+
}
|
|
1713
|
+
return {
|
|
1714
|
+
id: "mistral",
|
|
1715
|
+
provider: new OpenAICompatExtractor({
|
|
1716
|
+
name: "mistral-extract",
|
|
1717
|
+
model: env.PROMETHEUS_MEMORY_EXTRACT_MODEL ?? "mistral-small-latest",
|
|
1718
|
+
baseUrl: env.MISTRAL_BASE_URL ?? "https://api.mistral.ai/v1",
|
|
1719
|
+
apiKey
|
|
1720
|
+
})
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
if (forced === "openai") {
|
|
1724
|
+
const apiKey = env.OPENAI_API_KEY;
|
|
1725
|
+
if (apiKey === void 0 || apiKey === "") {
|
|
1726
|
+
throw new Error('PROMETHEUS_MEMORY_EXTRACT_PROVIDER="openai" requires OPENAI_API_KEY.');
|
|
1727
|
+
}
|
|
1728
|
+
return {
|
|
1729
|
+
id: "openai",
|
|
1730
|
+
provider: new OpenAICompatExtractor({
|
|
1731
|
+
name: "openai-extract",
|
|
1732
|
+
model: env.PROMETHEUS_MEMORY_EXTRACT_MODEL ?? "gpt-4o-mini",
|
|
1733
|
+
baseUrl: env.OPENAI_BASE_URL ?? "https://api.openai.com/v1",
|
|
1734
|
+
apiKey
|
|
1735
|
+
})
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
if (forced === "generic" || forced === "openai-compat") {
|
|
1739
|
+
const baseUrl = env.PROMETHEUS_MEMORY_EXTRACT_ENDPOINT;
|
|
1740
|
+
if (baseUrl === void 0 || baseUrl === "") {
|
|
1741
|
+
throw new Error(`PROMETHEUS_MEMORY_EXTRACT_PROVIDER="${forced}" requires PROMETHEUS_MEMORY_EXTRACT_ENDPOINT.`);
|
|
1742
|
+
}
|
|
1743
|
+
return {
|
|
1744
|
+
id: "generic",
|
|
1745
|
+
provider: new OpenAICompatExtractor({
|
|
1746
|
+
name: env.PROMETHEUS_MEMORY_EXTRACT_NAME ?? "generic-extract",
|
|
1747
|
+
model: env.PROMETHEUS_MEMORY_EXTRACT_MODEL ?? "default",
|
|
1748
|
+
baseUrl,
|
|
1749
|
+
...env.PROMETHEUS_MEMORY_EXTRACT_API_KEY ? { apiKey: env.PROMETHEUS_MEMORY_EXTRACT_API_KEY } : {}
|
|
1750
|
+
})
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
throw new Error(`unknown PROMETHEUS_MEMORY_EXTRACT_PROVIDER="${forced}" (expected "none", "mistral", "openai", or "generic")`);
|
|
1754
|
+
}
|
|
1596
1755
|
function composeFromEnv(opts) {
|
|
1597
1756
|
const env = opts.env;
|
|
1598
1757
|
const override = (opts.workspaceRootOverride ?? "").trim();
|
|
@@ -1605,6 +1764,7 @@ function composeFromEnv(opts) {
|
|
|
1605
1764
|
const dbPath = rawDbPath !== void 0 && rawDbPath !== "" ? rawDbPath : defaultMemoryDbPath();
|
|
1606
1765
|
const { id: embedderId, embedder } = discoverMemoryEmbedder(env);
|
|
1607
1766
|
const { id: rerankerId, provider: reranker } = discoverMemoryReranker(env);
|
|
1767
|
+
const { id: extractorId, provider: extractor } = discoverMemoryExtractor(env);
|
|
1608
1768
|
const backend = new SqliteMemoryBackend(dbPath, {
|
|
1609
1769
|
...embedder !== void 0 ? { embedder } : {},
|
|
1610
1770
|
...reranker !== null ? { reranker } : {}
|
|
@@ -1619,6 +1779,8 @@ function composeFromEnv(opts) {
|
|
|
1619
1779
|
embedderId,
|
|
1620
1780
|
reranker,
|
|
1621
1781
|
rerankerId,
|
|
1782
|
+
extractor,
|
|
1783
|
+
extractorId,
|
|
1622
1784
|
close: () => backend.close()
|
|
1623
1785
|
};
|
|
1624
1786
|
}
|
|
@@ -2048,7 +2210,7 @@ var setupInput = {
|
|
|
2048
2210
|
runtimes: z.array(runtimeEnum).min(1).optional()
|
|
2049
2211
|
};
|
|
2050
2212
|
function registerTools(server, deps) {
|
|
2051
|
-
const { backend, workspaceRoot, projectId, projectName, dbPath } = deps;
|
|
2213
|
+
const { backend, workspaceRoot, projectId, projectName, dbPath, extractor } = deps;
|
|
2052
2214
|
server.registerTool("read", {
|
|
2053
2215
|
title: "Recall agent memory",
|
|
2054
2216
|
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.",
|
|
@@ -2112,6 +2274,41 @@ ${p.value}`)
|
|
|
2112
2274
|
assertValueSize(texts);
|
|
2113
2275
|
assertNoSecrets(texts);
|
|
2114
2276
|
const scope = args.scope ?? "project";
|
|
2277
|
+
let facts = args.facts ?? [];
|
|
2278
|
+
let extractedCount = 0;
|
|
2279
|
+
if (extractor) {
|
|
2280
|
+
const prose = [args.plan ?? "", args.outcome ?? ""].join("\n").trim();
|
|
2281
|
+
if (prose !== "") {
|
|
2282
|
+
try {
|
|
2283
|
+
const mined = await extractor.extract(prose);
|
|
2284
|
+
const have = new Set(facts.map((f) => f.key));
|
|
2285
|
+
const fresh = mined.filter((f) => {
|
|
2286
|
+
if (have.has(f.key))
|
|
2287
|
+
return false;
|
|
2288
|
+
try {
|
|
2289
|
+
assertNoSecrets(`${f.key}
|
|
2290
|
+
${f.value}`);
|
|
2291
|
+
} catch {
|
|
2292
|
+
return false;
|
|
2293
|
+
}
|
|
2294
|
+
have.add(f.key);
|
|
2295
|
+
return true;
|
|
2296
|
+
});
|
|
2297
|
+
if (fresh.length > 0) {
|
|
2298
|
+
facts = [
|
|
2299
|
+
...facts,
|
|
2300
|
+
...fresh.map((f) => ({
|
|
2301
|
+
key: f.key,
|
|
2302
|
+
value: f.value,
|
|
2303
|
+
...f.confidence !== void 0 ? { confidence: f.confidence } : {}
|
|
2304
|
+
}))
|
|
2305
|
+
];
|
|
2306
|
+
extractedCount = fresh.length;
|
|
2307
|
+
}
|
|
2308
|
+
} catch {
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2115
2312
|
const written = await backend.consolidate({
|
|
2116
2313
|
projectId,
|
|
2117
2314
|
scope,
|
|
@@ -2119,10 +2316,10 @@ ${p.value}`)
|
|
|
2119
2316
|
sessionId: args.sessionId,
|
|
2120
2317
|
plan: args.plan,
|
|
2121
2318
|
outcome: args.outcome,
|
|
2122
|
-
facts
|
|
2319
|
+
facts,
|
|
2123
2320
|
procedures: args.procedures
|
|
2124
2321
|
});
|
|
2125
|
-
return textResult({ written: written.map(recordToJson) });
|
|
2322
|
+
return textResult({ written: written.map(recordToJson), extracted: extractedCount });
|
|
2126
2323
|
});
|
|
2127
2324
|
server.registerTool("search", {
|
|
2128
2325
|
title: "Search agent memory",
|
|
@@ -2208,6 +2405,10 @@ var SERVER_IDENTITY = {
|
|
|
2208
2405
|
var SERVER_INSTRUCTIONS = "Persistent agent memory for this workspace. At the START of a session or task, call memory_read to recall facts, decisions and procedures from earlier sessions. When the user states a durable preference, decision or correction, store it with memory_write. Use memory_search for keyword recall when memory_read is not specific enough. At the END of a session, consolidate what was learned with memory_capture. Run memory_setup once per workspace to install the memory protocol into runtime rule files. Never store secrets, API keys or credentials \u2014 such writes are rejected.";
|
|
2209
2406
|
|
|
2210
2407
|
// dist/bin.js
|
|
2408
|
+
function looksLikeMissingNativeBinding(msg) {
|
|
2409
|
+
return /bindings file|better_sqlite3\.node|could not locate the bindings|node_module_version|was compiled against a different|invalid elf|\.node['"\s]/i.test(msg);
|
|
2410
|
+
}
|
|
2411
|
+
var NATIVE_BINDING_HINT = '\nThis looks like the native `better-sqlite3` module failed to load \u2014 usually\nbecause npm `ignore-scripts=true` (corporate hardening) made `npx` skip the\nnative build, so the binary was never produced. Fix it once, keeping your\nhardening intact:\n npm install -g @prom.codes/memory-mcp --ignore-scripts=false --foreground-scripts\nthen point Claude Code at the built binary instead of npx:\n claude mcp add memory -- node "$(npm root -g)/@prom.codes/memory-mcp/dist/bin.js"\nDocs: https://prom.codes/docs/mcp/claude-code\n';
|
|
2211
2412
|
async function main() {
|
|
2212
2413
|
const env = process.env;
|
|
2213
2414
|
const explicitRoot = (env.PROMETHEUS_WORKSPACE_ROOT ?? "").trim();
|
|
@@ -2237,7 +2438,7 @@ async function main() {
|
|
|
2237
2438
|
env,
|
|
2238
2439
|
...override !== void 0 && override !== "" ? { workspaceRootOverride: override } : {}
|
|
2239
2440
|
});
|
|
2240
|
-
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}
|
|
2441
|
+
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}
|
|
2241
2442
|
`);
|
|
2242
2443
|
registerTools(server, composed);
|
|
2243
2444
|
};
|
|
@@ -2258,6 +2459,8 @@ async function main() {
|
|
|
2258
2459
|
const message = err instanceof Error ? err.message : String(err);
|
|
2259
2460
|
process.stderr.write(`prometheus-memory-mcp: fatal during boot: ${message}
|
|
2260
2461
|
`);
|
|
2462
|
+
if (looksLikeMissingNativeBinding(message))
|
|
2463
|
+
process.stderr.write(NATIVE_BINDING_HINT);
|
|
2261
2464
|
process.exit(1);
|
|
2262
2465
|
}
|
|
2263
2466
|
};
|
|
@@ -2272,5 +2475,7 @@ main().catch((err) => {
|
|
|
2272
2475
|
const message = err instanceof Error ? err.message : String(err);
|
|
2273
2476
|
process.stderr.write(`prometheus-memory-mcp: fatal: ${message}
|
|
2274
2477
|
`);
|
|
2478
|
+
if (looksLikeMissingNativeBinding(message))
|
|
2479
|
+
process.stderr.write(NATIVE_BINDING_HINT);
|
|
2275
2480
|
process.exit(1);
|
|
2276
2481
|
});
|