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.
- mem0_cli/__init__.py +3 -0
- mem0_cli/__main__.py +5 -0
- mem0_cli/app.py +1021 -0
- mem0_cli/backend/__init__.py +5 -0
- mem0_cli/backend/base.py +113 -0
- mem0_cli/backend/platform.py +325 -0
- mem0_cli/branding.py +130 -0
- mem0_cli/commands/__init__.py +1 -0
- mem0_cli/commands/config_cmd.py +108 -0
- mem0_cli/commands/entities.py +133 -0
- mem0_cli/commands/init_cmd.py +195 -0
- mem0_cli/commands/memory.py +469 -0
- mem0_cli/commands/utils.py +142 -0
- mem0_cli/config.py +176 -0
- mem0_cli/output.py +242 -0
- mem0_cli-0.1.0.dist-info/METADATA +57 -0
- mem0_cli-0.1.0.dist-info/RECORD +19 -0
- mem0_cli-0.1.0.dist-info/WHEEL +4 -0
- mem0_cli-0.1.0.dist-info/entry_points.txt +2 -0
mem0_cli/backend/base.py
ADDED
|
@@ -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}")
|