feat: add official mem0 CLI (Python & TypeScript) (#4575)
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
"""Configuration management for mem0 CLI.
|
||||
|
||||
Config precedence (highest to lowest):
|
||||
1. CLI flags (--api-key, --base-url, etc.)
|
||||
2. Environment variables (MEM0_API_KEY, etc.)
|
||||
3. Config file (~/.mem0/config.json)
|
||||
4. Defaults
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
CONFIG_DIR = Path.home() / ".mem0"
|
||||
CONFIG_FILE = CONFIG_DIR / "config.json"
|
||||
|
||||
DEFAULT_BASE_URL = "https://api.mem0.ai"
|
||||
CONFIG_VERSION = 1
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlatformConfig:
|
||||
api_key: str = ""
|
||||
base_url: str = DEFAULT_BASE_URL
|
||||
|
||||
|
||||
@dataclass
|
||||
class DefaultsConfig:
|
||||
user_id: str = ""
|
||||
agent_id: str = ""
|
||||
app_id: str = ""
|
||||
run_id: str = ""
|
||||
enable_graph: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class Mem0Config:
|
||||
version: int = CONFIG_VERSION
|
||||
defaults: DefaultsConfig = field(default_factory=DefaultsConfig)
|
||||
platform: PlatformConfig = field(default_factory=PlatformConfig)
|
||||
|
||||
|
||||
def ensure_config_dir() -> Path:
|
||||
"""Create ~/.mem0 directory with secure permissions if it doesn't exist."""
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
os.chmod(CONFIG_DIR, stat.S_IRWXU) # 0700
|
||||
return CONFIG_DIR
|
||||
|
||||
|
||||
def load_config() -> Mem0Config:
|
||||
"""Load config from file, applying env var overrides."""
|
||||
config = Mem0Config()
|
||||
|
||||
if CONFIG_FILE.exists():
|
||||
with open(CONFIG_FILE) as f:
|
||||
data = json.load(f)
|
||||
|
||||
config.version = data.get("version", CONFIG_VERSION)
|
||||
|
||||
plat = data.get("platform", {})
|
||||
config.platform.api_key = plat.get("api_key", "")
|
||||
config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL)
|
||||
|
||||
defaults = data.get("defaults", {})
|
||||
config.defaults.user_id = defaults.get("user_id", "")
|
||||
config.defaults.agent_id = defaults.get("agent_id", "")
|
||||
config.defaults.app_id = defaults.get("app_id", "")
|
||||
config.defaults.run_id = defaults.get("run_id", "")
|
||||
config.defaults.enable_graph = defaults.get("enable_graph", False)
|
||||
|
||||
# Environment variable overrides
|
||||
env_key = os.environ.get("MEM0_API_KEY")
|
||||
if env_key:
|
||||
config.platform.api_key = env_key
|
||||
|
||||
env_base = os.environ.get("MEM0_BASE_URL")
|
||||
if env_base:
|
||||
config.platform.base_url = env_base
|
||||
|
||||
env_user_id = os.environ.get("MEM0_USER_ID")
|
||||
if env_user_id:
|
||||
config.defaults.user_id = env_user_id
|
||||
|
||||
env_agent_id = os.environ.get("MEM0_AGENT_ID")
|
||||
if env_agent_id:
|
||||
config.defaults.agent_id = env_agent_id
|
||||
|
||||
env_app_id = os.environ.get("MEM0_APP_ID")
|
||||
if env_app_id:
|
||||
config.defaults.app_id = env_app_id
|
||||
|
||||
env_run_id = os.environ.get("MEM0_RUN_ID")
|
||||
if env_run_id:
|
||||
config.defaults.run_id = env_run_id
|
||||
|
||||
env_graph = os.environ.get("MEM0_ENABLE_GRAPH")
|
||||
if env_graph:
|
||||
config.defaults.enable_graph = env_graph.lower() in ("true", "1", "yes")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def save_config(config: Mem0Config) -> None:
|
||||
"""Write config to disk with secure permissions."""
|
||||
ensure_config_dir()
|
||||
|
||||
data: dict[str, Any] = {
|
||||
"version": config.version,
|
||||
"defaults": {
|
||||
"user_id": config.defaults.user_id,
|
||||
"agent_id": config.defaults.agent_id,
|
||||
"app_id": config.defaults.app_id,
|
||||
"run_id": config.defaults.run_id,
|
||||
"enable_graph": config.defaults.enable_graph,
|
||||
},
|
||||
"platform": {
|
||||
"api_key": config.platform.api_key,
|
||||
"base_url": config.platform.base_url,
|
||||
},
|
||||
}
|
||||
|
||||
with open(CONFIG_FILE, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
os.chmod(CONFIG_FILE, stat.S_IRUSR | stat.S_IWUSR) # 0600
|
||||
|
||||
|
||||
def redact_key(key: str) -> str:
|
||||
"""Redact an API key for display: m0-xxx...xxx"""
|
||||
if not key:
|
||||
return "(not set)"
|
||||
if len(key) <= 8:
|
||||
return key[:2] + "***"
|
||||
return key[:4] + "..." + key[-4:]
|
||||
|
||||
|
||||
def get_nested_value(config: Mem0Config, dotted_key: str) -> Any:
|
||||
"""Get a config value by dotted path, e.g. 'platform.api_key'."""
|
||||
parts = dotted_key.split(".")
|
||||
obj: Any = config
|
||||
for part in parts:
|
||||
if hasattr(obj, part):
|
||||
obj = getattr(obj, part)
|
||||
else:
|
||||
return None
|
||||
return obj
|
||||
|
||||
|
||||
def set_nested_value(config: Mem0Config, dotted_key: str, value: str) -> bool:
|
||||
"""Set a config value by dotted path. Returns True on success."""
|
||||
parts = dotted_key.split(".")
|
||||
obj: Any = config
|
||||
for part in parts[:-1]:
|
||||
if hasattr(obj, part):
|
||||
obj = getattr(obj, part)
|
||||
else:
|
||||
return False
|
||||
|
||||
final_key = parts[-1]
|
||||
if not hasattr(obj, final_key):
|
||||
return False
|
||||
|
||||
current = getattr(obj, final_key)
|
||||
# Type coercion
|
||||
if isinstance(current, bool):
|
||||
value = value.lower() in ("true", "1", "yes") # type: ignore[assignment]
|
||||
elif isinstance(current, int):
|
||||
value = int(value) # type: ignore[assignment]
|
||||
|
||||
setattr(obj, final_key, value)
|
||||
return True
|
||||
Reference in New Issue
Block a user