@bcmyguest/openmem 0.0.0-stage → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +136 -2
  3. package/bin/hindsight-project-index +530 -0
  4. package/cli/coding-agent/access-policy.mjs +38 -0
  5. package/cli/coding-agent/agents.mjs +34 -0
  6. package/cli/coding-agent/bank-from-cwd.mjs +140 -0
  7. package/cli/coding-agent/capabilities.mjs +23 -0
  8. package/cli/coding-agent/hook-harnesses.mjs +87 -0
  9. package/cli/coding-agent/hook-runner.mjs +260 -0
  10. package/cli/coding-agent/install-access.mjs +93 -0
  11. package/cli/coding-agent/install-shim.mjs +97 -0
  12. package/cli/coding-agent/opencode-config.mjs +91 -0
  13. package/cli/coding-agent/opencode-plugin.mjs +96 -0
  14. package/cli/coding-agent/runtime.mjs +39 -0
  15. package/cli/coding-agent/session-facade.mjs +62 -0
  16. package/cli/cognee-mcp.mjs +260 -0
  17. package/cli/feed-sync.mjs +11 -0
  18. package/cli/install-coding-agent.mjs +165 -0
  19. package/cli/install-memory-mcp.mjs +426 -0
  20. package/cli/is-main.mjs +18 -0
  21. package/cli/mcp/context.mjs +61 -0
  22. package/cli/mcp/export/cognee-dataset-name.mjs +12 -0
  23. package/cli/mcp/export/cognee.mjs +119 -0
  24. package/cli/mcp/export/hindsight.mjs +48 -0
  25. package/cli/mcp/export/index.mjs +26 -0
  26. package/cli/mcp/export/mem0.mjs +31 -0
  27. package/cli/mcp/llm-synthesize.mjs +50 -0
  28. package/cli/mcp/mem0-scope-id.mjs +22 -0
  29. package/cli/mcp/memory-facade.mjs +455 -0
  30. package/cli/mcp/memory-reflect-consolidate.mjs +120 -0
  31. package/cli/mcp/notebooklm-banks.mjs +21 -0
  32. package/cli/mcp/notebooklm-hindsight-rest.mjs +45 -0
  33. package/cli/mcp/notebooklm-provider.mjs +225 -0
  34. package/cli/mcp/pmm-records.mjs +23 -0
  35. package/cli/mcp/portability.mjs +158 -0
  36. package/cli/mcp/stdio.mjs +65 -0
  37. package/cli/mcp/unified-mcp-install.mjs +65 -0
  38. package/cli/mem0-mcp.mjs +108 -0
  39. package/cli/notebooklm-mcp.mjs +236 -0
  40. package/cli/openmem-mcp.mjs +35 -0
  41. package/cli/openmem.mjs +205 -0
  42. package/cli/roots.mjs +85 -0
  43. package/cli/stack.mjs +436 -0
  44. package/cli/switch-memory-provider.mjs +358 -0
  45. package/cli/wiring-report.mjs +125 -0
  46. package/compose.bench.yaml +12 -0
  47. package/compose.yaml +164 -0
  48. package/config/coding-agent.json +13 -0
  49. package/config/hindsight.env.example +24 -0
  50. package/config/index.yml +54 -0
  51. package/config/litellm.lemond.yaml +40 -0
  52. package/config/litellm.ollama.yaml +33 -0
  53. package/config/notebooklm.yml +33 -0
  54. package/config/openmem.toml.example +62 -0
  55. package/docs/OPENMEM.md +23 -0
  56. package/docs/architecture.md +219 -0
  57. package/docs/assets/README.md +10 -0
  58. package/docs/assets/setup-demo.gif +0 -0
  59. package/docs/coding-agent-hooks.md +51 -0
  60. package/docs/coding-agent-scopes.md +22 -0
  61. package/docs/coding-agents.md +51 -0
  62. package/docs/cognee.md +37 -0
  63. package/docs/concepts.md +112 -0
  64. package/docs/hindsight.md +74 -0
  65. package/docs/index-sources-roadmap.md +38 -0
  66. package/docs/index-sources.md +39 -0
  67. package/docs/indexer-sinks.md +122 -0
  68. package/docs/indexing.md +57 -0
  69. package/docs/manual-stack-setup.md +74 -0
  70. package/docs/mcp-install-guide.md +4 -0
  71. package/docs/mcp-setup.md +101 -0
  72. package/docs/mem0.md +103 -0
  73. package/docs/memory-portability.md +73 -0
  74. package/docs/memory-providers.md +246 -0
  75. package/docs/model-configuration.md +84 -0
  76. package/docs/notebooklm.md +83 -0
  77. package/docs/npx-install.md +170 -0
  78. package/docs/openmem-config.md +194 -0
  79. package/docs/provider-wizard-hooks.md +222 -0
  80. package/docs/quickstart.md +101 -0
  81. package/docs/setup-wizard.md +116 -0
  82. package/docs/sources-feeds.md +52 -0
  83. package/docs/sources-readwise.md +23 -0
  84. package/docs/sources-zotero.md +22 -0
  85. package/glama.json +4 -0
  86. package/indexer/Dockerfile +38 -0
  87. package/indexer/code_outline.py +146 -0
  88. package/indexer/feed_sync.py +107 -0
  89. package/indexer/index_config.py +290 -0
  90. package/indexer/pytest.ini +5 -0
  91. package/indexer/sinks/__init__.py +148 -0
  92. package/indexer/sinks/_http.py +44 -0
  93. package/indexer/sinks/cognee.py +329 -0
  94. package/indexer/sinks/hindsight.py +107 -0
  95. package/indexer/sinks/mem0.py +247 -0
  96. package/indexer/watch.py +240 -0
  97. package/package.json +95 -4
  98. package/providers/_template/compose.yaml +52 -0
  99. package/providers/cognee/OPERATOR.md +82 -0
  100. package/providers/cognee/compose.yaml +89 -0
  101. package/providers/cognee/lib/clack.mjs +157 -0
  102. package/providers/cognee/lib/coding-agent.mjs +26 -0
  103. package/providers/cognee/lib/cognee-mcp-install.mjs +153 -0
  104. package/providers/cognee/lib/config.mjs +98 -0
  105. package/providers/cognee/lib/env-file.mjs +194 -0
  106. package/providers/cognee/lib/env-setup.mjs +83 -0
  107. package/providers/cognee/lib/llm-env.mjs +382 -0
  108. package/providers/cognee/lib/ports.mjs +25 -0
  109. package/providers/cognee/lib/smoke.mjs +246 -0
  110. package/providers/cognee/lib/stack-env.mjs +134 -0
  111. package/providers/cognee/lib/state.mjs +101 -0
  112. package/providers/cognee/lib/summary.mjs +56 -0
  113. package/providers/cognee/lib/validate.mjs +97 -0
  114. package/providers/cognee/manifest.json +56 -0
  115. package/providers/cognee/wizard.mjs +134 -0
  116. package/providers/graphiti/manifest.json +15 -0
  117. package/providers/hindsight/lib/clack.mjs +245 -0
  118. package/providers/hindsight/lib/coding-agent.mjs +204 -0
  119. package/providers/hindsight/lib/config.mjs +73 -0
  120. package/providers/hindsight/lib/env-setup.mjs +71 -0
  121. package/providers/hindsight/lib/litellm-routes.mjs +187 -0
  122. package/providers/hindsight/lib/model-backend.mjs +214 -0
  123. package/providers/hindsight/lib/notebooklm-mcp-install.mjs +127 -0
  124. package/providers/hindsight/lib/ollama-compose.mjs +51 -0
  125. package/providers/hindsight/lib/ports.mjs +30 -0
  126. package/providers/hindsight/lib/probe.mjs +33 -0
  127. package/providers/hindsight/lib/stack-env.mjs +132 -0
  128. package/providers/hindsight/lib/state.mjs +75 -0
  129. package/providers/hindsight/lib/summary.mjs +43 -0
  130. package/providers/hindsight/lib/validate.mjs +75 -0
  131. package/providers/hindsight/manifest.json +58 -0
  132. package/providers/hindsight/wizard.mjs +149 -0
  133. package/providers/letta/manifest.json +29 -0
  134. package/providers/mem0/OPERATOR.md +124 -0
  135. package/providers/mem0/compose.yaml +57 -0
  136. package/providers/mem0/lib/coding-agent.mjs +58 -0
  137. package/providers/mem0/lib/config.mjs +45 -0
  138. package/providers/mem0/lib/configure.mjs +135 -0
  139. package/providers/mem0/lib/dotenv.mjs +71 -0
  140. package/providers/mem0/lib/env-file.mjs +110 -0
  141. package/providers/mem0/lib/http.mjs +69 -0
  142. package/providers/mem0/lib/mcp-install.mjs +165 -0
  143. package/providers/mem0/lib/secret.mjs +30 -0
  144. package/providers/mem0/lib/smoke.mjs +179 -0
  145. package/providers/mem0/lib/stack-env.mjs +216 -0
  146. package/providers/mem0/lib/state.mjs +67 -0
  147. package/providers/mem0/lib/summary.mjs +60 -0
  148. package/providers/mem0/lib/url.mjs +106 -0
  149. package/providers/mem0/lib/validate.mjs +158 -0
  150. package/providers/mem0/manifest.json +47 -0
  151. package/providers/mem0/wizard.mjs +133 -0
  152. package/providers/zep/manifest.json +29 -0
  153. package/scripts/check-pack.mjs +218 -0
  154. package/scripts/gen-masks.mjs +83 -0
  155. package/scripts/gen-masks.sh +4 -0
  156. package/scripts/promotion-provider-smoke.mjs +34 -0
  157. package/scripts/run-setup.sh +30 -0
  158. package/scripts/setup.mjs +94 -0
  159. package/scripts/setup.sh +5 -0
  160. package/scripts/spin-up.sh +45 -0
  161. package/scripts/wizard.sh +5 -0
  162. package/server.json +34 -0
  163. package/skills/openmem/SKILL.md +29 -0
  164. package/sync/Dockerfile +27 -0
  165. package/sync/memory_backend.py +87 -0
  166. package/sync/notebooklm_sync.py +664 -0
  167. package/wizard/bin/pm.mjs +97 -0
  168. package/wizard/bin/setup.mjs +33 -0
  169. package/wizard/lib/compose-version.mjs +32 -0
  170. package/wizard/lib/compose.mjs +70 -0
  171. package/wizard/lib/curl.mjs +103 -0
  172. package/wizard/lib/docker-build.mjs +148 -0
  173. package/wizard/lib/exec.mjs +41 -0
  174. package/wizard/lib/indexer.mjs +6 -0
  175. package/wizard/lib/load-machine-env.mjs +4 -0
  176. package/wizard/lib/log.mjs +32 -0
  177. package/wizard/lib/notebooklm-agent-access.mjs +92 -0
  178. package/wizard/lib/notebooklm.mjs +214 -0
  179. package/wizard/lib/parse-args.mjs +87 -0
  180. package/wizard/lib/paths.mjs +39 -0
  181. package/wizard/lib/pm-config.mjs +163 -0
  182. package/wizard/lib/ports.mjs +94 -0
  183. package/wizard/lib/prompts.mjs +57 -0
  184. package/wizard/lib/provider-wizard/helpers.mjs +98 -0
  185. package/wizard/lib/provider-wizard/run-steps.mjs +129 -0
  186. package/wizard/lib/providers/registry.mjs +262 -0
  187. package/wizard/lib/run-clack.mjs +108 -0
  188. package/wizard/lib/run-wizard.mjs +755 -0
  189. package/wizard/lib/validate-setup.mjs +62 -0
  190. package/wizard/lib/wizard-action.mjs +34 -0
  191. package/wizard/lib/wizard-conf.mjs +71 -0
@@ -0,0 +1,124 @@
1
+ # mem0 operator runbook (URL-only)
2
+
3
+ This stack does **not** run a mem0 container. You run a [self-hosted mem0 REST
4
+ server](https://docs.mem0.ai/open-source/features/rest-api); `./setup` only points
5
+ the shared indexer at it.
6
+
7
+ **Not** the hosted platform at `https://api.mem0.ai`. That API shape differs
8
+ (`/v1/…`); the indexer sink and wizard smoke talk to the open-source REST
9
+ server only (no `/v1/` prefix).
10
+
11
+ ## What `./setup` owns vs what you own
12
+
13
+ | You (provider-native) | This stack |
14
+ | --- | --- |
15
+ | Start the mem0 REST server (JWT, Postgres/pgvector, dashboard) | Write `MEM0_API_URL` / `MEM0_INDEXER_URL` into the stack `.env` |
16
+ | Embedder / LLM keys for mem0 itself | Indexer sink (`MEMORY_SINK=mem0`) and host-gateway rewrite |
17
+ | Create an API key (dashboard or `POST /api-keys`) | Store `MEM0_API_KEY` in `~/.config/personal-memory/mem0.env` (mode **600**) |
18
+ | Optional `AUTH_DISABLED=true` (dev only) | Smoke: `GET /docs`, retain, search, delete-by-id |
19
+
20
+ ## URLs
21
+
22
+ - **`MEM0_API_URL`** — host view (what you curl from this machine), e.g.
23
+ `http://127.0.0.1:18888`. Default port in the manifest is **18888**.
24
+ - **`MEM0_INDEXER_URL`** — container view. `./setup` derives it from
25
+ `MEM0_API_URL` by rewriting loopback hosts (`127.0.0.1`, `localhost`,
26
+ `0.0.0.0`, `[::1]`) to `host.docker.internal`. Do not hand-edit it to
27
+ `127.0.0.1` inside the container; validation requires it to match that rewrite.
28
+
29
+ ## Linux loopback
30
+
31
+ `host.docker.internal` is the docker bridge gateway, **not** the host's
32
+ `127.0.0.1`. A mem0 process bound only to `127.0.0.1` is unreachable from the
33
+ indexer container. Bind the server to `0.0.0.0` (or the bridge IP), or put a
34
+ real hostname in `MEM0_API_URL`. An `https://localhost` URL becomes
35
+ `https://host.docker.internal`, which usually fails TLS name checks — use a
36
+ hostname the certificate covers.
37
+
38
+ ## API key file
39
+
40
+ ```text
41
+ ~/.config/personal-memory/mem0.env # mode 600; directory mode 700
42
+ MEM0_API_KEY=<key>
43
+ ```
44
+
45
+ Never put `MEM0_API_KEY` in the stack `.env` (mode 644), `wizard.conf`, or
46
+ `config.toml`. Override the path with `MEM0_ENV_FILE` / `[memory.mem0] env_file`
47
+ if needed; then `./setup` must record that path in the stack `.env`.
48
+
49
+ With **`AUTH_DISABLED=true`** on the server (development only), no key is
50
+ required. Auth-enabled servers need a key or smoke/indexer calls return 401.
51
+
52
+ ## Embedder
53
+
54
+ Search fails if the server's embedder is misconfigured. Smoke looks for the
55
+ fact stored with `infer: false`; a miss points at the embedder key and server
56
+ logs, not at this stack.
57
+
58
+ ## Cleartext keys
59
+
60
+ Over plain `http://` to a non-loopback host, `X-API-Key` travels in cleartext.
61
+ Prefer `https://`, or keep the server on loopback / the same host.
62
+
63
+ ## Coding agents
64
+
65
+ There is no `pluginPackage`. Agents talk to your server via the Python SDK,
66
+ the self-hosted MCP bridge, or direct REST (`POST /memories`, `POST /search`). The
67
+ wizard's coding-agent step prints those paths and can register Cursor MCP; see
68
+ `providers/mem0/lib/coding-agent.mjs`.
69
+
70
+ ### Cursor MCP (`~/.cursor/mcp.json`)
71
+
72
+ Mem0's official MCP (`https://mcp.mem0.ai`) is **platform/cloud only**. For a
73
+ self-hosted REST server this repo registers [`@yeyuan98/mem0-mcp`](https://www.npmjs.com/package/@yeyuan98/mem0-mcp)
74
+ via the launcher `cli/mem0-mcp.mjs` (loads the key from the env file, then
75
+ `npx -y @yeyuan98/mem0-mcp`).
76
+
77
+ ```sh
78
+ MEMORY_PROVIDER=mem0 npx openmem install-mcp --yes
79
+ # checkout: MEMORY_PROVIDER=mem0 npx . install-mcp --yes
80
+ # or accept the coding-agent MCP confirm in setup
81
+ ```
82
+
83
+ Shape written (URL and env-file **path** only — never the key value):
84
+
85
+ ```json
86
+ {
87
+ "mcpServers": {
88
+ "mem0": {
89
+ "command": "node",
90
+ "args": ["/path/to/openmem/cli/mem0-mcp.mjs"],
91
+ "env": {
92
+ "MEM0_API_URL": "http://127.0.0.1:18888",
93
+ "MEM0_BASE_URL": "http://127.0.0.1:18888",
94
+ "MEM0_ENV_FILE": "~/.config/personal-memory/mem0.env"
95
+ }
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ **API key:** the launcher reads `MEM0_API_KEY` from `MEM0_ENV_FILE` when the
102
+ variable is not already in the process environment. You can also
103
+ `export MEM0_API_KEY=…` (e.g. `set -a; source ~/.config/personal-memory/mem0.env; set +a`)
104
+ before starting Cursor so the agent inherits it. **Never** put `MEM0_API_KEY` in
105
+ `mcp.json`. Existing `hindsight` / `codegraph` / `notebooklm` servers are left alone.
106
+
107
+ Restart Cursor Agent after install.
108
+
109
+ ### Switch to / from mem0
110
+
111
+ ```sh
112
+ npx openmem switch mem0 --yes
113
+ # later: npx openmem switch hindsight --yes
114
+ # checkout: npx . switch mem0 --yes
115
+ ```
116
+
117
+ Updates `wizard.conf` (and `config.toml` `[memory].provider` when present), restarts the stack
118
+ unless `--no-stack`, and runs `install-mcp --provider-only` unless `--no-mcp`. Does **not**
119
+ migrate memories. Flags: [README — Switch memory provider](../../README.md#switch-memory-provider).
120
+
121
+ ## Compose
122
+
123
+ `providers/mem0/compose.yaml` defines no mem0 service — only env +
124
+ `extra_hosts` on the shared `indexer`. Profile `mem0` selects that fragment.
@@ -0,0 +1,57 @@
1
+ # mem0 compose fragment (URL-only mode). Layered over compose.yaml; the wizard passes both, then `up -d`:
2
+ # docker compose -f compose.yaml -f providers/mem0/compose.yaml --profile mem0 up -d
3
+ # Add `-f compose.override.yaml` when you use the generated secret masks (Compose skips that file once -f is given).
4
+ #
5
+ # URL-only: this fragment defines NO service. The indexer talks to a mem0 REST server you already run (usually on
6
+ # the host, or on another machine); nothing here starts, builds or pulls a mem0 container. The reason is that the
7
+ # current self-hosted mem0 server (JWT/API-key auth, dashboard, Postgres + pgvector) has no current published image:
8
+ # `mem0/mem0-api-server:latest` on Docker Hub predates the auth. A mem0 service is a documented follow-up: build
9
+ # `server/` from a pinned mem0 git ref next to `pgvector/pgvector:pg17`, pinned and verified. Until then there is no
10
+ # `mem0` profile service, so `--profile mem0` only selects this fragment; it starts the provider-neutral indexer
11
+ # and its state-init step and nothing else.
12
+ #
13
+ # What the base compose.yaml gives you:
14
+ # - `indexer` and `indexer-state-init` have no profile and no provider dependency, so they start for every
15
+ # provider. This fragment points the indexer at the mem0 server.
16
+ # - `litellm` and `hindsight` carry the profile "hindsight" and do not start under this profile.
17
+ # Compose still interpolates every service before it filters by profile, so the stack .env needs LITELLM_CONFIG
18
+ # even though litellm never runs here. scripts/setup.mjs (providers/mem0/lib/stack-env.mjs) writes an inert value.
19
+ #
20
+ # Loopback caveat (Linux): host.docker.internal resolves to the docker bridge gateway, not to the host's 127.0.0.1.
21
+ # 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
22
+ # or to the docker bridge IP, or give MEM0_API_URL a real hostname. An https://localhost URL is rewritten to
23
+ # https://host.docker.internal, which will not match the server certificate name (use a hostname the certificate
24
+ # covers). ./setup prints a note when MEM0_API_URL is a loopback address.
25
+ #
26
+ # Stack .env keys read here (written by ./setup):
27
+ # MEM0_INDEXER_URL the server as the indexer container sees it. The wizard derives it from MEM0_API_URL (the host
28
+ # view) by rewriting localhost, 127.0.0.1, 0.0.0.0 and [::1] to host.docker.internal.
29
+ # MEM0_ENV_FILE optional; default ~/.config/personal-memory/mem0.env. Holds ONLY MEM0_API_KEY=... for the
30
+ # indexer (mode 600, outside the repo). Not needed when the server runs with AUTH_DISABLED.
31
+ # mem0's own server secrets (JWT_SECRET, ADMIN_API_KEY, embedder keys) are the operator's concern
32
+ # and never go through this stack.
33
+
34
+ services:
35
+ indexer:
36
+ environment:
37
+ # Selects indexer/sinks/mem0.py and gives it the server URL (container view, not the host view).
38
+ MEMORY_SINK: mem0
39
+ MEMORY_API_URL: ${MEM0_INDEXER_URL:?run ./setup (mem0 API URL)}
40
+ # required: false lets `config` and `up` run before the key file exists (Compose 2.24+).
41
+ env_file:
42
+ - path: ${MEM0_ENV_FILE:-${HOME}/.config/personal-memory/mem0.env}
43
+ required: false
44
+ # Reach a server on the host through host.docker.internal.
45
+ extra_hosts:
46
+ - "host.docker.internal:host-gateway"
47
+ # No depends_on: the server is external, and the indexer's watcher retries until it answers.
48
+
49
+ notebooklm:
50
+ environment:
51
+ MEMORY_SINK: mem0
52
+ MEMORY_API_URL: ${MEM0_INDEXER_URL:?run ./setup (mem0 API URL)}
53
+ env_file:
54
+ - path: ${MEM0_ENV_FILE:-${HOME}/.config/personal-memory/mem0.env}
55
+ required: false
56
+ extra_hosts:
57
+ - "host.docker.internal:host-gateway"
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Step "8. Coding-agent plugin", mem0 half: there is no npx pluginPackage for the
3
+ * self-hosted REST server. Print concrete SDK / MCP / direct-REST paths, then
4
+ * optionally merge the mem0 MCP into ~/.cursor/mcp.json (--yes or confirm).
5
+ */
6
+ import { installMem0Mcp } from "./mcp-install.mjs";
7
+ import { ensureState } from "./state.mjs";
8
+ import { displayUrl, resolveApiUrl, urlProblem } from "./url.mjs";
9
+
10
+ /** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
11
+
12
+ const SDK_DOCS = "https://docs.mem0.ai/open-source/python-quickstart";
13
+ const MCP_PACKAGE = "https://www.npmjs.com/package/@yeyuan98/mem0-mcp";
14
+ const REST_DOCS = "https://docs.mem0.ai/open-source/features/rest-api";
15
+ const OPERATOR = "providers/mem0/OPERATOR.md";
16
+
17
+ /**
18
+ * @param {Record<string, any>} ctx
19
+ * @param {Helpers} h
20
+ */
21
+ export async function setupCodingAgent(ctx, h) {
22
+ ensureState(ctx);
23
+ const raw = resolveApiUrl(ctx, h);
24
+ const base = urlProblem(raw) ? "(set MEM0_API_URL)" : displayUrl(raw);
25
+
26
+ h.info("mem0 has no coding-agent pluginPackage; use the self-hosted server directly:");
27
+ h.info(` 1. SDK — pip install mem0ai, point Memory at ${base} (see ${SDK_DOCS})`);
28
+ h.info(
29
+ ` 2. MCP — MEMORY_PROVIDER=mem0 npm run install-mcp -- --yes` +
30
+ ` (npx @yeyuan98/mem0-mcp via cli/mem0-mcp.mjs; see ${MCP_PACKAGE} and ${OPERATOR})`,
31
+ );
32
+ h.info(
33
+ ` 3. REST — POST ${base}/memories and POST ${base}/search with X-API-Key` +
34
+ ` (see ${REST_DOCS}; stack wiring in ${OPERATOR})`,
35
+ );
36
+
37
+ const codeRoot = ctx.packageRoot || ctx.repoRoot;
38
+ const stackRoot = ctx.repoRoot;
39
+ const wantMcp =
40
+ Boolean(ctx.yes) ||
41
+ (await h.confirm(ctx, "Register mem0 MCP in ~/.cursor/mcp.json now?", "y"));
42
+ if (wantMcp) {
43
+ await installMem0Mcp(
44
+ {
45
+ home: ctx.home,
46
+ repoRoot: codeRoot,
47
+ stackRoot,
48
+ dry: Boolean(ctx.dry),
49
+ env: process.env,
50
+ },
51
+ h,
52
+ );
53
+ } else {
54
+ h.info("skip MCP merge; later: MEMORY_PROVIDER=mem0 npm run install-mcp -- --yes");
55
+ }
56
+
57
+ h.ok("coding-agent: documented SDK / MCP / REST paths (pluginPackage remains null)");
58
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * mem0's slice of ~/.config/personal-memory/config.toml: the [memory.mem0] table
3
+ * (api_url, optional env_file). Never a key.
4
+ */
5
+ import { ensureState } from "./state.mjs";
6
+
7
+ /**
8
+ * @param {Record<string, unknown>} cfg merged TOML config
9
+ * @returns {{ apiUrl: string, envFile: string }}
10
+ */
11
+ export function defaultsFromConfig(cfg) {
12
+ const memory = /** @type {Record<string, unknown>} */ (cfg?.memory || {});
13
+ const mem0 = /** @type {Record<string, unknown>} */ (memory.mem0 || {});
14
+ return {
15
+ apiUrl: mem0.api_url != null ? String(mem0.api_url).trim() : "",
16
+ envFile: mem0.env_file != null ? String(mem0.env_file).trim() : "",
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Seed ctx.mem0 from [memory.mem0]. Env vars still win over these (see resolveApiUrl).
22
+ * @param {Record<string, any>} ctx
23
+ */
24
+ export function applyConfig(ctx) {
25
+ const state = ensureState(ctx);
26
+ const d = defaultsFromConfig(ctx.pmMerged || {});
27
+ if (d.apiUrl && !state.apiUrl) state.apiUrl = d.apiUrl;
28
+ if (d.envFile && !state.tomlEnvFile) state.tomlEnvFile = d.envFile;
29
+ }
30
+
31
+ /**
32
+ * Tables merged into config.toml: `memory.mem0`, which the wizard merges into [memory] (it
33
+ * always sets `provider` itself).
34
+ * @param {Record<string, any>} ctx
35
+ * @param {string} url the resolved host-view server URL
36
+ * @returns {Record<string, Record<string, unknown>>}
37
+ */
38
+ export function configToml(ctx, url) {
39
+ const state = ensureState(ctx);
40
+ /** @type {Record<string, unknown>} */
41
+ const mem0 = { api_url: url };
42
+ const envFile = (process.env.MEM0_ENV_FILE || state.tomlEnvFile || "").trim();
43
+ if (envFile) mem0.env_file = envFile;
44
+ return { memory: { mem0 } };
45
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Step "3. mem0 server URL and API key" (the hook is still named configureModelRouting).
3
+ * mem0 v1 is URL-only: this asks for the REST server URL and the API key the indexer sends,
4
+ * keeps the key in the mode 600 env file and never anywhere else. Keys can also be placed in
5
+ * that file by hand.
6
+ */
7
+ import path from "node:path";
8
+ import { applyConfig, configToml } from "./config.mjs";
9
+ import { dirTooOpen, fileMode, keyProblem, readKey, tooOpen, writeKey } from "./env-file.mjs";
10
+ import { capturedKey, captureKeyFromEnv } from "./secret.mjs";
11
+ import { ensureState, envFileProblem, resolveEnvFile } from "./state.mjs";
12
+ import { displayUrl, resolveApiUrl, urlProblem } from "./url.mjs";
13
+
14
+ /** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
15
+
16
+ /**
17
+ * @param {Record<string, any>} ctx
18
+ * @param {Helpers} h
19
+ */
20
+ export async function configureUrlAndKey(ctx, h) {
21
+ const state = ensureState(ctx);
22
+ h.step("3. mem0 server URL and API key");
23
+ const interactive = !ctx.yes && !ctx.skipBackendStep;
24
+ if (ctx.skipBackendStep) h.info("mem0 URL and key come from the setup UI (environment)");
25
+
26
+ // URL.
27
+ let raw = resolveApiUrl(ctx, h);
28
+ if (interactive) {
29
+ raw = String(await h.ask(ctx, "mem0 REST server URL (as this host sees it)", raw)).trim().replace(/\/+$/, "");
30
+ }
31
+ const urlIssue = urlProblem(raw);
32
+ if (urlIssue) {
33
+ h.problem(ctx, `mem0 API URL: ${urlIssue}`);
34
+ return;
35
+ }
36
+ const url = displayUrl(raw);
37
+ state.apiUrl = url;
38
+ state.url = url;
39
+ process.env.MEM0_API_URL = url;
40
+ h.ok(`mem0 server ${url} (an external server you run; this stack does not start it)`);
41
+
42
+ // Key file.
43
+ const envFile = resolveEnvFile(ctx, h);
44
+ const fileIssue = envFileProblem(envFile);
45
+ if (fileIssue) {
46
+ h.problem(ctx, fileIssue);
47
+ return;
48
+ }
49
+ const existing = readKey(envFile);
50
+ // Normally captured by the init hook (before any child process starts); this covers a caller
51
+ // that skipped init and is a no-op once the variables are gone.
52
+ captureKeyFromEnv();
53
+ const key = capturedKey();
54
+ if (key) h.info("using the API key from the environment");
55
+
56
+ if (key) {
57
+ const keyIssue = keyProblem(key);
58
+ if (keyIssue) {
59
+ h.problem(ctx, keyIssue);
60
+ return;
61
+ }
62
+ if (ctx.dry) {
63
+ h.info(`[dry-run] write MEM0_API_KEY to ${envFile} (mode 600, directory mode 700)`);
64
+ } else {
65
+ const result = writeKey(envFile, key);
66
+ if (result === "unchanged") h.ok(`kept ${envFile} (same key, mode 600)`);
67
+ else h.ok(`${result === "created" ? "created" : "updated"} ${envFile} with MEM0_API_KEY (mode 600)`);
68
+ }
69
+ } else if (existing) {
70
+ h.ok(`kept the API key already in ${envFile}`);
71
+ if (tooOpen(envFile)) {
72
+ h.warn(`${envFile} is group/other accessible (mode ${fileMode(envFile).toString(8)}); run: chmod 600 ${envFile}`);
73
+ }
74
+ } else {
75
+ // No prompt here: helpers.ask echoes what is typed. The setup UI (Clack) asks with a masked prompt.
76
+ h.info(
77
+ `no API key given. A server with auth enabled (the default) needs one: put MEM0_API_KEY=<key> in ${envFile} (mode 600), ` +
78
+ "or rerun with MEM0_API_KEY (or PM_MEM0_API_KEY) in the environment under --yes. " +
79
+ "A server run with AUTH_DISABLED does not need a key.",
80
+ );
81
+ }
82
+ if (dirTooOpen(envFile)) {
83
+ h.warn(`${path.dirname(envFile)} is accessible to group or others; run: chmod 700 ${path.dirname(envFile)}`);
84
+ }
85
+
86
+ // Non-secret state: wizard.conf (+ config.toml) record the URL only.
87
+ if (ctx.dry) h.info(`[dry-run] record MEM0_API_URL in ${ctx.wizardConf} and [memory.mem0] in config.toml`);
88
+ else h.saveWizardConf(ctx, { MEM0_API_URL: url }, { pmSections: configToml(ctx, url) });
89
+ }
90
+
91
+ /**
92
+ * Clack front end: ask the URL and (masked) key, hand them to the wizard through the
93
+ * environment and skip the wizard's own prompts.
94
+ * @param {{ repoRoot: string, home: string, wizardConf: string, pmMerged?: Record<string, unknown> }} ctx
95
+ * @param {typeof import("@clack/prompts")} ui
96
+ * @param {Helpers} h
97
+ * @returns {Promise<Record<string, string>>}
98
+ */
99
+ export async function clackSetup(ctx, ui, h) {
100
+ const stub = /** @type {Record<string, any>} */ (ctx);
101
+ ensureState(stub);
102
+ applyConfig(stub);
103
+ const current = resolveApiUrl(stub, h);
104
+ const existing = readKey(resolveEnvFile(stub, h));
105
+
106
+ const typed = await ui.text({
107
+ message: "mem0 REST server URL (as this host sees it)",
108
+ placeholder: current,
109
+ defaultValue: current,
110
+ validate: (v) => urlProblem(String(v || "").trim().replace(/\/+$/, "") || current) || undefined,
111
+ });
112
+ if (ui.isCancel(typed)) {
113
+ ui.cancel("Setup cancelled.");
114
+ process.exit(0);
115
+ }
116
+ const url = String(typed || "").trim().replace(/\/+$/, "") || current;
117
+
118
+ const secret = await ui.password({
119
+ message: existing
120
+ ? "mem0 API key (empty keeps the one in the env file)"
121
+ : "mem0 API key (empty if the server has auth disabled)",
122
+ validate: (v) => keyProblem(String(v || "").trim()) || undefined,
123
+ });
124
+ if (ui.isCancel(secret)) {
125
+ ui.cancel("Setup cancelled.");
126
+ process.exit(0);
127
+ }
128
+
129
+ /** @type {Record<string, string>} */
130
+ const handoff = { WIZARD_SKIP_BACKEND_STEP: "1", MEM0_API_URL: url };
131
+ const key = String(secret || "").trim();
132
+ if (key) handoff.PM_MEM0_API_KEY = key;
133
+ ui.note("Next: Docker, indexing, and starting the stack.", "mem0 configured");
134
+ return handoff;
135
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Read one key from an env file the way docker compose's env_file loader does, so the key the
3
+ * wizard checks is the key the indexer container gets. Node builtins only: scripts/setup.mjs
4
+ * loads this through stack-env.mjs, without the wizard helpers.
5
+ *
6
+ * Rules (verified against `docker compose config`): an optional `export ` prefix and
7
+ * whitespace around the key and the `=`; CRLF line ends; a value starting with " or ' reads
8
+ * up to the matching quote (anything after it is ignored); otherwise leading whitespace is
9
+ * dropped, the value stops at a space followed by `#` (a tab or a leading `#` does not start a
10
+ * comment) and is trimmed; the last duplicate wins; an unterminated quote
11
+ * reads as empty (compose refuses to load such a file).
12
+ */
13
+
14
+ /** @param {string} s */
15
+ function escapeRegExp(s) {
16
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17
+ }
18
+
19
+ /**
20
+ * The value after `=`.
21
+ * @param {string} rest
22
+ * @returns {string}
23
+ */
24
+ function parseValue(rest) {
25
+ const trimmed = rest.replace(/^\s+/, "");
26
+ const quote = trimmed[0];
27
+ if (quote === '"' || quote === "'") {
28
+ let out = "";
29
+ for (let i = 1; i < trimmed.length; i++) {
30
+ const c = trimmed[i];
31
+ if (quote === '"' && c === "\\" && i + 1 < trimmed.length) {
32
+ const next = trimmed[i + 1];
33
+ if (next === '"' || next === "\\") {
34
+ out += next;
35
+ i++;
36
+ continue;
37
+ }
38
+ }
39
+ if (c === quote) return out;
40
+ out += c;
41
+ }
42
+ return "";
43
+ }
44
+ return trimmed.replace(/ #.*$/, "").trim();
45
+ }
46
+
47
+ /**
48
+ * The value of `key` in env file text: the last matching line, "" when absent or empty.
49
+ * @param {string} text
50
+ * @param {string} key
51
+ * @returns {string}
52
+ */
53
+ export function parseEnvValue(text, key) {
54
+ const line = new RegExp(`^\\s*(?:export\\s+)?${escapeRegExp(key)}\\s*=(.*)$`);
55
+ let value = "";
56
+ for (const raw of String(text).split(/\r?\n/)) {
57
+ const m = line.exec(raw.replace(/\r$/, ""));
58
+ if (m) value = parseValue(m[1]);
59
+ }
60
+ return value;
61
+ }
62
+
63
+ /**
64
+ * True when any line assigns `key` (even to an empty value).
65
+ * @param {string} text
66
+ * @param {string} key
67
+ */
68
+ export function assignsKey(text, key) {
69
+ const line = new RegExp(`^\\s*(?:export\\s+)?${escapeRegExp(key)}\\s*=`);
70
+ return String(text).split(/\r?\n/).some((l) => line.test(l));
71
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The mem0 env file (default ~/.config/personal-memory/mem0.env): the indexer's MEM0_API_KEY,
3
+ * mode 600 in a mode 700 directory, outside the repo. Nothing here prints a key. Reads follow
4
+ * docker compose's env_file rules (see dotenv.mjs) so the wizard sees the key the indexer gets.
5
+ */
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { parseEnvValue } from "./dotenv.mjs";
9
+
10
+ const KEY_LINE = /^\s*(?:export\s+)?MEM0_API_KEY\s*=/;
11
+
12
+ /**
13
+ * The key must survive an env_file parser (compose interpolates '$', treats '#' and quotes
14
+ * specially) and a single line. The message never echoes the key.
15
+ * @param {string} key
16
+ * @returns {string} problem text, or ""
17
+ */
18
+ export function keyProblem(key) {
19
+ if (/[\x00-\x20\x7f"'`#$\\]/.test(key)) {
20
+ return "the API key has characters an env file cannot hold safely (whitespace, quotes, '#', '$', backslash)";
21
+ }
22
+ return "";
23
+ }
24
+
25
+ /**
26
+ * The MEM0_API_KEY value compose would load from the file (last duplicate wins, quotes and
27
+ * `export` handled), or "" when the file is absent, has no such line, or the value is empty.
28
+ * @param {string} file
29
+ */
30
+ export function readKey(file) {
31
+ if (!fs.existsSync(file)) return "";
32
+ return parseEnvValue(fs.readFileSync(file, "utf8"), "MEM0_API_KEY");
33
+ }
34
+
35
+ /** @param {string} file @returns {number} permission bits, or -1 when the file is absent */
36
+ export function fileMode(file) {
37
+ return fs.existsSync(file) ? fs.statSync(file).mode & 0o777 : -1;
38
+ }
39
+
40
+ /** True when group or others can read or write the file. */
41
+ export function tooOpen(file) {
42
+ const mode = fileMode(file);
43
+ return mode >= 0 && (mode & 0o077) !== 0;
44
+ }
45
+
46
+ /**
47
+ * True when the file's directory exists and group or others can enter or list it. The file
48
+ * mode still protects the key; this is the extra hygiene the wizard warns about.
49
+ * @param {string} file
50
+ */
51
+ export function dirTooOpen(file) {
52
+ const dir = path.dirname(file);
53
+ return fs.existsSync(dir) && (fs.statSync(dir).mode & 0o077) !== 0;
54
+ }
55
+
56
+ /**
57
+ * Follow a chain of symlinks (also dangling ones) to the path that would hold the data.
58
+ * @param {string} p
59
+ */
60
+ function resolveLink(p) {
61
+ let current = p;
62
+ for (let i = 0; i < 16; i++) {
63
+ let stat;
64
+ try {
65
+ stat = fs.lstatSync(current);
66
+ } catch {
67
+ return current;
68
+ }
69
+ if (!stat.isSymbolicLink()) return current;
70
+ current = path.resolve(path.dirname(current), fs.readlinkSync(current));
71
+ }
72
+ throw new Error("too many symbolic links in the mem0 env file path");
73
+ }
74
+
75
+ /**
76
+ * Write `MEM0_API_KEY=<key>` into the file: other lines are kept, every existing key line is
77
+ * replaced by one. A symlinked file or directory keeps its link and the data lands at the
78
+ * target. The directory is created with mode 700; the file is replaced atomically through a
79
+ * temp file created exclusively (`wx`) with mode 600 beside it, so the key is never briefly
80
+ * readable by others.
81
+ * @param {string} file
82
+ * @param {string} key
83
+ * @returns {"created"|"updated"|"unchanged"}
84
+ */
85
+ export function writeKey(file, key) {
86
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
87
+ const target = resolveLink(path.join(fs.realpathSync(path.dirname(file)), path.basename(file)));
88
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
89
+ const existed = fs.existsSync(target);
90
+ /** @type {string[]} */
91
+ let lines = [];
92
+ let previous = "";
93
+ if (existed) {
94
+ previous = fs.readFileSync(target, "utf8");
95
+ lines = previous.split(/\r?\n/);
96
+ while (lines.length && lines.at(-1) === "") lines.pop();
97
+ }
98
+ const kept = lines.filter((l) => !KEY_LINE.test(l));
99
+ const text = `${[...kept, `MEM0_API_KEY=${key}`].join("\n")}\n`;
100
+ if (existed && previous === text && !tooOpen(target)) return "unchanged";
101
+ const tmp = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
102
+ try {
103
+ fs.writeFileSync(tmp, text, { flag: "wx", mode: 0o600 });
104
+ fs.chmodSync(tmp, 0o600);
105
+ fs.renameSync(tmp, target);
106
+ } finally {
107
+ fs.rmSync(tmp, { force: true });
108
+ }
109
+ return existed ? "updated" : "created";
110
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Small fetch wrapper for the mem0 REST server. Every call goes to a path under the
3
+ * resolved mem0 URL; redirects are never followed (a custom header such as X-API-Key is
4
+ * not stripped on a cross-origin redirect), and errors never carry the key.
5
+ */
6
+
7
+ /**
8
+ * @param {string} text
9
+ * @param {string} secret
10
+ */
11
+ export function redact(text, secret) {
12
+ let out = String(text ?? "");
13
+ if (secret) out = out.split(secret).join("***");
14
+ return out;
15
+ }
16
+
17
+ /**
18
+ * Text from the server made safe to print: control characters (terminal escapes, newlines)
19
+ * become spaces and the length is capped.
20
+ * @param {unknown} text
21
+ * @param {number} [max]
22
+ */
23
+ export function clean(text, max = 200) {
24
+ return String(text ?? "")
25
+ .replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ")
26
+ .replace(/\s+/g, " ")
27
+ .trim()
28
+ .slice(0, max);
29
+ }
30
+
31
+ /** Short, key-free description of a fetch failure. @param {unknown} e */
32
+ export function describeError(e) {
33
+ const err = /** @type {{ name?: string, message?: string, cause?: { code?: string } }} */ (e);
34
+ if (err?.name === "TimeoutError" || err?.name === "AbortError") return "timed out";
35
+ return err?.cause?.code || err?.message || "request failed";
36
+ }
37
+
38
+ /**
39
+ * @param {string} base resolved, validated mem0 URL (no trailing slash)
40
+ * @param {string} method
41
+ * @param {string} route path (and query) under the base, starting with '/'
42
+ * @param {{ key?: string, body?: unknown, timeoutMs?: number }} [opts]
43
+ * @returns {Promise<{ status: number, ok: boolean, text: string, json: any }>}
44
+ */
45
+ export async function request(base, method, route, opts = {}) {
46
+ /** @type {Record<string, string>} */
47
+ const headers = { Accept: "application/json" };
48
+ if (opts.key) headers["X-API-Key"] = opts.key;
49
+ /** @type {RequestInit} */
50
+ const init = {
51
+ method,
52
+ headers,
53
+ redirect: "manual",
54
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 15000),
55
+ };
56
+ if (opts.body !== undefined) {
57
+ headers["Content-Type"] = "application/json";
58
+ init.body = JSON.stringify(opts.body);
59
+ }
60
+ const res = await fetch(`${base}${route}`, init);
61
+ const text = await res.text();
62
+ let json = null;
63
+ try {
64
+ json = text ? JSON.parse(text) : null;
65
+ } catch {
66
+ json = null;
67
+ }
68
+ return { status: res.status, ok: res.status >= 200 && res.status < 300, text, json };
69
+ }