mem0-cli 0.2.2__tar.gz → 0.2.3__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.2 → mem0_cli-0.2.3}/PKG-INFO +1 -1
  2. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/pyproject.toml +1 -1
  3. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/src/mem0_cli/__init__.py +1 -1
  4. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/src/mem0_cli/backend/platform.py +10 -4
  5. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/src/mem0_cli/config.py +12 -0
  6. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/src/mem0_cli/telemetry.py +45 -4
  7. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/src/mem0_cli/telemetry_sender.py +22 -0
  8. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/.gitignore +0 -0
  9. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/README.md +0 -0
  10. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/src/mem0_cli/__main__.py +0 -0
  11. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/src/mem0_cli/app.py +0 -0
  12. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/src/mem0_cli/backend/__init__.py +0 -0
  13. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/src/mem0_cli/backend/base.py +0 -0
  14. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/src/mem0_cli/branding.py +0 -0
  15. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/src/mem0_cli/commands/__init__.py +0 -0
  16. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/src/mem0_cli/commands/config_cmd.py +0 -0
  17. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/src/mem0_cli/commands/entities.py +0 -0
  18. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/src/mem0_cli/commands/events_cmd.py +0 -0
  19. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/src/mem0_cli/commands/init_cmd.py +0 -0
  20. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/src/mem0_cli/commands/memory.py +0 -0
  21. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/src/mem0_cli/commands/utils.py +0 -0
  22. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/src/mem0_cli/output.py +0 -0
  23. {mem0_cli-0.2.2 → mem0_cli-0.2.3}/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.2
3
+ Version: 0.2.3
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.2"
7
+ version = "0.2.3"
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.2"
3
+ __version__ = "0.2.3"
@@ -93,6 +93,7 @@ class PlatformBackend(Backend):
93
93
  payload["categories"] = categories
94
94
  if enable_graph:
95
95
  payload["enable_graph"] = True
96
+ payload["source"] = "CLI"
96
97
 
97
98
  return self._request("POST", "/v1/memories/", json=payload)
98
99
 
@@ -172,6 +173,7 @@ class PlatformBackend(Backend):
172
173
  payload["fields"] = fields
173
174
  if enable_graph:
174
175
  payload["enable_graph"] = True
176
+ payload["source"] = "CLI"
175
177
 
176
178
  result = self._request("POST", "/v2/memories/search/", json=payload)
177
179
  return (
@@ -181,7 +183,7 @@ class PlatformBackend(Backend):
181
183
  )
182
184
 
183
185
  def get(self, memory_id: str) -> dict:
184
- return self._request("GET", f"/v1/memories/{memory_id}/")
186
+ return self._request("GET", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
185
187
 
186
188
  def list_memories(
187
189
  self,
@@ -220,6 +222,7 @@ class PlatformBackend(Backend):
220
222
  payload["filters"] = api_filters
221
223
  if enable_graph:
222
224
  payload["enable_graph"] = True
225
+ payload["source"] = "CLI"
223
226
 
224
227
  result = self._request("POST", "/v2/memories/", json=payload, params=params)
225
228
  return (
@@ -236,6 +239,7 @@ class PlatformBackend(Backend):
236
239
  payload["text"] = content
237
240
  if metadata:
238
241
  payload["metadata"] = metadata
242
+ payload["source"] = "CLI"
239
243
  return self._request("PUT", f"/v1/memories/{memory_id}/", json=payload)
240
244
 
241
245
  def delete(
@@ -249,7 +253,7 @@ class PlatformBackend(Backend):
249
253
  run_id: str | None = None,
250
254
  ) -> dict:
251
255
  if all:
252
- params: dict[str, str] = {}
256
+ params: dict[str, str] = {"source": "CLI"}
253
257
  if user_id:
254
258
  params["user_id"] = user_id
255
259
  if agent_id:
@@ -260,7 +264,7 @@ class PlatformBackend(Backend):
260
264
  params["run_id"] = run_id
261
265
  return self._request("DELETE", "/v1/memories/", params=params)
262
266
  elif memory_id:
263
- return self._request("DELETE", f"/v1/memories/{memory_id}/")
267
+ return self._request("DELETE", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
264
268
  else:
265
269
  raise ValueError("Either memory_id or --all is required")
266
270
 
@@ -285,7 +289,9 @@ class PlatformBackend(Backend):
285
289
  # Delete each provided entity via the v2 path-based endpoint
286
290
  result: dict = {}
287
291
  for entity_type, entity_id in entities.items():
288
- result = self._request("DELETE", f"/v2/entities/{entity_type}/{entity_id}/")
292
+ result = self._request(
293
+ "DELETE", f"/v2/entities/{entity_type}/{entity_id}/", params={"source": "CLI"}
294
+ )
289
295
  return result
290
296
 
291
297
  def ping(self, timeout: float | None = None) -> dict:
@@ -39,11 +39,17 @@ class DefaultsConfig:
39
39
  enable_graph: bool = False
40
40
 
41
41
 
42
+ @dataclass
43
+ class TelemetryConfig:
44
+ anonymous_id: str = ""
45
+
46
+
42
47
  @dataclass
43
48
  class Mem0Config:
44
49
  version: int = CONFIG_VERSION
45
50
  defaults: DefaultsConfig = field(default_factory=DefaultsConfig)
46
51
  platform: PlatformConfig = field(default_factory=PlatformConfig)
52
+ telemetry: TelemetryConfig = field(default_factory=TelemetryConfig)
47
53
 
48
54
 
49
55
  SHORT_KEY_ALIASES: dict[str, str] = {
@@ -87,6 +93,9 @@ def load_config() -> Mem0Config:
87
93
  config.defaults.run_id = defaults.get("run_id", "")
88
94
  config.defaults.enable_graph = defaults.get("enable_graph", False)
89
95
 
96
+ telemetry = data.get("telemetry", {})
97
+ config.telemetry.anonymous_id = telemetry.get("anonymous_id", "")
98
+
90
99
  # Environment variable overrides
91
100
  env_key = os.environ.get("MEM0_API_KEY")
92
101
  if env_key:
@@ -137,6 +146,9 @@ def save_config(config: Mem0Config) -> None:
137
146
  "base_url": config.platform.base_url,
138
147
  "user_email": config.platform.user_email,
139
148
  },
149
+ "telemetry": {
150
+ "anonymous_id": config.telemetry.anonymous_id,
151
+ },
140
152
  }
141
153
 
142
154
  with open(CONFIG_FILE, "w") as f:
@@ -9,12 +9,14 @@ Disable with: MEM0_TELEMETRY=false
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import contextlib
12
13
  import hashlib
13
14
  import json
14
15
  import os
15
16
  import platform
16
17
  import subprocess
17
18
  import sys
19
+ import uuid
18
20
  from typing import Any
19
21
 
20
22
  POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"
@@ -26,11 +28,31 @@ def _is_telemetry_enabled() -> bool:
26
28
  return val not in ("false", "0", "no")
27
29
 
28
30
 
31
+ def _get_or_create_anonymous_id() -> str:
32
+ """Return a persistent per-machine anonymous ID, generating one if needed.
33
+
34
+ Stored in ~/.mem0/config.json under `telemetry.anonymous_id` so that
35
+ repeat runs on the same machine share one PostHog identity instead of
36
+ collapsing into a single shared fallback string.
37
+ """
38
+ from mem0_cli.config import load_config, save_config
39
+
40
+ config = load_config()
41
+ if config.telemetry.anonymous_id:
42
+ return config.telemetry.anonymous_id
43
+
44
+ new_id = f"cli-anon-{uuid.uuid4().hex}"
45
+ config.telemetry.anonymous_id = new_id
46
+ with contextlib.suppress(Exception):
47
+ save_config(config)
48
+ return new_id
49
+
50
+
29
51
  def _get_distinct_id() -> str:
30
52
  """Return a stable anonymous identifier for the current user.
31
53
 
32
- Priority: cached user_email (from /v1/ping/) > MD5(api_key) > fallback.
33
- Matches the SDK pattern in mem0/client/main.py.
54
+ Priority: cached user_email (from /v1/ping/) > MD5(api_key) >
55
+ persistent per-machine anonymous ID.
34
56
  """
35
57
  try:
36
58
  from mem0_cli.config import load_config
@@ -42,7 +64,10 @@ def _get_distinct_id() -> str:
42
64
  return hashlib.md5(config.platform.api_key.encode()).hexdigest()
43
65
  except Exception:
44
66
  pass
45
- return "anonymous-cli"
67
+ try:
68
+ return _get_or_create_anonymous_id()
69
+ except Exception:
70
+ return f"cli-anon-{uuid.uuid4().hex}"
46
71
 
47
72
 
48
73
  def capture_event(
@@ -61,12 +86,27 @@ def capture_event(
61
86
 
62
87
  try:
63
88
  from mem0_cli import __version__
64
- from mem0_cli.config import CONFIG_FILE, load_config
89
+ from mem0_cli.config import CONFIG_FILE, load_config, save_config
65
90
  from mem0_cli.state import is_agent_mode
66
91
 
67
92
  config = load_config()
68
93
  distinct_id = pre_resolved_email or _get_distinct_id()
69
94
 
95
+ # Detect anonymous → identified transition. If a stored anonymous_id
96
+ # exists and we just resolved to a real identity, fire a one-shot
97
+ # $identify event so PostHog stitches the pre-signup history onto
98
+ # the authenticated profile. Clear the stored id so we don't re-alias.
99
+ anon_id_to_alias: str | None = None
100
+ if (
101
+ distinct_id
102
+ and not distinct_id.startswith("cli-anon-")
103
+ and config.telemetry.anonymous_id
104
+ ):
105
+ anon_id_to_alias = config.telemetry.anonymous_id
106
+ config.telemetry.anonymous_id = ""
107
+ with contextlib.suppress(Exception):
108
+ save_config(config)
109
+
70
110
  payload = {
71
111
  "api_key": POSTHOG_API_KEY,
72
112
  "distinct_id": distinct_id,
@@ -92,6 +132,7 @@ def capture_event(
92
132
  "mem0_api_key": config.platform.api_key or "",
93
133
  "mem0_base_url": config.platform.base_url or "https://api.mem0.ai",
94
134
  "config_path": str(CONFIG_FILE),
135
+ "anon_distinct_id_to_alias": anon_id_to_alias,
95
136
  }
96
137
 
97
138
  subprocess.Popen(
@@ -27,9 +27,31 @@ def main() -> None:
27
27
  if ctx.get("needs_email") and ctx.get("mem0_api_key"):
28
28
  _resolve_and_cache_email(ctx, payload)
29
29
 
30
+ # Fire $identify *after* email resolution so PostHog links the stored
31
+ # anonymous id directly to the final identity (email, not the api-key
32
+ # hash). The regular event is sent next so it lands under the merged
33
+ # profile.
34
+ anon_id = ctx.get("anon_distinct_id_to_alias")
35
+ if anon_id:
36
+ _send_identify_event(ctx, payload, anon_id)
37
+
30
38
  _send_posthog_event(ctx["posthog_host"], payload)
31
39
 
32
40
 
41
+ def _send_identify_event(ctx: dict, payload: dict, anon_id: str) -> None:
42
+ """Send a PostHog $identify event aliasing anon_id → payload['distinct_id']."""
43
+ identify_payload = {
44
+ "api_key": payload["api_key"],
45
+ "event": "$identify",
46
+ "distinct_id": payload["distinct_id"],
47
+ "properties": {
48
+ "$anon_distinct_id": anon_id,
49
+ "$lib": payload.get("properties", {}).get("$lib", "posthog-python"),
50
+ },
51
+ }
52
+ _send_posthog_event(ctx["posthog_host"], identify_payload)
53
+
54
+
33
55
  def _resolve_and_cache_email(ctx: dict, payload: dict) -> None:
34
56
  """Call /v1/ping/ to get the user's email, update the payload, and cache it."""
35
57
  try:
File without changes
File without changes
File without changes
File without changes