mem0-cli 0.2.4__tar.gz → 0.2.7__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.4 → mem0_cli-0.2.7}/.gitignore +6 -2
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/PKG-INFO +1 -1
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/pyproject.toml +1 -1
- mem0_cli-0.2.7/src/mem0_cli/agent_detect.py +36 -0
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/app.py +130 -5
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/backend/platform.py +21 -2
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/branding.py +5 -3
- mem0_cli-0.2.7/src/mem0_cli/commands/agent_mode_cmd.py +239 -0
- mem0_cli-0.2.7/src/mem0_cli/commands/agent_rush_cmd.py +132 -0
- mem0_cli-0.2.7/src/mem0_cli/commands/identify_cmd.py +75 -0
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/commands/init_cmd.py +160 -4
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/commands/utils.py +1 -1
- mem0_cli-0.2.7/src/mem0_cli/commands/whoami_cmd.py +25 -0
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/config.py +45 -0
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/output.py +19 -0
- mem0_cli-0.2.7/src/mem0_cli/plugin_sync.py +119 -0
- mem0_cli-0.2.7/src/mem0_cli/state.py +45 -0
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/telemetry.py +4 -2
- mem0_cli-0.2.4/src/mem0_cli/state.py +0 -24
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/README.md +0 -0
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/__init__.py +0 -0
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/__main__.py +0 -0
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/backend/__init__.py +0 -0
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/backend/base.py +0 -0
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/commands/__init__.py +0 -0
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/commands/config_cmd.py +0 -0
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/commands/entities.py +0 -0
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/commands/events_cmd.py +0 -0
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/commands/memory.py +0 -0
- {mem0_cli-0.2.4 → mem0_cli-0.2.7}/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
|
|
|
@@ -851,6 +859,19 @@ def init(
|
|
|
851
859
|
force: bool = typer.Option(
|
|
852
860
|
False, "--force", help="Overwrite existing config without confirmation."
|
|
853
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
|
+
),
|
|
854
875
|
) -> None:
|
|
855
876
|
"""Interactive setup wizard for mem0 CLI.
|
|
856
877
|
|
|
@@ -859,10 +880,97 @@ def init(
|
|
|
859
880
|
mem0 init --api-key m0-xxx --user-id alice
|
|
860
881
|
mem0 init --email alice@company.com
|
|
861
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
|
|
862
885
|
"""
|
|
863
886
|
from mem0_cli.commands.init_cmd import run_init
|
|
864
887
|
|
|
865
|
-
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)
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
@app.command(name="whoami", rich_help_panel="Setup")
|
|
918
|
+
def whoami_cmd() -> None:
|
|
919
|
+
"""Print your AGENTRUSH identifier (default_user_id).
|
|
920
|
+
|
|
921
|
+
Example:
|
|
922
|
+
mem0 whoami
|
|
923
|
+
"""
|
|
924
|
+
from mem0_cli.commands.whoami_cmd import run_whoami
|
|
925
|
+
|
|
926
|
+
run_whoami()
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
# ── AGENTRUSH sub-app ─────────────────────────────────────────────────────
|
|
930
|
+
|
|
931
|
+
agent_rush_app = typer.Typer(
|
|
932
|
+
name="agent-rush",
|
|
933
|
+
help="AGENTRUSH game commands",
|
|
934
|
+
no_args_is_help=True,
|
|
935
|
+
rich_markup_mode="rich",
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
@agent_rush_app.callback(invoke_without_command=True)
|
|
940
|
+
def _agent_rush_callback(ctx: typer.Context) -> None:
|
|
941
|
+
if ctx.invoked_subcommand:
|
|
942
|
+
_fire_telemetry(f"agent-rush.{ctx.invoked_subcommand}")
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
@agent_rush_app.command(name="add")
|
|
946
|
+
def agent_rush_add(
|
|
947
|
+
content: str = typer.Argument(..., help="Memory content (50-1000 characters, no URLs)."),
|
|
948
|
+
) -> None:
|
|
949
|
+
"""Submit a memory to AGENTRUSH.
|
|
950
|
+
|
|
951
|
+
Example:
|
|
952
|
+
mem0 agent-rush add "I enjoy solving constraint-satisfaction problems."
|
|
953
|
+
"""
|
|
954
|
+
from mem0_cli.commands.agent_rush_cmd import run_agent_rush_add
|
|
955
|
+
|
|
956
|
+
run_agent_rush_add(content)
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
@agent_rush_app.command(name="search")
|
|
960
|
+
def agent_rush_search(
|
|
961
|
+
query: str = typer.Argument(..., help="Search query."),
|
|
962
|
+
) -> None:
|
|
963
|
+
"""Search AGENTRUSH memories.
|
|
964
|
+
|
|
965
|
+
Example:
|
|
966
|
+
mem0 agent-rush search "constraint satisfaction"
|
|
967
|
+
"""
|
|
968
|
+
from mem0_cli.commands.agent_rush_cmd import run_agent_rush_search
|
|
969
|
+
|
|
970
|
+
run_agent_rush_search(query)
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
app.add_typer(agent_rush_app, name="agent-rush", rich_help_panel="Setup")
|
|
866
974
|
|
|
867
975
|
|
|
868
976
|
# (entity_app registered at module level, below sub-group definitions)
|
|
@@ -1198,11 +1306,28 @@ def main() -> None:
|
|
|
1198
1306
|
import sys
|
|
1199
1307
|
|
|
1200
1308
|
# Allow --json/--agent anywhere in the command line (not just before subcommand).
|
|
1201
|
-
|
|
1202
|
-
|
|
1309
|
+
# Special case: `mem0 init --agent` is a subcommand flag (Agent Mode bootstrap)
|
|
1310
|
+
# consumed by init_cmd, not a global JSON-output toggle — leave it in argv.
|
|
1311
|
+
argv_rest = sys.argv[1:]
|
|
1312
|
+
is_init = "init" in argv_rest
|
|
1313
|
+
_global_flags = {"--json"} if is_init else {"--json", "--agent"}
|
|
1314
|
+
if any(a in _global_flags for a in argv_rest):
|
|
1203
1315
|
from mem0_cli.state import set_agent_mode
|
|
1204
1316
|
|
|
1205
1317
|
set_agent_mode(True)
|
|
1206
|
-
sys.argv = [sys.argv[0]] + [a for a in
|
|
1318
|
+
sys.argv = [sys.argv[0]] + [a for a in argv_rest if a not in _global_flags]
|
|
1207
1319
|
|
|
1208
|
-
|
|
1320
|
+
try:
|
|
1321
|
+
app()
|
|
1322
|
+
finally:
|
|
1323
|
+
# Surface any unclaimed Agent Mode notice once per command, after the
|
|
1324
|
+
# primary output. In JSON/agent mode the notice is folded into the
|
|
1325
|
+
# envelope by format_json_envelope, so skip the stderr banner there
|
|
1326
|
+
# to avoid duplicate output.
|
|
1327
|
+
from mem0_cli.state import is_agent_mode, take_notice
|
|
1328
|
+
|
|
1329
|
+
notice = take_notice()
|
|
1330
|
+
if notice and not is_agent_mode():
|
|
1331
|
+
from rich.console import Console
|
|
1332
|
+
|
|
1333
|
+
Console(stderr=True).print(f"\n[yellow]🔔 {notice}[/yellow]\n")
|
|
@@ -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,
|
|
@@ -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 at app.mem0.ai with your existing credentials.[/]"
|
|
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()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""mem0 agent-rush — AGENTRUSH game commands.
|
|
2
|
+
|
|
3
|
+
Wraps the platform's /v1/agent-rush/{memories/, memories/search/} endpoints.
|
|
4
|
+
Hardcoded routing; no flags needed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
import typer
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from mem0_cli.branding import print_error, print_success
|
|
17
|
+
from mem0_cli.config import load_config, save_config
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
err_console = Console(stderr=True)
|
|
21
|
+
|
|
22
|
+
_PII_WARNING_LINES = (
|
|
23
|
+
"",
|
|
24
|
+
"[yellow]⚠️ AGENTRUSH memories are PUBLIC — visible to any other player.[/yellow]",
|
|
25
|
+
"[yellow] Do not include real names, emails, secrets, work content, or PII.[/yellow]",
|
|
26
|
+
"",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_SOURCE_HEADERS = {
|
|
30
|
+
"X-Mem0-Source": "cli",
|
|
31
|
+
"X-Mem0-Client-Language": "python",
|
|
32
|
+
"X-Mem0-Mode": "agent-rush",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_ERROR_HINTS = {
|
|
36
|
+
"agentrush_search_first": "Run 3 'mem0 agent-rush search' commands before adding.",
|
|
37
|
+
"agentrush_search_quota": "You've used your 3 lifetime searches.",
|
|
38
|
+
"agentrush_add_quota": "You've used your 3 lifetime adds.",
|
|
39
|
+
"agentrush_not_agent_mode": "Re-run 'mem0 init --agent' to bootstrap an agent-mode key.",
|
|
40
|
+
"agentrush_length": "Memory text must be 50-1000 characters.",
|
|
41
|
+
"agentrush_no_urls": "URLs are not allowed.",
|
|
42
|
+
"agentrush_blocklist": "Content contains a blocked term.",
|
|
43
|
+
"agentrush_global_quota": "Event-wide cap reached. Try again later.",
|
|
44
|
+
"agentrush_not_provisioned": "AGENTRUSH is not provisioned in this environment.",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _call(path: str, body: dict) -> dict:
|
|
49
|
+
config = load_config()
|
|
50
|
+
if not config.platform.api_key:
|
|
51
|
+
print_error(err_console, "Not initialized. Run `mem0 init --agent` first.")
|
|
52
|
+
raise typer.Exit(1)
|
|
53
|
+
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
|
|
54
|
+
try:
|
|
55
|
+
with httpx.Client(timeout=30.0) as client:
|
|
56
|
+
resp = client.post(
|
|
57
|
+
f"{base_url}{path}",
|
|
58
|
+
headers={
|
|
59
|
+
**_SOURCE_HEADERS,
|
|
60
|
+
"Authorization": f"Token {config.platform.api_key}",
|
|
61
|
+
"Content-Type": "application/json",
|
|
62
|
+
},
|
|
63
|
+
json=body,
|
|
64
|
+
)
|
|
65
|
+
except httpx.HTTPError as exc:
|
|
66
|
+
print_error(err_console, f"Network error: {exc}")
|
|
67
|
+
raise typer.Exit(1) from exc
|
|
68
|
+
try:
|
|
69
|
+
data = resp.json()
|
|
70
|
+
except Exception:
|
|
71
|
+
data = {}
|
|
72
|
+
if resp.status_code >= 400:
|
|
73
|
+
code = (
|
|
74
|
+
(data.get("error") or {}).get("code", "unknown")
|
|
75
|
+
if isinstance(data, dict)
|
|
76
|
+
else "unknown"
|
|
77
|
+
)
|
|
78
|
+
print_error(err_console, f"AGENTRUSH error: {code}")
|
|
79
|
+
hint = _ERROR_HINTS.get(code)
|
|
80
|
+
if hint:
|
|
81
|
+
console.print(f" [dim]{hint}[/dim]")
|
|
82
|
+
raise typer.Exit(1)
|
|
83
|
+
return data
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _ensure_warning_acknowledged() -> None:
|
|
87
|
+
"""Block the first interactive add on the PII warning; pass-through for agents.
|
|
88
|
+
|
|
89
|
+
Interactive (TTY): show prompt, require explicit 'y', persist
|
|
90
|
+
`agent_rush.acknowledged_at` so we never ask the same machine twice.
|
|
91
|
+
|
|
92
|
+
Non-interactive (no TTY — typical when an agent runs the CLI): surface
|
|
93
|
+
the warning to stderr for the human reading the agent transcript and
|
|
94
|
+
proceed without prompting (agents can't answer y/N).
|
|
95
|
+
"""
|
|
96
|
+
config = load_config()
|
|
97
|
+
if config.agent_rush.acknowledged_at:
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
is_tty = sys.stdin.isatty() and sys.stdout.isatty()
|
|
101
|
+
if not is_tty:
|
|
102
|
+
for line in _PII_WARNING_LINES:
|
|
103
|
+
err_console.print(line)
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
for line in _PII_WARNING_LINES:
|
|
107
|
+
console.print(line)
|
|
108
|
+
answer = typer.prompt(" Continue? [y/N]", default="N", show_default=False).strip().lower()
|
|
109
|
+
if answer not in ("y", "yes"):
|
|
110
|
+
print_error(err_console, "Aborted.")
|
|
111
|
+
raise typer.Exit(1)
|
|
112
|
+
|
|
113
|
+
config.agent_rush.acknowledged_at = datetime.now(timezone.utc).isoformat()
|
|
114
|
+
save_config(config)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def run_agent_rush_add(content: str) -> None:
|
|
118
|
+
_ensure_warning_acknowledged()
|
|
119
|
+
result = _call("/v1/agent-rush/memories/", {"content": content})
|
|
120
|
+
event_id = result.get("event_id", "?")
|
|
121
|
+
print_success(console, f"Memory submitted (event_id: {event_id})")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def run_agent_rush_search(query: str) -> None:
|
|
125
|
+
result = _call("/v1/agent-rush/memories/search/", {"query": query})
|
|
126
|
+
memories = result.get("results") or result.get("memories") or []
|
|
127
|
+
if not memories:
|
|
128
|
+
console.print("[dim](no results)[/dim]")
|
|
129
|
+
return
|
|
130
|
+
for i, m in enumerate(memories[:5], start=1):
|
|
131
|
+
text = m.get("memory") if isinstance(m, dict) else str(m)
|
|
132
|
+
console.print(f" {i}. {text}")
|