From 844d633960f8a5974316d7fedb48af9e56c8ca2b Mon Sep 17 00:00:00 2001 From: Mgeeeek Date: Thu, 14 May 2026 16:30:52 +0530 Subject: [PATCH] feat(cli): emit JSON envelope on init --agent --json reuse paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PRD's documented form is `mem0 init --agent --json`, but rules 1/2 (env/config reuse) called printSuccess which is silenced under agent mode — so the command exited 0 with no output. Two related fixes: 1. Add `--json` as a subcommand-level option on init (Node only; Python's argv preprocessor already handles this). Lets the PRD-style invocation `mem0 init --agent --json` parse without "unknown option" error. 2. When agent mode is set AND a rule 1/2 reuse fires, emit the Dev Spec C4 envelope: { "status": "success", "command": "init", "data": { "api_key_saved": false, "api_key_source": "env" | "config", "agent_mode": false, "message": "Existing Mem0 API key found and reused..." } } Identical shape between Python and Node (parity). Verified live: `init --agent --json` against prod with a valid MEM0_API_KEY env returns the envelope above (no bootstrap call). --- cli/node/src/commands/init.ts | 30 ++++++++++++++---- cli/node/src/index.ts | 8 +++++ cli/python/src/mem0_cli/commands/init_cmd.py | 33 +++++++++++++++----- cli/python/src/mem0_cli/plugin_sync.py | 10 +++--- 4 files changed, 63 insertions(+), 18 deletions(-) diff --git a/cli/node/src/commands/init.ts b/cli/node/src/commands/init.ts index 9e73e3138..d96d55960 100644 --- a/cli/node/src/commands/init.ts +++ b/cli/node/src/commands/init.ts @@ -21,6 +21,8 @@ import { redactKey, saveConfig, } from "../config.js"; +import { formatJsonEnvelope } from "../output.js"; +import { isAgentMode } from "../state.js"; const { brand, dim } = colors; @@ -328,12 +330,30 @@ export async function runInit( const agentCtx = opts.agent === true || isAgentMode() || detectAgentCaller() !== null; if (!opts.apiKey && !opts.email && agentCtx) { + const emitReuseEnvelope = (source: "env" | "config") => { + if (isAgentMode()) { + formatJsonEnvelope({ + command: "init", + data: { + api_key_saved: false, + api_key_source: source, + agent_mode: false, + message: + "Existing Mem0 API key found and reused. No Agent Mode key was created.", + }, + }); + } else { + printSuccess( + source === "env" + ? "Existing MEM0_API_KEY is valid; reusing it. No new Agent Mode key was minted." + : "Existing API key in config is valid; reusing it. No new Agent Mode key was minted.", + ); + } + }; // 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.", - ); + emitReuseEnvelope("env"); fireInit("existing_key"); return; } @@ -342,9 +362,7 @@ export async function runInit( 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.", - ); + emitReuseEnvelope("config"); fireInit("existing_key"); return; } diff --git a/cli/node/src/index.ts b/cli/node/src/index.ts index f29a8887f..88654f39c 100644 --- a/cli/node/src/index.ts +++ b/cli/node/src/index.ts @@ -211,11 +211,19 @@ program "--source ", "Channel attribution for signup (e.g. github, hn, ph).", ) + // Accept `--json` at the init level too so the PRD-documented form + // `mem0 init --agent --json` works without requiring users to move it + // before the subcommand. Effect is identical to the global `--json`: + // flip agent-mode output state. + .option("--json", "Output as JSON (alias for global `--json`).", false) .addHelpText( "after", "\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice\n $ mem0 init --email you@example.com\n $ mem0 init --email you@example.com --code 123456\n $ mem0 init --agent # Bootstrap an Agent Mode account (unattended)\n $ mem0 init --email you@example.com # Claims an existing Agent Mode key when one is present", ) .action(async (opts) => { + // `--json` at init level mirrors the global flag — flip agent_mode + // state so downstream formatters use JSON envelopes. + if (opts.json) setAgentMode(true); const { runInit } = await import("./commands/init.js"); await runInit({ apiKey: opts.apiKey, diff --git a/cli/python/src/mem0_cli/commands/init_cmd.py b/cli/python/src/mem0_cli/commands/init_cmd.py index 5ec1ee6be..f13f8d3fe 100644 --- a/cli/python/src/mem0_cli/commands/init_cmd.py +++ b/cli/python/src/mem0_cli/commands/init_cmd.py @@ -258,23 +258,40 @@ def run_init( # 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: + from mem0_cli.output import format_json_envelope + from mem0_cli.state import is_agent_mode as _is_json_mode + + def _emit_reuse(source: str) -> None: + if _is_json_mode(): + format_json_envelope( + console, + command="init", + data={ + "api_key_saved": False, + "api_key_source": source, + "agent_mode": False, + "message": "Existing Mem0 API key found and reused. No Agent Mode key was created.", + }, + ) + else: + msg = ( + "Existing MEM0_API_KEY is valid; reusing it. No new Agent Mode key was minted." + if source == "env" + else "Existing API key in config is valid; reusing it. No new Agent Mode key was minted." + ) + print_success(console, msg) + # 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.", - ) + _emit_reuse("env") _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.", - ) + _emit_reuse("config") _fire_init("existing_key") return # Rule 3: mint a fresh shadow (no valid key to reuse). diff --git a/cli/python/src/mem0_cli/plugin_sync.py b/cli/python/src/mem0_cli/plugin_sync.py index f984c8cd5..ab9ba76ef 100644 --- a/cli/python/src/mem0_cli/plugin_sync.py +++ b/cli/python/src/mem0_cli/plugin_sync.py @@ -26,6 +26,7 @@ Out of scope (deliberately not touched): from __future__ import annotations +import contextlib import json import os import re @@ -77,7 +78,10 @@ def _update_claude_settings(path: Path, api_key: str) -> bool: # Match `export MEM0_API_KEY="..."` (or single quotes, or no quotes). # Use [ \t]* (not \s*) for trailing whitespace so a trailing newline at # end-of-file is preserved when MEM0_API_KEY is the last line. -_RC_LINE = re.compile(r'^([ \t]*export[ \t]+MEM0_API_KEY[ \t]*=[ \t]*)(["\']?)([^"\'\n]*)(["\']?)[ \t]*$', re.MULTILINE) +_RC_LINE = re.compile( + r'^([ \t]*export[ \t]+MEM0_API_KEY[ \t]*=[ \t]*)(["\']?)([^"\'\n]*)(["\']?)[ \t]*$', + re.MULTILINE, +) def _update_shell_rc(path: Path, api_key: str) -> bool: @@ -110,8 +114,6 @@ def _atomic_write_text(path: Path, content: str) -> None: os.chmod(tmp_path, path.stat().st_mode & 0o777) os.replace(tmp_path, path) except Exception: - try: + with contextlib.suppress(OSError): os.unlink(tmp_path) - except OSError: - pass raise