mem0-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ """Backend abstraction layer for mem0 CLI."""
2
+
3
+ from mem0_cli.backend.base import Backend, get_backend
4
+
5
+ __all__ = ["Backend", "get_backend"]
@@ -0,0 +1,113 @@
1
+ """Abstract backend interface and factory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any
7
+
8
+ from mem0_cli.config import Mem0Config
9
+
10
+
11
+ class Backend(ABC):
12
+ """Abstract interface for mem0 backends."""
13
+
14
+ @abstractmethod
15
+ def add(
16
+ self,
17
+ content: str | None = None,
18
+ messages: list[dict] | None = None,
19
+ *,
20
+ user_id: str | None = None,
21
+ agent_id: str | None = None,
22
+ app_id: str | None = None,
23
+ run_id: str | None = None,
24
+ metadata: dict | None = None,
25
+ immutable: bool = False,
26
+ infer: bool = True,
27
+ expires: str | None = None,
28
+ categories: list[str] | None = None,
29
+ enable_graph: bool = False,
30
+ ) -> dict: ...
31
+
32
+ @abstractmethod
33
+ def search(
34
+ self,
35
+ query: str,
36
+ *,
37
+ user_id: str | None = None,
38
+ agent_id: str | None = None,
39
+ app_id: str | None = None,
40
+ run_id: str | None = None,
41
+ top_k: int = 10,
42
+ threshold: float = 0.3,
43
+ rerank: bool = False,
44
+ keyword: bool = False,
45
+ filters: dict | None = None,
46
+ fields: list[str] | None = None,
47
+ enable_graph: bool = False,
48
+ ) -> list[dict]: ...
49
+
50
+ @abstractmethod
51
+ def get(self, memory_id: str) -> dict: ...
52
+
53
+ @abstractmethod
54
+ def list_memories(
55
+ self,
56
+ *,
57
+ user_id: str | None = None,
58
+ agent_id: str | None = None,
59
+ app_id: str | None = None,
60
+ run_id: str | None = None,
61
+ page: int = 1,
62
+ page_size: int = 100,
63
+ category: str | None = None,
64
+ after: str | None = None,
65
+ before: str | None = None,
66
+ enable_graph: bool = False,
67
+ ) -> list[dict]: ...
68
+
69
+ @abstractmethod
70
+ def update(
71
+ self, memory_id: str, content: str | None = None, metadata: dict | None = None
72
+ ) -> dict: ...
73
+
74
+ @abstractmethod
75
+ def delete(
76
+ self,
77
+ memory_id: str | None = None,
78
+ *,
79
+ all: bool = False,
80
+ user_id: str | None = None,
81
+ agent_id: str | None = None,
82
+ app_id: str | None = None,
83
+ run_id: str | None = None,
84
+ ) -> dict: ...
85
+
86
+ @abstractmethod
87
+ def delete_entities(
88
+ self,
89
+ *,
90
+ user_id: str | None = None,
91
+ agent_id: str | None = None,
92
+ app_id: str | None = None,
93
+ run_id: str | None = None,
94
+ ) -> dict: ...
95
+
96
+ @abstractmethod
97
+ def status(
98
+ self,
99
+ *,
100
+ user_id: str | None = None,
101
+ agent_id: str | None = None,
102
+ ) -> dict[str, Any]: ...
103
+
104
+ @abstractmethod
105
+ def entities(self, entity_type: str) -> list[dict]: ...
106
+
107
+
108
+
109
+ def get_backend(config: Mem0Config) -> Backend:
110
+ """Return the Platform backend."""
111
+ from mem0_cli.backend.platform import PlatformBackend
112
+
113
+ return PlatformBackend(config.platform)
@@ -0,0 +1,325 @@
1
+ """Platform (SaaS) backend — communicates with api.mem0.ai."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from mem0_cli.backend.base import Backend
10
+ from mem0_cli.config import PlatformConfig
11
+
12
+
13
+ class PlatformBackend(Backend):
14
+ """Backend that talks to the mem0 Platform API."""
15
+
16
+ def __init__(self, config: PlatformConfig) -> None:
17
+ self.config = config
18
+ self.base_url = config.base_url.rstrip("/")
19
+ self._client = httpx.Client(
20
+ base_url=self.base_url,
21
+ headers={
22
+ "Authorization": f"Token {config.api_key}",
23
+ "Content-Type": "application/json",
24
+ },
25
+ timeout=30.0,
26
+ )
27
+
28
+ def _request(self, method: str, path: str, **kwargs: Any) -> Any:
29
+ resp = self._client.request(method, path, **kwargs)
30
+ if resp.status_code == 401:
31
+ raise AuthError("Authentication failed. Your API key may be invalid or expired.")
32
+ if resp.status_code == 404:
33
+ raise NotFoundError(f"Resource not found: {path}")
34
+ if resp.status_code == 400:
35
+ # Extract API error detail when available
36
+ try:
37
+ detail = resp.json().get("detail", resp.text)
38
+ except Exception:
39
+ detail = resp.text
40
+ raise APIError(f"Bad request to {path}: {detail}")
41
+ resp.raise_for_status()
42
+ if resp.status_code == 204:
43
+ return {}
44
+ return resp.json()
45
+
46
+ def add(
47
+ self,
48
+ content: str | None = None,
49
+ messages: list[dict] | None = None,
50
+ *,
51
+ user_id: str | None = None,
52
+ agent_id: str | None = None,
53
+ app_id: str | None = None,
54
+ run_id: str | None = None,
55
+ metadata: dict | None = None,
56
+ immutable: bool = False,
57
+ infer: bool = True,
58
+ expires: str | None = None,
59
+ categories: list[str] | None = None,
60
+ enable_graph: bool = False,
61
+ ) -> dict:
62
+ payload: dict[str, Any] = {}
63
+
64
+ if messages:
65
+ payload["messages"] = messages
66
+ elif content:
67
+ payload["messages"] = [{"role": "user", "content": content}]
68
+
69
+ if user_id:
70
+ payload["user_id"] = user_id
71
+ if agent_id:
72
+ payload["agent_id"] = agent_id
73
+ if app_id:
74
+ payload["app_id"] = app_id
75
+ if run_id:
76
+ payload["run_id"] = run_id
77
+ if metadata:
78
+ payload["metadata"] = metadata
79
+ if immutable:
80
+ payload["immutable"] = True
81
+ if not infer:
82
+ payload["infer"] = False
83
+ if expires:
84
+ payload["expiration_date"] = expires
85
+ if categories:
86
+ payload["categories"] = categories
87
+ if enable_graph:
88
+ payload["enable_graph"] = True
89
+
90
+ return self._request("POST", "/v1/memories/", json=payload)
91
+
92
+ def _build_filters(
93
+ self,
94
+ *,
95
+ user_id: str | None = None,
96
+ agent_id: str | None = None,
97
+ app_id: str | None = None,
98
+ run_id: str | None = None,
99
+ extra_filters: dict | None = None,
100
+ ) -> dict | None:
101
+ """Build a filters dict for v2 API endpoints.
102
+
103
+ Entity IDs are ANDed (all provided IDs must match).
104
+ Extra filters (date ranges, categories) are also ANDed.
105
+ """
106
+ # If caller passed a pre-built filter structure (e.g. --filter from CLI), use it directly
107
+ if extra_filters and ("AND" in extra_filters or "OR" in extra_filters):
108
+ return extra_filters
109
+
110
+ # Build AND conditions for entity IDs
111
+ and_conditions: list[dict[str, Any]] = []
112
+ if user_id:
113
+ and_conditions.append({"user_id": user_id})
114
+ if agent_id:
115
+ and_conditions.append({"agent_id": agent_id})
116
+ if app_id:
117
+ and_conditions.append({"app_id": app_id})
118
+ if run_id:
119
+ and_conditions.append({"run_id": run_id})
120
+
121
+ # Append any extra filters (dates, categories)
122
+ if extra_filters:
123
+ for k, v in extra_filters.items():
124
+ and_conditions.append({k: v})
125
+
126
+ if len(and_conditions) == 1:
127
+ return and_conditions[0]
128
+ elif and_conditions:
129
+ return {"AND": and_conditions}
130
+ else:
131
+ return None
132
+
133
+ def search(
134
+ self,
135
+ query: str,
136
+ *,
137
+ user_id: str | None = None,
138
+ agent_id: str | None = None,
139
+ app_id: str | None = None,
140
+ run_id: str | None = None,
141
+ top_k: int = 10,
142
+ threshold: float = 0.3,
143
+ rerank: bool = False,
144
+ keyword: bool = False,
145
+ filters: dict | None = None,
146
+ fields: list[str] | None = None,
147
+ enable_graph: bool = False,
148
+ ) -> list[dict]:
149
+ payload: dict[str, Any] = {"query": query, "top_k": top_k, "threshold": threshold}
150
+
151
+ api_filters = self._build_filters(
152
+ user_id=user_id,
153
+ agent_id=agent_id,
154
+ app_id=app_id,
155
+ run_id=run_id,
156
+ extra_filters=filters,
157
+ )
158
+ if api_filters:
159
+ payload["filters"] = api_filters
160
+ if rerank:
161
+ payload["rerank"] = True
162
+ if keyword:
163
+ payload["keyword_search"] = True
164
+ if fields:
165
+ payload["fields"] = fields
166
+ if enable_graph:
167
+ payload["enable_graph"] = True
168
+
169
+ result = self._request("POST", "/v2/memories/search/", json=payload)
170
+ return (
171
+ result
172
+ if isinstance(result, list)
173
+ else result.get("results", result.get("memories", []))
174
+ )
175
+
176
+ def get(self, memory_id: str) -> dict:
177
+ return self._request("GET", f"/v1/memories/{memory_id}/")
178
+
179
+ def list_memories(
180
+ self,
181
+ *,
182
+ user_id: str | None = None,
183
+ agent_id: str | None = None,
184
+ app_id: str | None = None,
185
+ run_id: str | None = None,
186
+ page: int = 1,
187
+ page_size: int = 100,
188
+ category: str | None = None,
189
+ after: str | None = None,
190
+ before: str | None = None,
191
+ enable_graph: bool = False,
192
+ ) -> list[dict]:
193
+ payload: dict[str, Any] = {}
194
+ params = {"page": str(page), "page_size": str(page_size)}
195
+
196
+ # Build filters for v2 API — entity IDs and date filters go inside "filters"
197
+ extra: dict[str, Any] = {}
198
+ if category:
199
+ extra["categories"] = {"contains": category}
200
+ if after:
201
+ extra["created_at"] = {**(extra.get("created_at", {})), "gte": after}
202
+ if before:
203
+ extra["created_at"] = {**(extra.get("created_at", {})), "lte": before}
204
+
205
+ api_filters = self._build_filters(
206
+ user_id=user_id,
207
+ agent_id=agent_id,
208
+ app_id=app_id,
209
+ run_id=run_id,
210
+ extra_filters=extra if extra else None,
211
+ )
212
+ if api_filters:
213
+ payload["filters"] = api_filters
214
+ if enable_graph:
215
+ payload["enable_graph"] = True
216
+
217
+ result = self._request("POST", "/v2/memories/", json=payload, params=params)
218
+ return (
219
+ result
220
+ if isinstance(result, list)
221
+ else result.get("results", result.get("memories", []))
222
+ )
223
+
224
+ def update(
225
+ self, memory_id: str, content: str | None = None, metadata: dict | None = None
226
+ ) -> dict:
227
+ payload: dict[str, Any] = {}
228
+ if content:
229
+ payload["text"] = content
230
+ if metadata:
231
+ payload["metadata"] = metadata
232
+ return self._request("PUT", f"/v1/memories/{memory_id}/", json=payload)
233
+
234
+ def delete(
235
+ self,
236
+ memory_id: str | None = None,
237
+ *,
238
+ all: bool = False,
239
+ user_id: str | None = None,
240
+ agent_id: str | None = None,
241
+ app_id: str | None = None,
242
+ run_id: str | None = None,
243
+ ) -> dict:
244
+ if all:
245
+ params: dict[str, str] = {}
246
+ if user_id:
247
+ params["user_id"] = user_id
248
+ if agent_id:
249
+ params["agent_id"] = agent_id
250
+ if app_id:
251
+ params["app_id"] = app_id
252
+ if run_id:
253
+ params["run_id"] = run_id
254
+ return self._request("DELETE", "/v1/memories/", params=params)
255
+ elif memory_id:
256
+ return self._request("DELETE", f"/v1/memories/{memory_id}/")
257
+ else:
258
+ raise ValueError("Either memory_id or --all is required")
259
+
260
+ def delete_entities(
261
+ self,
262
+ *,
263
+ user_id: str | None = None,
264
+ agent_id: str | None = None,
265
+ app_id: str | None = None,
266
+ run_id: str | None = None,
267
+ ) -> dict:
268
+ params: dict[str, str] = {}
269
+ if user_id:
270
+ params["user_id"] = user_id
271
+ if agent_id:
272
+ params["agent_id"] = agent_id
273
+ if app_id:
274
+ params["app_id"] = app_id
275
+ if run_id:
276
+ params["run_id"] = run_id
277
+ if not params:
278
+ raise ValueError("At least one entity ID is required for delete_entities.")
279
+ return self._request("DELETE", "/v1/entities/", params=params)
280
+
281
+ def status(
282
+ self,
283
+ *,
284
+ user_id: str | None = None,
285
+ agent_id: str | None = None,
286
+ ) -> dict[str, Any]:
287
+ """Check connectivity by making a lightweight API call."""
288
+ try:
289
+ # If entity IDs are available, validate with a minimal memories list
290
+ if user_id or agent_id:
291
+ payload: dict[str, Any] = {}
292
+ params = {"page": "1", "page_size": "1"}
293
+ api_filters = self._build_filters(user_id=user_id, agent_id=agent_id)
294
+ if api_filters:
295
+ payload["filters"] = api_filters
296
+ self._request("POST", "/v2/memories/", json=payload, params=params)
297
+ else:
298
+ # No entity IDs — use entities endpoint to validate API key
299
+ self._request("GET", "/v1/entities/")
300
+ return {"connected": True, "backend": "platform", "base_url": self.base_url}
301
+ except Exception as e:
302
+ return {"connected": False, "backend": "platform", "error": str(e)}
303
+
304
+ def entities(self, entity_type: str) -> list[dict]:
305
+ result = self._request("GET", "/v1/entities/")
306
+ items = result if isinstance(result, list) else result.get("results", [])
307
+ # Filter by entity type client-side (API returns all types)
308
+ type_map = {"users": "user", "agents": "agent", "apps": "app", "runs": "run"}
309
+ target_type = type_map.get(entity_type)
310
+ if target_type:
311
+ items = [e for e in items if e.get("type", "").lower() == target_type]
312
+ return items
313
+
314
+
315
+
316
+ class AuthError(Exception):
317
+ pass
318
+
319
+
320
+ class NotFoundError(Exception):
321
+ pass
322
+
323
+
324
+ class APIError(Exception):
325
+ pass
mem0_cli/branding.py ADDED
@@ -0,0 +1,130 @@
1
+ """Branding and ASCII art for mem0 CLI."""
2
+
3
+ import os
4
+ import sys
5
+ import time
6
+ from contextlib import contextmanager
7
+
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.status import Status
11
+ from rich.text import Text
12
+
13
+ # stderr console for spinners, errors, and timing messages
14
+ _err = Console(stderr=True)
15
+
16
+ LOGO = r"""
17
+ ███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
18
+ ████╗ ████║██╔════╝████╗ ████║██╔═████╗ ██╔════╝██║ ██║
19
+ ██╔████╔██║█████╗ ██╔████╔██║██║██╔██║ ██║ ██║ ██║
20
+ ██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║████╔╝██║ ██║ ██║ ██║
21
+ ██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝ ╚██████╗███████╗██║
22
+ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝╚══════╝╚═╝
23
+ """
24
+
25
+ LOGO_MINI = "◆ mem0"
26
+
27
+ TAGLINE = "The Memory Layer for AI Agents"
28
+
29
+ BRAND_COLOR = "#8b5cf6" # Purple
30
+ ACCENT_COLOR = "#a78bfa"
31
+ SUCCESS_COLOR = "#22c55e"
32
+ ERROR_COLOR = "#ef4444"
33
+ WARNING_COLOR = "#f59e0b"
34
+ DIM_COLOR = "#6b7280"
35
+
36
+
37
+ def _sym(fancy: str, plain: str) -> str:
38
+ """Return *fancy* when stdout is a TTY with colour, else *plain*."""
39
+ if not sys.stdout.isatty() or os.environ.get("NO_COLOR") is not None:
40
+ return plain
41
+ return fancy
42
+
43
+
44
+ def print_banner(console: Console) -> None:
45
+ """Print the mem0 welcome banner."""
46
+ logo_text = Text(LOGO, style=f"bold {BRAND_COLOR}")
47
+ tagline = Text(f" {TAGLINE}\n", style=f"{ACCENT_COLOR}")
48
+
49
+ content = Text()
50
+ content.append_text(logo_text)
51
+ content.append_text(tagline)
52
+
53
+ panel = Panel(
54
+ content,
55
+ border_style=BRAND_COLOR,
56
+ padding=(0, 2),
57
+ subtitle=f"[{DIM_COLOR}]Python SDK · v{_get_version()}[/]",
58
+ subtitle_align="right",
59
+ )
60
+ console.print(panel)
61
+
62
+
63
+ def print_success(console: Console, message: str) -> None:
64
+ sym = _sym("✓", "[ok]")
65
+ console.print(f"[{SUCCESS_COLOR}]{sym}[/] {message}")
66
+
67
+
68
+ def print_error(console: Console, message: str, hint: str | None = None) -> None:
69
+ sym = _sym("✗", "[error]")
70
+ console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {message}")
71
+ if hint:
72
+ console.print(f" [{DIM_COLOR}]{hint}[/]")
73
+
74
+
75
+ def print_warning(console: Console, message: str) -> None:
76
+ sym = _sym("⚠", "[warn]")
77
+ console.print(f"[{WARNING_COLOR}]{sym}[/] {message}")
78
+
79
+
80
+ def print_info(console: Console, message: str) -> None:
81
+ sym = _sym("◆", "*")
82
+ console.print(f"[{BRAND_COLOR}]{sym}[/] {message}")
83
+
84
+
85
+ @contextmanager
86
+ def timed_status(console: Console, message: str):
87
+ """Spinner with automatic timing. Yields a context object for setting the final message.
88
+
89
+ The spinner and timing output are sent to stderr (via ``_err``) so they
90
+ never contaminate machine-readable stdout. The *console* parameter is
91
+ kept for backward compatibility but is not used for spinner output.
92
+ """
93
+
94
+ class _Ctx:
95
+ def __init__(self):
96
+ self.success_msg = ""
97
+ self.error_msg = ""
98
+
99
+ ctx = _Ctx()
100
+ start = time.perf_counter()
101
+ try:
102
+ with Status(f"[{DIM_COLOR}]{message}[/]", console=_err):
103
+ yield ctx
104
+ except Exception:
105
+ elapsed = time.perf_counter() - start
106
+ if ctx.error_msg:
107
+ print_error(_err, f"{ctx.error_msg} ({elapsed:.2f}s)")
108
+ raise
109
+ else:
110
+ elapsed = time.perf_counter() - start
111
+ if ctx.success_msg:
112
+ print_success(_err, f"{ctx.success_msg} ({elapsed:.2f}s)")
113
+
114
+
115
+ def print_scope(console: Console, **ids: str | None) -> None:
116
+ """Show active entity scope if any IDs are set."""
117
+ parts = []
118
+ for key, val in ids.items():
119
+ if val:
120
+ label = key.replace("_", " ").replace("id", "ID").strip()
121
+ parts.append(f"{label}={val}")
122
+ if parts:
123
+ scope_str = ", ".join(parts)
124
+ console.print(f" [{DIM_COLOR}]Scope: {scope_str}[/]")
125
+
126
+
127
+ def _get_version() -> str:
128
+ from mem0_cli import __version__
129
+
130
+ return __version__
@@ -0,0 +1 @@
1
+ """CLI command modules."""
@@ -0,0 +1,108 @@
1
+ """Config management commands: show, set, get."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+
8
+ from mem0_cli.branding import ACCENT_COLOR, BRAND_COLOR, DIM_COLOR, print_error, print_success
9
+ from mem0_cli.config import (
10
+ get_nested_value,
11
+ load_config,
12
+ redact_key,
13
+ save_config,
14
+ set_nested_value,
15
+ )
16
+
17
+ console = Console()
18
+ err_console = Console(stderr=True)
19
+
20
+
21
+ def cmd_config_show(*, output: str = "text") -> None:
22
+ """Display current configuration (secrets redacted)."""
23
+ from mem0_cli.output import format_json_envelope
24
+
25
+ config = load_config()
26
+
27
+ if output == "json":
28
+ format_json_envelope(
29
+ console,
30
+ command="config show",
31
+ data={
32
+ "defaults": {
33
+ "user_id": config.defaults.user_id or None,
34
+ "agent_id": config.defaults.agent_id or None,
35
+ "app_id": config.defaults.app_id or None,
36
+ "run_id": config.defaults.run_id or None,
37
+ "enable_graph": config.defaults.enable_graph,
38
+ },
39
+ "platform": {
40
+ "api_key": redact_key(config.platform.api_key),
41
+ "base_url": config.platform.base_url,
42
+ },
43
+ },
44
+ )
45
+ return
46
+
47
+ console.print()
48
+ console.print(f" [{BRAND_COLOR}]◆ mem0 Configuration[/]\n")
49
+
50
+ table = Table(border_style=BRAND_COLOR, header_style=f"bold {ACCENT_COLOR}", padding=(0, 2))
51
+ table.add_column("Key", style="bold")
52
+ table.add_column("Value")
53
+
54
+ # Defaults
55
+ table.add_row(
56
+ "defaults.user_id",
57
+ config.defaults.user_id or f"[{DIM_COLOR}](not set)[/]",
58
+ )
59
+ table.add_row(
60
+ "defaults.agent_id",
61
+ config.defaults.agent_id or f"[{DIM_COLOR}](not set)[/]",
62
+ )
63
+ table.add_row(
64
+ "defaults.app_id",
65
+ config.defaults.app_id or f"[{DIM_COLOR}](not set)[/]",
66
+ )
67
+ table.add_row(
68
+ "defaults.run_id",
69
+ config.defaults.run_id or f"[{DIM_COLOR}](not set)[/]",
70
+ )
71
+ table.add_row(
72
+ "defaults.enable_graph",
73
+ str(config.defaults.enable_graph).lower(),
74
+ )
75
+ table.add_row("", "")
76
+
77
+ # Platform
78
+ table.add_row("[bold]platform.api_key[/]", redact_key(config.platform.api_key))
79
+ table.add_row("platform.base_url", config.platform.base_url)
80
+
81
+ console.print(table)
82
+ console.print()
83
+
84
+
85
+ def cmd_config_get(key: str) -> None:
86
+ """Get a config value."""
87
+ config = load_config()
88
+ value = get_nested_value(config, key)
89
+
90
+ if value is None:
91
+ print_error(err_console, f"Unknown config key: {key}")
92
+ else:
93
+ # Redact secrets
94
+ if "api_key" in key or "key" in key.split(".")[-1:]:
95
+ console.print(redact_key(str(value)))
96
+ else:
97
+ console.print(str(value))
98
+
99
+
100
+ def cmd_config_set(key: str, value: str) -> None:
101
+ """Set a config value."""
102
+ config = load_config()
103
+ if set_nested_value(config, key, value):
104
+ save_config(config)
105
+ display = redact_key(value) if "key" in key else value
106
+ print_success(console, f"{key} = {display}")
107
+ else:
108
+ print_error(err_console, f"Unknown config key: {key}")