555e282cc4
pi-agent-plugin checks / lint (push) Has been cancelled
pi-agent-plugin checks / test (20) (push) Has been cancelled
pi-agent-plugin checks / test (22) (push) Has been cancelled
pi-agent-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / check_changes (push) Has been cancelled
TypeScript SDK CI / changelog_check (push) Has been cancelled
ci / changelog_check (push) Has been cancelled
ci / check_changes (push) Has been cancelled
ci / build_mem0 (3.10) (push) Has been cancelled
ci / build_mem0 (3.11) (push) Has been cancelled
ci / build_mem0 (3.12) (push) Has been cancelled
CLI Node CI / lint (push) Has been cancelled
CLI Node CI / test (20) (push) Has been cancelled
CLI Node CI / test (22) (push) Has been cancelled
CLI Node CI / build (push) Has been cancelled
CLI Python CI / lint (push) Has been cancelled
CLI Python CI / test (3.10) (push) Has been cancelled
CLI Python CI / test (3.11) (push) Has been cancelled
CLI Python CI / test (3.12) (push) Has been cancelled
CLI Python CI / build (push) Has been cancelled
openclaw checks / lint (push) Has been cancelled
openclaw checks / test (20) (push) Has been cancelled
openclaw checks / test (22) (push) Has been cancelled
openclaw checks / build (push) Has been cancelled
opencode-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (22) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (22) (push) Has been cancelled
120 lines
4.1 KiB
Python
120 lines
4.1 KiB
Python
"""Sync the active Mem0 API key into other ecosystem touchpoints.
|
|
|
|
Why this exists:
|
|
The CLI canonical state lives in ``~/.mem0/config.json``. But MCP servers
|
|
(Claude Code plugin, Codex plugin, etc.) read ``MEM0_API_KEY`` from env
|
|
vars or their own config files. Without a sync, an agent-mode bootstrap
|
|
mints a new key into config.json but the plugin's MCP keeps using the
|
|
old key from env — silent surprise.
|
|
|
|
Design:
|
|
- Update ONLY entries that already exist (never create new ones)
|
|
- Preserve all surrounding content / formatting / other keys
|
|
- Atomic writes (tmpfile + rename) so a crash mid-write doesn't corrupt
|
|
- Idempotent — re-running with the same key is a no-op
|
|
- Skip on dry_run
|
|
|
|
Targets currently handled:
|
|
- ``~/.claude/settings.json::env::MEM0_API_KEY`` (Claude Code env injection)
|
|
- ``~/.zshrc`` / ``~/.bashrc`` ``export MEM0_API_KEY="..."`` lines
|
|
|
|
Out of scope (deliberately not touched):
|
|
- Codex / Cursor MCP configs — would require schema-aware edits and
|
|
those tools don't have mem0 entries by default
|
|
- Plugin's own ``<plugin-dir>/.api_key`` file — plugin-managed
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import json
|
|
import os
|
|
import re
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
# Files we know how to update safely.
|
|
_CLAUDE_SETTINGS = Path.home() / ".claude" / "settings.json"
|
|
_SHELL_RCS = [Path.home() / ".zshrc", Path.home() / ".bashrc", Path.home() / ".bash_profile"]
|
|
|
|
|
|
def sync_api_key(api_key: str) -> list[str]:
|
|
"""Propagate ``api_key`` into known ecosystem touchpoints.
|
|
|
|
Returns the list of paths actually updated. Empty list means nothing
|
|
needed updating (either targets didn't exist or already had this value).
|
|
"""
|
|
if not api_key:
|
|
return []
|
|
updated: list[str] = []
|
|
if _update_claude_settings(_CLAUDE_SETTINGS, api_key):
|
|
updated.append(str(_CLAUDE_SETTINGS))
|
|
for rc in _SHELL_RCS:
|
|
if _update_shell_rc(rc, api_key):
|
|
updated.append(str(rc))
|
|
return updated
|
|
|
|
|
|
def _update_claude_settings(path: Path, api_key: str) -> bool:
|
|
"""Update ``env.MEM0_API_KEY`` in path. Returns True if file was changed."""
|
|
if not path.is_file():
|
|
return False
|
|
try:
|
|
with path.open("r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
except (json.JSONDecodeError, OSError):
|
|
return False
|
|
env = data.get("env")
|
|
if not isinstance(env, dict) or "MEM0_API_KEY" not in env:
|
|
# No existing entry — don't create one.
|
|
return False
|
|
if env["MEM0_API_KEY"] == api_key:
|
|
return False # already in sync
|
|
env["MEM0_API_KEY"] = api_key
|
|
_atomic_write_text(path, json.dumps(data, indent=2, ensure_ascii=False) + "\n")
|
|
return True
|
|
|
|
|
|
# Match `export MEM0_API_KEY="..."` (or single quotes, or no quotes).
|
|
# Use [ \t]* (not \s*) for trailing whitespace so a trailing newline at
|
|
# end-of-file is preserved when MEM0_API_KEY is the last line.
|
|
_RC_LINE = re.compile(
|
|
r'^([ \t]*export[ \t]+MEM0_API_KEY[ \t]*=[ \t]*)(["\']?)([^"\'\n]*)(["\']?)[ \t]*$',
|
|
re.MULTILINE,
|
|
)
|
|
|
|
|
|
def _update_shell_rc(path: Path, api_key: str) -> bool:
|
|
"""Update an existing ``export MEM0_API_KEY=...`` line in path."""
|
|
if not path.is_file():
|
|
return False
|
|
try:
|
|
text = path.read_text(encoding="utf-8")
|
|
except OSError:
|
|
return False
|
|
match = _RC_LINE.search(text)
|
|
if not match:
|
|
return False # no existing line
|
|
if match.group(3) == api_key:
|
|
return False
|
|
new_text = _RC_LINE.sub(lambda m: f'{m.group(1)}"{api_key}"', text, count=1)
|
|
_atomic_write_text(path, new_text)
|
|
return True
|
|
|
|
|
|
def _atomic_write_text(path: Path, content: str) -> None:
|
|
"""Write content to path atomically (temp + rename)."""
|
|
dirname = path.parent
|
|
fd, tmp_path = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=dirname)
|
|
try:
|
|
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
f.write(content)
|
|
# Preserve mode if the original existed.
|
|
if path.exists():
|
|
os.chmod(tmp_path, path.stat().st_mode & 0o777)
|
|
os.replace(tmp_path, path)
|
|
except Exception:
|
|
with contextlib.suppress(OSError):
|
|
os.unlink(tmp_path)
|
|
raise
|