@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.
Files changed (230) hide show
  1. package/.agents/skills/openmem/SKILL.md +34 -0
  2. package/LICENSE +190 -0
  3. package/README.md +140 -2
  4. package/bin/hindsight-project-index +530 -0
  5. package/cli/coding-agent/access-policy.mjs +38 -0
  6. package/cli/coding-agent/agents.mjs +34 -0
  7. package/cli/coding-agent/bank-from-cwd.mjs +140 -0
  8. package/cli/coding-agent/capabilities.mjs +34 -0
  9. package/cli/coding-agent/context-store.mjs +81 -0
  10. package/cli/coding-agent/context-trim.mjs +897 -0
  11. package/cli/coding-agent/file-io.mjs +72 -0
  12. package/cli/coding-agent/hook-harnesses.mjs +88 -0
  13. package/cli/coding-agent/hook-runner.mjs +284 -0
  14. package/cli/coding-agent/install-access.mjs +119 -0
  15. package/cli/coding-agent/install-context-proxy.mjs +333 -0
  16. package/cli/coding-agent/install-rtk.mjs +60 -0
  17. package/cli/coding-agent/install-shim.mjs +201 -0
  18. package/cli/coding-agent/opencode-config.mjs +107 -0
  19. package/cli/coding-agent/opencode-plugin.mjs +132 -0
  20. package/cli/coding-agent/opencode2-plugin/index.js +299 -0
  21. package/cli/coding-agent/recall-frame.mjs +29 -0
  22. package/cli/coding-agent/remove-upstream-access.mjs +122 -0
  23. package/cli/coding-agent/runtime.mjs +40 -0
  24. package/cli/coding-agent/session-db.mjs +626 -0
  25. package/cli/coding-agent/session-facade.mjs +86 -0
  26. package/cli/coding-agent/toml-edit.mjs +161 -0
  27. package/cli/cognee-mcp.mjs +260 -0
  28. package/cli/context-management.mjs +254 -0
  29. package/cli/feed-sync.mjs +27 -0
  30. package/cli/install-coding-agent.mjs +298 -0
  31. package/cli/install-memory-mcp.mjs +426 -0
  32. package/cli/is-main.mjs +18 -0
  33. package/cli/mcp/context-sql-runner.mjs +58 -0
  34. package/cli/mcp/context-store.mjs +670 -0
  35. package/cli/mcp/context.mjs +61 -0
  36. package/cli/mcp/export/cognee-dataset-name.mjs +12 -0
  37. package/cli/mcp/export/cognee.mjs +119 -0
  38. package/cli/mcp/export/hindsight.mjs +48 -0
  39. package/cli/mcp/export/index.mjs +26 -0
  40. package/cli/mcp/export/mem0.mjs +31 -0
  41. package/cli/mcp/knowledge.mjs +333 -0
  42. package/cli/mcp/llm-synthesize.mjs +50 -0
  43. package/cli/mcp/mem0-scope-id.mjs +22 -0
  44. package/cli/mcp/memory-facade.mjs +521 -0
  45. package/cli/mcp/memory-reflect-consolidate.mjs +120 -0
  46. package/cli/mcp/notebooklm-banks.mjs +21 -0
  47. package/cli/mcp/notebooklm-hindsight-rest.mjs +45 -0
  48. package/cli/mcp/notebooklm-provider.mjs +225 -0
  49. package/cli/mcp/pmm-records.mjs +23 -0
  50. package/cli/mcp/portability.mjs +158 -0
  51. package/cli/mcp/stdio.mjs +65 -0
  52. package/cli/mcp/unified-mcp-install.mjs +65 -0
  53. package/cli/mem0-mcp.mjs +108 -0
  54. package/cli/notebooklm-mcp.mjs +236 -0
  55. package/cli/openmem-mcp.mjs +38 -0
  56. package/cli/openmem.mjs +222 -0
  57. package/cli/roots.mjs +85 -0
  58. package/cli/schedule.mjs +176 -0
  59. package/cli/stack.mjs +438 -0
  60. package/cli/switch-memory-provider.mjs +358 -0
  61. package/cli/wiring-report.mjs +125 -0
  62. package/compose.bench.yaml +12 -0
  63. package/compose.yaml +227 -0
  64. package/config/coding-agent.json +13 -0
  65. package/config/hindsight.env.example +26 -0
  66. package/config/index.yml +54 -0
  67. package/config/litellm.lemond.yaml +40 -0
  68. package/config/litellm.ollama.yaml +33 -0
  69. package/config/notebooklm.yml +33 -0
  70. package/config/openmem.toml.example +124 -0
  71. package/docs/OPENMEM.md +23 -0
  72. package/docs/architecture.md +245 -0
  73. package/docs/assets/README.md +10 -0
  74. package/docs/assets/setup-demo.gif +0 -0
  75. package/docs/coding-agent-hooks.md +75 -0
  76. package/docs/coding-agent-scopes.md +22 -0
  77. package/docs/coding-agents.md +71 -0
  78. package/docs/cognee.md +37 -0
  79. package/docs/compatibility.md +80 -0
  80. package/docs/concepts.md +135 -0
  81. package/docs/context-management.md +255 -0
  82. package/docs/context-proxy.md +115 -0
  83. package/docs/context-research.md +215 -0
  84. package/docs/context-store.md +277 -0
  85. package/docs/hindsight.md +77 -0
  86. package/docs/index-sources-roadmap.md +38 -0
  87. package/docs/index-sources.md +39 -0
  88. package/docs/indexer-sinks.md +122 -0
  89. package/docs/indexing.md +57 -0
  90. package/docs/manual-stack-setup.md +74 -0
  91. package/docs/mcp-install-guide.md +4 -0
  92. package/docs/mcp-setup.md +104 -0
  93. package/docs/mem0.md +141 -0
  94. package/docs/memory-portability.md +73 -0
  95. package/docs/memory-providers.md +246 -0
  96. package/docs/meridian-litellm.md +198 -0
  97. package/docs/model-configuration.md +106 -0
  98. package/docs/notebooklm.md +83 -0
  99. package/docs/npx-install.md +172 -0
  100. package/docs/openmem-config.md +194 -0
  101. package/docs/provider-wizard-hooks.md +222 -0
  102. package/docs/quickstart.md +107 -0
  103. package/docs/setup-wizard.md +117 -0
  104. package/docs/sources-feeds.md +39 -0
  105. package/docs/sources-readwise.md +23 -0
  106. package/docs/sources-zotero.md +22 -0
  107. package/glama.json +4 -0
  108. package/indexer/Dockerfile +38 -0
  109. package/indexer/code_outline.py +146 -0
  110. package/indexer/feed_sync.py +107 -0
  111. package/indexer/index_config.py +290 -0
  112. package/indexer/pytest.ini +5 -0
  113. package/indexer/sinks/__init__.py +148 -0
  114. package/indexer/sinks/_http.py +44 -0
  115. package/indexer/sinks/cognee.py +329 -0
  116. package/indexer/sinks/hindsight.py +107 -0
  117. package/indexer/sinks/mem0.py +247 -0
  118. package/indexer/watch.py +240 -0
  119. package/package.json +96 -4
  120. package/providers/_template/compose.yaml +52 -0
  121. package/providers/cognee/OPERATOR.md +82 -0
  122. package/providers/cognee/compose.yaml +89 -0
  123. package/providers/cognee/lib/clack.mjs +157 -0
  124. package/providers/cognee/lib/coding-agent.mjs +26 -0
  125. package/providers/cognee/lib/cognee-mcp-install.mjs +153 -0
  126. package/providers/cognee/lib/config.mjs +98 -0
  127. package/providers/cognee/lib/env-file.mjs +194 -0
  128. package/providers/cognee/lib/env-setup.mjs +83 -0
  129. package/providers/cognee/lib/llm-env.mjs +382 -0
  130. package/providers/cognee/lib/ports.mjs +25 -0
  131. package/providers/cognee/lib/smoke.mjs +246 -0
  132. package/providers/cognee/lib/stack-env.mjs +134 -0
  133. package/providers/cognee/lib/state.mjs +101 -0
  134. package/providers/cognee/lib/summary.mjs +56 -0
  135. package/providers/cognee/lib/validate.mjs +97 -0
  136. package/providers/cognee/manifest.json +56 -0
  137. package/providers/cognee/wizard.mjs +134 -0
  138. package/providers/graphiti/manifest.json +15 -0
  139. package/providers/hindsight/lib/clack.mjs +347 -0
  140. package/providers/hindsight/lib/coding-agent.mjs +204 -0
  141. package/providers/hindsight/lib/config.mjs +79 -0
  142. package/providers/hindsight/lib/env-setup.mjs +72 -0
  143. package/providers/hindsight/lib/litellm-routes.mjs +196 -0
  144. package/providers/hindsight/lib/meridian.mjs +307 -0
  145. package/providers/hindsight/lib/model-backend.mjs +243 -0
  146. package/providers/hindsight/lib/notebooklm-mcp-install.mjs +127 -0
  147. package/providers/hindsight/lib/ollama-compose.mjs +51 -0
  148. package/providers/hindsight/lib/ports.mjs +30 -0
  149. package/providers/hindsight/lib/probe.mjs +54 -0
  150. package/providers/hindsight/lib/stack-env.mjs +125 -0
  151. package/providers/hindsight/lib/state.mjs +80 -0
  152. package/providers/hindsight/lib/summary.mjs +57 -0
  153. package/providers/hindsight/lib/validate.mjs +81 -0
  154. package/providers/hindsight/manifest.json +58 -0
  155. package/providers/hindsight/wizard.mjs +150 -0
  156. package/providers/letta/manifest.json +29 -0
  157. package/providers/mem0/OPERATOR.md +143 -0
  158. package/providers/mem0/compose.yaml +79 -0
  159. package/providers/mem0/lib/coding-agent.mjs +58 -0
  160. package/providers/mem0/lib/config.mjs +45 -0
  161. package/providers/mem0/lib/configure.mjs +201 -0
  162. package/providers/mem0/lib/dotenv.mjs +71 -0
  163. package/providers/mem0/lib/env-file.mjs +110 -0
  164. package/providers/mem0/lib/http.mjs +69 -0
  165. package/providers/mem0/lib/litellm.mjs +457 -0
  166. package/providers/mem0/lib/mcp-install.mjs +165 -0
  167. package/providers/mem0/lib/secret.mjs +30 -0
  168. package/providers/mem0/lib/smoke.mjs +179 -0
  169. package/providers/mem0/lib/stack-env.mjs +255 -0
  170. package/providers/mem0/lib/state.mjs +67 -0
  171. package/providers/mem0/lib/summary.mjs +88 -0
  172. package/providers/mem0/lib/url.mjs +106 -0
  173. package/providers/mem0/lib/validate.mjs +158 -0
  174. package/providers/mem0/manifest.json +47 -0
  175. package/providers/mem0/wizard.mjs +146 -0
  176. package/providers/zep/manifest.json +29 -0
  177. package/proxy/config.yaml +33 -0
  178. package/proxy/openmem_context/__init__.py +6 -0
  179. package/proxy/openmem_context/adapters.py +504 -0
  180. package/proxy/openmem_context/compress.py +53 -0
  181. package/proxy/openmem_context/core.py +606 -0
  182. package/proxy/openmem_context/embed.py +212 -0
  183. package/proxy/openmem_context/recall.py +49 -0
  184. package/proxy/openmem_context/session.py +70 -0
  185. package/proxy/openmem_context/sessiondb.py +276 -0
  186. package/proxy/openmem_context/settings.py +134 -0
  187. package/proxy/openmem_context/store.py +129 -0
  188. package/proxy/openmem_context/tokens.py +123 -0
  189. package/proxy/openmem_context/ttl.py +78 -0
  190. package/proxy/openmem_context_hook.py +34 -0
  191. package/scripts/check-pack.mjs +219 -0
  192. package/scripts/gen-masks.mjs +83 -0
  193. package/scripts/gen-masks.sh +4 -0
  194. package/scripts/promotion-provider-smoke.mjs +34 -0
  195. package/scripts/run-setup.sh +30 -0
  196. package/scripts/setup.mjs +107 -0
  197. package/scripts/setup.sh +5 -0
  198. package/scripts/spin-up.sh +45 -0
  199. package/scripts/wizard.sh +5 -0
  200. package/server.json +34 -0
  201. package/sync/Dockerfile +27 -0
  202. package/sync/memory_backend.py +87 -0
  203. package/sync/notebooklm_sync.py +664 -0
  204. package/wizard/bin/pm.mjs +97 -0
  205. package/wizard/bin/setup.mjs +33 -0
  206. package/wizard/lib/compose-version.mjs +32 -0
  207. package/wizard/lib/compose.mjs +70 -0
  208. package/wizard/lib/context-step.mjs +140 -0
  209. package/wizard/lib/curl.mjs +103 -0
  210. package/wizard/lib/docker-build.mjs +148 -0
  211. package/wizard/lib/exec.mjs +41 -0
  212. package/wizard/lib/indexer.mjs +6 -0
  213. package/wizard/lib/litellm-env.mjs +55 -0
  214. package/wizard/lib/load-machine-env.mjs +4 -0
  215. package/wizard/lib/log.mjs +32 -0
  216. package/wizard/lib/notebooklm-agent-access.mjs +92 -0
  217. package/wizard/lib/notebooklm.mjs +214 -0
  218. package/wizard/lib/parse-args.mjs +87 -0
  219. package/wizard/lib/paths.mjs +39 -0
  220. package/wizard/lib/pm-config.mjs +371 -0
  221. package/wizard/lib/ports.mjs +94 -0
  222. package/wizard/lib/prompts.mjs +85 -0
  223. package/wizard/lib/provider-wizard/helpers.mjs +98 -0
  224. package/wizard/lib/provider-wizard/run-steps.mjs +129 -0
  225. package/wizard/lib/providers/registry.mjs +262 -0
  226. package/wizard/lib/run-clack.mjs +108 -0
  227. package/wizard/lib/run-wizard.mjs +750 -0
  228. package/wizard/lib/validate-setup.mjs +62 -0
  229. package/wizard/lib/wizard-action.mjs +34 -0
  230. package/wizard/lib/wizard-conf.mjs +71 -0
@@ -0,0 +1,457 @@
1
+ /**
2
+ * Optional: route mem0's LLM (and, when the backend has one, its embedder) through the stack
3
+ * LiteLLM, the gateway Hindsight already uses. The wizard writes the LiteLLM route file, keeps
4
+ * provider keys in litellm.env (mode 600), turns on the compose profile "litellm" (published on
5
+ * 127.0.0.1 only by providers/mem0/compose.yaml) and prints the mem0 config blocks. It never
6
+ * edits the operator's mem0 server config.
7
+ *
8
+ * mem0 side (verified against mem0 main @ 26cdefb): the self-hosted REST server bundles only
9
+ * the openai, anthropic and gemini LLM providers and the openai and gemini embedders
10
+ * (server/main.py BUNDLED_*_PROVIDERS), so the snippet uses provider "openai" with
11
+ * openai_base_url pointing at LiteLLM, not provider "litellm". See docs/mem0.md.
12
+ */
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import { setLitellmEnvValue } from "../../../wizard/lib/litellm-env.mjs";
16
+ import {
17
+ bridgeReport,
18
+ containerApiBase,
19
+ dockerBridgeIp,
20
+ existingMeridianKey,
21
+ MERIDIAN_DEFAULT_BASE,
22
+ MERIDIAN_GUIDE,
23
+ MERIDIAN_NO_KEY,
24
+ MERIDIAN_RETAIN_DEFAULT,
25
+ MERIDIAN_TERMS_URL,
26
+ normalizeMeridianBase,
27
+ probeMeridian,
28
+ } from "../../hindsight/lib/meridian.mjs";
29
+
30
+ /** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
31
+
32
+ /** LiteLLM model_name aliases mem0 asks for (Hindsight uses memory-retain / reflect / consolidate). */
33
+ export const LLM_ALIAS = "memory-llm";
34
+ export const EMBED_ALIAS = "memory-embed";
35
+ export const DEFAULT_PORT = 4000;
36
+ export const BACKENDS = ["ollama", "custom", "anthropic", "meridian"];
37
+ /** Backends with an embedding model LiteLLM can serve. Claude (API or Meridian) has none. */
38
+ export const EMBED_BACKENDS = ["ollama", "custom"];
39
+
40
+ const OLLAMA_API_BASE = "http://host.docker.internal:11434";
41
+ const CUSTOM_API_BASE = "http://host.docker.internal:8080/v1";
42
+ const OLLAMA_DEFAULT_MODEL = "gemma4:e2b";
43
+ const OLLAMA_EMBED_DEFAULT = "nomic-embed-text";
44
+ /** Known output sizes, for vector_store.config.embedding_model_dims in the snippet. */
45
+ const EMBED_DIMS = { "nomic-embed-text": 768, "text-embedding-3-small": 1536, "text-embedding-3-large": 3072 };
46
+
47
+ /** wizard.conf keys this option owns. */
48
+ export const CONF_KEYS = [
49
+ "MEM0_LITELLM",
50
+ "MEM0_LITELLM_BACKEND",
51
+ "MEM0_LITELLM_MODEL",
52
+ "MEM0_LITELLM_API_BASE",
53
+ "MEM0_LITELLM_EMBED_MODEL",
54
+ "MEM0_LITELLM_PORT",
55
+ ];
56
+
57
+ /** The setup UI's masked ANTHROPIC_API_KEY answer, taken out of process.env by captureAnthropicKey. */
58
+ let capturedAnthropicKey = "";
59
+
60
+ /**
61
+ * Move PM_ANTHROPIC_API_KEY (the setup UI handoff) out of process.env so no child process
62
+ * (docker, curl) inherits it. Called from the init hook and again before the key is written.
63
+ */
64
+ export function captureAnthropicKey() {
65
+ const v = process.env.PM_ANTHROPIC_API_KEY;
66
+ if (v) capturedAnthropicKey = v.trim();
67
+ delete process.env.PM_ANTHROPIC_API_KEY;
68
+ }
69
+
70
+ /** @param {string} home */
71
+ export function routesFile(home) {
72
+ return path.join(home || "", ".config/personal-memory/mem0-litellm.yaml");
73
+ }
74
+
75
+ /** litellm.env: LITELLM_ENV_FILE, else Hindsight's default (one master key for the stack LiteLLM). */
76
+ export function litellmEnvFile(home) {
77
+ return process.env.LITELLM_ENV_FILE || path.join(home || "", ".config/hindsight/litellm.env");
78
+ }
79
+
80
+ /** @param {string} value */
81
+ export function portProblem(value) {
82
+ if (!/^\d+$/.test(String(value))) return "MEM0_LITELLM_PORT must be a port number";
83
+ const n = Number(value);
84
+ if (n < 1 || n > 65535) return "MEM0_LITELLM_PORT must be between 1 and 65535";
85
+ return "";
86
+ }
87
+
88
+ /** A value written into the YAML route file: no spaces, quotes or YAML syntax. */
89
+ function modelProblem(label, value) {
90
+ if (!value) return `${label} is empty`;
91
+ if (!/^[A-Za-z0-9._:/@+-]+$/.test(value)) return `${label} has characters a model id cannot have: ${value}`;
92
+ return "";
93
+ }
94
+
95
+ /** @param {string} value */
96
+ function apiBaseProblem(value) {
97
+ try {
98
+ const u = new URL(value);
99
+ if (u.protocol !== "http:" && u.protocol !== "https:") return "the API base must be http(s)";
100
+ if (u.username || u.password || u.search || u.hash) return "the API base must not hold credentials, a query or a fragment";
101
+ } catch {
102
+ return `the API base is not a URL: ${value}`;
103
+ }
104
+ return "";
105
+ }
106
+
107
+ /**
108
+ * Validate a choice. Returns the first problem or "".
109
+ * @param {{ backend: string, model: string, apiBase: string, embedModel: string, port: string|number }} c
110
+ */
111
+ export function choiceProblem(c) {
112
+ if (!BACKENDS.includes(c.backend)) return `MEM0_LITELLM_BACKEND must be one of ${BACKENDS.join(", ")} (got: ${c.backend})`;
113
+ const m = modelProblem("the model", c.model);
114
+ if (m) return m;
115
+ if (c.backend !== "anthropic") {
116
+ const a = apiBaseProblem(c.apiBase);
117
+ if (a) return a;
118
+ }
119
+ if (c.embedModel) {
120
+ if (!EMBED_BACKENDS.includes(c.backend)) return `the ${c.backend} backend has no embedding model; keep mem0's own embedder`;
121
+ const e = modelProblem("the embedding model", c.embedModel);
122
+ if (e) return e;
123
+ }
124
+ return portProblem(c.port);
125
+ }
126
+
127
+ /** One route entry. */
128
+ function route(alias, model, extra) {
129
+ return ` - model_name: ${alias}\n litellm_params:\n model: ${model}\n${extra}`;
130
+ }
131
+
132
+ /**
133
+ * The LiteLLM route file for mem0. Keys are referenced as os.environ/<NAME> and live in litellm.env.
134
+ * @param {{ backend: string, model: string, apiBase: string, embedModel: string }} c
135
+ * @returns {string}
136
+ */
137
+ export function renderRoutes(c) {
138
+ const head =
139
+ "# Written by ./setup (mem0, \"route mem0's LLM through the stack LiteLLM\"). Rerun ./setup to change it.\n" +
140
+ `# mem0 calls ${LLM_ALIAS}${c.embedModel ? ` and ${EMBED_ALIAS}` : ""}; keys live in litellm.env, never here.\n`;
141
+ let out = `${head}model_list:\n`;
142
+ switch (c.backend) {
143
+ case "ollama":
144
+ out += route(LLM_ALIAS, `ollama_chat/${c.model}`, ` api_base: ${c.apiBase}\n max_tokens: 4096\n timeout: 300\n`);
145
+ if (c.embedModel) out += route(EMBED_ALIAS, `ollama/${c.embedModel}`, ` api_base: ${c.apiBase}\n`);
146
+ break;
147
+ case "custom":
148
+ // Same key variable as Hindsight's custom route; set it in litellm.env when the server needs one.
149
+ out += route(
150
+ LLM_ALIAS,
151
+ `openai/${c.model}`,
152
+ ` api_base: ${c.apiBase}\n api_key: os.environ/LEMOND_API_KEY\n max_tokens: 4096\n timeout: 300\n`,
153
+ );
154
+ if (c.embedModel) {
155
+ out += route(EMBED_ALIAS, `openai/${c.embedModel}`, ` api_base: ${c.apiBase}\n api_key: os.environ/LEMOND_API_KEY\n`);
156
+ }
157
+ break;
158
+ case "anthropic":
159
+ out += route(LLM_ALIAS, `anthropic/${c.model}`, " api_key: os.environ/ANTHROPIC_API_KEY\n max_tokens: 4096\n timeout: 300\n");
160
+ break;
161
+ case "meridian":
162
+ out += route(
163
+ LLM_ALIAS,
164
+ `anthropic/${c.model}`,
165
+ ` api_base: ${c.apiBase}\n api_key: os.environ/MERIDIAN_API_KEY\n max_tokens: 4096\n timeout: 300\n` +
166
+ ' additional_drop_params: ["reasoning_effort"]\n',
167
+ );
168
+ break;
169
+ default:
170
+ throw new Error(`unknown backend ${c.backend}`);
171
+ }
172
+ return `${out}\ngeneral_settings:\n master_key: os.environ/LITELLM_MASTER_KEY\n`;
173
+ }
174
+
175
+ /**
176
+ * The mem0 config blocks that point mem0 at the stack LiteLLM. The key is a placeholder: the
177
+ * operator copies LITELLM_MASTER_KEY from litellm.env.
178
+ * @param {{ port: string|number, embedModel: string }} c
179
+ * @returns {Record<string, any>}
180
+ */
181
+ export function mem0Config(c) {
182
+ const base = `http://127.0.0.1:${c.port}/v1`;
183
+ const key = "<LITELLM_MASTER_KEY from litellm.env>";
184
+ /** @type {Record<string, any>} */
185
+ const cfg = { llm: { provider: "openai", config: { model: LLM_ALIAS, openai_base_url: base, api_key: key } } };
186
+ if (c.embedModel) {
187
+ cfg.embedder = { provider: "openai", config: { model: EMBED_ALIAS, openai_base_url: base, api_key: key } };
188
+ const dims = EMBED_DIMS[c.embedModel.replace(/:latest$/, "")];
189
+ cfg.vector_store = { config: { embedding_model_dims: dims || "<your embedding model's output size>" } };
190
+ }
191
+ return cfg;
192
+ }
193
+
194
+ /**
195
+ * Lines that print the snippet and how to apply it.
196
+ * @param {{ port: string|number, embedModel: string, backend: string }} c
197
+ * @param {string} envFile litellm.env
198
+ * @returns {string[]}
199
+ */
200
+ export function snippetLines(c, envFile) {
201
+ const json = JSON.stringify(mem0Config(c), null, 2);
202
+ const lines = [
203
+ "mem0 config for the stack LiteLLM (this wizard does not edit your mem0 server config):",
204
+ ...json.split("\n").map((l) => ` ${l}`),
205
+ ` api_key: the LITELLM_MASTER_KEY value in ${envFile} (not printed here).`,
206
+ " Self-hosted REST server: POST this JSON to /configure with an admin key (it deep-merges and persists).",
207
+ " Python SDK: merge the blocks into the dict you pass to Memory.from_config.",
208
+ ` The URL is loopback: it works for a mem0 server running on this host, not one inside another container.`,
209
+ ];
210
+ if (c.embedModel) {
211
+ lines.push(
212
+ " A different embedder changes the vector size: point vector_store at a new collection (or re-add the memories).",
213
+ );
214
+ } else {
215
+ lines.push(` Embeddings stay on mem0's own embedder: the ${c.backend} backend has no embedding model here.`);
216
+ }
217
+ lines.push(" mem0's openai LLM ignores openai_base_url when OPENROUTER_API_KEY is set in the server's environment.");
218
+ return lines;
219
+ }
220
+
221
+ /**
222
+ * The option's current answers: environment (--yes, setup UI handoff), then wizard.conf, then defaults.
223
+ * Model, API base and embedding model come from wizard.conf only when it was saved for the same backend.
224
+ * @param {Record<string, any>} ctx
225
+ * @param {Helpers} h
226
+ * @param {string} [backendOverride] the backend picked interactively
227
+ */
228
+ export function storedChoice(ctx, h, backendOverride = "") {
229
+ const conf = (k) => (ctx.wizardConf ? h.envValue(ctx.wizardConf, k) : "");
230
+ const stack = ctx.repoRoot ? path.join(ctx.repoRoot, ".env") : "";
231
+ const pick = (k, fallback = "") => String(process.env[k] ?? "").trim() || conf(k) || fallback;
232
+ const enabled = (process.env.MEM0_LITELLM ?? conf("MEM0_LITELLM")) === "1";
233
+ const backend = backendOverride || pick("MEM0_LITELLM_BACKEND", "ollama");
234
+ const sameBackend = conf("MEM0_LITELLM_BACKEND") === backend;
235
+ const fromConf = (k) => String(process.env[k] ?? "").trim() || (sameBackend ? conf(k) : "");
236
+ let apiBase = fromConf("MEM0_LITELLM_API_BASE");
237
+ let model = fromConf("MEM0_LITELLM_MODEL");
238
+ let embed = process.env.MEM0_LITELLM_EMBED_MODEL ?? (sameBackend ? conf("MEM0_LITELLM_EMBED_MODEL") : undefined);
239
+ if (backend === "ollama") {
240
+ apiBase ||= OLLAMA_API_BASE;
241
+ model ||= OLLAMA_DEFAULT_MODEL;
242
+ if (embed === undefined) embed = OLLAMA_EMBED_DEFAULT;
243
+ } else if (backend === "custom") {
244
+ apiBase ||= CUSTOM_API_BASE;
245
+ } else if (backend === "anthropic") {
246
+ apiBase = "";
247
+ model ||= MERIDIAN_RETAIN_DEFAULT;
248
+ } else if (backend === "meridian") {
249
+ model ||= MERIDIAN_RETAIN_DEFAULT;
250
+ }
251
+ embed = String(embed ?? "").trim();
252
+ if (embed === "none") embed = "";
253
+ const port =
254
+ String(process.env.MEM0_LITELLM_PORT ?? "").trim() ||
255
+ (stack && h.envValue(stack, "MEM0_LITELLM_PORT")) ||
256
+ conf("MEM0_LITELLM_PORT") ||
257
+ String(DEFAULT_PORT);
258
+ return { enabled, backend, model, apiBase, embedModel: embed, port };
259
+ }
260
+
261
+ /**
262
+ * Meridian: probe it the way Hindsight's wizard does, keep MERIDIAN_API_KEY in litellm.env.
263
+ * @param {Record<string, any>} ctx
264
+ * @param {Helpers} h
265
+ * @param {Record<string, any>} c
266
+ * @param {boolean} interactive
267
+ */
268
+ async function chooseMeridian(ctx, h, c, interactive) {
269
+ h.warn(`Claude subscription use through Meridian is governed by Anthropic's Consumer Terms: ${MERIDIAN_TERMS_URL}`);
270
+ h.info(`The wizard does not install or start Meridian; see ${MERIDIAN_GUIDE}.`);
271
+ const mctx = {
272
+ meridianEnvFile: path.join(ctx.home || "", ".config/meridian/meridian.env"),
273
+ litellmEnv: litellmEnvFile(ctx.home),
274
+ dockerBridgeIp: dockerBridgeIp(),
275
+ };
276
+ let hostBase = process.env.MERIDIAN_BASE || MERIDIAN_DEFAULT_BASE;
277
+ if (interactive) hostBase = await h.ask(ctx, "Meridian URL from this host", hostBase);
278
+ hostBase = normalizeMeridianBase(hostBase);
279
+ try {
280
+ // A saved or MEM0_LITELLM_API_BASE value wins under --yes; a typed or MERIDIAN_BASE URL replaces it.
281
+ if (interactive || process.env.MERIDIAN_BASE || !c.apiBase) c.apiBase = containerApiBase(hostBase);
282
+ } catch {
283
+ h.problem(ctx, `not a URL: ${hostBase}`);
284
+ return false;
285
+ }
286
+ const existing = existingMeridianKey(mctx, h);
287
+ c.meridianKey = existing.key;
288
+ if (ctx.dry) {
289
+ h.info(`[dry-run] probe Meridian at ${hostBase}`);
290
+ return true;
291
+ }
292
+ const probe = await probeMeridian(hostBase, existing.key, mctx.dockerBridgeIp);
293
+ if (!probe.answered) h.warn(`Meridian is not answering on ${hostBase}; start it first (${MERIDIAN_GUIDE}).`);
294
+ else if (probe.ids.length && !probe.ids.includes(c.model)) h.warn(`${c.model} is not listed by Meridian's /v1/models`);
295
+ const bridge = bridgeReport({ ...mctx, meridianBase: hostBase, apiBase: c.apiBase }, h.envValue, probe);
296
+ if (bridge) h[bridge.level](bridge.text);
297
+ return true;
298
+ }
299
+
300
+ /**
301
+ * Write litellm.env keys the backend needs (never printed): ANTHROPIC_API_KEY from the
302
+ * environment, MERIDIAN_API_KEY from the environment or Meridian's env file.
303
+ * @param {Record<string, any>} ctx
304
+ * @param {Helpers} h
305
+ * @param {Record<string, any>} c
306
+ */
307
+ function writeBackendKeys(ctx, h, c) {
308
+ const envFile = litellmEnvFile(ctx.home);
309
+ if (c.backend === "anthropic") {
310
+ captureAnthropicKey();
311
+ const key = String(capturedAnthropicKey || process.env.ANTHROPIC_API_KEY || "").trim();
312
+ if (ctx.dry) {
313
+ h.info(`[dry-run] ${key ? `set ANTHROPIC_API_KEY in ${envFile} (mode 600; value not printed)` : `check ANTHROPIC_API_KEY in ${envFile}`}`);
314
+ return;
315
+ }
316
+ if (key) {
317
+ const r = setLitellmEnvValue(envFile, "ANTHROPIC_API_KEY", key);
318
+ h.ok(`${r === "unchanged" ? "kept" : "set"} ANTHROPIC_API_KEY in ${envFile} (mode 600)`);
319
+ } else if (!h.envValue(envFile, "ANTHROPIC_API_KEY")) {
320
+ h.warn(`put ANTHROPIC_API_KEY=... in ${envFile} (mode 600), or rerun with ANTHROPIC_API_KEY in the environment`);
321
+ } else h.ok(`kept ANTHROPIC_API_KEY in ${envFile}`);
322
+ }
323
+ if (c.backend === "meridian") {
324
+ if (ctx.dry) {
325
+ h.info(`[dry-run] set MERIDIAN_API_KEY in ${envFile} (mode 600; value not printed)`);
326
+ return;
327
+ }
328
+ const value = c.meridianKey || h.envValue(envFile, "MERIDIAN_API_KEY") || MERIDIAN_NO_KEY;
329
+ setLitellmEnvValue(envFile, "MERIDIAN_API_KEY", value);
330
+ h.ok(`set MERIDIAN_API_KEY in ${envFile} (mode 600)`);
331
+ if (value === MERIDIAN_NO_KEY) h.warn("No MERIDIAN_API_KEY: anyone who can reach Meridian can use your subscription.");
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Write the route file (mode 644; it holds no secrets). Keeps a .bak of a different file.
337
+ * @param {Record<string, any>} ctx
338
+ * @param {Helpers} h
339
+ * @param {string} content
340
+ */
341
+ function writeRoutes(ctx, h, content) {
342
+ const file = routesFile(ctx.home);
343
+ if (ctx.dry) {
344
+ h.info(`[dry-run] write ${file}: ${LLM_ALIAS}${content.includes(EMBED_ALIAS) ? ` and ${EMBED_ALIAS}` : ""}`);
345
+ return;
346
+ }
347
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
348
+ if (fs.existsSync(file)) {
349
+ if (fs.readFileSync(file, "utf8") === content) {
350
+ h.ok(`${file} already has these routes`);
351
+ return;
352
+ }
353
+ fs.copyFileSync(file, `${file}.bak`);
354
+ }
355
+ const tmp = `${file}.${process.pid}.tmp`;
356
+ fs.writeFileSync(tmp, content, { mode: 0o644 });
357
+ fs.renameSync(tmp, file);
358
+ h.ok(`wrote ${file}`);
359
+ }
360
+
361
+ /**
362
+ * The option itself, run after the URL and key step. Fills ctx.mem0.litellm (null when off).
363
+ * @param {Record<string, any>} ctx
364
+ * @param {Helpers} h
365
+ * @returns {Promise<Record<string, string>|null>} wizard.conf keys to save, or null when off
366
+ */
367
+ export async function configureLitellm(ctx, h) {
368
+ const state = (ctx.mem0 ||= {});
369
+ const interactive = !ctx.yes && !ctx.skipBackendStep;
370
+ const c = storedChoice(ctx, h);
371
+ let enabled = c.enabled;
372
+ if (interactive) {
373
+ enabled = await h.confirm(ctx, "Route mem0's LLM through the stack LiteLLM (same gateway as Hindsight)?", c.enabled ? "y" : "n");
374
+ }
375
+ if (!enabled) {
376
+ state.litellm = null;
377
+ if (c.enabled) h.info("stack LiteLLM for mem0: off (the litellm profile is removed on this run)");
378
+ return null;
379
+ }
380
+ h.info("mem0's LLM goes through the stack LiteLLM (compose profile litellm, 127.0.0.1 only).");
381
+
382
+ if (interactive) {
383
+ h.info("1) Ollama 2) custom OpenAI-compatible URL 3) Anthropic API key 4) Claude subscription (Meridian)");
384
+ const defNum = String(BACKENDS.indexOf(c.backend) + 1 || 1);
385
+ const pick = await h.ask(ctx, "LiteLLM backend for mem0 (1-4)", defNum);
386
+ const backend = BACKENDS[Number(pick) - 1];
387
+ if (!backend) {
388
+ h.problem(ctx, "choose 1, 2, 3 or 4");
389
+ return null;
390
+ }
391
+ if (backend !== c.backend) Object.assign(c, storedChoice(ctx, h, backend), { enabled: true });
392
+ if (backend === "ollama" || backend === "custom") {
393
+ h.info("The litellm container calls this URL; host services are host.docker.internal.");
394
+ c.apiBase = String(await h.ask(ctx, "Model server base URL (as the container sees it)", c.apiBase)).trim();
395
+ }
396
+ c.model = String(await h.ask(ctx, "Model id for mem0's LLM", c.model)).trim();
397
+ if (EMBED_BACKENDS.includes(backend)) {
398
+ const e = String(await h.ask(ctx, "Embedding model through LiteLLM (none keeps mem0's own embedder)", c.embedModel || "none")).trim();
399
+ c.embedModel = e === "none" ? "" : e;
400
+ }
401
+ }
402
+ if (!EMBED_BACKENDS.includes(c.backend) && c.embedModel) {
403
+ h.info(`the ${c.backend} backend has no embedding model; mem0 keeps its own embedder`);
404
+ c.embedModel = "";
405
+ }
406
+ if (c.backend === "meridian" && !(await chooseMeridian(ctx, h, c, interactive))) return null;
407
+
408
+ const issue = choiceProblem(c);
409
+ if (issue) {
410
+ h.problem(ctx, `stack LiteLLM for mem0: ${issue}`);
411
+ return null;
412
+ }
413
+ // Loopback port for the mem0 server: kept when free or held by this project, else the next free one.
414
+ if (ctx.dry) {
415
+ const free = await h.portFree(Number(c.port));
416
+ h.info(`[dry-run] publish LiteLLM on 127.0.0.1:${c.port} (${free ? "free now" : "busy now: a real run offers the next free port"})`);
417
+ } else {
418
+ c.port = String(await h.choosePort(ctx, "LiteLLM (mem0)", Number(c.port), { envVars: ["MEM0_LITELLM_PORT"] }));
419
+ }
420
+ process.env.MEM0_LITELLM_PORT = c.port;
421
+
422
+ writeRoutes(ctx, h, renderRoutes(c));
423
+ writeBackendKeys(ctx, h, c);
424
+ state.litellm = { backend: c.backend, model: c.model, apiBase: c.apiBase, embedModel: c.embedModel, port: c.port };
425
+ for (const line of snippetLines(c, litellmEnvFile(ctx.home))) h.info(line);
426
+ return {
427
+ MEM0_LITELLM: "1",
428
+ MEM0_LITELLM_BACKEND: c.backend,
429
+ MEM0_LITELLM_MODEL: c.model,
430
+ MEM0_LITELLM_API_BASE: c.apiBase,
431
+ MEM0_LITELLM_EMBED_MODEL: c.embedModel,
432
+ MEM0_LITELLM_PORT: c.port,
433
+ };
434
+ }
435
+
436
+ /**
437
+ * Environment for scripts/setup.mjs when the option is on.
438
+ * @param {Record<string, any>} ctx
439
+ * @returns {Record<string, string>}
440
+ */
441
+ export function stackEnvFor(ctx) {
442
+ const l = ctx.mem0?.litellm;
443
+ // Off: override an exported MEM0_LITELLM=1 the operator declined; otherwise add nothing.
444
+ if (!l) return process.env.MEM0_LITELLM === "1" ? { MEM0_LITELLM: "0" } : {};
445
+ return { MEM0_LITELLM: "1", MEM0_LITELLM_PORT: l.port, MEM0_LITELLM_CONFIG: routesFile(ctx.home) };
446
+ }
447
+
448
+ /**
449
+ * Add or remove the compose profile "litellm" on the wizard's up command.
450
+ * @param {Record<string, any>} ctx
451
+ * @param {boolean} on
452
+ */
453
+ export function applyProfile(ctx, on) {
454
+ const extra = (ctx.extraComposeProfiles || []).filter((p) => p !== "litellm");
455
+ if (on) extra.push("litellm");
456
+ ctx.extraComposeProfiles = extra;
457
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Merge a usable mem0 MCP into ~/.cursor/mcp.json for the self-hosted REST server.
3
+ *
4
+ * Upstream stdio bridge: npx -y @yeyuan98/mem0-mcp (fronts MEM0_BASE_URL with X-API-Key).
5
+ * Official Mem0 MCP is cloud-only (https://mcp.mem0.ai); the OSS docs page is gone.
6
+ *
7
+ * mcp.json never holds MEM0_API_KEY. The launcher (cli/mem0-mcp.mjs) loads the key from
8
+ * MEM0_ENV_FILE / ~/.config/personal-memory/mem0.env when the process env lacks it.
9
+ */
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { envValue } from "../../../wizard/lib/paths.mjs";
14
+ import { DEFAULT_API_URL, defaultEnvFile } from "./state.mjs";
15
+
16
+ /** Sibling MCP servers this merge must never rewrite. */
17
+ export const MEM0_MCP_PROTECTED = Object.freeze(["hindsight", "codegraph", "notebooklm"]);
18
+
19
+ /** npm package that speaks the open-source REST API (not platform mcp.mem0.ai). */
20
+ export const MEM0_MCP_NPM_PACKAGE = "@yeyuan98/mem0-mcp";
21
+
22
+ /** @param {string} [pkgRoot] absolute package / checkout root */
23
+ export function mem0McpScriptPath(pkgRoot) {
24
+ const root =
25
+ pkgRoot ||
26
+ path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
27
+ return path.resolve(root, "cli/mem0-mcp.mjs");
28
+ }
29
+
30
+ /**
31
+ * MEM0_API_URL from env, stack .env, wizard.conf, then default. Never returns secrets.
32
+ * @param {{ home: string, stackRoot: string, env?: NodeJS.ProcessEnv }} opts
33
+ */
34
+ export function resolveMem0ApiUrl({ home, stackRoot, env = process.env }) {
35
+ const candidates = [
36
+ env.MEM0_API_URL,
37
+ env.PM_MEM0_API_URL,
38
+ envValue(path.join(stackRoot, ".env"), "MEM0_API_URL"),
39
+ envValue(path.join(home, ".config/hindsight/wizard.conf"), "MEM0_API_URL"),
40
+ ];
41
+ const first = candidates.map((v) => String(v || "").trim()).find(Boolean);
42
+ return (first || DEFAULT_API_URL).replace(/\/+$/, "");
43
+ }
44
+
45
+ /**
46
+ * Path to the API key file (path only — never read the key here).
47
+ * @param {{ home: string, stackRoot: string, env?: NodeJS.ProcessEnv }} opts
48
+ */
49
+ export function resolveMem0EnvFilePath({ home, stackRoot, env = process.env }) {
50
+ const candidates = [
51
+ env.MEM0_ENV_FILE,
52
+ envValue(path.join(stackRoot, ".env"), "MEM0_ENV_FILE"),
53
+ ];
54
+ const raw = candidates.map((v) => String(v || "").trim()).find(Boolean);
55
+ if (!raw) return defaultEnvFile(home);
56
+ if (raw.startsWith("~/")) return path.join(home, raw.slice(2));
57
+ if (path.isAbsolute(raw)) return raw;
58
+ return path.resolve(stackRoot, raw);
59
+ }
60
+
61
+ /**
62
+ * Cursor mcp.json entry: node launcher → npx @yeyuan98/mem0-mcp.
63
+ * Sets MEM0_API_URL + MEM0_BASE_URL (upstream reads BASE_URL). Path to key file only.
64
+ * @param {{ apiUrl: string, scriptPath: string, envFile?: string }} opts
65
+ */
66
+ export function mem0McpEntry({ apiUrl, scriptPath, envFile }) {
67
+ const url = String(apiUrl || "").replace(/\/+$/, "");
68
+ /** @type {Record<string, string>} */
69
+ const env = {
70
+ MEM0_API_URL: url,
71
+ MEM0_BASE_URL: url,
72
+ };
73
+ if (envFile) env.MEM0_ENV_FILE = envFile;
74
+ return {
75
+ command: "node",
76
+ args: [scriptPath],
77
+ env,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * @param {Record<string, any>} mcp
83
+ * @param {{ apiUrl: string, scriptPath: string, envFile?: string }} opts
84
+ * @returns {{ mcp: Record<string, any>, changed: boolean, skipped?: string, refused?: string }}
85
+ */
86
+ export function mergeMem0McpServer(mcp, opts) {
87
+ const entry = mem0McpEntry(opts);
88
+ const out = { ...mcp };
89
+ const servers = {
90
+ ...(mcp.mcpServers && typeof mcp.mcpServers === "object" ? mcp.mcpServers : {}),
91
+ };
92
+ const before = Object.fromEntries(MEM0_MCP_PROTECTED.map((k) => [k, servers[k]]));
93
+ const prev = servers.mem0;
94
+ const same = prev && JSON.stringify(prev) === JSON.stringify(entry);
95
+ if (same) {
96
+ return { mcp: { ...out, mcpServers: servers }, changed: false, skipped: "already configured" };
97
+ }
98
+ servers.mem0 = entry;
99
+ for (const k of MEM0_MCP_PROTECTED) {
100
+ if (before[k] !== undefined && servers[k] !== before[k]) {
101
+ return { mcp, changed: false, refused: `refusing to alter ${k} MCP entry` };
102
+ }
103
+ }
104
+ return { mcp: { ...out, mcpServers: servers }, changed: true };
105
+ }
106
+
107
+ /**
108
+ * @param {{
109
+ * home: string,
110
+ * repoRoot: string,
111
+ * stackRoot: string,
112
+ * dry?: boolean,
113
+ * env?: NodeJS.ProcessEnv,
114
+ * }} ctx
115
+ * @param {{ info: Function, ok: Function, warn: Function }} h
116
+ */
117
+ export async function installMem0Mcp(ctx, h) {
118
+ const env = ctx.env || process.env;
119
+ const apiUrl = resolveMem0ApiUrl({ home: ctx.home, stackRoot: ctx.stackRoot, env });
120
+ const envFile = resolveMem0EnvFilePath({ home: ctx.home, stackRoot: ctx.stackRoot, env });
121
+ const scriptPath = mem0McpScriptPath(ctx.repoRoot);
122
+ if (!fs.existsSync(scriptPath)) {
123
+ h.warn(`mem0 MCP launcher missing: ${scriptPath}`);
124
+ return { changed: false };
125
+ }
126
+
127
+ const mcpPath = path.join(ctx.home, ".cursor", "mcp.json");
128
+ let existing = {};
129
+ if (fs.existsSync(mcpPath)) {
130
+ try {
131
+ existing = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
132
+ } catch (err) {
133
+ h.warn(`cannot parse ${mcpPath}: ${err instanceof Error ? err.message : err}`);
134
+ return { changed: false };
135
+ }
136
+ }
137
+
138
+ const { mcp, changed, skipped, refused } = mergeMem0McpServer(existing, {
139
+ apiUrl,
140
+ scriptPath,
141
+ envFile,
142
+ });
143
+ if (refused) {
144
+ h.warn(refused);
145
+ return { changed: false, refused };
146
+ }
147
+ if (!changed) {
148
+ h.ok(`mem0 MCP already in ${mcpPath}${skipped ? ` (${skipped})` : ""}`);
149
+ return { changed: false, skipped };
150
+ }
151
+ if (ctx.dry) {
152
+ h.info(
153
+ `[dry-run] merge mem0 MCP into ${mcpPath} → node ${scriptPath}` +
154
+ ` (MEM0_API_URL=${apiUrl}; key via ${envFile} or MEM0_API_KEY in the agent env — never written to mcp.json)`,
155
+ );
156
+ return { changed: true, dry: true };
157
+ }
158
+ fs.mkdirSync(path.dirname(mcpPath), { recursive: true });
159
+ fs.writeFileSync(mcpPath, `${JSON.stringify(mcp, null, 2)}\n`);
160
+ h.ok(`registered mem0 MCP in ${mcpPath}`);
161
+ h.info(
162
+ `API key: export MEM0_API_KEY from ${envFile} (or leave it for the launcher to load). Never put the key in mcp.json.`,
163
+ );
164
+ return { changed: true };
165
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Holds the API key the operator handed over through the environment, out of process.env.
3
+ * The `init` hook runs before preflight spawns docker and curl, so capturing it there keeps
4
+ * the key out of every child process environment. The value lives in this module only: it is
5
+ * not a property of the wizard context and is never serialised.
6
+ */
7
+
8
+ let captured = "";
9
+
10
+ /**
11
+ * Move PM_MEM0_API_KEY (the Clack hand-off) and MEM0_API_KEY out of process.env. The hand-off
12
+ * wins. Nothing is restored. When neither variable is set an earlier capture is kept.
13
+ */
14
+ export function captureKeyFromEnv() {
15
+ const handoff = (process.env.PM_MEM0_API_KEY || "").trim();
16
+ const plain = (process.env.MEM0_API_KEY || "").trim();
17
+ delete process.env.PM_MEM0_API_KEY;
18
+ delete process.env.MEM0_API_KEY;
19
+ if (handoff || plain) captured = handoff || plain;
20
+ }
21
+
22
+ /** The captured key, or "". */
23
+ export function capturedKey() {
24
+ return captured;
25
+ }
26
+
27
+ /** Forget the captured key (tests). */
28
+ export function clearCapturedKey() {
29
+ captured = "";
30
+ }