mem0-cli 0.2.3__tar.gz → 0.2.5__tar.gz
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-0.2.3 → mem0_cli-0.2.5}/.gitignore +6 -2
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/PKG-INFO +1 -1
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/pyproject.toml +1 -1
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/src/mem0_cli/__init__.py +1 -1
- mem0_cli-0.2.5/src/mem0_cli/agent_detect.py +36 -0
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/src/mem0_cli/app.py +71 -43
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/src/mem0_cli/backend/base.py +0 -3
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/src/mem0_cli/backend/platform.py +26 -16
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/src/mem0_cli/branding.py +5 -3
- mem0_cli-0.2.5/src/mem0_cli/commands/agent_mode_cmd.py +239 -0
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/src/mem0_cli/commands/config_cmd.py +0 -5
- mem0_cli-0.2.5/src/mem0_cli/commands/identify_cmd.py +75 -0
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/src/mem0_cli/commands/init_cmd.py +160 -4
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/src/mem0_cli/commands/memory.py +0 -6
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/src/mem0_cli/commands/utils.py +1 -1
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/src/mem0_cli/config.py +31 -9
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/src/mem0_cli/output.py +19 -0
- mem0_cli-0.2.5/src/mem0_cli/plugin_sync.py +119 -0
- mem0_cli-0.2.5/src/mem0_cli/state.py +45 -0
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/src/mem0_cli/telemetry.py +4 -2
- mem0_cli-0.2.3/src/mem0_cli/state.py +0 -24
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/README.md +0 -0
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/src/mem0_cli/__main__.py +0 -0
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/src/mem0_cli/backend/__init__.py +0 -0
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/src/mem0_cli/commands/__init__.py +0 -0
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/src/mem0_cli/commands/entities.py +0 -0
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/src/mem0_cli/commands/events_cmd.py +0 -0
- {mem0_cli-0.2.3 → mem0_cli-0.2.5}/src/mem0_cli/telemetry_sender.py +0 -0
|
@@ -4,6 +4,10 @@ __pycache__/
|
|
|
4
4
|
*$py.class
|
|
5
5
|
**/node_modules/
|
|
6
6
|
|
|
7
|
+
# Self-hosted server local runtime state
|
|
8
|
+
server/history/
|
|
9
|
+
server/.env
|
|
10
|
+
|
|
7
11
|
# C extensions
|
|
8
12
|
*.so
|
|
9
13
|
|
|
@@ -15,8 +19,8 @@ dist/
|
|
|
15
19
|
downloads/
|
|
16
20
|
eggs/
|
|
17
21
|
.eggs/
|
|
18
|
-
lib/
|
|
19
|
-
lib64/
|
|
22
|
+
/lib/
|
|
23
|
+
/lib64/
|
|
20
24
|
parts/
|
|
21
25
|
sdist/
|
|
22
26
|
var/
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Detect whether the CLI is being invoked from inside an AI-agent context.
|
|
2
|
+
|
|
3
|
+
Used by `mem0 init` to auto-enter Agent Mode (Rule 3 bootstrap) when an
|
|
4
|
+
agent runtime env var is present. The return value is a context **trigger
|
|
5
|
+
only** — the canonical agent identity is self-declared by the agent via
|
|
6
|
+
``--agent-caller <name>`` (Proof Editor-style) and never sniffed from env
|
|
7
|
+
vars to fill the ``agent_caller`` field on the APIKey row.
|
|
8
|
+
|
|
9
|
+
Returns a short name or None. The list is curated, not exhaustive — env
|
|
10
|
+
vars we don't recognise fall through to None (caller treated as
|
|
11
|
+
non-agent). Honest reporting depends on ``--agent-caller``; this list is
|
|
12
|
+
just enough to enable the zero-friction auto-bootstrap UX.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
|
|
19
|
+
_AGENT_CALLER_ENV: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
20
|
+
("claude-code", ("CLAUDECODE", "CLAUDE_CODE")),
|
|
21
|
+
("cursor", ("CURSOR_AGENT", "CURSOR_SESSION_ID")),
|
|
22
|
+
("codex", ("CODEX_CLI", "OPENAI_CODEX")),
|
|
23
|
+
("cline", ("CLINE_AGENT", "CLINE")),
|
|
24
|
+
("continue", ("CONTINUE_AGENT", "CONTINUE_SESSION")),
|
|
25
|
+
("aider", ("AIDER_SESSION",)),
|
|
26
|
+
("goose", ("GOOSE_AGENT",)),
|
|
27
|
+
("windsurf", ("WINDSURF_AGENT",)),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def detect_agent_caller() -> str | None:
|
|
32
|
+
"""Return a canonical agent name if any agent env var is set, else None."""
|
|
33
|
+
for name, env_vars in _AGENT_CALLER_ENV:
|
|
34
|
+
if any(os.environ.get(v) for v in env_vars):
|
|
35
|
+
return name
|
|
36
|
+
return None
|
|
@@ -237,6 +237,14 @@ def main_callback(
|
|
|
237
237
|
cmd_version()
|
|
238
238
|
raise typer.Exit()
|
|
239
239
|
if ctx.invoked_subcommand:
|
|
240
|
+
# Stash the active subcommand name so the JSON error envelope
|
|
241
|
+
# (print_error in agent mode) can report which command failed
|
|
242
|
+
# instead of an empty `"command": ""` field.
|
|
243
|
+
from mem0_cli.state import set_current_command
|
|
244
|
+
|
|
245
|
+
set_current_command(ctx.invoked_subcommand)
|
|
246
|
+
if ctx.invoked_subcommand and ctx.invoked_subcommand != "init":
|
|
247
|
+
# init fires its own telemetry from init_cmd.run_init with full M1-M6 props.
|
|
240
248
|
_fire_telemetry(ctx.invoked_subcommand)
|
|
241
249
|
|
|
242
250
|
|
|
@@ -267,8 +275,6 @@ def add(
|
|
|
267
275
|
categories: str | None = typer.Option(
|
|
268
276
|
None, "--categories", help="Categories (JSON array or comma-separated)."
|
|
269
277
|
),
|
|
270
|
-
graph: bool = typer.Option(False, "--graph", help="Enable graph memory extraction."),
|
|
271
|
-
no_graph: bool = typer.Option(False, "--no-graph", help="Disable graph memory extraction."),
|
|
272
278
|
output: str = typer.Option(
|
|
273
279
|
"text", "--output", "-o", help="Output format: text, json, quiet.", rich_help_panel="Output"
|
|
274
280
|
),
|
|
@@ -295,13 +301,6 @@ def add(
|
|
|
295
301
|
backend, config = _get_backend_and_config(api_key, base_url)
|
|
296
302
|
ids = _resolve_ids(config, user_id=user_id, agent_id=agent_id, app_id=app_id, run_id=run_id)
|
|
297
303
|
|
|
298
|
-
if no_graph:
|
|
299
|
-
graph_enabled = False
|
|
300
|
-
elif graph:
|
|
301
|
-
graph_enabled = True
|
|
302
|
-
else:
|
|
303
|
-
graph_enabled = config.defaults.enable_graph
|
|
304
|
-
|
|
305
304
|
cmd_add(
|
|
306
305
|
backend,
|
|
307
306
|
text,
|
|
@@ -313,7 +312,6 @@ def add(
|
|
|
313
312
|
no_infer=no_infer,
|
|
314
313
|
expires=expires,
|
|
315
314
|
categories=categories,
|
|
316
|
-
enable_graph=graph_enabled,
|
|
317
315
|
output=output,
|
|
318
316
|
)
|
|
319
317
|
|
|
@@ -357,12 +355,6 @@ def search(
|
|
|
357
355
|
help="Specific fields to return (comma-separated).",
|
|
358
356
|
rich_help_panel="Search",
|
|
359
357
|
),
|
|
360
|
-
graph: bool = typer.Option(
|
|
361
|
-
False, "--graph", help="Enable graph in search.", rich_help_panel="Search"
|
|
362
|
-
),
|
|
363
|
-
no_graph: bool = typer.Option(
|
|
364
|
-
False, "--no-graph", help="Disable graph in search.", rich_help_panel="Search"
|
|
365
|
-
),
|
|
366
358
|
output: str = typer.Option(
|
|
367
359
|
"text", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
|
368
360
|
),
|
|
@@ -396,13 +388,6 @@ def search(
|
|
|
396
388
|
backend, config = _get_backend_and_config(api_key, base_url)
|
|
397
389
|
ids = _resolve_ids(config, user_id=user_id, agent_id=agent_id, app_id=app_id, run_id=run_id)
|
|
398
390
|
|
|
399
|
-
if no_graph:
|
|
400
|
-
graph_enabled = False
|
|
401
|
-
elif graph:
|
|
402
|
-
graph_enabled = True
|
|
403
|
-
else:
|
|
404
|
-
graph_enabled = config.defaults.enable_graph
|
|
405
|
-
|
|
406
391
|
cmd_search(
|
|
407
392
|
backend,
|
|
408
393
|
query,
|
|
@@ -413,7 +398,6 @@ def search(
|
|
|
413
398
|
keyword=keyword,
|
|
414
399
|
filter_json=filter_json,
|
|
415
400
|
fields=fields,
|
|
416
|
-
enable_graph=graph_enabled,
|
|
417
401
|
output=output,
|
|
418
402
|
)
|
|
419
403
|
|
|
@@ -480,12 +464,6 @@ def list_cmd(
|
|
|
480
464
|
before: str | None = typer.Option(
|
|
481
465
|
None, "--before", help="Created before (YYYY-MM-DD).", rich_help_panel="Filters"
|
|
482
466
|
),
|
|
483
|
-
graph: bool = typer.Option(
|
|
484
|
-
False, "--graph", help="Enable graph in listing.", rich_help_panel="Filters"
|
|
485
|
-
),
|
|
486
|
-
no_graph: bool = typer.Option(
|
|
487
|
-
False, "--no-graph", help="Disable graph in listing.", rich_help_panel="Filters"
|
|
488
|
-
),
|
|
489
467
|
output: str = typer.Option(
|
|
490
468
|
"table", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
|
491
469
|
),
|
|
@@ -511,13 +489,6 @@ def list_cmd(
|
|
|
511
489
|
backend, config = _get_backend_and_config(api_key, base_url)
|
|
512
490
|
ids = _resolve_ids(config, user_id=user_id, agent_id=agent_id, app_id=app_id, run_id=run_id)
|
|
513
491
|
|
|
514
|
-
if no_graph:
|
|
515
|
-
graph_enabled = False
|
|
516
|
-
elif graph:
|
|
517
|
-
graph_enabled = True
|
|
518
|
-
else:
|
|
519
|
-
graph_enabled = config.defaults.enable_graph
|
|
520
|
-
|
|
521
492
|
cmd_list(
|
|
522
493
|
backend,
|
|
523
494
|
**ids,
|
|
@@ -526,7 +497,6 @@ def list_cmd(
|
|
|
526
497
|
category=category,
|
|
527
498
|
after=after,
|
|
528
499
|
before=before,
|
|
529
|
-
enable_graph=graph_enabled,
|
|
530
500
|
output=output,
|
|
531
501
|
)
|
|
532
502
|
|
|
@@ -889,6 +859,19 @@ def init(
|
|
|
889
859
|
force: bool = typer.Option(
|
|
890
860
|
False, "--force", help="Overwrite existing config without confirmation."
|
|
891
861
|
),
|
|
862
|
+
agent_signal: bool = typer.Option(
|
|
863
|
+
False, "--agent", help="Bootstrap an unattended Agent Mode account (no email required)."
|
|
864
|
+
),
|
|
865
|
+
source: str | None = typer.Option(
|
|
866
|
+
None,
|
|
867
|
+
"--source",
|
|
868
|
+
help="Channel attribution for signup (e.g. github, hn, ph).",
|
|
869
|
+
),
|
|
870
|
+
agent_caller: str | None = typer.Option(
|
|
871
|
+
None,
|
|
872
|
+
"--agent-caller",
|
|
873
|
+
help="Self-declared agent identity (e.g. claude-code, cursor). Used with --agent to attribute Agent Mode signups.",
|
|
874
|
+
),
|
|
892
875
|
) -> None:
|
|
893
876
|
"""Interactive setup wizard for mem0 CLI.
|
|
894
877
|
|
|
@@ -897,10 +880,38 @@ def init(
|
|
|
897
880
|
mem0 init --api-key m0-xxx --user-id alice
|
|
898
881
|
mem0 init --email alice@company.com
|
|
899
882
|
mem0 init --email alice@company.com --code 482901
|
|
883
|
+
mem0 init --agent --agent-caller claude-code # AI agent self-identifies on Agent Mode bootstrap
|
|
884
|
+
mem0 init --email alice@company.com # Claims an existing Agent Mode key when one is present
|
|
900
885
|
"""
|
|
901
886
|
from mem0_cli.commands.init_cmd import run_init
|
|
902
887
|
|
|
903
|
-
run_init(
|
|
888
|
+
run_init(
|
|
889
|
+
api_key=api_key,
|
|
890
|
+
user_id=user_id,
|
|
891
|
+
email=email,
|
|
892
|
+
code=code,
|
|
893
|
+
force=force,
|
|
894
|
+
source=source,
|
|
895
|
+
agent=agent_signal,
|
|
896
|
+
agent_caller=agent_caller,
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
@app.command(rich_help_panel="Setup")
|
|
901
|
+
def identify(
|
|
902
|
+
name: str = typer.Argument(..., help="Agent identity (e.g. claude-code, cursor, my-bot)."),
|
|
903
|
+
) -> None:
|
|
904
|
+
"""Tag your active Agent Mode key with the AI agent that's using it.
|
|
905
|
+
|
|
906
|
+
Run this once after `mem0 init --agent` if you didn't pass --agent-caller.
|
|
907
|
+
Idempotent — re-running just overwrites the value.
|
|
908
|
+
|
|
909
|
+
Example:
|
|
910
|
+
mem0 identify claude-code
|
|
911
|
+
"""
|
|
912
|
+
from mem0_cli.commands.identify_cmd import run_identify
|
|
913
|
+
|
|
914
|
+
run_identify(name)
|
|
904
915
|
|
|
905
916
|
|
|
906
917
|
# (entity_app registered at module level, below sub-group definitions)
|
|
@@ -1236,11 +1247,28 @@ def main() -> None:
|
|
|
1236
1247
|
import sys
|
|
1237
1248
|
|
|
1238
1249
|
# Allow --json/--agent anywhere in the command line (not just before subcommand).
|
|
1239
|
-
|
|
1240
|
-
|
|
1250
|
+
# Special case: `mem0 init --agent` is a subcommand flag (Agent Mode bootstrap)
|
|
1251
|
+
# consumed by init_cmd, not a global JSON-output toggle — leave it in argv.
|
|
1252
|
+
argv_rest = sys.argv[1:]
|
|
1253
|
+
is_init = "init" in argv_rest
|
|
1254
|
+
_global_flags = {"--json"} if is_init else {"--json", "--agent"}
|
|
1255
|
+
if any(a in _global_flags for a in argv_rest):
|
|
1241
1256
|
from mem0_cli.state import set_agent_mode
|
|
1242
1257
|
|
|
1243
1258
|
set_agent_mode(True)
|
|
1244
|
-
sys.argv = [sys.argv[0]] + [a for a in
|
|
1259
|
+
sys.argv = [sys.argv[0]] + [a for a in argv_rest if a not in _global_flags]
|
|
1245
1260
|
|
|
1246
|
-
|
|
1261
|
+
try:
|
|
1262
|
+
app()
|
|
1263
|
+
finally:
|
|
1264
|
+
# Surface any unclaimed Agent Mode notice once per command, after the
|
|
1265
|
+
# primary output. In JSON/agent mode the notice is folded into the
|
|
1266
|
+
# envelope by format_json_envelope, so skip the stderr banner there
|
|
1267
|
+
# to avoid duplicate output.
|
|
1268
|
+
from mem0_cli.state import is_agent_mode, take_notice
|
|
1269
|
+
|
|
1270
|
+
notice = take_notice()
|
|
1271
|
+
if notice and not is_agent_mode():
|
|
1272
|
+
from rich.console import Console
|
|
1273
|
+
|
|
1274
|
+
Console(stderr=True).print(f"\n[yellow]🔔 {notice}[/yellow]\n")
|
|
@@ -26,7 +26,6 @@ class Backend(ABC):
|
|
|
26
26
|
infer: bool = True,
|
|
27
27
|
expires: str | None = None,
|
|
28
28
|
categories: list[str] | None = None,
|
|
29
|
-
enable_graph: bool = False,
|
|
30
29
|
) -> dict: ...
|
|
31
30
|
|
|
32
31
|
@abstractmethod
|
|
@@ -44,7 +43,6 @@ class Backend(ABC):
|
|
|
44
43
|
keyword: bool = False,
|
|
45
44
|
filters: dict | None = None,
|
|
46
45
|
fields: list[str] | None = None,
|
|
47
|
-
enable_graph: bool = False,
|
|
48
46
|
) -> list[dict]: ...
|
|
49
47
|
|
|
50
48
|
@abstractmethod
|
|
@@ -63,7 +61,6 @@ class Backend(ABC):
|
|
|
63
61
|
category: str | None = None,
|
|
64
62
|
after: str | None = None,
|
|
65
63
|
before: str | None = None,
|
|
66
|
-
enable_graph: bool = False,
|
|
67
64
|
) -> list[dict]: ...
|
|
68
65
|
|
|
69
66
|
@abstractmethod
|
|
@@ -30,7 +30,7 @@ class PlatformBackend(Backend):
|
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
33
|
-
from mem0_cli.state import is_agent_mode
|
|
33
|
+
from mem0_cli.state import capture_notice, is_agent_mode
|
|
34
34
|
|
|
35
35
|
self._client.headers["X-Mem0-Caller-Type"] = "agent" if is_agent_mode() else "user"
|
|
36
36
|
resp = self._client.request(method, path, **kwargs)
|
|
@@ -48,7 +48,26 @@ class PlatformBackend(Backend):
|
|
|
48
48
|
resp.raise_for_status()
|
|
49
49
|
if resp.status_code == 204:
|
|
50
50
|
return {}
|
|
51
|
-
|
|
51
|
+
data = resp.json()
|
|
52
|
+
|
|
53
|
+
# Pull the unclaimed-Agent-Mode notice out of the body (or the header
|
|
54
|
+
# fallback for endpoints that return non-dict / non-dict-leading
|
|
55
|
+
# payloads) and stash it for end-of-command surfacing.
|
|
56
|
+
notice = None
|
|
57
|
+
if isinstance(data, dict) and "mem0_notice" in data:
|
|
58
|
+
notice = data.pop("mem0_notice")
|
|
59
|
+
elif (
|
|
60
|
+
isinstance(data, list)
|
|
61
|
+
and data
|
|
62
|
+
and isinstance(data[0], dict)
|
|
63
|
+
and "mem0_notice" in data[0]
|
|
64
|
+
):
|
|
65
|
+
notice = data[0].pop("mem0_notice")
|
|
66
|
+
if notice is None:
|
|
67
|
+
notice = resp.headers.get("X-Mem0-Notice-Message") or None
|
|
68
|
+
capture_notice(notice)
|
|
69
|
+
|
|
70
|
+
return data
|
|
52
71
|
|
|
53
72
|
def add(
|
|
54
73
|
self,
|
|
@@ -64,7 +83,6 @@ class PlatformBackend(Backend):
|
|
|
64
83
|
infer: bool = True,
|
|
65
84
|
expires: str | None = None,
|
|
66
85
|
categories: list[str] | None = None,
|
|
67
|
-
enable_graph: bool = False,
|
|
68
86
|
) -> dict:
|
|
69
87
|
payload: dict[str, Any] = {}
|
|
70
88
|
|
|
@@ -91,11 +109,9 @@ class PlatformBackend(Backend):
|
|
|
91
109
|
payload["expiration_date"] = expires
|
|
92
110
|
if categories:
|
|
93
111
|
payload["categories"] = categories
|
|
94
|
-
if enable_graph:
|
|
95
|
-
payload["enable_graph"] = True
|
|
96
112
|
payload["source"] = "CLI"
|
|
97
113
|
|
|
98
|
-
return self._request("POST", "/
|
|
114
|
+
return self._request("POST", "/v3/memories/add/", json=payload)
|
|
99
115
|
|
|
100
116
|
def _build_filters(
|
|
101
117
|
self,
|
|
@@ -106,7 +122,7 @@ class PlatformBackend(Backend):
|
|
|
106
122
|
run_id: str | None = None,
|
|
107
123
|
extra_filters: dict | None = None,
|
|
108
124
|
) -> dict | None:
|
|
109
|
-
"""Build a filters dict for
|
|
125
|
+
"""Build a filters dict for v3 API endpoints.
|
|
110
126
|
|
|
111
127
|
Entity IDs are ANDed (all provided IDs must match).
|
|
112
128
|
Extra filters (date ranges, categories) are also ANDed.
|
|
@@ -152,7 +168,6 @@ class PlatformBackend(Backend):
|
|
|
152
168
|
keyword: bool = False,
|
|
153
169
|
filters: dict | None = None,
|
|
154
170
|
fields: list[str] | None = None,
|
|
155
|
-
enable_graph: bool = False,
|
|
156
171
|
) -> list[dict]:
|
|
157
172
|
payload: dict[str, Any] = {"query": query, "top_k": top_k, "threshold": threshold}
|
|
158
173
|
|
|
@@ -171,11 +186,9 @@ class PlatformBackend(Backend):
|
|
|
171
186
|
payload["keyword_search"] = True
|
|
172
187
|
if fields:
|
|
173
188
|
payload["fields"] = fields
|
|
174
|
-
if enable_graph:
|
|
175
|
-
payload["enable_graph"] = True
|
|
176
189
|
payload["source"] = "CLI"
|
|
177
190
|
|
|
178
|
-
result = self._request("POST", "/
|
|
191
|
+
result = self._request("POST", "/v3/memories/search/", json=payload)
|
|
179
192
|
return (
|
|
180
193
|
result
|
|
181
194
|
if isinstance(result, list)
|
|
@@ -197,12 +210,11 @@ class PlatformBackend(Backend):
|
|
|
197
210
|
category: str | None = None,
|
|
198
211
|
after: str | None = None,
|
|
199
212
|
before: str | None = None,
|
|
200
|
-
enable_graph: bool = False,
|
|
201
213
|
) -> list[dict]:
|
|
202
214
|
payload: dict[str, Any] = {}
|
|
203
215
|
params = {"page": str(page), "page_size": str(page_size)}
|
|
204
216
|
|
|
205
|
-
# Build filters
|
|
217
|
+
# Build filters — entity IDs and date filters go inside "filters"
|
|
206
218
|
extra: dict[str, Any] = {}
|
|
207
219
|
if category:
|
|
208
220
|
extra["categories"] = {"contains": category}
|
|
@@ -220,11 +232,9 @@ class PlatformBackend(Backend):
|
|
|
220
232
|
)
|
|
221
233
|
if api_filters:
|
|
222
234
|
payload["filters"] = api_filters
|
|
223
|
-
if enable_graph:
|
|
224
|
-
payload["enable_graph"] = True
|
|
225
235
|
payload["source"] = "CLI"
|
|
226
236
|
|
|
227
|
-
result = self._request("POST", "/
|
|
237
|
+
result = self._request("POST", "/v3/memories/", json=payload, params=params)
|
|
228
238
|
return (
|
|
229
239
|
result
|
|
230
240
|
if isinstance(result, list)
|
|
@@ -87,10 +87,12 @@ def print_error(console: Console, message: str, hint: str | None = None) -> None
|
|
|
87
87
|
}
|
|
88
88
|
print(_json.dumps(envelope))
|
|
89
89
|
return
|
|
90
|
+
from rich.markup import escape
|
|
91
|
+
|
|
90
92
|
sym = _sym("✗", "[error]")
|
|
91
|
-
console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {message}")
|
|
93
|
+
console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {escape(str(message))}")
|
|
92
94
|
if hint:
|
|
93
|
-
console.print(f" [{DIM_COLOR}]{hint}[/]")
|
|
95
|
+
console.print(f" [{DIM_COLOR}]{escape(str(hint))}[/]")
|
|
94
96
|
|
|
95
97
|
|
|
96
98
|
def print_warning(console: Console, message: str) -> None:
|
|
@@ -146,7 +148,7 @@ def timed_status(console: Console, message: str):
|
|
|
146
148
|
if "Authentication failed" in ctx.error_msg:
|
|
147
149
|
_err.print(
|
|
148
150
|
f" [{DIM_COLOR}]Run [bold]mem0 init[/bold] to reconfigure your API key"
|
|
149
|
-
f" · [bold]https://app.mem0.ai/dashboard/api-keys[/bold][/]"
|
|
151
|
+
f" · [bold]https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-python[/bold][/]"
|
|
150
152
|
)
|
|
151
153
|
raise
|
|
152
154
|
else:
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Agent Mode commands — bootstrap (unattended signup) and claim (OTP-based human upgrade)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.prompt import Prompt
|
|
14
|
+
|
|
15
|
+
from mem0_cli.branding import (
|
|
16
|
+
BRAND_COLOR,
|
|
17
|
+
DIM_COLOR,
|
|
18
|
+
print_error,
|
|
19
|
+
print_success,
|
|
20
|
+
)
|
|
21
|
+
from mem0_cli.config import Mem0Config, save_config
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
err_console = Console(stderr=True)
|
|
25
|
+
|
|
26
|
+
_SOURCE_HEADERS = {
|
|
27
|
+
"X-Mem0-Source": "cli",
|
|
28
|
+
"X-Mem0-Client-Language": "python",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _validate_envelope(envelope: Any) -> None:
|
|
33
|
+
"""Defend against partial/malformed backend responses.
|
|
34
|
+
|
|
35
|
+
A backend regression that returns ``{"api_key": null}`` would otherwise be
|
|
36
|
+
silently persisted, producing confusing downstream errors far from the
|
|
37
|
+
source. Fail fast with a clear message if the required fields are missing.
|
|
38
|
+
"""
|
|
39
|
+
if not isinstance(envelope, dict):
|
|
40
|
+
print_error(err_console, "Bootstrap response was not a JSON object.")
|
|
41
|
+
raise typer.Exit(1)
|
|
42
|
+
for field in ("api_key", "default_user_id"):
|
|
43
|
+
value = envelope.get(field)
|
|
44
|
+
if not isinstance(value, str) or not value:
|
|
45
|
+
print_error(
|
|
46
|
+
err_console,
|
|
47
|
+
f"Bootstrap response missing required field {field!r} — please update the CLI.",
|
|
48
|
+
)
|
|
49
|
+
raise typer.Exit(1)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def bootstrap_via_backend(
|
|
53
|
+
config: Mem0Config,
|
|
54
|
+
*,
|
|
55
|
+
source: str | None = None,
|
|
56
|
+
agent_caller: str | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""POST /api/v1/auth/agent_mode/ and mutate config in place.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
config: Mem0Config mutated in place with the new platform values.
|
|
62
|
+
source: ``--source`` flag passthrough (analytics tag, free-form).
|
|
63
|
+
agent_caller: Self-declared agent identity passed via ``--agent-caller``
|
|
64
|
+
(e.g. ``claude-code``, ``cursor``). May be None when the caller
|
|
65
|
+
omitted the flag; the agent can backfill later via
|
|
66
|
+
``mem0 identify <name>``. Sent to the backend in the request body
|
|
67
|
+
and saved into ``platform.agent_caller`` for local introspection.
|
|
68
|
+
|
|
69
|
+
Raises typer.Exit(1) on failure.
|
|
70
|
+
"""
|
|
71
|
+
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
|
|
72
|
+
body: dict[str, Any] = {}
|
|
73
|
+
if source:
|
|
74
|
+
body["source"] = source
|
|
75
|
+
if agent_caller:
|
|
76
|
+
body["agent_caller"] = agent_caller
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
with httpx.Client(timeout=30.0) as client:
|
|
80
|
+
resp = client.post(
|
|
81
|
+
f"{base_url}/api/v1/auth/agent_mode/",
|
|
82
|
+
headers={**_SOURCE_HEADERS, "Content-Type": "application/json"},
|
|
83
|
+
json=body,
|
|
84
|
+
)
|
|
85
|
+
except httpx.HTTPError as exc:
|
|
86
|
+
print_error(err_console, f"Network error contacting Mem0: {exc}")
|
|
87
|
+
raise typer.Exit(1) from exc
|
|
88
|
+
|
|
89
|
+
if resp.status_code == 429:
|
|
90
|
+
print_error(err_console, "Rate-limited. Try again in a few minutes.")
|
|
91
|
+
raise typer.Exit(1)
|
|
92
|
+
if resp.status_code == 503:
|
|
93
|
+
print_error(err_console, "Agent Mode is temporarily disabled. Try again later.")
|
|
94
|
+
raise typer.Exit(1)
|
|
95
|
+
if resp.status_code != 200:
|
|
96
|
+
detail = resp.text
|
|
97
|
+
try:
|
|
98
|
+
err_body = resp.json()
|
|
99
|
+
detail = err_body.get("error") or err_body.get("detail") or resp.text
|
|
100
|
+
except (json.JSONDecodeError, ValueError, AttributeError):
|
|
101
|
+
pass
|
|
102
|
+
# Backend's @ratelimit decorator raises PermissionDenied, which DRF
|
|
103
|
+
# translates to a generic 403 "You do not have permission to perform
|
|
104
|
+
# this action." That's opaque — surface as the rate-limit it actually is.
|
|
105
|
+
if resp.status_code == 403 and "permission" in str(detail).lower():
|
|
106
|
+
print_error(
|
|
107
|
+
err_console,
|
|
108
|
+
"Daily Agent Mode signup limit reached for this network (5/day). Try again from a different IP or after midnight UTC.",
|
|
109
|
+
)
|
|
110
|
+
raise typer.Exit(1)
|
|
111
|
+
print_error(err_console, f"Bootstrap failed: {detail}")
|
|
112
|
+
raise typer.Exit(1)
|
|
113
|
+
|
|
114
|
+
envelope = resp.json()
|
|
115
|
+
_validate_envelope(envelope)
|
|
116
|
+
config.platform.api_key = envelope["api_key"]
|
|
117
|
+
config.platform.base_url = base_url
|
|
118
|
+
config.platform.agent_mode = True
|
|
119
|
+
config.platform.created_via = "agent_mode"
|
|
120
|
+
config.platform.agent_caller = agent_caller or ""
|
|
121
|
+
config.platform.claimed_at = ""
|
|
122
|
+
config.platform.default_user_id = envelope["default_user_id"]
|
|
123
|
+
# Adopt the slug-derived user_id as the default scope for memory ops.
|
|
124
|
+
config.defaults.user_id = envelope["default_user_id"]
|
|
125
|
+
save_config(config)
|
|
126
|
+
|
|
127
|
+
print_success(console, f"Agent Mode active. Default user_id: {envelope['default_user_id']}")
|
|
128
|
+
notice = envelope.get("mem0_notice")
|
|
129
|
+
if notice:
|
|
130
|
+
console.print(f"\n[yellow]🔔 {notice}[/yellow]\n")
|
|
131
|
+
else:
|
|
132
|
+
# Fallback if the backend hasn't deployed the unified notice yet.
|
|
133
|
+
claim_cmd = envelope.get("claim_command", "mem0 init --email <your-email>")
|
|
134
|
+
console.print(f" [{DIM_COLOR}]To claim this account later: {claim_cmd}[/]")
|
|
135
|
+
|
|
136
|
+
if not agent_caller:
|
|
137
|
+
console.print(
|
|
138
|
+
f" [{DIM_COLOR}]If you are an AI agent, identify yourself: "
|
|
139
|
+
f"`mem0 identify <your-name>` (e.g. claude-code, cursor).[/]"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def claim_via_otp(config: Mem0Config, *, email: str, code: str | None = None) -> None:
|
|
144
|
+
"""Claim an existing Agent Mode account via OTP — no browser, no polling.
|
|
145
|
+
|
|
146
|
+
Reuses the standard email-code flow (`/api/v1/auth/email_code/` then
|
|
147
|
+
`/.../verify/`) and adds the local agent-mode API key in the verify body
|
|
148
|
+
as `agent_mode_api_key`. Backend's `verify_email_code` runs the
|
|
149
|
+
upgrade-in-place transaction inline and returns claim result.
|
|
150
|
+
|
|
151
|
+
On success: flips `platform.agent_mode=false`, sets `claimed_at`, stamps
|
|
152
|
+
`user_email`. The api_key value itself never changes.
|
|
153
|
+
"""
|
|
154
|
+
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
|
|
155
|
+
if not config.platform.api_key or not config.platform.agent_mode:
|
|
156
|
+
print_error(
|
|
157
|
+
err_console,
|
|
158
|
+
"This command requires an active Agent Mode config. Run `mem0 init` first.",
|
|
159
|
+
)
|
|
160
|
+
raise typer.Exit(1)
|
|
161
|
+
|
|
162
|
+
raw_key = config.platform.api_key
|
|
163
|
+
|
|
164
|
+
with httpx.Client(timeout=30.0) as client:
|
|
165
|
+
# Step 1: request OTP (unless --code provided)
|
|
166
|
+
if not code:
|
|
167
|
+
send = client.post(
|
|
168
|
+
f"{base_url}/api/v1/auth/email_code/",
|
|
169
|
+
headers={**_SOURCE_HEADERS, "Content-Type": "application/json"},
|
|
170
|
+
json={"email": email},
|
|
171
|
+
)
|
|
172
|
+
if send.status_code == 429:
|
|
173
|
+
print_error(err_console, "Too many attempts. Try again in a few minutes.")
|
|
174
|
+
raise typer.Exit(1)
|
|
175
|
+
if send.status_code != 200:
|
|
176
|
+
try:
|
|
177
|
+
detail = send.json().get("error", send.text)
|
|
178
|
+
except Exception:
|
|
179
|
+
detail = send.text
|
|
180
|
+
print_error(err_console, f"Failed to send code: {detail}")
|
|
181
|
+
raise typer.Exit(1)
|
|
182
|
+
|
|
183
|
+
print_success(console, f"Verification code sent to {email}. Check your inbox.")
|
|
184
|
+
|
|
185
|
+
if not sys.stdin.isatty():
|
|
186
|
+
print_error(
|
|
187
|
+
err_console,
|
|
188
|
+
"No --code provided and terminal is non-interactive.",
|
|
189
|
+
hint=f"Re-run: mem0 init --email {email} --code <code>",
|
|
190
|
+
)
|
|
191
|
+
raise typer.Exit(1)
|
|
192
|
+
|
|
193
|
+
console.print()
|
|
194
|
+
code = Prompt.ask(f" [{BRAND_COLOR}]Verification Code[/]")
|
|
195
|
+
if not code:
|
|
196
|
+
print_error(err_console, "Code is required.")
|
|
197
|
+
raise typer.Exit(1)
|
|
198
|
+
|
|
199
|
+
# Step 2: verify + claim in one shot
|
|
200
|
+
verify = client.post(
|
|
201
|
+
f"{base_url}/api/v1/auth/email_code/verify/",
|
|
202
|
+
headers={**_SOURCE_HEADERS, "Content-Type": "application/json"},
|
|
203
|
+
json={
|
|
204
|
+
"email": email,
|
|
205
|
+
"code": code.strip(),
|
|
206
|
+
"agent_mode_api_key": raw_key,
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if verify.status_code != 200:
|
|
211
|
+
try:
|
|
212
|
+
err_body = verify.json()
|
|
213
|
+
detail = err_body.get("error", verify.text)
|
|
214
|
+
code_str = err_body.get("code", "")
|
|
215
|
+
except (json.JSONDecodeError, ValueError, AttributeError):
|
|
216
|
+
detail = verify.text
|
|
217
|
+
code_str = ""
|
|
218
|
+
print_error(err_console, f"Claim failed: {detail}")
|
|
219
|
+
if code_str == "email_already_claimed":
|
|
220
|
+
console.print(
|
|
221
|
+
f" [{DIM_COLOR}]Tip: this email already has a Mem0 account. Sign in there and run `mem0 link <key>` to attach this agent.[/]"
|
|
222
|
+
)
|
|
223
|
+
raise typer.Exit(1)
|
|
224
|
+
|
|
225
|
+
claim_body = verify.json()
|
|
226
|
+
if not claim_body.get("claimed"):
|
|
227
|
+
print_error(err_console, f"Unexpected verify response: {claim_body}")
|
|
228
|
+
raise typer.Exit(1)
|
|
229
|
+
|
|
230
|
+
config.platform.agent_mode = False
|
|
231
|
+
config.platform.claimed_at = claim_body.get("claimed_at") or _utcnow_iso()
|
|
232
|
+
config.platform.user_email = email
|
|
233
|
+
config.platform.created_via = "email"
|
|
234
|
+
save_config(config)
|
|
235
|
+
print_success(console, f"Agent claimed to {email}. Your API key is unchanged.")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _utcnow_iso() -> str:
|
|
239
|
+
return datetime.now(timezone.utc).isoformat()
|