feat(cli): emit JSON envelope on init --agent --json reuse paths
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).
This commit is contained in:
@@ -21,6 +21,8 @@ import {
|
|||||||
redactKey,
|
redactKey,
|
||||||
saveConfig,
|
saveConfig,
|
||||||
} from "../config.js";
|
} from "../config.js";
|
||||||
|
import { formatJsonEnvelope } from "../output.js";
|
||||||
|
import { isAgentMode } from "../state.js";
|
||||||
|
|
||||||
const { brand, dim } = colors;
|
const { brand, dim } = colors;
|
||||||
|
|
||||||
@@ -328,12 +330,30 @@ export async function runInit(
|
|||||||
const agentCtx =
|
const agentCtx =
|
||||||
opts.agent === true || isAgentMode() || detectAgentCaller() !== null;
|
opts.agent === true || isAgentMode() || detectAgentCaller() !== null;
|
||||||
if (!opts.apiKey && !opts.email && agentCtx) {
|
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.
|
// Rule 1: env MEM0_API_KEY valid → reuse, no new key.
|
||||||
const envKey = (process.env.MEM0_API_KEY || "").trim();
|
const envKey = (process.env.MEM0_API_KEY || "").trim();
|
||||||
if (envKey && (await pingKey(envKey, baseUrl))) {
|
if (envKey && (await pingKey(envKey, baseUrl))) {
|
||||||
printSuccess(
|
emitReuseEnvelope("env");
|
||||||
"Existing MEM0_API_KEY is valid; reusing it. No new Agent Mode key was minted.",
|
|
||||||
);
|
|
||||||
fireInit("existing_key");
|
fireInit("existing_key");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -342,9 +362,7 @@ export async function runInit(
|
|||||||
savedConfig.platform.apiKey &&
|
savedConfig.platform.apiKey &&
|
||||||
(await pingKey(savedConfig.platform.apiKey, baseUrl))
|
(await pingKey(savedConfig.platform.apiKey, baseUrl))
|
||||||
) {
|
) {
|
||||||
printSuccess(
|
emitReuseEnvelope("config");
|
||||||
"Existing API key in config is valid; reusing it. No new Agent Mode key was minted.",
|
|
||||||
);
|
|
||||||
fireInit("existing_key");
|
fireInit("existing_key");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,11 +211,19 @@ program
|
|||||||
"--source <channel>",
|
"--source <channel>",
|
||||||
"Channel attribution for signup (e.g. github, hn, ph).",
|
"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(
|
.addHelpText(
|
||||||
"after",
|
"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",
|
"\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) => {
|
.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");
|
const { runInit } = await import("./commands/init.js");
|
||||||
await runInit({
|
await runInit({
|
||||||
apiKey: opts.apiKey,
|
apiKey: opts.apiKey,
|
||||||
|
|||||||
@@ -258,23 +258,40 @@ def run_init(
|
|||||||
# is no valid key to reuse — in that case overwriting is correct.
|
# 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)
|
_agent_ctx = agent or _global_agent_mode() or (detect_agent_caller() is not None)
|
||||||
if not api_key and not email and _agent_ctx:
|
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.
|
# Rule 1: env MEM0_API_KEY valid → reuse, no new key.
|
||||||
_env_key = (os.environ.get("MEM0_API_KEY") or "").strip()
|
_env_key = (os.environ.get("MEM0_API_KEY") or "").strip()
|
||||||
if _env_key and _ping_key(_env_key, base_url):
|
if _env_key and _ping_key(_env_key, base_url):
|
||||||
print_success(
|
_emit_reuse("env")
|
||||||
console,
|
|
||||||
"Existing MEM0_API_KEY is valid; reusing it. No new Agent Mode key was minted.",
|
|
||||||
)
|
|
||||||
_fire_init("existing_key")
|
_fire_init("existing_key")
|
||||||
return
|
return
|
||||||
# Rule 2: existing config api_key valid → reuse.
|
# Rule 2: existing config api_key valid → reuse.
|
||||||
if CONFIG_FILE.exists():
|
if CONFIG_FILE.exists():
|
||||||
_existing = load_config()
|
_existing = load_config()
|
||||||
if _existing.platform.api_key and _ping_key(_existing.platform.api_key, base_url):
|
if _existing.platform.api_key and _ping_key(_existing.platform.api_key, base_url):
|
||||||
print_success(
|
_emit_reuse("config")
|
||||||
console,
|
|
||||||
"Existing API key in config is valid; reusing it. No new Agent Mode key was minted.",
|
|
||||||
)
|
|
||||||
_fire_init("existing_key")
|
_fire_init("existing_key")
|
||||||
return
|
return
|
||||||
# Rule 3: mint a fresh shadow (no valid key to reuse).
|
# Rule 3: mint a fresh shadow (no valid key to reuse).
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ Out of scope (deliberately not touched):
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
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).
|
# Match `export MEM0_API_KEY="..."` (or single quotes, or no quotes).
|
||||||
# Use [ \t]* (not \s*) for trailing whitespace so a trailing newline at
|
# 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.
|
# 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:
|
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.chmod(tmp_path, path.stat().st_mode & 0o777)
|
||||||
os.replace(tmp_path, path)
|
os.replace(tmp_path, path)
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
with contextlib.suppress(OSError):
|
||||||
os.unlink(tmp_path)
|
os.unlink(tmp_path)
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
raise
|
raise
|
||||||
|
|||||||
Reference in New Issue
Block a user