mem0-cli 0.2.1__tar.gz → 0.2.2__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 (23) hide show
  1. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/PKG-INFO +1 -1
  2. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/pyproject.toml +1 -1
  3. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/__init__.py +1 -1
  4. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/app.py +78 -4
  5. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/backend/platform.py +22 -1
  6. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/commands/init_cmd.py +16 -0
  7. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/config.py +4 -0
  8. mem0_cli-0.2.2/src/mem0_cli/telemetry.py +105 -0
  9. mem0_cli-0.2.2/src/mem0_cli/telemetry_sender.py +86 -0
  10. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/.gitignore +0 -0
  11. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/README.md +0 -0
  12. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/__main__.py +0 -0
  13. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/backend/__init__.py +0 -0
  14. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/backend/base.py +0 -0
  15. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/branding.py +0 -0
  16. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/commands/__init__.py +0 -0
  17. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/commands/config_cmd.py +0 -0
  18. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/commands/entities.py +0 -0
  19. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/commands/events_cmd.py +0 -0
  20. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/commands/memory.py +0 -0
  21. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/commands/utils.py +0 -0
  22. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/output.py +0 -0
  23. {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mem0-cli
3
- Version: 0.2.1
3
+ Version: 0.2.2
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.1"
7
+ version = "0.2.2"
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"
@@ -1,3 +1,3 @@
1
1
  """mem0 CLI — the command-line interface for the mem0 memory layer."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.2.2"
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import contextlib
5
6
  import json as _json
6
7
  import os
7
8
  import stat as _stat_mod
@@ -12,7 +13,7 @@ import typer
12
13
  from rich.console import Console
13
14
 
14
15
  from mem0_cli import __version__
15
- from mem0_cli.branding import BRAND_COLOR, print_error
16
+ from mem0_cli.branding import BRAND_COLOR, print_error, print_warning
16
17
 
17
18
  console = Console()
18
19
  err_console = Console(stderr=True)
@@ -55,6 +56,44 @@ event_app = typer.Typer(
55
56
  # entity_app and event_app registered after Memory commands to control panel ordering
56
57
 
57
58
 
59
+ # ── Validated user identity (set by _get_backend_and_config) ──────────────
60
+
61
+ _validated_user_email: str | None = None
62
+
63
+ # ── Telemetry helper ─────────────────────────────────────────────────────
64
+
65
+
66
+ def _fire_telemetry(command_name: str, extra: dict | None = None) -> None:
67
+ """Fire a PostHog telemetry event (non-blocking, never fails)."""
68
+ try:
69
+ from mem0_cli.telemetry import capture_event
70
+
71
+ props = {"command": command_name}
72
+ if extra:
73
+ props.update(extra)
74
+ capture_event(f"cli.{command_name}", props, pre_resolved_email=_validated_user_email)
75
+ except Exception:
76
+ pass
77
+
78
+
79
+ @config_app.callback(invoke_without_command=True)
80
+ def _config_callback(ctx: typer.Context) -> None:
81
+ if ctx.invoked_subcommand:
82
+ _fire_telemetry(f"config.{ctx.invoked_subcommand}")
83
+
84
+
85
+ @entity_app.callback(invoke_without_command=True)
86
+ def _entity_callback(ctx: typer.Context) -> None:
87
+ if ctx.invoked_subcommand:
88
+ _fire_telemetry(f"entity.{ctx.invoked_subcommand}")
89
+
90
+
91
+ @event_app.callback(invoke_without_command=True)
92
+ def _event_callback(ctx: typer.Context) -> None:
93
+ if ctx.invoked_subcommand:
94
+ _fire_telemetry(f"event.{ctx.invoked_subcommand}")
95
+
96
+
58
97
  # ── Helpers ───────────────────────────────────────────────────────────────
59
98
 
60
99
 
@@ -62,9 +101,16 @@ def _get_backend_and_config(
62
101
  api_key: str | None = None,
63
102
  base_url: str | None = None,
64
103
  ):
65
- """Build and return the Platform backend plus the loaded config."""
104
+ """Build and return the Platform backend plus the loaded config.
105
+
106
+ Validates the API key upfront via ``/v1/ping/`` and caches the
107
+ resolved user email for telemetry.
108
+ """
109
+ global _validated_user_email
110
+
66
111
  from mem0_cli.backend import get_backend
67
- from mem0_cli.config import load_config
112
+ from mem0_cli.backend.platform import AuthError
113
+ from mem0_cli.config import load_config, save_config
68
114
 
69
115
  config = load_config()
70
116
 
@@ -81,7 +127,29 @@ def _get_backend_and_config(
81
127
  )
82
128
  raise typer.Exit(1)
83
129
 
84
- return get_backend(config), config
130
+ backend = get_backend(config)
131
+
132
+ # Validate the API key upfront with a fast timeout
133
+ try:
134
+ ping_data = backend.ping(timeout=5.0)
135
+ email = ping_data.get("user_email") if isinstance(ping_data, dict) else None
136
+ if email:
137
+ _validated_user_email = email
138
+ if config.platform.user_email != email:
139
+ config.platform.user_email = email
140
+ with contextlib.suppress(Exception):
141
+ save_config(config)
142
+ except AuthError:
143
+ print_error(
144
+ err_console,
145
+ "Invalid or expired API key.",
146
+ hint="Run 'mem0 init' or set MEM0_API_KEY environment variable.",
147
+ )
148
+ raise typer.Exit(1) from None
149
+ except Exception:
150
+ print_warning(err_console, "Could not validate API key (network issue). Proceeding anyway.")
151
+
152
+ return backend, config
85
153
 
86
154
 
87
155
  def _get_backend(
@@ -165,8 +233,11 @@ def main_callback(
165
233
  if version:
166
234
  from mem0_cli.commands.utils import cmd_version
167
235
 
236
+ _fire_telemetry("version")
168
237
  cmd_version()
169
238
  raise typer.Exit()
239
+ if ctx.invoked_subcommand:
240
+ _fire_telemetry(ctx.invoked_subcommand)
170
241
 
171
242
 
172
243
  # ── Memory: add ───────────────────────────────────────────────────────────
@@ -571,12 +642,14 @@ def delete(
571
642
 
572
643
  # ── Dispatch ─────────────────────────────────────────────────────
573
644
  if memory_id is not None:
645
+ _fire_telemetry("delete", {"delete_mode": "single"})
574
646
  from mem0_cli.commands.memory import cmd_delete
575
647
 
576
648
  backend = _get_backend(api_key, base_url)
577
649
  cmd_delete(backend, memory_id, dry_run=dry_run, force=force, output=output)
578
650
 
579
651
  elif all_:
652
+ _fire_telemetry("delete", {"delete_mode": "all"})
580
653
  from mem0_cli.commands.memory import cmd_delete_all
581
654
 
582
655
  backend, config = _get_backend_and_config(api_key, base_url)
@@ -584,6 +657,7 @@ def delete(
584
657
  cmd_delete_all(backend, force=force, dry_run=dry_run, all_=project, **ids, output=output)
585
658
 
586
659
  else: # --entity
660
+ _fire_telemetry("delete", {"delete_mode": "entity"})
587
661
  from mem0_cli.commands.entities import cmd_entities_delete
588
662
 
589
663
  backend = _get_backend(api_key, base_url)
@@ -6,6 +6,7 @@ from typing import Any
6
6
 
7
7
  import httpx
8
8
 
9
+ from mem0_cli import __version__
9
10
  from mem0_cli.backend.base import Backend
10
11
  from mem0_cli.config import PlatformConfig
11
12
 
@@ -21,11 +22,17 @@ class PlatformBackend(Backend):
21
22
  headers={
22
23
  "Authorization": f"Token {config.api_key}",
23
24
  "Content-Type": "application/json",
25
+ "X-Mem0-Source": "cli",
26
+ "X-Mem0-Client-Language": "python",
27
+ "X-Mem0-Client-Version": __version__,
24
28
  },
25
29
  timeout=30.0,
26
30
  )
27
31
 
28
32
  def _request(self, method: str, path: str, **kwargs: Any) -> Any:
33
+ from mem0_cli.state import is_agent_mode
34
+
35
+ self._client.headers["X-Mem0-Caller-Type"] = "agent" if is_agent_mode() else "user"
29
36
  resp = self._client.request(method, path, **kwargs)
30
37
  if resp.status_code == 401:
31
38
  raise AuthError("Authentication failed. Your API key may be invalid or expired.")
@@ -281,6 +288,20 @@ class PlatformBackend(Backend):
281
288
  result = self._request("DELETE", f"/v2/entities/{entity_type}/{entity_id}/")
282
289
  return result
283
290
 
291
+ def ping(self, timeout: float | None = None) -> dict:
292
+ """Call the ping endpoint and return the raw response.
293
+
294
+ When *timeout* is given it overrides the client-level timeout so that
295
+ validation pings can fail fast without blocking the user.
296
+ """
297
+ if timeout is not None:
298
+ resp = self._client.get("/v1/ping/", timeout=timeout)
299
+ if resp.status_code == 401:
300
+ raise AuthError("Authentication failed. Your API key may be invalid or expired.")
301
+ resp.raise_for_status()
302
+ return resp.json()
303
+ return self._request("GET", "/v1/ping/")
304
+
284
305
  def status(
285
306
  self,
286
307
  *,
@@ -289,7 +310,7 @@ class PlatformBackend(Backend):
289
310
  ) -> dict[str, Any]:
290
311
  """Check connectivity using the ping endpoint."""
291
312
  try:
292
- self._request("GET", "/v1/ping/")
313
+ self.ping()
293
314
  return {"connected": True, "backend": "platform", "base_url": self.base_url}
294
315
  except Exception as e:
295
316
  return {"connected": False, "backend": "platform", "error": str(e)}
@@ -108,6 +108,10 @@ def _email_login(
108
108
  The caller expects at minimum an ``api_key`` field.
109
109
  """
110
110
  url = base_url.rstrip("/")
111
+ _source_headers = {
112
+ "X-Mem0-Source": "cli",
113
+ "X-Mem0-Client-Language": "python",
114
+ }
111
115
 
112
116
  with httpx.Client(timeout=30.0) as client:
113
117
  # If code is already provided, skip sending — user already has a code
@@ -116,6 +120,7 @@ def _email_login(
116
120
  resp = client.post(
117
121
  f"{url}/api/v1/auth/email_code/",
118
122
  json={"email": email},
123
+ headers=_source_headers,
119
124
  )
120
125
  if resp.status_code == 429:
121
126
  print_error(err_console, "Too many attempts. Try again in a few minutes.")
@@ -148,6 +153,7 @@ def _email_login(
148
153
  resp = client.post(
149
154
  f"{url}/api/v1/auth/email_code/verify/",
150
155
  json={"email": email, "code": code.strip()},
156
+ headers=_source_headers,
151
157
  )
152
158
  if resp.status_code == 429:
153
159
  print_error(err_console, "Too many attempts. Try again in a few minutes.")
@@ -229,6 +235,7 @@ def run_init(
229
235
  raise typer.Exit(1)
230
236
  config.platform.api_key = api_key_val
231
237
  config.platform.base_url = base_url
238
+ config.platform.user_email = email
232
239
  config.defaults.user_id = (
233
240
  user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
234
241
  )
@@ -299,6 +306,7 @@ def run_init(
299
306
  raise typer.Exit(1)
300
307
  config.platform.api_key = api_key_val
301
308
  config.platform.base_url = base_url
309
+ config.platform.user_email = email_addr
302
310
  config.defaults.user_id = (
303
311
  user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
304
312
  )
@@ -384,6 +392,14 @@ def _validate_platform(config: Mem0Config) -> None:
384
392
  )
385
393
  if status.get("connected"):
386
394
  print_success(console, "Connected to mem0 Platform!")
395
+ # Cache user_email from ping response for telemetry distinct_id
396
+ try:
397
+ ping_data = backend.ping()
398
+ user_email = ping_data.get("user_email") if isinstance(ping_data, dict) else None
399
+ if user_email:
400
+ config.platform.user_email = user_email
401
+ except Exception:
402
+ pass
387
403
  else:
388
404
  print_error(
389
405
  err_console,
@@ -27,6 +27,7 @@ CONFIG_VERSION = 1
27
27
  class PlatformConfig:
28
28
  api_key: str = ""
29
29
  base_url: str = DEFAULT_BASE_URL
30
+ user_email: str = ""
30
31
 
31
32
 
32
33
  @dataclass
@@ -48,6 +49,7 @@ class Mem0Config:
48
49
  SHORT_KEY_ALIASES: dict[str, str] = {
49
50
  "api_key": "platform.api_key",
50
51
  "base_url": "platform.base_url",
52
+ "user_email": "platform.user_email",
51
53
  "user_id": "defaults.user_id",
52
54
  "agent_id": "defaults.agent_id",
53
55
  "app_id": "defaults.app_id",
@@ -76,6 +78,7 @@ def load_config() -> Mem0Config:
76
78
  plat = data.get("platform", {})
77
79
  config.platform.api_key = plat.get("api_key", "")
78
80
  config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL)
81
+ config.platform.user_email = plat.get("user_email", "")
79
82
 
80
83
  defaults = data.get("defaults", {})
81
84
  config.defaults.user_id = defaults.get("user_id", "")
@@ -132,6 +135,7 @@ def save_config(config: Mem0Config) -> None:
132
135
  "platform": {
133
136
  "api_key": config.platform.api_key,
134
137
  "base_url": config.platform.base_url,
138
+ "user_email": config.platform.user_email,
135
139
  },
136
140
  }
137
141
 
@@ -0,0 +1,105 @@
1
+ """CLI telemetry — anonymous usage tracking via PostHog.
2
+
3
+ Sends fire-and-forget events to PostHog by spawning a detached subprocess
4
+ (telemetry_sender.py). The parent CLI process exits immediately; the
5
+ subprocess handles email resolution, caching, and the HTTP POST.
6
+
7
+ Disable with: MEM0_TELEMETRY=false
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ import os
15
+ import platform
16
+ import subprocess
17
+ import sys
18
+ from typing import Any
19
+
20
+ POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"
21
+ POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/"
22
+
23
+
24
+ def _is_telemetry_enabled() -> bool:
25
+ val = os.environ.get("MEM0_TELEMETRY", "true").lower()
26
+ return val not in ("false", "0", "no")
27
+
28
+
29
+ def _get_distinct_id() -> str:
30
+ """Return a stable anonymous identifier for the current user.
31
+
32
+ Priority: cached user_email (from /v1/ping/) > MD5(api_key) > fallback.
33
+ Matches the SDK pattern in mem0/client/main.py.
34
+ """
35
+ try:
36
+ from mem0_cli.config import load_config
37
+
38
+ config = load_config()
39
+ if config.platform.user_email:
40
+ return config.platform.user_email
41
+ if config.platform.api_key:
42
+ return hashlib.md5(config.platform.api_key.encode()).hexdigest()
43
+ except Exception:
44
+ pass
45
+ return "anonymous-cli"
46
+
47
+
48
+ def capture_event(
49
+ event_name: str,
50
+ properties: dict[str, Any] | None = None,
51
+ pre_resolved_email: str | None = None,
52
+ ) -> None:
53
+ """Fire a PostHog event via a detached subprocess (non-blocking).
54
+
55
+ When *pre_resolved_email* is provided (e.g. from an upfront ping
56
+ validation), it is used directly as the PostHog distinct ID and the
57
+ subprocess skips its own ``/v1/ping/`` call.
58
+ """
59
+ if not _is_telemetry_enabled():
60
+ return
61
+
62
+ try:
63
+ from mem0_cli import __version__
64
+ from mem0_cli.config import CONFIG_FILE, load_config
65
+ from mem0_cli.state import is_agent_mode
66
+
67
+ config = load_config()
68
+ distinct_id = pre_resolved_email or _get_distinct_id()
69
+
70
+ payload = {
71
+ "api_key": POSTHOG_API_KEY,
72
+ "distinct_id": distinct_id,
73
+ "event": event_name,
74
+ "properties": {
75
+ "source": "CLI",
76
+ "language": "python",
77
+ "cli_version": __version__,
78
+ "agent_mode": is_agent_mode(),
79
+ "python_version": sys.version,
80
+ "os": sys.platform,
81
+ "os_version": platform.version(),
82
+ "$process_person_profile": False,
83
+ "$lib": "posthog-python",
84
+ **(properties or {}),
85
+ },
86
+ }
87
+
88
+ context = {
89
+ "payload": payload,
90
+ "posthog_host": POSTHOG_HOST,
91
+ "needs_email": not distinct_id or "@" not in distinct_id,
92
+ "mem0_api_key": config.platform.api_key or "",
93
+ "mem0_base_url": config.platform.base_url or "https://api.mem0.ai",
94
+ "config_path": str(CONFIG_FILE),
95
+ }
96
+
97
+ subprocess.Popen(
98
+ [sys.executable, "-m", "mem0_cli.telemetry_sender", json.dumps(context)],
99
+ stdout=subprocess.DEVNULL,
100
+ stderr=subprocess.DEVNULL,
101
+ start_new_session=True,
102
+ close_fds=True,
103
+ )
104
+ except Exception:
105
+ pass
@@ -0,0 +1,86 @@
1
+ """Standalone telemetry sender — runs as a detached subprocess.
2
+
3
+ Usage: python -m mem0_cli.telemetry_sender '<json context>'
4
+
5
+ This module is spawned by telemetry.capture_event() and runs independently
6
+ of the parent CLI process. It:
7
+
8
+ 1. Resolves the user's email via /v1/ping/ if not already cached
9
+ 2. Caches the email in ~/.mem0/config.json for future runs
10
+ 3. Sends the PostHog event
11
+
12
+ All errors are silently swallowed — this process must never produce output
13
+ or affect the user experience.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import sys
20
+ import urllib.request
21
+
22
+
23
+ def main() -> None:
24
+ ctx = json.loads(sys.argv[1])
25
+ payload = ctx["payload"]
26
+
27
+ if ctx.get("needs_email") and ctx.get("mem0_api_key"):
28
+ _resolve_and_cache_email(ctx, payload)
29
+
30
+ _send_posthog_event(ctx["posthog_host"], payload)
31
+
32
+
33
+ def _resolve_and_cache_email(ctx: dict, payload: dict) -> None:
34
+ """Call /v1/ping/ to get the user's email, update the payload, and cache it."""
35
+ try:
36
+ ping_url = ctx["mem0_base_url"].rstrip("/") + "/v1/ping/"
37
+ req = urllib.request.Request(
38
+ ping_url,
39
+ headers={
40
+ "Authorization": "Token " + ctx["mem0_api_key"],
41
+ "Content-Type": "application/json",
42
+ },
43
+ )
44
+ resp = urllib.request.urlopen(req, timeout=10)
45
+ data = json.loads(resp.read())
46
+ email = data.get("user_email")
47
+ if email:
48
+ payload["distinct_id"] = email
49
+ _cache_email(ctx.get("config_path"), email)
50
+ except Exception:
51
+ pass
52
+
53
+
54
+ def _cache_email(config_path: str | None, email: str) -> None:
55
+ """Write user_email into the config file for future runs."""
56
+ if not config_path:
57
+ return
58
+ try:
59
+ with open(config_path) as f:
60
+ cfg = json.load(f)
61
+ cfg.setdefault("platform", {})["user_email"] = email
62
+ with open(config_path, "w") as f:
63
+ json.dump(cfg, f, indent=2)
64
+ except Exception:
65
+ pass
66
+
67
+
68
+ def _send_posthog_event(posthog_host: str, payload: dict) -> None:
69
+ """POST the event to PostHog."""
70
+ try:
71
+ body = json.dumps(payload).encode()
72
+ req = urllib.request.Request(
73
+ posthog_host,
74
+ data=body,
75
+ headers={"Content-Type": "application/json"},
76
+ )
77
+ urllib.request.urlopen(req, timeout=10)
78
+ except Exception:
79
+ pass
80
+
81
+
82
+ if __name__ == "__main__":
83
+ import contextlib
84
+
85
+ with contextlib.suppress(Exception):
86
+ main()
File without changes
File without changes
File without changes