@bcmyguest/openmem 0.6.3 → 0.9.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 +81 -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 +899 -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 +230 -0
- package/docs/context-proxy.md +115 -0
- package/docs/context-research.md +226 -0
- package/docs/context-store.md +277 -0
- package/docs/evidence-plan.md +216 -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 +608 -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/run-setup.sh +14 -5
- 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
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect-only "Claude subscription (Meridian)" backend: probe a Meridian proxy the user
|
|
3
|
+
* already runs, write the LiteLLM route from docs/meridian-litellm.md, and keep
|
|
4
|
+
* MERIDIAN_API_KEY in litellm.env. The wizard never installs or starts Meridian.
|
|
5
|
+
*/
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fetchStatus, openAiModelIds } from "./probe.mjs";
|
|
10
|
+
import { ensureLitellmEnv } from "./stack-env.mjs";
|
|
11
|
+
import { storedOr } from "./state.mjs";
|
|
12
|
+
|
|
13
|
+
/** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
|
|
14
|
+
|
|
15
|
+
export const MERIDIAN_DEFAULT_PORT = "3456";
|
|
16
|
+
export const MERIDIAN_DEFAULT_BASE = `http://127.0.0.1:${MERIDIAN_DEFAULT_PORT}`;
|
|
17
|
+
/** Guide defaults (docs/meridian-litellm.md#route-file). */
|
|
18
|
+
export const MERIDIAN_RETAIN_DEFAULT = "claude-haiku-5-5";
|
|
19
|
+
export const MERIDIAN_REFLECT_DEFAULT = "claude-sonnet-5-5";
|
|
20
|
+
export const MERIDIAN_TERMS_URL = "https://www.anthropic.com/legal/consumer-terms";
|
|
21
|
+
export const MERIDIAN_GUIDE = "docs/meridian-litellm.md";
|
|
22
|
+
/** litellm.env value when Meridian runs without a key; LiteLLM still needs a non-empty one. */
|
|
23
|
+
export const MERIDIAN_NO_KEY = "placeholder";
|
|
24
|
+
|
|
25
|
+
const BRIDGE_TIMEOUT_MS = 2000;
|
|
26
|
+
const PROBE_TIMEOUT_MS = 5000;
|
|
27
|
+
|
|
28
|
+
/** IPv4 address of the default Docker bridge (docker0), which `host-gateway` resolves to. */
|
|
29
|
+
export function dockerBridgeIp() {
|
|
30
|
+
const nics = os.networkInterfaces().docker0 || [];
|
|
31
|
+
const v4 = nics.find((n) => n.family === "IPv4" || n.family === 4);
|
|
32
|
+
return v4 ? v4.address : "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @param {string} base */
|
|
36
|
+
export function normalizeMeridianBase(base) {
|
|
37
|
+
return base.trim().replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @param {string} hostname */
|
|
41
|
+
function isLoopback(hostname) {
|
|
42
|
+
return hostname === "localhost" || hostname === "[::1]" || hostname.startsWith("127.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @param {URL} u */
|
|
46
|
+
function portOf(u) {
|
|
47
|
+
return u.port || (u.protocol === "https:" ? "443" : "80");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The api_base the LiteLLM container uses: host.docker.internal for a loopback URL,
|
|
52
|
+
* otherwise the URL itself. No /v1: LiteLLM appends /v1/messages.
|
|
53
|
+
* @param {string} hostBase
|
|
54
|
+
*/
|
|
55
|
+
export function containerApiBase(hostBase) {
|
|
56
|
+
const u = new URL(hostBase);
|
|
57
|
+
if (!isLoopback(u.hostname)) return `${u.protocol}//${u.host}`;
|
|
58
|
+
return `http://host.docker.internal:${portOf(u)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* First usable model: a valid candidate, else `preferred` when listed, else the first id
|
|
63
|
+
* matching `family`, else the first id, else `preferred`.
|
|
64
|
+
* @param {string[]} ids ids from Meridian's /v1/models (may be empty)
|
|
65
|
+
* @param {string} preferred
|
|
66
|
+
* @param {RegExp} family
|
|
67
|
+
* @param {string[]} candidates explicit or stored choices, in priority order
|
|
68
|
+
*/
|
|
69
|
+
export function pickModel(ids, preferred, family, candidates = []) {
|
|
70
|
+
const valid = (m) => Boolean(m) && (ids.length ? ids.includes(m) : m.startsWith("claude-"));
|
|
71
|
+
const hit = candidates.find(valid);
|
|
72
|
+
if (hit) return hit;
|
|
73
|
+
if (!ids.length || ids.includes(preferred)) return preferred;
|
|
74
|
+
return ids.find((id) => family.test(id)) || ids[0];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Where an existing Meridian key comes from: the environment, Meridian's own env file
|
|
79
|
+
* (the guide's ~/.config/meridian/meridian.env), or litellm.env.
|
|
80
|
+
* @param {Record<string, any>} ctx
|
|
81
|
+
* @param {Helpers} h
|
|
82
|
+
* @returns {{ key: string, source: string }}
|
|
83
|
+
*/
|
|
84
|
+
export function existingMeridianKey(ctx, h) {
|
|
85
|
+
if (process.env.MERIDIAN_API_KEY) {
|
|
86
|
+
return { key: process.env.MERIDIAN_API_KEY, source: "MERIDIAN_API_KEY from the environment" };
|
|
87
|
+
}
|
|
88
|
+
const fromMeridian = h.envValue(ctx.meridianEnvFile, "MERIDIAN_API_KEY");
|
|
89
|
+
if (fromMeridian) return { key: fromMeridian, source: ctx.meridianEnvFile };
|
|
90
|
+
const fromLitellm = h.envValue(ctx.litellmEnv, "MERIDIAN_API_KEY");
|
|
91
|
+
if (fromLitellm && fromLitellm !== MERIDIAN_NO_KEY) return { key: fromLitellm, source: ctx.litellmEnv };
|
|
92
|
+
return { key: "", source: "" };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Probe Meridian from the host: /health (open), /v1/models (needs the key when set), and
|
|
97
|
+
* /health on the Docker bridge address to tell a loopback-only bind from 0.0.0.0.
|
|
98
|
+
* @param {string} base host URL without /v1
|
|
99
|
+
* @param {string} key MERIDIAN_API_KEY or ""
|
|
100
|
+
* @param {string} bridgeIp docker0 IPv4, or "" when unknown
|
|
101
|
+
*/
|
|
102
|
+
export async function probeMeridian(base, key, bridgeIp) {
|
|
103
|
+
const headers = key ? { "x-api-key": key } : {};
|
|
104
|
+
const health = await fetchStatus(`${base}/health`, { timeoutMs: PROBE_TIMEOUT_MS });
|
|
105
|
+
const models = await fetchStatus(`${base}/v1/models`, { headers, timeoutMs: PROBE_TIMEOUT_MS });
|
|
106
|
+
const u = new URL(base);
|
|
107
|
+
/** @type {"ok"|"unreachable"|"unknown"|"n/a"} */
|
|
108
|
+
let bridge = "n/a";
|
|
109
|
+
const answered = health.status > 0 || models.status > 0;
|
|
110
|
+
if (answered && isLoopback(u.hostname)) {
|
|
111
|
+
if (!bridgeIp) bridge = "unknown";
|
|
112
|
+
else {
|
|
113
|
+
const viaBridge = await fetchStatus(`http://${bridgeIp}:${portOf(u)}/health`, {
|
|
114
|
+
timeoutMs: BRIDGE_TIMEOUT_MS,
|
|
115
|
+
});
|
|
116
|
+
bridge = viaBridge.status > 0 ? "ok" : "unreachable";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
answered,
|
|
121
|
+
healthOk: health.status >= 200 && health.status < 300,
|
|
122
|
+
modelsStatus: models.status,
|
|
123
|
+
ids: models.status === 200 ? openAiModelIds(models.json) : [],
|
|
124
|
+
bridge,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** @param {string[]} names @param {Helpers} h */
|
|
129
|
+
function showModels(names, h) {
|
|
130
|
+
for (const n of names) h.info(` ${n}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* What the probe says about reachability from the LiteLLM container.
|
|
135
|
+
* @param {{ meridianBase: string, apiBase: string, dockerBridgeIp: string, meridianEnvFile: string }} ctx
|
|
136
|
+
* @param {(file: string, key: string) => string} envValue
|
|
137
|
+
* @param {Awaited<ReturnType<typeof probeMeridian>>} probe
|
|
138
|
+
* @returns {{ level: "ok"|"warn"|"info", text: string } | null}
|
|
139
|
+
*/
|
|
140
|
+
export function bridgeReport(ctx, envValue, probe) {
|
|
141
|
+
const u = new URL(ctx.meridianBase);
|
|
142
|
+
const port = portOf(u);
|
|
143
|
+
const bindHint = `Restart Meridian with MERIDIAN_HOST=0.0.0.0 (${MERIDIAN_GUIDE}#run-meridian).`;
|
|
144
|
+
const declaredHost = process.env.MERIDIAN_HOST || envValue(ctx.meridianEnvFile, "MERIDIAN_HOST");
|
|
145
|
+
if (probe.bridge === "ok") {
|
|
146
|
+
return {
|
|
147
|
+
level: "ok",
|
|
148
|
+
text: `Meridian also answers on the Docker bridge address ${ctx.dockerBridgeIp}:${port}; LiteLLM calls ${ctx.apiBase}. A firewall can still block containers; run the guide's smoke test.`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (probe.bridge === "unreachable") {
|
|
152
|
+
return {
|
|
153
|
+
level: "warn",
|
|
154
|
+
text: `Meridian answers on ${u.host} but not on the Docker bridge address ${ctx.dockerBridgeIp}:${port}: it binds 127.0.0.1 only, so the LiteLLM container cannot reach ${ctx.apiBase}. ${bindHint}`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (declaredHost && isLoopback(declaredHost)) {
|
|
158
|
+
return {
|
|
159
|
+
level: "warn",
|
|
160
|
+
text: `MERIDIAN_HOST is ${declaredHost}: the LiteLLM container cannot reach a loopback-only Meridian. ${bindHint}`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (isLoopback(u.hostname)) {
|
|
164
|
+
return {
|
|
165
|
+
level: "info",
|
|
166
|
+
text: `LiteLLM calls Meridian at ${ctx.apiBase}. On Linux, a Meridian bound to 127.0.0.1 (its default) is unreachable from the container; run it with MERIDIAN_HOST=0.0.0.0.`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Interactive and --yes Meridian questions. Fills ctx.meridianBase, ctx.meridianKey,
|
|
174
|
+
* ctx.apiBase, ctx.model and ctx.reflectModel.
|
|
175
|
+
* @param {Record<string, any>} ctx
|
|
176
|
+
* @param {Helpers} h
|
|
177
|
+
*/
|
|
178
|
+
export async function chooseMeridian(ctx, h) {
|
|
179
|
+
h.info(`The wizard does not install or start Meridian; see ${MERIDIAN_GUIDE}.`);
|
|
180
|
+
if (ctx.dockerBridgeIp === undefined) ctx.dockerBridgeIp = dockerBridgeIp();
|
|
181
|
+
const port = h.envValue(ctx.meridianEnvFile, "MERIDIAN_PORT") || MERIDIAN_DEFAULT_PORT;
|
|
182
|
+
const defaultBase =
|
|
183
|
+
process.env.MERIDIAN_BASE ||
|
|
184
|
+
storedOr(ctx, h, "meridian", "MEMORY_MERIDIAN_URL", `http://127.0.0.1:${port}`);
|
|
185
|
+
ctx.meridianBase = normalizeMeridianBase(await h.ask(ctx, "Meridian URL from this host", defaultBase));
|
|
186
|
+
try {
|
|
187
|
+
ctx.apiBase = containerApiBase(ctx.meridianBase);
|
|
188
|
+
} catch {
|
|
189
|
+
h.die(`not a URL: ${ctx.meridianBase}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const existing = existingMeridianKey(ctx, h);
|
|
193
|
+
const typed = await h.ask(
|
|
194
|
+
ctx,
|
|
195
|
+
`MERIDIAN_API_KEY (typed input is visible; blank uses ${existing.source || "no key"})`,
|
|
196
|
+
"",
|
|
197
|
+
);
|
|
198
|
+
ctx.meridianKey = typed || existing.key;
|
|
199
|
+
|
|
200
|
+
const probe = await probeMeridian(ctx.meridianBase, ctx.meridianKey, ctx.dockerBridgeIp);
|
|
201
|
+
if (!probe.answered) {
|
|
202
|
+
h.warn(
|
|
203
|
+
`Meridian is not answering on ${ctx.meridianBase}. Start it first (npm install -g @rynfar/meridian; claude login; meridian), see ${MERIDIAN_GUIDE}. The route uses the guide's default models.`,
|
|
204
|
+
);
|
|
205
|
+
} else {
|
|
206
|
+
h.ok(`Meridian answers on ${ctx.meridianBase}${probe.healthOk ? "" : " (/health not OK)"}`);
|
|
207
|
+
if (probe.modelsStatus === 401 || probe.modelsStatus === 403) {
|
|
208
|
+
h.warn(
|
|
209
|
+
`Meridian rejected ${ctx.meridianKey ? "the key" : "a request without a key"} on /v1/models (HTTP ${probe.modelsStatus}): set MERIDIAN_API_KEY to the key Meridian runs with.`,
|
|
210
|
+
);
|
|
211
|
+
} else if (probe.ids.length) {
|
|
212
|
+
h.ok(`Meridian lists ${probe.ids.length} models`);
|
|
213
|
+
showModels(probe.ids, h);
|
|
214
|
+
} else h.warn(`Meridian returned no model list (HTTP ${probe.modelsStatus}); using the guide's defaults`);
|
|
215
|
+
}
|
|
216
|
+
const bridge = bridgeReport(ctx, h.envValue, probe);
|
|
217
|
+
if (bridge) h[bridge.level](bridge.text);
|
|
218
|
+
if (!ctx.meridianKey) {
|
|
219
|
+
h.warn("No MERIDIAN_API_KEY: with MERIDIAN_HOST=0.0.0.0 anyone on the network can use your subscription.");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const retainDefault = pickModel(probe.ids, MERIDIAN_RETAIN_DEFAULT, /haiku/, [
|
|
223
|
+
process.env.PM_DEFAULT_MODEL,
|
|
224
|
+
storedOr(ctx, h, "meridian", "MEMORY_MODEL", ""),
|
|
225
|
+
]);
|
|
226
|
+
const reflectDefault = pickModel(probe.ids, MERIDIAN_REFLECT_DEFAULT, /sonnet/, [
|
|
227
|
+
process.env.PM_REFLECT_MODEL,
|
|
228
|
+
storedOr(ctx, h, "meridian", "MEMORY_REFLECT_MODEL", ""),
|
|
229
|
+
]);
|
|
230
|
+
ctx.model = await h.ask(ctx, "Meridian retain / consolidate model", retainDefault);
|
|
231
|
+
ctx.reflectModel = await h.ask(ctx, "Meridian reflect model (needs tool calls)", reflectDefault);
|
|
232
|
+
for (const m of [ctx.model, ctx.reflectModel]) {
|
|
233
|
+
if (probe.ids.length && !probe.ids.includes(m)) h.warn(`${m} is not listed by Meridian's /v1/models`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* One alias of the guide's route file.
|
|
239
|
+
* @param {string} alias
|
|
240
|
+
* @param {string} model
|
|
241
|
+
* @param {string} apiBase
|
|
242
|
+
*/
|
|
243
|
+
function renderAlias(alias, model, apiBase) {
|
|
244
|
+
return (
|
|
245
|
+
` - model_name: ${alias}\n` +
|
|
246
|
+
" litellm_params:\n" +
|
|
247
|
+
` model: anthropic/${model}\n` +
|
|
248
|
+
` api_base: ${apiBase}\n` +
|
|
249
|
+
" api_key: os.environ/MERIDIAN_API_KEY\n" +
|
|
250
|
+
" max_tokens: 4096\n" +
|
|
251
|
+
" timeout: 300\n" +
|
|
252
|
+
' additional_drop_params: ["reasoning_effort"]\n'
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The route file from docs/meridian-litellm.md#route-file. The key stays in litellm.env.
|
|
258
|
+
* @param {Record<string, any>} ctx
|
|
259
|
+
*/
|
|
260
|
+
export function renderMeridianRoutes(ctx) {
|
|
261
|
+
const reflect = ctx.reflectModel || ctx.model;
|
|
262
|
+
return (
|
|
263
|
+
"# Written by ./setup: LiteLLM routes for Hindsight, backend Meridian on the host (Claude subscription).\n" +
|
|
264
|
+
`# See ${MERIDIAN_GUIDE}. MERIDIAN_API_KEY lives in ~/.config/hindsight/litellm.env.\n` +
|
|
265
|
+
"# No thinking parameter: Meridian rejects thinking: disabled on Haiku 5.5, so reasoning_effort is dropped.\n" +
|
|
266
|
+
"model_list:\n" +
|
|
267
|
+
renderAlias("memory-retain", ctx.model, ctx.apiBase) +
|
|
268
|
+
renderAlias("memory-reflect", reflect, ctx.apiBase) +
|
|
269
|
+
renderAlias("memory-consolidate", ctx.model, ctx.apiBase) +
|
|
270
|
+
"\ngeneral_settings:\n master_key: os.environ/LITELLM_MASTER_KEY\n"
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Set MERIDIAN_API_KEY in litellm.env (mode 600), creating the file with a master key
|
|
276
|
+
* when it does not exist yet. Never prints the value.
|
|
277
|
+
* @param {Record<string, any>} ctx
|
|
278
|
+
* @param {Helpers} h
|
|
279
|
+
*/
|
|
280
|
+
export function writeMeridianKey(ctx, h) {
|
|
281
|
+
const value = ctx.meridianKey || h.envValue(ctx.litellmEnv, "MERIDIAN_API_KEY") || MERIDIAN_NO_KEY;
|
|
282
|
+
if (/[\r\n]/.test(value)) h.die("MERIDIAN_API_KEY must be one line");
|
|
283
|
+
if (ctx.dry) {
|
|
284
|
+
h.info(`[dry-run] set MERIDIAN_API_KEY in ${ctx.litellmEnv} (mode 600; value not printed)`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
h.ensureConfigDir(ctx);
|
|
288
|
+
ensureLitellmEnv(ctx.litellmEnv);
|
|
289
|
+
const lines = fs.readFileSync(ctx.litellmEnv, "utf8").replace(/\n$/, "").split("\n");
|
|
290
|
+
const line = `MERIDIAN_API_KEY=${value}`;
|
|
291
|
+
const at = lines.findIndex((l) => l.startsWith("MERIDIAN_API_KEY="));
|
|
292
|
+
if (at >= 0 && lines[at] === line) {
|
|
293
|
+
fs.chmodSync(ctx.litellmEnv, 0o600);
|
|
294
|
+
h.ok(`${ctx.litellmEnv} already has MERIDIAN_API_KEY`);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (at >= 0) lines[at] = line;
|
|
298
|
+
else {
|
|
299
|
+
if (value === MERIDIAN_NO_KEY) lines.push("# Meridian runs without MERIDIAN_API_KEY; LiteLLM needs a non-empty value.");
|
|
300
|
+
lines.push(line);
|
|
301
|
+
}
|
|
302
|
+
const tmp = path.join(path.dirname(ctx.litellmEnv), `.litellm.env.${process.pid}`);
|
|
303
|
+
fs.writeFileSync(tmp, `${lines.join("\n")}\n`, { mode: 0o600 });
|
|
304
|
+
fs.chmodSync(tmp, 0o600);
|
|
305
|
+
fs.renameSync(tmp, ctx.litellmEnv);
|
|
306
|
+
h.ok(`set MERIDIAN_API_KEY in ${ctx.litellmEnv} (mode 600)`);
|
|
307
|
+
}
|
|
@@ -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-`.
|