fix(cli): fix critical crashes, improve error messages, UX and validation in Node.js and Python SDKs (#4636)

This commit is contained in:
Saket Aryan
2026-03-31 20:28:48 +05:30
committed by GitHub
parent 93bd4e248c
commit 9bebcf45f7
14 changed files with 198 additions and 43 deletions
+9 -2
View File
@@ -281,8 +281,8 @@ def search(
# STEP 7: stdin fallback for query
if query is None:
query = _read_stdin()
if query is None:
print_error(err_console, "No query provided. Pass a query argument or pipe via stdin.")
if not query or not query.strip():
print_error(err_console, "Search query cannot be empty.")
raise typer.Exit(1)
backend, config = _get_backend_and_config(api_key, base_url)
@@ -1014,6 +1014,13 @@ def help(
console.print()
@app.command(rich_help_panel="Utility")
def version() -> None:
"""Show version and exit."""
from mem0_cli.commands.utils import cmd_version
cmd_version()
# Register config subgroup here so it appears after help in Management panel
app.add_typer(config_app, name="config", rich_help_panel="Management")
+41 -3
View File
@@ -95,6 +95,17 @@ def cmd_add(
except json.JSONDecodeError:
cats = [c.strip() for c in categories.split(",")]
# Validate --expires
if expires:
import re
if not re.match(r'^\d{4}-\d{2}-\d{2}$', expires):
print_error(err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).")
raise typer.Exit(1)
from datetime import date
if date.fromisoformat(expires) <= date.today():
print_error(err_console, "--expires date must be in the future.")
raise typer.Exit(1)
with timed_status(err_console, "Adding memory...") as ts:
try:
result = backend.add(
@@ -113,7 +124,6 @@ def cmd_add(
)
except Exception as e:
ts.error_msg = str(e)
print_error(err_console, str(e))
raise typer.Exit(1) from None
if output == "quiet":
@@ -164,6 +174,13 @@ def cmd_search(
if fields:
field_list = [f.strip() for f in fields.split(",")]
if top_k < 1:
print_error(err_console, "--top-k must be >= 1.")
raise typer.Exit(1)
if not (0.0 <= threshold <= 1.0):
print_error(err_console, "--threshold must be between 0.0 and 1.0.")
raise typer.Exit(1)
_start = _time.perf_counter()
with timed_status(err_console, "Searching memories...") as _ts:
try:
@@ -186,11 +203,14 @@ def cmd_search(
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "quiet":
return
if output == "json":
format_json(console, results)
elif output == "table":
if results:
format_memories_table(console, results)
format_memories_table(console, results, show_score=True)
print_result_summary(
console, len(results), duration_secs=_elapsed, user_id=user_id, agent_id=agent_id
)
@@ -238,6 +258,13 @@ def cmd_list(
output: str = "table",
) -> None:
"""List memories."""
if page_size < 1:
print_error(err_console, "--page-size must be >= 1.")
raise typer.Exit(1)
if page < 1:
print_error(err_console, "--page must be >= 1.")
raise typer.Exit(1)
_start = _time.perf_counter()
with timed_status(err_console, "Listing memories...") as _ts:
try:
@@ -258,8 +285,18 @@ def cmd_list(
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "quiet":
return
if output == "json":
format_json(console, results)
from mem0_cli.output import format_json_envelope
format_json_envelope(
console,
command="list",
data=results,
count=len(results),
scope={k: v for k, v in {"user_id": user_id, "agent_id": agent_id}.items() if v},
)
elif output == "table":
if results:
format_memories_table(console, results)
@@ -376,6 +413,7 @@ def cmd_delete_all(
# Project-wide wipe using wildcard entity IDs
if dry_run:
print_info(console, "Would delete ALL memories project-wide.")
print_info(console, "Run without --dry-run to see the actual count.")
print_info(console, "No changes made (dry run).")
return
+14 -1
View File
@@ -45,6 +45,17 @@ class Mem0Config:
platform: PlatformConfig = field(default_factory=PlatformConfig)
SHORT_KEY_ALIASES: dict[str, str] = {
"api_key": "platform.api_key",
"base_url": "platform.base_url",
"user_id": "defaults.user_id",
"agent_id": "defaults.agent_id",
"app_id": "defaults.app_id",
"run_id": "defaults.run_id",
"enable_graph": "defaults.enable_graph",
}
def ensure_config_dir() -> Path:
"""Create ~/.mem0 directory with secure permissions if it doesn't exist."""
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
@@ -140,7 +151,8 @@ def redact_key(key: str) -> str:
def get_nested_value(config: Mem0Config, dotted_key: str) -> Any:
"""Get a config value by dotted path, e.g. 'platform.api_key'."""
"""Get a config value by dotted path, e.g. 'platform.api_key' or short form 'api_key'."""
dotted_key = SHORT_KEY_ALIASES.get(dotted_key, dotted_key)
parts = dotted_key.split(".")
obj: Any = config
for part in parts:
@@ -153,6 +165,7 @@ def get_nested_value(config: Mem0Config, dotted_key: str) -> Any:
def set_nested_value(config: Mem0Config, dotted_key: str, value: str) -> bool:
"""Set a config value by dotted path. Returns True on success."""
dotted_key = SHORT_KEY_ALIASES.get(dotted_key, dotted_key)
parts = dotted_key.split(".")
obj: Any = config
for part in parts[:-1]:
+15 -5
View File
@@ -49,7 +49,7 @@ def format_memories_text(console: Console, memories: list[dict], title: str = "m
console.print()
def format_memories_table(console: Console, memories: list[dict]) -> None:
def format_memories_table(console: Console, memories: list[dict], *, show_score: bool = False) -> None:
"""Render memories in a rich table."""
table = Table(
border_style=BRAND_COLOR,
@@ -57,20 +57,30 @@ def format_memories_table(console: Console, memories: list[dict]) -> None:
row_styles=["", "dim"],
padding=(0, 1),
)
table.add_column("ID", style="dim", max_width=10)
table.add_column("ID", style="dim", max_width=38, no_wrap=True)
if show_score:
table.add_column("Score", max_width=7, justify="right")
table.add_column("Memory", max_width=50, no_wrap=False)
table.add_column("Category", max_width=14)
table.add_column("Created", max_width=12)
for mem in memories:
mem_id = mem.get("id", "")[:8]
mem_id = mem.get("id", "")
memory_text = mem.get("memory", mem.get("text", ""))
if len(memory_text) > 60:
memory_text = memory_text[:57] + "..."
categories = mem.get("categories", [])
cat = categories[0] if isinstance(categories, list) and categories else "—"
if isinstance(categories, list) and categories:
cat = categories[0] if len(categories) == 1 else f"{categories[0]} (+{len(categories) - 1})"
else:
cat = "—"
created = _format_date(mem.get("created_at")) or "—"
table.add_row(mem_id, memory_text, cat, created)
if show_score:
score = mem.get("score")
score_str = f"{score:.2f}" if score is not None else "—"
table.add_row(mem_id, score_str, memory_text, cat, created)
else:
table.add_row(mem_id, memory_text, cat, created)
console.print()
console.print(table)