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.
Files changed (30) hide show
  1. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/.gitignore +6 -2
  2. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/PKG-INFO +1 -1
  3. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/pyproject.toml +1 -1
  4. mem0_cli-0.2.7/src/mem0_cli/agent_detect.py +36 -0
  5. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/app.py +130 -5
  6. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/backend/platform.py +21 -2
  7. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/branding.py +5 -3
  8. mem0_cli-0.2.7/src/mem0_cli/commands/agent_mode_cmd.py +239 -0
  9. mem0_cli-0.2.7/src/mem0_cli/commands/agent_rush_cmd.py +132 -0
  10. mem0_cli-0.2.7/src/mem0_cli/commands/identify_cmd.py +75 -0
  11. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/commands/init_cmd.py +160 -4
  12. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/commands/utils.py +1 -1
  13. mem0_cli-0.2.7/src/mem0_cli/commands/whoami_cmd.py +25 -0
  14. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/config.py +45 -0
  15. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/output.py +19 -0
  16. mem0_cli-0.2.7/src/mem0_cli/plugin_sync.py +119 -0
  17. mem0_cli-0.2.7/src/mem0_cli/state.py +45 -0
  18. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/telemetry.py +4 -2
  19. mem0_cli-0.2.4/src/mem0_cli/state.py +0 -24
  20. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/README.md +0 -0
  21. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/__init__.py +0 -0
  22. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/__main__.py +0 -0
  23. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/backend/__init__.py +0 -0
  24. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/backend/base.py +0 -0
  25. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/commands/__init__.py +0 -0
  26. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/commands/config_cmd.py +0 -0
  27. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/commands/entities.py +0 -0
  28. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/commands/events_cmd.py +0 -0
  29. {mem0_cli-0.2.4 → mem0_cli-0.2.7}/src/mem0_cli/commands/memory.py +0 -0
  30. {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/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mem0-cli
3
- Version: 0.2.4
3
+ Version: 0.2.7
4
4
  Summary: The official CLI for mem0 — the memory layer for AI agents
5
5
  Author-email: "mem0.ai" <founders@mem0.ai>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mem0-cli"
7
- version = "0.2.4"
7
+ version = "0.2.7"
8
8
  description = "The official CLI for mem0 — the memory layer for AI agents"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -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(api_key=api_key, user_id=user_id, email=email, code=code, force=force)
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
- _json_flags = {"--json", "--agent"}
1202
- if any(a in _json_flags for a in sys.argv[1:]):
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 sys.argv[1:] if a not in _json_flags]
1318
+ sys.argv = [sys.argv[0]] + [a for a in argv_rest if a not in _global_flags]
1207
1319
 
1208
- app()
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
- return resp.json()
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}")