@bcmyguest/openmem 0.6.3 → 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 (102) hide show
  1. package/{skills → .agents/skills}/openmem/SKILL.md +5 -0
  2. package/README.md +80 -76
  3. package/cli/coding-agent/capabilities.mjs +14 -3
  4. package/cli/coding-agent/context-store.mjs +81 -0
  5. package/cli/coding-agent/context-trim.mjs +897 -0
  6. package/cli/coding-agent/file-io.mjs +72 -0
  7. package/cli/coding-agent/hook-harnesses.mjs +2 -1
  8. package/cli/coding-agent/hook-runner.mjs +28 -4
  9. package/cli/coding-agent/install-access.mjs +43 -17
  10. package/cli/coding-agent/install-context-proxy.mjs +333 -0
  11. package/cli/coding-agent/install-rtk.mjs +60 -0
  12. package/cli/coding-agent/install-shim.mjs +119 -15
  13. package/cli/coding-agent/opencode-config.mjs +21 -5
  14. package/cli/coding-agent/opencode-plugin.mjs +39 -3
  15. package/cli/coding-agent/opencode2-plugin/index.js +299 -0
  16. package/cli/coding-agent/recall-frame.mjs +29 -0
  17. package/cli/coding-agent/remove-upstream-access.mjs +122 -0
  18. package/cli/coding-agent/runtime.mjs +2 -1
  19. package/cli/coding-agent/session-db.mjs +626 -0
  20. package/cli/coding-agent/session-facade.mjs +30 -6
  21. package/cli/coding-agent/toml-edit.mjs +161 -0
  22. package/cli/context-management.mjs +254 -0
  23. package/cli/feed-sync.mjs +18 -2
  24. package/cli/install-coding-agent.mjs +173 -40
  25. package/cli/mcp/context-sql-runner.mjs +58 -0
  26. package/cli/mcp/context-store.mjs +670 -0
  27. package/cli/mcp/knowledge.mjs +333 -0
  28. package/cli/mcp/memory-facade.mjs +99 -33
  29. package/cli/openmem-mcp.mjs +3 -0
  30. package/cli/openmem.mjs +18 -1
  31. package/cli/schedule.mjs +176 -0
  32. package/cli/stack.mjs +2 -0
  33. package/cli/switch-memory-provider.mjs +1 -1
  34. package/compose.yaml +67 -4
  35. package/config/hindsight.env.example +2 -0
  36. package/config/openmem.toml.example +64 -2
  37. package/docs/architecture.md +35 -9
  38. package/docs/coding-agent-hooks.md +26 -2
  39. package/docs/coding-agents.md +27 -7
  40. package/docs/compatibility.md +80 -0
  41. package/docs/concepts.md +30 -7
  42. package/docs/context-management.md +255 -0
  43. package/docs/context-proxy.md +115 -0
  44. package/docs/context-research.md +215 -0
  45. package/docs/context-store.md +277 -0
  46. package/docs/hindsight.md +5 -2
  47. package/docs/mcp-setup.md +5 -2
  48. package/docs/mem0.md +39 -1
  49. package/docs/memory-providers.md +2 -2
  50. package/docs/meridian-litellm.md +198 -0
  51. package/docs/model-configuration.md +22 -0
  52. package/docs/npx-install.md +6 -4
  53. package/docs/openmem-config.md +2 -2
  54. package/docs/quickstart.md +12 -6
  55. package/docs/setup-wizard.md +5 -4
  56. package/docs/sources-feeds.md +9 -22
  57. package/package.json +5 -4
  58. package/providers/cognee/OPERATOR.md +2 -2
  59. package/providers/hindsight/lib/clack.mjs +122 -20
  60. package/providers/hindsight/lib/coding-agent.mjs +1 -1
  61. package/providers/hindsight/lib/config.mjs +6 -0
  62. package/providers/hindsight/lib/env-setup.mjs +2 -1
  63. package/providers/hindsight/lib/litellm-routes.mjs +21 -12
  64. package/providers/hindsight/lib/meridian.mjs +307 -0
  65. package/providers/hindsight/lib/model-backend.mjs +38 -9
  66. package/providers/hindsight/lib/probe.mjs +21 -0
  67. package/providers/hindsight/lib/stack-env.mjs +5 -12
  68. package/providers/hindsight/lib/state.mjs +6 -1
  69. package/providers/hindsight/lib/summary.mjs +16 -2
  70. package/providers/hindsight/lib/validate.mjs +6 -0
  71. package/providers/hindsight/wizard.mjs +3 -2
  72. package/providers/mem0/OPERATOR.md +21 -2
  73. package/providers/mem0/compose.yaml +24 -2
  74. package/providers/mem0/lib/coding-agent.mjs +1 -1
  75. package/providers/mem0/lib/configure.mjs +66 -0
  76. package/providers/mem0/lib/litellm.mjs +457 -0
  77. package/providers/mem0/lib/stack-env.mjs +40 -1
  78. package/providers/mem0/lib/summary.mjs +30 -2
  79. package/providers/mem0/manifest.json +1 -1
  80. package/providers/mem0/wizard.mjs +15 -2
  81. package/proxy/config.yaml +33 -0
  82. package/proxy/openmem_context/__init__.py +6 -0
  83. package/proxy/openmem_context/adapters.py +504 -0
  84. package/proxy/openmem_context/compress.py +53 -0
  85. package/proxy/openmem_context/core.py +606 -0
  86. package/proxy/openmem_context/embed.py +212 -0
  87. package/proxy/openmem_context/recall.py +49 -0
  88. package/proxy/openmem_context/session.py +70 -0
  89. package/proxy/openmem_context/sessiondb.py +276 -0
  90. package/proxy/openmem_context/settings.py +134 -0
  91. package/proxy/openmem_context/store.py +129 -0
  92. package/proxy/openmem_context/tokens.py +123 -0
  93. package/proxy/openmem_context/ttl.py +78 -0
  94. package/proxy/openmem_context_hook.py +34 -0
  95. package/scripts/check-pack.mjs +2 -1
  96. package/scripts/setup.mjs +14 -1
  97. package/server.json +2 -2
  98. package/wizard/lib/context-step.mjs +140 -0
  99. package/wizard/lib/litellm-env.mjs +55 -0
  100. package/wizard/lib/pm-config.mjs +212 -4
  101. package/wizard/lib/prompts.mjs +28 -0
  102. package/wizard/lib/run-wizard.mjs +26 -31
@@ -1,9 +1,18 @@
1
1
  /**
2
2
  * Hindsight model routing: how the LiteLLM proxy in front of Hindsight reaches an LLM
3
- * (lemond, Ollama or a custom OpenAI-compatible URL), plus the reflect-model question and
3
+ * (lemond, Ollama, a custom OpenAI-compatible URL or Meridian), plus the reflect-model question and
4
4
  * `lemonade pull`. The meta wizard knows none of this; it calls configureModelRouting.
5
5
  */
6
6
  import { applyRoute, litellmConfig, saveConf } from "./litellm-routes.mjs";
7
+ import {
8
+ chooseMeridian,
9
+ containerApiBase,
10
+ existingMeridianKey,
11
+ MERIDIAN_DEFAULT_BASE,
12
+ MERIDIAN_REFLECT_DEFAULT,
13
+ normalizeMeridianBase,
14
+ writeMeridianKey,
15
+ } from "./meridian.mjs";
7
16
  import { ollamaModelNames, openAiModelIds } from "./probe.mjs";
8
17
  import {
9
18
  backendDefault,
@@ -26,9 +35,9 @@ async function pickBackend(ctx, h) {
26
35
  return;
27
36
  }
28
37
  h.info("Pick a backend first; only that server is contacted.");
29
- h.info("1) lemond 2) ollama 3) custom OpenAI-compatible URL");
30
- const defNum = def === "lemond" ? "1" : def === "ollama" ? "2" : "3";
31
- const choice = await h.ask(ctx, "Model backend (1-3)", defNum);
38
+ h.info("1) lemond 2) ollama 3) custom OpenAI-compatible URL 4) Claude subscription (Meridian)");
39
+ const defNum = { lemond: "1", ollama: "2", custom: "3", meridian: "4" }[def] || "1";
40
+ const choice = await h.ask(ctx, "Model backend (1-4)", defNum);
32
41
  switch (choice) {
33
42
  case "1":
34
43
  ctx.backend = "lemond";
@@ -39,8 +48,11 @@ async function pickBackend(ctx, h) {
39
48
  case "3":
40
49
  ctx.backend = "custom";
41
50
  break;
51
+ case "4":
52
+ ctx.backend = "meridian";
53
+ break;
42
54
  default:
43
- h.die("choose 1, 2 or 3");
55
+ h.die("choose 1, 2, 3 or 4");
44
56
  }
45
57
  }
46
58
 
@@ -125,7 +137,12 @@ async function chooseOllama(ctx, h) {
125
137
  "MEMORY_MODEL",
126
138
  process.env.PM_DEFAULT_MODEL || ctx.ollamaDefaultModel,
127
139
  );
128
- if (ctx.ollamaJson) {
140
+ // Compose Ollama publishes 127.0.0.1:11434, so a saved compose choice must win over the host probe.
141
+ if (storedOr(ctx, h, "ollama", "MEMORY_OLLAMA_IN_COMPOSE", "0") === "1") {
142
+ ctx.ollamaInCompose = true;
143
+ ctx.apiBase = OLLAMA_COMPOSE_API_BASE;
144
+ h.info("Keeping Ollama in Docker (compose profile ollama); LiteLLM calls http://ollama:11434.");
145
+ } else if (ctx.ollamaJson) {
129
146
  ctx.ollamaInCompose = false;
130
147
  ctx.apiBase = OLLAMA_HOST_API_BASE;
131
148
  } else {
@@ -150,17 +167,18 @@ async function chooseCustom(ctx, h) {
150
167
  const defaultBase = storedOr(ctx, h, "custom", "MEMORY_API_BASE", ctx.customDefaultBase);
151
168
  const defaultModel = storedOr(ctx, h, "custom", "MEMORY_MODEL", ctx.lemondDefaultModel);
152
169
  h.info("The litellm container calls this URL. Host services use host.docker.internal.");
153
- h.info("Examples: lemond http://host.docker.internal:13305/v1 ; Meridian http://host.docker.internal:3456/v1");
170
+ h.info("Example: lemond http://host.docker.internal:13305/v1. For a Claude subscription through Meridian, pick backend 4.");
154
171
  ctx.apiBase = await h.ask(ctx, "OpenAI-compatible base URL", defaultBase);
155
172
  ctx.model = await h.ask(ctx, "Model id the server serves", defaultModel);
156
173
  await askReflectModel(ctx, h);
157
174
  }
158
175
 
159
176
  /**
160
- * Step "3. Model backend": choose lemond, Ollama or a custom URL, write the LiteLLM route
177
+ * Step "3. Model backend": choose lemond, Ollama, a custom URL or Meridian, write the LiteLLM route
161
178
  * override when the choice differs from the shipped routes, and record it in wizard.conf.
162
179
  * With the Clack UI done (ctx.skipBackendStep) the answers come from PM_BACKEND,
163
- * PM_DEFAULT_MODEL, MEMORY_API_BASE and PM_REFLECT_MODEL instead.
180
+ * PM_DEFAULT_MODEL, MEMORY_API_BASE and PM_REFLECT_MODEL instead (Meridian adds
181
+ * MERIDIAN_BASE and MERIDIAN_API_KEY).
164
182
  *
165
183
  * @param {Record<string, any>} ctx
166
184
  * @param {Helpers} h
@@ -179,8 +197,15 @@ export async function chooseModelBackend(ctx, h) {
179
197
  if (ctx.backend === "ollama" && !ctx.apiBase) {
180
198
  ctx.apiBase = ctx.ollamaInCompose ? OLLAMA_COMPOSE_API_BASE : OLLAMA_HOST_API_BASE;
181
199
  }
200
+ if (ctx.backend === "meridian") {
201
+ ctx.meridianBase = normalizeMeridianBase(process.env.MERIDIAN_BASE || MERIDIAN_DEFAULT_BASE);
202
+ ctx.apiBase ||= containerApiBase(ctx.meridianBase);
203
+ ctx.reflectModel ||= MERIDIAN_REFLECT_DEFAULT;
204
+ ctx.meridianKey = existingMeridianKey(ctx, h).key;
205
+ }
182
206
  h.ok(`backend ${ctx.backend}, model ${ctx.model} (setup UI)`);
183
207
  await applyRoute(ctx, h);
208
+ if (ctx.backend === "meridian") writeMeridianKey(ctx, h);
184
209
  saveConf(ctx, h);
185
210
  return;
186
211
  }
@@ -205,10 +230,14 @@ export async function chooseModelBackend(ctx, h) {
205
230
  case "custom":
206
231
  await chooseCustom(ctx, h);
207
232
  break;
233
+ case "meridian":
234
+ await chooseMeridian(ctx, h);
235
+ break;
208
236
  default:
209
237
  h.die(`unknown backend ${ctx.backend}`);
210
238
  }
211
239
  h.ok(`backend ${ctx.backend}, model ${ctx.model}`);
212
240
  await applyRoute(ctx, h);
241
+ if (ctx.backend === "meridian") writeMeridianKey(ctx, h);
213
242
  saveConf(ctx, h);
214
243
  }
@@ -31,3 +31,24 @@ export function normalizeOpenAiBase(base) {
31
31
  if (!b.endsWith("/v1")) b += "/v1";
32
32
  return b;
33
33
  }
34
+
35
+ /**
36
+ * GET `url` and report the HTTP status (0 when nothing answered) and the parsed JSON body.
37
+ * @param {string} url
38
+ * @param {{ headers?: Record<string, string>, timeoutMs?: number }} [opts]
39
+ * @returns {Promise<{ status: number, json: any }>}
40
+ */
41
+ export async function fetchStatus(url, { headers = {}, timeoutMs = TIMEOUT_MS } = {}) {
42
+ try {
43
+ const res = await fetch(url, { headers, signal: AbortSignal.timeout(timeoutMs) });
44
+ let json = null;
45
+ try {
46
+ json = await res.json();
47
+ } catch {
48
+ json = null;
49
+ }
50
+ return { status: res.status, json };
51
+ } catch {
52
+ return { status: 0, json: null };
53
+ }
54
+ }
@@ -4,11 +4,14 @@
4
4
  * Runs as a plain Node script step (no wizard context), so it only reads process
5
5
  * environment and the paths it is given.
6
6
  */
7
- import crypto from "node:crypto";
8
7
  import fs from "node:fs";
9
8
  import path from "node:path";
9
+ import { ensureLitellmEnv } from "../../../wizard/lib/litellm-env.mjs";
10
10
  import { envValue } from "../../../wizard/lib/paths.mjs";
11
11
 
12
+ // Moved to wizard/lib/litellm-env.mjs (mem0 shares it); re-exported for existing importers.
13
+ export { ensureLitellmEnv };
14
+
12
15
  /**
13
16
  * @param {{ repo: string, home: string, env: NodeJS.ProcessEnv }} opts
14
17
  * @returns {{ entries: [string, string][], drop: string[], report: string[] }}
@@ -25,17 +28,7 @@ export function prepareStackEnv({ repo, home, env }) {
25
28
  throw new Error(`MEMORY_BACKEND must be lemond or ollama (got: ${backend})`);
26
29
  }
27
30
 
28
- if (!fs.existsSync(litellmEnv)) {
29
- const key = crypto.randomBytes(24).toString("hex");
30
- const body =
31
- "# LiteLLM proxy secrets. Read by the litellm compose service. Do not commit.\n" +
32
- `LITELLM_MASTER_KEY=sk-${key}\n` +
33
- "# lemond ignores the key; any non-empty value works.\n" +
34
- "LEMOND_API_KEY=placeholder\n" +
35
- "# Hosted models: add the provider key here, e.g. ANTHROPIC_API_KEY=..., and reference it as os.environ/ANTHROPIC_API_KEY.\n";
36
- fs.writeFileSync(litellmEnv, body, { mode: 0o600 });
37
- console.log(`created ${litellmEnv} (mode 600)`);
38
- }
31
+ if (ensureLitellmEnv(litellmEnv)) console.log(`created ${litellmEnv} (mode 600)`);
39
32
 
40
33
  const litellmConfig = fs.existsSync(overrideConfig)
41
34
  ? overrideConfig
@@ -9,7 +9,7 @@ export const OLLAMA_HOST_API_BASE = "http://host.docker.internal:11434";
9
9
  /** LiteLLM api_base when Ollama is the compose `ollama` service. */
10
10
  export const OLLAMA_COMPOSE_API_BASE = "http://ollama:11434";
11
11
 
12
- export const BACKENDS = ["lemond", "ollama", "custom"];
12
+ export const BACKENDS = ["lemond", "ollama", "custom", "meridian"];
13
13
 
14
14
  /**
15
15
  * Fill the Hindsight fields of the wizard context. Idempotent: a field that is already
@@ -23,6 +23,8 @@ export function ensureState(ctx) {
23
23
  litellmEnv: path.join(hs, "litellm.env"),
24
24
  litellmOverride: path.join(hs, "litellm.yaml"),
25
25
  agentJson: path.join(ctx.home || "", ".hindsight/coding-agent.json"),
26
+ /** Meridian's own env file from docs/meridian-litellm.md (read for MERIDIAN_API_KEY/HOST/PORT). */
27
+ meridianEnvFile: path.join(ctx.home || "", ".config/meridian/meridian.env"),
26
28
  lemondBase: "http://127.0.0.1:13305",
27
29
  ollamaBase: "http://127.0.0.1:11434",
28
30
  lemondDefaultModel: LEMOND_DEFAULT_MODEL,
@@ -41,6 +43,9 @@ export function ensureState(ctx) {
41
43
  portApi: "",
42
44
  portCp: "",
43
45
  hindsightUrl: "",
46
+ /** Meridian URL as seen from the host, and the key for litellm.env (backend meridian). */
47
+ meridianBase: "",
48
+ meridianKey: "",
44
49
  };
45
50
  for (const [key, value] of Object.entries(defaults)) {
46
51
  if (ctx[key] === undefined) ctx[key] = value;
@@ -1,6 +1,7 @@
1
1
  /** Hindsight's contribution to the wizard's closing summary. */
2
2
  import fs from "node:fs";
3
3
  import { composeCommandText } from "../../../wizard/lib/compose.mjs";
4
+ import { MERIDIAN_GUIDE, MERIDIAN_TERMS_URL } from "./meridian.mjs";
4
5
  import { ensureState } from "./state.mjs";
5
6
 
6
7
  /**
@@ -19,14 +20,20 @@ export function summary(ctx) {
19
20
  ` control plane http://127.0.0.1:${ctx.portCp}`,
20
21
  ` model ${ctx.backend}, ${ctx.model} via ${modelWhere}`,
21
22
  ];
23
+ if (ctx.backend === "meridian") {
24
+ running.push(` reflect model ${ctx.reflectModel || ctx.model}`);
25
+ running.push(
26
+ ` Meridian ${ctx.meridianBase || "(host URL not recorded)"} on the host (not managed by setup; LiteLLM calls ${ctx.apiBase})`,
27
+ );
28
+ }
22
29
  if (ctx.ollamaInCompose) {
23
30
  running.push(` Ollama http://127.0.0.1:11434 (compose service; LiteLLM uses http://ollama:11434)`);
24
31
  }
25
32
  const configFiles = [` ${ctx.envFile}`];
26
33
  if (fs.existsSync(ctx.litellmOverride)) {
27
34
  configFiles.push(` ${ctx.litellmOverride} (model routes; replaces config/litellm.*.yaml)`);
28
- } else if (ctx.backend === "custom") {
29
- configFiles.push(` ${ctx.litellmOverride} (missing: custom backend needs it)`);
35
+ } else if (ctx.backend === "custom" || ctx.backend === "meridian") {
36
+ configFiles.push(` ${ctx.litellmOverride} (missing: ${ctx.backend} backend needs it)`);
30
37
  } else configFiles.push(` config/litellm.${ctx.backend}.yaml (shipped routes)`);
31
38
  const litellmUp = composeCommandText(ctx, "up", "-d", "litellm");
32
39
  const notes = [
@@ -34,6 +41,13 @@ export function summary(ctx) {
34
41
  `Reflect needs a capable model (memory-reflect alias). On a weak or remote LLM, edit ${ctx.litellmOverride} and point memory-reflect at Meridian, an OpenAI-compatible host, or a stronger local model — then: ${litellmUp}`,
35
42
  `Change later: rerun ./setup. For a route-only edit, run: ${litellmUp}`,
36
43
  ];
44
+ if (ctx.backend === "meridian") {
45
+ configFiles.push(` ${ctx.litellmEnv} (MERIDIAN_API_KEY)`);
46
+ notes.push(
47
+ `Meridian must keep running on this host with MERIDIAN_HOST=0.0.0.0 so the LiteLLM container reaches it; see ${MERIDIAN_GUIDE}.`,
48
+ `Claude subscription use through Meridian is governed by Anthropic's Consumer Terms: ${MERIDIAN_TERMS_URL}`,
49
+ );
50
+ }
37
51
  if (ctx.smokeFailed) {
38
52
  notes.unshift(
39
53
  `Smoke retain/recall did not finish — stack is up; fix the model/LLM queue via http://127.0.0.1:${ctx.portCp} then re-run setup or: curl the wizard-smoke retain against ${ctx.hindsightUrl}`,
@@ -69,6 +69,12 @@ export function validateHindsight(ctx, h) {
69
69
  const model = conf("MEMORY_MODEL");
70
70
  const shipped = backend === "ollama" ? ctx.ollamaDefaultModel : ctx.lemondDefaultModel;
71
71
  const reflect = conf("MEMORY_REFLECT_MODEL");
72
+ if (backend === "meridian") {
73
+ if (!fs.existsSync(ctx.litellmOverride)) errors.push("meridian backend without litellm.yaml override");
74
+ if (!h.envValue(ctx.litellmEnv, "MERIDIAN_API_KEY")) errors.push("litellm.env missing MERIDIAN_API_KEY");
75
+ checks.push("Meridian route and key");
76
+ return;
77
+ }
72
78
  if (backend !== "custom" && model !== shipped && !reflect && !fs.existsSync(ctx.litellmOverride)) {
73
79
  errors.push("custom model without litellm.yaml override");
74
80
  }
@@ -44,8 +44,8 @@ export async function afterProvider(ctx, h) {
44
44
  }
45
45
 
46
46
  /**
47
- * Step "3. Model backend": lemond, Ollama or a custom OpenAI-compatible URL behind the
48
- * LiteLLM proxy.
47
+ * Step "3. Model backend": lemond, Ollama, a custom OpenAI-compatible URL or a Claude
48
+ * subscription through Meridian, behind the LiteLLM proxy.
49
49
  * @param {Record<string, any>} ctx
50
50
  * @param {Helpers} h
51
51
  */
@@ -78,6 +78,7 @@ export async function hydrate(ctx, h) {
78
78
  ctx.model = confGet(ctx, h, "MEMORY_MODEL") || ctx.lemondDefaultModel;
79
79
  ctx.apiBase = confGet(ctx, h, "MEMORY_API_BASE") || "";
80
80
  ctx.reflectModel = confGet(ctx, h, "MEMORY_REFLECT_MODEL") || "";
81
+ ctx.meridianBase = confGet(ctx, h, "MEMORY_MERIDIAN_URL") || "";
81
82
  ctx.ollamaInCompose = confGet(ctx, h, "MEMORY_OLLAMA_IN_COMPOSE") === "1";
82
83
  applyComposeProfiles(ctx);
83
84
  }
@@ -16,6 +16,7 @@ server only (no `/v1/` prefix).
16
16
  | Embedder / LLM keys for mem0 itself | Indexer sink (`MEMORY_SINK=mem0`) and host-gateway rewrite |
17
17
  | Create an API key (dashboard or `POST /api-keys`) | Store `MEM0_API_KEY` in `~/.config/personal-memory/mem0.env` (mode **600**) |
18
18
  | Optional `AUTH_DISABLED=true` (dev only) | Smoke: `GET /docs`, retain, search, delete-by-id |
19
+ | Apply the printed `llm` / `embedder` blocks to mem0 (optional) | Optional stack LiteLLM for mem0's LLM: routes, `litellm.env`, profile `litellm` on `127.0.0.1` |
19
20
 
20
21
  ## URLs
21
22
 
@@ -76,7 +77,7 @@ via the launcher `cli/mem0-mcp.mjs` (loads the key from the env file, then
76
77
 
77
78
  ```sh
78
79
  MEMORY_PROVIDER=mem0 npx openmem install-mcp --yes
79
- # checkout: MEMORY_PROVIDER=mem0 npx . install-mcp --yes
80
+ # checkout: MEMORY_PROVIDER=mem0 node cli/openmem.mjs install-mcp --yes
80
81
  # or accept the coding-agent MCP confirm in setup
81
82
  ```
82
83
 
@@ -111,14 +112,32 @@ Restart Cursor Agent after install.
111
112
  ```sh
112
113
  npx openmem switch mem0 --yes
113
114
  # later: npx openmem switch hindsight --yes
114
- # checkout: npx . switch mem0 --yes
115
+ # checkout: node cli/openmem.mjs switch mem0 --yes
115
116
  ```
116
117
 
117
118
  Updates `wizard.conf` (and `config.toml` `[memory].provider` when present), restarts the stack
118
119
  unless `--no-stack`, and runs `install-mcp --provider-only` unless `--no-mcp`. Does **not**
119
120
  migrate memories. Flags: [README — Switch memory provider](../../README.md#switch-memory-provider).
120
121
 
122
+ ## Models through the stack LiteLLM
123
+
124
+ Optional (wizard step 3, or `MEM0_LITELLM=1` under `--yes`): mem0's LLM, and
125
+ its embedder when the backend has one, go through the stack LiteLLM. Inputs
126
+ under `--yes`: `MEM0_LITELLM_BACKEND` (`ollama`, `custom`, `anthropic`,
127
+ `meridian`), `MEM0_LITELLM_MODEL`, `MEM0_LITELLM_API_BASE`,
128
+ `MEM0_LITELLM_EMBED_MODEL` (`none` keeps mem0's embedder),
129
+ `MEM0_LITELLM_PORT` (default 4000), `ANTHROPIC_API_KEY`. The wizard writes
130
+ `~/.config/personal-memory/mem0-litellm.yaml`, keeps keys in
131
+ `~/.config/hindsight/litellm.env` (mode 600), and prints the mem0 `llm` /
132
+ `embedder` blocks (provider `openai`, `openai_base_url`
133
+ `http://127.0.0.1:<port>/v1`, `api_key` = `LITELLM_MASTER_KEY`). You apply
134
+ them; the wizard never edits the mem0 server config. Key sources and limits:
135
+ [docs/mem0.md](../../docs/mem0.md#models-through-the-stack-litellm-optional).
136
+
121
137
  ## Compose
122
138
 
123
139
  `providers/mem0/compose.yaml` defines no mem0 service — only env +
124
140
  `extra_hosts` on the shared `indexer`. Profile `mem0` selects that fragment.
141
+ It also overlays the base `litellm` service: under profile `litellm` it
142
+ publishes `127.0.0.1:${MEM0_LITELLM_PORT:-4000}` (never `0.0.0.0`) and its
143
+ entrypoint exits unless `LITELLM_MASTER_KEY` starts with `sk-`.
@@ -13,9 +13,14 @@
13
13
  # What the base compose.yaml gives you:
14
14
  # - `indexer` and `indexer-state-init` have no profile and no provider dependency, so they start for every
15
15
  # provider. This fragment points the indexer at the mem0 server.
16
- # - `litellm` and `hindsight` carry the profile "hindsight" and do not start under this profile.
16
+ # - `hindsight` carries the profile "hindsight" and never starts here.
17
+ # - `litellm` also carries its own profile "litellm". It starts only when the wizard's "route mem0's LLM through the
18
+ # stack LiteLLM" option is on, which writes COMPOSE_PROFILES=mem0,litellm. This fragment then publishes it on
19
+ # 127.0.0.1:${MEM0_LITELLM_PORT:-4000} (loopback only, never 0.0.0.0) for a mem0 server on the host, and the
20
+ # entrypoint below refuses to start without LITELLM_MASTER_KEY (sk-...) from LITELLM_ENV_FILE. See docs/mem0.md.
17
21
  # Compose still interpolates every service before it filters by profile, so the stack .env needs LITELLM_CONFIG
18
- # even though litellm never runs here. scripts/setup.mjs (providers/mem0/lib/stack-env.mjs) writes an inert value.
22
+ # even when litellm does not run. scripts/setup.mjs (providers/mem0/lib/stack-env.mjs) writes the mem0 route file
23
+ # when the option is on and an inert value otherwise.
19
24
  #
20
25
  # Loopback caveat (Linux): host.docker.internal resolves to the docker bridge gateway, not to the host's 127.0.0.1.
21
26
  # A mem0 server bound only to 127.0.0.1 is therefore NOT reachable from the indexer container. Bind it to 0.0.0.0
@@ -55,3 +60,20 @@ services:
55
60
  required: false
56
61
  extra_hosts:
57
62
  - "host.docker.internal:host-gateway"
63
+
64
+ # Only under the "litellm" profile (see the header). Loopback publish for a mem0 server running on this host.
65
+ litellm:
66
+ ports:
67
+ - "127.0.0.1:${MEM0_LITELLM_PORT:-4000}:4000"
68
+ # The port is reachable by every local user and process, so an unauthenticated proxy is not allowed: exit unless
69
+ # the env file supplies an sk- master key. The routes' general_settings.master_key then makes LiteLLM enforce it.
70
+ entrypoint:
71
+ - /bin/sh
72
+ - -c
73
+ - |
74
+ case "$${LITELLM_MASTER_KEY:-}" in
75
+ sk-?*) ;;
76
+ *) echo "litellm: LITELLM_MASTER_KEY=sk-... is missing from LITELLM_ENV_FILE; rerun ./setup" >&2; exit 1 ;;
77
+ esac
78
+ exec docker/prod_entrypoint.sh "$$@"
79
+ - litellm
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Step "8. Coding-agent plugin", mem0 half: there is no npx pluginPackage for the
2
+ * Step "9. Coding-agent plugin", mem0 half: there is no npx pluginPackage for the
3
3
  * self-hosted REST server. Print concrete SDK / MCP / direct-REST paths, then
4
4
  * optionally merge the mem0 MCP into ~/.cursor/mcp.json (--yes or confirm).
5
5
  */
@@ -7,6 +7,7 @@
7
7
  import path from "node:path";
8
8
  import { applyConfig, configToml } from "./config.mjs";
9
9
  import { dirTooOpen, fileMode, keyProblem, readKey, tooOpen, writeKey } from "./env-file.mjs";
10
+ import { EMBED_BACKENDS, storedChoice } from "./litellm.mjs";
10
11
  import { capturedKey, captureKeyFromEnv } from "./secret.mjs";
11
12
  import { ensureState, envFileProblem, resolveEnvFile } from "./state.mjs";
12
13
  import { displayUrl, resolveApiUrl, urlProblem } from "./url.mjs";
@@ -130,6 +131,71 @@ export async function clackSetup(ctx, ui, h) {
130
131
  const handoff = { WIZARD_SKIP_BACKEND_STEP: "1", MEM0_API_URL: url };
131
132
  const key = String(secret || "").trim();
132
133
  if (key) handoff.PM_MEM0_API_KEY = key;
134
+ Object.assign(handoff, await clackLitellm(stub, ui, h));
133
135
  ui.note("Next: Docker, indexing, and starting the stack.", "mem0 configured");
134
136
  return handoff;
135
137
  }
138
+
139
+ /**
140
+ * Clack questions for "route mem0's LLM through the stack LiteLLM". Returns the MEM0_LITELLM_*
141
+ * handoff (and ANTHROPIC_API_KEY when typed); the wizard step reads them under WIZARD_SKIP_BACKEND_STEP.
142
+ * @param {Record<string, any>} ctx
143
+ * @param {typeof import("@clack/prompts")} ui
144
+ * @param {Helpers} h
145
+ * @returns {Promise<Record<string, string>>}
146
+ */
147
+ async function clackLitellm(ctx, ui, h) {
148
+ const exitIfCancel = (v) => {
149
+ if (ui.isCancel(v)) {
150
+ ui.cancel("Setup cancelled.");
151
+ process.exit(0);
152
+ }
153
+ return v;
154
+ };
155
+ const current = storedChoice(ctx, h);
156
+ const on = exitIfCancel(
157
+ await ui.confirm({
158
+ message: "Route mem0's LLM through the stack LiteLLM (same gateway as Hindsight; 127.0.0.1 only)?",
159
+ initialValue: current.enabled,
160
+ }),
161
+ );
162
+ if (!on) return { MEM0_LITELLM: "0" };
163
+ const backend = /** @type {string} */ (
164
+ exitIfCancel(
165
+ await ui.select({
166
+ message: "LiteLLM backend for mem0's LLM",
167
+ options: [
168
+ { value: "ollama", label: "Ollama", hint: "LLM and embeddings (nomic-embed-text)" },
169
+ { value: "custom", label: "Custom OpenAI-compatible URL", hint: "lemond, vLLM, OpenAI, ..." },
170
+ { value: "anthropic", label: "Anthropic API key", hint: "Claude; no embeddings" },
171
+ { value: "meridian", label: "Claude subscription (Meridian)", hint: "Anthropic Consumer Terms apply; no embeddings" },
172
+ ],
173
+ initialValue: current.backend,
174
+ }),
175
+ )
176
+ );
177
+ const c = storedChoice(ctx, h, backend);
178
+ /** @type {Record<string, string>} */
179
+ const out = { MEM0_LITELLM: "1", MEM0_LITELLM_BACKEND: backend };
180
+ if (backend === "ollama" || backend === "custom") {
181
+ out.MEM0_LITELLM_API_BASE = String(
182
+ exitIfCancel(await ui.text({ message: "Model server base URL (as the litellm container sees it)", placeholder: c.apiBase, defaultValue: c.apiBase })) || c.apiBase,
183
+ ).trim();
184
+ }
185
+ out.MEM0_LITELLM_MODEL = String(
186
+ exitIfCancel(await ui.text({ message: "Model id for mem0's LLM", placeholder: c.model, defaultValue: c.model })) || c.model,
187
+ ).trim();
188
+ if (EMBED_BACKENDS.includes(backend)) {
189
+ const def = c.embedModel || "none";
190
+ out.MEM0_LITELLM_EMBED_MODEL = String(
191
+ exitIfCancel(await ui.text({ message: "Embedding model through LiteLLM (none keeps mem0's own embedder)", placeholder: def, defaultValue: def })) || def,
192
+ ).trim();
193
+ }
194
+ if (backend === "anthropic") {
195
+ const secret = String(
196
+ exitIfCancel(await ui.password({ message: "ANTHROPIC_API_KEY for litellm.env (empty keeps the one there)" })) || "",
197
+ ).trim();
198
+ if (secret) out.PM_ANTHROPIC_API_KEY = secret;
199
+ }
200
+ return out;
201
+ }