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.
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/PKG-INFO +1 -1
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/pyproject.toml +1 -1
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/__init__.py +1 -1
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/app.py +78 -4
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/backend/platform.py +22 -1
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/commands/init_cmd.py +16 -0
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/config.py +4 -0
- mem0_cli-0.2.2/src/mem0_cli/telemetry.py +105 -0
- mem0_cli-0.2.2/src/mem0_cli/telemetry_sender.py +86 -0
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/.gitignore +0 -0
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/README.md +0 -0
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/__main__.py +0 -0
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/backend/__init__.py +0 -0
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/backend/base.py +0 -0
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/branding.py +0 -0
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/commands/__init__.py +0 -0
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/commands/config_cmd.py +0 -0
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/commands/entities.py +0 -0
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/commands/events_cmd.py +0 -0
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/commands/memory.py +0 -0
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/commands/utils.py +0 -0
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/output.py +0 -0
- {mem0_cli-0.2.1 → mem0_cli-0.2.2}/src/mem0_cli/state.py +0 -0
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|