@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.
- package/{skills → .agents/skills}/openmem/SKILL.md +5 -0
- package/README.md +80 -76
- package/cli/coding-agent/capabilities.mjs +14 -3
- package/cli/coding-agent/context-store.mjs +81 -0
- package/cli/coding-agent/context-trim.mjs +897 -0
- package/cli/coding-agent/file-io.mjs +72 -0
- package/cli/coding-agent/hook-harnesses.mjs +2 -1
- package/cli/coding-agent/hook-runner.mjs +28 -4
- package/cli/coding-agent/install-access.mjs +43 -17
- package/cli/coding-agent/install-context-proxy.mjs +333 -0
- package/cli/coding-agent/install-rtk.mjs +60 -0
- package/cli/coding-agent/install-shim.mjs +119 -15
- package/cli/coding-agent/opencode-config.mjs +21 -5
- package/cli/coding-agent/opencode-plugin.mjs +39 -3
- package/cli/coding-agent/opencode2-plugin/index.js +299 -0
- package/cli/coding-agent/recall-frame.mjs +29 -0
- package/cli/coding-agent/remove-upstream-access.mjs +122 -0
- package/cli/coding-agent/runtime.mjs +2 -1
- package/cli/coding-agent/session-db.mjs +626 -0
- package/cli/coding-agent/session-facade.mjs +30 -6
- package/cli/coding-agent/toml-edit.mjs +161 -0
- package/cli/context-management.mjs +254 -0
- package/cli/feed-sync.mjs +18 -2
- package/cli/install-coding-agent.mjs +173 -40
- package/cli/mcp/context-sql-runner.mjs +58 -0
- package/cli/mcp/context-store.mjs +670 -0
- package/cli/mcp/knowledge.mjs +333 -0
- package/cli/mcp/memory-facade.mjs +99 -33
- package/cli/openmem-mcp.mjs +3 -0
- package/cli/openmem.mjs +18 -1
- package/cli/schedule.mjs +176 -0
- package/cli/stack.mjs +2 -0
- package/cli/switch-memory-provider.mjs +1 -1
- package/compose.yaml +67 -4
- package/config/hindsight.env.example +2 -0
- package/config/openmem.toml.example +64 -2
- package/docs/architecture.md +35 -9
- package/docs/coding-agent-hooks.md +26 -2
- package/docs/coding-agents.md +27 -7
- package/docs/compatibility.md +80 -0
- package/docs/concepts.md +30 -7
- package/docs/context-management.md +255 -0
- package/docs/context-proxy.md +115 -0
- package/docs/context-research.md +215 -0
- package/docs/context-store.md +277 -0
- package/docs/hindsight.md +5 -2
- package/docs/mcp-setup.md +5 -2
- package/docs/mem0.md +39 -1
- package/docs/memory-providers.md +2 -2
- package/docs/meridian-litellm.md +198 -0
- package/docs/model-configuration.md +22 -0
- package/docs/npx-install.md +6 -4
- package/docs/openmem-config.md +2 -2
- package/docs/quickstart.md +12 -6
- package/docs/setup-wizard.md +5 -4
- package/docs/sources-feeds.md +9 -22
- package/package.json +5 -4
- package/providers/cognee/OPERATOR.md +2 -2
- package/providers/hindsight/lib/clack.mjs +122 -20
- package/providers/hindsight/lib/coding-agent.mjs +1 -1
- package/providers/hindsight/lib/config.mjs +6 -0
- package/providers/hindsight/lib/env-setup.mjs +2 -1
- package/providers/hindsight/lib/litellm-routes.mjs +21 -12
- package/providers/hindsight/lib/meridian.mjs +307 -0
- package/providers/hindsight/lib/model-backend.mjs +38 -9
- package/providers/hindsight/lib/probe.mjs +21 -0
- package/providers/hindsight/lib/stack-env.mjs +5 -12
- package/providers/hindsight/lib/state.mjs +6 -1
- package/providers/hindsight/lib/summary.mjs +16 -2
- package/providers/hindsight/lib/validate.mjs +6 -0
- package/providers/hindsight/wizard.mjs +3 -2
- package/providers/mem0/OPERATOR.md +21 -2
- package/providers/mem0/compose.yaml +24 -2
- package/providers/mem0/lib/coding-agent.mjs +1 -1
- package/providers/mem0/lib/configure.mjs +66 -0
- package/providers/mem0/lib/litellm.mjs +457 -0
- package/providers/mem0/lib/stack-env.mjs +40 -1
- package/providers/mem0/lib/summary.mjs +30 -2
- package/providers/mem0/manifest.json +1 -1
- package/providers/mem0/wizard.mjs +15 -2
- package/proxy/config.yaml +33 -0
- package/proxy/openmem_context/__init__.py +6 -0
- package/proxy/openmem_context/adapters.py +504 -0
- package/proxy/openmem_context/compress.py +53 -0
- package/proxy/openmem_context/core.py +606 -0
- package/proxy/openmem_context/embed.py +212 -0
- package/proxy/openmem_context/recall.py +49 -0
- package/proxy/openmem_context/session.py +70 -0
- package/proxy/openmem_context/sessiondb.py +276 -0
- package/proxy/openmem_context/settings.py +134 -0
- package/proxy/openmem_context/store.py +129 -0
- package/proxy/openmem_context/tokens.py +123 -0
- package/proxy/openmem_context/ttl.py +78 -0
- package/proxy/openmem_context_hook.py +34 -0
- package/scripts/check-pack.mjs +2 -1
- package/scripts/setup.mjs +14 -1
- package/server.json +2 -2
- package/wizard/lib/context-step.mjs +140 -0
- package/wizard/lib/litellm-env.mjs +55 -0
- package/wizard/lib/pm-config.mjs +212 -4
- package/wizard/lib/prompts.mjs +28 -0
- 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
|
|
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 =
|
|
31
|
-
const choice = await h.ask(ctx, "Model backend (1-
|
|
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
|
|
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
|
-
|
|
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("
|
|
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
|
|
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 (
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
# - `
|
|
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
|
|
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 "
|
|
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
|
+
}
|