Adds a one-shot subcommand the agent runs when it bootstrapped without
--agent-caller: `mem0 identify claude-code` PATCHes the active key's
agent_caller via /api/v1/auth/agent_mode/caller/, then mirrors the
canonical value into ~/.mem0/config.json.
Bootstrap success message now prompts unidentified agents to run it.
If --agent-caller was passed on init, no prompt (already attributed).
Both runtimes (Python + Node) get the command + the prompt update.
Switch agent identity from env-var sniffing to explicit self-declaration
(Proof Editor-style). The agent now passes its own name when running
mem0 init --agent --agent-caller <name>.
Why: env-var detection was speculative for everything but Claude Code —
CLAUDECODE=1 is documented and verified, but CURSOR_AGENT, CODEX_CLI,
CLINE_AGENT etc. were plausible-sounding picks without upstream
confirmation. Self-declaration is honest, future-proof (no whitelist
treadmill as new agents emerge), and matches how Proof handles agent
join: explicit identity from the agent itself.
CLI changes:
- New --agent-caller <name> option on `mem0 init` (Python + Node)
- Removed detect_agent_caller() as the source for agent_caller field;
still used as a context trigger ("does this look like an agent?")
for Rule 3 auto-bootstrap and for telemetry-only event property —
identity goes to NULL unless --agent-caller is passed
- runInit / run_init accept agent_caller kwarg, forward to backend
- PostHog cli.init event uses the self-declared value (not env-sniffed)
Companion backend change (mem0ai/platform#2784) drops the whitelist
normalizer for an open sanitize so any sensible name is accepted.
The CLI's detect_agent_caller() already canonicalizes the caller (env-var
sniff returning "claude-code", "cursor", etc.) and reports it to PostHog —
but the value never reached the backend, so APIKey.agent_caller stayed
NULL. Join: send it in the bootstrap request body so the platform can
persist it (see mem0ai/platform#2784).
Wire-up:
- bootstrap_via_backend / bootstrapViaBackend gain an agent_caller kwarg,
pass it as request body field "agent_caller"
- init_cmd / init.ts call detect_agent_caller() once and forward
- platform.agent_caller persisted to ~/.mem0/config.json so the local
view matches what the backend stored
No change to created_via — that stays the channel enum ("agent_mode" /
"email" / "api_key"). agent_caller is the orthogonal "who started this".
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).
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).
When saveConfig writes a fresh api_key (e.g. agent-mode bootstrap, OTP
signup), propagate the value into other ecosystem locations that hold
the same key:
- ~/.claude/settings.json::env::MEM0_API_KEY (Claude Code env injection)
- ~/.zshrc / ~/.bashrc / ~/.bash_profile `export MEM0_API_KEY="..."`
Without this, agent-mode bootstrap mints a new shadow into config.json
but the Claude plugin's MCP server keeps using the OLD env-var key —
silent surprise.
Hard guarantees:
1. **Update-only**, never create. If a target file doesn't already
contain a MEM0_API_KEY entry, we leave it alone. The user's
existing setup decides which surfaces are managed; we don't
unilaterally start writing to new files.
2. **Preserve surrounding content.** JSON files keep all other keys.
Shell rc files keep all other lines, comments, and the trailing
newline (regex uses [ \t]* not \s*, which would eat the final \n
when MEM0_API_KEY is the last line of .zshrc).
3. **Atomic writes.** tmpfile + rename, so a crash mid-write leaves
the original intact.
4. **Idempotent.** If the target already has this value, no-op.
5. **Best-effort.** Any IOError in the sync is swallowed; the
canonical config.json write is never blocked by plugin-state.
Implemented identically in Python (plugin_sync.py) and Node
(plugin-sync.ts). Hooked into save_config() / saveConfig() so every
api_key change propagates without any caller plumbing.
Verified on a sandbox copy of real ~/.claude/settings.json and
~/.zshrc: only the MEM0_API_KEY values changed; all 36 other lines
in settings.json and 50+ lines in .zshrc preserved byte-for-byte
including the trailing newline.
Out of scope (deliberate non-changes):
- ~/.codex/config.toml — no mem0 server entry to update
- ~/.cursor/mcp.json — no mem0 server entry to update
- <plugin-install-dir>/.api_key — plugin-managed, different schema
Before this change, `mem0 init --agent` always minted a fresh shadow,
even when a valid MEM0_API_KEY env var (set by the Claude plugin or
shell rc) was already in place. The env var would then silently shadow
the freshly-minted shadow key on the next CLI call — leading to the
surprising "Agent Mode active but my personal account does the work"
behaviour.
Dev Spec §C4 already specifies the right precedence:
Rule 1: env MEM0_API_KEY valid → reuse, emit existing_key, no new key
Rule 2: config api_key valid → reuse, emit existing_key
Rule 3: mint a fresh shadow
This commit implements rules 1-3 in both runtimes (Python + Node), with
a 5-second ping to validate candidate keys against /v1/ping/.
Also reorders so the Agent Mode branch runs BEFORE the existing-config
overwrite guard. The guard's intent is "warn before overwriting a valid
key" — but rules 1/2 REUSE (not overwrite) a valid key, so the guard
must not fire on the agent path.
Net effect: the plugin's MEM0_API_KEY env var and the CLI's
config.json::platform.api_key now naturally co-exist:
- User installs plugin → MEM0_API_KEY set, persisted via shell rc
- User runs `mem0 init --agent` → rule 1 fires → existing key kept,
no shadow created, no confusion
- Plugin's MCP server (which reads ${MEM0_API_KEY}) and the CLI use
the same key
No new shadow accounts on prod from CI / repeat invocations.
External sandbox audit (Sandbox E2E Test Report 2026-05-13) surfaced
two CLI issues.
1. Node — `mem0 init --agent` silently falls through to the non-TTY
error when no agent env var is set. Commander resolves the
program-level `--agent` alias (for --json) before init's own
`--agent`; init's opts.agent stays false. Fix: enable Commander's
positional options so flags AFTER a subcommand belong to that
subcommand. Now `mem0 --agent <cmd>` is the JSON-output alias and
`mem0 init --agent` is the Agent Mode bootstrap flag.
The Python side already had an argv preprocessor doing the
equivalent; this brings Node to parity using Commander's built-in
mechanism (cleaner than mirroring the preprocessor).
Verified: `mem0 init --agent` with bogus MEM0_BASE_URL now hits the
bootstrap fetch and surfaces a network error (proving it reached
bootstrap_via_backend), where before it printed "Non-interactive
terminal detected" and never made an HTTP call.
2. Both — `config.platform.created_via` was inconsistent: set to
"agent_mode" / "email" on the agent/claim paths but left empty on
normal email signup and api-key paths. Now all four paths set it:
"agent_mode" (bootstrap), "email" (OTP signup or claim), "api_key"
(--api-key flag or interactive prompt). Downstream consumers can
reliably filter on created_via==value.
Backend error details can contain `[/...]` or other rich-markup-like text
(e.g. regex patterns in validation errors). print_error interpolated the
raw string into a markup template, so rich's parser would crash with
MarkupError instead of showing the actual error to the user.
Escape message + hint via rich.markup.escape so the colored prefix
still renders but the user-supplied content prints literally.
CLI now consumes the unified mem0_notice surface that the platform side
emits for unclaimed Agent Mode keys. The notice is a directive to the
LLM agent reading the output, with a verbatim sentence to relay to the
human owner. Two presentation paths:
- Human/text output: yellow stderr banner after the primary output,
once per command. Skipped in agent mode (the JSON envelope carries
it instead, so no duplication).
- JSON/agent output (--json/--agent): folded into the envelope as
"mem0_notice" so an agent parsing the output sees it without
inspecting HTTP headers.
CLI changes (Python + Node, kept in lockstep):
- state.{ts,py}: captureNotice / takeNotice helpers — last-write-wins
stash so multi-request commands fire the notice exactly once.
- backend/platform.{ts,py}: _request extracts notice from response
bodies (top-level dict or list[0]) with header fallback, strips
from downstream payload, captures for end-of-command surfacing.
- output.{ts,py}: JSON envelope formatters fold in any pending notice.
- index.ts / app.py: entrypoint surfaces notice on exit when not in
agent mode.
- commands/agent-mode.{ts,py}: init success path prints the platform's
notice verbatim (fallback to dim claim-command line if a stale
backend doesn't return it).
Init-flag handling fix: the Python argv preprocessor was stripping
--agent from sys.argv unconditionally as the global JSON-output alias.
That swallowed `mem0 init --agent` (where --agent is a subcommand flag
for unattended bootstrap). Now preserved when "init" is in argv.
Parity tests: cli/python/tests/test_agent_mode.py and
cli/node/tests/agent-mode.test.ts — 7 tests each, kept in sync.
cli-spec.json updated: init now lists --agent and --source.
Docs:
- README.md: Agent Mode promo at top of Quickstart.
- docs/llms.txt: fast-path block for AI agents reading the docs.
- skills/mem0/SKILL.md, skills/mem0-cli/SKILL.md,
skills/mem0-integrate/SKILL.md, mem0-plugin/skills/mem0/SKILL.md:
autonomous-setup section + fallback hints.
- mem0-plugin/README.md, openclaw/README.md: "Quick path for agents"
blocks above the human Quick Start.
Replace claim_via_device_flow / claimViaDeviceFlow with claim_via_otp /
claimViaOtp. The new flow:
1. POST /api/v1/auth/email_code/ with the user's email
2. Prompt for the verification code (or accept via --code for non-TTY)
3. POST /.../verify/ with {email, code, agent_mode_api_key: <local key>}
4. Backend's verify_email_code runs upgrade-in-place inline and returns
{claimed: true, claimed_at, ...}
No browser open, no localhost:3000 frontend dependency, no 10-minute poll
loop. Just two HTTP calls + an OTP prompt. Same upgrade-in-place
semantics on the backend; same key-value-unchanged guarantee for the
caller.
--code flag still supported on `mem0 init --email` for non-interactive
use (CI, agent-driven claim scripts).
New behavior on `mem0 init`:
- With no `--email`/`--api-key` AND a positive agent signal (`--agent`,
global `--json`/`--agent`, or one of the recognized agent env vars
CLAUDECODE / CURSOR_AGENT / CODEX_CLI / CLINE / CONTINUE / AIDER /
GOOSE / WINDSURF), bootstrap an unattended Agent Mode account via
POST /api/v1/auth/agent_mode/. No email, no OTP, no dashboard.
- With `--email <addr>` AND an existing config that has agent_mode=true,
run the claim device-flow against the existing key instead of minting
a fresh one. The raw API key never leaves the device; backend confirms
claim via the existing CLILoginRequest poll path. Config flips
agent_mode=false and stamps claimed_at on success.
- Bare `mem0 init` with no signal + no TTY still errors out — auto-bootstrap
requires a positive agent signal to avoid surprising pipe-using humans.
New flags:
--agent Force unattended Agent Mode bootstrap.
--source Channel attribution string for signup_source PostHog property.
Config schema extensions on PlatformConfig:
agent_mode, created_via, claimed_at, default_user_id.
Telemetry (M1-M6 from the growth doc):
- cli.init: mode (agent|email|api_key|existing_key), agent_caller,
signup_source, claimed_agent_mode (bool when --email claims an
existing agent-mode config).
- All cli.* events: agent_mode reflects config.platform.agent_mode (the
bootstrap flag), not the output-format flag — per the growth-doc spec.