@bcmyguest/openmem 0.0.0-stage → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/openmem/SKILL.md +34 -0
- package/LICENSE +190 -0
- package/README.md +140 -2
- package/bin/hindsight-project-index +530 -0
- package/cli/coding-agent/access-policy.mjs +38 -0
- package/cli/coding-agent/agents.mjs +34 -0
- package/cli/coding-agent/bank-from-cwd.mjs +140 -0
- package/cli/coding-agent/capabilities.mjs +34 -0
- 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 +88 -0
- package/cli/coding-agent/hook-runner.mjs +284 -0
- package/cli/coding-agent/install-access.mjs +119 -0
- 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 +201 -0
- package/cli/coding-agent/opencode-config.mjs +107 -0
- package/cli/coding-agent/opencode-plugin.mjs +132 -0
- 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 +40 -0
- package/cli/coding-agent/session-db.mjs +626 -0
- package/cli/coding-agent/session-facade.mjs +86 -0
- package/cli/coding-agent/toml-edit.mjs +161 -0
- package/cli/cognee-mcp.mjs +260 -0
- package/cli/context-management.mjs +254 -0
- package/cli/feed-sync.mjs +27 -0
- package/cli/install-coding-agent.mjs +298 -0
- package/cli/install-memory-mcp.mjs +426 -0
- package/cli/is-main.mjs +18 -0
- package/cli/mcp/context-sql-runner.mjs +58 -0
- package/cli/mcp/context-store.mjs +670 -0
- package/cli/mcp/context.mjs +61 -0
- package/cli/mcp/export/cognee-dataset-name.mjs +12 -0
- package/cli/mcp/export/cognee.mjs +119 -0
- package/cli/mcp/export/hindsight.mjs +48 -0
- package/cli/mcp/export/index.mjs +26 -0
- package/cli/mcp/export/mem0.mjs +31 -0
- package/cli/mcp/knowledge.mjs +333 -0
- package/cli/mcp/llm-synthesize.mjs +50 -0
- package/cli/mcp/mem0-scope-id.mjs +22 -0
- package/cli/mcp/memory-facade.mjs +521 -0
- package/cli/mcp/memory-reflect-consolidate.mjs +120 -0
- package/cli/mcp/notebooklm-banks.mjs +21 -0
- package/cli/mcp/notebooklm-hindsight-rest.mjs +45 -0
- package/cli/mcp/notebooklm-provider.mjs +225 -0
- package/cli/mcp/pmm-records.mjs +23 -0
- package/cli/mcp/portability.mjs +158 -0
- package/cli/mcp/stdio.mjs +65 -0
- package/cli/mcp/unified-mcp-install.mjs +65 -0
- package/cli/mem0-mcp.mjs +108 -0
- package/cli/notebooklm-mcp.mjs +236 -0
- package/cli/openmem-mcp.mjs +38 -0
- package/cli/openmem.mjs +222 -0
- package/cli/roots.mjs +85 -0
- package/cli/schedule.mjs +176 -0
- package/cli/stack.mjs +438 -0
- package/cli/switch-memory-provider.mjs +358 -0
- package/cli/wiring-report.mjs +125 -0
- package/compose.bench.yaml +12 -0
- package/compose.yaml +227 -0
- package/config/coding-agent.json +13 -0
- package/config/hindsight.env.example +26 -0
- package/config/index.yml +54 -0
- package/config/litellm.lemond.yaml +40 -0
- package/config/litellm.ollama.yaml +33 -0
- package/config/notebooklm.yml +33 -0
- package/config/openmem.toml.example +124 -0
- package/docs/OPENMEM.md +23 -0
- package/docs/architecture.md +245 -0
- package/docs/assets/README.md +10 -0
- package/docs/assets/setup-demo.gif +0 -0
- package/docs/coding-agent-hooks.md +75 -0
- package/docs/coding-agent-scopes.md +22 -0
- package/docs/coding-agents.md +71 -0
- package/docs/cognee.md +37 -0
- package/docs/compatibility.md +80 -0
- package/docs/concepts.md +135 -0
- 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 +77 -0
- package/docs/index-sources-roadmap.md +38 -0
- package/docs/index-sources.md +39 -0
- package/docs/indexer-sinks.md +122 -0
- package/docs/indexing.md +57 -0
- package/docs/manual-stack-setup.md +74 -0
- package/docs/mcp-install-guide.md +4 -0
- package/docs/mcp-setup.md +104 -0
- package/docs/mem0.md +141 -0
- package/docs/memory-portability.md +73 -0
- package/docs/memory-providers.md +246 -0
- package/docs/meridian-litellm.md +198 -0
- package/docs/model-configuration.md +106 -0
- package/docs/notebooklm.md +83 -0
- package/docs/npx-install.md +172 -0
- package/docs/openmem-config.md +194 -0
- package/docs/provider-wizard-hooks.md +222 -0
- package/docs/quickstart.md +107 -0
- package/docs/setup-wizard.md +117 -0
- package/docs/sources-feeds.md +39 -0
- package/docs/sources-readwise.md +23 -0
- package/docs/sources-zotero.md +22 -0
- package/glama.json +4 -0
- package/indexer/Dockerfile +38 -0
- package/indexer/code_outline.py +146 -0
- package/indexer/feed_sync.py +107 -0
- package/indexer/index_config.py +290 -0
- package/indexer/pytest.ini +5 -0
- package/indexer/sinks/__init__.py +148 -0
- package/indexer/sinks/_http.py +44 -0
- package/indexer/sinks/cognee.py +329 -0
- package/indexer/sinks/hindsight.py +107 -0
- package/indexer/sinks/mem0.py +247 -0
- package/indexer/watch.py +240 -0
- package/package.json +96 -4
- package/providers/_template/compose.yaml +52 -0
- package/providers/cognee/OPERATOR.md +82 -0
- package/providers/cognee/compose.yaml +89 -0
- package/providers/cognee/lib/clack.mjs +157 -0
- package/providers/cognee/lib/coding-agent.mjs +26 -0
- package/providers/cognee/lib/cognee-mcp-install.mjs +153 -0
- package/providers/cognee/lib/config.mjs +98 -0
- package/providers/cognee/lib/env-file.mjs +194 -0
- package/providers/cognee/lib/env-setup.mjs +83 -0
- package/providers/cognee/lib/llm-env.mjs +382 -0
- package/providers/cognee/lib/ports.mjs +25 -0
- package/providers/cognee/lib/smoke.mjs +246 -0
- package/providers/cognee/lib/stack-env.mjs +134 -0
- package/providers/cognee/lib/state.mjs +101 -0
- package/providers/cognee/lib/summary.mjs +56 -0
- package/providers/cognee/lib/validate.mjs +97 -0
- package/providers/cognee/manifest.json +56 -0
- package/providers/cognee/wizard.mjs +134 -0
- package/providers/graphiti/manifest.json +15 -0
- package/providers/hindsight/lib/clack.mjs +347 -0
- package/providers/hindsight/lib/coding-agent.mjs +204 -0
- package/providers/hindsight/lib/config.mjs +79 -0
- package/providers/hindsight/lib/env-setup.mjs +72 -0
- package/providers/hindsight/lib/litellm-routes.mjs +196 -0
- package/providers/hindsight/lib/meridian.mjs +307 -0
- package/providers/hindsight/lib/model-backend.mjs +243 -0
- package/providers/hindsight/lib/notebooklm-mcp-install.mjs +127 -0
- package/providers/hindsight/lib/ollama-compose.mjs +51 -0
- package/providers/hindsight/lib/ports.mjs +30 -0
- package/providers/hindsight/lib/probe.mjs +54 -0
- package/providers/hindsight/lib/stack-env.mjs +125 -0
- package/providers/hindsight/lib/state.mjs +80 -0
- package/providers/hindsight/lib/summary.mjs +57 -0
- package/providers/hindsight/lib/validate.mjs +81 -0
- package/providers/hindsight/manifest.json +58 -0
- package/providers/hindsight/wizard.mjs +150 -0
- package/providers/letta/manifest.json +29 -0
- package/providers/mem0/OPERATOR.md +143 -0
- package/providers/mem0/compose.yaml +79 -0
- package/providers/mem0/lib/coding-agent.mjs +58 -0
- package/providers/mem0/lib/config.mjs +45 -0
- package/providers/mem0/lib/configure.mjs +201 -0
- package/providers/mem0/lib/dotenv.mjs +71 -0
- package/providers/mem0/lib/env-file.mjs +110 -0
- package/providers/mem0/lib/http.mjs +69 -0
- package/providers/mem0/lib/litellm.mjs +457 -0
- package/providers/mem0/lib/mcp-install.mjs +165 -0
- package/providers/mem0/lib/secret.mjs +30 -0
- package/providers/mem0/lib/smoke.mjs +179 -0
- package/providers/mem0/lib/stack-env.mjs +255 -0
- package/providers/mem0/lib/state.mjs +67 -0
- package/providers/mem0/lib/summary.mjs +88 -0
- package/providers/mem0/lib/url.mjs +106 -0
- package/providers/mem0/lib/validate.mjs +158 -0
- package/providers/mem0/manifest.json +47 -0
- package/providers/mem0/wizard.mjs +146 -0
- package/providers/zep/manifest.json +29 -0
- 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 +219 -0
- package/scripts/gen-masks.mjs +83 -0
- package/scripts/gen-masks.sh +4 -0
- package/scripts/promotion-provider-smoke.mjs +34 -0
- package/scripts/run-setup.sh +30 -0
- package/scripts/setup.mjs +107 -0
- package/scripts/setup.sh +5 -0
- package/scripts/spin-up.sh +45 -0
- package/scripts/wizard.sh +5 -0
- package/server.json +34 -0
- package/sync/Dockerfile +27 -0
- package/sync/memory_backend.py +87 -0
- package/sync/notebooklm_sync.py +664 -0
- package/wizard/bin/pm.mjs +97 -0
- package/wizard/bin/setup.mjs +33 -0
- package/wizard/lib/compose-version.mjs +32 -0
- package/wizard/lib/compose.mjs +70 -0
- package/wizard/lib/context-step.mjs +140 -0
- package/wizard/lib/curl.mjs +103 -0
- package/wizard/lib/docker-build.mjs +148 -0
- package/wizard/lib/exec.mjs +41 -0
- package/wizard/lib/indexer.mjs +6 -0
- package/wizard/lib/litellm-env.mjs +55 -0
- package/wizard/lib/load-machine-env.mjs +4 -0
- package/wizard/lib/log.mjs +32 -0
- package/wizard/lib/notebooklm-agent-access.mjs +92 -0
- package/wizard/lib/notebooklm.mjs +214 -0
- package/wizard/lib/parse-args.mjs +87 -0
- package/wizard/lib/paths.mjs +39 -0
- package/wizard/lib/pm-config.mjs +371 -0
- package/wizard/lib/ports.mjs +94 -0
- package/wizard/lib/prompts.mjs +85 -0
- package/wizard/lib/provider-wizard/helpers.mjs +98 -0
- package/wizard/lib/provider-wizard/run-steps.mjs +129 -0
- package/wizard/lib/providers/registry.mjs +262 -0
- package/wizard/lib/run-clack.mjs +108 -0
- package/wizard/lib/run-wizard.mjs +750 -0
- package/wizard/lib/validate-setup.mjs +62 -0
- package/wizard/lib/wizard-action.mjs +34 -0
- package/wizard/lib/wizard-conf.mjs +71 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke test for a mem0 REST server (replaces the manifest HTTP smoke, which has no header
|
|
3
|
+
* support): GET /docs, POST /memories (verbatim, infer false), POST /search must find the
|
|
4
|
+
* fact, then delete what was created. Cleanup runs on every outcome.
|
|
5
|
+
*/
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { readKey } from "./env-file.mjs";
|
|
8
|
+
import { clean, describeError, redact, request } from "./http.mjs";
|
|
9
|
+
import { capturedKey } from "./secret.mjs";
|
|
10
|
+
import { ensureState, resolveEnvFile } from "./state.mjs";
|
|
11
|
+
import { displayUrl, resolveApiUrl, urlProblem } from "./url.mjs";
|
|
12
|
+
|
|
13
|
+
/** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
|
|
14
|
+
|
|
15
|
+
export const SMOKE_USER = "wizard-smoke";
|
|
16
|
+
export const SMOKE_RUN = "wizard-smoke-doc";
|
|
17
|
+
export const SMOKE_FACT = "Wizard smoke test: the project codename is heron.";
|
|
18
|
+
|
|
19
|
+
const RETAIN_BODY = {
|
|
20
|
+
messages: [{ role: "user", content: SMOKE_FACT }],
|
|
21
|
+
user_id: SMOKE_USER,
|
|
22
|
+
agent_id: "openmem-wizard",
|
|
23
|
+
run_id: SMOKE_RUN,
|
|
24
|
+
infer: false,
|
|
25
|
+
};
|
|
26
|
+
const SEARCH_BODY = {
|
|
27
|
+
query: "What is the project codename?",
|
|
28
|
+
filters: { user_id: SMOKE_USER },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
class SmokeFailure extends Error {}
|
|
32
|
+
|
|
33
|
+
/** Server ids are UUIDs; anything else is never put in a URL or printed. */
|
|
34
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
35
|
+
|
|
36
|
+
/** @param {any} json @returns {string[]} */
|
|
37
|
+
function memoryIds(json) {
|
|
38
|
+
const list = Array.isArray(json) ? json : Array.isArray(json?.results) ? json.results : json?.id ? [json] : [];
|
|
39
|
+
return list.map((r) => r?.id).filter((id) => typeof id === "string" && SAFE_ID.test(id));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @param {any} json @returns {string} */
|
|
43
|
+
function memoryText(json) {
|
|
44
|
+
const list = Array.isArray(json) ? json : Array.isArray(json?.results) ? json.results : [];
|
|
45
|
+
return list.map((r) => String(r?.memory ?? r?.text ?? "")).join("\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The key the indexer would send: the env file first (the indexer reads only that), then the
|
|
50
|
+
* key captured from the environment, then process.env (a caller that skipped init).
|
|
51
|
+
* @param {Record<string, any>} ctx
|
|
52
|
+
* @param {Helpers} h
|
|
53
|
+
*/
|
|
54
|
+
function smokeKey(ctx, h) {
|
|
55
|
+
const fromFile = readKey(resolveEnvFile(ctx, h));
|
|
56
|
+
if (fromFile) return { key: fromFile, fromFile: true };
|
|
57
|
+
return { key: capturedKey() || (process.env.MEM0_API_KEY || "").trim(), fromFile: false };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {Record<string, any>} ctx
|
|
62
|
+
* @param {Helpers} h
|
|
63
|
+
*/
|
|
64
|
+
export async function smoke(ctx, h) {
|
|
65
|
+
ensureState(ctx);
|
|
66
|
+
const raw = resolveApiUrl(ctx, h);
|
|
67
|
+
const issue = urlProblem(raw);
|
|
68
|
+
if (issue) {
|
|
69
|
+
h.problem(ctx, `mem0 API URL: ${issue}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const base = displayUrl(raw);
|
|
73
|
+
|
|
74
|
+
if (ctx.dry) {
|
|
75
|
+
h.info(`[dry-run] mem0 smoke test against ${base}:`);
|
|
76
|
+
h.info("[dry-run] 1. GET /docs must answer 2xx");
|
|
77
|
+
h.info(`[dry-run] 2. POST /memories one fact (infer false, user_id ${SMOKE_USER}, run_id ${SMOKE_RUN})`);
|
|
78
|
+
h.info("[dry-run] 3. POST /search the codename; the result must contain 'heron'");
|
|
79
|
+
h.info("[dry-run] 4. DELETE /memories/{id} for every memory created in step 2");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Everything that can throw sits inside the try, so cleanup always runs and an error never
|
|
84
|
+
// carries the key (messages are fixed text plus a short, key-free reason).
|
|
85
|
+
let key = "";
|
|
86
|
+
/** @type {string[]} */
|
|
87
|
+
let ids = [];
|
|
88
|
+
let failure = "";
|
|
89
|
+
/** @param {{ status: number, text: string }} res @param {string} what */
|
|
90
|
+
const failed = (res, what) => {
|
|
91
|
+
if (res.status === 401 || res.status === 403) {
|
|
92
|
+
return new SmokeFailure(
|
|
93
|
+
`${what}: the mem0 server answered ${res.status}. Put MEM0_API_KEY in ${resolveEnvFile(ctx, h)} ` +
|
|
94
|
+
"(mode 600) or run the server with AUTH_DISABLED, then rerun",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const body = clean(redact(res.text, key));
|
|
98
|
+
return new SmokeFailure(`${what}: the mem0 server answered ${res.status}${body ? ` (${body})` : ""}`);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
h.info(`smoke test: mem0 at ${base}`);
|
|
102
|
+
try {
|
|
103
|
+
let fromFile = true;
|
|
104
|
+
try {
|
|
105
|
+
({ key, fromFile } = smokeKey(ctx, h));
|
|
106
|
+
} catch (e) {
|
|
107
|
+
throw new SmokeFailure(
|
|
108
|
+
`cannot read the mem0 env file ${resolveEnvFile(ctx, h)} (${clean(/** @type {any} */ (e)?.code || "error", 40)})`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (key && !fromFile) {
|
|
112
|
+
h.warn(
|
|
113
|
+
`the API key comes from the environment only; the indexer reads ${path.basename(resolveEnvFile(ctx, h))} ` +
|
|
114
|
+
"(put MEM0_API_KEY there)",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 1. Reachability (open route, no key).
|
|
119
|
+
const docs = await request(base, "GET", "/docs", { timeoutMs: 10000 });
|
|
120
|
+
if (!docs.ok) throw failed(docs, "GET /docs");
|
|
121
|
+
h.ok("GET /docs answered");
|
|
122
|
+
|
|
123
|
+
// 2. Retain verbatim. infer:false must return at least one results[].id (same rule as the sink).
|
|
124
|
+
const retain = await request(base, "POST", "/memories", { key, body: RETAIN_BODY, timeoutMs: 120000 });
|
|
125
|
+
if (!retain.ok) throw failed(retain, "POST /memories");
|
|
126
|
+
ids = memoryIds(retain.json);
|
|
127
|
+
if (!ids.length) {
|
|
128
|
+
throw new SmokeFailure(
|
|
129
|
+
"POST /memories returned no memory ids (empty results); the server must store with infer:false and return results[].id",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
h.ok(`stored the smoke fact (${ids.length} memory)`);
|
|
133
|
+
|
|
134
|
+
// 3. Search must find it.
|
|
135
|
+
const search = await request(base, "POST", "/search", { key, body: SEARCH_BODY, timeoutMs: 120000 });
|
|
136
|
+
if (!search.ok) throw failed(search, "POST /search");
|
|
137
|
+
if (!/heron/i.test(memoryText(search.json))) {
|
|
138
|
+
throw new SmokeFailure(
|
|
139
|
+
"POST /search did not return the smoke fact: check the mem0 server's embedder key and provider " +
|
|
140
|
+
"configuration, and its logs",
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
h.ok("POST /search found the smoke fact");
|
|
144
|
+
} catch (e) {
|
|
145
|
+
failure = e instanceof SmokeFailure ? e.message : `mem0 request failed (${clean(redact(describeError(e), key), 80)})`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 4. Cleanup, whatever happened above: delete this run's memories and any the wizard's fixed
|
|
149
|
+
// user_id/run_id left behind earlier (a failed cleanup, an interrupted run).
|
|
150
|
+
try {
|
|
151
|
+
const found = new Set(ids);
|
|
152
|
+
try {
|
|
153
|
+
const listed = await request(
|
|
154
|
+
base,
|
|
155
|
+
"GET",
|
|
156
|
+
`/memories?user_id=${SMOKE_USER}&run_id=${SMOKE_RUN}&top_k=1000`,
|
|
157
|
+
{ key, timeoutMs: 30000 },
|
|
158
|
+
);
|
|
159
|
+
if (listed.ok) for (const id of memoryIds(listed.json)) found.add(id);
|
|
160
|
+
} catch {
|
|
161
|
+
// The lookup is best effort; this run's own ids are still deleted below.
|
|
162
|
+
}
|
|
163
|
+
let removed = 0;
|
|
164
|
+
for (const id of found) {
|
|
165
|
+
try {
|
|
166
|
+
const res = await request(base, "DELETE", `/memories/${encodeURIComponent(id)}`, { key, timeoutMs: 30000 });
|
|
167
|
+
if (res.ok || res.status === 404) removed++;
|
|
168
|
+
else h.warn(`could not delete smoke memory ${clean(id, 128)}: the server answered ${res.status}`);
|
|
169
|
+
} catch (e) {
|
|
170
|
+
h.warn(`could not delete smoke memory ${clean(id, 128)}: ${clean(redact(describeError(e), key), 80)}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (found.size && removed === found.size) h.ok("deleted the smoke memories");
|
|
174
|
+
} catch (e) {
|
|
175
|
+
h.warn(`smoke cleanup failed: ${clean(redact(describeError(e), key), 80)}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (failure) h.die(failure);
|
|
179
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mem0's contribution to scripts/setup.mjs: the stack .env entries that point the indexer at an
|
|
3
|
+
* existing mem0 server, and the end-of-run notices about the indexer's API key file and about
|
|
4
|
+
* a loopback-only server. Runs as a plain Node script step (no wizard context), so it only reads
|
|
5
|
+
* process environment and the paths it is given. It reads the key file only to see whether a key
|
|
6
|
+
* exists (the same parse as docker compose) and never prints or returns the key.
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { ensureLitellmEnv } from "../../../wizard/lib/litellm-env.mjs";
|
|
12
|
+
import { envValue } from "../../../wizard/lib/paths.mjs";
|
|
13
|
+
import { parseEnvValue } from "./dotenv.mjs";
|
|
14
|
+
import { envFileProblem } from "./state.mjs";
|
|
15
|
+
|
|
16
|
+
/** Hostnames that mean "this machine" on the host and so must become the host gateway in a container. */
|
|
17
|
+
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "0.0.0.0", "[::1]"]);
|
|
18
|
+
|
|
19
|
+
/** Appended to every URL error. The messages never echo the URL, which may hold a secret. */
|
|
20
|
+
const SECRET_HINT = "put MEM0_API_KEY in the mem0 env file instead of the URL";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse and validate the mem0 server URL as the operator's host sees it.
|
|
24
|
+
* @param {string} hostUrl
|
|
25
|
+
* @returns {URL}
|
|
26
|
+
*/
|
|
27
|
+
function parseHostUrl(hostUrl) {
|
|
28
|
+
const text = String(hostUrl ?? "").trim();
|
|
29
|
+
// The URL parser silently drops tabs and newlines, so check the raw text: a newline would
|
|
30
|
+
// otherwise let one value write a second KEY=value line into the stack .env.
|
|
31
|
+
if (/[\x00-\x20\x7f]/.test(text)) {
|
|
32
|
+
throw new Error(`MEM0_API_URL must not contain spaces or control characters; ${SECRET_HINT}`);
|
|
33
|
+
}
|
|
34
|
+
if (text.includes("$")) {
|
|
35
|
+
throw new Error(`MEM0_API_URL must not contain '$' (compose would interpolate it); ${SECRET_HINT}`);
|
|
36
|
+
}
|
|
37
|
+
let url;
|
|
38
|
+
try {
|
|
39
|
+
url = new URL(text);
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error("MEM0_API_URL is not a valid URL (expected http://host:port or https://host:port)");
|
|
42
|
+
}
|
|
43
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
44
|
+
throw new Error(`MEM0_API_URL must use http or https (got: ${url.protocol.replace(":", "")})`);
|
|
45
|
+
}
|
|
46
|
+
if (url.username || url.password) {
|
|
47
|
+
// The stack .env is world-readable; secrets belong in the mode 600 env file.
|
|
48
|
+
throw new Error(`MEM0_API_URL must not contain credentials; ${SECRET_HINT}`);
|
|
49
|
+
}
|
|
50
|
+
if (url.search || url.hash) {
|
|
51
|
+
throw new Error(`MEM0_API_URL must not contain a query or fragment; ${SECRET_HINT}`);
|
|
52
|
+
}
|
|
53
|
+
return url;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The normalized host-view URL (scheme, host, port and path; no trailing slash). This is what
|
|
58
|
+
* the stack .env stores as MEM0_API_URL, never the raw string.
|
|
59
|
+
* @param {string} hostUrl
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
export function normalizeHostUrl(hostUrl) {
|
|
63
|
+
return parseHostUrl(hostUrl).toString().replace(/\/+$/, "");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The server URL as the indexer container sees it: loopback hosts become host.docker.internal;
|
|
68
|
+
* scheme, port and path are kept and a trailing slash is removed.
|
|
69
|
+
* @param {string} hostUrl URL as the operator's host sees it (for example http://127.0.0.1:18888)
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
export function indexerUrl(hostUrl) {
|
|
73
|
+
const url = parseHostUrl(hostUrl);
|
|
74
|
+
if (LOOPBACK_HOSTS.has(url.hostname)) url.hostname = "host.docker.internal";
|
|
75
|
+
return url.toString().replace(/\/+$/, "");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Absolute path of the shipped LiteLLM config under the code root. */
|
|
79
|
+
function shippedLitellmConfig() {
|
|
80
|
+
const moduleRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
81
|
+
const root = path.resolve(process.env.PM_PACKAGE_ROOT || moduleRoot);
|
|
82
|
+
const file = path.join(root, "config", "litellm.lemond.yaml");
|
|
83
|
+
if (!fs.existsSync(file)) {
|
|
84
|
+
throw new Error("shipped config/litellm.lemond.yaml missing: broken install");
|
|
85
|
+
}
|
|
86
|
+
return file;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The env file path for a custom location: MEM0_ENV_FILE from the process environment, with a
|
|
91
|
+
* leading ~ expanded, or "" when the process does not set one.
|
|
92
|
+
* @param {NodeJS.ProcessEnv} env
|
|
93
|
+
* @param {string} [home]
|
|
94
|
+
*/
|
|
95
|
+
function customEnvFile(env, home) {
|
|
96
|
+
const raw = (env.MEM0_ENV_FILE || "").trim();
|
|
97
|
+
if (!raw) return "";
|
|
98
|
+
if (raw === "~" || raw.startsWith("~/")) return path.join(home || env.HOME || "", raw.slice(2));
|
|
99
|
+
return raw;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check the stack LiteLLM option's inputs and make sure litellm.env has a master key.
|
|
104
|
+
* @param {{ home?: string, env: NodeJS.ProcessEnv }} opts
|
|
105
|
+
* @returns {{ config: string, port: string }}
|
|
106
|
+
*/
|
|
107
|
+
function litellmOption({ home, env }) {
|
|
108
|
+
const config = (env.MEM0_LITELLM_CONFIG || "").trim() || path.join(home || "", ".config/personal-memory/mem0-litellm.yaml");
|
|
109
|
+
if (!path.isAbsolute(config) || /[\x00-\x20\x7f$"'`\\]/.test(config)) {
|
|
110
|
+
throw new Error("MEM0_LITELLM_CONFIG must be an absolute path without spaces, quotes, '$' or backslashes");
|
|
111
|
+
}
|
|
112
|
+
if (!fs.existsSync(config)) throw new Error(`${config} is missing: rerun ./setup to write the mem0 LiteLLM routes`);
|
|
113
|
+
const port = (env.MEM0_LITELLM_PORT || "4000").trim();
|
|
114
|
+
if (!/^\d+$/.test(port) || Number(port) < 1 || Number(port) > 65535) {
|
|
115
|
+
throw new Error("MEM0_LITELLM_PORT must be a port number (1-65535)");
|
|
116
|
+
}
|
|
117
|
+
const litellmEnv = (env.LITELLM_ENV_FILE || "").trim() || path.join(home || "", ".config/hindsight/litellm.env");
|
|
118
|
+
if (ensureLitellmEnv(litellmEnv)) console.log(`created ${litellmEnv} (mode 600)`);
|
|
119
|
+
const key = envValue(litellmEnv, "LITELLM_MASTER_KEY");
|
|
120
|
+
if (!/^sk-\S+$/.test(key)) {
|
|
121
|
+
throw new Error(`LITELLM_MASTER_KEY=sk-... is missing from ${litellmEnv}; the loopback LiteLLM refuses to start without it`);
|
|
122
|
+
}
|
|
123
|
+
return { config, port };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {{ repo: string, home?: string, env: NodeJS.ProcessEnv }} opts
|
|
128
|
+
* @returns {{ entries: [string, string][], drop: string[], report: string[] }}
|
|
129
|
+
* entries: KEY=value lines to add to the stack .env; drop: keys to remove first;
|
|
130
|
+
* report: KEY=value strings echoed in the "wrote .env" line
|
|
131
|
+
*/
|
|
132
|
+
export function prepareStackEnv({ repo, home, env }) {
|
|
133
|
+
const stackEnv = path.join(repo, ".env");
|
|
134
|
+
const rawUrl = (env.MEM0_API_URL || envValue(stackEnv, "MEM0_API_URL")).trim();
|
|
135
|
+
if (!rawUrl) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
"MEM0_API_URL is required (the mem0 server URL as this host sees it, e.g. http://127.0.0.1:18888); " +
|
|
138
|
+
"the wizard sets it before running setup",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const hostUrl = normalizeHostUrl(rawUrl);
|
|
142
|
+
const containerUrl = indexerUrl(rawUrl);
|
|
143
|
+
|
|
144
|
+
// compose.yaml interpolates every service before it filters by profile, and `litellm` mounts
|
|
145
|
+
// ${LITELLM_CONFIG:?...}. litellm is gated by the profile "hindsight", so under the mem0 profile it never
|
|
146
|
+
// runs and the value is inert; it only has to be set (to an existing file) for `docker compose config`
|
|
147
|
+
// and `up` to get past interpolation. Precedence: the operator's process env, then a non-empty value
|
|
148
|
+
// already in the stack .env, then the shipped config. LITELLM_CONFIG is always dropped and rewritten, which
|
|
149
|
+
// also removes an empty `LITELLM_CONFIG=` line that would still fail the `:?` check.
|
|
150
|
+
//
|
|
151
|
+
// With the stack LiteLLM option on (MEM0_LITELLM=1, set by the wizard), litellm runs under the profile "litellm":
|
|
152
|
+
// LITELLM_CONFIG is the mem0 route file, litellm.env must hold an sk- master key (created here when missing, the
|
|
153
|
+
// same file and mechanism as Hindsight), and MEM0_LITELLM_PORT is the loopback port.
|
|
154
|
+
const litellm = env.MEM0_LITELLM === "1" ? litellmOption({ home, env }) : null;
|
|
155
|
+
const litellmConfig =
|
|
156
|
+
litellm?.config ||
|
|
157
|
+
(env.LITELLM_CONFIG || "").trim() ||
|
|
158
|
+
envValue(stackEnv, "LITELLM_CONFIG").trim() ||
|
|
159
|
+
shippedLitellmConfig();
|
|
160
|
+
|
|
161
|
+
const profiles = ["mem0"];
|
|
162
|
+
if (litellm) profiles.push("litellm");
|
|
163
|
+
const notebookYml = path.join(home, ".config/hindsight/notebooklm.yml");
|
|
164
|
+
if (fs.existsSync(notebookYml) && /^enabled:\s*true\b/m.test(fs.readFileSync(notebookYml, "utf8"))) {
|
|
165
|
+
profiles.push("notebooklm");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** @type {[string, string][]} */
|
|
169
|
+
const entries = [
|
|
170
|
+
["MEM0_API_URL", hostUrl],
|
|
171
|
+
["MEM0_INDEXER_URL", containerUrl],
|
|
172
|
+
["MEMORY_API_URL", containerUrl],
|
|
173
|
+
// Like Hindsight, name the profile in the stack .env so a plain `docker compose config` agrees with the wizard.
|
|
174
|
+
["COMPOSE_PROFILES", profiles.join(",")],
|
|
175
|
+
["MEMORY_SINK", "mem0"],
|
|
176
|
+
["LITELLM_CONFIG", litellmConfig],
|
|
177
|
+
];
|
|
178
|
+
const drop = ["MEM0_API_URL", "MEM0_INDEXER_URL", "COMPOSE_PROFILES", "LITELLM_CONFIG", "MEMORY_API_URL", "MEMORY_SINK"];
|
|
179
|
+
const report = [`MEM0_INDEXER_URL=${containerUrl}`, `COMPOSE_PROFILES=${profiles.join(",")}`, `LITELLM_CONFIG=${litellmConfig}`];
|
|
180
|
+
if (litellm) {
|
|
181
|
+
entries.push(["MEM0_LITELLM_PORT", litellm.port]);
|
|
182
|
+
drop.push("MEM0_LITELLM_PORT");
|
|
183
|
+
report.push(`MEM0_LITELLM_PORT=${litellm.port}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// A custom key file location is a path, not a secret: persist it so a hand-run `docker compose`
|
|
187
|
+
// and `./pm up` read the same env_file as this run. With no custom path in the process
|
|
188
|
+
// environment an existing line is left alone (the wizard reads it back too).
|
|
189
|
+
const envFile = customEnvFile(env, home);
|
|
190
|
+
if (envFile) {
|
|
191
|
+
const problem = envFileProblem(envFile);
|
|
192
|
+
if (problem) throw new Error(`MEM0_ENV_FILE: ${problem}`);
|
|
193
|
+
entries.push(["MEM0_ENV_FILE", envFile]);
|
|
194
|
+
drop.push("MEM0_ENV_FILE");
|
|
195
|
+
report.push(`MEM0_ENV_FILE=${envFile}`);
|
|
196
|
+
}
|
|
197
|
+
return { entries, drop, report };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Printed at the end of setup: the indexer's mem0 key file is missing, has no key or is too open,
|
|
202
|
+
* and the server URL points at a loopback address the container may not reach.
|
|
203
|
+
* @param {{ repo?: string, home: string, env: NodeJS.ProcessEnv }} opts
|
|
204
|
+
* @returns {string[]}
|
|
205
|
+
*/
|
|
206
|
+
export function setupNotices({ repo, home, env }) {
|
|
207
|
+
const stackEnvFile = repo ? path.join(repo, ".env") : "";
|
|
208
|
+
const configured = customEnvFile(env, home) || (stackEnvFile ? customEnvFile({ MEM0_ENV_FILE: envValue(stackEnvFile, "MEM0_ENV_FILE") }, home) : "");
|
|
209
|
+
const file = configured || path.join(home, ".config/personal-memory/mem0.env");
|
|
210
|
+
// Same parse as docker compose: an empty, commented or unterminated value is no key.
|
|
211
|
+
const hasKey = fs.existsSync(file) && parseEnvValue(fs.readFileSync(file, "utf8"), "MEM0_API_KEY") !== "";
|
|
212
|
+
/** @type {string[]} */
|
|
213
|
+
const notes = [];
|
|
214
|
+
if (!hasKey) {
|
|
215
|
+
notes.push(
|
|
216
|
+
`NOTE: mem0 needs an API key for the indexer: put MEM0_API_KEY=... in ${file} (mode 600).`,
|
|
217
|
+
" Not needed when the server runs with AUTH_DISABLED.",
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (fs.existsSync(file)) {
|
|
221
|
+
const mode = fs.statSync(file).mode & 0o777;
|
|
222
|
+
if (mode & 0o077) {
|
|
223
|
+
notes.push(
|
|
224
|
+
`NOTE: ${file} is readable by group or others (mode ${mode.toString(8)}). Run: chmod 600 ${file}`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const dir = path.dirname(file);
|
|
228
|
+
const dirMode = fs.statSync(dir).mode & 0o777;
|
|
229
|
+
if (dirMode & 0o077) {
|
|
230
|
+
notes.push(`NOTE: ${dir} is accessible to group or others (mode ${dirMode.toString(8)}). Run: chmod 700 ${dir}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const rawUrl = (env.MEM0_API_URL || (stackEnvFile ? envValue(stackEnvFile, "MEM0_API_URL") : "")).trim();
|
|
235
|
+
if (rawUrl) {
|
|
236
|
+
try {
|
|
237
|
+
const url = parseHostUrl(rawUrl);
|
|
238
|
+
if (LOOPBACK_HOSTS.has(url.hostname)) {
|
|
239
|
+
notes.push(
|
|
240
|
+
"NOTE: MEM0_API_URL is a loopback address; the indexer container reaches it as host.docker.internal.",
|
|
241
|
+
" On Linux a mem0 server bound only to 127.0.0.1 is NOT reachable that way: bind it to 0.0.0.0",
|
|
242
|
+
" or the docker bridge IP, or use a hostname in MEM0_API_URL.",
|
|
243
|
+
);
|
|
244
|
+
if (url.protocol === "https:") {
|
|
245
|
+
notes.push(
|
|
246
|
+
" An https loopback URL becomes host.docker.internal, which will not match the server certificate name.",
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
// prepareStackEnv already rejected an unusable URL; nothing to add here.
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return notes;
|
|
255
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mem0 wizard state on the context object, the default URL, and the API key file path.
|
|
3
|
+
* mem0 v1 is URL-only: the operator runs a mem0 REST server and this stack points the
|
|
4
|
+
* indexer at it.
|
|
5
|
+
*/
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
/** A stock mem0 server publishes 8888, the Hindsight default; this repo avoids it. */
|
|
9
|
+
export const DEFAULT_API_URL = "http://127.0.0.1:18888";
|
|
10
|
+
|
|
11
|
+
/** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Fill the mem0 fields of the wizard context. Idempotent: a field that is already set
|
|
15
|
+
* (TOML mapping in `init`, a test) is kept.
|
|
16
|
+
* @param {Record<string, any>} ctx
|
|
17
|
+
*/
|
|
18
|
+
export function ensureState(ctx) {
|
|
19
|
+
const state = (ctx.mem0 ||= {});
|
|
20
|
+
const defaults = {
|
|
21
|
+
// [memory.mem0] api_url / env_file, filled by the init hook.
|
|
22
|
+
apiUrl: "",
|
|
23
|
+
tomlEnvFile: "",
|
|
24
|
+
// wizard.conf value, filled by the hydrate hook.
|
|
25
|
+
savedUrl: "",
|
|
26
|
+
// The validated host-view URL this run uses, set by resolvePorts / configureModelRouting.
|
|
27
|
+
url: "",
|
|
28
|
+
};
|
|
29
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
30
|
+
if (state[key] === undefined) state[key] = value;
|
|
31
|
+
}
|
|
32
|
+
return state;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @param {string} home */
|
|
36
|
+
export function defaultEnvFile(home) {
|
|
37
|
+
return path.join(home || "", ".config/personal-memory/mem0.env");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The file that holds MEM0_API_KEY for the indexer: MEM0_ENV_FILE, then [memory.mem0]
|
|
42
|
+
* env_file, then MEM0_ENV_FILE in the stack .env (written by scripts/setup.mjs), then
|
|
43
|
+
* ~/.config/personal-memory/mem0.env.
|
|
44
|
+
* @param {Record<string, any>} ctx
|
|
45
|
+
* @param {Helpers} h
|
|
46
|
+
*/
|
|
47
|
+
export function resolveEnvFile(ctx, h) {
|
|
48
|
+
const state = ensureState(ctx);
|
|
49
|
+
const stackValue = ctx.repoRoot ? h.envValue(path.join(ctx.repoRoot, ".env"), "MEM0_ENV_FILE") : "";
|
|
50
|
+
const raw = (process.env.MEM0_ENV_FILE || state.tomlEnvFile || stackValue || "").trim();
|
|
51
|
+
if (!raw) return defaultEnvFile(ctx.home);
|
|
52
|
+
return h.expandHome(raw, ctx.home);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* What is wrong with an env file path, or "". Compose interpolates the path in the
|
|
57
|
+
* indexer's `env_file` and the stack .env stores it, so it must be absolute and free of control
|
|
58
|
+
* characters, quotes, '$', backslashes, ' #' and leading or trailing spaces.
|
|
59
|
+
* @param {string} file
|
|
60
|
+
*/
|
|
61
|
+
export function envFileProblem(file) {
|
|
62
|
+
if (!path.isAbsolute(file)) return "the mem0 env file path must be absolute (MEM0_ENV_FILE or env_file)";
|
|
63
|
+
if (/[\x00-\x1f\x7f$"'`\\]/.test(file) || file.includes(" #") || file !== file.trim()) {
|
|
64
|
+
return "the mem0 env file path must not contain control characters, quotes, '$', backslashes, ' #' or edge spaces";
|
|
65
|
+
}
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** mem0's contribution to the wizard's closing summary. */
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readKey } from "./env-file.mjs";
|
|
4
|
+
import { LLM_ALIAS, litellmEnvFile, routesFile } from "./litellm.mjs";
|
|
5
|
+
import { MERIDIAN_TERMS_URL } from "../../hindsight/lib/meridian.mjs";
|
|
6
|
+
import { resolveEnvFile, ensureState } from "./state.mjs";
|
|
7
|
+
import { containerUrl, displayUrl, resolveApiUrl, urlProblem } from "./url.mjs";
|
|
8
|
+
|
|
9
|
+
/** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
|
|
10
|
+
|
|
11
|
+
const DOCS = "https://docs.mem0.ai/open-source/features/rest-api";
|
|
12
|
+
const OPERATOR = "providers/mem0/OPERATOR.md";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {Record<string, any>} ctx
|
|
16
|
+
* @param {Helpers} h
|
|
17
|
+
* @returns {{ running: string[], configFiles: string[], notes: string[], logServices: string[] }}
|
|
18
|
+
*/
|
|
19
|
+
export function summary(ctx, h) {
|
|
20
|
+
ensureState(ctx);
|
|
21
|
+
const raw = resolveApiUrl(ctx, h);
|
|
22
|
+
const valid = !urlProblem(raw);
|
|
23
|
+
const url = valid ? displayUrl(raw) : "(invalid URL)";
|
|
24
|
+
const envFile = resolveEnvFile(ctx, h);
|
|
25
|
+
|
|
26
|
+
const running = [
|
|
27
|
+
` mem0 API ${url} external server (not managed by this stack)`,
|
|
28
|
+
` indexer sink mem0 -> ${valid ? containerUrl(raw) : "(invalid URL)"} (the container's view)`,
|
|
29
|
+
];
|
|
30
|
+
const configFiles = [
|
|
31
|
+
` ${envFile} (MEM0_API_KEY for the indexer, mode 600)`,
|
|
32
|
+
` ${path.join(ctx.repoRoot, ".env")} (MEM0_API_URL, MEM0_INDEXER_URL; no secrets)`,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// The stack LiteLLM option: this run's answer, else what the last configure run saved (`--up`, `--down`).
|
|
36
|
+
const saved = (k) => (ctx.wizardConf ? h.envValue(ctx.wizardConf, k) : "");
|
|
37
|
+
const litellm =
|
|
38
|
+
ctx.mem0?.litellm !== undefined
|
|
39
|
+
? ctx.mem0.litellm
|
|
40
|
+
: saved("MEM0_LITELLM") === "1"
|
|
41
|
+
? { backend: saved("MEM0_LITELLM_BACKEND"), model: saved("MEM0_LITELLM_MODEL"), port: saved("MEM0_LITELLM_PORT") || "4000" }
|
|
42
|
+
: null;
|
|
43
|
+
/** @type {string[]} */
|
|
44
|
+
const logServices = ["indexer"];
|
|
45
|
+
if (litellm) {
|
|
46
|
+
running.push(
|
|
47
|
+
` LiteLLM (mem0) http://127.0.0.1:${litellm.port}/v1 ${LLM_ALIAS} -> ${litellm.backend} ${litellm.model}; loopback only, master key required`,
|
|
48
|
+
);
|
|
49
|
+
configFiles.push(` ${routesFile(ctx.home)} (mem0 LiteLLM routes)`, ` ${litellmEnvFile(ctx.home)} (LITELLM_MASTER_KEY and provider keys, mode 600)`);
|
|
50
|
+
logServices.push("litellm");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @type {string[]} */
|
|
54
|
+
const notes = [];
|
|
55
|
+
if (ctx.action === "down") return { running, configFiles, notes, logServices };
|
|
56
|
+
let loopback = false;
|
|
57
|
+
if (valid) {
|
|
58
|
+
const parsed = new URL(raw);
|
|
59
|
+
loopback = ["127.0.0.1", "localhost", "0.0.0.0", "[::1]"].includes(parsed.hostname);
|
|
60
|
+
}
|
|
61
|
+
let hasKey = false;
|
|
62
|
+
try {
|
|
63
|
+
hasKey = readKey(envFile) !== "";
|
|
64
|
+
} catch {
|
|
65
|
+
hasKey = false;
|
|
66
|
+
}
|
|
67
|
+
if (!hasKey) {
|
|
68
|
+
notes.push(
|
|
69
|
+
`API key: create one in your mem0 dashboard (port varies by install) or with POST /api-keys, then put MEM0_API_KEY=<key> in ${envFile} (mode 600). ` +
|
|
70
|
+
"A server run with AUTH_DISABLED=true needs none (development only).",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
notes.push(
|
|
74
|
+
`The mem0 server is yours to run: first-time setup (JWT_SECRET, embedder keys) is provider-native — ${OPERATOR} and ${DOCS}`,
|
|
75
|
+
loopback
|
|
76
|
+
? "The indexer container reaches a loopback server as host.docker.internal; on Linux a server bound only to 127.0.0.1 is not reachable that way: bind it to 0.0.0.0 or the docker bridge IP, or use a hostname in MEM0_API_URL."
|
|
77
|
+
: "The indexer container must be able to reach the server at the URL above (it uses the same host name).",
|
|
78
|
+
);
|
|
79
|
+
if (litellm) {
|
|
80
|
+
notes.push(
|
|
81
|
+
`mem0's LLM: point your mem0 config at http://127.0.0.1:${litellm.port}/v1 (provider openai, model ${LLM_ALIAS}, api_key = LITELLM_MASTER_KEY); ./setup printed the blocks. See docs/mem0.md.`,
|
|
82
|
+
);
|
|
83
|
+
if (litellm.backend === "meridian") {
|
|
84
|
+
notes.push(`Claude subscription use through Meridian is governed by Anthropic's Consumer Terms: ${MERIDIAN_TERMS_URL}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { running, configFiles, notes, logServices };
|
|
88
|
+
}
|