feat(cli): add email verification code login to mem0 init (#4623)

Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
This commit is contained in:
Kabir Kohli
2026-03-31 04:27:02 -07:00
committed by GitHub
parent dbac83218f
commit af19495f66
7 changed files with 389 additions and 14 deletions
+13
View File
@@ -153,8 +153,19 @@ Interactive setup wizard for mem0 CLI.
|-----------------|--------|----------|---------|------|
| `--api-key` | string | No | - | API key (skip prompt). |
| `-u, --user-id` | string | No | - | Default user ID (skip prompt). |
| `--email` | string | No | - | Login via email verification code. |
| `--code` | string | No | - | Verification code (use with --email for non-interactive login). |
**Behavior:**
*Email login flow (when `--email` is provided):*
- Sends a 6-digit verification code to the email via `POST /api/v1/auth/email_code/`.
- If `--code` is also provided, verifies immediately (fully non-interactive).
- If `--code` is not provided, prompts for the code interactively.
- On success: receives API key, org_id, project_id. Saves to config. Creates account if email is new.
- Cannot be combined with `--api-key`.
*API key flow (existing behavior):*
- If both `--api-key` and `--user-id` are provided, runs non-interactively (no prompts).
- If running in a non-TTY without both flags, prints an error with usage hint and exits.
- Interactive mode: prints banner, prompts for API key (masked with `*`), prompts for default user ID (default: `mem0-cli`), validates connection, saves config.
@@ -164,6 +175,8 @@ Interactive setup wizard for mem0 CLI.
```bash
mem0 init
mem0 init --api-key m0-xxx --user-id alice
mem0 init --email alice@company.com
mem0 init --email alice@company.com --code 482901
```
---
+8 -2
View File
@@ -17,9 +17,15 @@ Both packages install a `mem0` binary with identical behavior.
## Quick start
```bash
# Authenticate and save config
# Interactive setup wizard
mem0 init
# Or login via email (get a new API key)
mem0 init --email alice@company.com
# Or authenticate with an existing API key
mem0 init --api-key m0-xxx
# Add a memory
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
@@ -40,7 +46,7 @@ mem0 delete <memory-id>
| Command | Description |
|---------|-------------|
| `mem0 init` | Interactive setup wizard — configures API key and default user ID |
| `mem0 init` | Setup wizard — login via email or configure API key manually |
| `mem0 add` | Add a memory from text, JSON messages, a file, or stdin |
| `mem0 search` | Search memories using natural language |
| `mem0 list` | List memories with optional filters and pagination |
+8 -3
View File
@@ -503,15 +503,20 @@
},
{
"name": "init",
"description": "Interactive setup wizard for mem0 CLI.",
"usage": "mem0 init",
"description": "Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
"usage": "mem0 init [OPTIONS]",
"needsBackend": false,
"needsConfig": false,
"resolveIds": false,
"resolveGraph": false,
"confirmDangerous": false,
"arguments": [],
"options": []
"options": [
{ "name": "api-key", "flags": ["--api-key"], "type": "string", "default": null, "help": "API key (skip prompt)." },
{ "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": "status",
+177 -2
View File
@@ -10,11 +10,94 @@ import {
printSuccess,
colors,
} from "../branding.js";
import { type Mem0Config, createDefaultConfig, saveConfig } from "../config.js";
import { type Mem0Config, createDefaultConfig, loadConfig, saveConfig, DEFAULT_BASE_URL } from "../config.js";
import { PlatformBackend } from "../backend/platform.js";
const { brand, dim } = colors;
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
function validateEmail(email: string): void {
if (!EMAIL_RE.test(email)) {
printError(`Invalid email address: ${JSON.stringify(email)}`);
process.exit(1);
}
}
async function emailLogin(
email: string,
code: string | undefined,
baseUrl: string,
): Promise<Record<string, unknown>> {
const url = baseUrl.replace(/\/+$/, "");
if (!code) {
const resp = await fetch(`${url}/api/v1/auth/email_code/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(30_000),
});
if (resp.status === 429) {
printError("Too many attempts. Try again in a few minutes.");
process.exit(1);
}
if (!resp.ok) {
let detail: string;
try {
const body = (await resp.json()) as Record<string, unknown>;
detail = (body["error"] ?? body["detail"] ?? resp.statusText) as string;
} catch {
detail = resp.statusText;
}
printError(`Failed to send code: ${detail}`);
process.exit(1);
}
printSuccess("Verification code sent! Check your email.");
if (!process.stdin.isTTY) {
printError(
"No --code provided and terminal is non-interactive.",
"Run: mem0 init --email <email> --code <code>",
);
process.exit(1);
}
console.log();
const entered = await promptLine(` ${brand("Verification Code")}`);
if (!entered) {
printError("Code is required.");
process.exit(1);
}
code = entered;
}
const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, code: code.trim() }),
signal: AbortSignal.timeout(30_000),
});
if (verifyResp.status === 429) {
printError("Too many attempts. Try again in a few minutes.");
process.exit(1);
}
if (!verifyResp.ok) {
let detail: string;
try {
const body = (await verifyResp.json()) as Record<string, unknown>;
detail = (body["error"] ?? body["detail"] ?? verifyResp.statusText) as string;
} catch {
detail = verifyResp.statusText;
}
printError(`Verification failed: ${detail}`);
process.exit(1);
}
return verifyResp.json() as Promise<Record<string, unknown>>;
}
function promptSecret(label: string): Promise<string> {
return new Promise((resolve, reject) => {
process.stdout.write(label);
@@ -124,8 +207,57 @@ async function validatePlatform(config: Mem0Config): Promise<void> {
}
}
export async function runInit(opts: { apiKey?: string; userId?: string } = {}): Promise<void> {
export async function runInit(
opts: { apiKey?: string; userId?: string; email?: string; code?: string } = {},
): Promise<void> {
const config = createDefaultConfig();
const savedConfig = loadConfig();
const baseUrl =
process.env["MEM0_BASE_URL"] || savedConfig.platform.baseUrl || DEFAULT_BASE_URL;
// Guards
if (opts.code && !opts.email) {
printError("--code requires --email.");
process.exit(1);
}
if (opts.email && opts.apiKey) {
printError("Cannot use both --api-key and --email.");
process.exit(1);
}
// ── Email login flow ──────────────────────────────────────────────────────
if (opts.email) {
const email = opts.email.trim().toLowerCase();
validateEmail(email);
printBanner();
console.log();
printInfo(`Logging in as ${email}...\n`);
const result = await emailLogin(email, opts.code, baseUrl);
const apiKeyVal = result["api_key"] as string | undefined;
if (!apiKeyVal) {
printError("Auth succeeded but no API key was returned. Contact support.");
process.exit(1);
}
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli";
saveConfig(config);
console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
console.log();
return;
}
// ── API key flow ──────────────────────────────────────────────────────────
// Non-interactive: both flags provided
if (opts.apiKey && opts.userId) {
@@ -154,6 +286,49 @@ export async function runInit(opts: { apiKey?: string; userId?: string } = {}):
if (opts.apiKey) {
config.platform.apiKey = opts.apiKey;
} else {
console.log(` ${brand("How would you like to authenticate?")}`);
console.log(` ${dim("1.")} Login with email ${dim("(recommended)")}`);
console.log(` ${dim("2.")} Enter API key manually`);
console.log();
const choice = await promptLine(` ${brand("Choose")} [1/2]`, "1");
if (choice === "1") {
console.log();
const emailAddr = await promptLine(` ${brand("Email")}`);
if (!emailAddr) {
printError("Email is required.");
process.exit(1);
}
const email = emailAddr.trim().toLowerCase();
validateEmail(email);
printInfo(`Logging in as ${email}...\n`);
const result = await emailLogin(email, undefined, baseUrl);
const apiKeyVal = result["api_key"] as string | undefined;
if (!apiKeyVal) {
printError("Auth succeeded but no API key was returned. Contact support.");
process.exit(1);
}
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli";
saveConfig(config);
console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
console.log();
return;
}
// choice === "2": fall through to API key prompt
await setupPlatform(config);
}
+8 -3
View File
@@ -110,13 +110,18 @@ program
program
.command("init")
.description("Interactive setup wizard for mem0 CLI.")
.description("Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).")
.option("--api-key <key>", "API key (skip prompt).")
.option("-u, --user-id <id>", "Default user ID (skip prompt).")
.addHelpText("after", "\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice")
.option("--email <email>", "Login via email verification code.")
.option("--code <code>", "Verification code (use with --email for non-interactive login).")
.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",
)
.action(async (opts) => {
const { runInit } = await import("./commands/init.js");
await runInit({ apiKey: opts.apiKey, userId: opts.userId });
await runInit({ apiKey: opts.apiKey, userId: opts.userId, email: opts.email, code: opts.code });
});
// ── Memory: add ───────────────────────────────────────────────────────────
+5 -1
View File
@@ -695,16 +695,20 @@ app.add_typer(entity_app, name="entity", rich_help_panel="Management")
def init(
api_key: str | None = typer.Option(None, "--api-key", help="API key (skip prompt)."),
user_id: str | None = typer.Option(None, "--user-id", "-u", help="Default user ID (skip prompt)."),
email: str | None = typer.Option(None, "--email", help="Login via email verification code."),
code: str | None = typer.Option(None, "--code", help="Verification code (use with --email for non-interactive login)."),
) -> None:
"""Interactive setup wizard for mem0 CLI.
Examples:
mem0 init
mem0 init --api-key m0-xxx --user-id alice
mem0 init --email alice@company.com
mem0 init --email alice@company.com --code 482901
"""
from mem0_cli.commands.init_cmd import run_init
run_init(api_key=api_key, user_id=user_id)
run_init(api_key=api_key, user_id=user_id, email=email, code=code)
# (entity_app registered at module level, below sub-group definitions)
+170 -3
View File
@@ -2,8 +2,11 @@
from __future__ import annotations
import os
import re
import sys
import httpx
import typer
from rich.console import Console
from rich.prompt import Prompt
@@ -16,7 +19,7 @@ from mem0_cli.branding import (
print_info,
print_success,
)
from mem0_cli.config import Mem0Config, save_config
from mem0_cli.config import DEFAULT_BASE_URL, Mem0Config, save_config
console = Console()
err_console = Console(stderr=True)
@@ -84,7 +87,89 @@ def _prompt_secret(label: str) -> str:
return "".join(chars)
def run_init(*, api_key: str | None = None, user_id: str | None = None) -> None:
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
def _validate_email(email: str) -> None:
"""Exit with an error if *email* doesn't look like a valid address."""
if not _EMAIL_RE.match(email):
print_error(err_console, f"Invalid email address: {email!r}")
raise typer.Exit(1)
def _email_login(
email: str,
code: str | None,
base_url: str,
) -> dict:
"""Run the email verification code login flow.
Returns the parsed JSON response from the verify endpoint.
The caller expects at minimum an ``api_key`` field.
"""
url = base_url.rstrip("/")
with httpx.Client(timeout=30.0) as client:
# If code is already provided, skip sending — user already has a code
if not code:
# Step 1: Request verification code
resp = client.post(
f"{url}/api/v1/auth/email_code/",
json={"email": email},
)
if resp.status_code == 429:
print_error(err_console, "Too many attempts. Try again in a few minutes.")
raise typer.Exit(1)
if resp.status_code != 200:
try:
detail = resp.json().get("error", resp.text)
except Exception:
detail = resp.text
print_error(err_console, f"Failed to send code: {detail}")
raise typer.Exit(1)
print_success(console, "Verification code sent! Check your email.")
# Step 2: Get code from user
if not sys.stdin.isatty():
print_error(
err_console,
"No --code provided and terminal is non-interactive.",
hint="Run: mem0 init --email <email> --code <code>",
)
raise typer.Exit(1)
console.print()
code = Prompt.ask(f" [{BRAND_COLOR}]Verification Code[/]")
if not code:
print_error(err_console, "Code is required.")
raise typer.Exit(1)
# Step 3: Verify code
resp = client.post(
f"{url}/api/v1/auth/email_code/verify/",
json={"email": email, "code": code.strip()},
)
if resp.status_code == 429:
print_error(err_console, "Too many attempts. Try again in a few minutes.")
raise typer.Exit(1)
if resp.status_code != 200:
try:
detail = resp.json().get("error", resp.text)
except Exception:
detail = resp.text
print_error(err_console, f"Verification failed: {detail}")
raise typer.Exit(1)
return resp.json()
def run_init(
*,
api_key: str | None = None,
user_id: str | None = None,
email: str | None = None,
code: str | None = None,
) -> None:
"""Interactive setup wizard for mem0 CLI.
When both *api_key* and *user_id* are supplied, all prompts are skipped
@@ -93,6 +178,48 @@ def run_init(*, api_key: str | None = None, user_id: str | None = None) -> None:
"""
config = Mem0Config()
base_url = os.environ.get("MEM0_BASE_URL", config.platform.base_url or DEFAULT_BASE_URL)
if code and not email:
print_error(err_console, "--code requires --email.")
raise typer.Exit(1)
# ── Email login flow ──────────────────────────────────────────────
if email:
if api_key:
print_error(err_console, "Cannot use both --api-key and --email.")
raise typer.Exit(1)
email = email.strip().lower()
_validate_email(email)
print_banner(console)
console.print()
print_info(console, f"Logging in as {email}...\n")
result = _email_login(email, code, base_url)
api_key_val = result.get("api_key")
if not api_key_val:
print_error(err_console, "Auth succeeded but no API key was returned. Contact support.")
raise typer.Exit(1)
config.platform.api_key = api_key_val
config.platform.base_url = base_url
config.defaults.user_id = user_id or "mem0-cli"
save_config(config)
console.print()
print_success(console, "Authenticated! Configuration saved to ~/.mem0/config.json")
console.print()
console.print(f" [{DIM_COLOR}]Get started:[/]")
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode"[/]')
console.print(f' [{DIM_COLOR}] mem0 search "preferences"[/]')
console.print()
return
# ── API key flow (existing) ───────────────────────────────────────
# Fully non-interactive when both flags provided
if api_key and user_id:
config.platform.api_key = api_key
@@ -115,7 +242,47 @@ def run_init(*, api_key: str | None = None, user_id: str | None = None) -> None:
console.print()
print_info(console, "Welcome! Let's set up your mem0 CLI.\n")
# Use provided flags or prompt
# If no flags at all, ask user how they want to authenticate
if not api_key:
console.print(f" [{BRAND_COLOR}]How would you like to authenticate?[/]")
console.print(f" [{DIM_COLOR}]1.[/] Login with email [{DIM_COLOR}](recommended)[/]")
console.print(f" [{DIM_COLOR}]2.[/] Enter API key manually")
console.print()
choice = Prompt.ask(f" [{BRAND_COLOR}]Choose[/]", choices=["1", "2"], default="1")
if choice == "1":
console.print()
email_addr = Prompt.ask(f" [{BRAND_COLOR}]Email[/]")
if not email_addr:
print_error(err_console, "Email is required.")
raise typer.Exit(1)
email_addr = email_addr.strip().lower()
_validate_email(email_addr)
print_info(console, f"Logging in as {email_addr}...\n")
result = _email_login(email_addr, None, base_url)
api_key_val = result.get("api_key")
if not api_key_val:
print_error(err_console, "Auth succeeded but no API key was returned. Contact support.")
raise typer.Exit(1)
config.platform.api_key = api_key_val
config.platform.base_url = base_url
config.defaults.user_id = user_id or "mem0-cli"
save_config(config)
console.print()
print_success(console, "Authenticated! Configuration saved to ~/.mem0/config.json")
console.print()
console.print(f" [{DIM_COLOR}]Get started:[/]")
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode"[/]')
console.print(f' [{DIM_COLOR}] mem0 search "preferences"[/]')
console.print()
return
# API key flow
if api_key:
config.platform.api_key = api_key
else: