diff --git a/docs/integrations/antigravity.mdx b/docs/integrations/antigravity.mdx
index 08158ffa0..f5be83f34 100644
--- a/docs/integrations/antigravity.mdx
+++ b/docs/integrations/antigravity.mdx
@@ -28,6 +28,10 @@ echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc && source ~/.bashrc
```
+
+ Already set up the [Mem0 CLI](/platform/cli) with `mem0 init`? The Mem0 plugin also reads the key it saved in `~/.mem0/config.json`, so you can skip this step. `MEM0_API_KEY` takes precedence when set.
+
+
## Installation
**Option A: degit** (recommended):
diff --git a/docs/integrations/claude-code.mdx b/docs/integrations/claude-code.mdx
index 4277bb90e..271bc22d3 100644
--- a/docs/integrations/claude-code.mdx
+++ b/docs/integrations/claude-code.mdx
@@ -166,7 +166,7 @@ claude plugin update mem0@mem0-plugins --scope user
| Problem | Fix |
| --- | --- |
-| Missing key | Reinstall with `--config api_key="$MEM0_API_KEY"` while the var is set. |
+| Missing key | Reinstall with `--config api_key="$MEM0_API_KEY"` while the var is set, or run `mem0 init` with the [Mem0 CLI](/platform/cli). The plugin falls back to the key it saves. |
| `401 Unauthorized` | API key is invalid or expired. Run `/mem0:status` to confirm. |
| No memory after ending a session | Extraction runs in the background. Wait a moment, then search again. |
| Sidekick won't start | Must be in a Git repo. Check that your Claude Code version supports plugin agents and worktrees. |
diff --git a/docs/integrations/codex.mdx b/docs/integrations/codex.mdx
index a91f4f8f0..b67555754 100644
--- a/docs/integrations/codex.mdx
+++ b/docs/integrations/codex.mdx
@@ -35,6 +35,10 @@ source ~/.bashrc
```
+
+ Already set up the [Mem0 CLI](/platform/cli) with `mem0 init`? The plugin (Option A) also reads the key it saved in `~/.mem0/config.json`, so you can skip this step. `MEM0_API_KEY` takes precedence when set.
+
+
## Installation
### Option A: Plugin Marketplace (Recommended)
diff --git a/docs/integrations/cursor.mdx b/docs/integrations/cursor.mdx
index 49eb8861b..c2419354c 100644
--- a/docs/integrations/cursor.mdx
+++ b/docs/integrations/cursor.mdx
@@ -35,6 +35,10 @@ source ~/.bashrc
```
+
+ Already set up the [Mem0 CLI](/platform/cli) with `mem0 init`? The full plugin (Option A) also reads the key it saved in `~/.mem0/config.json`, so you can skip this step. That includes Cursor opened from the Dock, which does not load your shell profile. A key from the plugin configuration or the environment takes precedence.
+
+
Already have `mem0` configured as an MCP server in Cursor? Remove the existing entry from your Cursor MCP settings before installing to avoid duplicate tools.
@@ -164,6 +168,7 @@ Captured prompts and responses retain their full redacted text without a per-mes
## Troubleshooting
- **"Connection failed"**: Verify `MEM0_API_KEY` is set: `echo $MEM0_API_KEY`
+- **Still asked for an API key**: Run `mem0 init` with the [Mem0 CLI](/platform/cli). The full plugin falls back to the key it saves.
- **Duplicate tools**: Do not combine the full plugin with an MCP-only option. Remove the standalone `mem0` MCP entry before installing the plugin.
- **No tools appearing**: Go to Cursor Settings > MCP and verify the `mem0` server shows as connected
diff --git a/docs/integrations/deepseek-plugin.mdx b/docs/integrations/deepseek-plugin.mdx
index 0242496e1..80dbdfab0 100644
--- a/docs/integrations/deepseek-plugin.mdx
+++ b/docs/integrations/deepseek-plugin.mdx
@@ -56,6 +56,10 @@ source ~/.bashrc
```
+
+ Already set up the [Mem0 CLI](/platform/cli) with `mem0 init`? The plugin also reads the key it saved in `~/.mem0/config.json`, so you can skip this step. `config.apiKey` and `MEM0_API_KEY` take precedence.
+
+
## Try it locally
1. Build and pack the plugin:
@@ -103,7 +107,7 @@ For a Mem0 Platform on-prem or dedicated deployment, point `config.host` at that
| Field | Required | Default | Notes |
|---|---|---|---|
-| `apiKey` | no | `$MEM0_API_KEY` | Mem0 platform API key |
+| `apiKey` | no | `$MEM0_API_KEY` | Mem0 platform API key. Falls back to the key `mem0 init` saved. |
| `userId` | yes | | Default entity that owns the memories |
| `allowUserOverride` | no | `false` | Permit model-selected access to a different user only in a trusted multi-user deployment |
| `host` | no | `api.mem0.ai` | Platform base URL (on-prem / dedicated) |
diff --git a/docs/integrations/kimi.mdx b/docs/integrations/kimi.mdx
index e6bd27706..7d1af870a 100644
--- a/docs/integrations/kimi.mdx
+++ b/docs/integrations/kimi.mdx
@@ -99,7 +99,7 @@ Captured prompts and responses retain their full redacted text without a per-mes
| Problem | Fix |
| --- | --- |
-| Missing API key | Start Kimi from a shell where `MEM0_API_KEY` is exported. |
+| Missing API key | Start Kimi from a shell where `MEM0_API_KEY` is exported, or run `mem0 init` with the [Mem0 CLI](/platform/cli). The plugin falls back to the key it saves. |
| Plugin changes do not appear | Run `/plugins reload`, then `/reload` or `/new`. |
| MCP server is disabled | Run `/plugins mcp enable mem0 mem0`, then `/reload`. |
| No memory in a later session | Wait a moment for the background flush, then ask Kimi to search memory explicitly. |
diff --git a/docs/integrations/opencode.mdx b/docs/integrations/opencode.mdx
index 70c5a6558..541eae3b7 100644
--- a/docs/integrations/opencode.mdx
+++ b/docs/integrations/opencode.mdx
@@ -26,6 +26,10 @@ echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc && source ~/.bashrc
```
+
+ Already set up the [Mem0 CLI](/platform/cli) with `mem0 init`? The OpenCode plugin also reads the key it saved in `~/.mem0/config.json`, so you can skip this step. `MEM0_API_KEY` and your shell profile take precedence.
+
+
## Installation
### Option A: Plugin Install (Recommended)
diff --git a/docs/integrations/pi-agent.mdx b/docs/integrations/pi-agent.mdx
index 5a00c5d28..9240f8159 100644
--- a/docs/integrations/pi-agent.mdx
+++ b/docs/integrations/pi-agent.mdx
@@ -40,6 +40,10 @@ source ~/.bashrc
```
+
+ Already set up the [Mem0 CLI](/platform/cli) with `mem0 init`? The extension also reads the key it saved in `~/.mem0/config.json`, so you can skip this step. `MEM0_API_KEY` and `apiKey` in `mem0-config.json` take precedence.
+
+
## Installation
```bash
@@ -68,7 +72,7 @@ For advanced settings, create `~/.pi/agent/mem0-config.json`:
| Key | Type | Default | Description |
|-----|------|---------|-------------|
-| `apiKey` | `string` | `$MEM0_API_KEY` | Mem0 API key. Environment variable takes precedence. |
+| `apiKey` | `string` | `$MEM0_API_KEY` | Mem0 API key. Environment variable takes precedence. Falls back to the key `mem0 init` saved. |
| `userId` | `string` | `$MEM0_USER_ID` or `"default"` | User identity for memory scoping |
| `autoCapture` | `boolean` | `true` | Store facts from conversations automatically |
| `defaultScope` | `string` | `"project"` | Default memory scope: `project`, `session`, or `global` |
@@ -147,7 +151,7 @@ You: What do you know about my preferences?
## Troubleshooting
-- **"No API key found"**: Verify `MEM0_API_KEY` is set: `echo $MEM0_API_KEY`. If empty, add it to your shell profile (see Prerequisites)
+- **"No API key found"**: Verify `MEM0_API_KEY` is set: `echo $MEM0_API_KEY`. If empty, add it to your shell profile (see Prerequisites) or run `mem0 init`
- **Extension not loading**: Check Pi startup output for errors. For a source checkout, run `pnpm build`, then `pi -e ./dist/entry.js` from the plugin directory
- **Memories not capturing**: Verify `autoCapture` is `true` (default). Check `/mem0-status` for connection health
- **Wrong project detected**: The plugin uses the git repository root as `app_id`. If not in a git repo, it falls back to the working directory name. Run `/mem0-status` to see the detected project
diff --git a/integrations/agent-plugin-core/python/memory_core.py b/integrations/agent-plugin-core/python/memory_core.py
index 9b420a063..7695f0ac0 100644
--- a/integrations/agent-plugin-core/python/memory_core.py
+++ b/integrations/agent-plugin-core/python/memory_core.py
@@ -378,30 +378,46 @@ def resolve_repo(cwd: str | None) -> RepoContext:
return _resolve_repo_cached(os.path.abspath(cwd or os.getcwd()))
+_PLUGIN_API_KEY_ENV = (
+ "PLUGIN_OPTION_API_KEY",
+ "CLAUDE_PLUGIN_OPTION_API_KEY",
+ "CLAUDE_PLUGIN_OPTION_MEM0_API_KEY",
+)
+
+
+def _configured(value: object) -> str:
+ """The stripped value, or empty when the host left its ${placeholder} unexpanded."""
+ text = value.strip() if isinstance(value, str) else ""
+ return "" if text.startswith("${") and text.endswith("}") else text
+
+
+def _first_env(*names: str) -> str:
+ return next((value for name in names if (value := _configured(os.environ.get(name)))), "")
+
+
+def _mem0_cli_api_key() -> str:
+ """The key `mem0 init` saved to the Mem0 CLI config."""
+ try:
+ config = json.loads((Path.home() / ".mem0" / "config.json").read_text(encoding="utf-8"))
+ return _configured(config["platform"]["api_key"])
+ except (OSError, ValueError, LookupError, TypeError):
+ return ""
+
+
def api_key() -> str:
- configured = (
- os.environ.get("MEM0_API_KEY")
- or os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
+ configured = _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV)
if configured:
return configured
try:
- return (data_dir() / "api-key").read_text(encoding="utf-8").strip()
+ cached = _configured((data_dir() / "api-key").read_text(encoding="utf-8"))
except OSError:
- return ""
+ cached = ""
+ return cached or _mem0_cli_api_key()
def cache_plugin_api_key() -> bool:
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
- configured = (
- os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
+ configured = _first_env(*_PLUGIN_API_KEY_ENV)
if not configured:
return False
@@ -429,14 +445,7 @@ def cache_plugin_api_key() -> bool:
def clear_stale_api_key_cache() -> bool:
"""Drop the cached key file once every configured key source is gone."""
- configured = (
- os.environ.get("MEM0_API_KEY")
- or os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
- if configured:
+ if _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV):
return False
path = data_dir() / "api-key"
if not path.exists():
@@ -459,12 +468,7 @@ def detached_process_kwargs(platform: str | None = None) -> dict:
def _plugin_option(name: str, fallback: str = "") -> str:
- return (
- os.environ.get(f"PLUGIN_OPTION_{name.upper()}")
- or os.environ.get(f"CLAUDE_PLUGIN_OPTION_{name.upper()}")
- or os.environ.get(fallback)
- or ""
- ).strip()
+ return _first_env(f"PLUGIN_OPTION_{name.upper()}", f"CLAUDE_PLUGIN_OPTION_{name.upper()}", fallback)
def user_id() -> str:
diff --git a/integrations/agent-plugin-core/skills/status/SKILL.md.tmpl b/integrations/agent-plugin-core/skills/status/SKILL.md.tmpl
index c1ac9e8f9..a695d0f53 100644
--- a/integrations/agent-plugin-core/skills/status/SKILL.md.tmpl
+++ b/integrations/agent-plugin-core/skills/status/SKILL.md.tmpl
@@ -19,5 +19,7 @@ API key is configured, the event/flush/retrieval counts (`flushes` is the
number of completed flushes, not a pending count), and the doctor check
results. If doctor reports an authentication failure (401 / invalid key), say
clearly that the Mem0 API key is invalid or expired and that memories are NOT
-being created. Never report an auth failure as "no memories found". Suggest
-reinstalling with `--config api_key=...` in that case.
+being created. Never report an auth failure as "no memories found". When the
+key is missing or invalid, suggest updating the plugin's API key setting,
+exporting `MEM0_API_KEY`, or running `mem0 init` (the plugin reads the key the
+Mem0 CLI saves in `~/.mem0/config.json`).
diff --git a/integrations/agent-plugin-core/typescript/src/credentials.ts b/integrations/agent-plugin-core/typescript/src/credentials.ts
new file mode 100644
index 000000000..118b32a7b
--- /dev/null
+++ b/integrations/agent-plugin-core/typescript/src/credentials.ts
@@ -0,0 +1,11 @@
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+
+export function mem0CliApiKey(homeDir: string): string {
+ try {
+ const key = JSON.parse(readFileSync(join(homeDir, ".mem0", "config.json"), "utf8"))?.platform?.api_key;
+ return typeof key === "string" ? key.trim() : "";
+ } catch {
+ return "";
+ }
+}
diff --git a/integrations/agent-plugin-core/typescript/tests/credentials.test.ts b/integrations/agent-plugin-core/typescript/tests/credentials.test.ts
new file mode 100644
index 000000000..434116203
--- /dev/null
+++ b/integrations/agent-plugin-core/typescript/tests/credentials.test.ts
@@ -0,0 +1,26 @@
+import assert from "node:assert/strict";
+import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import test from "node:test";
+
+import { mem0CliApiKey } from "../src/credentials.ts";
+
+function homeWithCliConfig(config: string): string {
+ const home = mkdtempSync(join(tmpdir(), "mem0-cli-home-"));
+ mkdirSync(join(home, ".mem0"));
+ writeFileSync(join(home, ".mem0", "config.json"), config);
+ return home;
+}
+
+test("reads the key mem0 init saved", () => {
+ const home = homeWithCliConfig(JSON.stringify({ platform: { api_key: " m0-cli-key\n" } }));
+ assert.equal(mem0CliApiKey(home), "m0-cli-key");
+});
+
+test("missing, malformed, or non-string config reads as no key", () => {
+ assert.equal(mem0CliApiKey(mkdtempSync(join(tmpdir(), "mem0-cli-home-"))), "");
+ assert.equal(mem0CliApiKey(homeWithCliConfig("{not json")), "");
+ assert.equal(mem0CliApiKey(homeWithCliConfig("null")), "");
+ assert.equal(mem0CliApiKey(homeWithCliConfig(JSON.stringify({ platform: { api_key: 42 } }))), "");
+});
diff --git a/integrations/antigravity-plugin/core/memory_core.py b/integrations/antigravity-plugin/core/memory_core.py
index 9b420a063..7695f0ac0 100644
--- a/integrations/antigravity-plugin/core/memory_core.py
+++ b/integrations/antigravity-plugin/core/memory_core.py
@@ -378,30 +378,46 @@ def resolve_repo(cwd: str | None) -> RepoContext:
return _resolve_repo_cached(os.path.abspath(cwd or os.getcwd()))
+_PLUGIN_API_KEY_ENV = (
+ "PLUGIN_OPTION_API_KEY",
+ "CLAUDE_PLUGIN_OPTION_API_KEY",
+ "CLAUDE_PLUGIN_OPTION_MEM0_API_KEY",
+)
+
+
+def _configured(value: object) -> str:
+ """The stripped value, or empty when the host left its ${placeholder} unexpanded."""
+ text = value.strip() if isinstance(value, str) else ""
+ return "" if text.startswith("${") and text.endswith("}") else text
+
+
+def _first_env(*names: str) -> str:
+ return next((value for name in names if (value := _configured(os.environ.get(name)))), "")
+
+
+def _mem0_cli_api_key() -> str:
+ """The key `mem0 init` saved to the Mem0 CLI config."""
+ try:
+ config = json.loads((Path.home() / ".mem0" / "config.json").read_text(encoding="utf-8"))
+ return _configured(config["platform"]["api_key"])
+ except (OSError, ValueError, LookupError, TypeError):
+ return ""
+
+
def api_key() -> str:
- configured = (
- os.environ.get("MEM0_API_KEY")
- or os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
+ configured = _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV)
if configured:
return configured
try:
- return (data_dir() / "api-key").read_text(encoding="utf-8").strip()
+ cached = _configured((data_dir() / "api-key").read_text(encoding="utf-8"))
except OSError:
- return ""
+ cached = ""
+ return cached or _mem0_cli_api_key()
def cache_plugin_api_key() -> bool:
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
- configured = (
- os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
+ configured = _first_env(*_PLUGIN_API_KEY_ENV)
if not configured:
return False
@@ -429,14 +445,7 @@ def cache_plugin_api_key() -> bool:
def clear_stale_api_key_cache() -> bool:
"""Drop the cached key file once every configured key source is gone."""
- configured = (
- os.environ.get("MEM0_API_KEY")
- or os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
- if configured:
+ if _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV):
return False
path = data_dir() / "api-key"
if not path.exists():
@@ -459,12 +468,7 @@ def detached_process_kwargs(platform: str | None = None) -> dict:
def _plugin_option(name: str, fallback: str = "") -> str:
- return (
- os.environ.get(f"PLUGIN_OPTION_{name.upper()}")
- or os.environ.get(f"CLAUDE_PLUGIN_OPTION_{name.upper()}")
- or os.environ.get(fallback)
- or ""
- ).strip()
+ return _first_env(f"PLUGIN_OPTION_{name.upper()}", f"CLAUDE_PLUGIN_OPTION_{name.upper()}", fallback)
def user_id() -> str:
diff --git a/integrations/antigravity-plugin/skills/status/SKILL.md b/integrations/antigravity-plugin/skills/status/SKILL.md
index d417e4a40..f080e7655 100644
--- a/integrations/antigravity-plugin/skills/status/SKILL.md
+++ b/integrations/antigravity-plugin/skills/status/SKILL.md
@@ -19,5 +19,7 @@ API key is configured, the event/flush/retrieval counts (`flushes` is the
number of completed flushes, not a pending count), and the doctor check
results. If doctor reports an authentication failure (401 / invalid key), say
clearly that the Mem0 API key is invalid or expired and that memories are NOT
-being created. Never report an auth failure as "no memories found". Suggest
-reinstalling with `--config api_key=...` in that case.
+being created. Never report an auth failure as "no memories found". When the
+key is missing or invalid, suggest updating the plugin's API key setting,
+exporting `MEM0_API_KEY`, or running `mem0 init` (the plugin reads the key the
+Mem0 CLI saves in `~/.mem0/config.json`).
diff --git a/integrations/claude-code-plugin/core/memory_core.py b/integrations/claude-code-plugin/core/memory_core.py
index 9b420a063..7695f0ac0 100644
--- a/integrations/claude-code-plugin/core/memory_core.py
+++ b/integrations/claude-code-plugin/core/memory_core.py
@@ -378,30 +378,46 @@ def resolve_repo(cwd: str | None) -> RepoContext:
return _resolve_repo_cached(os.path.abspath(cwd or os.getcwd()))
+_PLUGIN_API_KEY_ENV = (
+ "PLUGIN_OPTION_API_KEY",
+ "CLAUDE_PLUGIN_OPTION_API_KEY",
+ "CLAUDE_PLUGIN_OPTION_MEM0_API_KEY",
+)
+
+
+def _configured(value: object) -> str:
+ """The stripped value, or empty when the host left its ${placeholder} unexpanded."""
+ text = value.strip() if isinstance(value, str) else ""
+ return "" if text.startswith("${") and text.endswith("}") else text
+
+
+def _first_env(*names: str) -> str:
+ return next((value for name in names if (value := _configured(os.environ.get(name)))), "")
+
+
+def _mem0_cli_api_key() -> str:
+ """The key `mem0 init` saved to the Mem0 CLI config."""
+ try:
+ config = json.loads((Path.home() / ".mem0" / "config.json").read_text(encoding="utf-8"))
+ return _configured(config["platform"]["api_key"])
+ except (OSError, ValueError, LookupError, TypeError):
+ return ""
+
+
def api_key() -> str:
- configured = (
- os.environ.get("MEM0_API_KEY")
- or os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
+ configured = _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV)
if configured:
return configured
try:
- return (data_dir() / "api-key").read_text(encoding="utf-8").strip()
+ cached = _configured((data_dir() / "api-key").read_text(encoding="utf-8"))
except OSError:
- return ""
+ cached = ""
+ return cached or _mem0_cli_api_key()
def cache_plugin_api_key() -> bool:
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
- configured = (
- os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
+ configured = _first_env(*_PLUGIN_API_KEY_ENV)
if not configured:
return False
@@ -429,14 +445,7 @@ def cache_plugin_api_key() -> bool:
def clear_stale_api_key_cache() -> bool:
"""Drop the cached key file once every configured key source is gone."""
- configured = (
- os.environ.get("MEM0_API_KEY")
- or os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
- if configured:
+ if _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV):
return False
path = data_dir() / "api-key"
if not path.exists():
@@ -459,12 +468,7 @@ def detached_process_kwargs(platform: str | None = None) -> dict:
def _plugin_option(name: str, fallback: str = "") -> str:
- return (
- os.environ.get(f"PLUGIN_OPTION_{name.upper()}")
- or os.environ.get(f"CLAUDE_PLUGIN_OPTION_{name.upper()}")
- or os.environ.get(fallback)
- or ""
- ).strip()
+ return _first_env(f"PLUGIN_OPTION_{name.upper()}", f"CLAUDE_PLUGIN_OPTION_{name.upper()}", fallback)
def user_id() -> str:
diff --git a/integrations/claude-code-plugin/skills/status/SKILL.md b/integrations/claude-code-plugin/skills/status/SKILL.md
index ed5e49b3f..b945d710f 100644
--- a/integrations/claude-code-plugin/skills/status/SKILL.md
+++ b/integrations/claude-code-plugin/skills/status/SKILL.md
@@ -19,5 +19,7 @@ API key is configured, the event/flush/retrieval counts (`flushes` is the
number of completed flushes, not a pending count), and the doctor check
results. If doctor reports an authentication failure (401 / invalid key), say
clearly that the Mem0 API key is invalid or expired and that memories are NOT
-being created. Never report an auth failure as "no memories found". Suggest
-reinstalling with `--config api_key=...` in that case.
+being created. Never report an auth failure as "no memories found". When the
+key is missing or invalid, suggest updating the plugin's API key setting,
+exporting `MEM0_API_KEY`, or running `mem0 init` (the plugin reads the key the
+Mem0 CLI saves in `~/.mem0/config.json`).
diff --git a/integrations/claude-code-plugin/tests/test_memory_core.py b/integrations/claude-code-plugin/tests/test_memory_core.py
index b6b1c28f9..401508c8e 100644
--- a/integrations/claude-code-plugin/tests/test_memory_core.py
+++ b/integrations/claude-code-plugin/tests/test_memory_core.py
@@ -31,9 +31,13 @@ def isolated_env(tmp_path, monkeypatch):
monkeypatch.setenv("MEM0_CODE_DATA_DIR", str(tmp_path / "data"))
monkeypatch.setenv("MEM0_CODE_USER_ID", "test-user")
monkeypatch.delenv("MEM0_API_KEY", raising=False)
+ monkeypatch.delenv("PLUGIN_OPTION_API_KEY", raising=False)
+ monkeypatch.delenv("PLUGIN_OPTION_USER_ID", raising=False)
monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_API_KEY", raising=False)
monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", raising=False)
monkeypatch.delenv("CLAUDE_PLUGIN_DATA", raising=False)
+ monkeypatch.setenv("HOME", str(tmp_path / "home"))
+ monkeypatch.setenv("USERPROFILE", str(tmp_path / "home"))
# The 0.2.x plugin exports these into every hooked shell; without this the
# suite fails for anyone running it inside a session with that plugin active.
monkeypatch.delenv("MEM0_PROJECT_ID", raising=False)
@@ -3750,6 +3754,71 @@ def test_stale_cached_api_key_is_cleared_when_config_is_removed(
assert memory_core.clear_stale_api_key_cache() is False
+def _mem0_cli_init(home: Path, config: object) -> None:
+ (home / ".mem0").mkdir(parents=True, exist_ok=True)
+ (home / ".mem0" / "config.json").write_text(json.dumps(config), encoding="utf-8")
+
+
+def test_api_key_falls_back_to_the_mem0_cli_config(isolated_env):
+ _mem0_cli_init(isolated_env / "home", {"platform": {"api_key": " m0-cli-key\n"}})
+
+ assert memory_core.api_key() == "m0-cli-key"
+
+
+@pytest.mark.parametrize(
+ "config",
+ [{"platform": {}}, {"platform": "m0-oops"}, {"platform": {"api_key": 42}}, ["m0-list"]],
+)
+def test_malformed_mem0_cli_config_reads_as_no_key(isolated_env, config):
+ _mem0_cli_init(isolated_env / "home", config)
+
+ assert memory_core.api_key() == ""
+
+
+def test_unreadable_mem0_cli_config_reads_as_no_key(isolated_env):
+ (isolated_env / "home" / ".mem0").mkdir(parents=True)
+ (isolated_env / "home" / ".mem0" / "config.json").write_text("{not json", encoding="utf-8")
+
+ assert memory_core.api_key() == ""
+
+
+def test_plugin_configured_key_wins_over_the_mem0_cli_config(isolated_env, monkeypatch):
+ _mem0_cli_init(isolated_env / "home", {"platform": {"api_key": "m0-cli-key"}})
+ monkeypatch.setenv("PLUGIN_OPTION_API_KEY", "m0-plugin-key")
+ assert memory_core.cache_plugin_api_key() is True
+ assert memory_core.api_key() == "m0-plugin-key"
+
+ monkeypatch.delenv("PLUGIN_OPTION_API_KEY")
+ assert memory_core.api_key() == "m0-plugin-key"
+
+
+def test_unexpanded_host_placeholder_is_never_used_as_the_api_key(isolated_env, monkeypatch):
+ monkeypatch.setenv("PLUGIN_OPTION_API_KEY", "${api_key}")
+
+ assert memory_core.cache_plugin_api_key() is False
+ assert not (isolated_env / "data" / "api-key").exists()
+ assert memory_core.api_key() == ""
+
+ _mem0_cli_init(isolated_env / "home", {"platform": {"api_key": "m0-cli-key"}})
+ assert memory_core.api_key() == "m0-cli-key"
+
+
+def test_placeholder_cached_by_an_older_plugin_is_ignored(isolated_env):
+ (isolated_env / "data").mkdir()
+ (isolated_env / "data" / "api-key").write_text("${api_key}", encoding="utf-8")
+ _mem0_cli_init(isolated_env / "home", {"platform": {"api_key": "m0-cli-key"}})
+
+ assert memory_core.api_key() == "m0-cli-key"
+
+
+def test_unexpanded_placeholder_plugin_options_fall_back(isolated_env, monkeypatch):
+ monkeypatch.setenv("PLUGIN_OPTION_USER_ID", "${user_id}")
+ monkeypatch.setenv("PLUGIN_OPTION_TOP_K", "${top_k}")
+
+ assert memory_core.user_id() == "test-user"
+ assert memory_core._plugin_option("top_k") == ""
+
+
def _big_batch_messages() -> list[dict[str, str]]:
return [
{"role": "user", "content": "A" * 20000},
diff --git a/integrations/claude-code-plugin/tests/test_telemetry.py b/integrations/claude-code-plugin/tests/test_telemetry.py
index 73bf8f121..7e29bfbc8 100644
--- a/integrations/claude-code-plugin/tests/test_telemetry.py
+++ b/integrations/claude-code-plugin/tests/test_telemetry.py
@@ -24,6 +24,8 @@ def isolated_env(tmp_path, monkeypatch):
monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_API_KEY", raising=False)
monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", raising=False)
monkeypatch.delenv("MEM0_API_URL", raising=False)
+ monkeypatch.setenv("HOME", str(tmp_path / "home"))
+ monkeypatch.setenv("USERPROFILE", str(tmp_path / "home"))
return tmp_path
diff --git a/integrations/codex-plugin/core/memory_core.py b/integrations/codex-plugin/core/memory_core.py
index 9b420a063..7695f0ac0 100644
--- a/integrations/codex-plugin/core/memory_core.py
+++ b/integrations/codex-plugin/core/memory_core.py
@@ -378,30 +378,46 @@ def resolve_repo(cwd: str | None) -> RepoContext:
return _resolve_repo_cached(os.path.abspath(cwd or os.getcwd()))
+_PLUGIN_API_KEY_ENV = (
+ "PLUGIN_OPTION_API_KEY",
+ "CLAUDE_PLUGIN_OPTION_API_KEY",
+ "CLAUDE_PLUGIN_OPTION_MEM0_API_KEY",
+)
+
+
+def _configured(value: object) -> str:
+ """The stripped value, or empty when the host left its ${placeholder} unexpanded."""
+ text = value.strip() if isinstance(value, str) else ""
+ return "" if text.startswith("${") and text.endswith("}") else text
+
+
+def _first_env(*names: str) -> str:
+ return next((value for name in names if (value := _configured(os.environ.get(name)))), "")
+
+
+def _mem0_cli_api_key() -> str:
+ """The key `mem0 init` saved to the Mem0 CLI config."""
+ try:
+ config = json.loads((Path.home() / ".mem0" / "config.json").read_text(encoding="utf-8"))
+ return _configured(config["platform"]["api_key"])
+ except (OSError, ValueError, LookupError, TypeError):
+ return ""
+
+
def api_key() -> str:
- configured = (
- os.environ.get("MEM0_API_KEY")
- or os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
+ configured = _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV)
if configured:
return configured
try:
- return (data_dir() / "api-key").read_text(encoding="utf-8").strip()
+ cached = _configured((data_dir() / "api-key").read_text(encoding="utf-8"))
except OSError:
- return ""
+ cached = ""
+ return cached or _mem0_cli_api_key()
def cache_plugin_api_key() -> bool:
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
- configured = (
- os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
+ configured = _first_env(*_PLUGIN_API_KEY_ENV)
if not configured:
return False
@@ -429,14 +445,7 @@ def cache_plugin_api_key() -> bool:
def clear_stale_api_key_cache() -> bool:
"""Drop the cached key file once every configured key source is gone."""
- configured = (
- os.environ.get("MEM0_API_KEY")
- or os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
- if configured:
+ if _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV):
return False
path = data_dir() / "api-key"
if not path.exists():
@@ -459,12 +468,7 @@ def detached_process_kwargs(platform: str | None = None) -> dict:
def _plugin_option(name: str, fallback: str = "") -> str:
- return (
- os.environ.get(f"PLUGIN_OPTION_{name.upper()}")
- or os.environ.get(f"CLAUDE_PLUGIN_OPTION_{name.upper()}")
- or os.environ.get(fallback)
- or ""
- ).strip()
+ return _first_env(f"PLUGIN_OPTION_{name.upper()}", f"CLAUDE_PLUGIN_OPTION_{name.upper()}", fallback)
def user_id() -> str:
diff --git a/integrations/codex-plugin/skills/status/SKILL.md b/integrations/codex-plugin/skills/status/SKILL.md
index 4f67cc829..8d9556a72 100644
--- a/integrations/codex-plugin/skills/status/SKILL.md
+++ b/integrations/codex-plugin/skills/status/SKILL.md
@@ -19,5 +19,7 @@ API key is configured, the event/flush/retrieval counts (`flushes` is the
number of completed flushes, not a pending count), and the doctor check
results. If doctor reports an authentication failure (401 / invalid key), say
clearly that the Mem0 API key is invalid or expired and that memories are NOT
-being created. Never report an auth failure as "no memories found". Suggest
-reinstalling with `--config api_key=...` in that case.
+being created. Never report an auth failure as "no memories found". When the
+key is missing or invalid, suggest updating the plugin's API key setting,
+exporting `MEM0_API_KEY`, or running `mem0 init` (the plugin reads the key the
+Mem0 CLI saves in `~/.mem0/config.json`).
diff --git a/integrations/cursor-plugin/core/memory_core.py b/integrations/cursor-plugin/core/memory_core.py
index 9b420a063..7695f0ac0 100644
--- a/integrations/cursor-plugin/core/memory_core.py
+++ b/integrations/cursor-plugin/core/memory_core.py
@@ -378,30 +378,46 @@ def resolve_repo(cwd: str | None) -> RepoContext:
return _resolve_repo_cached(os.path.abspath(cwd or os.getcwd()))
+_PLUGIN_API_KEY_ENV = (
+ "PLUGIN_OPTION_API_KEY",
+ "CLAUDE_PLUGIN_OPTION_API_KEY",
+ "CLAUDE_PLUGIN_OPTION_MEM0_API_KEY",
+)
+
+
+def _configured(value: object) -> str:
+ """The stripped value, or empty when the host left its ${placeholder} unexpanded."""
+ text = value.strip() if isinstance(value, str) else ""
+ return "" if text.startswith("${") and text.endswith("}") else text
+
+
+def _first_env(*names: str) -> str:
+ return next((value for name in names if (value := _configured(os.environ.get(name)))), "")
+
+
+def _mem0_cli_api_key() -> str:
+ """The key `mem0 init` saved to the Mem0 CLI config."""
+ try:
+ config = json.loads((Path.home() / ".mem0" / "config.json").read_text(encoding="utf-8"))
+ return _configured(config["platform"]["api_key"])
+ except (OSError, ValueError, LookupError, TypeError):
+ return ""
+
+
def api_key() -> str:
- configured = (
- os.environ.get("MEM0_API_KEY")
- or os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
+ configured = _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV)
if configured:
return configured
try:
- return (data_dir() / "api-key").read_text(encoding="utf-8").strip()
+ cached = _configured((data_dir() / "api-key").read_text(encoding="utf-8"))
except OSError:
- return ""
+ cached = ""
+ return cached or _mem0_cli_api_key()
def cache_plugin_api_key() -> bool:
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
- configured = (
- os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
+ configured = _first_env(*_PLUGIN_API_KEY_ENV)
if not configured:
return False
@@ -429,14 +445,7 @@ def cache_plugin_api_key() -> bool:
def clear_stale_api_key_cache() -> bool:
"""Drop the cached key file once every configured key source is gone."""
- configured = (
- os.environ.get("MEM0_API_KEY")
- or os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
- if configured:
+ if _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV):
return False
path = data_dir() / "api-key"
if not path.exists():
@@ -459,12 +468,7 @@ def detached_process_kwargs(platform: str | None = None) -> dict:
def _plugin_option(name: str, fallback: str = "") -> str:
- return (
- os.environ.get(f"PLUGIN_OPTION_{name.upper()}")
- or os.environ.get(f"CLAUDE_PLUGIN_OPTION_{name.upper()}")
- or os.environ.get(fallback)
- or ""
- ).strip()
+ return _first_env(f"PLUGIN_OPTION_{name.upper()}", f"CLAUDE_PLUGIN_OPTION_{name.upper()}", fallback)
def user_id() -> str:
diff --git a/integrations/cursor-plugin/skills/status/SKILL.md b/integrations/cursor-plugin/skills/status/SKILL.md
index 23ad94e33..1b6c9fa3c 100644
--- a/integrations/cursor-plugin/skills/status/SKILL.md
+++ b/integrations/cursor-plugin/skills/status/SKILL.md
@@ -19,5 +19,7 @@ API key is configured, the event/flush/retrieval counts (`flushes` is the
number of completed flushes, not a pending count), and the doctor check
results. If doctor reports an authentication failure (401 / invalid key), say
clearly that the Mem0 API key is invalid or expired and that memories are NOT
-being created. Never report an auth failure as "no memories found". Suggest
-reinstalling with `--config api_key=...` in that case.
+being created. Never report an auth failure as "no memories found". When the
+key is missing or invalid, suggest updating the plugin's API key setting,
+exporting `MEM0_API_KEY`, or running `mem0 init` (the plugin reads the key the
+Mem0 CLI saves in `~/.mem0/config.json`).
diff --git a/integrations/deepseek-plugin/src/index.ts b/integrations/deepseek-plugin/src/index.ts
index c5eb64390..16f183360 100644
--- a/integrations/deepseek-plugin/src/index.ts
+++ b/integrations/deepseek-plugin/src/index.ts
@@ -10,6 +10,7 @@
* tools registered via `ctx.tools.register(...)` are auto-unregistered when the
* plugin unmounts (Cordis revertible effects).
*/
+import { homedir } from "node:os";
import type { Context } from "@deepseek-ai/cordis";
import type {} from "@deepseek-ai/dsh-agent";
import type { PromptAssembly } from "@deepseek-ai/dsh-system-prompt";
@@ -20,6 +21,7 @@ import { formatMemoryList, formatAddResult } from "./formatting.ts";
import { truncateOutput } from "./output.ts";
import { resolveSearchFilters, resolveAddParams } from "./scoping.ts";
import { captureEvent, errorKind } from "./telemetry.ts";
+import { mem0CliApiKey } from "../../agent-plugin-core/typescript/src/credentials.ts";
import { createMemoryLifecycle } from "../../agent-plugin-core/typescript/src/lifecycle.ts";
import {
USER_RECALL_HEADING,
@@ -94,7 +96,7 @@ const scopeParams = {
} as const;
export function apply(ctx: Context, config: Config): void {
- const apiKey = config.apiKey ?? process.env.MEM0_API_KEY;
+ const apiKey = config.apiKey || process.env.MEM0_API_KEY || mem0CliApiKey(homedir());
if (!apiKey) {
throw new Error("deepseek-plugin: set config.apiKey or the MEM0_API_KEY env var");
}
diff --git a/integrations/deepseek-plugin/tests/apply.test.ts b/integrations/deepseek-plugin/tests/apply.test.ts
index 797b69695..484231381 100644
--- a/integrations/deepseek-plugin/tests/apply.test.ts
+++ b/integrations/deepseek-plugin/tests/apply.test.ts
@@ -1,12 +1,19 @@
+import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Offline mock of the Mem0 SDK so these tests never touch the network.
const mockSearch = vi.fn();
const mockAdd = vi.fn();
+const mockClientOptions = vi.fn();
vi.mock("mem0ai", () => ({
MemoryClient: class {
search = mockSearch;
add = mockAdd;
+ constructor(options: unknown) {
+ mockClientOptions(options);
+ }
},
}));
@@ -49,18 +56,24 @@ function applyAndCollectListeners(config: Config): Map
let savedKey: string | undefined;
let savedTelemetry: string | undefined;
+let savedHome: string | undefined;
beforeEach(() => {
savedKey = process.env.MEM0_API_KEY;
savedTelemetry = process.env.MEM0_TELEMETRY;
+ savedHome = process.env.HOME;
process.env.MEM0_TELEMETRY = "false";
+ process.env.HOME = mkdtempSync(join(tmpdir(), "deepseek-home-"));
mockSearch.mockReset();
mockAdd.mockReset();
+ mockClientOptions.mockReset();
});
afterEach(() => {
if (savedKey === undefined) delete process.env.MEM0_API_KEY;
else process.env.MEM0_API_KEY = savedKey;
+ if (savedHome === undefined) delete process.env.HOME;
+ else process.env.HOME = savedHome;
if (savedTelemetry === undefined) delete process.env.MEM0_TELEMETRY;
else process.env.MEM0_TELEMETRY = savedTelemetry;
});
@@ -71,6 +84,17 @@ describe("apply() config validation", () => {
expect(() => applyAndCollect({ userId: "u" } as Config)).toThrow(/apiKey|MEM0_API_KEY/);
});
+ it("falls back to the key mem0 init saved", () => {
+ delete process.env.MEM0_API_KEY;
+ const home = process.env.HOME as string;
+ mkdirSync(join(home, ".mem0"));
+ writeFileSync(join(home, ".mem0", "config.json"), JSON.stringify({ platform: { api_key: "m0-cli-key" } }));
+
+ applyAndCollect({ userId: "u" } as Config);
+
+ expect(mockClientOptions).toHaveBeenCalledWith({ apiKey: "m0-cli-key" });
+ });
+
it("throws when userId is missing", () => {
expect(() => applyAndCollect({ apiKey: "k", userId: "" } as Config)).toThrow(/userId/);
});
diff --git a/integrations/kimi-plugin/core/memory_core.py b/integrations/kimi-plugin/core/memory_core.py
index 9b420a063..7695f0ac0 100644
--- a/integrations/kimi-plugin/core/memory_core.py
+++ b/integrations/kimi-plugin/core/memory_core.py
@@ -378,30 +378,46 @@ def resolve_repo(cwd: str | None) -> RepoContext:
return _resolve_repo_cached(os.path.abspath(cwd or os.getcwd()))
+_PLUGIN_API_KEY_ENV = (
+ "PLUGIN_OPTION_API_KEY",
+ "CLAUDE_PLUGIN_OPTION_API_KEY",
+ "CLAUDE_PLUGIN_OPTION_MEM0_API_KEY",
+)
+
+
+def _configured(value: object) -> str:
+ """The stripped value, or empty when the host left its ${placeholder} unexpanded."""
+ text = value.strip() if isinstance(value, str) else ""
+ return "" if text.startswith("${") and text.endswith("}") else text
+
+
+def _first_env(*names: str) -> str:
+ return next((value for name in names if (value := _configured(os.environ.get(name)))), "")
+
+
+def _mem0_cli_api_key() -> str:
+ """The key `mem0 init` saved to the Mem0 CLI config."""
+ try:
+ config = json.loads((Path.home() / ".mem0" / "config.json").read_text(encoding="utf-8"))
+ return _configured(config["platform"]["api_key"])
+ except (OSError, ValueError, LookupError, TypeError):
+ return ""
+
+
def api_key() -> str:
- configured = (
- os.environ.get("MEM0_API_KEY")
- or os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
+ configured = _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV)
if configured:
return configured
try:
- return (data_dir() / "api-key").read_text(encoding="utf-8").strip()
+ cached = _configured((data_dir() / "api-key").read_text(encoding="utf-8"))
except OSError:
- return ""
+ cached = ""
+ return cached or _mem0_cli_api_key()
def cache_plugin_api_key() -> bool:
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
- configured = (
- os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
+ configured = _first_env(*_PLUGIN_API_KEY_ENV)
if not configured:
return False
@@ -429,14 +445,7 @@ def cache_plugin_api_key() -> bool:
def clear_stale_api_key_cache() -> bool:
"""Drop the cached key file once every configured key source is gone."""
- configured = (
- os.environ.get("MEM0_API_KEY")
- or os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
- if configured:
+ if _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV):
return False
path = data_dir() / "api-key"
if not path.exists():
@@ -459,12 +468,7 @@ def detached_process_kwargs(platform: str | None = None) -> dict:
def _plugin_option(name: str, fallback: str = "") -> str:
- return (
- os.environ.get(f"PLUGIN_OPTION_{name.upper()}")
- or os.environ.get(f"CLAUDE_PLUGIN_OPTION_{name.upper()}")
- or os.environ.get(fallback)
- or ""
- ).strip()
+ return _first_env(f"PLUGIN_OPTION_{name.upper()}", f"CLAUDE_PLUGIN_OPTION_{name.upper()}", fallback)
def user_id() -> str:
diff --git a/integrations/kimi-plugin/skills/status/SKILL.md b/integrations/kimi-plugin/skills/status/SKILL.md
index 165c0c675..db19f3f17 100644
--- a/integrations/kimi-plugin/skills/status/SKILL.md
+++ b/integrations/kimi-plugin/skills/status/SKILL.md
@@ -19,5 +19,7 @@ API key is configured, the event/flush/retrieval counts (`flushes` is the
number of completed flushes, not a pending count), and the doctor check
results. If doctor reports an authentication failure (401 / invalid key), say
clearly that the Mem0 API key is invalid or expired and that memories are NOT
-being created. Never report an auth failure as "no memories found". Suggest
-reinstalling with `--config api_key=...` in that case.
+being created. Never report an auth failure as "no memories found". When the
+key is missing or invalid, suggest updating the plugin's API key setting,
+exporting `MEM0_API_KEY`, or running `mem0 init` (the plugin reads the key the
+Mem0 CLI saves in `~/.mem0/config.json`).
diff --git a/integrations/mem0-agent-plugin/core/memory_core.py b/integrations/mem0-agent-plugin/core/memory_core.py
index 9b420a063..7695f0ac0 100644
--- a/integrations/mem0-agent-plugin/core/memory_core.py
+++ b/integrations/mem0-agent-plugin/core/memory_core.py
@@ -378,30 +378,46 @@ def resolve_repo(cwd: str | None) -> RepoContext:
return _resolve_repo_cached(os.path.abspath(cwd or os.getcwd()))
+_PLUGIN_API_KEY_ENV = (
+ "PLUGIN_OPTION_API_KEY",
+ "CLAUDE_PLUGIN_OPTION_API_KEY",
+ "CLAUDE_PLUGIN_OPTION_MEM0_API_KEY",
+)
+
+
+def _configured(value: object) -> str:
+ """The stripped value, or empty when the host left its ${placeholder} unexpanded."""
+ text = value.strip() if isinstance(value, str) else ""
+ return "" if text.startswith("${") and text.endswith("}") else text
+
+
+def _first_env(*names: str) -> str:
+ return next((value for name in names if (value := _configured(os.environ.get(name)))), "")
+
+
+def _mem0_cli_api_key() -> str:
+ """The key `mem0 init` saved to the Mem0 CLI config."""
+ try:
+ config = json.loads((Path.home() / ".mem0" / "config.json").read_text(encoding="utf-8"))
+ return _configured(config["platform"]["api_key"])
+ except (OSError, ValueError, LookupError, TypeError):
+ return ""
+
+
def api_key() -> str:
- configured = (
- os.environ.get("MEM0_API_KEY")
- or os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
+ configured = _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV)
if configured:
return configured
try:
- return (data_dir() / "api-key").read_text(encoding="utf-8").strip()
+ cached = _configured((data_dir() / "api-key").read_text(encoding="utf-8"))
except OSError:
- return ""
+ cached = ""
+ return cached or _mem0_cli_api_key()
def cache_plugin_api_key() -> bool:
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
- configured = (
- os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
+ configured = _first_env(*_PLUGIN_API_KEY_ENV)
if not configured:
return False
@@ -429,14 +445,7 @@ def cache_plugin_api_key() -> bool:
def clear_stale_api_key_cache() -> bool:
"""Drop the cached key file once every configured key source is gone."""
- configured = (
- os.environ.get("MEM0_API_KEY")
- or os.environ.get("PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
- or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
- or ""
- ).strip()
- if configured:
+ if _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV):
return False
path = data_dir() / "api-key"
if not path.exists():
@@ -459,12 +468,7 @@ def detached_process_kwargs(platform: str | None = None) -> dict:
def _plugin_option(name: str, fallback: str = "") -> str:
- return (
- os.environ.get(f"PLUGIN_OPTION_{name.upper()}")
- or os.environ.get(f"CLAUDE_PLUGIN_OPTION_{name.upper()}")
- or os.environ.get(fallback)
- or ""
- ).strip()
+ return _first_env(f"PLUGIN_OPTION_{name.upper()}", f"CLAUDE_PLUGIN_OPTION_{name.upper()}", fallback)
def user_id() -> str:
diff --git a/integrations/mem0-agent-plugin/skills/status/SKILL.md b/integrations/mem0-agent-plugin/skills/status/SKILL.md
index 7717e656b..f62ff9ceb 100644
--- a/integrations/mem0-agent-plugin/skills/status/SKILL.md
+++ b/integrations/mem0-agent-plugin/skills/status/SKILL.md
@@ -18,5 +18,7 @@ API key is configured, the event/flush/retrieval counts (`flushes` is the
number of completed flushes, not a pending count), and the doctor check
results. If doctor reports an authentication failure (401 / invalid key), say
clearly that the Mem0 API key is invalid or expired and that memories are NOT
-being created. Never report an auth failure as "no memories found". Suggest
-reinstalling with `--config api_key=...` in that case.
+being created. Never report an auth failure as "no memories found". When the
+key is missing or invalid, suggest updating the plugin's API key setting,
+exporting `MEM0_API_KEY`, or running `mem0 init` (the plugin reads the key the
+Mem0 CLI saves in `~/.mem0/config.json`).
diff --git a/integrations/opencode-plugin/api-key.test.ts b/integrations/opencode-plugin/api-key.test.ts
index ca2a2a931..c52bb8ecb 100644
--- a/integrations/opencode-plugin/api-key.test.ts
+++ b/integrations/opencode-plugin/api-key.test.ts
@@ -102,6 +102,16 @@ describe("resolveApiKey", () => {
expect(resolveApiKey({}, dir)).toBe("m0-later");
});
+ test("falls back to the key mem0 init saved", () => {
+ const dir = home();
+ mkdirSync(join(dir, ".mem0"));
+ writeFileSync(join(dir, ".mem0", "config.json"), JSON.stringify({platform: {api_key: "m0-cli-key"}}));
+ expect(resolveApiKey({}, dir)).toBe("m0-cli-key");
+
+ writeFileSync(join(dir, ".zshrc"), "export MEM0_API_KEY=m0-from-profile\n");
+ expect(resolveApiKey({}, dir)).toBe("m0-from-profile");
+ });
+
test("ignores unsupported files and invalid assignments", () => {
const dir = home();
writeFileSync(join(dir, ".env"), "MEM0_API_KEY=unsupported\n");
diff --git a/integrations/opencode-plugin/api-key.ts b/integrations/opencode-plugin/api-key.ts
index b3eae9543..b1ea38043 100644
--- a/integrations/opencode-plugin/api-key.ts
+++ b/integrations/opencode-plugin/api-key.ts
@@ -1,6 +1,7 @@
import {readFileSync} from "fs";
import {homedir} from "os";
import {join} from "path";
+import {mem0CliApiKey} from "../agent-plugin-core/typescript/src/credentials.ts";
const PROFILE_FILES = [".zshrc", ".bashrc", ".zprofile", ".bash_profile", ".profile"];
@@ -26,5 +27,5 @@ export function resolveApiKey(env: NodeJS.ProcessEnv = process.env, homeDir = ho
}
}
- return "";
+ return mem0CliApiKey(homeDir);
}
diff --git a/integrations/pi-agent-plugin/src/config/index.ts b/integrations/pi-agent-plugin/src/config/index.ts
index 36746b50e..faf61b559 100644
--- a/integrations/pi-agent-plugin/src/config/index.ts
+++ b/integrations/pi-agent-plugin/src/config/index.ts
@@ -1,6 +1,7 @@
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
+import { mem0CliApiKey } from "../../../agent-plugin-core/typescript/src/credentials.ts";
import type { Mem0Config } from "../types.ts";
const AGENT_ROOT = path.join(os.homedir(), ".pi", "agent");
@@ -36,6 +37,9 @@ export function loadConfig(): Mem0Config {
if (process.env.MEM0_API_KEY) {
config.apiKey = process.env.MEM0_API_KEY;
}
+ if (!config.apiKey) {
+ config.apiKey = mem0CliApiKey(os.homedir());
+ }
if (process.env.MEM0_USER_ID) {
config.userId = process.env.MEM0_USER_ID;
}
diff --git a/integrations/pi-agent-plugin/tests/config.test.ts b/integrations/pi-agent-plugin/tests/config.test.ts
index 524cbd2ef..018222b71 100644
--- a/integrations/pi-agent-plugin/tests/config.test.ts
+++ b/integrations/pi-agent-plugin/tests/config.test.ts
@@ -1,5 +1,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import * as fs from "node:fs";
+import * as path from "node:path";
import { loadConfig } from "../src/config/index.ts";
vi.mock("node:fs");
@@ -33,6 +34,17 @@ describe("loadConfig", () => {
expect(config.apiKey).toBe("");
});
+ it("falls back to the key mem0 init saved", () => {
+ delete process.env.MEM0_API_KEY;
+ vi.mocked(fs.readFileSync).mockImplementation((file) => {
+ if (String(file).endsWith(path.join(".mem0", "config.json"))) {
+ return JSON.stringify({ platform: { api_key: "m0-cli-key" } });
+ }
+ throw new Error("ENOENT");
+ });
+ expect(loadConfig().apiKey).toBe("m0-cli-key");
+ });
+
it("reads config file and merges with defaults", () => {
delete process.env.MEM0_API_KEY;
delete process.env.MEM0_USER_ID;