mem0-cli 0.2.4__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.
Files changed (28) hide show
  1. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/.gitignore +6 -2
  2. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/PKG-INFO +1 -1
  3. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/pyproject.toml +1 -1
  4. mem0_cli-0.2.5/src/mem0_cli/agent_detect.py +36 -0
  5. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/src/mem0_cli/app.py +71 -5
  6. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/src/mem0_cli/backend/platform.py +21 -2
  7. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/src/mem0_cli/branding.py +5 -3
  8. mem0_cli-0.2.5/src/mem0_cli/commands/agent_mode_cmd.py +239 -0
  9. mem0_cli-0.2.5/src/mem0_cli/commands/identify_cmd.py +75 -0
  10. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/src/mem0_cli/commands/init_cmd.py +160 -4
  11. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/src/mem0_cli/commands/utils.py +1 -1
  12. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/src/mem0_cli/config.py +31 -0
  13. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/src/mem0_cli/output.py +19 -0
  14. mem0_cli-0.2.5/src/mem0_cli/plugin_sync.py +119 -0
  15. mem0_cli-0.2.5/src/mem0_cli/state.py +45 -0
  16. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/src/mem0_cli/telemetry.py +4 -2
  17. mem0_cli-0.2.4/src/mem0_cli/state.py +0 -24
  18. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/README.md +0 -0
  19. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/src/mem0_cli/__init__.py +0 -0
  20. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/src/mem0_cli/__main__.py +0 -0
  21. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/src/mem0_cli/backend/__init__.py +0 -0
  22. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/src/mem0_cli/backend/base.py +0 -0
  23. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/src/mem0_cli/commands/__init__.py +0 -0
  24. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/src/mem0_cli/commands/config_cmd.py +0 -0
  25. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/src/mem0_cli/commands/entities.py +0 -0
  26. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/src/mem0_cli/commands/events_cmd.py +0 -0
  27. {mem0_cli-0.2.4 → mem0_cli-0.2.5}/src/mem0_cli/commands/memory.py +0 -0
  28. {mem0_cli-0.2.4 → 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/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mem0-cli
3
- Version: 0.2.4
3
+ Version: 0.2.5
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.5"
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,38 @@ 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)
866
915
 
867
916
 
868
917
  # (entity_app registered at module level, below sub-group definitions)
@@ -1198,11 +1247,28 @@ def main() -> None:
1198
1247
  import sys
1199
1248
 
1200
1249
  # 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:]):
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):
1203
1256
  from mem0_cli.state import set_agent_mode
1204
1257
 
1205
1258
  set_agent_mode(True)
1206
- sys.argv = [sys.argv[0]] + [a for a in sys.argv[1:] if a not in _json_flags]
1259
+ sys.argv = [sys.argv[0]] + [a for a in argv_rest if a not in _global_flags]
1207
1260
 
1208
- app()
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")
@@ -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 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()
@@ -0,0 +1,75 @@
1
+ """mem0 identify — declare which agent owns the current agent-mode key.
2
+
3
+ Used when `mem0 init --agent` ran without --agent-caller, so the backend
4
+ saved agent_caller=NULL. The agent re-runs `mem0 identify <name>` to PATCH
5
+ its own row with its real identity. Idempotent — running it again just
6
+ overwrites.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import httpx
12
+ import typer
13
+ from rich.console import Console
14
+
15
+ from mem0_cli.branding import print_error, print_success
16
+ from mem0_cli.config import load_config, save_config
17
+
18
+ console = Console()
19
+ err_console = Console(stderr=True)
20
+
21
+ _SOURCE_HEADERS = {
22
+ "X-Mem0-Source": "cli",
23
+ "X-Mem0-Client-Language": "python",
24
+ }
25
+
26
+
27
+ def run_identify(name: str) -> None:
28
+ """PATCH the active agent-mode key's agent_caller field."""
29
+ config = load_config()
30
+ if not config.platform.api_key:
31
+ print_error(
32
+ err_console,
33
+ "No API key configured. Run `mem0 init --agent` first.",
34
+ )
35
+ raise typer.Exit(1)
36
+ if not config.platform.agent_mode:
37
+ print_error(
38
+ err_console,
39
+ "This command only works on unclaimed agent-mode keys.",
40
+ )
41
+ raise typer.Exit(1)
42
+
43
+ name = (name or "").strip()
44
+ if not name:
45
+ print_error(err_console, "Agent name is required.")
46
+ raise typer.Exit(1)
47
+
48
+ base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
49
+ try:
50
+ with httpx.Client(timeout=30.0) as client:
51
+ resp = client.patch(
52
+ f"{base_url}/api/v1/auth/agent_mode/caller/",
53
+ headers={
54
+ **_SOURCE_HEADERS,
55
+ "Authorization": f"Token {config.platform.api_key}",
56
+ "Content-Type": "application/json",
57
+ },
58
+ json={"agent_caller": name},
59
+ )
60
+ except httpx.HTTPError as exc:
61
+ print_error(err_console, f"Network error: {exc}")
62
+ raise typer.Exit(1) from exc
63
+
64
+ if resp.status_code != 200:
65
+ try:
66
+ detail = resp.json().get("error", resp.text)
67
+ except Exception:
68
+ detail = resp.text
69
+ print_error(err_console, f"Identify failed: {detail}")
70
+ raise typer.Exit(1)
71
+
72
+ canonical = resp.json().get("agent_caller", name)
73
+ config.platform.agent_caller = canonical
74
+ save_config(config)
75
+ print_success(console, f"Identified as {canonical}.")
@@ -19,7 +19,13 @@ from mem0_cli.branding import (
19
19
  print_info,
20
20
  print_success,
21
21
  )
22
- from mem0_cli.config import CONFIG_FILE, DEFAULT_BASE_URL, Mem0Config, load_config, save_config
22
+ from mem0_cli.config import (
23
+ CONFIG_FILE,
24
+ DEFAULT_BASE_URL,
25
+ Mem0Config,
26
+ load_config,
27
+ save_config,
28
+ )
23
29
 
24
30
  console = Console()
25
31
  err_console = Console(stderr=True)
@@ -97,6 +103,25 @@ def _validate_email(email: str) -> None:
97
103
  raise typer.Exit(1)
98
104
 
99
105
 
106
+ def _ping_key(api_key: str, base_url: str, timeout: float = 5.0) -> bool:
107
+ """Validate api_key against /v1/ping/.
108
+
109
+ Returns False ONLY on a definitive "invalid key" signal (HTTP 401 / 403).
110
+ Network errors, timeouts, and 5xx responses return True so we prefer
111
+ reusing an existing key over silently minting a new shadow on a transient
112
+ blip (which would also clobber config + plugin-sync targets).
113
+ """
114
+ try:
115
+ resp = httpx.get(
116
+ f"{base_url.rstrip('/')}/v1/ping/",
117
+ headers={"Authorization": f"Token {api_key}"},
118
+ timeout=timeout,
119
+ )
120
+ except httpx.HTTPError:
121
+ return True # unknown — prefer reuse
122
+ return resp.status_code not in (401, 403)
123
+
124
+
100
125
  def _email_login(
101
126
  email: str,
102
127
  code: str | None,
@@ -176,21 +201,143 @@ def run_init(
176
201
  email: str | None = None,
177
202
  code: str | None = None,
178
203
  force: bool = False,
204
+ source: str | None = None,
205
+ agent: bool = False,
206
+ agent_caller: str | None = None,
179
207
  ) -> None:
180
208
  """Interactive setup wizard for mem0 CLI.
181
209
 
182
210
  When both *api_key* and *user_id* are supplied, all prompts are skipped
183
211
  (non-interactive mode). When running in a non-TTY without the required
184
212
  flags, an error message is printed.
213
+
214
+ Agent Mode dispatch (no email/api-key flags):
215
+ - If existing config has an active API key → reuse (existing_key path).
216
+ - Else if any positive agent signal (--agent, --json global, agent env
217
+ var, or `agent` flag) → POST /api/v1/auth/agent_mode/ and write config.
218
+ - Else fall through to the interactive wizard.
219
+
220
+ Claim dispatch:
221
+ - If `--email` is set AND existing config has `agent_mode=true`, run the
222
+ claim device-flow against the existing key instead of minting a new
223
+ email-based key.
185
224
  """
225
+ from mem0_cli.agent_detect import detect_agent_caller
226
+ from mem0_cli.commands.agent_mode_cmd import bootstrap_via_backend, claim_via_otp
227
+ from mem0_cli.state import is_agent_mode as _global_agent_mode
228
+ from mem0_cli.telemetry import capture_event
229
+
230
+ def _fire_init(mode: str, *, claimed: bool = False) -> None:
231
+ """Fire cli.init telemetry with M1-M6 properties."""
232
+ props: dict = {"command": "init", "mode": mode}
233
+ if agent_caller:
234
+ # Self-declared via --agent-caller; not sniffed from env vars.
235
+ props["agent_caller"] = agent_caller
236
+ if source:
237
+ props["signup_source"] = source
238
+ if claimed:
239
+ props["claimed_agent_mode"] = True
240
+ capture_event("cli.init", props)
241
+
186
242
  config = Mem0Config()
187
243
 
188
244
  base_url = os.environ.get("MEM0_BASE_URL", config.platform.base_url or DEFAULT_BASE_URL)
245
+ config.platform.base_url = base_url
189
246
 
190
247
  if code and not email:
191
248
  print_error(err_console, "--code requires --email.")
192
249
  raise typer.Exit(1)
193
250
 
251
+ # ── Email + existing agent-mode config → claim flow ─────────────────
252
+ if email and CONFIG_FILE.exists():
253
+ existing = load_config()
254
+ if existing.platform.agent_mode and existing.platform.api_key:
255
+ email = email.strip().lower()
256
+ _validate_email(email)
257
+ print_info(console, f"Claiming Agent Mode account to {email}...")
258
+ claim_via_otp(existing, email=email, code=code)
259
+ _fire_init("email", claimed=True)
260
+ return
261
+
262
+ # ── Agent Mode path runs BEFORE the existing-config guard ──────────
263
+ # Rules 1/2 REUSE a valid existing key (not overwrite), so we must
264
+ # short-circuit before the guard prompts. Rule 3 mints only when there
265
+ # is no valid key to reuse — in that case overwriting is correct.
266
+ _agent_ctx = agent or _global_agent_mode() or (detect_agent_caller() is not None)
267
+ if not api_key and not email and _agent_ctx:
268
+ from mem0_cli.output import format_json_envelope
269
+ from mem0_cli.state import is_agent_mode as _is_json_mode
270
+
271
+ def _emit_reuse(source: str) -> None:
272
+ if _is_json_mode():
273
+ format_json_envelope(
274
+ console,
275
+ command="init",
276
+ data={
277
+ "api_key_saved": False,
278
+ "api_key_source": source,
279
+ "agent_mode": False,
280
+ "message": "Existing Mem0 API key found and reused. No Agent Mode key was created.",
281
+ },
282
+ )
283
+ else:
284
+ msg = (
285
+ "Existing MEM0_API_KEY is valid; reusing it. No new Agent Mode key was minted."
286
+ if source == "env"
287
+ else "Existing API key in config is valid; reusing it. No new Agent Mode key was minted."
288
+ )
289
+ print_success(console, msg)
290
+
291
+ def _maybe_identify(key: str) -> None:
292
+ """Best-effort PATCH agent_caller when --agent-caller is supplied on a
293
+ reused key. Silent no-op on any failure — reuse must not break.
294
+ """
295
+ if not agent_caller:
296
+ return
297
+ try:
298
+ resp = httpx.patch(
299
+ f"{base_url.rstrip('/')}/api/v1/auth/agent_mode/caller/",
300
+ headers={
301
+ "Authorization": f"Token {key}",
302
+ "Content-Type": "application/json",
303
+ },
304
+ json={"agent_caller": agent_caller},
305
+ timeout=10.0,
306
+ )
307
+ # Also reflect in local config so introspection matches backend.
308
+ if resp.status_code == 200 and CONFIG_FILE.exists():
309
+ try:
310
+ cfg = load_config()
311
+ cfg.platform.agent_caller = resp.json().get("agent_caller", agent_caller)
312
+ save_config(cfg)
313
+ except Exception:
314
+ pass
315
+ except httpx.HTTPError:
316
+ pass
317
+
318
+ # Rule 1: env MEM0_API_KEY valid → reuse, no new key.
319
+ _env_key = (os.environ.get("MEM0_API_KEY") or "").strip()
320
+ if _env_key and _ping_key(_env_key, base_url):
321
+ _maybe_identify(_env_key)
322
+ _emit_reuse("env")
323
+ _fire_init("existing_key")
324
+ return
325
+ # Rule 2: existing config api_key valid → reuse.
326
+ if CONFIG_FILE.exists():
327
+ _existing = load_config()
328
+ if _existing.platform.api_key and _ping_key(_existing.platform.api_key, base_url):
329
+ _maybe_identify(_existing.platform.api_key)
330
+ _emit_reuse("config")
331
+ _fire_init("existing_key")
332
+ return
333
+ # Rule 3: mint a fresh shadow (no valid key to reuse).
334
+ # agent_caller is the agent's self-declared identity from --agent-caller
335
+ # (Proof Editor-style). Env-var auto-detect is still used above to
336
+ # decide we're in an agent context, but never to fill identity.
337
+ bootstrap_via_backend(config, source=source, agent_caller=agent_caller)
338
+ _fire_init("agent")
339
+ return
340
+
194
341
  # Warn if an existing config with an API key would be overwritten
195
342
  if not force and CONFIG_FILE.exists():
196
343
  existing = load_config()
@@ -236,6 +383,7 @@ def run_init(
236
383
  config.platform.api_key = api_key_val
237
384
  config.platform.base_url = base_url
238
385
  config.platform.user_email = email
386
+ config.platform.created_via = "email"
239
387
  config.defaults.user_id = (
240
388
  user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
241
389
  )
@@ -252,6 +400,8 @@ def run_init(
252
400
  return
253
401
 
254
402
  # ── API key flow (existing) ───────────────────────────────────────
403
+ # (Agent Mode branch runs earlier — see above, before the existing-config
404
+ # guard, so Rules 1/2 can REUSE a valid key without prompting overwrite.)
255
405
 
256
406
  # Non-TTY: resolve defaults so partial flags work in pipelines / CI
257
407
  if not sys.stdin.isatty():
@@ -259,7 +409,7 @@ def run_init(
259
409
  print_error(
260
410
  err_console,
261
411
  "Non-interactive terminal detected and --api-key is required.",
262
- hint="Run: mem0 init --api-key <key> [--user-id <id>]",
412
+ hint="Run: mem0 init --api-key <key>, --email <addr>, or --agent for unattended Agent Mode bootstrap.",
263
413
  )
264
414
  raise typer.Exit(1)
265
415
  user_id = user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
@@ -267,6 +417,7 @@ def run_init(
267
417
  # Fully non-interactive when both flags provided
268
418
  if api_key and user_id:
269
419
  config.platform.api_key = api_key
420
+ config.platform.created_via = "api_key"
270
421
  config.defaults.user_id = user_id
271
422
  _validate_platform(config)
272
423
  save_config(config)
@@ -307,6 +458,7 @@ def run_init(
307
458
  config.platform.api_key = api_key_val
308
459
  config.platform.base_url = base_url
309
460
  config.platform.user_email = email_addr
461
+ config.platform.created_via = "email"
310
462
  config.defaults.user_id = (
311
463
  user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
312
464
  )
@@ -325,6 +477,7 @@ def run_init(
325
477
  # API key flow
326
478
  if api_key:
327
479
  config.platform.api_key = api_key
480
+ config.platform.created_via = "api_key"
328
481
  else:
329
482
  _setup_platform(config)
330
483
 
@@ -352,7 +505,9 @@ def run_init(
352
505
  def _setup_platform(config: Mem0Config) -> None:
353
506
  """Platform setup flow."""
354
507
  console.print()
355
- console.print(f" [{DIM_COLOR}]Get your API key at https://app.mem0.ai/dashboard/api-keys[/]")
508
+ console.print(
509
+ f" [{DIM_COLOR}]Get your API key at https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-python[/]"
510
+ )
356
511
  console.print()
357
512
 
358
513
  console.print(f" [{BRAND_COLOR}]API Key[/]: ", end="")
@@ -362,6 +517,7 @@ def _setup_platform(config: Mem0Config) -> None:
362
517
  raise typer.Exit(1)
363
518
 
364
519
  config.platform.api_key = api_key
520
+ config.platform.created_via = "api_key"
365
521
 
366
522
 
367
523
  def _setup_defaults(config: Mem0Config) -> None:
@@ -404,7 +560,7 @@ def _validate_platform(config: Mem0Config) -> None:
404
560
  print_error(
405
561
  err_console,
406
562
  f"Could not connect: {status.get('error', 'Unknown error')}",
407
- hint="Visit https://app.mem0.ai/dashboard/api-keys to get a new key, then run mem0 init again.",
563
+ hint="Visit https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-python to get a new key, then run mem0 init again.",
408
564
  )
409
565
  except Exception as e:
410
566
  print_error(err_console, f"Connection test failed: {e}")
@@ -77,7 +77,7 @@ def cmd_status(
77
77
  f" [{DIM_COLOR}]Run [bold]mem0 init[/bold] to reconfigure your API key[/]"
78
78
  )
79
79
  lines.append(
80
- f" [{DIM_COLOR}]Get a key at [bold]https://app.mem0.ai/dashboard/api-keys[/bold][/]"
80
+ f" [{DIM_COLOR}]Get a key at [bold]https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-python[/bold][/]"
81
81
  )
82
82
  lines.append(f" [{DIM_COLOR}]Latency:[/] {_elapsed:.2f}s")
83
83
 
@@ -28,6 +28,14 @@ class PlatformConfig:
28
28
  api_key: str = ""
29
29
  base_url: str = DEFAULT_BASE_URL
30
30
  user_email: str = ""
31
+ # Agent Mode (unclaimed-shadow signup)
32
+ agent_mode: bool = False # True while the key is an unclaimed agent-mode key
33
+ created_via: str = "" # "agent_mode" | "email" | "api_key" | "existing_key"
34
+ agent_caller: str = (
35
+ "" # canonical agent name when created_via == "agent_mode" (e.g. "claude-code")
36
+ )
37
+ claimed_at: str = "" # ISO timestamp once the agent has been claimed by a human
38
+ default_user_id: str = "" # `user_<slug>` returned by bootstrap; used as auto-default
31
39
 
32
40
 
33
41
  @dataclass
@@ -83,6 +91,11 @@ def load_config() -> Mem0Config:
83
91
  config.platform.api_key = plat.get("api_key", "")
84
92
  config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL)
85
93
  config.platform.user_email = plat.get("user_email", "")
94
+ config.platform.agent_mode = bool(plat.get("agent_mode", False))
95
+ config.platform.created_via = plat.get("created_via", "")
96
+ config.platform.agent_caller = plat.get("agent_caller", "")
97
+ config.platform.claimed_at = plat.get("claimed_at", "")
98
+ config.platform.default_user_id = plat.get("default_user_id", "")
86
99
 
87
100
  defaults = data.get("defaults", {})
88
101
  config.defaults.user_id = defaults.get("user_id", "")
@@ -136,6 +149,11 @@ def save_config(config: Mem0Config) -> None:
136
149
  "api_key": config.platform.api_key,
137
150
  "base_url": config.platform.base_url,
138
151
  "user_email": config.platform.user_email,
152
+ "agent_mode": config.platform.agent_mode,
153
+ "created_via": config.platform.created_via,
154
+ "agent_caller": config.platform.agent_caller,
155
+ "claimed_at": config.platform.claimed_at,
156
+ "default_user_id": config.platform.default_user_id,
139
157
  },
140
158
  "telemetry": {
141
159
  "anonymous_id": config.telemetry.anonymous_id,
@@ -147,6 +165,19 @@ def save_config(config: Mem0Config) -> None:
147
165
 
148
166
  os.chmod(CONFIG_FILE, stat.S_IRUSR | stat.S_IWUSR) # 0600
149
167
 
168
+ # Propagate the active api_key to ecosystem touchpoints (Claude Code
169
+ # plugin env injection, shell rc exports). Idempotent — only updates
170
+ # EXISTING entries; never creates new ones. Best-effort: any IOError
171
+ # in the sync is swallowed so config.json is always the authoritative
172
+ # write, never blocked by plugin-state issues.
173
+ if config.platform.api_key:
174
+ try:
175
+ from mem0_cli.plugin_sync import sync_api_key
176
+
177
+ sync_api_key(config.platform.api_key)
178
+ except Exception:
179
+ pass
180
+
150
181
 
151
182
  def redact_key(key: str) -> str:
152
183
  """Redact an API key for display: m0-xxx...xxx"""
@@ -229,6 +229,16 @@ def format_json_envelope(
229
229
  if error:
230
230
  envelope["error"] = error
231
231
  envelope["data"] = data
232
+
233
+ # If the platform flagged this as an unclaimed Agent Mode account, surface
234
+ # the notice inside the JSON envelope so an agent consuming the output
235
+ # sees it without needing to inspect HTTP headers.
236
+ from mem0_cli.state import take_notice
237
+
238
+ notice = take_notice()
239
+ if notice:
240
+ envelope["mem0_notice"] = notice
241
+
232
242
  console.print_json(json.dumps(envelope, default=str))
233
243
 
234
244
 
@@ -323,6 +333,15 @@ def format_agent_envelope(
323
333
  if count is not None:
324
334
  envelope["count"] = count
325
335
  envelope["data"] = sanitize_agent_data(command, data)
336
+
337
+ # Surface the unclaimed-Agent-Mode notice (if any) in the envelope so an
338
+ # agent reading the JSON output sees it without inspecting HTTP headers.
339
+ from mem0_cli.state import take_notice
340
+
341
+ notice = take_notice()
342
+ if notice:
343
+ envelope["mem0_notice"] = notice
344
+
326
345
  console.print_json(json.dumps(envelope, default=str))
327
346
 
328
347
 
@@ -0,0 +1,119 @@
1
+ """Sync the active Mem0 API key into other ecosystem touchpoints.
2
+
3
+ Why this exists:
4
+ The CLI canonical state lives in ``~/.mem0/config.json``. But MCP servers
5
+ (Claude Code plugin, Codex plugin, etc.) read ``MEM0_API_KEY`` from env
6
+ vars or their own config files. Without a sync, an agent-mode bootstrap
7
+ mints a new key into config.json but the plugin's MCP keeps using the
8
+ old key from env — silent surprise.
9
+
10
+ Design:
11
+ - Update ONLY entries that already exist (never create new ones)
12
+ - Preserve all surrounding content / formatting / other keys
13
+ - Atomic writes (tmpfile + rename) so a crash mid-write doesn't corrupt
14
+ - Idempotent — re-running with the same key is a no-op
15
+ - Skip on dry_run
16
+
17
+ Targets currently handled:
18
+ - ``~/.claude/settings.json::env::MEM0_API_KEY`` (Claude Code env injection)
19
+ - ``~/.zshrc`` / ``~/.bashrc`` ``export MEM0_API_KEY="..."`` lines
20
+
21
+ Out of scope (deliberately not touched):
22
+ - Codex / Cursor MCP configs — would require schema-aware edits and
23
+ those tools don't have mem0 entries by default
24
+ - Plugin's own ``<plugin-dir>/.api_key`` file — plugin-managed
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import contextlib
30
+ import json
31
+ import os
32
+ import re
33
+ import tempfile
34
+ from pathlib import Path
35
+
36
+ # Files we know how to update safely.
37
+ _CLAUDE_SETTINGS = Path.home() / ".claude" / "settings.json"
38
+ _SHELL_RCS = [Path.home() / ".zshrc", Path.home() / ".bashrc", Path.home() / ".bash_profile"]
39
+
40
+
41
+ def sync_api_key(api_key: str) -> list[str]:
42
+ """Propagate ``api_key`` into known ecosystem touchpoints.
43
+
44
+ Returns the list of paths actually updated. Empty list means nothing
45
+ needed updating (either targets didn't exist or already had this value).
46
+ """
47
+ if not api_key:
48
+ return []
49
+ updated: list[str] = []
50
+ if _update_claude_settings(_CLAUDE_SETTINGS, api_key):
51
+ updated.append(str(_CLAUDE_SETTINGS))
52
+ for rc in _SHELL_RCS:
53
+ if _update_shell_rc(rc, api_key):
54
+ updated.append(str(rc))
55
+ return updated
56
+
57
+
58
+ def _update_claude_settings(path: Path, api_key: str) -> bool:
59
+ """Update ``env.MEM0_API_KEY`` in path. Returns True if file was changed."""
60
+ if not path.is_file():
61
+ return False
62
+ try:
63
+ with path.open("r", encoding="utf-8") as f:
64
+ data = json.load(f)
65
+ except (json.JSONDecodeError, OSError):
66
+ return False
67
+ env = data.get("env")
68
+ if not isinstance(env, dict) or "MEM0_API_KEY" not in env:
69
+ # No existing entry — don't create one.
70
+ return False
71
+ if env["MEM0_API_KEY"] == api_key:
72
+ return False # already in sync
73
+ env["MEM0_API_KEY"] = api_key
74
+ _atomic_write_text(path, json.dumps(data, indent=2, ensure_ascii=False) + "\n")
75
+ return True
76
+
77
+
78
+ # Match `export MEM0_API_KEY="..."` (or single quotes, or no quotes).
79
+ # Use [ \t]* (not \s*) for trailing whitespace so a trailing newline at
80
+ # end-of-file is preserved when MEM0_API_KEY is the last line.
81
+ _RC_LINE = re.compile(
82
+ r'^([ \t]*export[ \t]+MEM0_API_KEY[ \t]*=[ \t]*)(["\']?)([^"\'\n]*)(["\']?)[ \t]*$',
83
+ re.MULTILINE,
84
+ )
85
+
86
+
87
+ def _update_shell_rc(path: Path, api_key: str) -> bool:
88
+ """Update an existing ``export MEM0_API_KEY=...`` line in path."""
89
+ if not path.is_file():
90
+ return False
91
+ try:
92
+ text = path.read_text(encoding="utf-8")
93
+ except OSError:
94
+ return False
95
+ match = _RC_LINE.search(text)
96
+ if not match:
97
+ return False # no existing line
98
+ if match.group(3) == api_key:
99
+ return False
100
+ new_text = _RC_LINE.sub(lambda m: f'{m.group(1)}"{api_key}"', text, count=1)
101
+ _atomic_write_text(path, new_text)
102
+ return True
103
+
104
+
105
+ def _atomic_write_text(path: Path, content: str) -> None:
106
+ """Write content to path atomically (temp + rename)."""
107
+ dirname = path.parent
108
+ fd, tmp_path = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=dirname)
109
+ try:
110
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
111
+ f.write(content)
112
+ # Preserve mode if the original existed.
113
+ if path.exists():
114
+ os.chmod(tmp_path, path.stat().st_mode & 0o777)
115
+ os.replace(tmp_path, path)
116
+ except Exception:
117
+ with contextlib.suppress(OSError):
118
+ os.unlink(tmp_path)
119
+ raise
@@ -0,0 +1,45 @@
1
+ """Agent mode state — set by the root callback, read by commands and branding."""
2
+
3
+ from __future__ import annotations
4
+
5
+ _agent_mode: bool = False
6
+ _current_command: str = ""
7
+ _pending_notice: str = ""
8
+
9
+
10
+ def is_agent_mode() -> bool:
11
+ return _agent_mode
12
+
13
+
14
+ def set_agent_mode(val: bool) -> None:
15
+ global _agent_mode
16
+ _agent_mode = val
17
+
18
+
19
+ def get_current_command() -> str:
20
+ return _current_command
21
+
22
+
23
+ def set_current_command(name: str) -> None:
24
+ global _current_command
25
+ _current_command = name
26
+
27
+
28
+ def capture_notice(notice: str | None) -> None:
29
+ """Stash a Mem0 backend notice for end-of-command surfacing.
30
+
31
+ Called from the platform backend after each response so the notice can
32
+ be printed once per command (regardless of how many sub-requests fired).
33
+ Last-write-wins is fine — the message text is identical across requests.
34
+ """
35
+ global _pending_notice
36
+ if notice:
37
+ _pending_notice = notice
38
+
39
+
40
+ def take_notice() -> str:
41
+ """Return and clear the pending notice."""
42
+ global _pending_notice
43
+ msg = _pending_notice
44
+ _pending_notice = ""
45
+ return msg
@@ -87,7 +87,6 @@ def capture_event(
87
87
  try:
88
88
  from mem0_cli import __version__
89
89
  from mem0_cli.config import CONFIG_FILE, load_config, save_config
90
- from mem0_cli.state import is_agent_mode
91
90
 
92
91
  config = load_config()
93
92
  distinct_id = pre_resolved_email or _get_distinct_id()
@@ -107,6 +106,9 @@ def capture_event(
107
106
  with contextlib.suppress(Exception):
108
107
  save_config(config)
109
108
 
109
+ # M4: every cli.* event carries agent_mode based on the config flag
110
+ # (unclaimed Agent Mode key). This is the growth-doc property used to
111
+ # join init → add → search funnels in PostHog.
110
112
  payload = {
111
113
  "api_key": POSTHOG_API_KEY,
112
114
  "distinct_id": distinct_id,
@@ -115,7 +117,7 @@ def capture_event(
115
117
  "source": "CLI",
116
118
  "language": "python",
117
119
  "cli_version": __version__,
118
- "agent_mode": is_agent_mode(),
120
+ "agent_mode": bool(config.platform.agent_mode),
119
121
  "python_version": sys.version,
120
122
  "os": sys.platform,
121
123
  "os_version": platform.version(),
@@ -1,24 +0,0 @@
1
- """Agent mode state — set by the root callback, read by commands and branding."""
2
-
3
- from __future__ import annotations
4
-
5
- _agent_mode: bool = False
6
- _current_command: str = ""
7
-
8
-
9
- def is_agent_mode() -> bool:
10
- return _agent_mode
11
-
12
-
13
- def set_agent_mode(val: bool) -> None:
14
- global _agent_mode
15
- _agent_mode = val
16
-
17
-
18
- def get_current_command() -> str:
19
- return _current_command
20
-
21
-
22
- def set_current_command(name: str) -> None:
23
- global _current_command
24
- _current_command = name
File without changes