ci(cli): add CI pipelines for Node and Python CLI SDKs (#4640)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -253,8 +253,12 @@ def search(
|
||||
help="Specific fields to return (comma-separated).",
|
||||
rich_help_panel="Search",
|
||||
),
|
||||
graph: bool = typer.Option(False, "--graph", help="Enable graph in search.", rich_help_panel="Search"),
|
||||
no_graph: bool = typer.Option(False, "--no-graph", help="Disable graph in search.", rich_help_panel="Search"),
|
||||
graph: bool = typer.Option(
|
||||
False, "--graph", help="Enable graph in search.", rich_help_panel="Search"
|
||||
),
|
||||
no_graph: bool = typer.Option(
|
||||
False, "--no-graph", help="Disable graph in search.", rich_help_panel="Search"
|
||||
),
|
||||
output: str = typer.Option(
|
||||
"text", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
||||
),
|
||||
@@ -372,8 +376,12 @@ def list_cmd(
|
||||
before: str | None = typer.Option(
|
||||
None, "--before", help="Created before (YYYY-MM-DD).", rich_help_panel="Filters"
|
||||
),
|
||||
graph: bool = typer.Option(False, "--graph", help="Enable graph in listing.", rich_help_panel="Filters"),
|
||||
no_graph: bool = typer.Option(False, "--no-graph", help="Disable graph in listing.", rich_help_panel="Filters"),
|
||||
graph: bool = typer.Option(
|
||||
False, "--graph", help="Enable graph in listing.", rich_help_panel="Filters"
|
||||
),
|
||||
no_graph: bool = typer.Option(
|
||||
False, "--no-graph", help="Disable graph in listing.", rich_help_panel="Filters"
|
||||
),
|
||||
output: str = typer.Option(
|
||||
"table", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
||||
),
|
||||
@@ -463,11 +471,19 @@ def update(
|
||||
|
||||
@app.command(rich_help_panel="Memory")
|
||||
def delete(
|
||||
memory_id: str | None = typer.Argument(None, help="Memory ID to delete (omit when using --all or --entity)."),
|
||||
memory_id: str | None = typer.Argument(
|
||||
None, help="Memory ID to delete (omit when using --all or --entity)."
|
||||
),
|
||||
all_: bool = typer.Option(False, "--all", help="Delete all memories matching scope filters."),
|
||||
entity: bool = typer.Option(False, "--entity", help="Delete the entity itself and all its memories (cascade)."),
|
||||
project: bool = typer.Option(False, "--project", help="With --all: delete ALL memories project-wide."),
|
||||
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting."),
|
||||
entity: bool = typer.Option(
|
||||
False, "--entity", help="Delete the entity itself and all its memories (cascade)."
|
||||
),
|
||||
project: bool = typer.Option(
|
||||
False, "--project", help="With --all: delete ALL memories project-wide."
|
||||
),
|
||||
dry_run: bool = typer.Option(
|
||||
False, "--dry-run", help="Show what would be deleted without deleting."
|
||||
),
|
||||
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
|
||||
user_id: str | None = typer.Option(
|
||||
None, "--user-id", "-u", help="Scope to user.", rich_help_panel="Scope"
|
||||
@@ -641,14 +657,12 @@ def entity_delete(
|
||||
agent_id: str | None = typer.Option(
|
||||
None, "--agent-id", help="Agent ID.", rich_help_panel="Scope"
|
||||
),
|
||||
app_id: str | None = typer.Option(
|
||||
None, "--app-id", help="App ID.", rich_help_panel="Scope"
|
||||
),
|
||||
run_id: str | None = typer.Option(
|
||||
None, "--run-id", help="Run ID.", rich_help_panel="Scope"
|
||||
),
|
||||
app_id: str | None = typer.Option(None, "--app-id", help="App ID.", rich_help_panel="Scope"),
|
||||
run_id: str | None = typer.Option(None, "--run-id", help="Run ID.", rich_help_panel="Scope"),
|
||||
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
|
||||
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting."),
|
||||
dry_run: bool = typer.Option(
|
||||
False, "--dry-run", help="Show what would be deleted without deleting."
|
||||
),
|
||||
output: str = typer.Option(
|
||||
"text", "--output", "-o", help="Output: text, json, quiet.", rich_help_panel="Output"
|
||||
),
|
||||
@@ -694,9 +708,13 @@ app.add_typer(entity_app, name="entity", rich_help_panel="Management")
|
||||
@app.command(rich_help_panel="Management")
|
||||
def init(
|
||||
api_key: str | None = typer.Option(None, "--api-key", help="API key (skip prompt)."),
|
||||
user_id: str | None = typer.Option(None, "--user-id", "-u", help="Default user ID (skip prompt)."),
|
||||
user_id: str | None = typer.Option(
|
||||
None, "--user-id", "-u", help="Default user ID (skip prompt)."
|
||||
),
|
||||
email: str | None = typer.Option(None, "--email", help="Login via email verification code."),
|
||||
code: str | None = typer.Option(None, "--code", help="Verification code (use with --email for non-interactive login)."),
|
||||
code: str | None = typer.Option(
|
||||
None, "--code", help="Verification code (use with --email for non-interactive login)."
|
||||
),
|
||||
) -> None:
|
||||
"""Interactive setup wizard for mem0 CLI.
|
||||
|
||||
@@ -747,7 +765,6 @@ def status(
|
||||
)
|
||||
|
||||
|
||||
|
||||
@app.command("import", rich_help_panel="Management")
|
||||
def import_cmd(
|
||||
file_path: str = typer.Argument(..., help="JSON file to import."),
|
||||
@@ -1018,6 +1035,7 @@ def help(
|
||||
def version() -> None:
|
||||
"""Show version and exit."""
|
||||
from mem0_cli.commands.utils import cmd_version
|
||||
|
||||
cmd_version()
|
||||
|
||||
|
||||
|
||||
@@ -105,7 +105,6 @@ class Backend(ABC):
|
||||
def entities(self, entity_type: str) -> list[dict]: ...
|
||||
|
||||
|
||||
|
||||
def get_backend(config: Mem0Config) -> Backend:
|
||||
"""Return the Platform backend."""
|
||||
from mem0_cli.backend.platform import PlatformBackend
|
||||
|
||||
@@ -312,7 +312,6 @@ class PlatformBackend(Backend):
|
||||
return items
|
||||
|
||||
|
||||
|
||||
class AuthError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@@ -28,7 +28,9 @@ def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> Non
|
||||
"""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)}")
|
||||
print_error(
|
||||
err_console, f"Invalid entity type: {entity_type}. Use: {', '.join(valid_types)}"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
_start = _time.perf_counter()
|
||||
@@ -76,7 +78,9 @@ def cmd_entities_delete(
|
||||
) -> 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.")
|
||||
print_error(
|
||||
err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id."
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
if dry_run:
|
||||
|
||||
@@ -265,7 +265,9 @@ def run_init(
|
||||
|
||||
api_key_val = result.get("api_key")
|
||||
if not api_key_val:
|
||||
print_error(err_console, "Auth succeeded but no API key was returned. Contact support.")
|
||||
print_error(
|
||||
err_console, "Auth succeeded but no API key was returned. Contact support."
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
config.platform.api_key = api_key_val
|
||||
config.platform.base_url = base_url
|
||||
|
||||
@@ -98,10 +98,14 @@ def cmd_add(
|
||||
# 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).")
|
||||
|
||||
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)
|
||||
@@ -290,6 +294,7 @@ def cmd_list(
|
||||
|
||||
if output == "json":
|
||||
from mem0_cli.output import format_json_envelope
|
||||
|
||||
format_json_envelope(
|
||||
console,
|
||||
command="list",
|
||||
|
||||
@@ -110,7 +110,9 @@ def cmd_import(
|
||||
added = 0
|
||||
failed = 0
|
||||
_start = _time.perf_counter()
|
||||
for item in track(data, description=f"[{DIM_COLOR}]Importing memories...[/]", console=err_console):
|
||||
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
|
||||
|
||||
@@ -49,7 +49,9 @@ def format_memories_text(console: Console, memories: list[dict], title: str = "m
|
||||
console.print()
|
||||
|
||||
|
||||
def format_memories_table(console: Console, memories: list[dict], *, show_score: bool = False) -> 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,
|
||||
@@ -71,7 +73,11 @@ def format_memories_table(console: Console, memories: list[dict], *, show_score:
|
||||
memory_text = memory_text[:57] + "..."
|
||||
categories = mem.get("categories", [])
|
||||
if isinstance(categories, list) and categories:
|
||||
cat = categories[0] if len(categories) == 1 else f"{categories[0]} (+{len(categories) - 1})"
|
||||
cat = (
|
||||
categories[0]
|
||||
if len(categories) == 1
|
||||
else f"{categories[0]} (+{len(categories) - 1})"
|
||||
)
|
||||
else:
|
||||
cat = "—"
|
||||
created = _format_date(mem.get("created_at")) or "—"
|
||||
|
||||
@@ -8,11 +8,19 @@ boundaries).
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[mKJHABCDfsu]")
|
||||
|
||||
|
||||
def _strip_ansi(text: str) -> str:
|
||||
"""Remove ANSI escape codes so substring checks work regardless of color mode."""
|
||||
return _ANSI_RE.sub("", text)
|
||||
|
||||
|
||||
def _run(
|
||||
args: list[str],
|
||||
@@ -28,22 +36,35 @@ def _run(
|
||||
reads config from ``<home_dir>/.mem0/config.json`` instead
|
||||
of the user's real config. This is critical for tests that
|
||||
depend on a clean (no API key) or custom config state.
|
||||
|
||||
Returns a CompletedProcess whose stdout/stderr have ANSI escape codes
|
||||
stripped. GitHub Actions sets FORCE_COLOR=1 which causes Rich/Typer to
|
||||
fragment option names like --user-id into separately-styled ANSI segments,
|
||||
making plain ``in`` checks fail. Stripping here is version-agnostic and
|
||||
ensures all assertions see the same plain text regardless of terminal env.
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
# Strip all MEM0_ env vars so tests start clean
|
||||
for key in list(env.keys()):
|
||||
if key.startswith("MEM0_"):
|
||||
del env[key]
|
||||
env.pop("FORCE_COLOR", None)
|
||||
if home_dir:
|
||||
env["HOME"] = home_dir
|
||||
if env_override:
|
||||
env.update(env_override)
|
||||
return subprocess.run(
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
)
|
||||
return subprocess.CompletedProcess(
|
||||
args=result.args,
|
||||
returncode=result.returncode,
|
||||
stdout=_strip_ansi(result.stdout),
|
||||
stderr=_strip_ansi(result.stderr),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -67,7 +88,6 @@ class TestCLIIntegration:
|
||||
assert result.returncode == 0
|
||||
assert "0.1.0" in result.stdout
|
||||
|
||||
|
||||
def test_add_help(self):
|
||||
result = _run(["add", "--help"])
|
||||
assert result.returncode == 0
|
||||
@@ -161,7 +181,12 @@ class TestCLIIsolated:
|
||||
)
|
||||
assert result.returncode != 0
|
||||
combined = result.stderr + result.stdout
|
||||
assert "memory ID" in combined.lower() or "--all" in combined or "--entity" in combined or "Error" in combined
|
||||
assert (
|
||||
"memory ID" in combined.lower()
|
||||
or "--all" in combined
|
||||
or "--entity" in combined
|
||||
or "Error" in combined
|
||||
)
|
||||
|
||||
def test_config_show_clean(self, clean_home):
|
||||
"""config show with no config should still work."""
|
||||
|
||||
@@ -62,7 +62,7 @@ class TestAddCommand:
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output="text",
|
||||
)
|
||||
@@ -88,7 +88,7 @@ class TestAddCommand:
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output="text",
|
||||
)
|
||||
@@ -113,7 +113,7 @@ class TestAddCommand:
|
||||
metadata='{"source": "test"}',
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output="text",
|
||||
)
|
||||
@@ -139,7 +139,7 @@ class TestAddCommand:
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output="json",
|
||||
)
|
||||
@@ -165,7 +165,7 @@ class TestAddCommand:
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output="quiet",
|
||||
)
|
||||
@@ -196,7 +196,7 @@ class TestAddCommand:
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output="text",
|
||||
)
|
||||
@@ -221,7 +221,7 @@ class TestAddCommand:
|
||||
metadata="not-json",
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output="text",
|
||||
)
|
||||
@@ -247,7 +247,7 @@ class TestAddCommand:
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output="text",
|
||||
)
|
||||
@@ -272,7 +272,7 @@ class TestAddCommand:
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
expires=None,
|
||||
categories="health,prefs",
|
||||
output="text",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user