diff --git a/cli/node/src/commands/init.ts b/cli/node/src/commands/init.ts index f03c45c26..9e73e3138 100644 --- a/cli/node/src/commands/init.ts +++ b/cli/node/src/commands/init.ts @@ -33,6 +33,22 @@ function validateEmail(email: string): void { } } +async function pingKey( + apiKey: string, + baseUrl: string, + timeoutMs = 5000, +): Promise { + try { + const resp = await fetch(`${baseUrl.replace(/\/+$/, "")}/v1/ping/`, { + headers: { Authorization: `Token ${apiKey}` }, + signal: AbortSignal.timeout(timeoutMs), + }); + return resp.status === 200; + } catch { + return false; + } +} + async function emailLogin( email: string, code: string | undefined, @@ -304,6 +320,40 @@ export async function runInit( return; } + // ── Agent Mode path runs BEFORE the existing-config guard ────────────── + // Rule 1/2 will REUSE a valid existing key (not overwrite), so we must + // short-circuit before the guard prompts the user about overwriting. + // Rule 3 only mints when there's no valid key to reuse — in that case + // overwriting is what the user wants. + const agentCtx = + opts.agent === true || isAgentMode() || detectAgentCaller() !== null; + if (!opts.apiKey && !opts.email && agentCtx) { + // Rule 1: env MEM0_API_KEY valid → reuse, no new key. + const envKey = (process.env.MEM0_API_KEY || "").trim(); + if (envKey && (await pingKey(envKey, baseUrl))) { + printSuccess( + "Existing MEM0_API_KEY is valid; reusing it. No new Agent Mode key was minted.", + ); + fireInit("existing_key"); + return; + } + // Rule 2: existing config api_key valid → reuse. + if ( + savedConfig.platform.apiKey && + (await pingKey(savedConfig.platform.apiKey, baseUrl)) + ) { + printSuccess( + "Existing API key in config is valid; reusing it. No new Agent Mode key was minted.", + ); + fireInit("existing_key"); + return; + } + // Rule 3: mint a fresh shadow (no valid key to reuse). + await bootstrapViaBackend(config, { source: opts.source ?? null }); + fireInit("agent"); + return; + } + // Warn if an existing config with an API key would be overwritten if ( !opts.force && @@ -375,19 +425,9 @@ export async function runInit( return; } - // ── Agent Mode auto-bootstrap (no email, no api_key flag) ─────────── - // Positive agent signal required: --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. - const agentCtx = - opts.agent === true || isAgentMode() || detectAgentCaller() !== null; - if (!opts.apiKey && !opts.email && agentCtx) { - await bootstrapViaBackend(config, { source: opts.source ?? null }); - fireInit("agent"); - return; - } - // ── API key flow ────────────────────────────────────────────────────────── + // (Agent Mode branch runs earlier — see above, before the existing-config + // guard, so Rules 1/2 can REUSE a valid key without prompting overwrite.) // Non-TTY: resolve defaults so partial flags work in pipelines / CI if (!process.stdin.isTTY) { diff --git a/cli/python/src/mem0_cli/commands/init_cmd.py b/cli/python/src/mem0_cli/commands/init_cmd.py index 775d41992..5ec1ee6be 100644 --- a/cli/python/src/mem0_cli/commands/init_cmd.py +++ b/cli/python/src/mem0_cli/commands/init_cmd.py @@ -103,6 +103,19 @@ def _validate_email(email: str) -> None: raise typer.Exit(1) +def _ping_key(api_key: str, base_url: str, timeout: float = 5.0) -> bool: + """True if api_key passes /v1/ping/ against base_url within timeout.""" + try: + resp = httpx.get( + f"{base_url.rstrip('/')}/v1/ping/", + headers={"Authorization": f"Token {api_key}"}, + timeout=timeout, + ) + return resp.status_code == 200 + except Exception: + return False + + def _email_login( email: str, code: str | None, @@ -239,6 +252,36 @@ def run_init( _fire_init("email", claimed=True) return + # ── Agent Mode path runs BEFORE the existing-config guard ────────── + # Rules 1/2 REUSE a valid existing key (not overwrite), so we must + # short-circuit before the guard prompts. Rule 3 mints only when there + # is no valid key to reuse — in that case overwriting is correct. + _agent_ctx = agent or _global_agent_mode() or (detect_agent_caller() is not None) + if not api_key and not email and _agent_ctx: + # Rule 1: env MEM0_API_KEY valid → reuse, no new key. + _env_key = (os.environ.get("MEM0_API_KEY") or "").strip() + if _env_key and _ping_key(_env_key, base_url): + print_success( + console, + "Existing MEM0_API_KEY is valid; reusing it. No new Agent Mode key was minted.", + ) + _fire_init("existing_key") + return + # Rule 2: existing config api_key valid → reuse. + if CONFIG_FILE.exists(): + _existing = load_config() + if _existing.platform.api_key and _ping_key(_existing.platform.api_key, base_url): + print_success( + console, + "Existing API key in config is valid; reusing it. No new Agent Mode key was minted.", + ) + _fire_init("existing_key") + return + # Rule 3: mint a fresh shadow (no valid key to reuse). + bootstrap_via_backend(config, source=source) + _fire_init("agent") + return + # Warn if an existing config with an API key would be overwritten if not force and CONFIG_FILE.exists(): existing = load_config() @@ -300,17 +343,9 @@ def run_init( console.print() 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) ─────────────────────────────────────── + # (Agent Mode branch runs earlier — see above, before the existing-config + # guard, so Rules 1/2 can REUSE a valid key without prompting overwrite.) # Non-TTY: resolve defaults so partial flags work in pipelines / CI if not sys.stdin.isatty():