From f21e9fe2b3ab3cfc04b9cbc589da9aa1e3663c7c Mon Sep 17 00:00:00 2001 From: Mgeeeek Date: Thu, 14 May 2026 16:43:17 +0530 Subject: [PATCH] fix(cli): JSON error envelope shows command name + clearer rate-limit message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two papercuts surfaced when prod's bootstrap returned 403: {"status": "error", "command": "", "error": "Bootstrap failed: {\"detail\":\"You do not have permission to perform this action.\"}", "data": null} 1. `"command": ""` — the JSON error envelope (printError under agent mode) reads from current_command state, but nothing ever called setCurrentCommand on the init subcommand. Hook into Node's preAction and Python's main_callback to stash the active subcommand name so error envelopes report which command failed. 2. Opaque rate-limit message. The backend's @ratelimit decorator raises PermissionDenied → DRF translates to generic 403 "You do not have permission to perform this action." Users don't know it's a rate limit. Detect status_code==403 with /permission/i in the detail and surface the actual reason: "Daily Agent Mode signup limit reached for this network (5/day). Try again from a different IP or after midnight UTC." Both fixes mirrored in Python (agent_mode_cmd.py, app.py) and Node (agent-mode.ts, index.ts). --- cli/node/src/commands/agent-mode.ts | 14 ++++++++++++-- cli/node/src/index.ts | 6 +++++- cli/python/src/mem0_cli/app.py | 7 +++++++ .../src/mem0_cli/commands/agent_mode_cmd.py | 15 +++++++++++++-- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/cli/node/src/commands/agent-mode.ts b/cli/node/src/commands/agent-mode.ts index 33ac2f883..c7090d05b 100644 --- a/cli/node/src/commands/agent-mode.ts +++ b/cli/node/src/commands/agent-mode.ts @@ -64,11 +64,21 @@ export async function bootstrapViaBackend( if (!resp.ok) { let detail: string = resp.statusText; try { - const errBody = (await resp.json()) as { error?: string }; - if (errBody.error) detail = errBody.error; + const body = (await resp.json()) as { error?: string; detail?: string }; + detail = body.error ?? body.detail ?? resp.statusText; } catch { /* leave detail as statusText */ } + // Backend's @ratelimit decorator raises PermissionDenied, which DRF + // translates to a generic 403 "You do not have permission to perform + // this action." That's opaque — surface it as the rate-limit message + // it actually is. + if (resp.status === 403 && /permission/i.test(detail)) { + printError( + "Daily Agent Mode signup limit reached for this network (5/day). Try again from a different IP or after midnight UTC.", + ); + process.exit(1); + } printError(`Bootstrap failed: ${detail}`); process.exit(1); } diff --git a/cli/node/src/index.ts b/cli/node/src/index.ts index 88654f39c..376761c88 100644 --- a/cli/node/src/index.ts +++ b/cli/node/src/index.ts @@ -13,7 +13,7 @@ import { colors, printError, printWarning } from "./branding.js"; import type { Mem0Config } from "./config.js"; import { loadConfig, saveConfig } from "./config.js"; import { richFormatHelp } from "./help.js"; -import { isAgentMode, setAgentMode, takeNotice } from "./state.js"; +import { isAgentMode, setAgentMode, setCurrentCommand, takeNotice } from "./state.js"; import { captureEvent } from "./telemetry.js"; import { CLI_VERSION } from "./version.js"; @@ -171,6 +171,10 @@ program.hook("preAction", (_thisCommand, actionCommand) => { parentName && parentName !== "mem0" ? `${parentName}.${commandName}` : commandName; + // Stash the active command name in shared state so the JSON + // error envelope (printError) can report which command failed + // instead of an empty `"command": ""` field. + setCurrentCommand(fullCommand); // init fires its own telemetry from runInit with full M1-M6 props // (mode/agent_caller/signup_source/claimed_agent_mode); skip the // auto-fire here so we don't double-count. diff --git a/cli/python/src/mem0_cli/app.py b/cli/python/src/mem0_cli/app.py index 9c49a2ad0..2f1fe4e4a 100644 --- a/cli/python/src/mem0_cli/app.py +++ b/cli/python/src/mem0_cli/app.py @@ -236,6 +236,13 @@ def main_callback( _fire_telemetry("version") cmd_version() raise typer.Exit() + if ctx.invoked_subcommand: + # Stash the active subcommand name so the JSON error envelope + # (print_error in agent mode) can report which command failed + # instead of an empty `"command": ""` field. + from mem0_cli.state import set_current_command + + set_current_command(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) diff --git a/cli/python/src/mem0_cli/commands/agent_mode_cmd.py b/cli/python/src/mem0_cli/commands/agent_mode_cmd.py index 6623ed220..e28ba7faf 100644 --- a/cli/python/src/mem0_cli/commands/agent_mode_cmd.py +++ b/cli/python/src/mem0_cli/commands/agent_mode_cmd.py @@ -61,10 +61,21 @@ def bootstrap_via_backend( print_error(err_console, "Agent Mode is temporarily disabled. Try again later.") raise typer.Exit(1) if resp.status_code != 200: + detail = resp.text try: - detail = resp.json().get("error", resp.text) + body = resp.json() + detail = body.get("error") or body.get("detail") or resp.text except Exception: - detail = resp.text + pass + # Backend's @ratelimit decorator raises PermissionDenied, which DRF + # translates to a generic 403 "You do not have permission to perform + # this action." That's opaque — surface as the rate-limit it actually is. + if resp.status_code == 403 and "permission" in str(detail).lower(): + print_error( + err_console, + "Daily Agent Mode signup limit reached for this network (5/day). Try again from a different IP or after midnight UTC.", + ) + raise typer.Exit(1) print_error(err_console, f"Bootstrap failed: {detail}") raise typer.Exit(1)