fix(plugins): read the mem0 CLI key and ignore unexpanded host placeholders
The Python plugin core and the opencode, pi and deepseek plugins now fall back to the key `mem0 init` saves in ~/.mem0/config.json, so a configured CLI is enough to authenticate. The Python core also stops using or caching a literal ${api_key} left behind when a host (Cursor) does not expand its plugin variables, which is what kept asking for auth after the key was set.
Fixes #7346
This commit is contained in:
@@ -28,6 +28,10 @@ echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc && source ~/.bashrc
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Tip>
|
||||
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.
|
||||
</Tip>
|
||||
|
||||
## Installation
|
||||
|
||||
**Option A: degit** (recommended):
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -35,6 +35,10 @@ source ~/.bashrc
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Tip>
|
||||
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.
|
||||
</Tip>
|
||||
|
||||
## Installation
|
||||
|
||||
### Option A: Plugin Marketplace (Recommended)
|
||||
|
||||
@@ -35,6 +35,10 @@ source ~/.bashrc
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Tip>
|
||||
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.
|
||||
</Tip>
|
||||
|
||||
<Warning>
|
||||
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.
|
||||
</Warning>
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -56,6 +56,10 @@ source ~/.bashrc
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Tip>
|
||||
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.
|
||||
</Tip>
|
||||
|
||||
## 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) |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -26,6 +26,10 @@ echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc && source ~/.bashrc
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Tip>
|
||||
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.
|
||||
</Tip>
|
||||
|
||||
## Installation
|
||||
|
||||
### Option A: Plugin Install (Recommended)
|
||||
|
||||
@@ -40,6 +40,10 @@ source ~/.bashrc
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Tip>
|
||||
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.
|
||||
</Tip>
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
@@ -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 } }))), "");
|
||||
});
|
||||
@@ -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:
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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<string, HarnessListener>
|
||||
|
||||
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/);
|
||||
});
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user