feat: add official mem0 CLI (Python & TypeScript) (#4575)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""CLI command modules."""
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Config management commands: show, set, get."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from mem0_cli.branding import ACCENT_COLOR, BRAND_COLOR, DIM_COLOR, print_error, print_success
|
||||
from mem0_cli.config import (
|
||||
get_nested_value,
|
||||
load_config,
|
||||
redact_key,
|
||||
save_config,
|
||||
set_nested_value,
|
||||
)
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
|
||||
def cmd_config_show(*, output: str = "text") -> None:
|
||||
"""Display current configuration (secrets redacted)."""
|
||||
from mem0_cli.output import format_json_envelope
|
||||
|
||||
config = load_config()
|
||||
|
||||
if output == "json":
|
||||
format_json_envelope(
|
||||
console,
|
||||
command="config show",
|
||||
data={
|
||||
"defaults": {
|
||||
"user_id": config.defaults.user_id or None,
|
||||
"agent_id": config.defaults.agent_id or None,
|
||||
"app_id": config.defaults.app_id or None,
|
||||
"run_id": config.defaults.run_id or None,
|
||||
"enable_graph": config.defaults.enable_graph,
|
||||
},
|
||||
"platform": {
|
||||
"api_key": redact_key(config.platform.api_key),
|
||||
"base_url": config.platform.base_url,
|
||||
},
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
console.print()
|
||||
console.print(f" [{BRAND_COLOR}]◆ mem0 Configuration[/]\n")
|
||||
|
||||
table = Table(border_style=BRAND_COLOR, header_style=f"bold {ACCENT_COLOR}", padding=(0, 2))
|
||||
table.add_column("Key", style="bold")
|
||||
table.add_column("Value")
|
||||
|
||||
# Defaults
|
||||
table.add_row(
|
||||
"defaults.user_id",
|
||||
config.defaults.user_id or f"[{DIM_COLOR}](not set)[/]",
|
||||
)
|
||||
table.add_row(
|
||||
"defaults.agent_id",
|
||||
config.defaults.agent_id or f"[{DIM_COLOR}](not set)[/]",
|
||||
)
|
||||
table.add_row(
|
||||
"defaults.app_id",
|
||||
config.defaults.app_id or f"[{DIM_COLOR}](not set)[/]",
|
||||
)
|
||||
table.add_row(
|
||||
"defaults.run_id",
|
||||
config.defaults.run_id or f"[{DIM_COLOR}](not set)[/]",
|
||||
)
|
||||
table.add_row(
|
||||
"defaults.enable_graph",
|
||||
str(config.defaults.enable_graph).lower(),
|
||||
)
|
||||
table.add_row("", "")
|
||||
|
||||
# Platform
|
||||
table.add_row("[bold]platform.api_key[/]", redact_key(config.platform.api_key))
|
||||
table.add_row("platform.base_url", config.platform.base_url)
|
||||
|
||||
console.print(table)
|
||||
console.print()
|
||||
|
||||
|
||||
def cmd_config_get(key: str) -> None:
|
||||
"""Get a config value."""
|
||||
config = load_config()
|
||||
value = get_nested_value(config, key)
|
||||
|
||||
if value is None:
|
||||
print_error(err_console, f"Unknown config key: {key}")
|
||||
else:
|
||||
# Redact secrets
|
||||
if "api_key" in key or "key" in key.split(".")[-1:]:
|
||||
console.print(redact_key(str(value)))
|
||||
else:
|
||||
console.print(str(value))
|
||||
|
||||
|
||||
def cmd_config_set(key: str, value: str) -> None:
|
||||
"""Set a config value."""
|
||||
config = load_config()
|
||||
if set_nested_value(config, key, value):
|
||||
save_config(config)
|
||||
display = redact_key(value) if "key" in key else value
|
||||
print_success(console, f"{key} = {display}")
|
||||
else:
|
||||
print_error(err_console, f"Unknown config key: {key}")
|
||||
@@ -0,0 +1,133 @@
|
||||
"""Entity management commands."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time as _time
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from mem0_cli.backend.base import Backend
|
||||
from mem0_cli.branding import (
|
||||
ACCENT_COLOR,
|
||||
BRAND_COLOR,
|
||||
DIM_COLOR,
|
||||
print_error,
|
||||
print_info,
|
||||
print_success,
|
||||
timed_status,
|
||||
)
|
||||
from mem0_cli.output import format_json
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
|
||||
def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> None:
|
||||
"""List entities of a given type."""
|
||||
valid_types = {"users", "agents", "apps", "runs"}
|
||||
if entity_type not in valid_types:
|
||||
print_error(err_console, f"Invalid entity type: {entity_type}. Use: {', '.join(valid_types)}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, f"Fetching {entity_type}...") as _ts:
|
||||
try:
|
||||
results = backend.entities(entity_type)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e), hint="This feature may require the mem0 Platform.")
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "json":
|
||||
format_json(console, results)
|
||||
return
|
||||
|
||||
if not results:
|
||||
print_info(console, f"No {entity_type} found.")
|
||||
return
|
||||
|
||||
table = Table(border_style=BRAND_COLOR, header_style=f"bold {ACCENT_COLOR}", padding=(0, 1))
|
||||
table.add_column("Name / ID", style="bold")
|
||||
table.add_column("Created", max_width=12)
|
||||
|
||||
for entity in results:
|
||||
name = entity.get("name", entity.get("id", "—"))
|
||||
created = str(entity.get("created_at", "—"))[:10]
|
||||
table.add_row(str(name), created)
|
||||
|
||||
console.print()
|
||||
console.print(table)
|
||||
console.print(f" [{DIM_COLOR}]{len(results)} {entity_type} ({_elapsed:.2f}s)[/]")
|
||||
console.print()
|
||||
|
||||
|
||||
def cmd_entities_delete(
|
||||
backend: Backend,
|
||||
*,
|
||||
user_id: str | None,
|
||||
agent_id: str | None,
|
||||
app_id: str | None,
|
||||
run_id: str | None,
|
||||
force: bool,
|
||||
dry_run: bool = False,
|
||||
output: str,
|
||||
) -> None:
|
||||
"""Delete an entity and all its memories (cascade delete)."""
|
||||
if not any([user_id, agent_id, app_id, run_id]):
|
||||
print_error(err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if dry_run:
|
||||
scope_parts = []
|
||||
if user_id:
|
||||
scope_parts.append(f"user={user_id}")
|
||||
if agent_id:
|
||||
scope_parts.append(f"agent={agent_id}")
|
||||
if app_id:
|
||||
scope_parts.append(f"app={app_id}")
|
||||
if run_id:
|
||||
scope_parts.append(f"run={run_id}")
|
||||
scope = ", ".join(scope_parts)
|
||||
print_info(console, f"Would delete entity {scope} and all its memories.")
|
||||
print_info(console, "No changes made (dry run).")
|
||||
return
|
||||
|
||||
if not force:
|
||||
scope_parts = []
|
||||
if user_id:
|
||||
scope_parts.append(f"user={user_id}")
|
||||
if agent_id:
|
||||
scope_parts.append(f"agent={agent_id}")
|
||||
if app_id:
|
||||
scope_parts.append(f"app={app_id}")
|
||||
if run_id:
|
||||
scope_parts.append(f"run={run_id}")
|
||||
scope = ", ".join(scope_parts)
|
||||
|
||||
confirm = typer.confirm(
|
||||
f"\n \u26a0 Delete entity {scope} AND all its memories? This cannot be undone."
|
||||
)
|
||||
if not confirm:
|
||||
print_info(console, "Cancelled.")
|
||||
raise typer.Exit(0)
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Deleting entity...") as _ts:
|
||||
try:
|
||||
result = backend.delete_entities(
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
app_id=app_id,
|
||||
run_id=run_id,
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "json":
|
||||
format_json(console, result)
|
||||
elif output != "quiet":
|
||||
print_success(console, f"Entity deleted with all memories ({_elapsed:.2f}s)")
|
||||
@@ -0,0 +1,195 @@
|
||||
"""mem0 init — interactive setup wizard."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.prompt import Prompt
|
||||
|
||||
from mem0_cli.branding import (
|
||||
BRAND_COLOR,
|
||||
DIM_COLOR,
|
||||
print_banner,
|
||||
print_error,
|
||||
print_info,
|
||||
print_success,
|
||||
)
|
||||
from mem0_cli.config import Mem0Config, save_config
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
|
||||
def _prompt_secret(label: str) -> str:
|
||||
"""Prompt for a secret value, echoing '*' for each character typed."""
|
||||
sys.stdout.write(label)
|
||||
sys.stdout.flush()
|
||||
|
||||
chars: list[str] = []
|
||||
|
||||
if sys.platform == "win32":
|
||||
import msvcrt
|
||||
|
||||
while True:
|
||||
ch = msvcrt.getwch()
|
||||
if ch in ("\r", "\n"):
|
||||
sys.stdout.write("\n")
|
||||
sys.stdout.flush()
|
||||
break
|
||||
if ch == "\x03":
|
||||
raise KeyboardInterrupt
|
||||
if ch in ("\x08", "\x7f"): # backspace
|
||||
if chars:
|
||||
chars.pop()
|
||||
sys.stdout.write("\b \b")
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
chars.append(ch)
|
||||
sys.stdout.write("*")
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
import termios
|
||||
import tty
|
||||
|
||||
fd = sys.stdin.fileno()
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(fd)
|
||||
while True:
|
||||
ch = sys.stdin.read(1)
|
||||
if ch in ("\r", "\n"):
|
||||
sys.stdout.write("\r\n")
|
||||
sys.stdout.flush()
|
||||
break
|
||||
if ch == "\x03":
|
||||
raise KeyboardInterrupt
|
||||
if ch in ("\x7f", "\x08"): # backspace/delete
|
||||
if chars:
|
||||
chars.pop()
|
||||
sys.stdout.write("\b \b")
|
||||
sys.stdout.flush()
|
||||
elif ch == "\x15": # Ctrl+U — clear line
|
||||
sys.stdout.write("\b \b" * len(chars))
|
||||
sys.stdout.flush()
|
||||
chars = []
|
||||
elif ch >= " ": # ignore other control characters
|
||||
chars.append(ch)
|
||||
sys.stdout.write("*")
|
||||
sys.stdout.flush()
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
|
||||
return "".join(chars)
|
||||
|
||||
|
||||
def run_init(*, api_key: str | None = None, user_id: str | None = None) -> None:
|
||||
"""Interactive setup wizard for mem0 CLI.
|
||||
|
||||
When both *api_key* and *user_id* are supplied, all prompts are skipped
|
||||
(non-interactive mode). When running in a non-TTY without the required
|
||||
flags, an error message is printed.
|
||||
"""
|
||||
config = Mem0Config()
|
||||
|
||||
# Fully non-interactive when both flags provided
|
||||
if api_key and user_id:
|
||||
config.platform.api_key = api_key
|
||||
config.defaults.user_id = user_id
|
||||
_validate_platform(config)
|
||||
save_config(config)
|
||||
print_success(console, "Configuration saved to ~/.mem0/config.json")
|
||||
return
|
||||
|
||||
# Non-TTY without full flags -> error
|
||||
if not sys.stdin.isatty():
|
||||
if not api_key or not user_id:
|
||||
print_error(
|
||||
err_console,
|
||||
"Non-interactive terminal detected and required flags missing.",
|
||||
hint="Run: mem0 init --api-key <key> --user-id <id>",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
print_banner(console)
|
||||
console.print()
|
||||
print_info(console, "Welcome! Let's set up your mem0 CLI.\n")
|
||||
|
||||
# Use provided flags or prompt
|
||||
if api_key:
|
||||
config.platform.api_key = api_key
|
||||
else:
|
||||
_setup_platform(config)
|
||||
|
||||
if user_id:
|
||||
config.defaults.user_id = user_id
|
||||
else:
|
||||
_setup_defaults(config)
|
||||
|
||||
_validate_platform(config)
|
||||
|
||||
save_config(config)
|
||||
console.print()
|
||||
print_success(console, "Configuration saved to ~/.mem0/config.json")
|
||||
console.print()
|
||||
console.print(f" [{DIM_COLOR}]Get started:[/]")
|
||||
if config.defaults.user_id:
|
||||
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode"[/]')
|
||||
console.print(f' [{DIM_COLOR}] mem0 search "preferences"[/]')
|
||||
else:
|
||||
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode" --user-id alice[/]')
|
||||
console.print(f' [{DIM_COLOR}] mem0 search "preferences" --user-id alice[/]')
|
||||
console.print()
|
||||
|
||||
|
||||
def _setup_platform(config: Mem0Config) -> None:
|
||||
"""Platform setup flow."""
|
||||
console.print()
|
||||
console.print(f" [{DIM_COLOR}]Get your API key at https://app.mem0.ai/dashboard/api-keys[/]")
|
||||
console.print()
|
||||
|
||||
console.print(f" [{BRAND_COLOR}]API Key[/]: ", end="")
|
||||
api_key = _prompt_secret("")
|
||||
if not api_key:
|
||||
print_error(err_console, "API key is required.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
config.platform.api_key = api_key
|
||||
|
||||
|
||||
def _setup_defaults(config: Mem0Config) -> None:
|
||||
"""Collect default entity IDs."""
|
||||
console.print()
|
||||
print_info(console, "Set default entity IDs (press Enter to skip).\n")
|
||||
|
||||
user_id = Prompt.ask(
|
||||
f" [{BRAND_COLOR}]Default User ID[/] [{DIM_COLOR}](recommended)[/]",
|
||||
default="mem0-cli",
|
||||
)
|
||||
if user_id:
|
||||
config.defaults.user_id = user_id
|
||||
|
||||
|
||||
def _validate_platform(config: Mem0Config) -> None:
|
||||
"""Validate platform connection after all inputs are collected."""
|
||||
console.print()
|
||||
print_info(console, "Validating connection...")
|
||||
try:
|
||||
from mem0_cli.backend.platform import PlatformBackend
|
||||
|
||||
backend = PlatformBackend(config.platform)
|
||||
status = backend.status(
|
||||
user_id=config.defaults.user_id or None,
|
||||
agent_id=config.defaults.agent_id or None,
|
||||
)
|
||||
if status.get("connected"):
|
||||
print_success(console, "Connected to mem0 Platform!")
|
||||
else:
|
||||
print_error(
|
||||
err_console,
|
||||
f"Could not connect: {status.get('error', 'Unknown error')}",
|
||||
hint="Check your API key and try again.",
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, f"Connection test failed: {e}")
|
||||
@@ -0,0 +1,469 @@
|
||||
"""Memory CRUD commands: add, search, get, list, update, delete."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time as _time
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
|
||||
from mem0_cli.backend.base import Backend
|
||||
from mem0_cli.branding import (
|
||||
print_error,
|
||||
print_info,
|
||||
print_scope,
|
||||
print_success,
|
||||
timed_status,
|
||||
)
|
||||
from mem0_cli.output import (
|
||||
format_add_result,
|
||||
format_json,
|
||||
format_memories_table,
|
||||
format_memories_text,
|
||||
format_single_memory,
|
||||
print_result_summary,
|
||||
)
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
|
||||
def cmd_add(
|
||||
backend: Backend,
|
||||
text: str | None,
|
||||
*,
|
||||
user_id: str | None,
|
||||
agent_id: str | None,
|
||||
app_id: str | None,
|
||||
run_id: str | None,
|
||||
messages: str | None,
|
||||
file: Path | None,
|
||||
metadata: str | None,
|
||||
immutable: bool,
|
||||
no_infer: bool,
|
||||
expires: str | None,
|
||||
categories: str | None,
|
||||
enable_graph: bool = False,
|
||||
output: str = "text",
|
||||
) -> None:
|
||||
"""Add a memory."""
|
||||
msgs = None
|
||||
content = text
|
||||
|
||||
# Read from file
|
||||
if file:
|
||||
try:
|
||||
raw = Path(file).read_text()
|
||||
msgs = json.loads(raw)
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
print_error(err_console, f"Failed to read file: {e}")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
# Parse messages JSON
|
||||
elif messages:
|
||||
try:
|
||||
msgs = json.loads(messages)
|
||||
except json.JSONDecodeError as e:
|
||||
print_error(err_console, f"Invalid JSON in --messages: {e}")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
# Read from stdin if no text and stdin is piped
|
||||
elif not content and not sys.stdin.isatty():
|
||||
content = sys.stdin.read().strip()
|
||||
|
||||
if not content and not msgs:
|
||||
print_error(
|
||||
err_console, "No content provided. Pass text, --messages, --file, or pipe via stdin."
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
meta = None
|
||||
if metadata:
|
||||
try:
|
||||
meta = json.loads(metadata)
|
||||
except json.JSONDecodeError:
|
||||
print_error(err_console, "Invalid JSON in --metadata.")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
cats = None
|
||||
if categories:
|
||||
try:
|
||||
cats = json.loads(categories)
|
||||
except json.JSONDecodeError:
|
||||
cats = [c.strip() for c in categories.split(",")]
|
||||
|
||||
with timed_status(err_console, "Adding memory...") as ts:
|
||||
try:
|
||||
result = backend.add(
|
||||
content=content,
|
||||
messages=msgs,
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
app_id=app_id,
|
||||
run_id=run_id,
|
||||
metadata=meta,
|
||||
immutable=immutable,
|
||||
infer=not no_infer,
|
||||
expires=expires,
|
||||
categories=cats,
|
||||
enable_graph=enable_graph,
|
||||
)
|
||||
except Exception as e:
|
||||
ts.error_msg = str(e)
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
if output == "quiet":
|
||||
return
|
||||
|
||||
if output == "json":
|
||||
format_add_result(console, result, output)
|
||||
return
|
||||
|
||||
console.print()
|
||||
print_scope(console, user_id=user_id, agent_id=agent_id, app_id=app_id, run_id=run_id)
|
||||
# Count results
|
||||
results = result if isinstance(result, list) else result.get("results", [result])
|
||||
count = len(results) if results else 0
|
||||
print_success(
|
||||
console, f"Memory processed — {count} memor{'y' if count == 1 else 'ies'} extracted"
|
||||
)
|
||||
format_add_result(console, result, output)
|
||||
|
||||
|
||||
def cmd_search(
|
||||
backend: Backend,
|
||||
query: str,
|
||||
*,
|
||||
user_id: str | None,
|
||||
agent_id: str | None,
|
||||
app_id: str | None,
|
||||
run_id: str | None,
|
||||
top_k: int,
|
||||
threshold: float,
|
||||
rerank: bool,
|
||||
keyword: bool,
|
||||
filter_json: str | None,
|
||||
fields: str | None,
|
||||
enable_graph: bool = False,
|
||||
output: str = "text",
|
||||
) -> None:
|
||||
"""Search memories."""
|
||||
filters = None
|
||||
if filter_json:
|
||||
try:
|
||||
filters = json.loads(filter_json)
|
||||
except json.JSONDecodeError:
|
||||
print_error(err_console, "Invalid JSON in --filter.")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
field_list = None
|
||||
if fields:
|
||||
field_list = [f.strip() for f in fields.split(",")]
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Searching memories...") as _ts:
|
||||
try:
|
||||
results = backend.search(
|
||||
query,
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
app_id=app_id,
|
||||
run_id=run_id,
|
||||
top_k=top_k,
|
||||
threshold=threshold,
|
||||
rerank=rerank,
|
||||
keyword=keyword,
|
||||
filters=filters,
|
||||
fields=field_list,
|
||||
enable_graph=enable_graph,
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "json":
|
||||
format_json(console, results)
|
||||
elif output == "table":
|
||||
if results:
|
||||
format_memories_table(console, results)
|
||||
print_result_summary(
|
||||
console, len(results), duration_secs=_elapsed, user_id=user_id, agent_id=agent_id
|
||||
)
|
||||
else:
|
||||
console.print()
|
||||
print_info(console, "No memories found matching your query.")
|
||||
console.print()
|
||||
else:
|
||||
if results:
|
||||
format_memories_text(console, results)
|
||||
print_result_summary(
|
||||
console, len(results), duration_secs=_elapsed, user_id=user_id, agent_id=agent_id
|
||||
)
|
||||
else:
|
||||
console.print()
|
||||
print_info(console, "No memories found matching your query.")
|
||||
console.print()
|
||||
|
||||
|
||||
def cmd_get(backend: Backend, memory_id: str, *, output: str) -> None:
|
||||
"""Get a specific memory by ID."""
|
||||
with timed_status(err_console, "Fetching memory...") as _ts:
|
||||
try:
|
||||
result = backend.get(memory_id)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
format_single_memory(console, result, output)
|
||||
|
||||
|
||||
def cmd_list(
|
||||
backend: Backend,
|
||||
*,
|
||||
user_id: str | None,
|
||||
agent_id: str | None,
|
||||
app_id: str | None,
|
||||
run_id: str | None,
|
||||
page: int,
|
||||
page_size: int,
|
||||
category: str | None,
|
||||
after: str | None,
|
||||
before: str | None,
|
||||
enable_graph: bool = False,
|
||||
output: str = "table",
|
||||
) -> None:
|
||||
"""List memories."""
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Listing memories...") as _ts:
|
||||
try:
|
||||
results = backend.list_memories(
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
app_id=app_id,
|
||||
run_id=run_id,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
category=category,
|
||||
after=after,
|
||||
before=before,
|
||||
enable_graph=enable_graph,
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "json":
|
||||
format_json(console, results)
|
||||
elif output == "table":
|
||||
if results:
|
||||
format_memories_table(console, results)
|
||||
print_result_summary(
|
||||
console,
|
||||
len(results),
|
||||
duration_secs=_elapsed,
|
||||
page=page,
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
)
|
||||
else:
|
||||
console.print()
|
||||
print_info(console, "No memories found.")
|
||||
console.print()
|
||||
else:
|
||||
if results:
|
||||
format_memories_text(console, results, title="memories")
|
||||
print_result_summary(
|
||||
console,
|
||||
len(results),
|
||||
duration_secs=_elapsed,
|
||||
page=page,
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
)
|
||||
else:
|
||||
console.print()
|
||||
print_info(console, "No memories found.")
|
||||
console.print()
|
||||
|
||||
|
||||
def cmd_update(
|
||||
backend: Backend,
|
||||
memory_id: str,
|
||||
text: str | None,
|
||||
*,
|
||||
metadata: str | None,
|
||||
output: str,
|
||||
) -> None:
|
||||
"""Update a memory."""
|
||||
meta = None
|
||||
if metadata:
|
||||
try:
|
||||
meta = json.loads(metadata)
|
||||
except json.JSONDecodeError:
|
||||
print_error(err_console, "Invalid JSON in --metadata.")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Updating memory...") as _ts:
|
||||
try:
|
||||
result = backend.update(memory_id, content=text, metadata=meta)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "json":
|
||||
format_json(console, result)
|
||||
elif output != "quiet":
|
||||
print_success(console, f"Memory {memory_id[:8]} updated ({_elapsed:.2f}s)")
|
||||
|
||||
|
||||
def cmd_delete(
|
||||
backend: Backend,
|
||||
memory_id: str,
|
||||
*,
|
||||
dry_run: bool = False,
|
||||
force: bool = False,
|
||||
output: str,
|
||||
) -> None:
|
||||
"""Delete a single memory by ID."""
|
||||
if dry_run:
|
||||
# Fetch and display what would be deleted
|
||||
try:
|
||||
mem = backend.get(memory_id)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
format_single_memory(console, mem, output)
|
||||
print_info(console, "No changes made (dry run).")
|
||||
return
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Deleting...") as _ts:
|
||||
try:
|
||||
result = backend.delete(memory_id=memory_id)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "json":
|
||||
format_json(console, result)
|
||||
elif output != "quiet":
|
||||
print_success(console, f"Memory {memory_id[:8]} deleted ({_elapsed:.2f}s)")
|
||||
|
||||
|
||||
def cmd_delete_all(
|
||||
backend: Backend,
|
||||
*,
|
||||
force: bool,
|
||||
dry_run: bool = False,
|
||||
all_: bool = False,
|
||||
user_id: str | None,
|
||||
agent_id: str | None,
|
||||
app_id: str | None,
|
||||
run_id: str | None,
|
||||
output: str,
|
||||
) -> None:
|
||||
"""Delete all memories matching a scope."""
|
||||
if all_:
|
||||
# Project-wide wipe using wildcard entity IDs
|
||||
if dry_run:
|
||||
print_info(console, "Would delete ALL memories project-wide.")
|
||||
print_info(console, "No changes made (dry run).")
|
||||
return
|
||||
|
||||
if not force:
|
||||
confirm = typer.confirm(
|
||||
"\n ⚠ Delete ALL memories across the ENTIRE project? This cannot be undone."
|
||||
)
|
||||
if not confirm:
|
||||
print_info(console, "Cancelled.")
|
||||
raise typer.Exit(0)
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Deleting all memories project-wide...") as _ts:
|
||||
try:
|
||||
result = backend.delete(
|
||||
all=True,
|
||||
user_id="*",
|
||||
agent_id="*",
|
||||
app_id="*",
|
||||
run_id="*",
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "json":
|
||||
format_json(console, result)
|
||||
elif output != "quiet":
|
||||
if isinstance(result, dict) and "message" in result:
|
||||
print_info(console, "Deletion started. Memories will be removed in the background.")
|
||||
else:
|
||||
print_success(console, f"All project memories deleted ({_elapsed:.2f}s)")
|
||||
return
|
||||
|
||||
if dry_run:
|
||||
# List matching memories and show count
|
||||
try:
|
||||
results = backend.list_memories(
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
app_id=app_id,
|
||||
run_id=run_id,
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
count = len(results)
|
||||
print_info(console, f"Would delete {count} memor{'y' if count == 1 else 'ies'}.")
|
||||
print_info(console, "No changes made (dry run).")
|
||||
return
|
||||
|
||||
if not force:
|
||||
scope_parts = []
|
||||
if user_id:
|
||||
scope_parts.append(f"user={user_id}")
|
||||
if agent_id:
|
||||
scope_parts.append(f"agent={agent_id}")
|
||||
if app_id:
|
||||
scope_parts.append(f"app={app_id}")
|
||||
if run_id:
|
||||
scope_parts.append(f"run={run_id}")
|
||||
scope = ", ".join(scope_parts) if scope_parts else "ALL entities"
|
||||
|
||||
confirm = typer.confirm(f"\n ⚠ Delete ALL memories for {scope}? This cannot be undone.")
|
||||
if not confirm:
|
||||
print_info(console, "Cancelled.")
|
||||
raise typer.Exit(0)
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Deleting all memories...") as _ts:
|
||||
try:
|
||||
result = backend.delete(
|
||||
all=True,
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
app_id=app_id,
|
||||
run_id=run_id,
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "json":
|
||||
format_json(console, result)
|
||||
elif output != "quiet":
|
||||
if isinstance(result, dict) and "message" in result:
|
||||
print_info(console, "Deletion started. Memories will be removed in the background.")
|
||||
else:
|
||||
print_success(console, f"All matching memories deleted ({_elapsed:.2f}s)")
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Utility commands: status, version, import."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time as _time
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.progress import track
|
||||
|
||||
from mem0_cli import __version__
|
||||
from mem0_cli.backend.base import Backend
|
||||
from mem0_cli.branding import (
|
||||
BRAND_COLOR,
|
||||
DIM_COLOR,
|
||||
ERROR_COLOR,
|
||||
SUCCESS_COLOR,
|
||||
print_error,
|
||||
print_success,
|
||||
timed_status,
|
||||
)
|
||||
from mem0_cli.config import load_config
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
|
||||
def cmd_status(
|
||||
backend: Backend,
|
||||
*,
|
||||
user_id: str | None = None,
|
||||
agent_id: str | None = None,
|
||||
output: str = "text",
|
||||
) -> None:
|
||||
"""Check connectivity and auth."""
|
||||
from mem0_cli.output import format_json_envelope
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Checking connection...") as _ts:
|
||||
result = backend.status(user_id=user_id, agent_id=agent_id)
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "json":
|
||||
format_json_envelope(
|
||||
console,
|
||||
command="status",
|
||||
data={
|
||||
"connected": result.get("connected", False),
|
||||
"backend": result.get("backend", "?"),
|
||||
"base_url": result.get("base_url", ""),
|
||||
"latency_ms": int(_elapsed * 1000),
|
||||
},
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
return
|
||||
|
||||
lines = []
|
||||
if result.get("connected"):
|
||||
lines.append(f" [{SUCCESS_COLOR}]●[/] Connected")
|
||||
else:
|
||||
lines.append(f" [{ERROR_COLOR}]●[/] Disconnected")
|
||||
|
||||
lines.append(f" [{DIM_COLOR}]Backend:[/] {result.get('backend', '?')}")
|
||||
if result.get("base_url"):
|
||||
lines.append(f" [{DIM_COLOR}]API URL:[/] {result['base_url']}")
|
||||
if result.get("error"):
|
||||
lines.append(f" [{ERROR_COLOR}]Error:[/] {result['error']}")
|
||||
lines.append(f" [{DIM_COLOR}]Latency:[/] {_elapsed:.2f}s")
|
||||
|
||||
content = "\n".join(lines)
|
||||
panel = Panel(
|
||||
content,
|
||||
title=f"[{BRAND_COLOR}]Connection Status[/]",
|
||||
title_align="left",
|
||||
border_style=BRAND_COLOR,
|
||||
padding=(1, 1),
|
||||
)
|
||||
console.print()
|
||||
console.print(panel)
|
||||
console.print()
|
||||
|
||||
|
||||
def cmd_version() -> None:
|
||||
"""Show version."""
|
||||
console.print(f" [{BRAND_COLOR}]◆ Mem0[/] CLI v{__version__}")
|
||||
|
||||
|
||||
def cmd_import(
|
||||
backend: Backend,
|
||||
file_path: str,
|
||||
*,
|
||||
user_id: str | None,
|
||||
agent_id: str | None,
|
||||
output: str = "text",
|
||||
) -> None:
|
||||
"""Import memories from a JSON file."""
|
||||
from mem0_cli.output import format_json_envelope
|
||||
|
||||
try:
|
||||
data = json.loads(Path(file_path).read_text())
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
print_error(err_console, f"Failed to read file: {e}")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
if not isinstance(data, list):
|
||||
data = [data]
|
||||
|
||||
added = 0
|
||||
failed = 0
|
||||
_start = _time.perf_counter()
|
||||
for item in track(data, description=f"[{DIM_COLOR}]Importing memories...[/]", console=err_console):
|
||||
content = item.get("memory", item.get("text", item.get("content", "")))
|
||||
if not content:
|
||||
failed += 1
|
||||
continue
|
||||
try:
|
||||
backend.add(
|
||||
content=content,
|
||||
user_id=user_id or item.get("user_id"),
|
||||
agent_id=agent_id or item.get("agent_id"),
|
||||
metadata=item.get("metadata"),
|
||||
)
|
||||
added += 1
|
||||
except Exception:
|
||||
failed += 1
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "json":
|
||||
format_json_envelope(
|
||||
console,
|
||||
command="import",
|
||||
data={"added": added, "failed": failed, "duration_s": round(_elapsed, 2)},
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
return
|
||||
|
||||
print_success(err_console, f"Imported {added} memories ({_elapsed:.2f}s)")
|
||||
if failed:
|
||||
print_error(err_console, f"{failed} memories failed to import.")
|
||||
Reference in New Issue
Block a user