feat(cli): Agent Mode bootstrap + claim flow (Python)
New behavior on `mem0 init`:
- With no `--email`/`--api-key` AND a positive agent signal (`--agent`,
global `--json`/`--agent`, or one of the recognized agent env vars
CLAUDECODE / CURSOR_AGENT / CODEX_CLI / CLINE / CONTINUE / AIDER /
GOOSE / WINDSURF), bootstrap an unattended Agent Mode account via
POST /api/v1/auth/agent_mode/. No email, no OTP, no dashboard.
- With `--email <addr>` AND an existing config that has agent_mode=true,
run the claim device-flow against the existing key instead of minting
a fresh one. The raw API key never leaves the device; backend confirms
claim via the existing CLILoginRequest poll path. Config flips
agent_mode=false and stamps claimed_at on success.
- Bare `mem0 init` with no signal + no TTY still errors out — auto-bootstrap
requires a positive agent signal to avoid surprising pipe-using humans.
New flags:
--agent Force unattended Agent Mode bootstrap.
--source Channel attribution string for signup_source PostHog property.
Config schema extensions on PlatformConfig:
agent_mode, created_via, claimed_at, default_user_id.
Telemetry (M1-M6 from the growth doc):
- cli.init: mode (agent|email|api_key|existing_key), agent_caller,
signup_source, claimed_agent_mode (bool when --email claims an
existing agent-mode config).
- All cli.* events: agent_mode reflects config.platform.agent_mode (the
bootstrap flag), not the output-format flag — per the growth-doc spec.
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
"""Detect which AI agent is invoking the CLI via environment variables.
|
||||||
|
|
||||||
|
Used by `mem0 init` to:
|
||||||
|
1. Decide whether to auto-bootstrap an Agent Mode key (positive agent signal).
|
||||||
|
2. Tag the `agent_caller` PostHog property on the cli.init event.
|
||||||
|
|
||||||
|
Returns a canonical short name or None when no agent is detected. The list
|
||||||
|
is curated, not exhaustive — agents we don't recognise fall through to None,
|
||||||
|
which groups into the "unknown" bucket on dashboards.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
_AGENT_CALLER_ENV: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||||
|
("claude-code", ("CLAUDECODE", "CLAUDE_CODE")),
|
||||||
|
("cursor", ("CURSOR_AGENT", "CURSOR_SESSION_ID")),
|
||||||
|
("codex", ("CODEX_CLI", "OPENAI_CODEX")),
|
||||||
|
("cline", ("CLINE_AGENT", "CLINE")),
|
||||||
|
("continue", ("CONTINUE_AGENT", "CONTINUE_SESSION")),
|
||||||
|
("aider", ("AIDER_SESSION",)),
|
||||||
|
("goose", ("GOOSE_AGENT",)),
|
||||||
|
("windsurf", ("WINDSURF_AGENT",)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def detect_agent_caller() -> str | None:
|
||||||
|
"""Return a canonical agent name if any agent env var is set, else None."""
|
||||||
|
for name, env_vars in _AGENT_CALLER_ENV:
|
||||||
|
if any(os.environ.get(v) for v in env_vars):
|
||||||
|
return name
|
||||||
|
return None
|
||||||
@@ -236,7 +236,8 @@ def main_callback(
|
|||||||
_fire_telemetry("version")
|
_fire_telemetry("version")
|
||||||
cmd_version()
|
cmd_version()
|
||||||
raise typer.Exit()
|
raise typer.Exit()
|
||||||
if ctx.invoked_subcommand:
|
if ctx.invoked_subcommand and ctx.invoked_subcommand != "init":
|
||||||
|
# init fires its own telemetry from init_cmd.run_init with full M1-M6 props.
|
||||||
_fire_telemetry(ctx.invoked_subcommand)
|
_fire_telemetry(ctx.invoked_subcommand)
|
||||||
|
|
||||||
|
|
||||||
@@ -851,6 +852,12 @@ def init(
|
|||||||
force: bool = typer.Option(
|
force: bool = typer.Option(
|
||||||
False, "--force", help="Overwrite existing config without confirmation."
|
False, "--force", help="Overwrite existing config without confirmation."
|
||||||
),
|
),
|
||||||
|
agent_signal: bool = typer.Option(
|
||||||
|
False, "--agent", help="Bootstrap an unattended Agent Mode account (no email required)."
|
||||||
|
),
|
||||||
|
source: str | None = typer.Option(
|
||||||
|
None, "--source", help="Channel attribution for signup (e.g. github, hn, ph).",
|
||||||
|
),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Interactive setup wizard for mem0 CLI.
|
"""Interactive setup wizard for mem0 CLI.
|
||||||
|
|
||||||
@@ -859,10 +866,20 @@ def init(
|
|||||||
mem0 init --api-key m0-xxx --user-id alice
|
mem0 init --api-key m0-xxx --user-id alice
|
||||||
mem0 init --email alice@company.com
|
mem0 init --email alice@company.com
|
||||||
mem0 init --email alice@company.com --code 482901
|
mem0 init --email alice@company.com --code 482901
|
||||||
|
mem0 init --agent # Bootstrap an Agent Mode account (unattended)
|
||||||
|
mem0 init --email alice@company.com # Claims an existing Agent Mode key when one is present
|
||||||
"""
|
"""
|
||||||
from mem0_cli.commands.init_cmd import run_init
|
from mem0_cli.commands.init_cmd import run_init
|
||||||
|
|
||||||
run_init(api_key=api_key, user_id=user_id, email=email, code=code, force=force)
|
run_init(
|
||||||
|
api_key=api_key,
|
||||||
|
user_id=user_id,
|
||||||
|
email=email,
|
||||||
|
code=code,
|
||||||
|
force=force,
|
||||||
|
source=source,
|
||||||
|
agent=agent_signal,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# (entity_app registered at module level, below sub-group definitions)
|
# (entity_app registered at module level, below sub-group definitions)
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
"""Agent Mode commands — bootstrap (unattended signup) and claim (human upgrade)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import secrets
|
||||||
|
import time
|
||||||
|
import webbrowser
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import typer
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
from mem0_cli.branding import (
|
||||||
|
DIM_COLOR,
|
||||||
|
print_error,
|
||||||
|
print_info,
|
||||||
|
print_success,
|
||||||
|
)
|
||||||
|
from mem0_cli.config import Mem0Config, save_config
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
err_console = Console(stderr=True)
|
||||||
|
|
||||||
|
# Claim polling: 2-second interval, 10-minute timeout matches the backend's
|
||||||
|
# CLILoginRequest expires_at (15 minutes — we give up before the token does).
|
||||||
|
_POLL_INTERVAL_SECS = 2
|
||||||
|
_POLL_TIMEOUT_SECS = 600
|
||||||
|
|
||||||
|
_SOURCE_HEADERS = {
|
||||||
|
"X-Mem0-Source": "cli",
|
||||||
|
"X-Mem0-Client-Language": "python",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def bootstrap_via_backend(
|
||||||
|
config: Mem0Config,
|
||||||
|
*,
|
||||||
|
source: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""POST /api/v1/auth/agent_mode/ and mutate config in place.
|
||||||
|
|
||||||
|
Returns nothing — the caller saves the config and prints follow-up messages.
|
||||||
|
Raises typer.Exit(1) on failure.
|
||||||
|
"""
|
||||||
|
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
|
||||||
|
body: dict[str, Any] = {}
|
||||||
|
if source:
|
||||||
|
body["source"] = source
|
||||||
|
|
||||||
|
try:
|
||||||
|
with httpx.Client(timeout=30.0) as client:
|
||||||
|
resp = client.post(
|
||||||
|
f"{base_url}/api/v1/auth/agent_mode/",
|
||||||
|
headers={**_SOURCE_HEADERS, "Content-Type": "application/json"},
|
||||||
|
json=body,
|
||||||
|
)
|
||||||
|
except httpx.HTTPError as exc:
|
||||||
|
print_error(err_console, f"Network error contacting Mem0: {exc}")
|
||||||
|
raise typer.Exit(1) from exc
|
||||||
|
|
||||||
|
if resp.status_code == 429:
|
||||||
|
print_error(err_console, "Rate-limited. Try again in a few minutes.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
if resp.status_code == 503:
|
||||||
|
print_error(err_console, "Agent Mode is temporarily disabled. Try again later.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
try:
|
||||||
|
detail = resp.json().get("error", resp.text)
|
||||||
|
except Exception:
|
||||||
|
detail = resp.text
|
||||||
|
print_error(err_console, f"Bootstrap failed: {detail}")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
envelope = resp.json()
|
||||||
|
config.platform.api_key = envelope["api_key"]
|
||||||
|
config.platform.base_url = base_url
|
||||||
|
config.platform.agent_mode = True
|
||||||
|
config.platform.created_via = "agent_mode"
|
||||||
|
config.platform.claimed_at = ""
|
||||||
|
config.platform.default_user_id = envelope["default_user_id"]
|
||||||
|
# Adopt the slug-derived user_id as the default scope for memory ops.
|
||||||
|
config.defaults.user_id = envelope["default_user_id"]
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
print_success(console, f"Agent Mode active. Default user_id: {envelope['default_user_id']}")
|
||||||
|
console.print(f" [{DIM_COLOR}]To claim this account later: {envelope.get('claim_command', 'mem0 init --email <your-email>')}[/]")
|
||||||
|
|
||||||
|
|
||||||
|
def claim_via_device_flow(config: Mem0Config, *, email: str) -> None:
|
||||||
|
"""Run the claim flow against an existing agent-mode config.
|
||||||
|
|
||||||
|
Reuses the existing CLI device flow (initiate_cli_login → frontend OTP →
|
||||||
|
associate_cli_token → get_api_key_from_cli_token poll). The raw API key
|
||||||
|
never leaves the device — backend confirms claim, CLI updates only
|
||||||
|
`platform.agent_mode` and `platform.claimed_at`.
|
||||||
|
"""
|
||||||
|
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
|
||||||
|
if not config.platform.api_key or not config.platform.agent_mode:
|
||||||
|
print_error(
|
||||||
|
err_console,
|
||||||
|
"This command requires an active Agent Mode config. Run `mem0 init` first.",
|
||||||
|
)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
cli_token = secrets.token_urlsafe(32)
|
||||||
|
raw_key = config.platform.api_key
|
||||||
|
|
||||||
|
with httpx.Client(timeout=30.0) as client:
|
||||||
|
try:
|
||||||
|
init_resp = client.post(
|
||||||
|
f"{base_url}/api/v1/accounts/cli_login/",
|
||||||
|
json={"token": cli_token, "claim_for_apikey": raw_key},
|
||||||
|
headers=_SOURCE_HEADERS,
|
||||||
|
)
|
||||||
|
except httpx.HTTPError as exc:
|
||||||
|
print_error(err_console, f"Could not initiate claim: {exc}")
|
||||||
|
raise typer.Exit(1) from exc
|
||||||
|
|
||||||
|
if init_resp.status_code != 200:
|
||||||
|
try:
|
||||||
|
detail = init_resp.json().get("error", init_resp.text)
|
||||||
|
except Exception:
|
||||||
|
detail = init_resp.text
|
||||||
|
print_error(err_console, f"Could not initiate claim: {detail}")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
login_url = init_resp.json().get("login_url", "")
|
||||||
|
print_info(console, "Open in your browser to claim:")
|
||||||
|
console.print(f" [{DIM_COLOR}]{login_url}[/]")
|
||||||
|
try:
|
||||||
|
webbrowser.open(login_url)
|
||||||
|
except Exception:
|
||||||
|
pass # Printing the URL is sufficient
|
||||||
|
|
||||||
|
# Poll for completion
|
||||||
|
deadline = time.monotonic() + _POLL_TIMEOUT_SECS
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
time.sleep(_POLL_INTERVAL_SECS)
|
||||||
|
try:
|
||||||
|
poll = client.post(
|
||||||
|
f"{base_url}/api/v1/accounts/get_api_key_from_cli_token/",
|
||||||
|
json={"token": cli_token},
|
||||||
|
headers=_SOURCE_HEADERS,
|
||||||
|
)
|
||||||
|
except httpx.HTTPError:
|
||||||
|
continue # transient — keep polling
|
||||||
|
|
||||||
|
if poll.status_code != 200:
|
||||||
|
# 400 "Token expired" / "Invalid token" → bail
|
||||||
|
try:
|
||||||
|
err = poll.json().get("error", "")
|
||||||
|
except Exception:
|
||||||
|
err = poll.text
|
||||||
|
if "expired" in err.lower():
|
||||||
|
print_error(err_console, "Claim link expired. Run `mem0 init --email <addr>` again.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
body = poll.json()
|
||||||
|
if body.get("claimed"):
|
||||||
|
config.platform.agent_mode = False
|
||||||
|
config.platform.claimed_at = body.get("claimed_at") or _utcnow_iso()
|
||||||
|
config.platform.user_email = email
|
||||||
|
config.platform.created_via = "email"
|
||||||
|
save_config(config)
|
||||||
|
print_success(console, f"Agent claimed to {email}. Your API key is unchanged.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print_error(err_console, "Claim timed out. Run `mem0 init --email <addr>` again.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _utcnow_iso() -> str:
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
@@ -182,21 +182,63 @@ def run_init(
|
|||||||
email: str | None = None,
|
email: str | None = None,
|
||||||
code: str | None = None,
|
code: str | None = None,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
|
source: str | None = None,
|
||||||
|
agent: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Interactive setup wizard for mem0 CLI.
|
"""Interactive setup wizard for mem0 CLI.
|
||||||
|
|
||||||
When both *api_key* and *user_id* are supplied, all prompts are skipped
|
When both *api_key* and *user_id* are supplied, all prompts are skipped
|
||||||
(non-interactive mode). When running in a non-TTY without the required
|
(non-interactive mode). When running in a non-TTY without the required
|
||||||
flags, an error message is printed.
|
flags, an error message is printed.
|
||||||
|
|
||||||
|
Agent Mode dispatch (no email/api-key flags):
|
||||||
|
- If existing config has an active API key → reuse (existing_key path).
|
||||||
|
- Else if any positive agent signal (--agent, --json global, agent env
|
||||||
|
var, or `agent` flag) → POST /api/v1/auth/agent_mode/ and write config.
|
||||||
|
- Else fall through to the interactive wizard.
|
||||||
|
|
||||||
|
Claim dispatch:
|
||||||
|
- If `--email` is set AND existing config has `agent_mode=true`, run the
|
||||||
|
claim device-flow against the existing key instead of minting a new
|
||||||
|
email-based key.
|
||||||
"""
|
"""
|
||||||
|
from mem0_cli.agent_detect import detect_agent_caller
|
||||||
|
from mem0_cli.commands.agent_mode_cmd import bootstrap_via_backend, claim_via_device_flow
|
||||||
|
from mem0_cli.state import is_agent_mode as _global_agent_mode
|
||||||
|
from mem0_cli.telemetry import capture_event
|
||||||
|
|
||||||
|
def _fire_init(mode: str, *, claimed: bool = False) -> None:
|
||||||
|
"""Fire cli.init telemetry with M1-M6 properties."""
|
||||||
|
props: dict = {"command": "init", "mode": mode}
|
||||||
|
agent_caller = detect_agent_caller()
|
||||||
|
if agent_caller:
|
||||||
|
props["agent_caller"] = agent_caller
|
||||||
|
if source:
|
||||||
|
props["signup_source"] = source
|
||||||
|
if claimed:
|
||||||
|
props["claimed_agent_mode"] = True
|
||||||
|
capture_event("cli.init", props)
|
||||||
|
|
||||||
config = Mem0Config()
|
config = Mem0Config()
|
||||||
|
|
||||||
base_url = os.environ.get("MEM0_BASE_URL", config.platform.base_url or DEFAULT_BASE_URL)
|
base_url = os.environ.get("MEM0_BASE_URL", config.platform.base_url or DEFAULT_BASE_URL)
|
||||||
|
config.platform.base_url = base_url
|
||||||
|
|
||||||
if code and not email:
|
if code and not email:
|
||||||
print_error(err_console, "--code requires --email.")
|
print_error(err_console, "--code requires --email.")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
# ── Email + existing agent-mode config → claim flow ─────────────────
|
||||||
|
if email and CONFIG_FILE.exists():
|
||||||
|
existing = load_config()
|
||||||
|
if existing.platform.agent_mode and existing.platform.api_key:
|
||||||
|
email = email.strip().lower()
|
||||||
|
_validate_email(email)
|
||||||
|
print_info(console, f"Claiming Agent Mode account to {email}...")
|
||||||
|
claim_via_device_flow(existing, email=email)
|
||||||
|
_fire_init("email", claimed=True)
|
||||||
|
return
|
||||||
|
|
||||||
# Warn if an existing config with an API key would be overwritten
|
# Warn if an existing config with an API key would be overwritten
|
||||||
if not force and CONFIG_FILE.exists():
|
if not force and CONFIG_FILE.exists():
|
||||||
existing = load_config()
|
existing = load_config()
|
||||||
@@ -257,6 +299,16 @@ def run_init(
|
|||||||
console.print()
|
console.print()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# ── Agent Mode auto-bootstrap (no email, no api_key flag) ─────────
|
||||||
|
# Positive agent signal required: explicit --agent flag (local or global)
|
||||||
|
# OR a recognized agent env var. Pure "no TTY" alone is NOT enough — pipe
|
||||||
|
# users would get surprised by a silent shadow signup.
|
||||||
|
agent_ctx = agent or _global_agent_mode() or (detect_agent_caller() is not None)
|
||||||
|
if not api_key and not email and agent_ctx:
|
||||||
|
bootstrap_via_backend(config, source=source)
|
||||||
|
_fire_init("agent")
|
||||||
|
return
|
||||||
|
|
||||||
# ── API key flow (existing) ───────────────────────────────────────
|
# ── API key flow (existing) ───────────────────────────────────────
|
||||||
|
|
||||||
# Non-TTY: resolve defaults so partial flags work in pipelines / CI
|
# Non-TTY: resolve defaults so partial flags work in pipelines / CI
|
||||||
@@ -265,7 +317,7 @@ def run_init(
|
|||||||
print_error(
|
print_error(
|
||||||
err_console,
|
err_console,
|
||||||
"Non-interactive terminal detected and --api-key is required.",
|
"Non-interactive terminal detected and --api-key is required.",
|
||||||
hint="Run: mem0 init --api-key <key> [--user-id <id>]",
|
hint="Run: mem0 init --api-key <key>, --email <addr>, or --agent for unattended Agent Mode bootstrap.",
|
||||||
)
|
)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
user_id = user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
user_id = user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ class PlatformConfig:
|
|||||||
api_key: str = ""
|
api_key: str = ""
|
||||||
base_url: str = DEFAULT_BASE_URL
|
base_url: str = DEFAULT_BASE_URL
|
||||||
user_email: str = ""
|
user_email: str = ""
|
||||||
|
# Agent Mode (unclaimed-shadow signup)
|
||||||
|
agent_mode: bool = False # True while the key is an unclaimed agent-mode key
|
||||||
|
created_via: str = "" # "agent_mode" | "email" | "api_key" | "existing_key"
|
||||||
|
claimed_at: str = "" # ISO timestamp once the agent has been claimed by a human
|
||||||
|
default_user_id: str = "" # `user_<slug>` returned by bootstrap; used as auto-default
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -83,6 +88,10 @@ def load_config() -> Mem0Config:
|
|||||||
config.platform.api_key = plat.get("api_key", "")
|
config.platform.api_key = plat.get("api_key", "")
|
||||||
config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL)
|
config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL)
|
||||||
config.platform.user_email = plat.get("user_email", "")
|
config.platform.user_email = plat.get("user_email", "")
|
||||||
|
config.platform.agent_mode = bool(plat.get("agent_mode", False))
|
||||||
|
config.platform.created_via = plat.get("created_via", "")
|
||||||
|
config.platform.claimed_at = plat.get("claimed_at", "")
|
||||||
|
config.platform.default_user_id = plat.get("default_user_id", "")
|
||||||
|
|
||||||
defaults = data.get("defaults", {})
|
defaults = data.get("defaults", {})
|
||||||
config.defaults.user_id = defaults.get("user_id", "")
|
config.defaults.user_id = defaults.get("user_id", "")
|
||||||
@@ -136,6 +145,10 @@ def save_config(config: Mem0Config) -> None:
|
|||||||
"api_key": config.platform.api_key,
|
"api_key": config.platform.api_key,
|
||||||
"base_url": config.platform.base_url,
|
"base_url": config.platform.base_url,
|
||||||
"user_email": config.platform.user_email,
|
"user_email": config.platform.user_email,
|
||||||
|
"agent_mode": config.platform.agent_mode,
|
||||||
|
"created_via": config.platform.created_via,
|
||||||
|
"claimed_at": config.platform.claimed_at,
|
||||||
|
"default_user_id": config.platform.default_user_id,
|
||||||
},
|
},
|
||||||
"telemetry": {
|
"telemetry": {
|
||||||
"anonymous_id": config.telemetry.anonymous_id,
|
"anonymous_id": config.telemetry.anonymous_id,
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ def capture_event(
|
|||||||
try:
|
try:
|
||||||
from mem0_cli import __version__
|
from mem0_cli import __version__
|
||||||
from mem0_cli.config import CONFIG_FILE, load_config, save_config
|
from mem0_cli.config import CONFIG_FILE, load_config, save_config
|
||||||
from mem0_cli.state import is_agent_mode
|
|
||||||
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
distinct_id = pre_resolved_email or _get_distinct_id()
|
distinct_id = pre_resolved_email or _get_distinct_id()
|
||||||
@@ -107,6 +106,9 @@ def capture_event(
|
|||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
save_config(config)
|
save_config(config)
|
||||||
|
|
||||||
|
# M4: every cli.* event carries agent_mode based on the config flag
|
||||||
|
# (unclaimed Agent Mode key). This is the growth-doc property used to
|
||||||
|
# join init → add → search funnels in PostHog.
|
||||||
payload = {
|
payload = {
|
||||||
"api_key": POSTHOG_API_KEY,
|
"api_key": POSTHOG_API_KEY,
|
||||||
"distinct_id": distinct_id,
|
"distinct_id": distinct_id,
|
||||||
@@ -115,7 +117,7 @@ def capture_event(
|
|||||||
"source": "CLI",
|
"source": "CLI",
|
||||||
"language": "python",
|
"language": "python",
|
||||||
"cli_version": __version__,
|
"cli_version": __version__,
|
||||||
"agent_mode": is_agent_mode(),
|
"agent_mode": bool(config.platform.agent_mode),
|
||||||
"python_version": sys.version,
|
"python_version": sys.version,
|
||||||
"os": sys.platform,
|
"os": sys.platform,
|
||||||
"os_version": platform.version(),
|
"os_version": platform.version(),
|
||||||
|
|||||||
Reference in New Issue
Block a user