Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a5497469d | |||
| 12617b6d8d | |||
| cbfb37c623 | |||
| 71dc0cae07 |
@@ -12,7 +12,7 @@
|
||||
"name": "mem0",
|
||||
"source": "./integrations/claude-code-plugin",
|
||||
"description": "Cross-session memory and token savings for coding agents.",
|
||||
"version": "0.3.3"
|
||||
"version": "0.3.4"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"name": "mem0",
|
||||
"source": "./integrations/cursor-plugin",
|
||||
"description": "Cross-session memory and token savings for coding agents.",
|
||||
"version": "0.3.3"
|
||||
"version": "0.3.4"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{
|
||||
"id": "mem0",
|
||||
"displayName": "Mem0",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"description": "Cross-session memory and token savings for coding agents.",
|
||||
"homepage": "https://mem0.ai",
|
||||
"keywords": ["memory", "personalization", "mcp", "semantic-search"],
|
||||
|
||||
@@ -2100,6 +2100,16 @@ A full-featured command-line interface for Mem0, available in both Python and No
|
||||
<Tabs>
|
||||
<Tab title="Mem0 Plugin">
|
||||
|
||||
<Update label="2026-09-25" description="Portable agent plugin v0.3.4">
|
||||
|
||||
**Fixes:**
|
||||
- **Auth:** Falls back to the API key `mem0 init` saved in `~/.mem0/config.json` when no key is set in the plugin settings or `MEM0_API_KEY`, so search and the status check no longer report a missing key after `mem0 init` ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
- **Auth:** A plugin setting that reaches the plugin as an unexpanded `${api_key}` or `${user_id}` placeholder is treated as unset. It is no longer sent as the API key, cached to disk, or used as the user ID ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
- **State:** The `search_memories` MCP server now keeps its state in `~/.mem0/coding-agent-plugin`, the same directory the status, pause, resume and forget skills use, instead of `~/.mem0/mem0-plugin` ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
- **Packaging:** `PLUGIN_VERSION` bumped to `0.3.4`, so the `mem0-plugin/<version>` wire header and `plugin_version` telemetry field identify builds with this fix ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-08" description="Shared agent plugin runtime">
|
||||
|
||||
**Changed:**
|
||||
@@ -2411,6 +2421,15 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
|
||||
|
||||
<Tab title="Claude Code">
|
||||
|
||||
<Update label="2026-09-25" description="Claude Code plugin v0.3.4">
|
||||
|
||||
**Fixes:**
|
||||
- **Auth:** Falls back to the API key `mem0 init` saved in `~/.mem0/config.json` when no key is set in the plugin settings or `MEM0_API_KEY`, so search and the status check no longer report a missing key after `mem0 init` ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
- **Auth:** A plugin setting that reaches the plugin as an unexpanded `${api_key}` or `${user_id}` placeholder is treated as unset. It is no longer sent as the API key, cached to disk, or used as the user ID ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
- **Packaging:** `PLUGIN_VERSION` bumped to `0.3.4`, so the `mem0-plugin/<version>` wire header and `plugin_version` telemetry field identify builds with this fix ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-23" description="Claude Code plugin v0.3.3">
|
||||
|
||||
**Improvements:**
|
||||
@@ -2452,6 +2471,16 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
|
||||
|
||||
<Tab title="Cursor">
|
||||
|
||||
<Update label="2026-09-25" description="Cursor plugin v0.3.4">
|
||||
|
||||
**Fixes:**
|
||||
- **Auth:** An API key set in the Cursor plugin settings now reaches the `search_memories` MCP server. The server looked for the cached key in `~/.mem0/mem0-plugin` while the hooks cached it in `~/.mem0/cursor-plugin`, so search asked for auth whenever Cursor did not pass the setting to the MCP server directly ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
- **Auth:** Falls back to the API key `mem0 init` saved in `~/.mem0/config.json` when no key is set in the plugin settings or `MEM0_API_KEY`, so search and the status check no longer report a missing key after `mem0 init` ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
- **Auth:** A plugin setting that reaches the plugin as an unexpanded `${api_key}` or `${user_id}` placeholder is treated as unset. It is no longer sent as the API key, cached to disk, or used as the user ID ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
- **Packaging:** `PLUGIN_VERSION` bumped to `0.3.4`, so the `mem0-plugin/<version>` wire header and `plugin_version` telemetry field identify builds with this fix ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-23" description="Cursor plugin v0.3.3">
|
||||
|
||||
**Improvements:**
|
||||
@@ -2492,6 +2521,14 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
|
||||
|
||||
<Tab title="Codex">
|
||||
|
||||
<Update label="2026-09-25" description="Codex plugin v0.3.4">
|
||||
|
||||
**Fixes:**
|
||||
- **Auth:** Falls back to the API key `mem0 init` saved in `~/.mem0/config.json` when `MEM0_API_KEY` is not set, so search and the status check no longer report a missing key after `mem0 init` ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
- **Packaging:** `PLUGIN_VERSION` bumped to `0.3.4`, so the `mem0-plugin/<version>` wire header and `plugin_version` telemetry field identify builds with this fix ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-23" description="Codex plugin v0.3.3">
|
||||
|
||||
**Improvements:**
|
||||
@@ -2531,6 +2568,13 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
|
||||
|
||||
<Tab title="OpenCode">
|
||||
|
||||
<Update label="2026-09-25" description="OpenCode plugin v0.4.2">
|
||||
|
||||
**Fixes:**
|
||||
- **Auth:** Falls back to the API key `mem0 init` saved in `~/.mem0/config.json` when neither `MEM0_API_KEY` nor a shell profile sets one, so search and the status check no longer report a missing key after `mem0 init` ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-23" description="OpenCode plugin v0.4.1">
|
||||
|
||||
**Improvements:**
|
||||
@@ -2646,6 +2690,15 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
|
||||
|
||||
<Tab title="Antigravity">
|
||||
|
||||
<Update label="2026-09-25" description="Antigravity plugin v0.3.4">
|
||||
|
||||
**Fixes:**
|
||||
- **Auth:** Falls back to the API key `mem0 init` saved in `~/.mem0/config.json` when `MEM0_API_KEY` is not set, so search and the status check no longer report a missing key after `mem0 init` ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
- **Data directory:** The `search_memories` MCP server uses `~/.mem0/antigravity-plugin`, the data directory the hooks use, instead of `~/.mem0/mem0-plugin` ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
- **Packaging:** `PLUGIN_VERSION` bumped to `0.3.4`, so the `mem0-plugin/<version>` wire header and `plugin_version` telemetry field identify builds with this fix ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-23" description="Antigravity plugin v0.3.3">
|
||||
|
||||
**Improvements:**
|
||||
@@ -2759,6 +2812,15 @@ Existing memories written by the previous versions are not rewritten. If your me
|
||||
|
||||
<Tab title="Kimi">
|
||||
|
||||
<Update label="2026-09-25" description="Kimi Code plugin v0.3.4">
|
||||
|
||||
**Fixes:**
|
||||
- **Auth:** Falls back to the API key `mem0 init` saved in `~/.mem0/config.json` when `MEM0_API_KEY` is not set, so search and the status check no longer report a missing key after `mem0 init` ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
- **Data directory:** The `search_memories` MCP server uses `~/.mem0/kimi-plugin`, the data directory the hooks use, instead of `~/.mem0/mem0-plugin` ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
- **Packaging:** `PLUGIN_VERSION` bumped to `0.3.4`, so the `mem0-plugin/<version>` wire header and `plugin_version` telemetry field identify builds with this fix ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-23" description="Kimi Code plugin v0.3.3">
|
||||
|
||||
**Improvements:**
|
||||
@@ -3117,6 +3179,13 @@ Existing memories written by the previous versions are not rewritten. If your me
|
||||
|
||||
<Tab title="Pi Agent">
|
||||
|
||||
<Update label="2026-09-25" description="Pi Agent plugin v0.3.3">
|
||||
|
||||
**Fixes:**
|
||||
- **Auth:** Falls back to the API key `mem0 init` saved in `~/.mem0/config.json` when neither `MEM0_API_KEY` nor `~/.pi/agent/mem0-config.json` sets one, so search and the status check no longer report a missing key after `mem0 init` ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-23" description="Pi Agent plugin v0.3.2">
|
||||
|
||||
**Improvements:**
|
||||
@@ -3246,6 +3315,14 @@ Existing memories written by the previous versions are not rewritten. If your me
|
||||
|
||||
<Tab title="DeepSeek Harness">
|
||||
|
||||
<Update label="2026-09-25" description="deepseek-plugin v0.3.3">
|
||||
|
||||
**Fixes:**
|
||||
- **Auth:** Falls back to the API key `mem0 init` saved in `~/.mem0/config.json` when neither `config.apiKey` nor `MEM0_API_KEY` is set, so search and the status check no longer report a missing key after `mem0 init` ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
- **Config:** An empty `apiKey` now counts as unset and falls through to `MEM0_API_KEY` and the `mem0 init` key, instead of failing validation ([#7449](https://github.com/mem0ai/mem0/pull/7449))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-23" description="deepseek-plugin v0.3.2">
|
||||
|
||||
**Improvements:**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -103,6 +103,7 @@ def _render_harness_id(host: str, *, portable: bool = False) -> str:
|
||||
"\n"
|
||||
f'HARNESS_ID = "{host}"\n'
|
||||
f'SOURCE_TAG = "{tag}"\n'
|
||||
f'DATA_DIR_NAME = "{host}-plugin"\n'
|
||||
"\n"
|
||||
"# Platform-side vocabulary (mem0_event.source + X-Application). The whole\n"
|
||||
"# plugin family is one source; which editor it runs in is the application.\n"
|
||||
|
||||
@@ -28,12 +28,23 @@ from typing import Any, Iterable
|
||||
|
||||
import telemetry
|
||||
|
||||
# Read from the generated per-host module so a new entrypoint is correct without
|
||||
# remembering to configure anything.
|
||||
try: # pragma: no cover - absent only in the un-built shared source tree
|
||||
from _harness_id import DATA_DIR_NAME as _DATA_DIR_NAME
|
||||
from _harness_id import PLATFORM_APPLICATION as _PLATFORM_APPLICATION
|
||||
from _harness_id import PLATFORM_SOURCE as _PLATFORM_SOURCE
|
||||
except ImportError:
|
||||
_DATA_DIR_NAME = "mem0-plugin"
|
||||
_PLATFORM_SOURCE = "MEM0_PLUGIN"
|
||||
_PLATFORM_APPLICATION = ""
|
||||
|
||||
DEFAULT_API_URL = "https://api.mem0.ai"
|
||||
PLUGIN_VERSION = "0.3.3"
|
||||
PLUGIN_VERSION = "0.3.4"
|
||||
|
||||
_harness_name: str = "generic"
|
||||
_harness_env_prefix: str = "MEM0_PLUGIN"
|
||||
_harness_data_dir_name: str = "mem0-plugin"
|
||||
_harness_data_dir_name: str = _DATA_DIR_NAME
|
||||
_harness_source_tag: str = "mem0_plugin"
|
||||
|
||||
|
||||
@@ -378,30 +389,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 +456,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 +479,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:
|
||||
@@ -1798,16 +1813,6 @@ def extraction_message_batches(
|
||||
return batches
|
||||
|
||||
|
||||
# Platform surface attribution. Read from the generated per-host module so a new
|
||||
# entrypoint is correct without remembering to configure anything.
|
||||
try: # pragma: no cover - absent only in the un-built shared source tree
|
||||
from _harness_id import PLATFORM_APPLICATION as _PLATFORM_APPLICATION
|
||||
from _harness_id import PLATFORM_SOURCE as _PLATFORM_SOURCE
|
||||
except ImportError:
|
||||
_PLATFORM_SOURCE = "MEM0_PLUGIN"
|
||||
_PLATFORM_APPLICATION = ""
|
||||
|
||||
|
||||
def platform_headers(key: str) -> dict[str, str]:
|
||||
"""Auth plus the three surface-identity headers.
|
||||
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -92,6 +92,7 @@ def test_the_portable_bundle_declares_no_host_application(tmp_path: Path) -> Non
|
||||
assert identity["PLATFORM_APPLICATION"] == ""
|
||||
# The PostHog-side label is still useful for grouping and stays populated.
|
||||
assert identity["HARNESS_ID"] == "coding-agent"
|
||||
assert identity["DATA_DIR_NAME"] == "coding-agent-plugin"
|
||||
assert identity["PLATFORM_SOURCE"] == "MEM0_PLUGIN"
|
||||
|
||||
|
||||
@@ -100,6 +101,7 @@ def test_a_native_bundle_names_the_host_it_was_built_for(host: str, tmp_path: Pa
|
||||
identity = _harness_identity(build(host, "native", tmp_path / host))
|
||||
|
||||
assert identity["PLATFORM_APPLICATION"] == host
|
||||
assert identity["DATA_DIR_NAME"] == f"{host}-plugin"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host", ["claude-code", "cursor", "codex", "kimi", "antigravity"])
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
"""Launch each host's real hooks and MCP server the way the host does, and check the search is authenticated."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from string import Template
|
||||
|
||||
import pytest
|
||||
|
||||
INTEGRATIONS = Path(__file__).resolve().parents[2]
|
||||
KEY = "m0-plugin-setting-key"
|
||||
CODEX_INHERITED_ENV = ("HOME", "PATH", "LANG", "TMPDIR")
|
||||
|
||||
|
||||
class _Mem0Api(BaseHTTPRequestHandler):
|
||||
authorizations: list[str]
|
||||
|
||||
def do_POST(self) -> None:
|
||||
self.rfile.read(int(self.headers.get("Content-Length") or 0))
|
||||
self.authorizations.append(self.headers.get("Authorization", ""))
|
||||
body = b'{"results": []}'
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, *args: object) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mem0_api():
|
||||
handler = type("Handler", (_Mem0Api,), {"authorizations": []})
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
yield f"http://127.0.0.1:{server.server_port}", handler.authorizations
|
||||
server.shutdown()
|
||||
|
||||
|
||||
def _json(path: Path) -> dict:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _expand(value: str, variables: dict[str, str]) -> str:
|
||||
return Template(value).safe_substitute(variables)
|
||||
|
||||
|
||||
def _run_hook(command: str, cwd: Path, env: dict[str, str], payload: dict) -> None:
|
||||
result = subprocess.run(
|
||||
["sh", "-c", command], cwd=cwd, env=env, input=json.dumps(payload), text=True, capture_output=True
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
def _search(argv: list[str], cwd: Path, env: dict[str, str]) -> dict:
|
||||
requests = [
|
||||
{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05"}},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/call",
|
||||
"params": {"name": "search_memories", "arguments": {"query": "earlier fixes"}},
|
||||
},
|
||||
]
|
||||
result = subprocess.run(
|
||||
argv,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
input="\n".join(json.dumps(request) for request in requests) + "\n",
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=30,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
return json.loads(result.stdout.splitlines()[-1])
|
||||
|
||||
|
||||
def _claude_code(host_env: dict[str, str], tmp_path: Path, repo: Path, setting: bool) -> tuple:
|
||||
root = INTEGRATIONS / "claude-code-plugin"
|
||||
plugin_env = {"CLAUDE_PLUGIN_ROOT": str(root), "CLAUDE_PLUGIN_DATA": str(tmp_path / "claude-data")}
|
||||
hook_env = {**host_env, **plugin_env, **({"CLAUDE_PLUGIN_OPTION_API_KEY": KEY} if setting else {})}
|
||||
command = _json(root / "hooks" / "hooks.json")["hooks"]["SessionStart"][0]["hooks"][0]["command"]
|
||||
_run_hook(command, repo, hook_env, {"session_id": "s1", "cwd": str(repo)})
|
||||
|
||||
server = _json(root / ".mcp.json")["mcpServers"]["mem0"]
|
||||
env = {**host_env, **{name: _expand(value, plugin_env) for name, value in server["env"].items()}}
|
||||
return [sys.executable, *(_expand(argument, plugin_env) for argument in server["args"])], repo, env
|
||||
|
||||
|
||||
def _cursor(host_env: dict[str, str], tmp_path: Path, repo: Path, setting: bool) -> tuple:
|
||||
root = INTEGRATIONS / "cursor-plugin"
|
||||
command = _json(root / "hooks" / "hooks.json")["hooks"]["sessionStart"][0]["command"]
|
||||
if setting:
|
||||
command = command.replace("${api_key}", KEY)
|
||||
hook_env = {**host_env, "CURSOR_PLUGIN_ROOT": str(root)}
|
||||
_run_hook(command, repo, hook_env, {"conversation_id": "c1", "workspace_roots": [str(repo)]})
|
||||
|
||||
server = _json(root / "mcp.json")["mcpServers"]["mem0"]
|
||||
args = [_expand(argument, {"CURSOR_PLUGIN_ROOT": str(root)}) for argument in server["args"]]
|
||||
return [sys.executable, *args], repo, {**host_env, **server["env"]}
|
||||
|
||||
|
||||
def _codex(host_env: dict[str, str], tmp_path: Path, repo: Path, setting: bool) -> tuple:
|
||||
root = INTEGRATIONS / "codex-plugin"
|
||||
codex_env = {**host_env, **({"MEM0_API_KEY": KEY} if setting else {})}
|
||||
data = str(tmp_path / "codex-data")
|
||||
hook_env = {**codex_env, "PLUGIN_ROOT": str(root), "PLUGIN_DATA": data, "CLAUDE_PLUGIN_DATA": data}
|
||||
command = _json(root / "hooks" / "hooks.json")["hooks"]["SessionStart"][0]["hooks"][0]["command"]
|
||||
_run_hook(command, repo, hook_env, {"session_id": "s1", "cwd": str(repo)})
|
||||
|
||||
server = _json(root / ".mcp.json")["mcpServers"]["mem0"]
|
||||
forwarded = [*CODEX_INHERITED_ENV, *server["env_vars"]]
|
||||
env = {name: codex_env[name] for name in forwarded if name in codex_env}
|
||||
return [sys.executable, *server["args"]], root / server["cwd"], env
|
||||
|
||||
|
||||
HOSTS = {"claude-code": _claude_code, "cursor": _cursor, "codex": _codex}
|
||||
|
||||
|
||||
def _mem0_init(home: Path, key: str) -> None:
|
||||
(home / ".mem0").mkdir(parents=True, exist_ok=True)
|
||||
(home / ".mem0" / "config.json").write_text(json.dumps({"platform": {"api_key": key}}), encoding="utf-8")
|
||||
|
||||
|
||||
def _host_env(home: Path, api_url: str) -> dict[str, str]:
|
||||
return {
|
||||
"HOME": str(home),
|
||||
"PATH": os.environ["PATH"],
|
||||
"MEM0_API_URL": api_url,
|
||||
"MEM0_TELEMETRY": "false",
|
||||
"MEM0_CODE_USER_ID": "test-user",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key_source", ["plugin setting", "mem0 init"])
|
||||
@pytest.mark.parametrize("host", sorted(HOSTS))
|
||||
def test_mcp_server_searches_with_the_key_the_user_configured(host, key_source, tmp_path, mem0_api):
|
||||
"""The key reaches the MCP server however the host delivers it: plugin setting, env, or `mem0 init`."""
|
||||
api_url, authorizations = mem0_api
|
||||
home = tmp_path / "home"
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
if key_source == "mem0 init":
|
||||
_mem0_init(home, KEY)
|
||||
|
||||
argv, cwd, env = HOSTS[host](_host_env(home, api_url), tmp_path, repo, key_source == "plugin setting")
|
||||
response = _search(argv, cwd, env)
|
||||
|
||||
assert not response["result"].get("isError"), response
|
||||
assert authorizations == [f"Token {KEY}"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host", sorted(HOSTS))
|
||||
def test_a_removed_plugin_setting_gives_way_to_a_later_mem0_init(host, tmp_path, mem0_api):
|
||||
"""The key saved from a plugin setting is dropped once the setting is gone, so a newer `mem0 init` key wins."""
|
||||
api_url, authorizations = mem0_api
|
||||
home = tmp_path / "home"
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
host_env = _host_env(home, api_url)
|
||||
HOSTS[host](host_env, tmp_path, repo, True)
|
||||
_mem0_init(home, "m0-mem0-init-key")
|
||||
|
||||
argv, cwd, env = HOSTS[host](host_env, tmp_path, repo, False)
|
||||
response = _search(argv, cwd, env)
|
||||
|
||||
assert not response["result"].get("isError"), response
|
||||
assert authorizations == ["Token m0-mem0-init-key"]
|
||||
@@ -83,6 +83,26 @@ def test_identity_resolves_without_init(harness, spec):
|
||||
assert out == f"{harness} {source_tag}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("harness,spec", sorted(HOSTS.items()))
|
||||
def test_data_dir_resolves_without_init(harness, spec):
|
||||
"""mcp_server never configures the harness, so its default must match configure_harness(harness) in hooks and skills."""
|
||||
directory, _ = spec
|
||||
core = _core_dir(directory)
|
||||
if not core.exists():
|
||||
pytest.skip(f"{directory} is not built in this tree")
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
out = _run(
|
||||
core,
|
||||
Path(tmp),
|
||||
"import os; os.environ.pop('MEM0_CODE_DATA_DIR')\n"
|
||||
"import mcp_server, memory_core; print(memory_core.data_dir())\n"
|
||||
f"memory_core.configure_harness({harness!r}); print(memory_core.data_dir())",
|
||||
)
|
||||
expected = str(Path(tmp) / ".mem0" / f"{harness}-plugin")
|
||||
assert out.splitlines() == [expected, expected]
|
||||
|
||||
|
||||
def test_mcp_server_records_the_real_harness():
|
||||
"""mcp_server imports telemetry and never initialises it (server.py has no init).
|
||||
|
||||
|
||||
@@ -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 } }))), "");
|
||||
});
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
HARNESS_ID = "antigravity"
|
||||
SOURCE_TAG = "ANTIGRAVITY_PLUGIN"
|
||||
DATA_DIR_NAME = "antigravity-plugin"
|
||||
|
||||
# Platform-side vocabulary (mem0_event.source + X-Application). The whole
|
||||
# plugin family is one source; which editor it runs in is the application.
|
||||
|
||||
@@ -28,12 +28,23 @@ from typing import Any, Iterable
|
||||
|
||||
import telemetry
|
||||
|
||||
# Read from the generated per-host module so a new entrypoint is correct without
|
||||
# remembering to configure anything.
|
||||
try: # pragma: no cover - absent only in the un-built shared source tree
|
||||
from _harness_id import DATA_DIR_NAME as _DATA_DIR_NAME
|
||||
from _harness_id import PLATFORM_APPLICATION as _PLATFORM_APPLICATION
|
||||
from _harness_id import PLATFORM_SOURCE as _PLATFORM_SOURCE
|
||||
except ImportError:
|
||||
_DATA_DIR_NAME = "mem0-plugin"
|
||||
_PLATFORM_SOURCE = "MEM0_PLUGIN"
|
||||
_PLATFORM_APPLICATION = ""
|
||||
|
||||
DEFAULT_API_URL = "https://api.mem0.ai"
|
||||
PLUGIN_VERSION = "0.3.3"
|
||||
PLUGIN_VERSION = "0.3.4"
|
||||
|
||||
_harness_name: str = "generic"
|
||||
_harness_env_prefix: str = "MEM0_PLUGIN"
|
||||
_harness_data_dir_name: str = "mem0-plugin"
|
||||
_harness_data_dir_name: str = _DATA_DIR_NAME
|
||||
_harness_source_tag: str = "mem0_plugin"
|
||||
|
||||
|
||||
@@ -378,30 +389,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 +456,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 +479,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:
|
||||
@@ -1798,16 +1813,6 @@ def extraction_message_batches(
|
||||
return batches
|
||||
|
||||
|
||||
# Platform surface attribution. Read from the generated per-host module so a new
|
||||
# entrypoint is correct without remembering to configure anything.
|
||||
try: # pragma: no cover - absent only in the un-built shared source tree
|
||||
from _harness_id import PLATFORM_APPLICATION as _PLATFORM_APPLICATION
|
||||
from _harness_id import PLATFORM_SOURCE as _PLATFORM_SOURCE
|
||||
except ImportError:
|
||||
_PLATFORM_SOURCE = "MEM0_PLUGIN"
|
||||
_PLATFORM_APPLICATION = ""
|
||||
|
||||
|
||||
def platform_headers(key: str) -> dict[str, str]:
|
||||
"""Auth plus the three surface-identity headers.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "mem0",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"homepage": "https://docs.mem0.ai/integrations/antigravity",
|
||||
"native": {
|
||||
"pluginRoot": "${ANTIGRAVITY_PLUGIN_ROOT}",
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"description": "Cross-session memory and token savings for coding agents.",
|
||||
"author": {
|
||||
"name": "Mem0"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
HARNESS_ID = "claude-code"
|
||||
SOURCE_TAG = "CLAUDE_CODE_PLUGIN"
|
||||
DATA_DIR_NAME = "claude-code-plugin"
|
||||
|
||||
# Platform-side vocabulary (mem0_event.source + X-Application). The whole
|
||||
# plugin family is one source; which editor it runs in is the application.
|
||||
|
||||
@@ -28,12 +28,23 @@ from typing import Any, Iterable
|
||||
|
||||
import telemetry
|
||||
|
||||
# Read from the generated per-host module so a new entrypoint is correct without
|
||||
# remembering to configure anything.
|
||||
try: # pragma: no cover - absent only in the un-built shared source tree
|
||||
from _harness_id import DATA_DIR_NAME as _DATA_DIR_NAME
|
||||
from _harness_id import PLATFORM_APPLICATION as _PLATFORM_APPLICATION
|
||||
from _harness_id import PLATFORM_SOURCE as _PLATFORM_SOURCE
|
||||
except ImportError:
|
||||
_DATA_DIR_NAME = "mem0-plugin"
|
||||
_PLATFORM_SOURCE = "MEM0_PLUGIN"
|
||||
_PLATFORM_APPLICATION = ""
|
||||
|
||||
DEFAULT_API_URL = "https://api.mem0.ai"
|
||||
PLUGIN_VERSION = "0.3.3"
|
||||
PLUGIN_VERSION = "0.3.4"
|
||||
|
||||
_harness_name: str = "generic"
|
||||
_harness_env_prefix: str = "MEM0_PLUGIN"
|
||||
_harness_data_dir_name: str = "mem0-plugin"
|
||||
_harness_data_dir_name: str = _DATA_DIR_NAME
|
||||
_harness_source_tag: str = "mem0_plugin"
|
||||
|
||||
|
||||
@@ -378,30 +389,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 +456,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 +479,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:
|
||||
@@ -1798,16 +1813,6 @@ def extraction_message_batches(
|
||||
return batches
|
||||
|
||||
|
||||
# Platform surface attribution. Read from the generated per-host module so a new
|
||||
# entrypoint is correct without remembering to configure anything.
|
||||
try: # pragma: no cover - absent only in the un-built shared source tree
|
||||
from _harness_id import PLATFORM_APPLICATION as _PLATFORM_APPLICATION
|
||||
from _harness_id import PLATFORM_SOURCE as _PLATFORM_SOURCE
|
||||
except ImportError:
|
||||
_PLATFORM_SOURCE = "MEM0_PLUGIN"
|
||||
_PLATFORM_APPLICATION = ""
|
||||
|
||||
|
||||
def platform_headers(key: str) -> dict[str, str]:
|
||||
"""Auth plus the three surface-identity headers.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "mem0",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"homepage": "https://docs.mem0.ai/integrations/claude-code",
|
||||
"native": {
|
||||
"pluginRoot": "${CLAUDE_PLUGIN_ROOT}",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"description": "Cross-session memory and token savings for coding agents.",
|
||||
"author": { "name": "Mem0", "email": "support@mem0.ai" },
|
||||
"homepage": "https://docs.mem0.ai/integrations/codex",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
HARNESS_ID = "codex"
|
||||
SOURCE_TAG = "CODEX_PLUGIN"
|
||||
DATA_DIR_NAME = "codex-plugin"
|
||||
|
||||
# Platform-side vocabulary (mem0_event.source + X-Application). The whole
|
||||
# plugin family is one source; which editor it runs in is the application.
|
||||
|
||||
@@ -28,12 +28,23 @@ from typing import Any, Iterable
|
||||
|
||||
import telemetry
|
||||
|
||||
# Read from the generated per-host module so a new entrypoint is correct without
|
||||
# remembering to configure anything.
|
||||
try: # pragma: no cover - absent only in the un-built shared source tree
|
||||
from _harness_id import DATA_DIR_NAME as _DATA_DIR_NAME
|
||||
from _harness_id import PLATFORM_APPLICATION as _PLATFORM_APPLICATION
|
||||
from _harness_id import PLATFORM_SOURCE as _PLATFORM_SOURCE
|
||||
except ImportError:
|
||||
_DATA_DIR_NAME = "mem0-plugin"
|
||||
_PLATFORM_SOURCE = "MEM0_PLUGIN"
|
||||
_PLATFORM_APPLICATION = ""
|
||||
|
||||
DEFAULT_API_URL = "https://api.mem0.ai"
|
||||
PLUGIN_VERSION = "0.3.3"
|
||||
PLUGIN_VERSION = "0.3.4"
|
||||
|
||||
_harness_name: str = "generic"
|
||||
_harness_env_prefix: str = "MEM0_PLUGIN"
|
||||
_harness_data_dir_name: str = "mem0-plugin"
|
||||
_harness_data_dir_name: str = _DATA_DIR_NAME
|
||||
_harness_source_tag: str = "mem0_plugin"
|
||||
|
||||
|
||||
@@ -378,30 +389,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 +456,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 +479,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:
|
||||
@@ -1798,16 +1813,6 @@ def extraction_message_batches(
|
||||
return batches
|
||||
|
||||
|
||||
# Platform surface attribution. Read from the generated per-host module so a new
|
||||
# entrypoint is correct without remembering to configure anything.
|
||||
try: # pragma: no cover - absent only in the un-built shared source tree
|
||||
from _harness_id import PLATFORM_APPLICATION as _PLATFORM_APPLICATION
|
||||
from _harness_id import PLATFORM_SOURCE as _PLATFORM_SOURCE
|
||||
except ImportError:
|
||||
_PLATFORM_SOURCE = "MEM0_PLUGIN"
|
||||
_PLATFORM_APPLICATION = ""
|
||||
|
||||
|
||||
def platform_headers(key: str) -> dict[str, str]:
|
||||
"""Auth plus the three surface-identity headers.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "mem0",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"homepage": "https://docs.mem0.ai/integrations/codex",
|
||||
"native": {
|
||||
"pluginRoot": "${PLUGIN_ROOT}",
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"description": "Cross-session memory and token savings for coding agents.",
|
||||
"author": { "name": "Mem0", "email": "support@mem0.ai" },
|
||||
"homepage": "https://docs.mem0.ai/integrations/cursor",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
HARNESS_ID = "cursor"
|
||||
SOURCE_TAG = "CURSOR_PLUGIN"
|
||||
DATA_DIR_NAME = "cursor-plugin"
|
||||
|
||||
# Platform-side vocabulary (mem0_event.source + X-Application). The whole
|
||||
# plugin family is one source; which editor it runs in is the application.
|
||||
|
||||
@@ -28,12 +28,23 @@ from typing import Any, Iterable
|
||||
|
||||
import telemetry
|
||||
|
||||
# Read from the generated per-host module so a new entrypoint is correct without
|
||||
# remembering to configure anything.
|
||||
try: # pragma: no cover - absent only in the un-built shared source tree
|
||||
from _harness_id import DATA_DIR_NAME as _DATA_DIR_NAME
|
||||
from _harness_id import PLATFORM_APPLICATION as _PLATFORM_APPLICATION
|
||||
from _harness_id import PLATFORM_SOURCE as _PLATFORM_SOURCE
|
||||
except ImportError:
|
||||
_DATA_DIR_NAME = "mem0-plugin"
|
||||
_PLATFORM_SOURCE = "MEM0_PLUGIN"
|
||||
_PLATFORM_APPLICATION = ""
|
||||
|
||||
DEFAULT_API_URL = "https://api.mem0.ai"
|
||||
PLUGIN_VERSION = "0.3.3"
|
||||
PLUGIN_VERSION = "0.3.4"
|
||||
|
||||
_harness_name: str = "generic"
|
||||
_harness_env_prefix: str = "MEM0_PLUGIN"
|
||||
_harness_data_dir_name: str = "mem0-plugin"
|
||||
_harness_data_dir_name: str = _DATA_DIR_NAME
|
||||
_harness_source_tag: str = "mem0_plugin"
|
||||
|
||||
|
||||
@@ -378,30 +389,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 +456,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 +479,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:
|
||||
@@ -1798,16 +1813,6 @@ def extraction_message_batches(
|
||||
return batches
|
||||
|
||||
|
||||
# Platform surface attribution. Read from the generated per-host module so a new
|
||||
# entrypoint is correct without remembering to configure anything.
|
||||
try: # pragma: no cover - absent only in the un-built shared source tree
|
||||
from _harness_id import PLATFORM_APPLICATION as _PLATFORM_APPLICATION
|
||||
from _harness_id import PLATFORM_SOURCE as _PLATFORM_SOURCE
|
||||
except ImportError:
|
||||
_PLATFORM_SOURCE = "MEM0_PLUGIN"
|
||||
_PLATFORM_APPLICATION = ""
|
||||
|
||||
|
||||
def platform_headers(key: str) -> dict[str, str]:
|
||||
"""Auth plus the three surface-identity headers.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "mem0",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"homepage": "https://docs.mem0.ai/integrations/cursor",
|
||||
"native": {
|
||||
"pluginRoot": "${CURSOR_PLUGIN_ROOT}",
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/deepseek-plugin",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"description": "Mem0 long-term memory as a native DeepSeek Harness (Cordis) plugin.",
|
||||
"type": "module",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
HARNESS_ID = "kimi"
|
||||
SOURCE_TAG = "KIMI_PLUGIN"
|
||||
DATA_DIR_NAME = "kimi-plugin"
|
||||
|
||||
# Platform-side vocabulary (mem0_event.source + X-Application). The whole
|
||||
# plugin family is one source; which editor it runs in is the application.
|
||||
|
||||
@@ -28,12 +28,23 @@ from typing import Any, Iterable
|
||||
|
||||
import telemetry
|
||||
|
||||
# Read from the generated per-host module so a new entrypoint is correct without
|
||||
# remembering to configure anything.
|
||||
try: # pragma: no cover - absent only in the un-built shared source tree
|
||||
from _harness_id import DATA_DIR_NAME as _DATA_DIR_NAME
|
||||
from _harness_id import PLATFORM_APPLICATION as _PLATFORM_APPLICATION
|
||||
from _harness_id import PLATFORM_SOURCE as _PLATFORM_SOURCE
|
||||
except ImportError:
|
||||
_DATA_DIR_NAME = "mem0-plugin"
|
||||
_PLATFORM_SOURCE = "MEM0_PLUGIN"
|
||||
_PLATFORM_APPLICATION = ""
|
||||
|
||||
DEFAULT_API_URL = "https://api.mem0.ai"
|
||||
PLUGIN_VERSION = "0.3.3"
|
||||
PLUGIN_VERSION = "0.3.4"
|
||||
|
||||
_harness_name: str = "generic"
|
||||
_harness_env_prefix: str = "MEM0_PLUGIN"
|
||||
_harness_data_dir_name: str = "mem0-plugin"
|
||||
_harness_data_dir_name: str = _DATA_DIR_NAME
|
||||
_harness_source_tag: str = "mem0_plugin"
|
||||
|
||||
|
||||
@@ -378,30 +389,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 +456,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 +479,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:
|
||||
@@ -1798,16 +1813,6 @@ def extraction_message_batches(
|
||||
return batches
|
||||
|
||||
|
||||
# Platform surface attribution. Read from the generated per-host module so a new
|
||||
# entrypoint is correct without remembering to configure anything.
|
||||
try: # pragma: no cover - absent only in the un-built shared source tree
|
||||
from _harness_id import PLATFORM_APPLICATION as _PLATFORM_APPLICATION
|
||||
from _harness_id import PLATFORM_SOURCE as _PLATFORM_SOURCE
|
||||
except ImportError:
|
||||
_PLATFORM_SOURCE = "MEM0_PLUGIN"
|
||||
_PLATFORM_APPLICATION = ""
|
||||
|
||||
|
||||
def platform_headers(key: str) -> dict[str, str]:
|
||||
"""Auth plus the three surface-identity headers.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"description": "Cross-session memory and token savings for coding agents.",
|
||||
"keywords": ["memory", "coding-agents", "continual-learning", "token-efficiency"],
|
||||
"author": { "name": "Mem0", "email": "support@mem0.ai" },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "mem0",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"homepage": "https://docs.mem0.ai/integrations/kimi",
|
||||
"native": {
|
||||
"pluginRoot": "${KIMI_PLUGIN_ROOT}",
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
HARNESS_ID = "coding-agent"
|
||||
SOURCE_TAG = "CODING_AGENT_PLUGIN"
|
||||
DATA_DIR_NAME = "coding-agent-plugin"
|
||||
|
||||
# Platform-side vocabulary (mem0_event.source + X-Application). The whole
|
||||
# plugin family is one source; which editor it runs in is the application.
|
||||
|
||||
@@ -28,12 +28,23 @@ from typing import Any, Iterable
|
||||
|
||||
import telemetry
|
||||
|
||||
# Read from the generated per-host module so a new entrypoint is correct without
|
||||
# remembering to configure anything.
|
||||
try: # pragma: no cover - absent only in the un-built shared source tree
|
||||
from _harness_id import DATA_DIR_NAME as _DATA_DIR_NAME
|
||||
from _harness_id import PLATFORM_APPLICATION as _PLATFORM_APPLICATION
|
||||
from _harness_id import PLATFORM_SOURCE as _PLATFORM_SOURCE
|
||||
except ImportError:
|
||||
_DATA_DIR_NAME = "mem0-plugin"
|
||||
_PLATFORM_SOURCE = "MEM0_PLUGIN"
|
||||
_PLATFORM_APPLICATION = ""
|
||||
|
||||
DEFAULT_API_URL = "https://api.mem0.ai"
|
||||
PLUGIN_VERSION = "0.3.3"
|
||||
PLUGIN_VERSION = "0.3.4"
|
||||
|
||||
_harness_name: str = "generic"
|
||||
_harness_env_prefix: str = "MEM0_PLUGIN"
|
||||
_harness_data_dir_name: str = "mem0-plugin"
|
||||
_harness_data_dir_name: str = _DATA_DIR_NAME
|
||||
_harness_source_tag: str = "mem0_plugin"
|
||||
|
||||
|
||||
@@ -378,30 +389,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 +456,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 +479,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:
|
||||
@@ -1798,16 +1813,6 @@ def extraction_message_batches(
|
||||
return batches
|
||||
|
||||
|
||||
# Platform surface attribution. Read from the generated per-host module so a new
|
||||
# entrypoint is correct without remembering to configure anything.
|
||||
try: # pragma: no cover - absent only in the un-built shared source tree
|
||||
from _harness_id import PLATFORM_APPLICATION as _PLATFORM_APPLICATION
|
||||
from _harness_id import PLATFORM_SOURCE as _PLATFORM_SOURCE
|
||||
except ImportError:
|
||||
_PLATFORM_SOURCE = "MEM0_PLUGIN"
|
||||
_PLATFORM_APPLICATION = ""
|
||||
|
||||
|
||||
def platform_headers(key: str) -> dict[str, str]:
|
||||
"""Auth plus the three surface-identity headers.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
||||
"name": "mem0",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"description": "Cross-session memory and token savings for coding agents.",
|
||||
"author": {
|
||||
"name": "Mem0",
|
||||
|
||||
@@ -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,6 @@
|
||||
{
|
||||
"name": "@mem0/opencode-plugin",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.2",
|
||||
"type": "module",
|
||||
"description": "Mem0 persistent memory plugin for OpenCode — add, search, and manage memories across sessions",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/pi-agent-plugin",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"type": "module",
|
||||
"description": "Mem0 memory extension for Pi Agent persistent, scoped, semantic memory across sessions and projects",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"category": "Productivity",
|
||||
"description": "Cross-session memory and token savings for coding agents.",
|
||||
"version": "0.3.3"
|
||||
"version": "0.3.4"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user