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;