feat(cli): Platform option parity across Python and Node CLIs (MEM-5893) (#6696)

This commit is contained in:
Kartik
2026-08-03 17:10:44 +05:30
committed by GitHub
parent 50bdaaea0c
commit c90bdbdce0
29 changed files with 2030 additions and 541 deletions
+85 -4
View File
@@ -273,7 +273,21 @@ def add(
no_infer: bool = typer.Option(False, "--no-infer", help="Skip inference, store raw."),
expires: str | None = typer.Option(None, "--expires", help="Expiration date (YYYY-MM-DD)."),
categories: str | None = typer.Option(
None, "--categories", help="Categories (JSON array or comma-separated)."
None, "--categories", help="Not supported on add, use --custom-categories instead."
),
custom_instructions: str | None = typer.Option(
None, "--custom-instructions", help="Custom instructions for fact extraction."
),
custom_categories: str | None = typer.Option(
None,
"--custom-categories",
help="Custom categories as a JSON array of {name: description} objects.",
),
structured_data_schema: str | None = typer.Option(
None, "--structured-data-schema", help="Schema for structured data extraction, as JSON."
),
timestamp: int | None = typer.Option(
None, "--timestamp", help="Unix timestamp for the memory."
),
output: str = typer.Option(
"text", "--output", "-o", help="Output format: text, json, quiet.", rich_help_panel="Output"
@@ -312,6 +326,10 @@ def add(
no_infer=no_infer,
expires=expires,
categories=categories,
custom_instructions=custom_instructions,
custom_categories=custom_categories,
structured_data_schema=structured_data_schema,
timestamp=timestamp,
output=output,
)
@@ -355,6 +373,21 @@ def search(
help="Specific fields to return (comma-separated).",
rich_help_panel="Search",
),
show_expired: bool = typer.Option(
False, "--show-expired", help="Include expired memories.", rich_help_panel="Search"
),
reference_date: str | None = typer.Option(
None,
"--reference-date",
help="Reference date for relative queries (YYYY-MM-DD or unix timestamp).",
rich_help_panel="Search",
),
latest_only: bool = typer.Option(
False,
"--latest-only",
help="Only return the latest version of each memory.",
rich_help_panel="Search",
),
output: str = typer.Option(
"text", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
),
@@ -398,6 +431,9 @@ def search(
keyword=keyword,
filter_json=filter_json,
fields=fields,
show_expired=show_expired,
reference_date=reference_date,
latest_only=latest_only,
output=output,
)
@@ -464,6 +500,15 @@ def list_cmd(
before: str | None = typer.Option(
None, "--before", help="Created before (YYYY-MM-DD).", rich_help_panel="Filters"
),
show_expired: bool = typer.Option(
False, "--show-expired", help="Include expired memories.", rich_help_panel="Filters"
),
latest_only: bool = typer.Option(
False,
"--latest-only",
help="Only return the latest version of each memory.",
rich_help_panel="Filters",
),
output: str = typer.Option(
"table", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
),
@@ -497,6 +542,8 @@ def list_cmd(
category=category,
after=after,
before=before,
show_expired=show_expired,
latest_only=latest_only,
output=output,
)
@@ -509,6 +556,10 @@ def update(
memory_id: str = typer.Argument(..., help="Memory ID to update."),
text: str | None = typer.Argument(None, help="New memory text."),
metadata: str | None = typer.Option(None, "--metadata", "-m", help="Update metadata (JSON)."),
expires: str | None = typer.Option(None, "--expires", help="Expiration date (YYYY-MM-DD)."),
timestamp: int | None = typer.Option(
None, "--timestamp", help="Unix timestamp for the memory."
),
output: str = typer.Option(
"text", "--output", "-o", help="Output: text, json, quiet.", rich_help_panel="Output"
),
@@ -537,7 +588,15 @@ def update(
text = _read_stdin()
backend = _get_backend(api_key, base_url)
cmd_update(backend, memory_id, text, metadata=metadata, output=output)
cmd_update(
backend,
memory_id,
text,
metadata=metadata,
expires=expires,
timestamp=timestamp,
output=output,
)
# ── Memory: delete ────────────────────────────────────────────────────────
@@ -559,6 +618,9 @@ def delete(
False, "--dry-run", help="Show what would be deleted without deleting."
),
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
delete_linked: bool = typer.Option(
False, "--delete-linked", help="Also delete memories linked to this memory."
),
user_id: str | None = typer.Option(
None, "--user-id", "-u", help="Scope to user.", rich_help_panel="Scope"
),
@@ -616,7 +678,14 @@ def delete(
from mem0_cli.commands.memory import cmd_delete
backend = _get_backend(api_key, base_url)
cmd_delete(backend, memory_id, dry_run=dry_run, force=force, output=output)
cmd_delete(
backend,
memory_id,
dry_run=dry_run,
force=force,
delete_linked=delete_linked,
output=output,
)
elif all_:
_fire_telemetry("delete", {"delete_mode": "all"})
@@ -1068,7 +1137,11 @@ def _build_help_json() -> dict:
"--immutable": "Prevent future updates.",
"--no-infer": "Skip inference, store raw.",
"--expires": "Expiration date (YYYY-MM-DD).",
"--categories": "Categories (JSON array or comma-separated).",
"--categories": "Not supported on add, use --custom-categories instead.",
"--custom-instructions": "Custom instructions for fact extraction.",
"--custom-categories": "Custom categories as a JSON array of {name: description} objects.",
"--structured-data-schema": "Schema for structured data extraction, as JSON.",
"--timestamp": "Unix timestamp for the memory.",
"--graph": "Enable graph memory extraction.",
"--no-graph": "Disable graph memory extraction.",
"--output, -o": "Output format: text, json, quiet.",
@@ -1087,6 +1160,9 @@ def _build_help_json() -> dict:
"--keyword": "Use keyword search instead of semantic.",
"--filter": "Advanced filter expression (JSON).",
"--fields": "Specific fields to return (comma-separated).",
"--show-expired": "Include expired memories.",
"--reference-date": "Reference date for relative queries (YYYY-MM-DD or unix timestamp).",
"--latest-only": "Only return the latest version of each memory.",
"--graph": "Enable graph in search.",
"--no-graph": "Disable graph in search.",
"--output, -o": "Output format: text, json, table.",
@@ -1110,6 +1186,8 @@ def _build_help_json() -> dict:
"--category": "Filter by category.",
"--after": "Created after (YYYY-MM-DD).",
"--before": "Created before (YYYY-MM-DD).",
"--show-expired": "Include expired memories.",
"--latest-only": "Only return the latest version of each memory.",
"--graph": "Enable graph in listing.",
"--no-graph": "Disable graph in listing.",
"--output, -o": "Output format: text, json, table.",
@@ -1124,6 +1202,8 @@ def _build_help_json() -> dict:
},
"options": {
"--metadata, -m": "Update metadata (JSON).",
"--expires": "Expiration date (YYYY-MM-DD).",
"--timestamp": "Unix timestamp for the memory.",
"--output, -o": "Output format: text, json, quiet.",
},
},
@@ -1140,6 +1220,7 @@ def _build_help_json() -> dict:
"--all": "Delete all memories matching scope filters.",
"--entity": "Delete the entity itself and all its memories (cascade).",
"--project": "With --all: delete ALL memories project-wide.",
"--delete-linked": "Also delete memories linked to this memory.",
"--dry-run": "Show what would be deleted without deleting.",
"--force": "Skip confirmation.",
"--user-id, -u": "Scope to user.",
+17 -2
View File
@@ -25,7 +25,10 @@ class Backend(ABC):
immutable: bool = False,
infer: bool = True,
expires: str | None = None,
categories: list[str] | None = None,
custom_instructions: str | None = None,
custom_categories: list[dict] | None = None,
structured_data_schema: dict | None = None,
timestamp: int | None = None,
) -> dict: ...
@abstractmethod
@@ -43,6 +46,9 @@ class Backend(ABC):
keyword: bool = False,
filters: dict | None = None,
fields: list[str] | None = None,
show_expired: bool = False,
reference_date: str | None = None,
latest_only: bool = False,
) -> list[dict]: ...
@abstractmethod
@@ -61,11 +67,19 @@ class Backend(ABC):
category: str | None = None,
after: str | None = None,
before: str | None = None,
show_expired: bool = False,
latest_only: bool = False,
) -> list[dict]: ...
@abstractmethod
def update(
self, memory_id: str, content: str | None = None, metadata: dict | None = None
self,
memory_id: str,
content: str | None = None,
metadata: dict | None = None,
*,
expiration_date: str | None = None,
timestamp: int | None = None,
) -> dict: ...
@abstractmethod
@@ -78,6 +92,7 @@ class Backend(ABC):
agent_id: str | None = None,
app_id: str | None = None,
run_id: str | None = None,
delete_linked: bool = False,
) -> dict: ...
@abstractmethod
+43 -5
View File
@@ -87,7 +87,10 @@ class PlatformBackend(Backend):
immutable: bool = False,
infer: bool = True,
expires: str | None = None,
categories: list[str] | None = None,
custom_instructions: str | None = None,
custom_categories: list[dict] | None = None,
structured_data_schema: dict | None = None,
timestamp: int | None = None,
) -> dict:
payload: dict[str, Any] = {}
@@ -112,8 +115,14 @@ class PlatformBackend(Backend):
payload["infer"] = False
if expires:
payload["expiration_date"] = expires
if categories:
payload["categories"] = categories
if custom_instructions:
payload["custom_instructions"] = custom_instructions
if custom_categories:
payload["custom_categories"] = custom_categories
if structured_data_schema:
payload["structured_data_schema"] = structured_data_schema
if timestamp is not None:
payload["timestamp"] = timestamp
payload["source"] = "CLI"
return self._request("POST", "/v3/memories/add/", json=payload)
@@ -173,6 +182,9 @@ class PlatformBackend(Backend):
keyword: bool = False,
filters: dict | None = None,
fields: list[str] | None = None,
show_expired: bool = False,
reference_date: str | None = None,
latest_only: bool = False,
) -> list[dict]:
payload: dict[str, Any] = {"query": query, "top_k": top_k, "threshold": threshold}
@@ -191,6 +203,12 @@ class PlatformBackend(Backend):
payload["keyword_search"] = True
if fields:
payload["fields"] = fields
if show_expired:
payload["show_expired"] = True
if reference_date is not None:
payload["reference_date"] = reference_date
if latest_only:
payload["latest_only"] = True
payload["source"] = "CLI"
result = self._request("POST", "/v3/memories/search/", json=payload)
@@ -219,6 +237,8 @@ class PlatformBackend(Backend):
category: str | None = None,
after: str | None = None,
before: str | None = None,
show_expired: bool = False,
latest_only: bool = False,
) -> list[dict]:
payload: dict[str, Any] = {}
params = {"page": str(page), "page_size": str(page_size)}
@@ -241,6 +261,10 @@ class PlatformBackend(Backend):
)
if api_filters:
payload["filters"] = api_filters
if show_expired:
payload["show_expired"] = True
if latest_only:
payload["latest_only"] = True
payload["source"] = "CLI"
result = self._request("POST", "/v3/memories/", json=payload, params=params)
@@ -251,13 +275,23 @@ class PlatformBackend(Backend):
)
def update(
self, memory_id: str, content: str | None = None, metadata: dict | None = None
self,
memory_id: str,
content: str | None = None,
metadata: dict | None = None,
*,
expiration_date: str | None = None,
timestamp: int | None = None,
) -> dict:
payload: dict[str, Any] = {}
if content:
payload["text"] = content
if metadata:
payload["metadata"] = metadata
if expiration_date:
payload["expiration_date"] = expiration_date
if timestamp is not None:
payload["timestamp"] = timestamp
payload["source"] = "CLI"
return self._request(
"PUT",
@@ -274,6 +308,7 @@ class PlatformBackend(Backend):
agent_id: str | None = None,
app_id: str | None = None,
run_id: str | None = None,
delete_linked: bool = False,
) -> dict:
if all:
params: dict[str, str] = {"source": "CLI"}
@@ -287,10 +322,13 @@ class PlatformBackend(Backend):
params["run_id"] = run_id
return self._request("DELETE", "/v1/memories/", params=params)
elif memory_id:
params = {"source": "CLI"}
if delete_linked:
params["delete_linked"] = "true"
return self._request(
"DELETE",
f"/v1/memories/{_encode_path_segment(memory_id)}/",
params={"source": "CLI"},
params=params,
)
else:
raise ValueError("Either memory_id or --all is required")
+67 -20
View File
@@ -4,9 +4,11 @@ from __future__ import annotations
import json
import os
import re
import stat as _stat_mod
import sys
import time as _time
from datetime import date
from pathlib import Path
import typer
@@ -47,6 +49,18 @@ def _stdin_is_piped() -> bool:
return False
def _validate_expires(value: str) -> None:
"""Exit 1 if value is not a future YYYY-MM-DD date."""
if not re.match(r"^\d{4}-\d{2}-\d{2}$", value):
print_error(
err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31)."
)
raise typer.Exit(1)
if date.fromisoformat(value) <= date.today():
print_error(err_console, "--expires date must be in the future.")
raise typer.Exit(1)
def cmd_add(
backend: Backend,
text: str | None,
@@ -62,6 +76,10 @@ def cmd_add(
no_infer: bool,
expires: str | None,
categories: str | None,
custom_instructions: str | None = None,
custom_categories: str | None = None,
structured_data_schema: str | None = None,
timestamp: int | None = None,
output: str = "text",
) -> None:
"""Add a memory."""
@@ -70,6 +88,13 @@ def cmd_add(
set_current_command("add")
if is_agent_mode():
output = "agent"
if categories:
print_error(
err_console, "--categories is not supported on add. Use --custom-categories instead."
)
raise typer.Exit(1)
msgs = None
content = text
@@ -108,27 +133,24 @@ def cmd_add(
print_error(err_console, "Invalid JSON in --metadata.")
raise typer.Exit(1) from None
cats = None
if categories:
custom_cats = None
if custom_categories:
try:
cats = json.loads(categories)
custom_cats = json.loads(custom_categories)
except json.JSONDecodeError:
cats = [c.strip() for c in categories.split(",")]
print_error(err_console, "Invalid JSON in --custom-categories.")
raise typer.Exit(1) from None
schema = None
if structured_data_schema:
try:
schema = json.loads(structured_data_schema)
except json.JSONDecodeError:
print_error(err_console, "Invalid JSON in --structured-data-schema.")
raise typer.Exit(1) from None
# 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)
_validate_expires(expires)
with timed_status(err_console, "Adding memory...") as ts:
try:
@@ -143,7 +165,10 @@ def cmd_add(
immutable=immutable,
infer=not no_infer,
expires=expires,
categories=cats,
custom_instructions=custom_instructions,
custom_categories=custom_cats,
structured_data_schema=schema,
timestamp=timestamp,
)
except Exception as e:
ts.error_msg = str(e)
@@ -224,6 +249,9 @@ def cmd_search(
keyword: bool,
filter_json: str | None,
fields: str | None,
show_expired: bool = False,
reference_date: str | None = None,
latest_only: bool = False,
output: str = "text",
) -> None:
"""Search memories."""
@@ -266,6 +294,9 @@ def cmd_search(
keyword=keyword,
filters=filters,
fields=field_list,
show_expired=show_expired,
reference_date=reference_date,
latest_only=latest_only,
)
except Exception as e:
print_error(err_console, str(e))
@@ -352,6 +383,8 @@ def cmd_list(
category: str | None,
after: str | None,
before: str | None,
show_expired: bool = False,
latest_only: bool = False,
output: str = "table",
) -> None:
"""List memories."""
@@ -380,6 +413,8 @@ def cmd_list(
category=category,
after=after,
before=before,
show_expired=show_expired,
latest_only=latest_only,
)
except Exception as e:
print_error(err_console, str(e))
@@ -446,6 +481,8 @@ def cmd_update(
text: str | None,
*,
metadata: str | None,
expires: str | None = None,
timestamp: int | None = None,
output: str,
) -> None:
"""Update a memory."""
@@ -462,10 +499,19 @@ def cmd_update(
print_error(err_console, "Invalid JSON in --metadata.")
raise typer.Exit(1) from None
if expires:
_validate_expires(expires)
_start = _time.perf_counter()
with timed_status(err_console, "Updating memory...") as _ts:
try:
result = backend.update(memory_id, content=text, metadata=meta)
result = backend.update(
memory_id,
content=text,
metadata=meta,
expiration_date=expires,
timestamp=timestamp,
)
except Exception as e:
print_error(err_console, str(e))
raise typer.Exit(1) from None
@@ -490,6 +536,7 @@ def cmd_delete(
*,
dry_run: bool = False,
force: bool = False,
delete_linked: bool = False,
output: str,
) -> None:
"""Delete a single memory by ID."""
@@ -512,7 +559,7 @@ def cmd_delete(
_start = _time.perf_counter()
with timed_status(err_console, "Deleting...") as _ts:
try:
result = backend.delete(memory_id=memory_id)
result = backend.delete(memory_id=memory_id, delete_linked=delete_linked)
except Exception as e:
print_error(err_console, str(e))
raise typer.Exit(1) from None
+20 -4
View File
@@ -262,16 +262,32 @@ def sanitize_agent_data(command: str, data: Any) -> Any:
return result
if command == "search":
return [pick(r, ["id", "memory", "score", "created_at", "categories"]) for r in data]
return [
pick(r, ["id", "memory", "score", "created_at", "categories", "expiration_date"])
for r in data
]
if command == "list":
return [pick(r, ["id", "memory", "created_at", "categories"]) for r in data]
return [
pick(r, ["id", "memory", "created_at", "categories", "expiration_date"]) for r in data
]
if command == "get":
return pick(data, ["id", "memory", "created_at", "updated_at", "categories", "metadata"])
return pick(
data,
[
"id",
"memory",
"created_at",
"updated_at",
"categories",
"metadata",
"expiration_date",
],
)
if command == "update":
return pick(data, ["id", "memory"])
return pick(data, ["id", "memory", "expiration_date"])
if command in ("delete", "delete-all", "entity delete"):
return data