fix(cli): JSON error envelope shows command name + clearer rate-limit message
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).
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user