diff --git a/README.md b/README.md
index a5520c6e3..d1233ad1d 100644
--- a/README.md
+++ b/README.md
@@ -86,6 +86,8 @@ See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for upgra
## 🚀 Quickstart Guide
+> **Agent Mode — for AI agents setting up Mem0 autonomously.** Run `mem0 init --agent --json` (after `pip install mem0-cli` or `npm install -g @mem0/cli`) to mint an evaluation Mem0 API key in <5 seconds — no email, no dashboard, no OTP. Use it immediately against the Platform or the MCP server. The human owner can claim ownership later with `mem0 init --email `: memories transfer, the same key keeps working, and the agent isn't disrupted.
+
| | Library | Self-Hosted Server | Cloud Platform |
|---|---------|-------------------|----------------|
| **Best for** | Testing, prototyping | Teams running on their own infrastructure | Zero-ops production use |
diff --git a/cli/cli-spec.json b/cli/cli-spec.json
index 4826bd1ad..437bb8e46 100644
--- a/cli/cli-spec.json
+++ b/cli/cli-spec.json
@@ -503,7 +503,7 @@
},
{
"name": "init",
- "description": "Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
+ "description": "Setup wizard for mem0 CLI. Supports Agent Mode bootstrap (--agent), email login (--email), or manual API key (--api-key).",
"usage": "mem0 init [OPTIONS]",
"needsBackend": false,
"needsConfig": false,
@@ -516,7 +516,9 @@
{ "name": "user-id", "flags": ["-u", "--user-id"], "type": "string", "default": null, "help": "Default user ID (skip prompt)." },
{ "name": "email", "flags": ["--email"], "type": "string", "default": null, "help": "Login via email verification code." },
{ "name": "code", "flags": ["--code"], "type": "string", "default": null, "help": "Verification code (use with --email for non-interactive login)." },
- { "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Overwrite existing config without confirmation." }
+ { "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Overwrite existing config without confirmation." },
+ { "name": "agent", "flags": ["--agent"], "type": "boolean", "default": false, "help": "Bootstrap an unattended Agent Mode account (no email required)." },
+ { "name": "source", "flags": ["--source"], "type": "string", "default": null, "help": "Channel attribution for signup (e.g. github, hn, ph)." }
]
},
{
diff --git a/cli/node/src/backend/platform.ts b/cli/node/src/backend/platform.ts
index 3d7b012ad..57eb51dde 100644
--- a/cli/node/src/backend/platform.ts
+++ b/cli/node/src/backend/platform.ts
@@ -3,7 +3,7 @@
*/
import type { PlatformConfig } from "../config.js";
-import { isAgentMode } from "../state.js";
+import { captureNotice, isAgentMode } from "../state.js";
import { CLI_VERSION } from "../version.js";
import {
APIError,
@@ -90,7 +90,39 @@ export class PlatformBackend implements Backend {
if (resp.status === 204) {
return {};
}
- return resp.json();
+
+ const data = await resp.json();
+
+ // Pull the unclaimed-Agent-Mode notice out of the body (or the header
+ // fallback for endpoints returning non-dict / non-dict-leading payloads)
+ // and stash for end-of-command surfacing.
+ let notice: string | null = null;
+ if (
+ data &&
+ typeof data === "object" &&
+ !Array.isArray(data) &&
+ "mem0_notice" in data
+ ) {
+ notice = (data as Record).mem0_notice as string;
+ // biome-ignore lint/performance/noDelete: intentional strip so downstream consumers don't see duplicate notice
+ delete (data as Record).mem0_notice;
+ } else if (
+ Array.isArray(data) &&
+ data.length > 0 &&
+ typeof data[0] === "object" &&
+ data[0] !== null &&
+ "mem0_notice" in data[0]
+ ) {
+ notice = (data[0] as Record).mem0_notice as string;
+ // biome-ignore lint/performance/noDelete: see above.
+ delete (data[0] as Record).mem0_notice;
+ }
+ if (!notice) {
+ notice = resp.headers.get("X-Mem0-Notice-Message") ?? null;
+ }
+ captureNotice(notice);
+
+ return data;
}
async add(
diff --git a/cli/node/src/commands/agent-mode.ts b/cli/node/src/commands/agent-mode.ts
index dd351dc9e..33ac2f883 100644
--- a/cli/node/src/commands/agent-mode.ts
+++ b/cli/node/src/commands/agent-mode.ts
@@ -21,13 +21,17 @@ export interface BootstrapEnvelope {
mcp_url?: string;
smoke_test_url?: string;
claim_command?: string;
+ mem0_notice?: string;
}
export async function bootstrapViaBackend(
config: Mem0Config,
{ source }: { source?: string | null } = {},
): Promise {
- const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(/\/+$/, "");
+ const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(
+ /\/+$/,
+ "",
+ );
const body: Record = {};
if (source) body.source = source;
@@ -43,7 +47,9 @@ export async function bootstrapViaBackend(
signal: AbortSignal.timeout(30_000),
});
} catch (err) {
- printError(`Network error contacting Mem0: ${err instanceof Error ? err.message : String(err)}`);
+ printError(
+ `Network error contacting Mem0: ${err instanceof Error ? err.message : String(err)}`,
+ );
process.exit(1);
}
@@ -79,10 +85,16 @@ export async function bootstrapViaBackend(
config.defaults.userId = envelope.default_user_id;
saveConfig(config);
- printSuccess(`Agent Mode active. Default user_id: ${envelope.default_user_id}`);
- console.log(
- ` ${dim(`To claim this account later: ${envelope.claim_command ?? "mem0 init --email "}`)}`,
+ printSuccess(
+ `Agent Mode active. Default user_id: ${envelope.default_user_id}`,
);
+ if (envelope.mem0_notice) {
+ console.log(`\n\x1b[33m🔔 ${envelope.mem0_notice}\x1b[0m\n`);
+ } else {
+ // Fallback for older backends without the unified notice field.
+ const claimCmd = envelope.claim_command ?? "mem0 init --email ";
+ console.log(` ${dim(`To claim this account later: ${claimCmd}`)}`);
+ }
}
/**
@@ -97,9 +109,14 @@ export async function claimViaOtp(
config: Mem0Config,
{ email, code }: { email: string; code?: string },
): Promise {
- const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(/\/+$/, "");
+ const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(
+ /\/+$/,
+ "",
+ );
if (!config.platform.apiKey || !config.platform.agentMode) {
- printError("This command requires an active Agent Mode config. Run `mem0 init` first.");
+ printError(
+ "This command requires an active Agent Mode config. Run `mem0 init` first.",
+ );
process.exit(1);
}
@@ -163,7 +180,10 @@ export async function claimViaOtp(
let detail: string = verifyResp.statusText;
let errCode = "";
try {
- const errBody = (await verifyResp.json()) as { error?: string; code?: string };
+ const errBody = (await verifyResp.json()) as {
+ error?: string;
+ code?: string;
+ };
if (errBody.error) detail = errBody.error;
if (errBody.code) errCode = errBody.code;
} catch {
@@ -178,7 +198,10 @@ export async function claimViaOtp(
process.exit(1);
}
- const body = (await verifyResp.json()) as { claimed?: boolean; claimed_at?: string };
+ const body = (await verifyResp.json()) as {
+ claimed?: boolean;
+ claimed_at?: string;
+ };
if (!body.claimed) {
printError(`Unexpected verify response: ${JSON.stringify(body)}`);
process.exit(1);
diff --git a/cli/node/src/commands/init.ts b/cli/node/src/commands/init.ts
index 29a422fa3..a00a35430 100644
--- a/cli/node/src/commands/init.ts
+++ b/cli/node/src/commands/init.ts
@@ -258,7 +258,10 @@ export async function runInit(
const { isAgentMode } = await import("../state.js");
const { captureEvent } = await import("../telemetry.js");
- const fireInit = (mode: "agent" | "email" | "api_key" | "existing_key", claimed = false) => {
+ const fireInit = (
+ mode: "agent" | "email" | "api_key" | "existing_key",
+ claimed = false,
+ ) => {
const props: Record = { command: "init", mode };
const caller = detectAgentCaller();
if (caller) props.agent_caller = caller;
@@ -286,7 +289,12 @@ export async function runInit(
}
// ── Claim flow: --email against an existing agent-mode config ───────────
- if (opts.email && fs.existsSync(CONFIG_FILE) && savedConfig.platform.agentMode && savedConfig.platform.apiKey) {
+ if (
+ opts.email &&
+ fs.existsSync(CONFIG_FILE) &&
+ savedConfig.platform.agentMode &&
+ savedConfig.platform.apiKey
+ ) {
const email = opts.email.trim().toLowerCase();
validateEmail(email);
printInfo(`Claiming Agent Mode account to ${email}...`);
@@ -370,9 +378,7 @@ export async function runInit(
// 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;
+ opts.agent === true || isAgentMode() || detectAgentCaller() !== null;
if (!opts.apiKey && !opts.email && agentCtx) {
await bootstrapViaBackend(config, { source: opts.source ?? null });
fireInit("agent");
diff --git a/cli/node/src/index.ts b/cli/node/src/index.ts
index d329a32af..ff9bb61aa 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 { setAgentMode } from "./state.js";
+import { isAgentMode, setAgentMode, takeNotice } from "./state.js";
import { captureEvent } from "./telemetry.js";
import { CLI_VERSION } from "./version.js";
@@ -197,8 +197,15 @@ program
"Verification code (use with --email for non-interactive login).",
)
.option("--force", "Overwrite existing config without confirmation.", false)
- .option("--agent", "Bootstrap an unattended Agent Mode account (no email required).", false)
- .option("--source ", "Channel attribution for signup (e.g. github, hn, ph).")
+ .option(
+ "--agent",
+ "Bootstrap an unattended Agent Mode account (no email required).",
+ false,
+ )
+ .option(
+ "--source ",
+ "Channel attribution for signup (e.g. github, hn, ph).",
+ )
.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",
@@ -777,4 +784,16 @@ program
// ── Entrypoint ────────────────────────────────────────────────────────────
-program.parse();
+// Surface any unclaimed Agent Mode notice once per command, after the primary
+// output. In JSON/agent mode the notice is folded into the envelope by
+// formatJsonEnvelope, so skip the stderr banner there to avoid duplication.
+function surfaceNotice(): void {
+ const notice = takeNotice();
+ if (notice && !isAgentMode()) {
+ process.stderr.write(`\n\x1b[33m🔔 ${notice}\x1b[0m\n\n`);
+ }
+}
+
+program.parseAsync().finally(() => {
+ surfaceNotice();
+});
diff --git a/cli/node/src/output.ts b/cli/node/src/output.ts
index 83cfa98b1..fa14849e7 100644
--- a/cli/node/src/output.ts
+++ b/cli/node/src/output.ts
@@ -5,6 +5,7 @@
import boxen from "boxen";
import Table from "cli-table3";
import { colors, sym } from "./branding.js";
+import { takeNotice } from "./state.js";
const { brand, accent, success, error: errorColor, dim } = colors;
@@ -244,6 +245,15 @@ export function formatJsonEnvelope(opts: {
if (opts.count !== undefined) envelope.count = opts.count;
if (opts.error) envelope.error = opts.error;
envelope.data = opts.data;
+
+ // If the platform flagged this as an unclaimed Agent Mode account, surface
+ // the notice inside the JSON envelope so an agent consuming the output
+ // sees it without needing to inspect HTTP headers.
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const { takeNotice } = require("./state.js");
+ const notice = takeNotice();
+ if (notice) envelope.mem0_notice = notice;
+
console.log(JSON.stringify(envelope, null, 2));
}
@@ -356,6 +366,12 @@ export function formatAgentEnvelope(opts: {
}
if (opts.count !== undefined) envelope.count = opts.count;
envelope.data = sanitizeAgentData(opts.command, opts.data);
+
+ // Surface the unclaimed-Agent-Mode notice (if any) in the envelope so an
+ // agent reading the JSON output sees it without inspecting HTTP headers.
+ const notice = takeNotice();
+ if (notice) envelope.mem0_notice = notice;
+
console.log(JSON.stringify(envelope, null, 2));
}
diff --git a/cli/node/src/state.ts b/cli/node/src/state.ts
index a654c5d8e..724068925 100644
--- a/cli/node/src/state.ts
+++ b/cli/node/src/state.ts
@@ -5,6 +5,7 @@
let _agentMode = false;
let _currentCommand = "";
+let _pendingNotice = "";
export function isAgentMode(): boolean {
return _agentMode;
@@ -21,3 +22,19 @@ export function getCurrentCommand(): string {
export function setCurrentCommand(name: string): void {
_currentCommand = name;
}
+
+/**
+ * Stash a Mem0 backend notice (Agent Mode unclaimed reminder) for end-of-
+ * command surfacing. Called from the platform backend after each response so
+ * the notice prints once per command regardless of how many sub-requests
+ * fired. Last-write-wins is fine — the message text is identical.
+ */
+export function captureNotice(notice: string | null | undefined): void {
+ if (notice) _pendingNotice = notice;
+}
+
+export function takeNotice(): string {
+ const msg = _pendingNotice;
+ _pendingNotice = "";
+ return msg;
+}
diff --git a/cli/node/tests/agent-mode.test.ts b/cli/node/tests/agent-mode.test.ts
new file mode 100644
index 000000000..79e3203ae
--- /dev/null
+++ b/cli/node/tests/agent-mode.test.ts
@@ -0,0 +1,141 @@
+/**
+ * Parity tests for `mem0 init --agent` (Agent Mode bootstrap).
+ *
+ * Mirror of `cli/python/tests/test_agent_mode.py` — both files MUST stay
+ * in sync so that the Python and Node CLIs expose an identical surface
+ * for the Agent Mode entrypoint. If you add a flag here, add the same
+ * assertion on the Python side (and vice versa).
+ *
+ * Network-bound bootstrap is covered by the platform-side E2E suite
+ * (`backend/tests/e2e/test_05_agent_mode.py`); these tests only verify
+ * the CLI surface that ships in the binary.
+ */
+
+import { describe, it, expect } from "vitest";
+import { execSync } from "node:child_process";
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+
+function run(
+ args: string[],
+ opts: { home?: string; env?: Record } = {},
+): { stdout: string; stderr: string; exitCode: number } {
+ const env = { ...process.env };
+ for (const key of Object.keys(env)) {
+ if (key.startsWith("MEM0_")) delete env[key];
+ }
+ if (opts.home) env.HOME = opts.home;
+ if (opts.env) Object.assign(env, opts.env);
+
+ try {
+ const stdout = execSync(`npx tsx src/index.ts ${args.join(" ")}`, {
+ cwd: path.join(__dirname, ".."),
+ env,
+ encoding: "utf-8",
+ timeout: 15000,
+ });
+ return { stdout, stderr: "", exitCode: 0 };
+ } catch (e: any) {
+ return {
+ stdout: e.stdout ?? "",
+ stderr: e.stderr ?? "",
+ exitCode: e.status ?? 1,
+ };
+ }
+}
+
+function cleanHome(): string {
+ return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
+}
+
+describe("init flag surface", () => {
+ it("init --help lists --agent", () => {
+ const result = run(["init", "--help"]);
+ expect(result.exitCode).toBe(0);
+ expect(result.stdout).toContain("--agent");
+ });
+
+ it("init --help describes Agent Mode", () => {
+ const result = run(["init", "--help"]);
+ expect(result.exitCode).toBe(0);
+ // Description must mention what --agent actually does so an agent
+ // reading the help can self-discover the bootstrap entrypoint.
+ expect(
+ result.stdout.includes("Agent Mode") ||
+ result.stdout.toLowerCase().includes("unattended"),
+ ).toBe(true);
+ });
+
+ it("init --help lists --source", () => {
+ const result = run(["init", "--help"]);
+ expect(result.exitCode).toBe(0);
+ expect(result.stdout).toContain("--source");
+ });
+
+ it("init --help lists --email and --code", () => {
+ const result = run(["init", "--help"]);
+ expect(result.exitCode).toBe(0);
+ expect(result.stdout).toContain("--email");
+ expect(result.stdout).toContain("--code");
+ });
+});
+
+describe("argv preprocessing — --agent reaches init subcommand", () => {
+ // Regression for the bug where the global --agent JSON-alias swallowed
+ // the init-level --agent flag, making `mem0 init --agent` behave like
+ // the plain interactive wizard.
+
+ it("init --agent triggers bootstrap branch (not the wizard)", () => {
+ const home = cleanHome();
+ const result = run(["init", "--agent"], {
+ home,
+ env: {
+ MEM0_BASE_URL: "http://127.0.0.1:1", // blackhole
+ FORCE_COLOR: "0",
+ },
+ });
+ const combined = (result.stdout + result.stderr).toLowerCase();
+ // Either bootstrap-attempt error, or a connection/network error —
+ // both prove the --agent path executed (the wizard would prompt for
+ // input and succeed/hang, not surface a network error).
+ expect(
+ combined.includes("agent") ||
+ combined.includes("connect") ||
+ combined.includes("network") ||
+ combined.includes("fetch") ||
+ combined.includes("bootstrap"),
+ ).toBe(true);
+ fs.rmSync(home, { recursive: true, force: true });
+ });
+});
+
+describe("JSON envelope on network failure", () => {
+ it("init --agent --json does not leak a stack trace when backend is unreachable", () => {
+ const home = cleanHome();
+ const result = run(["init", "--agent", "--json"], {
+ home,
+ env: {
+ MEM0_BASE_URL: "http://127.0.0.1:1",
+ FORCE_COLOR: "0",
+ },
+ });
+ const combined = result.stdout + result.stderr;
+ // No raw Node stack should escape the agent-mode handler.
+ expect(combined).not.toMatch(/at \w+\s*\(.+\.ts:\d+/);
+ expect(combined).not.toContain("UnhandledPromiseRejection");
+ expect(result.exitCode).not.toBe(0);
+ fs.rmSync(home, { recursive: true, force: true });
+ });
+});
+
+describe("top-level help lists init", () => {
+ // `mem0 --help` must list `init` so agents walking the top-level help
+ // can discover the Agent Mode entrypoint without prior knowledge.
+
+ it("--help lists init", () => {
+ const result = run(["--help"]);
+ expect(result.exitCode).toBe(0);
+ expect(result.stdout).toContain("init");
+ });
+});
diff --git a/cli/python/src/mem0_cli/app.py b/cli/python/src/mem0_cli/app.py
index 51e170e5a..9c49a2ad0 100644
--- a/cli/python/src/mem0_cli/app.py
+++ b/cli/python/src/mem0_cli/app.py
@@ -856,7 +856,9 @@ def init(
False, "--agent", help="Bootstrap an unattended Agent Mode account (no email required)."
),
source: str | None = typer.Option(
- None, "--source", help="Channel attribution for signup (e.g. github, hn, ph).",
+ None,
+ "--source",
+ help="Channel attribution for signup (e.g. github, hn, ph).",
),
) -> None:
"""Interactive setup wizard for mem0 CLI.
@@ -1215,11 +1217,28 @@ def main() -> None:
import sys
# Allow --json/--agent anywhere in the command line (not just before subcommand).
- _json_flags = {"--json", "--agent"}
- if any(a in _json_flags for a in sys.argv[1:]):
+ # Special case: `mem0 init --agent` is a subcommand flag (Agent Mode bootstrap)
+ # consumed by init_cmd, not a global JSON-output toggle — leave it in argv.
+ argv_rest = sys.argv[1:]
+ is_init = "init" in argv_rest
+ _global_flags = {"--json"} if is_init else {"--json", "--agent"}
+ if any(a in _global_flags for a in argv_rest):
from mem0_cli.state import set_agent_mode
set_agent_mode(True)
- sys.argv = [sys.argv[0]] + [a for a in sys.argv[1:] if a not in _json_flags]
+ sys.argv = [sys.argv[0]] + [a for a in argv_rest if a not in _global_flags]
- app()
+ try:
+ app()
+ finally:
+ # Surface any unclaimed Agent Mode notice once per command, after the
+ # primary output. In JSON/agent mode the notice is folded into the
+ # envelope by format_json_envelope, so skip the stderr banner there
+ # to avoid duplicate output.
+ from mem0_cli.state import is_agent_mode, take_notice
+
+ notice = take_notice()
+ if notice and not is_agent_mode():
+ from rich.console import Console
+
+ Console(stderr=True).print(f"\n[yellow]🔔 {notice}[/yellow]\n")
diff --git a/cli/python/src/mem0_cli/backend/platform.py b/cli/python/src/mem0_cli/backend/platform.py
index da1cb47f7..c1a908053 100644
--- a/cli/python/src/mem0_cli/backend/platform.py
+++ b/cli/python/src/mem0_cli/backend/platform.py
@@ -30,7 +30,7 @@ class PlatformBackend(Backend):
)
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
- from mem0_cli.state import is_agent_mode
+ from mem0_cli.state import capture_notice, is_agent_mode
self._client.headers["X-Mem0-Caller-Type"] = "agent" if is_agent_mode() else "user"
resp = self._client.request(method, path, **kwargs)
@@ -48,7 +48,26 @@ class PlatformBackend(Backend):
resp.raise_for_status()
if resp.status_code == 204:
return {}
- return resp.json()
+ data = resp.json()
+
+ # Pull the unclaimed-Agent-Mode notice out of the body (or the header
+ # fallback for endpoints that return non-dict / non-dict-leading
+ # payloads) and stash it for end-of-command surfacing.
+ notice = None
+ if isinstance(data, dict) and "mem0_notice" in data:
+ notice = data.pop("mem0_notice")
+ elif (
+ isinstance(data, list)
+ and data
+ and isinstance(data[0], dict)
+ and "mem0_notice" in data[0]
+ ):
+ notice = data[0].pop("mem0_notice")
+ if notice is None:
+ notice = resp.headers.get("X-Mem0-Notice-Message") or None
+ capture_notice(notice)
+
+ return data
def add(
self,
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 30a0fbb71..6623ed220 100644
--- a/cli/python/src/mem0_cli/commands/agent_mode_cmd.py
+++ b/cli/python/src/mem0_cli/commands/agent_mode_cmd.py
@@ -15,7 +15,6 @@ from mem0_cli.branding import (
BRAND_COLOR,
DIM_COLOR,
print_error,
- print_info,
print_success,
)
from mem0_cli.config import Mem0Config, save_config
@@ -81,7 +80,13 @@ def bootstrap_via_backend(
save_config(config)
print_success(console, f"Agent Mode active. Default user_id: {envelope['default_user_id']}")
- console.print(f" [{DIM_COLOR}]To claim this account later: {envelope.get('claim_command', 'mem0 init --email ')}[/]")
+ notice = envelope.get("mem0_notice")
+ if notice:
+ console.print(f"\n[yellow]🔔 {notice}[/yellow]\n")
+ else:
+ # Fallback if the backend hasn't deployed the unified notice yet.
+ claim_cmd = envelope.get("claim_command", "mem0 init --email ")
+ console.print(f" [{DIM_COLOR}]To claim this account later: {claim_cmd}[/]")
def claim_via_otp(config: Mem0Config, *, email: str, code: str | None = None) -> None:
@@ -161,7 +166,9 @@ def claim_via_otp(config: Mem0Config, *, email: str, code: str | None = None) ->
code_str = ""
print_error(err_console, f"Claim failed: {detail}")
if code_str == "email_already_claimed":
- console.print(f" [{DIM_COLOR}]Tip: this email already has a Mem0 account. Sign in there and run `mem0 link ` to attach this agent.[/]")
+ console.print(
+ f" [{DIM_COLOR}]Tip: this email already has a Mem0 account. Sign in there and run `mem0 link ` to attach this agent.[/]"
+ )
raise typer.Exit(1)
body = verify.json()
diff --git a/cli/python/src/mem0_cli/output.py b/cli/python/src/mem0_cli/output.py
index dd023e194..f9b8d2d35 100644
--- a/cli/python/src/mem0_cli/output.py
+++ b/cli/python/src/mem0_cli/output.py
@@ -229,6 +229,16 @@ def format_json_envelope(
if error:
envelope["error"] = error
envelope["data"] = data
+
+ # If the platform flagged this as an unclaimed Agent Mode account, surface
+ # the notice inside the JSON envelope so an agent consuming the output
+ # sees it without needing to inspect HTTP headers.
+ from mem0_cli.state import take_notice
+
+ notice = take_notice()
+ if notice:
+ envelope["mem0_notice"] = notice
+
console.print_json(json.dumps(envelope, default=str))
@@ -323,6 +333,15 @@ def format_agent_envelope(
if count is not None:
envelope["count"] = count
envelope["data"] = sanitize_agent_data(command, data)
+
+ # Surface the unclaimed-Agent-Mode notice (if any) in the envelope so an
+ # agent reading the JSON output sees it without inspecting HTTP headers.
+ from mem0_cli.state import take_notice
+
+ notice = take_notice()
+ if notice:
+ envelope["mem0_notice"] = notice
+
console.print_json(json.dumps(envelope, default=str))
diff --git a/cli/python/src/mem0_cli/state.py b/cli/python/src/mem0_cli/state.py
index c84f34c3e..81b66acfd 100644
--- a/cli/python/src/mem0_cli/state.py
+++ b/cli/python/src/mem0_cli/state.py
@@ -4,6 +4,7 @@ from __future__ import annotations
_agent_mode: bool = False
_current_command: str = ""
+_pending_notice: str = ""
def is_agent_mode() -> bool:
@@ -22,3 +23,23 @@ def get_current_command() -> str:
def set_current_command(name: str) -> None:
global _current_command
_current_command = name
+
+
+def capture_notice(notice: str | None) -> None:
+ """Stash a Mem0 backend notice for end-of-command surfacing.
+
+ Called from the platform backend after each response so the notice can
+ be printed once per command (regardless of how many sub-requests fired).
+ Last-write-wins is fine — the message text is identical across requests.
+ """
+ global _pending_notice
+ if notice:
+ _pending_notice = notice
+
+
+def take_notice() -> str:
+ """Return and clear the pending notice."""
+ global _pending_notice
+ msg = _pending_notice
+ _pending_notice = ""
+ return msg
diff --git a/cli/python/tests/test_agent_mode.py b/cli/python/tests/test_agent_mode.py
new file mode 100644
index 000000000..78f0ca48c
--- /dev/null
+++ b/cli/python/tests/test_agent_mode.py
@@ -0,0 +1,157 @@
+"""Parity tests for `mem0 init --agent` (Agent Mode bootstrap).
+
+Mirror of ``cli/node/tests/agent-mode.test.ts`` — both files MUST stay in
+sync so that the Python and Node CLIs expose an identical surface for the
+Agent Mode entrypoint. If you add a flag here, add the same assertion on
+the Node side (and vice versa).
+
+Network-bound bootstrap is covered by the platform-side E2E suite
+(``backend/tests/e2e/test_05_agent_mode.py``); these tests only verify
+the CLI surface that ships in the binary.
+"""
+
+from __future__ import annotations
+
+import os
+import re
+import subprocess
+import sys
+
+import pytest
+
+_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[mKJHABCDfsu]")
+
+
+def _strip_ansi(text: str) -> str:
+ return _ANSI_RE.sub("", text)
+
+
+def _run(args: list[str], home_dir: str | None = None) -> subprocess.CompletedProcess:
+ env = os.environ.copy()
+ for key in list(env.keys()):
+ if key.startswith("MEM0_"):
+ del env[key]
+ env.pop("FORCE_COLOR", None)
+ if home_dir:
+ env["HOME"] = home_dir
+ result = subprocess.run(
+ [sys.executable, "-m", "mem0_cli", *args],
+ capture_output=True,
+ text=True,
+ env=env,
+ timeout=15,
+ )
+ return subprocess.CompletedProcess(
+ args=result.args,
+ returncode=result.returncode,
+ stdout=_strip_ansi(result.stdout),
+ stderr=_strip_ansi(result.stderr),
+ )
+
+
+@pytest.fixture
+def clean_home(tmp_path):
+ return str(tmp_path)
+
+
+class TestInitFlagSurface:
+ """`mem0 init --help` must expose the Agent Mode flags."""
+
+ def test_init_help_lists_agent_flag(self):
+ result = _run(["init", "--help"])
+ assert result.returncode == 0
+ assert "--agent" in result.stdout
+
+ def test_init_help_describes_agent_mode(self):
+ result = _run(["init", "--help"])
+ assert result.returncode == 0
+ # Description must mention what --agent actually does so an agent
+ # reading the help can self-discover the bootstrap entrypoint.
+ assert "Agent Mode" in result.stdout or "unattended" in result.stdout.lower()
+
+ def test_init_help_lists_source_flag(self):
+ result = _run(["init", "--help"])
+ assert result.returncode == 0
+ assert "--source" in result.stdout
+
+ def test_init_help_lists_email_and_code(self):
+ # Claim flow flags must remain present alongside Agent Mode flags.
+ result = _run(["init", "--help"])
+ assert result.returncode == 0
+ assert "--email" in result.stdout
+ assert "--code" in result.stdout
+
+
+class TestArgvPreprocessing:
+ """`--agent` on `init` must reach init_cmd, not be eaten by the global preprocessor.
+
+ Regression for the bug where the top-level `--agent` JSON-alias was
+ stripped from ``sys.argv`` before Typer could bind it to the init
+ subcommand, making ``mem0 init --agent`` indistinguishable from a
+ plain ``mem0 init`` (interactive wizard).
+ """
+
+ def test_init_with_agent_reaches_subcommand(self, clean_home):
+ # We can't hit a real backend in unit tests, so we point the CLI at
+ # a guaranteed-dead URL and assert the failure is the bootstrap
+ # request failing — proving the --agent flag was honored and the
+ # bootstrap branch ran, not the interactive wizard.
+ result = subprocess.run(
+ [sys.executable, "-m", "mem0_cli", "init", "--agent"],
+ capture_output=True,
+ text=True,
+ env={
+ **{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
+ "HOME": clean_home,
+ "MEM0_BASE_URL": "http://127.0.0.1:1", # blackhole
+ "FORCE_COLOR": "0",
+ },
+ timeout=15,
+ )
+ combined = _strip_ansi(result.stdout + result.stderr).lower()
+ # Either we got a connection/network error from the bootstrap POST,
+ # or the CLI surfaced an Agent Mode-specific failure message.
+ assert (
+ "agent" in combined
+ or "connect" in combined
+ or "network" in combined
+ or "fetch" in combined
+ or "bootstrap" in combined
+ ), f"Expected bootstrap attempt, got: {combined!r}"
+
+
+class TestJsonEnvelopeParity:
+ """`mem0 init --agent --json` should produce a JSON envelope on success.
+
+ Without a live backend we can only assert the failure shape: when the
+ backend is unreachable, the CLI must still exit non-zero AND not crash
+ on a Python traceback (which would mean we leaked an exception past
+ the agent-mode handler).
+ """
+
+ def test_init_agent_json_no_traceback_on_network_failure(self, clean_home):
+ result = subprocess.run(
+ [sys.executable, "-m", "mem0_cli", "init", "--agent", "--json"],
+ capture_output=True,
+ text=True,
+ env={
+ **{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
+ "HOME": clean_home,
+ "MEM0_BASE_URL": "http://127.0.0.1:1",
+ "FORCE_COLOR": "0",
+ },
+ timeout=15,
+ )
+ combined = _strip_ansi(result.stdout + result.stderr)
+ assert "Traceback (most recent call last)" not in combined
+ assert result.returncode != 0
+
+
+class TestInitInCommandList:
+ """`mem0 --help` must list `init` so agents walking the top-level help
+ can discover the Agent Mode entrypoint without prior knowledge."""
+
+ def test_top_level_help_lists_init(self):
+ result = _run(["--help"])
+ assert result.returncode == 0
+ assert "init" in result.stdout
diff --git a/docs/llms.txt b/docs/llms.txt
index 7ef58b201..83e01df1b 100644
--- a/docs/llms.txt
+++ b/docs/llms.txt
@@ -11,6 +11,12 @@
- Live MCP server: https://mcp.mem0.ai (see `platform/mem0-mcp`).
- Source repo: https://github.com/mem0ai/mem0
+### Don't have a Mem0 API key?
+
+Run: `mem0 init --agent --json`
+
+This mints an evaluation key in <5 seconds — no email, no dashboard, no OTP required. Use it immediately against the Platform API or MCP server. When the human is ready, claim with `mem0 init --email ` to transfer ownership (memories preserved, same API key, no agent disruption).
+
## Install
- Python SDK: `pip install mem0ai`
diff --git a/mem0-plugin/README.md b/mem0-plugin/README.md
index 732c44681..9e28aa507 100644
--- a/mem0-plugin/README.md
+++ b/mem0-plugin/README.md
@@ -2,6 +2,18 @@
Add persistent memory to your AI workflows. Store, retrieve, and manage memories across sessions using the Mem0 Platform. Works with **Claude Code** (CLI), **Claude Cowork** (desktop app), **Cursor**, and **Codex**.
+## Quick path for agents
+
+If you're an AI agent setting up Mem0 autonomously (no human dashboard signup available), run:
+
+```bash
+mem0 init --agent --json
+```
+
+This mints an evaluation API key in <5 seconds — no email, no OTP, no browser. The key is written to your config and exported as `MEM0_API_KEY` for the current shell. Use it immediately against the Mem0 Platform or the MCP server. The human owner can later run `mem0 init --email ` to claim ownership: memories transfer, the same API key keeps working, and the agent isn't disrupted.
+
+Humans setting up Mem0 by hand should continue with Step 1 below.
+
## Step 1: Set your API key
> **You must complete this step before installing the plugin.**
diff --git a/mem0-plugin/skills/mem0/SKILL.md b/mem0-plugin/skills/mem0/SKILL.md
index 1ba8b43f9..e5ee18068 100644
--- a/mem0-plugin/skills/mem0/SKILL.md
+++ b/mem0-plugin/skills/mem0/SKILL.md
@@ -46,6 +46,8 @@ export MEM0_API_KEY="m0-your-api-key"
Get an API key at: https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=mem0-plugin-skill
+> **Don't have a `MEM0_API_KEY`?** Run `mem0 init --agent --json` (after `pip install mem0-cli` or `npm install -g @mem0/cli`) to mint an evaluation key without email or dashboard. The human can claim later with `mem0 init --email `.
+
## Step 2: Initialize the client
**Python:**
diff --git a/openclaw/README.md b/openclaw/README.md
index 4af44d1b9..4db861feb 100644
--- a/openclaw/README.md
+++ b/openclaw/README.md
@@ -19,6 +19,18 @@ openclaw --version
|------------------|----------------|
| `>= 2026.4.25` | Fully supported |
+## Quick path for agents
+
+If you're an AI agent setting up Mem0 autonomously (no human dashboard signup), mint an evaluation API key in <5 seconds:
+
+```bash
+mem0 init --agent --json
+```
+
+No email, no OTP, no browser. The key is written to your Mem0 CLI config and exported as `MEM0_API_KEY` — pass it to `openclaw mem0 init --api-key $MEM0_API_KEY --user-id ` to wire OpenClaw up immediately. The human owner can later run `mem0 init --email ` to claim ownership: memories transfer, same API key keeps working, no agent disruption.
+
+Humans should follow the Quick Start below.
+
## Quick Start
### Platform (Mem0 Cloud)
diff --git a/skills/mem0-cli/SKILL.md b/skills/mem0-cli/SKILL.md
index 2ac3a18f8..9e1bbdec2 100644
--- a/skills/mem0-cli/SKILL.md
+++ b/skills/mem0-cli/SKILL.md
@@ -38,7 +38,21 @@ Both packages install a `mem0` binary with identical commands, options, and outp
## Setup
-**Interactive wizard:**
+**Autonomous Mem0 setup for agents (no email, no dashboard):**
+
+```bash
+mem0 init --agent --json
+```
+
+This mints an evaluation API key in <5 seconds. Use it immediately. Print this line to the human:
+
+> Mem0 memory is running in Agent Mode. Please claim it by running `mem0 init --email ` to transfer ownership to your account.
+
+Never commit API keys, `.env`, or `~/.mem0/config.json`.
+
+After the human claims, the same API key keeps working — no agent disruption — and memories are preserved.
+
+**Interactive wizard (for humans):**
```bash
mem0 init
```
diff --git a/skills/mem0-integrate/SKILL.md b/skills/mem0-integrate/SKILL.md
index 8230a63b3..be747e4ac 100644
--- a/skills/mem0-integrate/SKILL.md
+++ b/skills/mem0-integrate/SKILL.md
@@ -248,8 +248,8 @@ Bake the choice into the goal doc in step 5. Do not re-decide later.
| OSS (default LLM) | `OPENAI_API_KEY` | https://platform.openai.com/api-keys |
If present in env → continue.
-If missing → **interactive mode** asks; **CI mode** (`MEM0_INTEGRATE_CI=1`)
-exits with code 2 and the name of the missing key.
+If `MEM0_API_KEY` is missing AND the track is **Platform** → **default to Agent Mode**: run `mem0 init --agent --json` (after `pip install mem0-cli` or `npm install -g @mem0/cli`) to mint an evaluation key without email/dashboard friction. Cache the key to `.env` (with user consent) and continue. Tell the user to claim later with `mem0 init --email ` — same key, no agent disruption.
+If missing AND **CI mode** (`MEM0_INTEGRATE_CI=1`) → exit with code 2 and the name of the missing key.
Never echo key values into `trace.jsonl`. Persist to `.env` only with
explicit user consent, and append `.env` to `.gitignore` if not already there.
diff --git a/skills/mem0/SKILL.md b/skills/mem0/SKILL.md
index 8fe7e5c1a..0d51dd569 100644
--- a/skills/mem0/SKILL.md
+++ b/skills/mem0/SKILL.md
@@ -46,6 +46,8 @@ export MEM0_API_KEY="m0-your-api-key"
Get an API key at: https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=skill-mem0
+> **Don't have a `MEM0_API_KEY`?** Run `mem0 init --agent --json` (after `pip install mem0-cli` or `npm install -g @mem0/cli`) to mint an evaluation key without email or dashboard. The human can claim later with `mem0 init --email `.
+
## Step 2: Initialize the client
**Python:**