feat(cli): add email verification code login to mem0 init (#4623)
Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
This commit is contained in:
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user