@bcmyguest/openmem 0.0.0-stage → 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/.agents/skills/openmem/SKILL.md +34 -0
- package/LICENSE +190 -0
- package/README.md +140 -2
- package/bin/hindsight-project-index +530 -0
- package/cli/coding-agent/access-policy.mjs +38 -0
- package/cli/coding-agent/agents.mjs +34 -0
- package/cli/coding-agent/bank-from-cwd.mjs +140 -0
- package/cli/coding-agent/capabilities.mjs +34 -0
- package/cli/coding-agent/context-store.mjs +81 -0
- package/cli/coding-agent/context-trim.mjs +897 -0
- package/cli/coding-agent/file-io.mjs +72 -0
- package/cli/coding-agent/hook-harnesses.mjs +88 -0
- package/cli/coding-agent/hook-runner.mjs +284 -0
- package/cli/coding-agent/install-access.mjs +119 -0
- package/cli/coding-agent/install-context-proxy.mjs +333 -0
- package/cli/coding-agent/install-rtk.mjs +60 -0
- package/cli/coding-agent/install-shim.mjs +201 -0
- package/cli/coding-agent/opencode-config.mjs +107 -0
- package/cli/coding-agent/opencode-plugin.mjs +132 -0
- package/cli/coding-agent/opencode2-plugin/index.js +299 -0
- package/cli/coding-agent/recall-frame.mjs +29 -0
- package/cli/coding-agent/remove-upstream-access.mjs +122 -0
- package/cli/coding-agent/runtime.mjs +40 -0
- package/cli/coding-agent/session-db.mjs +626 -0
- package/cli/coding-agent/session-facade.mjs +86 -0
- package/cli/coding-agent/toml-edit.mjs +161 -0
- package/cli/cognee-mcp.mjs +260 -0
- package/cli/context-management.mjs +254 -0
- package/cli/feed-sync.mjs +27 -0
- package/cli/install-coding-agent.mjs +298 -0
- package/cli/install-memory-mcp.mjs +426 -0
- package/cli/is-main.mjs +18 -0
- package/cli/mcp/context-sql-runner.mjs +58 -0
- package/cli/mcp/context-store.mjs +670 -0
- package/cli/mcp/context.mjs +61 -0
- package/cli/mcp/export/cognee-dataset-name.mjs +12 -0
- package/cli/mcp/export/cognee.mjs +119 -0
- package/cli/mcp/export/hindsight.mjs +48 -0
- package/cli/mcp/export/index.mjs +26 -0
- package/cli/mcp/export/mem0.mjs +31 -0
- package/cli/mcp/knowledge.mjs +333 -0
- package/cli/mcp/llm-synthesize.mjs +50 -0
- package/cli/mcp/mem0-scope-id.mjs +22 -0
- package/cli/mcp/memory-facade.mjs +521 -0
- package/cli/mcp/memory-reflect-consolidate.mjs +120 -0
- package/cli/mcp/notebooklm-banks.mjs +21 -0
- package/cli/mcp/notebooklm-hindsight-rest.mjs +45 -0
- package/cli/mcp/notebooklm-provider.mjs +225 -0
- package/cli/mcp/pmm-records.mjs +23 -0
- package/cli/mcp/portability.mjs +158 -0
- package/cli/mcp/stdio.mjs +65 -0
- package/cli/mcp/unified-mcp-install.mjs +65 -0
- package/cli/mem0-mcp.mjs +108 -0
- package/cli/notebooklm-mcp.mjs +236 -0
- package/cli/openmem-mcp.mjs +38 -0
- package/cli/openmem.mjs +222 -0
- package/cli/roots.mjs +85 -0
- package/cli/schedule.mjs +176 -0
- package/cli/stack.mjs +438 -0
- package/cli/switch-memory-provider.mjs +358 -0
- package/cli/wiring-report.mjs +125 -0
- package/compose.bench.yaml +12 -0
- package/compose.yaml +227 -0
- package/config/coding-agent.json +13 -0
- package/config/hindsight.env.example +26 -0
- package/config/index.yml +54 -0
- package/config/litellm.lemond.yaml +40 -0
- package/config/litellm.ollama.yaml +33 -0
- package/config/notebooklm.yml +33 -0
- package/config/openmem.toml.example +124 -0
- package/docs/OPENMEM.md +23 -0
- package/docs/architecture.md +245 -0
- package/docs/assets/README.md +10 -0
- package/docs/assets/setup-demo.gif +0 -0
- package/docs/coding-agent-hooks.md +75 -0
- package/docs/coding-agent-scopes.md +22 -0
- package/docs/coding-agents.md +71 -0
- package/docs/cognee.md +37 -0
- package/docs/compatibility.md +80 -0
- package/docs/concepts.md +135 -0
- package/docs/context-management.md +255 -0
- package/docs/context-proxy.md +115 -0
- package/docs/context-research.md +215 -0
- package/docs/context-store.md +277 -0
- package/docs/hindsight.md +77 -0
- package/docs/index-sources-roadmap.md +38 -0
- package/docs/index-sources.md +39 -0
- package/docs/indexer-sinks.md +122 -0
- package/docs/indexing.md +57 -0
- package/docs/manual-stack-setup.md +74 -0
- package/docs/mcp-install-guide.md +4 -0
- package/docs/mcp-setup.md +104 -0
- package/docs/mem0.md +141 -0
- package/docs/memory-portability.md +73 -0
- package/docs/memory-providers.md +246 -0
- package/docs/meridian-litellm.md +198 -0
- package/docs/model-configuration.md +106 -0
- package/docs/notebooklm.md +83 -0
- package/docs/npx-install.md +172 -0
- package/docs/openmem-config.md +194 -0
- package/docs/provider-wizard-hooks.md +222 -0
- package/docs/quickstart.md +107 -0
- package/docs/setup-wizard.md +117 -0
- package/docs/sources-feeds.md +39 -0
- package/docs/sources-readwise.md +23 -0
- package/docs/sources-zotero.md +22 -0
- package/glama.json +4 -0
- package/indexer/Dockerfile +38 -0
- package/indexer/code_outline.py +146 -0
- package/indexer/feed_sync.py +107 -0
- package/indexer/index_config.py +290 -0
- package/indexer/pytest.ini +5 -0
- package/indexer/sinks/__init__.py +148 -0
- package/indexer/sinks/_http.py +44 -0
- package/indexer/sinks/cognee.py +329 -0
- package/indexer/sinks/hindsight.py +107 -0
- package/indexer/sinks/mem0.py +247 -0
- package/indexer/watch.py +240 -0
- package/package.json +96 -4
- package/providers/_template/compose.yaml +52 -0
- package/providers/cognee/OPERATOR.md +82 -0
- package/providers/cognee/compose.yaml +89 -0
- package/providers/cognee/lib/clack.mjs +157 -0
- package/providers/cognee/lib/coding-agent.mjs +26 -0
- package/providers/cognee/lib/cognee-mcp-install.mjs +153 -0
- package/providers/cognee/lib/config.mjs +98 -0
- package/providers/cognee/lib/env-file.mjs +194 -0
- package/providers/cognee/lib/env-setup.mjs +83 -0
- package/providers/cognee/lib/llm-env.mjs +382 -0
- package/providers/cognee/lib/ports.mjs +25 -0
- package/providers/cognee/lib/smoke.mjs +246 -0
- package/providers/cognee/lib/stack-env.mjs +134 -0
- package/providers/cognee/lib/state.mjs +101 -0
- package/providers/cognee/lib/summary.mjs +56 -0
- package/providers/cognee/lib/validate.mjs +97 -0
- package/providers/cognee/manifest.json +56 -0
- package/providers/cognee/wizard.mjs +134 -0
- package/providers/graphiti/manifest.json +15 -0
- package/providers/hindsight/lib/clack.mjs +347 -0
- package/providers/hindsight/lib/coding-agent.mjs +204 -0
- package/providers/hindsight/lib/config.mjs +79 -0
- package/providers/hindsight/lib/env-setup.mjs +72 -0
- package/providers/hindsight/lib/litellm-routes.mjs +196 -0
- package/providers/hindsight/lib/meridian.mjs +307 -0
- package/providers/hindsight/lib/model-backend.mjs +243 -0
- package/providers/hindsight/lib/notebooklm-mcp-install.mjs +127 -0
- package/providers/hindsight/lib/ollama-compose.mjs +51 -0
- package/providers/hindsight/lib/ports.mjs +30 -0
- package/providers/hindsight/lib/probe.mjs +54 -0
- package/providers/hindsight/lib/stack-env.mjs +125 -0
- package/providers/hindsight/lib/state.mjs +80 -0
- package/providers/hindsight/lib/summary.mjs +57 -0
- package/providers/hindsight/lib/validate.mjs +81 -0
- package/providers/hindsight/manifest.json +58 -0
- package/providers/hindsight/wizard.mjs +150 -0
- package/providers/letta/manifest.json +29 -0
- package/providers/mem0/OPERATOR.md +143 -0
- package/providers/mem0/compose.yaml +79 -0
- package/providers/mem0/lib/coding-agent.mjs +58 -0
- package/providers/mem0/lib/config.mjs +45 -0
- package/providers/mem0/lib/configure.mjs +201 -0
- package/providers/mem0/lib/dotenv.mjs +71 -0
- package/providers/mem0/lib/env-file.mjs +110 -0
- package/providers/mem0/lib/http.mjs +69 -0
- package/providers/mem0/lib/litellm.mjs +457 -0
- package/providers/mem0/lib/mcp-install.mjs +165 -0
- package/providers/mem0/lib/secret.mjs +30 -0
- package/providers/mem0/lib/smoke.mjs +179 -0
- package/providers/mem0/lib/stack-env.mjs +255 -0
- package/providers/mem0/lib/state.mjs +67 -0
- package/providers/mem0/lib/summary.mjs +88 -0
- package/providers/mem0/lib/url.mjs +106 -0
- package/providers/mem0/lib/validate.mjs +158 -0
- package/providers/mem0/manifest.json +47 -0
- package/providers/mem0/wizard.mjs +146 -0
- package/providers/zep/manifest.json +29 -0
- package/proxy/config.yaml +33 -0
- package/proxy/openmem_context/__init__.py +6 -0
- package/proxy/openmem_context/adapters.py +504 -0
- package/proxy/openmem_context/compress.py +53 -0
- package/proxy/openmem_context/core.py +606 -0
- package/proxy/openmem_context/embed.py +212 -0
- package/proxy/openmem_context/recall.py +49 -0
- package/proxy/openmem_context/session.py +70 -0
- package/proxy/openmem_context/sessiondb.py +276 -0
- package/proxy/openmem_context/settings.py +134 -0
- package/proxy/openmem_context/store.py +129 -0
- package/proxy/openmem_context/tokens.py +123 -0
- package/proxy/openmem_context/ttl.py +78 -0
- package/proxy/openmem_context_hook.py +34 -0
- package/scripts/check-pack.mjs +219 -0
- package/scripts/gen-masks.mjs +83 -0
- package/scripts/gen-masks.sh +4 -0
- package/scripts/promotion-provider-smoke.mjs +34 -0
- package/scripts/run-setup.sh +30 -0
- package/scripts/setup.mjs +107 -0
- package/scripts/setup.sh +5 -0
- package/scripts/spin-up.sh +45 -0
- package/scripts/wizard.sh +5 -0
- package/server.json +34 -0
- package/sync/Dockerfile +27 -0
- package/sync/memory_backend.py +87 -0
- package/sync/notebooklm_sync.py +664 -0
- package/wizard/bin/pm.mjs +97 -0
- package/wizard/bin/setup.mjs +33 -0
- package/wizard/lib/compose-version.mjs +32 -0
- package/wizard/lib/compose.mjs +70 -0
- package/wizard/lib/context-step.mjs +140 -0
- package/wizard/lib/curl.mjs +103 -0
- package/wizard/lib/docker-build.mjs +148 -0
- package/wizard/lib/exec.mjs +41 -0
- package/wizard/lib/indexer.mjs +6 -0
- package/wizard/lib/litellm-env.mjs +55 -0
- package/wizard/lib/load-machine-env.mjs +4 -0
- package/wizard/lib/log.mjs +32 -0
- package/wizard/lib/notebooklm-agent-access.mjs +92 -0
- package/wizard/lib/notebooklm.mjs +214 -0
- package/wizard/lib/parse-args.mjs +87 -0
- package/wizard/lib/paths.mjs +39 -0
- package/wizard/lib/pm-config.mjs +371 -0
- package/wizard/lib/ports.mjs +94 -0
- package/wizard/lib/prompts.mjs +85 -0
- package/wizard/lib/provider-wizard/helpers.mjs +98 -0
- package/wizard/lib/provider-wizard/run-steps.mjs +129 -0
- package/wizard/lib/providers/registry.mjs +262 -0
- package/wizard/lib/run-clack.mjs +108 -0
- package/wizard/lib/run-wizard.mjs +750 -0
- package/wizard/lib/validate-setup.mjs +62 -0
- package/wizard/lib/wizard-action.mjs +34 -0
- package/wizard/lib/wizard-conf.mjs +71 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Hindsight REST helpers for notebooklm::* banks (shared by MCP server and provider adapter). */
|
|
2
|
+
|
|
3
|
+
export async function listBanks(apiUrl, fetchFn = fetch) {
|
|
4
|
+
const res = await fetchFn(`${apiUrl}/v1/default/banks`);
|
|
5
|
+
if (!res.ok) throw new Error(`GET /v1/default/banks → ${res.status} ${res.statusText}`);
|
|
6
|
+
const data = await res.json();
|
|
7
|
+
return Array.isArray(data?.banks) ? data.banks : Array.isArray(data) ? data : [];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function listMemories(apiUrl, bankId, query, opts = {}, fetchFn = fetch) {
|
|
11
|
+
const limit = Number.isFinite(opts.limit) ? Math.max(0, Math.floor(opts.limit)) : 20;
|
|
12
|
+
const offset = Number.isFinite(opts.offset) ? Math.max(0, Math.floor(opts.offset)) : 0;
|
|
13
|
+
const qs = new URLSearchParams({ q: query, limit: String(limit), offset: String(offset) });
|
|
14
|
+
const url = `${apiUrl}/v1/default/banks/${encodeURIComponent(bankId)}/memories/list?${qs}`;
|
|
15
|
+
const res = await fetchFn(url);
|
|
16
|
+
const text = await res.text();
|
|
17
|
+
if (!res.ok) throw new Error(`GET memories/list ${bankId} → ${res.status}: ${text.slice(0, 400)}`);
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(text);
|
|
20
|
+
} catch {
|
|
21
|
+
return text;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function recallBank(apiUrl, bankId, query, fetchFn = fetch, opts = {}) {
|
|
26
|
+
const url = `${apiUrl}/v1/default/banks/${encodeURIComponent(bankId)}/memories/recall`;
|
|
27
|
+
const body = {
|
|
28
|
+
query,
|
|
29
|
+
budget: opts.budget || "low",
|
|
30
|
+
max_tokens: opts.max_tokens ?? 1024,
|
|
31
|
+
include: { entities: null, chunks: null, source_facts: null },
|
|
32
|
+
};
|
|
33
|
+
const res = await fetchFn(url, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { "content-type": "application/json" },
|
|
36
|
+
body: JSON.stringify(body),
|
|
37
|
+
});
|
|
38
|
+
const text = await res.text();
|
|
39
|
+
if (!res.ok) throw new Error(`POST recall ${bankId} → ${res.status}: ${text.slice(0, 400)}`);
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(text);
|
|
42
|
+
} catch {
|
|
43
|
+
return text;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { cogneeDatasetName } from "./export/cognee-dataset-name.mjs";
|
|
3
|
+
import { mem0ScopeId } from "./mem0-scope-id.mjs";
|
|
4
|
+
import { bankAllowed, filterNotebooklmBanks, isNotebooklmBank } from "./notebooklm-banks.mjs";
|
|
5
|
+
import { listBanks, listMemories, recallBank } from "./notebooklm-hindsight-rest.mjs";
|
|
6
|
+
import { search as cogneeSearch } from "../cognee-mcp.mjs";
|
|
7
|
+
|
|
8
|
+
function readStateBanks(statePath) {
|
|
9
|
+
if (!statePath || !fs.existsSync(statePath)) return [];
|
|
10
|
+
try {
|
|
11
|
+
const data = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
12
|
+
const docs = data?.documents && typeof data.documents === "object" ? data.documents : {};
|
|
13
|
+
const banks = new Set();
|
|
14
|
+
for (const row of Object.values(docs)) {
|
|
15
|
+
if (row?.bank && isNotebooklmBank(String(row.bank))) banks.add(String(row.bank));
|
|
16
|
+
}
|
|
17
|
+
return [...banks];
|
|
18
|
+
} catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function mem0Headers(ctx) {
|
|
24
|
+
/** @type {Record<string, string>} */
|
|
25
|
+
const headers = {};
|
|
26
|
+
if (ctx.mem0?.apiKey) headers["X-API-Key"] = ctx.mem0.apiKey;
|
|
27
|
+
return headers;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function mem0Get(base, path, headers, fetchFn) {
|
|
31
|
+
const res = await fetchFn(`${base.replace(/\/$/, "")}${path}`, { headers });
|
|
32
|
+
const text = await res.text();
|
|
33
|
+
if (!res.ok) throw new Error(`GET ${path} → ${res.status}: ${text.slice(0, 300)}`);
|
|
34
|
+
return JSON.parse(text);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function mem0MemoryText(row) {
|
|
38
|
+
return row?.memory ?? row?.text ?? row?.content ?? row?.data ?? "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @param {import("./context.mjs").McpContext} ctx @param {string[]} allowlist @param {typeof fetch} fetchFn */
|
|
42
|
+
export async function listNotebooklmBanks(ctx, allowlist, fetchFn = fetch) {
|
|
43
|
+
if (ctx.provider === "hindsight") {
|
|
44
|
+
const url = ctx.hindsightUrl || "http://127.0.0.1:8888";
|
|
45
|
+
return filterNotebooklmBanks(await listBanks(url, fetchFn), allowlist).map((b) => ({
|
|
46
|
+
bank_id: b.bank_id || b.id || b.name,
|
|
47
|
+
fact_count: b.fact_count ?? null,
|
|
48
|
+
name: b.name ?? null,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
if (ctx.provider === "mem0") {
|
|
52
|
+
const base = ctx.mem0?.baseUrl;
|
|
53
|
+
if (!base) throw new Error("mem0 API URL not configured");
|
|
54
|
+
const headers = mem0Headers(ctx);
|
|
55
|
+
const statePath = process.env.NOTEBOOKLM_STATE || "";
|
|
56
|
+
const candidates = new Set();
|
|
57
|
+
if (allowlist.includes("*")) {
|
|
58
|
+
for (const b of readStateBanks(statePath)) candidates.add(b);
|
|
59
|
+
if (!candidates.size && !statePath) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"mem0: set NOTEBOOKLM_STATE to the sync state.json path, or list explicit notebooklmAccess.banks",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const entry of allowlist) {
|
|
66
|
+
if (entry === "*") continue;
|
|
67
|
+
candidates.add(entry.startsWith("notebooklm::") ? entry : `notebooklm::${entry}`);
|
|
68
|
+
}
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const bankId of candidates) {
|
|
71
|
+
if (!bankAllowed(bankId, allowlist)) continue;
|
|
72
|
+
const uid = encodeURIComponent(mem0ScopeId(bankId));
|
|
73
|
+
const json = await mem0Get(base, `/memories?user_id=${uid}&top_k=1000`, headers, fetchFn);
|
|
74
|
+
const results = Array.isArray(json?.results) ? json.results : [];
|
|
75
|
+
if (results.length || !allowlist.includes("*")) {
|
|
76
|
+
out.push({ bank_id: bankId, fact_count: results.length, name: null });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
if (ctx.provider === "cognee") {
|
|
82
|
+
const base = ctx.cognee?.baseUrl;
|
|
83
|
+
if (!base) throw new Error("Cognee API URL not configured");
|
|
84
|
+
const headers = { ...(ctx.cognee?.apiKey ? { "X-Api-Key": ctx.cognee.apiKey } : {}) };
|
|
85
|
+
const res = await fetchFn(`${base.replace(/\/$/, "")}/api/v1/datasets`, { headers });
|
|
86
|
+
const text = await res.text();
|
|
87
|
+
if (!res.ok) throw new Error(`GET datasets → ${res.status}: ${text.slice(0, 300)}`);
|
|
88
|
+
const datasets = JSON.parse(text);
|
|
89
|
+
const list = Array.isArray(datasets) ? datasets : datasets?.datasets || [];
|
|
90
|
+
const byBank = new Map();
|
|
91
|
+
for (const ds of list) {
|
|
92
|
+
const id = ds.id ?? ds.dataset_id;
|
|
93
|
+
if (!id) continue;
|
|
94
|
+
const dataRes = await fetchFn(`${base.replace(/\/$/, "")}/api/v1/datasets/${id}/data?limit=5`, { headers });
|
|
95
|
+
if (!dataRes.ok) continue;
|
|
96
|
+
const page = await dataRes.json();
|
|
97
|
+
const rows = Array.isArray(page) ? page : page?.data || [];
|
|
98
|
+
for (const row of rows) {
|
|
99
|
+
const meta = row?.external_metadata ?? row?.externalMetadata;
|
|
100
|
+
let bank = null;
|
|
101
|
+
if (Array.isArray(meta)) {
|
|
102
|
+
for (const m of meta) if (m?.bank) bank = m.bank;
|
|
103
|
+
} else if (meta && typeof meta === "object" && meta.bank) bank = meta.bank;
|
|
104
|
+
else if (typeof meta === "string") {
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(meta);
|
|
107
|
+
if (parsed?.bank) bank = parsed.bank;
|
|
108
|
+
} catch {
|
|
109
|
+
/* ignore */
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (bank && isNotebooklmBank(bank) && bankAllowed(bank, allowlist)) {
|
|
113
|
+
byBank.set(bank, { bank_id: bank, fact_count: null, name: ds.name ?? null });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return [...byBank.values()];
|
|
118
|
+
}
|
|
119
|
+
throw new Error(`notebooklm MCP does not support provider ${ctx.provider}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** @param {import("./context.mjs").McpContext} ctx @param {{ bankId: string, query: string, mode: string, limit: number, hindsightApiUrl?: string }} opts @param {typeof fetch} fetchFn */
|
|
123
|
+
export async function searchNotebooklmBank(ctx, opts, fetchFn = fetch) {
|
|
124
|
+
const { bankId, query, mode, limit, hindsightApiUrl } = opts;
|
|
125
|
+
if (ctx.provider === "hindsight") {
|
|
126
|
+
const apiUrl = ctx.hindsightUrl || hindsightApiUrl || "http://127.0.0.1:8888";
|
|
127
|
+
if (mode === "recall") {
|
|
128
|
+
return { bank_id: bankId, query, mode, result: await recallBank(apiUrl, bankId, query, fetchFn) };
|
|
129
|
+
}
|
|
130
|
+
const listed = await listMemories(apiUrl, bankId, query, { limit }, fetchFn);
|
|
131
|
+
const items = Array.isArray(listed?.items)
|
|
132
|
+
? listed.items.map((m) => ({
|
|
133
|
+
id: m.id ?? m.memory_id ?? null,
|
|
134
|
+
type: m.type ?? null,
|
|
135
|
+
text: m.text ?? m.content ?? null,
|
|
136
|
+
}))
|
|
137
|
+
: listed;
|
|
138
|
+
return {
|
|
139
|
+
bank_id: bankId,
|
|
140
|
+
query,
|
|
141
|
+
mode: "list",
|
|
142
|
+
total: listed?.total ?? null,
|
|
143
|
+
limit: listed?.limit ?? limit,
|
|
144
|
+
offset: listed?.offset ?? 0,
|
|
145
|
+
items,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (ctx.provider === "mem0") {
|
|
149
|
+
const base = ctx.mem0?.baseUrl;
|
|
150
|
+
if (!base) throw new Error("mem0 API URL not configured");
|
|
151
|
+
const headers = { "content-type": "application/json", ...mem0Headers(ctx) };
|
|
152
|
+
const uid = mem0ScopeId(bankId);
|
|
153
|
+
if (mode === "recall") {
|
|
154
|
+
const res = await fetchFn(`${base.replace(/\/$/, "")}/search`, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers,
|
|
157
|
+
body: JSON.stringify({ query, filters: { user_id: uid }, top_k: limit }),
|
|
158
|
+
});
|
|
159
|
+
const text = await res.text();
|
|
160
|
+
if (!res.ok) throw new Error(`POST /search → ${res.status}: ${text.slice(0, 300)}`);
|
|
161
|
+
return { bank_id: bankId, query, mode: "recall", result: JSON.parse(text) };
|
|
162
|
+
}
|
|
163
|
+
const json = await mem0Get(
|
|
164
|
+
base,
|
|
165
|
+
`/memories?user_id=${encodeURIComponent(uid)}&top_k=${Math.min(limit, 1000)}`,
|
|
166
|
+
mem0Headers(ctx),
|
|
167
|
+
fetchFn,
|
|
168
|
+
);
|
|
169
|
+
const q = query.toLowerCase();
|
|
170
|
+
const results = (Array.isArray(json?.results) ? json.results : []).filter((r) =>
|
|
171
|
+
mem0MemoryText(r).toLowerCase().includes(q),
|
|
172
|
+
);
|
|
173
|
+
return {
|
|
174
|
+
bank_id: bankId,
|
|
175
|
+
query,
|
|
176
|
+
mode: "list",
|
|
177
|
+
items: results.slice(0, limit).map((r) => ({
|
|
178
|
+
id: r.id ?? null,
|
|
179
|
+
type: null,
|
|
180
|
+
text: mem0MemoryText(r),
|
|
181
|
+
})),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (ctx.provider === "cognee") {
|
|
185
|
+
const dataset = cogneeDatasetName(bankId);
|
|
186
|
+
if (mode === "recall") {
|
|
187
|
+
const result = await cogneeSearch(
|
|
188
|
+
{ baseUrl: ctx.cognee.baseUrl, apiKey: ctx.cognee?.apiKey },
|
|
189
|
+
{ query, datasets: [dataset] },
|
|
190
|
+
fetchFn,
|
|
191
|
+
);
|
|
192
|
+
return { bank_id: bankId, query, mode: "recall", result };
|
|
193
|
+
}
|
|
194
|
+
const base = ctx.cognee.baseUrl.replace(/\/$/, "");
|
|
195
|
+
const headers = { ...(ctx.cognee?.apiKey ? { "X-Api-Key": ctx.cognee.apiKey } : {}) };
|
|
196
|
+
const dsRes = await fetchFn(`${base}/api/v1/datasets`, { headers });
|
|
197
|
+
const dsText = await dsRes.text();
|
|
198
|
+
if (!dsRes.ok) throw new Error(`GET datasets → ${dsRes.status}`);
|
|
199
|
+
const datasets = JSON.parse(dsText);
|
|
200
|
+
const list = Array.isArray(datasets) ? datasets : datasets?.datasets || [];
|
|
201
|
+
const match = list.find((d) => (d.name || d.dataset_name) === dataset);
|
|
202
|
+
if (!match?.id) throw new Error(`Cognee dataset not found for bank ${bankId}`);
|
|
203
|
+
const q = query.toLowerCase();
|
|
204
|
+
const items = [];
|
|
205
|
+
let offset = 0;
|
|
206
|
+
while (items.length < limit) {
|
|
207
|
+
const pageRes = await fetchFn(`${base}/api/v1/datasets/${match.id}/data?limit=50&offset=${offset}`, { headers });
|
|
208
|
+
if (!pageRes.ok) break;
|
|
209
|
+
const page = await pageRes.json();
|
|
210
|
+
const rows = Array.isArray(page) ? page : page?.data || [];
|
|
211
|
+
if (!rows.length) break;
|
|
212
|
+
for (const row of rows) {
|
|
213
|
+
const text = row?.text ?? row?.content ?? row?.data ?? "";
|
|
214
|
+
if (String(text).toLowerCase().includes(q)) {
|
|
215
|
+
items.push({ id: row.id ?? null, type: null, text: String(text) });
|
|
216
|
+
if (items.length >= limit) break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
offset += rows.length;
|
|
220
|
+
if (rows.length < 50) break;
|
|
221
|
+
}
|
|
222
|
+
return { bank_id: bankId, query, mode: "list", items };
|
|
223
|
+
}
|
|
224
|
+
throw new Error(`notebooklm MCP does not support provider ${ctx.provider}`);
|
|
225
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** PMM record ordering and duplicate fingerprints (export/import). */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stable sort for deterministic exports and diffs.
|
|
5
|
+
* @param {object[]} records
|
|
6
|
+
*/
|
|
7
|
+
export function sortPmmRecords(records) {
|
|
8
|
+
return [...records].sort((a, b) => {
|
|
9
|
+
const sa = String(a.scope_key ?? "");
|
|
10
|
+
const sb = String(b.scope_key ?? "");
|
|
11
|
+
if (sa !== sb) return sa.localeCompare(sb);
|
|
12
|
+
const ia = String(a.id ?? "");
|
|
13
|
+
const ib = String(b.id ?? "");
|
|
14
|
+
if (ia !== ib) return ia.localeCompare(ib);
|
|
15
|
+
return String(a.content ?? "").localeCompare(String(b.content ?? ""));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @param {string} content */
|
|
20
|
+
export function pmmContentFingerprint(content) {
|
|
21
|
+
const t = String(content || "").trim().replace(/\s+/g, " ");
|
|
22
|
+
return t;
|
|
23
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/** PMM (Personal Memory Manifest) export/import — orchestrator over provider adapters. */
|
|
2
|
+
|
|
3
|
+
import { runProviderExport } from "./export/index.mjs";
|
|
4
|
+
import { exportMem0 } from "./export/mem0.mjs";
|
|
5
|
+
import { pmmContentFingerprint, sortPmmRecords } from "./pmm-records.mjs";
|
|
6
|
+
|
|
7
|
+
export const PMM_VERSION = 1;
|
|
8
|
+
export const PMM_SUPPORTED_MAJOR = 1;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {unknown} version
|
|
12
|
+
*/
|
|
13
|
+
export function assertPmmVersion(version) {
|
|
14
|
+
const n = Number(version);
|
|
15
|
+
if (!Number.isFinite(n) || Math.floor(n) !== n) {
|
|
16
|
+
throw new Error(`unsupported pmm_version ${String(version)}`);
|
|
17
|
+
}
|
|
18
|
+
const major = n;
|
|
19
|
+
if (major !== PMM_SUPPORTED_MAJOR) {
|
|
20
|
+
throw new Error(`unsupported PMM major version ${major} (supported: ${PMM_SUPPORTED_MAJOR})`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {{ provider: string, scope: Record<string, unknown>, records: object[] }} parts
|
|
26
|
+
*/
|
|
27
|
+
export function buildPmm(parts) {
|
|
28
|
+
return {
|
|
29
|
+
pmm_version: PMM_VERSION,
|
|
30
|
+
exported_at: new Date().toISOString(),
|
|
31
|
+
source_provider: parts.provider,
|
|
32
|
+
scope: parts.scope,
|
|
33
|
+
records: sortPmmRecords(parts.records || []),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {ReturnType<typeof buildPmm>} pmm
|
|
39
|
+
* @param {string} format
|
|
40
|
+
*/
|
|
41
|
+
export function serializePmm(pmm, format) {
|
|
42
|
+
if (format === "jsonl") {
|
|
43
|
+
const header = { ...pmm, records: undefined };
|
|
44
|
+
const lines = [JSON.stringify(header), ...(pmm.records || []).map((r) => JSON.stringify(r))];
|
|
45
|
+
return `${lines.join("\n")}\n`;
|
|
46
|
+
}
|
|
47
|
+
return pmm;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} data
|
|
52
|
+
* @returns {{ pmm: ReturnType<typeof buildPmm> }}
|
|
53
|
+
*/
|
|
54
|
+
export function parsePmm(data) {
|
|
55
|
+
const trimmed = String(data || "").trim();
|
|
56
|
+
if (!trimmed) throw new Error("import data is empty");
|
|
57
|
+
let document = null;
|
|
58
|
+
try { document = JSON.parse(trimmed); } catch { /* JSONL contains multiple JSON objects. */ }
|
|
59
|
+
if (document && typeof document === "object" && !Array.isArray(document)) {
|
|
60
|
+
const pmm = document;
|
|
61
|
+
assertPmmVersion(pmm.pmm_version);
|
|
62
|
+
return { pmm: { ...pmm, records: sortPmmRecords(pmm.records || []) } };
|
|
63
|
+
}
|
|
64
|
+
/** @type {ReturnType<typeof buildPmm>["records"]} */
|
|
65
|
+
const records = [];
|
|
66
|
+
let meta = null;
|
|
67
|
+
for (const line of trimmed.split("\n")) {
|
|
68
|
+
const row = line.trim();
|
|
69
|
+
if (!row) continue;
|
|
70
|
+
const obj = JSON.parse(row);
|
|
71
|
+
if (obj.pmm_version === PMM_VERSION && obj.source_provider) {
|
|
72
|
+
meta = obj;
|
|
73
|
+
if (Array.isArray(obj.records)) records.push(...obj.records);
|
|
74
|
+
} else if (obj.content != null) {
|
|
75
|
+
records.push(obj);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!meta) throw new Error("jsonl import requires a PMM header line or a single JSON object");
|
|
79
|
+
assertPmmVersion(meta.pmm_version);
|
|
80
|
+
return { pmm: { ...meta, records: sortPmmRecords(records) } };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {ReturnType<typeof buildPmm>} pmm
|
|
85
|
+
* @param {import("./context.mjs").resolveMcpContext extends (...a: any) => infer R ? R : never} ctx
|
|
86
|
+
* @param {typeof fetch} fetchFn
|
|
87
|
+
*/
|
|
88
|
+
async function mem0ExistingKeys(ctx, userId, fetchFn) {
|
|
89
|
+
const { records } = await exportMem0(ctx, { user_id: userId }, fetchFn);
|
|
90
|
+
const ids = new Set();
|
|
91
|
+
const fingerprints = new Set();
|
|
92
|
+
for (const r of records) {
|
|
93
|
+
if (r.id) ids.add(String(r.id));
|
|
94
|
+
fingerprints.add(pmmContentFingerprint(String(r.content || "")));
|
|
95
|
+
}
|
|
96
|
+
return { ids, fingerprints };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {import("./context.mjs").resolveMcpContext extends (...a: any) => infer R ? R : never} ctx
|
|
101
|
+
* @param {{ bank?: string, format?: string }} args
|
|
102
|
+
* @param {typeof fetch} fetchFn
|
|
103
|
+
*/
|
|
104
|
+
export async function memoryExport(ctx, args, fetchFn = fetch) {
|
|
105
|
+
const format = String(args.format || "json").toLowerCase();
|
|
106
|
+
const { scope, records, note } = await runProviderExport(ctx.provider, ctx, args, fetchFn);
|
|
107
|
+
const pmm = buildPmm({ provider: ctx.provider, scope, records });
|
|
108
|
+
if (note) pmm.note = note;
|
|
109
|
+
return serializePmm(pmm, format);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {import("./context.mjs").resolveMcpContext extends (...a: any) => infer R ? R : never} ctx
|
|
114
|
+
* @param {string} data
|
|
115
|
+
* @param {{ merge_strategy?: string }} args
|
|
116
|
+
* @param {typeof fetch} fetchFn
|
|
117
|
+
* @param {(ctx: any, name: string, toolArgs: object, fetchFn: typeof fetch) => Promise<any>} addFn
|
|
118
|
+
*/
|
|
119
|
+
export async function memoryImport(ctx, data, args, fetchFn, addFn) {
|
|
120
|
+
const { pmm } = parsePmm(data);
|
|
121
|
+
const strategy = String(args.merge_strategy || "skip_duplicates");
|
|
122
|
+
/** @type {string[]} */
|
|
123
|
+
const imported = [];
|
|
124
|
+
/** @type {string[]} */
|
|
125
|
+
const skipped = [];
|
|
126
|
+
/** @type {Map<string, { ids: Set<string>, fingerprints: Set<string> }>} */
|
|
127
|
+
const mem0Seen = new Map();
|
|
128
|
+
for (const rec of pmm.records || []) {
|
|
129
|
+
const content = String(rec.content || "").trim();
|
|
130
|
+
if (!content) continue;
|
|
131
|
+
const bank = String(rec.scope_key || pmm.scope?.bank || pmm.scope?.user_id || "openmem-mcp");
|
|
132
|
+
if (strategy === "skip_duplicates" && ctx.provider === "mem0") {
|
|
133
|
+
if (!mem0Seen.has(bank)) mem0Seen.set(bank, await mem0ExistingKeys(ctx, bank, fetchFn));
|
|
134
|
+
const seen = mem0Seen.get(bank);
|
|
135
|
+
const fp = pmmContentFingerprint(content);
|
|
136
|
+
if ((rec.id && seen.ids.has(String(rec.id))) || seen.fingerprints.has(fp)) {
|
|
137
|
+
skipped.push(rec.id ? String(rec.id) : fp.slice(0, 40));
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
seen.fingerprints.add(fp);
|
|
141
|
+
if (rec.id) seen.ids.add(String(rec.id));
|
|
142
|
+
}
|
|
143
|
+
const result = await addFn(ctx, "memory_add", { content, bank }, fetchFn);
|
|
144
|
+
if (result?.isError) {
|
|
145
|
+
throw new Error(`memory import failed in bank ${bank}: ${result.content?.[0]?.text || "memory_add returned an error"}`);
|
|
146
|
+
}
|
|
147
|
+
imported.push(rec.id ? String(rec.id) : content.slice(0, 40));
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
pmm_version: PMM_VERSION,
|
|
151
|
+
target_provider: ctx.provider,
|
|
152
|
+
source_provider: pmm.source_provider,
|
|
153
|
+
merge_strategy: strategy,
|
|
154
|
+
imported_count: imported.length,
|
|
155
|
+
skipped_count: skipped.length,
|
|
156
|
+
cross_provider: pmm.source_provider !== ctx.provider,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** MCP stdio framing (NDJSON + Content-Length). Shared by unified and provider MCP servers. */
|
|
2
|
+
|
|
3
|
+
/** @param {unknown} obj @param {"ndjson" | "content-length"} framing */
|
|
4
|
+
export function writeMessage(obj, framing = "ndjson") {
|
|
5
|
+
if (obj == null) return;
|
|
6
|
+
const body = JSON.stringify(obj);
|
|
7
|
+
if (framing === "content-length") {
|
|
8
|
+
process.stdout.write(`Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
process.stdout.write(`${body}\n`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @param {AsyncIterable<Buffer | string>} input @param {(msg: Record<string, unknown>) => Promise<unknown>} onMsg */
|
|
15
|
+
export async function serveStdio(input, onMsg) {
|
|
16
|
+
let buf = Buffer.alloc(0);
|
|
17
|
+
for await (const chunk of input) {
|
|
18
|
+
buf = Buffer.concat([buf, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
|
19
|
+
while (buf.length) {
|
|
20
|
+
const asStr = buf.toString("utf8");
|
|
21
|
+
const crlf = asStr.indexOf("\r\n\r\n");
|
|
22
|
+
const lf = asStr.indexOf("\n\n");
|
|
23
|
+
const headerEnd = crlf >= 0 ? crlf : lf;
|
|
24
|
+
const sepLen = crlf >= 0 ? 4 : lf >= 0 ? 2 : -1;
|
|
25
|
+
if (sepLen > 0 && /content-length\s*:/i.test(asStr.slice(0, headerEnd))) {
|
|
26
|
+
const m = /Content-Length:\s*(\d+)/i.exec(asStr.slice(0, headerEnd));
|
|
27
|
+
if (!m) {
|
|
28
|
+
buf = buf.subarray(Buffer.byteLength(asStr.slice(0, headerEnd + sepLen), "utf8"));
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const len = Number(m[1]);
|
|
32
|
+
const headerBytes = Buffer.byteLength(asStr.slice(0, headerEnd + sepLen), "utf8");
|
|
33
|
+
if (buf.length < headerBytes + len) break;
|
|
34
|
+
const body = buf.subarray(headerBytes, headerBytes + len).toString("utf8");
|
|
35
|
+
buf = buf.subarray(headerBytes + len);
|
|
36
|
+
let msg;
|
|
37
|
+
try {
|
|
38
|
+
msg = JSON.parse(body);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
writeMessage(
|
|
41
|
+
{
|
|
42
|
+
jsonrpc: "2.0",
|
|
43
|
+
id: null,
|
|
44
|
+
error: { code: -32700, message: `Parse error: ${err instanceof Error ? err.message : err}` },
|
|
45
|
+
},
|
|
46
|
+
"content-length",
|
|
47
|
+
);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
writeMessage(await onMsg(msg), "content-length");
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const nl = asStr.indexOf("\n");
|
|
54
|
+
if (nl < 0) break;
|
|
55
|
+
const line = asStr.slice(0, nl).trim();
|
|
56
|
+
buf = buf.subarray(Buffer.byteLength(asStr.slice(0, nl + 1), "utf8"));
|
|
57
|
+
if (!line.startsWith("{")) continue;
|
|
58
|
+
try {
|
|
59
|
+
writeMessage(await onMsg(JSON.parse(line)), "ndjson");
|
|
60
|
+
} catch {
|
|
61
|
+
/* skip bad line */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { mergeNamedMcpServer } from "../install-memory-mcp.mjs";
|
|
5
|
+
|
|
6
|
+
/** @param {string} repoRoot */
|
|
7
|
+
export function unifiedMcpScriptPath(repoRoot) {
|
|
8
|
+
return path.resolve(repoRoot, "cli/openmem-mcp.mjs");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {{ provider: string, scriptPath: string, stackRoot?: string }} opts
|
|
13
|
+
*/
|
|
14
|
+
export function unifiedMcpEntry({ provider, scriptPath, stackRoot }) {
|
|
15
|
+
/** @type {Record<string, string>} */
|
|
16
|
+
const env = { MEMORY_PROVIDER: provider };
|
|
17
|
+
if (stackRoot) env.PM_STACK_ROOT = stackRoot;
|
|
18
|
+
return {
|
|
19
|
+
command: "node",
|
|
20
|
+
args: [scriptPath],
|
|
21
|
+
env,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {{ home: string, repoRoot: string, stackRoot: string, provider: string, dry?: boolean }} opts
|
|
27
|
+
* @param {{ ok: Function, info: Function }} h
|
|
28
|
+
*/
|
|
29
|
+
export function installUnifiedMcp(opts, h) {
|
|
30
|
+
const mcpPath = path.join(opts.home, ".cursor/mcp.json");
|
|
31
|
+
const entry = unifiedMcpEntry({
|
|
32
|
+
provider: opts.provider,
|
|
33
|
+
scriptPath: unifiedMcpScriptPath(opts.repoRoot),
|
|
34
|
+
stackRoot: opts.stackRoot,
|
|
35
|
+
});
|
|
36
|
+
if (opts.dry) {
|
|
37
|
+
h.info(`[dry-run] merge openmem MCP into ${mcpPath}`);
|
|
38
|
+
return { changed: false };
|
|
39
|
+
}
|
|
40
|
+
const mcp = fs.existsSync(mcpPath) ? JSON.parse(fs.readFileSync(mcpPath, "utf8")) : {};
|
|
41
|
+
const { mcp: next, changed, skipped } = mergeNamedMcpServer(mcp, "openmem", entry, {
|
|
42
|
+
protect: ["hindsight", "codegraph", "notebooklm", "mem0", "cognee"],
|
|
43
|
+
});
|
|
44
|
+
const servers = { ...(next.mcpServers && typeof next.mcpServers === "object" ? next.mcpServers : {}) };
|
|
45
|
+
const droppedLegacy = Boolean(servers["personal-memory"]);
|
|
46
|
+
if (droppedLegacy) delete servers["personal-memory"];
|
|
47
|
+
const finalMcp = { ...next, mcpServers: servers };
|
|
48
|
+
const wrote = Boolean(changed) || droppedLegacy;
|
|
49
|
+
if (wrote) {
|
|
50
|
+
fs.mkdirSync(path.dirname(mcpPath), { recursive: true });
|
|
51
|
+
fs.writeFileSync(mcpPath, `${JSON.stringify(finalMcp, null, 2)}\n`);
|
|
52
|
+
h.ok(`openmem MCP → ${mcpPath}`);
|
|
53
|
+
} else if (skipped) {
|
|
54
|
+
h.info(`openmem MCP ${skipped}`);
|
|
55
|
+
} else {
|
|
56
|
+
h.info("openmem MCP already configured");
|
|
57
|
+
}
|
|
58
|
+
return { changed: wrote };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** @param {string} [pkgRoot] */
|
|
62
|
+
export function defaultUnifiedMcpScriptPath(pkgRoot) {
|
|
63
|
+
const root = pkgRoot || path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
64
|
+
return unifiedMcpScriptPath(root);
|
|
65
|
+
}
|
package/cli/mem0-mcp.mjs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Stdio launcher for the self-hosted mem0 MCP bridge.
|
|
4
|
+
*
|
|
5
|
+
* Loads MEM0_API_KEY from MEM0_ENV_FILE / ~/.config/personal-memory/mem0.env when unset,
|
|
6
|
+
* maps MEM0_API_URL → MEM0_BASE_URL for @yeyuan98/mem0-mcp, then execs:
|
|
7
|
+
* npx -y @yeyuan98/mem0-mcp
|
|
8
|
+
*
|
|
9
|
+
* Never prints the API key. Cursor mcp.json must not embed the secret — only the URL
|
|
10
|
+
* and optional MEM0_ENV_FILE path (see providers/mem0/lib/mcp-install.mjs).
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { isMainModule } from "./is-main.mjs";
|
|
17
|
+
import { readKey } from "../providers/mem0/lib/env-file.mjs";
|
|
18
|
+
import { MEM0_MCP_NPM_PACKAGE } from "../providers/mem0/lib/mcp-install.mjs";
|
|
19
|
+
import { defaultEnvFile } from "../providers/mem0/lib/state.mjs";
|
|
20
|
+
|
|
21
|
+
const BIN = "mem0-mcp";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
25
|
+
* @param {string} [home]
|
|
26
|
+
*/
|
|
27
|
+
export function resolveEnvFileForLaunch(env = process.env, home = process.env.HOME || os.homedir()) {
|
|
28
|
+
const raw = String(env.MEM0_ENV_FILE || "").trim();
|
|
29
|
+
if (!raw) return defaultEnvFile(home);
|
|
30
|
+
if (raw.startsWith("~/")) return path.join(home, raw.slice(2));
|
|
31
|
+
return path.isAbsolute(raw) ? raw : path.resolve(raw);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Prepare env for the upstream MCP: BASE_URL + API_KEY. Mutates a shallow copy only.
|
|
36
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
37
|
+
* @returns {{ env: NodeJS.ProcessEnv, envFile: string, hadKeyInEnv: boolean }}
|
|
38
|
+
*/
|
|
39
|
+
export function prepareMem0McpEnv(env = process.env) {
|
|
40
|
+
const next = { ...env };
|
|
41
|
+
const apiUrl = String(next.MEM0_API_URL || next.MEM0_BASE_URL || "")
|
|
42
|
+
.trim()
|
|
43
|
+
.replace(/\/+$/, "");
|
|
44
|
+
if (apiUrl) {
|
|
45
|
+
next.MEM0_API_URL = apiUrl;
|
|
46
|
+
next.MEM0_BASE_URL = apiUrl;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const hadKeyInEnv = Boolean(String(next.MEM0_API_KEY || "").trim());
|
|
50
|
+
const envFile = resolveEnvFileForLaunch(next);
|
|
51
|
+
if (!hadKeyInEnv) {
|
|
52
|
+
const fromFile = readKey(envFile);
|
|
53
|
+
if (fromFile) next.MEM0_API_KEY = fromFile;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { env: next, envFile, hadKeyInEnv };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {{ env?: NodeJS.ProcessEnv, spawnFn?: typeof spawn }} [opts]
|
|
61
|
+
* @returns {Promise<number>} exit code
|
|
62
|
+
*/
|
|
63
|
+
export function runMem0Mcp(opts = {}) {
|
|
64
|
+
const spawnFn = opts.spawnFn || spawn;
|
|
65
|
+
const { env, envFile } = prepareMem0McpEnv(opts.env || process.env);
|
|
66
|
+
|
|
67
|
+
if (!String(env.MEM0_BASE_URL || "").trim()) {
|
|
68
|
+
console.error(
|
|
69
|
+
`${BIN}: set MEM0_API_URL or MEM0_BASE_URL to your self-hosted mem0 REST server (e.g. http://127.0.0.1:18888)`,
|
|
70
|
+
);
|
|
71
|
+
return Promise.resolve(1);
|
|
72
|
+
}
|
|
73
|
+
if (!String(env.MEM0_API_KEY || "").trim()) {
|
|
74
|
+
console.error(
|
|
75
|
+
`${BIN}: MEM0_API_KEY missing. Put it in ${envFile} (mode 600) or export it in the agent environment.` +
|
|
76
|
+
` (AUTH_DISABLED=true servers still need a non-empty placeholder for this MCP bridge.)`,
|
|
77
|
+
);
|
|
78
|
+
return Promise.resolve(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
const child = spawnFn("npx", ["-y", MEM0_MCP_NPM_PACKAGE], {
|
|
83
|
+
env,
|
|
84
|
+
stdio: "inherit",
|
|
85
|
+
shell: false,
|
|
86
|
+
});
|
|
87
|
+
child.on("error", (err) => {
|
|
88
|
+
console.error(`${BIN}: failed to start npx ${MEM0_MCP_NPM_PACKAGE}: ${err.message}`);
|
|
89
|
+
resolve(1);
|
|
90
|
+
});
|
|
91
|
+
child.on("exit", (code, signal) => {
|
|
92
|
+
if (signal) resolve(1);
|
|
93
|
+
else resolve(code == null ? 1 : code);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function main() {
|
|
99
|
+
const code = await runMem0Mcp();
|
|
100
|
+
process.exit(code);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (isMainModule(import.meta.url)) {
|
|
104
|
+
main().catch((err) => {
|
|
105
|
+
console.error(`${BIN}: ${err instanceof Error ? err.message : err}`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
});
|
|
108
|
+
}
|