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:
@@ -378,30 +378,46 @@ def resolve_repo(cwd: str | None) -> RepoContext:
|
||||
return _resolve_repo_cached(os.path.abspath(cwd or os.getcwd()))
|
||||
|
||||
|
||||
_PLUGIN_API_KEY_ENV = (
|
||||
"PLUGIN_OPTION_API_KEY",
|
||||
"CLAUDE_PLUGIN_OPTION_API_KEY",
|
||||
"CLAUDE_PLUGIN_OPTION_MEM0_API_KEY",
|
||||
)
|
||||
|
||||
|
||||
def _configured(value: object) -> str:
|
||||
"""The stripped value, or empty when the host left its ${placeholder} unexpanded."""
|
||||
text = value.strip() if isinstance(value, str) else ""
|
||||
return "" if text.startswith("${") and text.endswith("}") else text
|
||||
|
||||
|
||||
def _first_env(*names: str) -> str:
|
||||
return next((value for name in names if (value := _configured(os.environ.get(name)))), "")
|
||||
|
||||
|
||||
def _mem0_cli_api_key() -> str:
|
||||
"""The key `mem0 init` saved to the Mem0 CLI config."""
|
||||
try:
|
||||
config = json.loads((Path.home() / ".mem0" / "config.json").read_text(encoding="utf-8"))
|
||||
return _configured(config["platform"]["api_key"])
|
||||
except (OSError, ValueError, LookupError, TypeError):
|
||||
return ""
|
||||
|
||||
|
||||
def api_key() -> str:
|
||||
configured = (
|
||||
os.environ.get("MEM0_API_KEY")
|
||||
or os.environ.get("PLUGIN_OPTION_API_KEY")
|
||||
or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
|
||||
or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
|
||||
or ""
|
||||
).strip()
|
||||
configured = _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV)
|
||||
if configured:
|
||||
return configured
|
||||
try:
|
||||
return (data_dir() / "api-key").read_text(encoding="utf-8").strip()
|
||||
cached = _configured((data_dir() / "api-key").read_text(encoding="utf-8"))
|
||||
except OSError:
|
||||
return ""
|
||||
cached = ""
|
||||
return cached or _mem0_cli_api_key()
|
||||
|
||||
|
||||
def cache_plugin_api_key() -> bool:
|
||||
"""Bridge host's hook-only sensitive config into plugin-owned storage."""
|
||||
configured = (
|
||||
os.environ.get("PLUGIN_OPTION_API_KEY")
|
||||
or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
|
||||
or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
|
||||
or ""
|
||||
).strip()
|
||||
configured = _first_env(*_PLUGIN_API_KEY_ENV)
|
||||
if not configured:
|
||||
return False
|
||||
|
||||
@@ -429,14 +445,7 @@ def cache_plugin_api_key() -> bool:
|
||||
|
||||
def clear_stale_api_key_cache() -> bool:
|
||||
"""Drop the cached key file once every configured key source is gone."""
|
||||
configured = (
|
||||
os.environ.get("MEM0_API_KEY")
|
||||
or os.environ.get("PLUGIN_OPTION_API_KEY")
|
||||
or os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY")
|
||||
or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY")
|
||||
or ""
|
||||
).strip()
|
||||
if configured:
|
||||
if _first_env("MEM0_API_KEY", *_PLUGIN_API_KEY_ENV):
|
||||
return False
|
||||
path = data_dir() / "api-key"
|
||||
if not path.exists():
|
||||
@@ -459,12 +468,7 @@ def detached_process_kwargs(platform: str | None = None) -> dict:
|
||||
|
||||
|
||||
def _plugin_option(name: str, fallback: str = "") -> str:
|
||||
return (
|
||||
os.environ.get(f"PLUGIN_OPTION_{name.upper()}")
|
||||
or os.environ.get(f"CLAUDE_PLUGIN_OPTION_{name.upper()}")
|
||||
or os.environ.get(fallback)
|
||||
or ""
|
||||
).strip()
|
||||
return _first_env(f"PLUGIN_OPTION_{name.upper()}", f"CLAUDE_PLUGIN_OPTION_{name.upper()}", fallback)
|
||||
|
||||
|
||||
def user_id() -> str:
|
||||
|
||||
@@ -19,5 +19,7 @@ API key is configured, the event/flush/retrieval counts (`flushes` is the
|
||||
number of completed flushes, not a pending count), and the doctor check
|
||||
results. If doctor reports an authentication failure (401 / invalid key), say
|
||||
clearly that the Mem0 API key is invalid or expired and that memories are NOT
|
||||
being created. Never report an auth failure as "no memories found". Suggest
|
||||
reinstalling with `--config api_key=...` in that case.
|
||||
being created. Never report an auth failure as "no memories found". When the
|
||||
key is missing or invalid, suggest updating the plugin's API key setting,
|
||||
exporting `MEM0_API_KEY`, or running `mem0 init` (the plugin reads the key the
|
||||
Mem0 CLI saves in `~/.mem0/config.json`).
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
export function mem0CliApiKey(homeDir: string): string {
|
||||
try {
|
||||
const key = JSON.parse(readFileSync(join(homeDir, ".mem0", "config.json"), "utf8"))?.platform?.api_key;
|
||||
return typeof key === "string" ? key.trim() : "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import { mem0CliApiKey } from "../src/credentials.ts";
|
||||
|
||||
function homeWithCliConfig(config: string): string {
|
||||
const home = mkdtempSync(join(tmpdir(), "mem0-cli-home-"));
|
||||
mkdirSync(join(home, ".mem0"));
|
||||
writeFileSync(join(home, ".mem0", "config.json"), config);
|
||||
return home;
|
||||
}
|
||||
|
||||
test("reads the key mem0 init saved", () => {
|
||||
const home = homeWithCliConfig(JSON.stringify({ platform: { api_key: " m0-cli-key\n" } }));
|
||||
assert.equal(mem0CliApiKey(home), "m0-cli-key");
|
||||
});
|
||||
|
||||
test("missing, malformed, or non-string config reads as no key", () => {
|
||||
assert.equal(mem0CliApiKey(mkdtempSync(join(tmpdir(), "mem0-cli-home-"))), "");
|
||||
assert.equal(mem0CliApiKey(homeWithCliConfig("{not json")), "");
|
||||
assert.equal(mem0CliApiKey(homeWithCliConfig("null")), "");
|
||||
assert.equal(mem0CliApiKey(homeWithCliConfig(JSON.stringify({ platform: { api_key: 42 } }))), "");
|
||||
});
|
||||
Reference in New Issue
Block a user