feat(cli): add email verification code login to mem0 init (#4623)
Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
This commit is contained in:
co-authored by
Saket Aryan
parent
dbac83218f
commit
af19495f66
@@ -153,8 +153,19 @@ Interactive setup wizard for mem0 CLI.
|
|||||||
|-----------------|--------|----------|---------|------|
|
|-----------------|--------|----------|---------|------|
|
||||||
| `--api-key` | string | No | - | API key (skip prompt). |
|
| `--api-key` | string | No | - | API key (skip prompt). |
|
||||||
| `-u, --user-id` | string | No | - | Default user ID (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:**
|
**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 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.
|
- 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.
|
- 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
|
```bash
|
||||||
mem0 init
|
mem0 init
|
||||||
mem0 init --api-key m0-xxx --user-id alice
|
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
|
## Quick start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Authenticate and save config
|
# Interactive setup wizard
|
||||||
mem0 init
|
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
|
# Add a memory
|
||||||
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
||||||
|
|
||||||
@@ -40,7 +46,7 @@ mem0 delete <memory-id>
|
|||||||
|
|
||||||
| Command | Description |
|
| 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 add` | Add a memory from text, JSON messages, a file, or stdin |
|
||||||
| `mem0 search` | Search memories using natural language |
|
| `mem0 search` | Search memories using natural language |
|
||||||
| `mem0 list` | List memories with optional filters and pagination |
|
| `mem0 list` | List memories with optional filters and pagination |
|
||||||
|
|||||||
+8
-3
@@ -503,15 +503,20 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "init",
|
"name": "init",
|
||||||
"description": "Interactive setup wizard for mem0 CLI.",
|
"description": "Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
|
||||||
"usage": "mem0 init",
|
"usage": "mem0 init [OPTIONS]",
|
||||||
"needsBackend": false,
|
"needsBackend": false,
|
||||||
"needsConfig": false,
|
"needsConfig": false,
|
||||||
"resolveIds": false,
|
"resolveIds": false,
|
||||||
"resolveGraph": false,
|
"resolveGraph": false,
|
||||||
"confirmDangerous": false,
|
"confirmDangerous": false,
|
||||||
"arguments": [],
|
"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",
|
"name": "status",
|
||||||
|
|||||||
@@ -10,11 +10,94 @@ import {
|
|||||||
printSuccess,
|
printSuccess,
|
||||||
colors,
|
colors,
|
||||||
} from "../branding.js";
|
} 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";
|
import { PlatformBackend } from "../backend/platform.js";
|
||||||
|
|
||||||
const { brand, dim } = colors;
|
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> {
|
function promptSecret(label: string): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
process.stdout.write(label);
|
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 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
|
// Non-interactive: both flags provided
|
||||||
if (opts.apiKey && opts.userId) {
|
if (opts.apiKey && opts.userId) {
|
||||||
@@ -154,6 +286,49 @@ export async function runInit(opts: { apiKey?: string; userId?: string } = {}):
|
|||||||
if (opts.apiKey) {
|
if (opts.apiKey) {
|
||||||
config.platform.apiKey = opts.apiKey;
|
config.platform.apiKey = opts.apiKey;
|
||||||
} else {
|
} 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);
|
await setupPlatform(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,13 +110,18 @@ program
|
|||||||
|
|
||||||
program
|
program
|
||||||
.command("init")
|
.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("--api-key <key>", "API key (skip prompt).")
|
||||||
.option("-u, --user-id <id>", "Default user ID (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) => {
|
.action(async (opts) => {
|
||||||
const { runInit } = await import("./commands/init.js");
|
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 ───────────────────────────────────────────────────────────
|
// ── Memory: add ───────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -695,16 +695,20 @@ app.add_typer(entity_app, name="entity", rich_help_panel="Management")
|
|||||||
def init(
|
def init(
|
||||||
api_key: str | None = typer.Option(None, "--api-key", help="API key (skip prompt)."),
|
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)."),
|
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:
|
) -> None:
|
||||||
"""Interactive setup wizard for mem0 CLI.
|
"""Interactive setup wizard for mem0 CLI.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
mem0 init
|
mem0 init
|
||||||
mem0 init --api-key m0-xxx --user-id alice
|
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
|
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)
|
# (entity_app registered at module level, below sub-group definitions)
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
import httpx
|
||||||
import typer
|
import typer
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.prompt import Prompt
|
from rich.prompt import Prompt
|
||||||
@@ -16,7 +19,7 @@ from mem0_cli.branding import (
|
|||||||
print_info,
|
print_info,
|
||||||
print_success,
|
print_success,
|
||||||
)
|
)
|
||||||
from mem0_cli.config import Mem0Config, save_config
|
from mem0_cli.config import DEFAULT_BASE_URL, Mem0Config, save_config
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
err_console = Console(stderr=True)
|
err_console = Console(stderr=True)
|
||||||
@@ -84,7 +87,89 @@ def _prompt_secret(label: str) -> str:
|
|||||||
return "".join(chars)
|
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.
|
"""Interactive setup wizard for mem0 CLI.
|
||||||
|
|
||||||
When both *api_key* and *user_id* are supplied, all prompts are skipped
|
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()
|
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
|
# Fully non-interactive when both flags provided
|
||||||
if api_key and user_id:
|
if api_key and user_id:
|
||||||
config.platform.api_key = api_key
|
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()
|
console.print()
|
||||||
print_info(console, "Welcome! Let's set up your mem0 CLI.\n")
|
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:
|
if api_key:
|
||||||
config.platform.api_key = api_key
|
config.platform.api_key = api_key
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user