@bcmyguest/openmem 0.0.0-stage → 0.6.3

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.
Files changed (191) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +136 -2
  3. package/bin/hindsight-project-index +530 -0
  4. package/cli/coding-agent/access-policy.mjs +38 -0
  5. package/cli/coding-agent/agents.mjs +34 -0
  6. package/cli/coding-agent/bank-from-cwd.mjs +140 -0
  7. package/cli/coding-agent/capabilities.mjs +23 -0
  8. package/cli/coding-agent/hook-harnesses.mjs +87 -0
  9. package/cli/coding-agent/hook-runner.mjs +260 -0
  10. package/cli/coding-agent/install-access.mjs +93 -0
  11. package/cli/coding-agent/install-shim.mjs +97 -0
  12. package/cli/coding-agent/opencode-config.mjs +91 -0
  13. package/cli/coding-agent/opencode-plugin.mjs +96 -0
  14. package/cli/coding-agent/runtime.mjs +39 -0
  15. package/cli/coding-agent/session-facade.mjs +62 -0
  16. package/cli/cognee-mcp.mjs +260 -0
  17. package/cli/feed-sync.mjs +11 -0
  18. package/cli/install-coding-agent.mjs +165 -0
  19. package/cli/install-memory-mcp.mjs +426 -0
  20. package/cli/is-main.mjs +18 -0
  21. package/cli/mcp/context.mjs +61 -0
  22. package/cli/mcp/export/cognee-dataset-name.mjs +12 -0
  23. package/cli/mcp/export/cognee.mjs +119 -0
  24. package/cli/mcp/export/hindsight.mjs +48 -0
  25. package/cli/mcp/export/index.mjs +26 -0
  26. package/cli/mcp/export/mem0.mjs +31 -0
  27. package/cli/mcp/llm-synthesize.mjs +50 -0
  28. package/cli/mcp/mem0-scope-id.mjs +22 -0
  29. package/cli/mcp/memory-facade.mjs +455 -0
  30. package/cli/mcp/memory-reflect-consolidate.mjs +120 -0
  31. package/cli/mcp/notebooklm-banks.mjs +21 -0
  32. package/cli/mcp/notebooklm-hindsight-rest.mjs +45 -0
  33. package/cli/mcp/notebooklm-provider.mjs +225 -0
  34. package/cli/mcp/pmm-records.mjs +23 -0
  35. package/cli/mcp/portability.mjs +158 -0
  36. package/cli/mcp/stdio.mjs +65 -0
  37. package/cli/mcp/unified-mcp-install.mjs +65 -0
  38. package/cli/mem0-mcp.mjs +108 -0
  39. package/cli/notebooklm-mcp.mjs +236 -0
  40. package/cli/openmem-mcp.mjs +35 -0
  41. package/cli/openmem.mjs +205 -0
  42. package/cli/roots.mjs +85 -0
  43. package/cli/stack.mjs +436 -0
  44. package/cli/switch-memory-provider.mjs +358 -0
  45. package/cli/wiring-report.mjs +125 -0
  46. package/compose.bench.yaml +12 -0
  47. package/compose.yaml +164 -0
  48. package/config/coding-agent.json +13 -0
  49. package/config/hindsight.env.example +24 -0
  50. package/config/index.yml +54 -0
  51. package/config/litellm.lemond.yaml +40 -0
  52. package/config/litellm.ollama.yaml +33 -0
  53. package/config/notebooklm.yml +33 -0
  54. package/config/openmem.toml.example +62 -0
  55. package/docs/OPENMEM.md +23 -0
  56. package/docs/architecture.md +219 -0
  57. package/docs/assets/README.md +10 -0
  58. package/docs/assets/setup-demo.gif +0 -0
  59. package/docs/coding-agent-hooks.md +51 -0
  60. package/docs/coding-agent-scopes.md +22 -0
  61. package/docs/coding-agents.md +51 -0
  62. package/docs/cognee.md +37 -0
  63. package/docs/concepts.md +112 -0
  64. package/docs/hindsight.md +74 -0
  65. package/docs/index-sources-roadmap.md +38 -0
  66. package/docs/index-sources.md +39 -0
  67. package/docs/indexer-sinks.md +122 -0
  68. package/docs/indexing.md +57 -0
  69. package/docs/manual-stack-setup.md +74 -0
  70. package/docs/mcp-install-guide.md +4 -0
  71. package/docs/mcp-setup.md +101 -0
  72. package/docs/mem0.md +103 -0
  73. package/docs/memory-portability.md +73 -0
  74. package/docs/memory-providers.md +246 -0
  75. package/docs/model-configuration.md +84 -0
  76. package/docs/notebooklm.md +83 -0
  77. package/docs/npx-install.md +170 -0
  78. package/docs/openmem-config.md +194 -0
  79. package/docs/provider-wizard-hooks.md +222 -0
  80. package/docs/quickstart.md +101 -0
  81. package/docs/setup-wizard.md +116 -0
  82. package/docs/sources-feeds.md +52 -0
  83. package/docs/sources-readwise.md +23 -0
  84. package/docs/sources-zotero.md +22 -0
  85. package/glama.json +4 -0
  86. package/indexer/Dockerfile +38 -0
  87. package/indexer/code_outline.py +146 -0
  88. package/indexer/feed_sync.py +107 -0
  89. package/indexer/index_config.py +290 -0
  90. package/indexer/pytest.ini +5 -0
  91. package/indexer/sinks/__init__.py +148 -0
  92. package/indexer/sinks/_http.py +44 -0
  93. package/indexer/sinks/cognee.py +329 -0
  94. package/indexer/sinks/hindsight.py +107 -0
  95. package/indexer/sinks/mem0.py +247 -0
  96. package/indexer/watch.py +240 -0
  97. package/package.json +95 -4
  98. package/providers/_template/compose.yaml +52 -0
  99. package/providers/cognee/OPERATOR.md +82 -0
  100. package/providers/cognee/compose.yaml +89 -0
  101. package/providers/cognee/lib/clack.mjs +157 -0
  102. package/providers/cognee/lib/coding-agent.mjs +26 -0
  103. package/providers/cognee/lib/cognee-mcp-install.mjs +153 -0
  104. package/providers/cognee/lib/config.mjs +98 -0
  105. package/providers/cognee/lib/env-file.mjs +194 -0
  106. package/providers/cognee/lib/env-setup.mjs +83 -0
  107. package/providers/cognee/lib/llm-env.mjs +382 -0
  108. package/providers/cognee/lib/ports.mjs +25 -0
  109. package/providers/cognee/lib/smoke.mjs +246 -0
  110. package/providers/cognee/lib/stack-env.mjs +134 -0
  111. package/providers/cognee/lib/state.mjs +101 -0
  112. package/providers/cognee/lib/summary.mjs +56 -0
  113. package/providers/cognee/lib/validate.mjs +97 -0
  114. package/providers/cognee/manifest.json +56 -0
  115. package/providers/cognee/wizard.mjs +134 -0
  116. package/providers/graphiti/manifest.json +15 -0
  117. package/providers/hindsight/lib/clack.mjs +245 -0
  118. package/providers/hindsight/lib/coding-agent.mjs +204 -0
  119. package/providers/hindsight/lib/config.mjs +73 -0
  120. package/providers/hindsight/lib/env-setup.mjs +71 -0
  121. package/providers/hindsight/lib/litellm-routes.mjs +187 -0
  122. package/providers/hindsight/lib/model-backend.mjs +214 -0
  123. package/providers/hindsight/lib/notebooklm-mcp-install.mjs +127 -0
  124. package/providers/hindsight/lib/ollama-compose.mjs +51 -0
  125. package/providers/hindsight/lib/ports.mjs +30 -0
  126. package/providers/hindsight/lib/probe.mjs +33 -0
  127. package/providers/hindsight/lib/stack-env.mjs +132 -0
  128. package/providers/hindsight/lib/state.mjs +75 -0
  129. package/providers/hindsight/lib/summary.mjs +43 -0
  130. package/providers/hindsight/lib/validate.mjs +75 -0
  131. package/providers/hindsight/manifest.json +58 -0
  132. package/providers/hindsight/wizard.mjs +149 -0
  133. package/providers/letta/manifest.json +29 -0
  134. package/providers/mem0/OPERATOR.md +124 -0
  135. package/providers/mem0/compose.yaml +57 -0
  136. package/providers/mem0/lib/coding-agent.mjs +58 -0
  137. package/providers/mem0/lib/config.mjs +45 -0
  138. package/providers/mem0/lib/configure.mjs +135 -0
  139. package/providers/mem0/lib/dotenv.mjs +71 -0
  140. package/providers/mem0/lib/env-file.mjs +110 -0
  141. package/providers/mem0/lib/http.mjs +69 -0
  142. package/providers/mem0/lib/mcp-install.mjs +165 -0
  143. package/providers/mem0/lib/secret.mjs +30 -0
  144. package/providers/mem0/lib/smoke.mjs +179 -0
  145. package/providers/mem0/lib/stack-env.mjs +216 -0
  146. package/providers/mem0/lib/state.mjs +67 -0
  147. package/providers/mem0/lib/summary.mjs +60 -0
  148. package/providers/mem0/lib/url.mjs +106 -0
  149. package/providers/mem0/lib/validate.mjs +158 -0
  150. package/providers/mem0/manifest.json +47 -0
  151. package/providers/mem0/wizard.mjs +133 -0
  152. package/providers/zep/manifest.json +29 -0
  153. package/scripts/check-pack.mjs +218 -0
  154. package/scripts/gen-masks.mjs +83 -0
  155. package/scripts/gen-masks.sh +4 -0
  156. package/scripts/promotion-provider-smoke.mjs +34 -0
  157. package/scripts/run-setup.sh +30 -0
  158. package/scripts/setup.mjs +94 -0
  159. package/scripts/setup.sh +5 -0
  160. package/scripts/spin-up.sh +45 -0
  161. package/scripts/wizard.sh +5 -0
  162. package/server.json +34 -0
  163. package/skills/openmem/SKILL.md +29 -0
  164. package/sync/Dockerfile +27 -0
  165. package/sync/memory_backend.py +87 -0
  166. package/sync/notebooklm_sync.py +664 -0
  167. package/wizard/bin/pm.mjs +97 -0
  168. package/wizard/bin/setup.mjs +33 -0
  169. package/wizard/lib/compose-version.mjs +32 -0
  170. package/wizard/lib/compose.mjs +70 -0
  171. package/wizard/lib/curl.mjs +103 -0
  172. package/wizard/lib/docker-build.mjs +148 -0
  173. package/wizard/lib/exec.mjs +41 -0
  174. package/wizard/lib/indexer.mjs +6 -0
  175. package/wizard/lib/load-machine-env.mjs +4 -0
  176. package/wizard/lib/log.mjs +32 -0
  177. package/wizard/lib/notebooklm-agent-access.mjs +92 -0
  178. package/wizard/lib/notebooklm.mjs +214 -0
  179. package/wizard/lib/parse-args.mjs +87 -0
  180. package/wizard/lib/paths.mjs +39 -0
  181. package/wizard/lib/pm-config.mjs +163 -0
  182. package/wizard/lib/ports.mjs +94 -0
  183. package/wizard/lib/prompts.mjs +57 -0
  184. package/wizard/lib/provider-wizard/helpers.mjs +98 -0
  185. package/wizard/lib/provider-wizard/run-steps.mjs +129 -0
  186. package/wizard/lib/providers/registry.mjs +262 -0
  187. package/wizard/lib/run-clack.mjs +108 -0
  188. package/wizard/lib/run-wizard.mjs +755 -0
  189. package/wizard/lib/validate-setup.mjs +62 -0
  190. package/wizard/lib/wizard-action.mjs +34 -0
  191. package/wizard/lib/wizard-conf.mjs +71 -0
@@ -0,0 +1,455 @@
1
+ import { search as cogneeSearch } from "../cognee-mcp.mjs";
2
+ import { callMemoryConsolidate, callMemoryReflect } from "./memory-reflect-consolidate.mjs";
3
+ import { memoryExport, memoryImport } from "./portability.mjs";
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { parseDocument } from "yaml";
8
+ import { bankFromCwd } from "../coding-agent/bank-from-cwd.mjs";
9
+ import { accessAllowed, accessConfigPath, readAccessConfig } from "../coding-agent/access-policy.mjs";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
13
+
14
+ export const UNIFIED_MCP_TOOLS = [
15
+ {
16
+ name: "memory_project_scope",
17
+ description: "Read the local project bank mapping and session access policy for a workspace. Makes no memory-provider calls.",
18
+ inputSchema: {
19
+ type: "object",
20
+ properties: { cwd: { type: "string", description: "Absolute workspace path (defaults to the MCP process working directory)" } },
21
+ additionalProperties: false,
22
+ },
23
+ },
24
+ {
25
+ name: "memory_add",
26
+ description:
27
+ "Store text in the active memory provider (Hindsight bank, mem0 user scope, or Cognee dataset ingest when supported). " +
28
+ "On Hindsight, defaults to async ingest so the MCP returns before LLM extraction finishes (avoids client timeouts).",
29
+ inputSchema: {
30
+ type: "object",
31
+ properties: {
32
+ content: { type: "string", description: "Text to remember" },
33
+ bank: { type: "string", description: "Hindsight bank or mem0 user_id scope (default: openmem-mcp)" },
34
+ metadata: { type: "object", description: "Optional provider-specific metadata" },
35
+ async: {
36
+ type: "boolean",
37
+ description:
38
+ "Hindsight only: queue background ingest (default true). Set false to wait for extraction (slow; may hit MCP timeouts).",
39
+ },
40
+ },
41
+ required: ["content"],
42
+ additionalProperties: false,
43
+ },
44
+ },
45
+ {
46
+ name: "memory_search",
47
+ description: "Semantic or graph search over memories in the active provider.",
48
+ inputSchema: {
49
+ type: "object",
50
+ properties: {
51
+ query: { type: "string" },
52
+ bank: { type: "string", description: "Optional bank / user_id scope" },
53
+ limit: { type: "number", description: "Max results where supported" },
54
+ },
55
+ required: ["query"],
56
+ additionalProperties: false,
57
+ },
58
+ },
59
+ {
60
+ name: "memory_recall",
61
+ description:
62
+ "Retrieve context for a question (Hindsight recall API; mem0/Cognee use search under the hood).",
63
+ inputSchema: {
64
+ type: "object",
65
+ properties: {
66
+ query: { type: "string" },
67
+ bank: { type: "string" },
68
+ limit: { type: "number" },
69
+ },
70
+ required: ["query"],
71
+ additionalProperties: false,
72
+ },
73
+ },
74
+ {
75
+ name: "memory_forget",
76
+ description: "Delete a memory by id (mem0) or drop an entire Hindsight bank when bank is set without memory_id.",
77
+ inputSchema: {
78
+ type: "object",
79
+ properties: {
80
+ memory_id: { type: "string", description: "Provider memory id (mem0)" },
81
+ bank: { type: "string", description: "Hindsight bank to delete, or mem0 user_id scope for bulk delete" },
82
+ },
83
+ additionalProperties: false,
84
+ },
85
+ },
86
+ {
87
+ name: "memory_export",
88
+ description:
89
+ "Export memories as PMM v1 (json/jsonl). Hindsight lists via /memories/list; mem0 GET /memories; Cognee dataset /data.",
90
+ inputSchema: {
91
+ type: "object",
92
+ properties: {
93
+ format: { type: "string", enum: ["json", "jsonl"], description: "Serialization (default json)" },
94
+ bank: {
95
+ type: "string",
96
+ description: "Hindsight bank, mem0 user_id, or indexer bank for Cognee dataset mapping. Omit on Hindsight to export all banks.",
97
+ },
98
+ },
99
+ additionalProperties: false,
100
+ },
101
+ },
102
+ {
103
+ name: "memory_import",
104
+ description: "Import PMM v1 payload into the active provider (best-effort cross-provider).",
105
+ inputSchema: {
106
+ type: "object",
107
+ properties: {
108
+ data: { type: "string", description: "Serialized export payload" },
109
+ merge_strategy: { type: "string", enum: ["skip_duplicates", "overwrite"] },
110
+ },
111
+ required: ["data"],
112
+ additionalProperties: false,
113
+ },
114
+ },
115
+ {
116
+ name: "memory_provider_status",
117
+ description: "Report active MEMORY_PROVIDER and resolved backend URL (no secrets).",
118
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
119
+ },
120
+ {
121
+ name: "memory_reflect",
122
+ description:
123
+ "Synthesize session context for a question. Hindsight uses recall; mem0/Cognee search then optional LiteLLM synthesis (PM_LLM_URL / stack LiteLLM).",
124
+ inputSchema: {
125
+ type: "object",
126
+ properties: {
127
+ query: { type: "string" },
128
+ bank: { type: "string" },
129
+ limit: { type: "number" },
130
+ },
131
+ required: ["query"],
132
+ additionalProperties: false,
133
+ },
134
+ },
135
+ {
136
+ name: "memory_consolidate",
137
+ description: "Provider maintenance: Cognee cognify for a bank/dataset; Hindsight notes LiteLLM route; mem0 skips.",
138
+ inputSchema: {
139
+ type: "object",
140
+ properties: { bank: { type: "string" } },
141
+ additionalProperties: false,
142
+ },
143
+ },
144
+ ];
145
+
146
+ export function textResult(value, isError = false) {
147
+ const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
148
+ return { content: [{ type: "text", text }], isError };
149
+ }
150
+
151
+ /**
152
+ * @param {string} url
153
+ * @param {RequestInit} init
154
+ * @param {typeof fetch} [fetchFn]
155
+ */
156
+ async function httpJson(url, init, fetchFn = fetch) {
157
+ const res = await fetchFn(url, init);
158
+ const text = await res.text();
159
+ let json;
160
+ try {
161
+ json = text ? JSON.parse(text) : null;
162
+ } catch {
163
+ json = text;
164
+ }
165
+ if (!res.ok) {
166
+ const detail = typeof json === "string" ? json : JSON.stringify(json);
167
+ throw new Error(`${init.method || "GET"} ${url} → ${res.status}: ${String(detail).slice(0, 400)}`);
168
+ }
169
+ return json;
170
+ }
171
+
172
+ const DEFAULT_BANK = "openmem-mcp";
173
+
174
+ /** Hindsight ingest: default async so Cursor/Claude MCP clients do not time out on extraction. */
175
+ function hindsightMemoryAddAsync(args) {
176
+ if (args.async === true) return true;
177
+ if (args.async === false) return false;
178
+ const env = String(process.env.PM_MEMORY_ADD_ASYNC || "").trim().toLowerCase();
179
+ if (env === "0" || env === "false" || env === "no") return false;
180
+ if (env === "1" || env === "true" || env === "yes") return true;
181
+ return true;
182
+ }
183
+
184
+ /**
185
+ * @param {ReturnType<import("./context.mjs").resolveMcpContext>} ctx
186
+ * @param {string} name
187
+ * @param {Record<string, unknown>} [args]
188
+ * @param {typeof fetch} [fetchFn]
189
+ */
190
+ export async function callUnifiedTool(ctx, name, args = {}, fetchFn = fetch) {
191
+ if (name === "memory_project_scope") {
192
+ const cwd = args.cwd === undefined ? process.cwd() : args.cwd;
193
+ if (typeof cwd !== "string" || !cwd.trim() || !path.isAbsolute(cwd)) {
194
+ return textResult("cwd must be an absolute workspace path", true);
195
+ }
196
+ const home = process.env.HOME || os.homedir();
197
+ const accessConfig = readAccessConfig(home);
198
+ const indexPath = process.env.HINDSIGHT_INDEX_CONFIG
199
+ ? path.resolve(process.env.HINDSIGHT_INDEX_CONFIG.replace(/^~(?=$|\/)/, home))
200
+ : fs.existsSync(path.join(home, ".config", "hindsight", "index.yml"))
201
+ ? path.join(home, ".config", "hindsight", "index.yml")
202
+ : path.join(PKG_ROOT, "config", "index.yml");
203
+ let indexBanks;
204
+ try {
205
+ const doc = parseDocument(fs.readFileSync(indexPath, "utf8"), { uniqueKeys: true, version: "1.1" });
206
+ if (doc.errors.length) throw doc.errors[0];
207
+ indexBanks = doc.toJS()?.banks;
208
+ } catch (err) {
209
+ return textResult(`Unable to read active index.yml (${indexPath}): ${err.message}`, true);
210
+ }
211
+ const permitted = accessAllowed(cwd, accessConfig);
212
+ return textResult({
213
+ cwd: path.resolve(cwd),
214
+ bank: bankFromCwd(cwd, { home, env: process.env, banks: indexBanks || {}, mapPathToBank: accessConfig?.mapPathToBank }),
215
+ accessPermitted: permitted,
216
+ accessConfigPath: accessConfigPath(home),
217
+ personalMemoryMapping: accessConfig?.mapPathToBank || {},
218
+ indexConfigPath: indexPath,
219
+ indexBanks: indexBanks || {},
220
+ policyConfigValid: accessConfig !== null,
221
+ });
222
+ }
223
+ if (name === "memory_export") {
224
+ const out = await memoryExport(ctx, args, fetchFn);
225
+ return textResult(typeof out === "string" ? out : out);
226
+ }
227
+ if (name === "memory_import") {
228
+ const data = String(args.data || "");
229
+ if (!data.trim()) return textResult("data is required", true);
230
+ const summary = await memoryImport(ctx, data, args, fetchFn, callUnifiedTool);
231
+ return textResult(summary);
232
+ }
233
+ if (name === "memory_provider_status") {
234
+ return textResult({
235
+ provider: ctx.provider,
236
+ hindsightUrl: ctx.hindsightUrl,
237
+ mem0BaseUrl: ctx.mem0?.baseUrl,
238
+ cogneeBaseUrl: ctx.cognee?.baseUrl,
239
+ });
240
+ }
241
+ if (name === "memory_reflect") {
242
+ try {
243
+ const payload = await callMemoryReflect(ctx, args, (n, a, f) => callUnifiedTool(ctx, n, a, f), fetchFn);
244
+ return textResult(payload);
245
+ } catch (err) {
246
+ return textResult(err instanceof Error ? err.message : String(err), true);
247
+ }
248
+ }
249
+ if (name === "memory_consolidate") {
250
+ try {
251
+ return textResult(await callMemoryConsolidate(ctx, args, fetchFn));
252
+ } catch (err) {
253
+ return textResult(err instanceof Error ? err.message : String(err), true);
254
+ }
255
+ }
256
+
257
+ if (ctx.provider === "hindsight") {
258
+ return callHindsight(ctx, name, args, fetchFn);
259
+ }
260
+ if (ctx.provider === "mem0") {
261
+ return callMem0(ctx, name, args, fetchFn);
262
+ }
263
+ if (ctx.provider === "cognee") {
264
+ return callCognee(ctx, name, args, fetchFn);
265
+ }
266
+ return textResult(`unsupported provider "${ctx.provider}"`, true);
267
+ }
268
+
269
+ /**
270
+ * @param {ReturnType<import("./context.mjs").resolveMcpContext>} ctx
271
+ */
272
+ async function callHindsight(ctx, name, args, fetchFn) {
273
+ const base = ctx.hindsightUrl;
274
+ if (!base) return textResult("Hindsight API URL not configured", true);
275
+ const bank = String(args.bank || DEFAULT_BANK).trim() || DEFAULT_BANK;
276
+ const encBank = encodeURIComponent(bank);
277
+
278
+ if (name === "memory_add") {
279
+ const content = String(args.content || "").trim();
280
+ if (!content) return textResult("content is required", true);
281
+ const json = await httpJson(
282
+ `${base}/v1/default/banks/${encBank}/memories`,
283
+ {
284
+ method: "POST",
285
+ headers: { "content-type": "application/json" },
286
+ body: JSON.stringify({ items: [{ content }], async: hindsightMemoryAddAsync(args) }),
287
+ },
288
+ fetchFn,
289
+ );
290
+ return textResult({ provider: "hindsight", bank, result: json });
291
+ }
292
+ if (name === "memory_search" || name === "memory_recall") {
293
+ const query = String(args.query || "").trim();
294
+ if (!query) return textResult("query is required", true);
295
+ const json = await httpJson(
296
+ `${base}/v1/default/banks/${encBank}/memories/recall`,
297
+ {
298
+ method: "POST",
299
+ headers: { "content-type": "application/json" },
300
+ body: JSON.stringify({ query, ...(args.budget ? { budget: args.budget } : {}),
301
+ ...(args.maxTokens ? { max_tokens: args.maxTokens } : {}) }),
302
+ },
303
+ fetchFn,
304
+ );
305
+ return textResult({ provider: "hindsight", bank, query, result: json });
306
+ }
307
+ if (name === "memory_forget") {
308
+ const memoryId = String(args.memory_id || "").trim();
309
+ if (memoryId) {
310
+ return textResult(
311
+ "Hindsight per-memory delete is not wired in this MCP yet; pass bank to delete a test bank or use the Hindsight API/docs.",
312
+ true,
313
+ );
314
+ }
315
+ const targetBank = String(args.bank || "").trim();
316
+ if (!targetBank) return textResult("memory_id or bank is required for Hindsight", true);
317
+ await httpJson(
318
+ `${base}/v1/default/banks/${encodeURIComponent(targetBank)}`,
319
+ { method: "DELETE" },
320
+ fetchFn,
321
+ );
322
+ return textResult({ provider: "hindsight", deleted_bank: targetBank });
323
+ }
324
+ return textResult(`unknown tool: ${name}`, true);
325
+ }
326
+
327
+ async function callMem0(ctx, name, args, fetchFn) {
328
+ const base = ctx.mem0?.baseUrl;
329
+ if (!base) return textResult("mem0 API URL not configured", true);
330
+ const key = ctx.mem0?.apiKey;
331
+ /** @type {Record<string, string>} */
332
+ const headers = { "content-type": "application/json" };
333
+ if (key) headers["X-API-Key"] = key;
334
+ const userId = String(args.bank || DEFAULT_BANK).trim() || DEFAULT_BANK;
335
+
336
+ if (name === "memory_add") {
337
+ const content = String(args.content || "").trim();
338
+ if (!content) return textResult("content is required", true);
339
+ const json = await httpJson(
340
+ `${base}/memories`,
341
+ {
342
+ method: "POST",
343
+ headers,
344
+ body: JSON.stringify({
345
+ messages: [{ role: "user", content }],
346
+ user_id: userId,
347
+ agent_id: "openmem-mcp",
348
+ infer: false,
349
+ }),
350
+ },
351
+ fetchFn,
352
+ );
353
+ return textResult({ provider: "mem0", user_id: userId, result: json });
354
+ }
355
+ if (name === "memory_search" || name === "memory_recall") {
356
+ const query = String(args.query || "").trim();
357
+ if (!query) return textResult("query is required", true);
358
+ const limit = Number(args.limit) > 0 ? Number(args.limit) : 10;
359
+ const json = await httpJson(
360
+ `${base}/search`,
361
+ {
362
+ method: "POST",
363
+ headers,
364
+ body: JSON.stringify({ query, filters: { user_id: userId }, top_k: limit }),
365
+ },
366
+ fetchFn,
367
+ );
368
+ return textResult({ provider: "mem0", user_id: userId, query, result: json });
369
+ }
370
+ if (name === "memory_forget") {
371
+ const memoryId = String(args.memory_id || "").trim();
372
+ if (memoryId) {
373
+ await httpJson(`${base}/memories/${encodeURIComponent(memoryId)}`, { method: "DELETE", headers }, fetchFn);
374
+ return textResult({ provider: "mem0", deleted: memoryId });
375
+ }
376
+ return textResult("mem0 bulk forget requires memory_id (use mem0 API or dashboard for scoped purge)", true);
377
+ }
378
+ return textResult(`unknown tool: ${name}`, true);
379
+ }
380
+
381
+ async function callCognee(ctx, name, args, fetchFn) {
382
+ const base = ctx.cognee?.baseUrl;
383
+ if (!base) return textResult("Cognee API URL not configured", true);
384
+ const env = ctx.cognee?.apiKey ? { COGNEE_API_KEY: ctx.cognee.apiKey } : process.env;
385
+
386
+ if (name === "memory_add") {
387
+ const content = String(args.content || "").trim();
388
+ if (!content) return textResult("content is required", true);
389
+ const dataset = String(args.bank || DEFAULT_BANK).trim() || DEFAULT_BANK;
390
+ // Cognee 1.6.x add is multipart/form-urlencoded: JSON bodies ignore datasetName.
391
+ // Text goes in raw_data; file uploads use the data field (see Cognee OpenAPI).
392
+ const form = new FormData();
393
+ form.set("datasetName", dataset);
394
+ form.set("raw_data", content);
395
+ const headers = {};
396
+ if (ctx.cognee?.apiKey) headers["X-Api-Key"] = ctx.cognee.apiKey;
397
+ const json = await httpJson(`${base}/api/v1/add`, { method: "POST", headers, body: form }, fetchFn);
398
+ return textResult({
399
+ provider: "cognee",
400
+ dataset,
401
+ result: json,
402
+ note: "Run cognify on the dataset for graph search to see new material.",
403
+ });
404
+ }
405
+ if (name === "memory_search" || name === "memory_recall") {
406
+ const query = String(args.query || "").trim();
407
+ if (!query) return textResult("query is required", true);
408
+ const datasets = args.bank ? [String(args.bank)] : undefined;
409
+ const result = await cogneeSearch(base, { query, datasets }, fetchFn, env);
410
+ return textResult({ provider: "cognee", query, result });
411
+ }
412
+ if (name === "memory_forget") {
413
+ return textResult("Cognee delete is not exposed in unified MCP yet; use Cognee API or indexer.", true);
414
+ }
415
+ return textResult(`unknown tool: ${name}`, true);
416
+ }
417
+
418
+ /**
419
+ * @param {Record<string, unknown>} msg
420
+ * @param {ReturnType<import("./context.mjs").resolveMcpContext>} ctx
421
+ * @param {typeof fetch} [fetchFn]
422
+ */
423
+ export async function handleUnifiedMessage(msg, ctx, fetchFn = fetch) {
424
+ const { id, method, params } = msg;
425
+ if (method === "initialize") {
426
+ const protocolVersion =
427
+ typeof params?.protocolVersion === "string" && params.protocolVersion ? params.protocolVersion : "2024-11-05";
428
+ return {
429
+ jsonrpc: "2.0",
430
+ id,
431
+ result: {
432
+ protocolVersion,
433
+ capabilities: { tools: {} },
434
+ serverInfo: { name: "openmem", version: "0.1.0" },
435
+ },
436
+ };
437
+ }
438
+ if (method === "notifications/initialized" || method === "initialized") return null;
439
+ if (method === "tools/list") return { jsonrpc: "2.0", id, result: { tools: UNIFIED_MCP_TOOLS } };
440
+ if (method === "tools/call") {
441
+ try {
442
+ const result = await callUnifiedTool(ctx, String(params?.name || ""), params?.arguments || {}, fetchFn);
443
+ return { jsonrpc: "2.0", id, result };
444
+ } catch (err) {
445
+ return {
446
+ jsonrpc: "2.0",
447
+ id,
448
+ result: textResult(err instanceof Error ? err.message : String(err), true),
449
+ };
450
+ }
451
+ }
452
+ if (method === "ping") return { jsonrpc: "2.0", id, result: {} };
453
+ if (id === undefined) return null;
454
+ return { jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } };
455
+ }
@@ -0,0 +1,120 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { resolveStackRoot } from "../stack.mjs";
4
+ import { cogneeDatasetName } from "./export/cognee-dataset-name.mjs";
5
+ import { resolveLlmApiKey, resolveLlmChatUrl, resolveLlmModel, synthesizeReflectAnswer } from "./llm-synthesize.mjs";
6
+
7
+ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
8
+
9
+ const DEFAULT_BANK = "openmem-mcp";
10
+
11
+ function hitsFromMem0(json) {
12
+ const rows = json?.results || json?.memories || [];
13
+ return rows.map((r) => r.memory ?? r.text ?? r.content ?? JSON.stringify(r)).join("\n---\n");
14
+ }
15
+
16
+ function hitsFromCognee(json) {
17
+ if (typeof json === "string") return json;
18
+ const rows = json?.search_result || json?.results || json?.data || json;
19
+ return typeof rows === "string" ? rows : JSON.stringify(rows, null, 2);
20
+ }
21
+
22
+ /**
23
+ * @param {ReturnType<import("./context.mjs").resolveMcpContext>} ctx
24
+ * @param {Record<string, unknown>} args
25
+ * @param {(name: string, args: Record<string, unknown>, fetchFn: typeof fetch) => Promise<{ content: { text: string }[], isError?: boolean }>} callTool
26
+ * @param {typeof fetch} fetchFn
27
+ */
28
+ export async function callMemoryReflect(ctx, args, callTool, fetchFn = fetch) {
29
+ const query = String(args.query || "").trim();
30
+ if (!query) throw new Error("query is required");
31
+ const bank = String(args.bank || DEFAULT_BANK).trim() || DEFAULT_BANK;
32
+ const limit = Number(args.limit) > 0 ? Number(args.limit) : 12;
33
+
34
+ if (ctx.provider === "hindsight" && ctx.hindsightUrl) {
35
+ const base = ctx.hindsightUrl.replace(/\/+$/, "");
36
+ const enc = encodeURIComponent(bank);
37
+ const res = await fetchFn(`${base}/v1/default/banks/${enc}/reflect`, {
38
+ method: "POST",
39
+ headers: { "content-type": "application/json" },
40
+ body: JSON.stringify({ query, budget: "low", max_tokens: 1024 }),
41
+ });
42
+ const text = await res.text();
43
+ if (!res.ok) throw new Error(`Hindsight reflect → ${res.status}: ${text.slice(0, 300)}`);
44
+ const result = JSON.parse(text);
45
+ return { provider: "hindsight", bank, query, mode: "hindsight_reflect", answer: result.text, result };
46
+ }
47
+
48
+ const searchOut = await callTool("memory_search", { query, bank, limit }, fetchFn);
49
+ const raw = searchOut.content?.[0]?.text || "";
50
+ let parsed;
51
+ try {
52
+ parsed = JSON.parse(raw);
53
+ } catch {
54
+ parsed = { text: raw };
55
+ }
56
+ const hits =
57
+ ctx.provider === "mem0"
58
+ ? hitsFromMem0(parsed?.result ?? parsed)
59
+ : ctx.provider === "cognee"
60
+ ? hitsFromCognee(parsed?.result ?? parsed)
61
+ : raw;
62
+
63
+ const home = process.env.HOME || "";
64
+ const stackRoot = resolveStackRoot({ pkgRoot: PKG_ROOT, home, env: process.env }).root;
65
+ const llmUrl = resolveLlmChatUrl({ home, stackRoot, env: process.env });
66
+ const apiKey = resolveLlmApiKey(process.env);
67
+ try {
68
+ const synthesized = await synthesizeReflectAnswer(
69
+ { query, context: hits || "(no hits)", baseUrl: llmUrl, apiKey, model: resolveLlmModel() },
70
+ fetchFn,
71
+ );
72
+ return {
73
+ provider: ctx.provider,
74
+ bank,
75
+ query,
76
+ mode: "search_synthesize",
77
+ llm: llmUrl,
78
+ answer: synthesized,
79
+ };
80
+ } catch (err) {
81
+ return {
82
+ provider: ctx.provider,
83
+ bank,
84
+ query,
85
+ mode: "search_only",
86
+ note: `LLM synthesis unavailable (${err instanceof Error ? err.message : err}); returning raw hits`,
87
+ hits,
88
+ };
89
+ }
90
+ }
91
+
92
+ /**
93
+ * @param {ReturnType<import("./context.mjs").resolveMcpContext>} ctx
94
+ * @param {Record<string, unknown>} args
95
+ * @param {typeof fetch} fetchFn
96
+ */
97
+ export async function callMemoryConsolidate(ctx, args, fetchFn = fetch) {
98
+ const bank = String(args.bank || DEFAULT_BANK).trim() || DEFAULT_BANK;
99
+ if (ctx.provider === "cognee" && ctx.cognee?.baseUrl) {
100
+ const base = ctx.cognee.baseUrl.replace(/\/+$/, "");
101
+ const dataset = cogneeDatasetName(bank);
102
+ const headers = { "content-type": "application/json", ...(ctx.cognee.apiKey ? { "X-Api-Key": ctx.cognee.apiKey } : {}) };
103
+ const res = await fetchFn(`${base}/api/v1/cognify`, {
104
+ method: "POST",
105
+ headers,
106
+ body: JSON.stringify({ datasets: [dataset], runInBackground: true }),
107
+ });
108
+ const text = await res.text();
109
+ if (!res.ok) throw new Error(`Cognee cognify → ${res.status}: ${text.slice(0, 300)}`);
110
+ return { provider: "cognee", bank, dataset, result: JSON.parse(text) };
111
+ }
112
+ if (ctx.provider === "hindsight" && ctx.hindsightUrl) {
113
+ return {
114
+ provider: "hindsight",
115
+ bank,
116
+ note: "Hindsight consolidation runs via the Hindsight API/LiteLLM memory-consolidate route during retain cycles; no separate MCP trigger yet.",
117
+ };
118
+ }
119
+ return { provider: ctx.provider, bank, skipped: true, reason: "No consolidate equivalent for this provider" };
120
+ }
@@ -0,0 +1,21 @@
1
+ export const PREFIX = "notebooklm::";
2
+
3
+ export function isNotebooklmBank(bankId) {
4
+ return typeof bankId === "string" && bankId.startsWith(PREFIX);
5
+ }
6
+
7
+ export function notebooklmSlug(bankId) {
8
+ return isNotebooklmBank(bankId) ? bankId.slice(PREFIX.length) : null;
9
+ }
10
+
11
+ export function bankAllowed(bankId, allowlist) {
12
+ if (!isNotebooklmBank(bankId)) return false;
13
+ const list = Array.isArray(allowlist) && allowlist.length ? allowlist : ["*"];
14
+ if (list.includes("*")) return true;
15
+ const slug = notebooklmSlug(bankId);
16
+ return list.some((e) => e === bankId || e === slug);
17
+ }
18
+
19
+ export function filterNotebooklmBanks(banks, allowlist) {
20
+ return (banks || []).filter((b) => bankAllowed(b.bank_id || b.id || b.name || "", allowlist));
21
+ }
@@ -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
+ }