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>
|
</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
|
## Installation
|
||||||
|
|
||||||
**Option A: degit** (recommended):
|
**Option A: degit** (recommended):
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ claude plugin update mem0@mem0-plugins --scope user
|
|||||||
|
|
||||||
| Problem | Fix |
|
| 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. |
|
| `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. |
|
| 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. |
|
| 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>
|
</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
|
## Installation
|
||||||
|
|
||||||
### Option A: Plugin Marketplace (Recommended)
|
### Option A: Plugin Marketplace (Recommended)
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ source ~/.bashrc
|
|||||||
```
|
```
|
||||||
</CodeGroup>
|
</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>
|
<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.
|
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>
|
</Warning>
|
||||||
@@ -164,6 +168,7 @@ Captured prompts and responses retain their full redacted text without a per-mes
|
|||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
- **"Connection failed"**: Verify `MEM0_API_KEY` is set: `echo $MEM0_API_KEY`
|
- **"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.
|
- **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
|
- **No tools appearing**: Go to Cursor Settings > MCP and verify the `mem0` server shows as connected
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ source ~/.bashrc
|
|||||||
```
|
```
|
||||||
</CodeGroup>
|
</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
|
## Try it locally
|
||||||
|
|
||||||
1. Build and pack the plugin:
|
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 |
|
| 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 |
|
| `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 |
|
| `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) |
|
| `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 |
|
| 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`. |
|
| Plugin changes do not appear | Run `/plugins reload`, then `/reload` or `/new`. |
|
||||||
| MCP server is disabled | Run `/plugins mcp enable mem0 mem0`, then `/reload`. |
|
| 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. |
|
| 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>
|
</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
|
## Installation
|
||||||
|
|
||||||
### Option A: Plugin Install (Recommended)
|
### Option A: Plugin Install (Recommended)
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ source ~/.bashrc
|
|||||||
```
|
```
|
||||||
</CodeGroup>
|
</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
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -68,7 +72,7 @@ For advanced settings, create `~/.pi/agent/mem0-config.json`:
|
|||||||
|
|
||||||
| Key | Type | Default | Description |
|
| 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 |
|
| `userId` | `string` | `$MEM0_USER_ID` or `"default"` | User identity for memory scoping |
|
||||||
| `autoCapture` | `boolean` | `true` | Store facts from conversations automatically |
|
| `autoCapture` | `boolean` | `true` | Store facts from conversations automatically |
|
||||||
| `defaultScope` | `string` | `"project"` | Default memory scope: `project`, `session`, or `global` |
|
| `defaultScope` | `string` | `"project"` | Default memory scope: `project`, `session`, or `global` |
|
||||||
@@ -147,7 +151,7 @@ You: What do you know about my preferences?
|
|||||||
|
|
||||||
## Troubleshooting
|
## 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
|
- **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
|
- **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
|
- **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()))
|
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:
|
def api_key() -> str:
|
||||||
configured = (
|
configured = _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV)
|
||||||
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 configured:
|
||||||
return configured
|
return configured
|
||||||
try:
|
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:
|
except OSError:
|
||||||
return ""
|
cached = ""
|
||||||
|
return cached or _mem0_cli_api_key()
|
||||||
|
|
||||||
|
|
||||||
def cache_plugin_api_key() -> bool:
|
def cache_plugin_api_key() -> bool:
|
||||||
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
|
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
|
||||||
configured = (
|
configured = _first_env(*_PLUGIN_API_KEY_ENV)
|
||||||
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 not configured:
|
if not configured:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -429,14 +445,7 @@ def cache_plugin_api_key() -> bool:
|
|||||||
|
|
||||||
def clear_stale_api_key_cache() -> bool:
|
def clear_stale_api_key_cache() -> bool:
|
||||||
"""Drop the cached key file once every configured key source is gone."""
|
"""Drop the cached key file once every configured key source is gone."""
|
||||||
configured = (
|
if _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV):
|
||||||
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:
|
|
||||||
return False
|
return False
|
||||||
path = data_dir() / "api-key"
|
path = data_dir() / "api-key"
|
||||||
if not path.exists():
|
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:
|
def _plugin_option(name: str, fallback: str = "") -> str:
|
||||||
return (
|
return _first_env(f"PLUGIN_OPTION_{name.upper()}", f"CLAUDE_PLUGIN_OPTION_{name.upper()}", fallback)
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
def user_id() -> str:
|
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
|
number of completed flushes, not a pending count), and the doctor check
|
||||||
results. If doctor reports an authentication failure (401 / invalid key), say
|
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
|
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
|
being created. Never report an auth failure as "no memories found". When the
|
||||||
reinstalling with `--config api_key=...` in that case.
|
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()))
|
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:
|
def api_key() -> str:
|
||||||
configured = (
|
configured = _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV)
|
||||||
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 configured:
|
||||||
return configured
|
return configured
|
||||||
try:
|
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:
|
except OSError:
|
||||||
return ""
|
cached = ""
|
||||||
|
return cached or _mem0_cli_api_key()
|
||||||
|
|
||||||
|
|
||||||
def cache_plugin_api_key() -> bool:
|
def cache_plugin_api_key() -> bool:
|
||||||
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
|
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
|
||||||
configured = (
|
configured = _first_env(*_PLUGIN_API_KEY_ENV)
|
||||||
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 not configured:
|
if not configured:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -429,14 +445,7 @@ def cache_plugin_api_key() -> bool:
|
|||||||
|
|
||||||
def clear_stale_api_key_cache() -> bool:
|
def clear_stale_api_key_cache() -> bool:
|
||||||
"""Drop the cached key file once every configured key source is gone."""
|
"""Drop the cached key file once every configured key source is gone."""
|
||||||
configured = (
|
if _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV):
|
||||||
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:
|
|
||||||
return False
|
return False
|
||||||
path = data_dir() / "api-key"
|
path = data_dir() / "api-key"
|
||||||
if not path.exists():
|
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:
|
def _plugin_option(name: str, fallback: str = "") -> str:
|
||||||
return (
|
return _first_env(f"PLUGIN_OPTION_{name.upper()}", f"CLAUDE_PLUGIN_OPTION_{name.upper()}", fallback)
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
def user_id() -> str:
|
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
|
number of completed flushes, not a pending count), and the doctor check
|
||||||
results. If doctor reports an authentication failure (401 / invalid key), say
|
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
|
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
|
being created. Never report an auth failure as "no memories found". When the
|
||||||
reinstalling with `--config api_key=...` in that case.
|
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()))
|
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:
|
def api_key() -> str:
|
||||||
configured = (
|
configured = _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV)
|
||||||
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 configured:
|
||||||
return configured
|
return configured
|
||||||
try:
|
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:
|
except OSError:
|
||||||
return ""
|
cached = ""
|
||||||
|
return cached or _mem0_cli_api_key()
|
||||||
|
|
||||||
|
|
||||||
def cache_plugin_api_key() -> bool:
|
def cache_plugin_api_key() -> bool:
|
||||||
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
|
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
|
||||||
configured = (
|
configured = _first_env(*_PLUGIN_API_KEY_ENV)
|
||||||
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 not configured:
|
if not configured:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -429,14 +445,7 @@ def cache_plugin_api_key() -> bool:
|
|||||||
|
|
||||||
def clear_stale_api_key_cache() -> bool:
|
def clear_stale_api_key_cache() -> bool:
|
||||||
"""Drop the cached key file once every configured key source is gone."""
|
"""Drop the cached key file once every configured key source is gone."""
|
||||||
configured = (
|
if _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV):
|
||||||
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:
|
|
||||||
return False
|
return False
|
||||||
path = data_dir() / "api-key"
|
path = data_dir() / "api-key"
|
||||||
if not path.exists():
|
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:
|
def _plugin_option(name: str, fallback: str = "") -> str:
|
||||||
return (
|
return _first_env(f"PLUGIN_OPTION_{name.upper()}", f"CLAUDE_PLUGIN_OPTION_{name.upper()}", fallback)
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
def user_id() -> str:
|
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
|
number of completed flushes, not a pending count), and the doctor check
|
||||||
results. If doctor reports an authentication failure (401 / invalid key), say
|
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
|
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
|
being created. Never report an auth failure as "no memories found". When the
|
||||||
reinstalling with `--config api_key=...` in that case.
|
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_DATA_DIR", str(tmp_path / "data"))
|
||||||
monkeypatch.setenv("MEM0_CODE_USER_ID", "test-user")
|
monkeypatch.setenv("MEM0_CODE_USER_ID", "test-user")
|
||||||
monkeypatch.delenv("MEM0_API_KEY", raising=False)
|
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_API_KEY", raising=False)
|
||||||
monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", raising=False)
|
monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", raising=False)
|
||||||
monkeypatch.delenv("CLAUDE_PLUGIN_DATA", 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
|
# 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.
|
# suite fails for anyone running it inside a session with that plugin active.
|
||||||
monkeypatch.delenv("MEM0_PROJECT_ID", raising=False)
|
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
|
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]]:
|
def _big_batch_messages() -> list[dict[str, str]]:
|
||||||
return [
|
return [
|
||||||
{"role": "user", "content": "A" * 20000},
|
{"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_API_KEY", raising=False)
|
||||||
monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", raising=False)
|
monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", raising=False)
|
||||||
monkeypatch.delenv("MEM0_API_URL", 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
|
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()))
|
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:
|
def api_key() -> str:
|
||||||
configured = (
|
configured = _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV)
|
||||||
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 configured:
|
||||||
return configured
|
return configured
|
||||||
try:
|
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:
|
except OSError:
|
||||||
return ""
|
cached = ""
|
||||||
|
return cached or _mem0_cli_api_key()
|
||||||
|
|
||||||
|
|
||||||
def cache_plugin_api_key() -> bool:
|
def cache_plugin_api_key() -> bool:
|
||||||
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
|
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
|
||||||
configured = (
|
configured = _first_env(*_PLUGIN_API_KEY_ENV)
|
||||||
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 not configured:
|
if not configured:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -429,14 +445,7 @@ def cache_plugin_api_key() -> bool:
|
|||||||
|
|
||||||
def clear_stale_api_key_cache() -> bool:
|
def clear_stale_api_key_cache() -> bool:
|
||||||
"""Drop the cached key file once every configured key source is gone."""
|
"""Drop the cached key file once every configured key source is gone."""
|
||||||
configured = (
|
if _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV):
|
||||||
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:
|
|
||||||
return False
|
return False
|
||||||
path = data_dir() / "api-key"
|
path = data_dir() / "api-key"
|
||||||
if not path.exists():
|
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:
|
def _plugin_option(name: str, fallback: str = "") -> str:
|
||||||
return (
|
return _first_env(f"PLUGIN_OPTION_{name.upper()}", f"CLAUDE_PLUGIN_OPTION_{name.upper()}", fallback)
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
def user_id() -> str:
|
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
|
number of completed flushes, not a pending count), and the doctor check
|
||||||
results. If doctor reports an authentication failure (401 / invalid key), say
|
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
|
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
|
being created. Never report an auth failure as "no memories found". When the
|
||||||
reinstalling with `--config api_key=...` in that case.
|
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()))
|
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:
|
def api_key() -> str:
|
||||||
configured = (
|
configured = _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV)
|
||||||
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 configured:
|
||||||
return configured
|
return configured
|
||||||
try:
|
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:
|
except OSError:
|
||||||
return ""
|
cached = ""
|
||||||
|
return cached or _mem0_cli_api_key()
|
||||||
|
|
||||||
|
|
||||||
def cache_plugin_api_key() -> bool:
|
def cache_plugin_api_key() -> bool:
|
||||||
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
|
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
|
||||||
configured = (
|
configured = _first_env(*_PLUGIN_API_KEY_ENV)
|
||||||
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 not configured:
|
if not configured:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -429,14 +445,7 @@ def cache_plugin_api_key() -> bool:
|
|||||||
|
|
||||||
def clear_stale_api_key_cache() -> bool:
|
def clear_stale_api_key_cache() -> bool:
|
||||||
"""Drop the cached key file once every configured key source is gone."""
|
"""Drop the cached key file once every configured key source is gone."""
|
||||||
configured = (
|
if _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV):
|
||||||
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:
|
|
||||||
return False
|
return False
|
||||||
path = data_dir() / "api-key"
|
path = data_dir() / "api-key"
|
||||||
if not path.exists():
|
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:
|
def _plugin_option(name: str, fallback: str = "") -> str:
|
||||||
return (
|
return _first_env(f"PLUGIN_OPTION_{name.upper()}", f"CLAUDE_PLUGIN_OPTION_{name.upper()}", fallback)
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
def user_id() -> str:
|
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
|
number of completed flushes, not a pending count), and the doctor check
|
||||||
results. If doctor reports an authentication failure (401 / invalid key), say
|
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
|
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
|
being created. Never report an auth failure as "no memories found". When the
|
||||||
reinstalling with `--config api_key=...` in that case.
|
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
|
* tools registered via `ctx.tools.register(...)` are auto-unregistered when the
|
||||||
* plugin unmounts (Cordis revertible effects).
|
* plugin unmounts (Cordis revertible effects).
|
||||||
*/
|
*/
|
||||||
|
import { homedir } from "node:os";
|
||||||
import type { Context } from "@deepseek-ai/cordis";
|
import type { Context } from "@deepseek-ai/cordis";
|
||||||
import type {} from "@deepseek-ai/dsh-agent";
|
import type {} from "@deepseek-ai/dsh-agent";
|
||||||
import type { PromptAssembly } from "@deepseek-ai/dsh-system-prompt";
|
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 { truncateOutput } from "./output.ts";
|
||||||
import { resolveSearchFilters, resolveAddParams } from "./scoping.ts";
|
import { resolveSearchFilters, resolveAddParams } from "./scoping.ts";
|
||||||
import { captureEvent, errorKind } from "./telemetry.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 { createMemoryLifecycle } from "../../agent-plugin-core/typescript/src/lifecycle.ts";
|
||||||
import {
|
import {
|
||||||
USER_RECALL_HEADING,
|
USER_RECALL_HEADING,
|
||||||
@@ -94,7 +96,7 @@ const scopeParams = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export function apply(ctx: Context, config: Config): void {
|
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) {
|
if (!apiKey) {
|
||||||
throw new Error("deepseek-plugin: set config.apiKey or the MEM0_API_KEY env var");
|
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";
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
|
||||||
// Offline mock of the Mem0 SDK so these tests never touch the network.
|
// Offline mock of the Mem0 SDK so these tests never touch the network.
|
||||||
const mockSearch = vi.fn();
|
const mockSearch = vi.fn();
|
||||||
const mockAdd = vi.fn();
|
const mockAdd = vi.fn();
|
||||||
|
const mockClientOptions = vi.fn();
|
||||||
vi.mock("mem0ai", () => ({
|
vi.mock("mem0ai", () => ({
|
||||||
MemoryClient: class {
|
MemoryClient: class {
|
||||||
search = mockSearch;
|
search = mockSearch;
|
||||||
add = mockAdd;
|
add = mockAdd;
|
||||||
|
constructor(options: unknown) {
|
||||||
|
mockClientOptions(options);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -49,18 +56,24 @@ function applyAndCollectListeners(config: Config): Map<string, HarnessListener>
|
|||||||
|
|
||||||
let savedKey: string | undefined;
|
let savedKey: string | undefined;
|
||||||
let savedTelemetry: string | undefined;
|
let savedTelemetry: string | undefined;
|
||||||
|
let savedHome: string | undefined;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
savedKey = process.env.MEM0_API_KEY;
|
savedKey = process.env.MEM0_API_KEY;
|
||||||
savedTelemetry = process.env.MEM0_TELEMETRY;
|
savedTelemetry = process.env.MEM0_TELEMETRY;
|
||||||
|
savedHome = process.env.HOME;
|
||||||
process.env.MEM0_TELEMETRY = "false";
|
process.env.MEM0_TELEMETRY = "false";
|
||||||
|
process.env.HOME = mkdtempSync(join(tmpdir(), "deepseek-home-"));
|
||||||
mockSearch.mockReset();
|
mockSearch.mockReset();
|
||||||
mockAdd.mockReset();
|
mockAdd.mockReset();
|
||||||
|
mockClientOptions.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
if (savedKey === undefined) delete process.env.MEM0_API_KEY;
|
if (savedKey === undefined) delete process.env.MEM0_API_KEY;
|
||||||
else process.env.MEM0_API_KEY = savedKey;
|
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;
|
if (savedTelemetry === undefined) delete process.env.MEM0_TELEMETRY;
|
||||||
else process.env.MEM0_TELEMETRY = savedTelemetry;
|
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/);
|
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", () => {
|
it("throws when userId is missing", () => {
|
||||||
expect(() => applyAndCollect({ apiKey: "k", userId: "" } as Config)).toThrow(/userId/);
|
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()))
|
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:
|
def api_key() -> str:
|
||||||
configured = (
|
configured = _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV)
|
||||||
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 configured:
|
||||||
return configured
|
return configured
|
||||||
try:
|
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:
|
except OSError:
|
||||||
return ""
|
cached = ""
|
||||||
|
return cached or _mem0_cli_api_key()
|
||||||
|
|
||||||
|
|
||||||
def cache_plugin_api_key() -> bool:
|
def cache_plugin_api_key() -> bool:
|
||||||
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
|
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
|
||||||
configured = (
|
configured = _first_env(*_PLUGIN_API_KEY_ENV)
|
||||||
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 not configured:
|
if not configured:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -429,14 +445,7 @@ def cache_plugin_api_key() -> bool:
|
|||||||
|
|
||||||
def clear_stale_api_key_cache() -> bool:
|
def clear_stale_api_key_cache() -> bool:
|
||||||
"""Drop the cached key file once every configured key source is gone."""
|
"""Drop the cached key file once every configured key source is gone."""
|
||||||
configured = (
|
if _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV):
|
||||||
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:
|
|
||||||
return False
|
return False
|
||||||
path = data_dir() / "api-key"
|
path = data_dir() / "api-key"
|
||||||
if not path.exists():
|
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:
|
def _plugin_option(name: str, fallback: str = "") -> str:
|
||||||
return (
|
return _first_env(f"PLUGIN_OPTION_{name.upper()}", f"CLAUDE_PLUGIN_OPTION_{name.upper()}", fallback)
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
def user_id() -> str:
|
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
|
number of completed flushes, not a pending count), and the doctor check
|
||||||
results. If doctor reports an authentication failure (401 / invalid key), say
|
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
|
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
|
being created. Never report an auth failure as "no memories found". When the
|
||||||
reinstalling with `--config api_key=...` in that case.
|
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()))
|
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:
|
def api_key() -> str:
|
||||||
configured = (
|
configured = _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV)
|
||||||
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 configured:
|
||||||
return configured
|
return configured
|
||||||
try:
|
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:
|
except OSError:
|
||||||
return ""
|
cached = ""
|
||||||
|
return cached or _mem0_cli_api_key()
|
||||||
|
|
||||||
|
|
||||||
def cache_plugin_api_key() -> bool:
|
def cache_plugin_api_key() -> bool:
|
||||||
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
|
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
|
||||||
configured = (
|
configured = _first_env(*_PLUGIN_API_KEY_ENV)
|
||||||
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 not configured:
|
if not configured:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -429,14 +445,7 @@ def cache_plugin_api_key() -> bool:
|
|||||||
|
|
||||||
def clear_stale_api_key_cache() -> bool:
|
def clear_stale_api_key_cache() -> bool:
|
||||||
"""Drop the cached key file once every configured key source is gone."""
|
"""Drop the cached key file once every configured key source is gone."""
|
||||||
configured = (
|
if _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV):
|
||||||
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:
|
|
||||||
return False
|
return False
|
||||||
path = data_dir() / "api-key"
|
path = data_dir() / "api-key"
|
||||||
if not path.exists():
|
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:
|
def _plugin_option(name: str, fallback: str = "") -> str:
|
||||||
return (
|
return _first_env(f"PLUGIN_OPTION_{name.upper()}", f"CLAUDE_PLUGIN_OPTION_{name.upper()}", fallback)
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
def user_id() -> str:
|
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
|
number of completed flushes, not a pending count), and the doctor check
|
||||||
results. If doctor reports an authentication failure (401 / invalid key), say
|
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
|
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
|
being created. Never report an auth failure as "no memories found". When the
|
||||||
reinstalling with `--config api_key=...` in that case.
|
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");
|
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", () => {
|
test("ignores unsupported files and invalid assignments", () => {
|
||||||
const dir = home();
|
const dir = home();
|
||||||
writeFileSync(join(dir, ".env"), "MEM0_API_KEY=unsupported\n");
|
writeFileSync(join(dir, ".env"), "MEM0_API_KEY=unsupported\n");
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {readFileSync} from "fs";
|
import {readFileSync} from "fs";
|
||||||
import {homedir} from "os";
|
import {homedir} from "os";
|
||||||
import {join} from "path";
|
import {join} from "path";
|
||||||
|
import {mem0CliApiKey} from "../agent-plugin-core/typescript/src/credentials.ts";
|
||||||
|
|
||||||
const PROFILE_FILES = [".zshrc", ".bashrc", ".zprofile", ".bash_profile", ".profile"];
|
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 fs from "node:fs";
|
||||||
import * as os from "node:os";
|
import * as os from "node:os";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
|
import { mem0CliApiKey } from "../../../agent-plugin-core/typescript/src/credentials.ts";
|
||||||
import type { Mem0Config } from "../types.ts";
|
import type { Mem0Config } from "../types.ts";
|
||||||
|
|
||||||
const AGENT_ROOT = path.join(os.homedir(), ".pi", "agent");
|
const AGENT_ROOT = path.join(os.homedir(), ".pi", "agent");
|
||||||
@@ -36,6 +37,9 @@ export function loadConfig(): Mem0Config {
|
|||||||
if (process.env.MEM0_API_KEY) {
|
if (process.env.MEM0_API_KEY) {
|
||||||
config.apiKey = 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) {
|
if (process.env.MEM0_USER_ID) {
|
||||||
config.userId = 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 { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
|
import * as path from "node:path";
|
||||||
import { loadConfig } from "../src/config/index.ts";
|
import { loadConfig } from "../src/config/index.ts";
|
||||||
|
|
||||||
vi.mock("node:fs");
|
vi.mock("node:fs");
|
||||||
@@ -33,6 +34,17 @@ describe("loadConfig", () => {
|
|||||||
expect(config.apiKey).toBe("");
|
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", () => {
|
it("reads config file and merges with defaults", () => {
|
||||||
delete process.env.MEM0_API_KEY;
|
delete process.env.MEM0_API_KEY;
|
||||||
delete process.env.MEM0_USER_ID;
|
delete process.env.MEM0_USER_ID;
|
||||||
|
|||||||
Reference in New Issue
Block a user