diff --git a/cli/node/src/commands/agent-mode.ts b/cli/node/src/commands/agent-mode.ts index 727f88181..581be997b 100644 --- a/cli/node/src/commands/agent-mode.ts +++ b/cli/node/src/commands/agent-mode.ts @@ -110,6 +110,12 @@ export async function bootstrapViaBackend( const claimCmd = envelope.claim_command ?? "mem0 init --email "; console.log(` ${dim(`To claim this account later: ${claimCmd}`)}`); } + + if (!agentCaller) { + console.log( + ` ${dim("If you are an AI agent, identify yourself: `mem0 identify ` (e.g. claude-code, cursor).")}`, + ); + } } /** diff --git a/cli/node/src/commands/identify.ts b/cli/node/src/commands/identify.ts new file mode 100644 index 000000000..978247171 --- /dev/null +++ b/cli/node/src/commands/identify.ts @@ -0,0 +1,75 @@ +/** + * mem0 identify — declare which agent owns the current agent-mode key. + * + * Used when `mem0 init --agent` ran without --agent-caller, so the backend + * saved agent_caller=NULL. The agent re-runs `mem0 identify ` to PATCH + * its own row with its real identity. Idempotent. + */ + +import { printError, printSuccess } from "../branding.js"; +import { loadConfig, saveConfig } from "../config.js"; + +const SOURCE_HEADERS = { + "X-Mem0-Source": "cli", + "X-Mem0-Client-Language": "node", +} as const; + +export async function runIdentify(name: string): Promise { + const config = loadConfig(); + if (!config.platform.apiKey) { + printError("No API key configured. Run `mem0 init --agent` first."); + process.exit(1); + } + if (!config.platform.agentMode) { + printError("This command only works on unclaimed agent-mode keys."); + process.exit(1); + } + + const clean = (name ?? "").trim(); + if (!clean) { + printError("Agent name is required."); + process.exit(1); + } + + const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace( + /\/+$/, + "", + ); + + let resp: Response; + try { + resp = await fetch(`${baseUrl}/api/v1/auth/agent_mode/caller/`, { + method: "PATCH", + headers: { + ...SOURCE_HEADERS, + Authorization: `Token ${config.platform.apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ agent_caller: clean }), + signal: AbortSignal.timeout(30_000), + }); + } catch (err) { + printError( + `Network error: ${err instanceof Error ? err.message : String(err)}`, + ); + process.exit(1); + } + + if (!resp.ok) { + let detail: string = resp.statusText; + try { + const body = (await resp.json()) as { error?: string }; + if (body.error) detail = body.error; + } catch { + /* leave as statusText */ + } + printError(`Identify failed: ${detail}`); + process.exit(1); + } + + const body = (await resp.json()) as { agent_caller?: string }; + const canonical = body.agent_caller ?? clean; + config.platform.agentCaller = canonical; + saveConfig(config); + printSuccess(`Identified as ${canonical}.`); +} diff --git a/cli/node/src/index.ts b/cli/node/src/index.ts index 9f4ed3431..219c63b1e 100644 --- a/cli/node/src/index.ts +++ b/cli/node/src/index.ts @@ -250,6 +250,18 @@ program }); }); +// ── Setup: identify (post-bootstrap agent self-tag) ────────────────────── + +program + .command("identify ") + .description( + "Tag your active Agent Mode key with the AI agent that's using it (e.g. claude-code, cursor).", + ) + .action(async (name: string) => { + const { runIdentify } = await import("./commands/identify.js"); + await runIdentify(name); + }); + // ── Memory: add ─────────────────────────────────────────────────────────── program diff --git a/cli/python/src/mem0_cli/app.py b/cli/python/src/mem0_cli/app.py index 59f314918..b115164e9 100644 --- a/cli/python/src/mem0_cli/app.py +++ b/cli/python/src/mem0_cli/app.py @@ -897,6 +897,23 @@ def init( ) +@app.command(rich_help_panel="Setup") +def identify( + name: str = typer.Argument(..., help="Agent identity (e.g. claude-code, cursor, my-bot)."), +) -> None: + """Tag your active Agent Mode key with the AI agent that's using it. + + Run this once after `mem0 init --agent` if you didn't pass --agent-caller. + Idempotent — re-running just overwrites the value. + + Example: + mem0 identify claude-code + """ + from mem0_cli.commands.identify_cmd import run_identify + + run_identify(name) + + # (entity_app registered at module level, below sub-group definitions) 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 a6f41ff5a..e88aeb9ce 100644 --- a/cli/python/src/mem0_cli/commands/agent_mode_cmd.py +++ b/cli/python/src/mem0_cli/commands/agent_mode_cmd.py @@ -109,6 +109,12 @@ def bootstrap_via_backend( claim_cmd = envelope.get("claim_command", "mem0 init --email ") console.print(f" [{DIM_COLOR}]To claim this account later: {claim_cmd}[/]") + if not agent_caller: + console.print( + f" [{DIM_COLOR}]If you are an AI agent, identify yourself: " + f"`mem0 identify ` (e.g. claude-code, cursor).[/]" + ) + def claim_via_otp(config: Mem0Config, *, email: str, code: str | None = None) -> None: """Claim an existing Agent Mode account via OTP — no browser, no polling. diff --git a/cli/python/src/mem0_cli/commands/identify_cmd.py b/cli/python/src/mem0_cli/commands/identify_cmd.py new file mode 100644 index 000000000..bc2d41137 --- /dev/null +++ b/cli/python/src/mem0_cli/commands/identify_cmd.py @@ -0,0 +1,75 @@ +"""mem0 identify — declare which agent owns the current agent-mode key. + +Used when `mem0 init --agent` ran without --agent-caller, so the backend +saved agent_caller=NULL. The agent re-runs `mem0 identify ` to PATCH +its own row with its real identity. Idempotent — running it again just +overwrites. +""" + +from __future__ import annotations + +import httpx +import typer +from rich.console import Console + +from mem0_cli.branding import print_error, print_success +from mem0_cli.config import load_config, save_config + +console = Console() +err_console = Console(stderr=True) + +_SOURCE_HEADERS = { + "X-Mem0-Source": "cli", + "X-Mem0-Client-Language": "python", +} + + +def run_identify(name: str) -> None: + """PATCH the active agent-mode key's agent_caller field.""" + config = load_config() + if not config.platform.api_key: + print_error( + err_console, + "No API key configured. Run `mem0 init --agent` first.", + ) + raise typer.Exit(1) + if not config.platform.agent_mode: + print_error( + err_console, + "This command only works on unclaimed agent-mode keys.", + ) + raise typer.Exit(1) + + name = (name or "").strip() + if not name: + print_error(err_console, "Agent name is required.") + raise typer.Exit(1) + + base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/") + try: + with httpx.Client(timeout=30.0) as client: + resp = client.patch( + f"{base_url}/api/v1/auth/agent_mode/caller/", + headers={ + **_SOURCE_HEADERS, + "Authorization": f"Token {config.platform.api_key}", + "Content-Type": "application/json", + }, + json={"agent_caller": name}, + ) + except httpx.HTTPError as exc: + print_error(err_console, f"Network error: {exc}") + raise typer.Exit(1) from exc + + if resp.status_code != 200: + try: + detail = resp.json().get("error", resp.text) + except Exception: + detail = resp.text + print_error(err_console, f"Identify failed: {detail}") + raise typer.Exit(1) + + canonical = resp.json().get("agent_caller", name) + config.platform.agent_caller = canonical + save_config(config) + print_success(console, f"Identified as {canonical}.")