Surface mem0_notice for unclaimed Agent Mode keys + docs
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.
This commit is contained in:
@@ -86,6 +86,8 @@ See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for upgra
|
|||||||
|
|
||||||
## 🚀 Quickstart Guide <a name="quickstart"></a>
|
## 🚀 Quickstart Guide <a name="quickstart"></a>
|
||||||
|
|
||||||
|
> **Agent Mode — for AI agents setting up Mem0 autonomously.** Run `mem0 init --agent --json` (after `pip install mem0-cli` or `npm install -g @mem0/cli`) to mint an evaluation Mem0 API key in <5 seconds — no email, no dashboard, no OTP. Use it immediately against the Platform or the MCP server. The human owner can claim ownership later with `mem0 init --email <their-email>`: memories transfer, the same key keeps working, and the agent isn't disrupted.
|
||||||
|
|
||||||
| | Library | Self-Hosted Server | Cloud Platform |
|
| | Library | Self-Hosted Server | Cloud Platform |
|
||||||
|---|---------|-------------------|----------------|
|
|---|---------|-------------------|----------------|
|
||||||
| **Best for** | Testing, prototyping | Teams running on their own infrastructure | Zero-ops production use |
|
| **Best for** | Testing, prototyping | Teams running on their own infrastructure | Zero-ops production use |
|
||||||
|
|||||||
+4
-2
@@ -503,7 +503,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "init",
|
"name": "init",
|
||||||
"description": "Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
|
"description": "Setup wizard for mem0 CLI. Supports Agent Mode bootstrap (--agent), email login (--email), or manual API key (--api-key).",
|
||||||
"usage": "mem0 init [OPTIONS]",
|
"usage": "mem0 init [OPTIONS]",
|
||||||
"needsBackend": false,
|
"needsBackend": false,
|
||||||
"needsConfig": false,
|
"needsConfig": false,
|
||||||
@@ -516,7 +516,9 @@
|
|||||||
{ "name": "user-id", "flags": ["-u", "--user-id"], "type": "string", "default": null, "help": "Default user ID (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": "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": "code", "flags": ["--code"], "type": "string", "default": null, "help": "Verification code (use with --email for non-interactive login)." },
|
||||||
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Overwrite existing config without confirmation." }
|
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Overwrite existing config without confirmation." },
|
||||||
|
{ "name": "agent", "flags": ["--agent"], "type": "boolean", "default": false, "help": "Bootstrap an unattended Agent Mode account (no email required)." },
|
||||||
|
{ "name": "source", "flags": ["--source"], "type": "string", "default": null, "help": "Channel attribution for signup (e.g. github, hn, ph)." }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { PlatformConfig } from "../config.js";
|
import type { PlatformConfig } from "../config.js";
|
||||||
import { isAgentMode } from "../state.js";
|
import { captureNotice, isAgentMode } from "../state.js";
|
||||||
import { CLI_VERSION } from "../version.js";
|
import { CLI_VERSION } from "../version.js";
|
||||||
import {
|
import {
|
||||||
APIError,
|
APIError,
|
||||||
@@ -90,7 +90,39 @@ export class PlatformBackend implements Backend {
|
|||||||
if (resp.status === 204) {
|
if (resp.status === 204) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return resp.json();
|
|
||||||
|
const data = await resp.json();
|
||||||
|
|
||||||
|
// Pull the unclaimed-Agent-Mode notice out of the body (or the header
|
||||||
|
// fallback for endpoints returning non-dict / non-dict-leading payloads)
|
||||||
|
// and stash for end-of-command surfacing.
|
||||||
|
let notice: string | null = null;
|
||||||
|
if (
|
||||||
|
data &&
|
||||||
|
typeof data === "object" &&
|
||||||
|
!Array.isArray(data) &&
|
||||||
|
"mem0_notice" in data
|
||||||
|
) {
|
||||||
|
notice = (data as Record<string, unknown>).mem0_notice as string;
|
||||||
|
// biome-ignore lint/performance/noDelete: intentional strip so downstream consumers don't see duplicate notice
|
||||||
|
delete (data as Record<string, unknown>).mem0_notice;
|
||||||
|
} else if (
|
||||||
|
Array.isArray(data) &&
|
||||||
|
data.length > 0 &&
|
||||||
|
typeof data[0] === "object" &&
|
||||||
|
data[0] !== null &&
|
||||||
|
"mem0_notice" in data[0]
|
||||||
|
) {
|
||||||
|
notice = (data[0] as Record<string, unknown>).mem0_notice as string;
|
||||||
|
// biome-ignore lint/performance/noDelete: see above.
|
||||||
|
delete (data[0] as Record<string, unknown>).mem0_notice;
|
||||||
|
}
|
||||||
|
if (!notice) {
|
||||||
|
notice = resp.headers.get("X-Mem0-Notice-Message") ?? null;
|
||||||
|
}
|
||||||
|
captureNotice(notice);
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async add(
|
async add(
|
||||||
|
|||||||
@@ -21,13 +21,17 @@ export interface BootstrapEnvelope {
|
|||||||
mcp_url?: string;
|
mcp_url?: string;
|
||||||
smoke_test_url?: string;
|
smoke_test_url?: string;
|
||||||
claim_command?: string;
|
claim_command?: string;
|
||||||
|
mem0_notice?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function bootstrapViaBackend(
|
export async function bootstrapViaBackend(
|
||||||
config: Mem0Config,
|
config: Mem0Config,
|
||||||
{ source }: { source?: string | null } = {},
|
{ source }: { source?: string | null } = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(/\/+$/, "");
|
const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(
|
||||||
|
/\/+$/,
|
||||||
|
"",
|
||||||
|
);
|
||||||
const body: Record<string, unknown> = {};
|
const body: Record<string, unknown> = {};
|
||||||
if (source) body.source = source;
|
if (source) body.source = source;
|
||||||
|
|
||||||
@@ -43,7 +47,9 @@ export async function bootstrapViaBackend(
|
|||||||
signal: AbortSignal.timeout(30_000),
|
signal: AbortSignal.timeout(30_000),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
printError(`Network error contacting Mem0: ${err instanceof Error ? err.message : String(err)}`);
|
printError(
|
||||||
|
`Network error contacting Mem0: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,10 +85,16 @@ export async function bootstrapViaBackend(
|
|||||||
config.defaults.userId = envelope.default_user_id;
|
config.defaults.userId = envelope.default_user_id;
|
||||||
saveConfig(config);
|
saveConfig(config);
|
||||||
|
|
||||||
printSuccess(`Agent Mode active. Default user_id: ${envelope.default_user_id}`);
|
printSuccess(
|
||||||
console.log(
|
`Agent Mode active. Default user_id: ${envelope.default_user_id}`,
|
||||||
` ${dim(`To claim this account later: ${envelope.claim_command ?? "mem0 init --email <your-email>"}`)}`,
|
|
||||||
);
|
);
|
||||||
|
if (envelope.mem0_notice) {
|
||||||
|
console.log(`\n\x1b[33m🔔 ${envelope.mem0_notice}\x1b[0m\n`);
|
||||||
|
} else {
|
||||||
|
// Fallback for older backends without the unified notice field.
|
||||||
|
const claimCmd = envelope.claim_command ?? "mem0 init --email <your-email>";
|
||||||
|
console.log(` ${dim(`To claim this account later: ${claimCmd}`)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,9 +109,14 @@ export async function claimViaOtp(
|
|||||||
config: Mem0Config,
|
config: Mem0Config,
|
||||||
{ email, code }: { email: string; code?: string },
|
{ email, code }: { email: string; code?: string },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(/\/+$/, "");
|
const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(
|
||||||
|
/\/+$/,
|
||||||
|
"",
|
||||||
|
);
|
||||||
if (!config.platform.apiKey || !config.platform.agentMode) {
|
if (!config.platform.apiKey || !config.platform.agentMode) {
|
||||||
printError("This command requires an active Agent Mode config. Run `mem0 init` first.");
|
printError(
|
||||||
|
"This command requires an active Agent Mode config. Run `mem0 init` first.",
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +180,10 @@ export async function claimViaOtp(
|
|||||||
let detail: string = verifyResp.statusText;
|
let detail: string = verifyResp.statusText;
|
||||||
let errCode = "";
|
let errCode = "";
|
||||||
try {
|
try {
|
||||||
const errBody = (await verifyResp.json()) as { error?: string; code?: string };
|
const errBody = (await verifyResp.json()) as {
|
||||||
|
error?: string;
|
||||||
|
code?: string;
|
||||||
|
};
|
||||||
if (errBody.error) detail = errBody.error;
|
if (errBody.error) detail = errBody.error;
|
||||||
if (errBody.code) errCode = errBody.code;
|
if (errBody.code) errCode = errBody.code;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -178,7 +198,10 @@ export async function claimViaOtp(
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = (await verifyResp.json()) as { claimed?: boolean; claimed_at?: string };
|
const body = (await verifyResp.json()) as {
|
||||||
|
claimed?: boolean;
|
||||||
|
claimed_at?: string;
|
||||||
|
};
|
||||||
if (!body.claimed) {
|
if (!body.claimed) {
|
||||||
printError(`Unexpected verify response: ${JSON.stringify(body)}`);
|
printError(`Unexpected verify response: ${JSON.stringify(body)}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -258,7 +258,10 @@ export async function runInit(
|
|||||||
const { isAgentMode } = await import("../state.js");
|
const { isAgentMode } = await import("../state.js");
|
||||||
const { captureEvent } = await import("../telemetry.js");
|
const { captureEvent } = await import("../telemetry.js");
|
||||||
|
|
||||||
const fireInit = (mode: "agent" | "email" | "api_key" | "existing_key", claimed = false) => {
|
const fireInit = (
|
||||||
|
mode: "agent" | "email" | "api_key" | "existing_key",
|
||||||
|
claimed = false,
|
||||||
|
) => {
|
||||||
const props: Record<string, unknown> = { command: "init", mode };
|
const props: Record<string, unknown> = { command: "init", mode };
|
||||||
const caller = detectAgentCaller();
|
const caller = detectAgentCaller();
|
||||||
if (caller) props.agent_caller = caller;
|
if (caller) props.agent_caller = caller;
|
||||||
@@ -286,7 +289,12 @@ export async function runInit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Claim flow: --email against an existing agent-mode config ───────────
|
// ── Claim flow: --email against an existing agent-mode config ───────────
|
||||||
if (opts.email && fs.existsSync(CONFIG_FILE) && savedConfig.platform.agentMode && savedConfig.platform.apiKey) {
|
if (
|
||||||
|
opts.email &&
|
||||||
|
fs.existsSync(CONFIG_FILE) &&
|
||||||
|
savedConfig.platform.agentMode &&
|
||||||
|
savedConfig.platform.apiKey
|
||||||
|
) {
|
||||||
const email = opts.email.trim().toLowerCase();
|
const email = opts.email.trim().toLowerCase();
|
||||||
validateEmail(email);
|
validateEmail(email);
|
||||||
printInfo(`Claiming Agent Mode account to ${email}...`);
|
printInfo(`Claiming Agent Mode account to ${email}...`);
|
||||||
@@ -370,9 +378,7 @@ export async function runInit(
|
|||||||
// recognized agent env var. Pure "no TTY" alone is NOT enough — pipe
|
// recognized agent env var. Pure "no TTY" alone is NOT enough — pipe
|
||||||
// users would get surprised by a silent shadow signup.
|
// users would get surprised by a silent shadow signup.
|
||||||
const agentCtx =
|
const agentCtx =
|
||||||
opts.agent === true ||
|
opts.agent === true || isAgentMode() || detectAgentCaller() !== null;
|
||||||
isAgentMode() ||
|
|
||||||
detectAgentCaller() !== null;
|
|
||||||
if (!opts.apiKey && !opts.email && agentCtx) {
|
if (!opts.apiKey && !opts.email && agentCtx) {
|
||||||
await bootstrapViaBackend(config, { source: opts.source ?? null });
|
await bootstrapViaBackend(config, { source: opts.source ?? null });
|
||||||
fireInit("agent");
|
fireInit("agent");
|
||||||
|
|||||||
+23
-4
@@ -13,7 +13,7 @@ import { colors, printError, printWarning } from "./branding.js";
|
|||||||
import type { Mem0Config } from "./config.js";
|
import type { Mem0Config } from "./config.js";
|
||||||
import { loadConfig, saveConfig } from "./config.js";
|
import { loadConfig, saveConfig } from "./config.js";
|
||||||
import { richFormatHelp } from "./help.js";
|
import { richFormatHelp } from "./help.js";
|
||||||
import { setAgentMode } from "./state.js";
|
import { isAgentMode, setAgentMode, takeNotice } from "./state.js";
|
||||||
import { captureEvent } from "./telemetry.js";
|
import { captureEvent } from "./telemetry.js";
|
||||||
import { CLI_VERSION } from "./version.js";
|
import { CLI_VERSION } from "./version.js";
|
||||||
|
|
||||||
@@ -197,8 +197,15 @@ program
|
|||||||
"Verification code (use with --email for non-interactive login).",
|
"Verification code (use with --email for non-interactive login).",
|
||||||
)
|
)
|
||||||
.option("--force", "Overwrite existing config without confirmation.", false)
|
.option("--force", "Overwrite existing config without confirmation.", false)
|
||||||
.option("--agent", "Bootstrap an unattended Agent Mode account (no email required).", false)
|
.option(
|
||||||
.option("--source <channel>", "Channel attribution for signup (e.g. github, hn, ph).")
|
"--agent",
|
||||||
|
"Bootstrap an unattended Agent Mode account (no email required).",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.option(
|
||||||
|
"--source <channel>",
|
||||||
|
"Channel attribution for signup (e.g. github, hn, ph).",
|
||||||
|
)
|
||||||
.addHelpText(
|
.addHelpText(
|
||||||
"after",
|
"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\n $ mem0 init --agent # Bootstrap an Agent Mode account (unattended)\n $ mem0 init --email you@example.com # Claims an existing Agent Mode key when one is present",
|
"\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\n $ mem0 init --agent # Bootstrap an Agent Mode account (unattended)\n $ mem0 init --email you@example.com # Claims an existing Agent Mode key when one is present",
|
||||||
@@ -777,4 +784,16 @@ program
|
|||||||
|
|
||||||
// ── Entrypoint ────────────────────────────────────────────────────────────
|
// ── Entrypoint ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
program.parse();
|
// Surface any unclaimed Agent Mode notice once per command, after the primary
|
||||||
|
// output. In JSON/agent mode the notice is folded into the envelope by
|
||||||
|
// formatJsonEnvelope, so skip the stderr banner there to avoid duplication.
|
||||||
|
function surfaceNotice(): void {
|
||||||
|
const notice = takeNotice();
|
||||||
|
if (notice && !isAgentMode()) {
|
||||||
|
process.stderr.write(`\n\x1b[33m🔔 ${notice}\x1b[0m\n\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
program.parseAsync().finally(() => {
|
||||||
|
surfaceNotice();
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import boxen from "boxen";
|
import boxen from "boxen";
|
||||||
import Table from "cli-table3";
|
import Table from "cli-table3";
|
||||||
import { colors, sym } from "./branding.js";
|
import { colors, sym } from "./branding.js";
|
||||||
|
import { takeNotice } from "./state.js";
|
||||||
|
|
||||||
const { brand, accent, success, error: errorColor, dim } = colors;
|
const { brand, accent, success, error: errorColor, dim } = colors;
|
||||||
|
|
||||||
@@ -244,6 +245,15 @@ export function formatJsonEnvelope(opts: {
|
|||||||
if (opts.count !== undefined) envelope.count = opts.count;
|
if (opts.count !== undefined) envelope.count = opts.count;
|
||||||
if (opts.error) envelope.error = opts.error;
|
if (opts.error) envelope.error = opts.error;
|
||||||
envelope.data = opts.data;
|
envelope.data = opts.data;
|
||||||
|
|
||||||
|
// If the platform flagged this as an unclaimed Agent Mode account, surface
|
||||||
|
// the notice inside the JSON envelope so an agent consuming the output
|
||||||
|
// sees it without needing to inspect HTTP headers.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
const { takeNotice } = require("./state.js");
|
||||||
|
const notice = takeNotice();
|
||||||
|
if (notice) envelope.mem0_notice = notice;
|
||||||
|
|
||||||
console.log(JSON.stringify(envelope, null, 2));
|
console.log(JSON.stringify(envelope, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,6 +366,12 @@ export function formatAgentEnvelope(opts: {
|
|||||||
}
|
}
|
||||||
if (opts.count !== undefined) envelope.count = opts.count;
|
if (opts.count !== undefined) envelope.count = opts.count;
|
||||||
envelope.data = sanitizeAgentData(opts.command, opts.data);
|
envelope.data = sanitizeAgentData(opts.command, opts.data);
|
||||||
|
|
||||||
|
// Surface the unclaimed-Agent-Mode notice (if any) in the envelope so an
|
||||||
|
// agent reading the JSON output sees it without inspecting HTTP headers.
|
||||||
|
const notice = takeNotice();
|
||||||
|
if (notice) envelope.mem0_notice = notice;
|
||||||
|
|
||||||
console.log(JSON.stringify(envelope, null, 2));
|
console.log(JSON.stringify(envelope, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
let _agentMode = false;
|
let _agentMode = false;
|
||||||
let _currentCommand = "";
|
let _currentCommand = "";
|
||||||
|
let _pendingNotice = "";
|
||||||
|
|
||||||
export function isAgentMode(): boolean {
|
export function isAgentMode(): boolean {
|
||||||
return _agentMode;
|
return _agentMode;
|
||||||
@@ -21,3 +22,19 @@ export function getCurrentCommand(): string {
|
|||||||
export function setCurrentCommand(name: string): void {
|
export function setCurrentCommand(name: string): void {
|
||||||
_currentCommand = name;
|
_currentCommand = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stash a Mem0 backend notice (Agent Mode unclaimed reminder) for end-of-
|
||||||
|
* command surfacing. Called from the platform backend after each response so
|
||||||
|
* the notice prints once per command regardless of how many sub-requests
|
||||||
|
* fired. Last-write-wins is fine — the message text is identical.
|
||||||
|
*/
|
||||||
|
export function captureNotice(notice: string | null | undefined): void {
|
||||||
|
if (notice) _pendingNotice = notice;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function takeNotice(): string {
|
||||||
|
const msg = _pendingNotice;
|
||||||
|
_pendingNotice = "";
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* Parity tests for `mem0 init --agent` (Agent Mode bootstrap).
|
||||||
|
*
|
||||||
|
* Mirror of `cli/python/tests/test_agent_mode.py` — both files MUST stay
|
||||||
|
* in sync so that the Python and Node CLIs expose an identical surface
|
||||||
|
* for the Agent Mode entrypoint. If you add a flag here, add the same
|
||||||
|
* assertion on the Python side (and vice versa).
|
||||||
|
*
|
||||||
|
* Network-bound bootstrap is covered by the platform-side E2E suite
|
||||||
|
* (`backend/tests/e2e/test_05_agent_mode.py`); these tests only verify
|
||||||
|
* the CLI surface that ships in the binary.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
function run(
|
||||||
|
args: string[],
|
||||||
|
opts: { home?: string; env?: Record<string, string> } = {},
|
||||||
|
): { stdout: string; stderr: string; exitCode: number } {
|
||||||
|
const env = { ...process.env };
|
||||||
|
for (const key of Object.keys(env)) {
|
||||||
|
if (key.startsWith("MEM0_")) delete env[key];
|
||||||
|
}
|
||||||
|
if (opts.home) env.HOME = opts.home;
|
||||||
|
if (opts.env) Object.assign(env, opts.env);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stdout = execSync(`npx tsx src/index.ts ${args.join(" ")}`, {
|
||||||
|
cwd: path.join(__dirname, ".."),
|
||||||
|
env,
|
||||||
|
encoding: "utf-8",
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
|
return { stdout, stderr: "", exitCode: 0 };
|
||||||
|
} catch (e: any) {
|
||||||
|
return {
|
||||||
|
stdout: e.stdout ?? "",
|
||||||
|
stderr: e.stderr ?? "",
|
||||||
|
exitCode: e.status ?? 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanHome(): string {
|
||||||
|
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("init flag surface", () => {
|
||||||
|
it("init --help lists --agent", () => {
|
||||||
|
const result = run(["init", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("--agent");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("init --help describes Agent Mode", () => {
|
||||||
|
const result = run(["init", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
// Description must mention what --agent actually does so an agent
|
||||||
|
// reading the help can self-discover the bootstrap entrypoint.
|
||||||
|
expect(
|
||||||
|
result.stdout.includes("Agent Mode") ||
|
||||||
|
result.stdout.toLowerCase().includes("unattended"),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("init --help lists --source", () => {
|
||||||
|
const result = run(["init", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("--source");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("init --help lists --email and --code", () => {
|
||||||
|
const result = run(["init", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("--email");
|
||||||
|
expect(result.stdout).toContain("--code");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("argv preprocessing — --agent reaches init subcommand", () => {
|
||||||
|
// Regression for the bug where the global --agent JSON-alias swallowed
|
||||||
|
// the init-level --agent flag, making `mem0 init --agent` behave like
|
||||||
|
// the plain interactive wizard.
|
||||||
|
|
||||||
|
it("init --agent triggers bootstrap branch (not the wizard)", () => {
|
||||||
|
const home = cleanHome();
|
||||||
|
const result = run(["init", "--agent"], {
|
||||||
|
home,
|
||||||
|
env: {
|
||||||
|
MEM0_BASE_URL: "http://127.0.0.1:1", // blackhole
|
||||||
|
FORCE_COLOR: "0",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const combined = (result.stdout + result.stderr).toLowerCase();
|
||||||
|
// Either bootstrap-attempt error, or a connection/network error —
|
||||||
|
// both prove the --agent path executed (the wizard would prompt for
|
||||||
|
// input and succeed/hang, not surface a network error).
|
||||||
|
expect(
|
||||||
|
combined.includes("agent") ||
|
||||||
|
combined.includes("connect") ||
|
||||||
|
combined.includes("network") ||
|
||||||
|
combined.includes("fetch") ||
|
||||||
|
combined.includes("bootstrap"),
|
||||||
|
).toBe(true);
|
||||||
|
fs.rmSync(home, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("JSON envelope on network failure", () => {
|
||||||
|
it("init --agent --json does not leak a stack trace when backend is unreachable", () => {
|
||||||
|
const home = cleanHome();
|
||||||
|
const result = run(["init", "--agent", "--json"], {
|
||||||
|
home,
|
||||||
|
env: {
|
||||||
|
MEM0_BASE_URL: "http://127.0.0.1:1",
|
||||||
|
FORCE_COLOR: "0",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const combined = result.stdout + result.stderr;
|
||||||
|
// No raw Node stack should escape the agent-mode handler.
|
||||||
|
expect(combined).not.toMatch(/at \w+\s*\(.+\.ts:\d+/);
|
||||||
|
expect(combined).not.toContain("UnhandledPromiseRejection");
|
||||||
|
expect(result.exitCode).not.toBe(0);
|
||||||
|
fs.rmSync(home, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("top-level help lists init", () => {
|
||||||
|
// `mem0 --help` must list `init` so agents walking the top-level help
|
||||||
|
// can discover the Agent Mode entrypoint without prior knowledge.
|
||||||
|
|
||||||
|
it("--help lists init", () => {
|
||||||
|
const result = run(["--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("init");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -856,7 +856,9 @@ def init(
|
|||||||
False, "--agent", help="Bootstrap an unattended Agent Mode account (no email required)."
|
False, "--agent", help="Bootstrap an unattended Agent Mode account (no email required)."
|
||||||
),
|
),
|
||||||
source: str | None = typer.Option(
|
source: str | None = typer.Option(
|
||||||
None, "--source", help="Channel attribution for signup (e.g. github, hn, ph).",
|
None,
|
||||||
|
"--source",
|
||||||
|
help="Channel attribution for signup (e.g. github, hn, ph).",
|
||||||
),
|
),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Interactive setup wizard for mem0 CLI.
|
"""Interactive setup wizard for mem0 CLI.
|
||||||
@@ -1215,11 +1217,28 @@ def main() -> None:
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Allow --json/--agent anywhere in the command line (not just before subcommand).
|
# Allow --json/--agent anywhere in the command line (not just before subcommand).
|
||||||
_json_flags = {"--json", "--agent"}
|
# Special case: `mem0 init --agent` is a subcommand flag (Agent Mode bootstrap)
|
||||||
if any(a in _json_flags for a in sys.argv[1:]):
|
# consumed by init_cmd, not a global JSON-output toggle — leave it in argv.
|
||||||
|
argv_rest = sys.argv[1:]
|
||||||
|
is_init = "init" in argv_rest
|
||||||
|
_global_flags = {"--json"} if is_init else {"--json", "--agent"}
|
||||||
|
if any(a in _global_flags for a in argv_rest):
|
||||||
from mem0_cli.state import set_agent_mode
|
from mem0_cli.state import set_agent_mode
|
||||||
|
|
||||||
set_agent_mode(True)
|
set_agent_mode(True)
|
||||||
sys.argv = [sys.argv[0]] + [a for a in sys.argv[1:] if a not in _json_flags]
|
sys.argv = [sys.argv[0]] + [a for a in argv_rest if a not in _global_flags]
|
||||||
|
|
||||||
app()
|
try:
|
||||||
|
app()
|
||||||
|
finally:
|
||||||
|
# Surface any unclaimed Agent Mode notice once per command, after the
|
||||||
|
# primary output. In JSON/agent mode the notice is folded into the
|
||||||
|
# envelope by format_json_envelope, so skip the stderr banner there
|
||||||
|
# to avoid duplicate output.
|
||||||
|
from mem0_cli.state import is_agent_mode, take_notice
|
||||||
|
|
||||||
|
notice = take_notice()
|
||||||
|
if notice and not is_agent_mode():
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
Console(stderr=True).print(f"\n[yellow]🔔 {notice}[/yellow]\n")
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class PlatformBackend(Backend):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
||||||
from mem0_cli.state import is_agent_mode
|
from mem0_cli.state import capture_notice, is_agent_mode
|
||||||
|
|
||||||
self._client.headers["X-Mem0-Caller-Type"] = "agent" if is_agent_mode() else "user"
|
self._client.headers["X-Mem0-Caller-Type"] = "agent" if is_agent_mode() else "user"
|
||||||
resp = self._client.request(method, path, **kwargs)
|
resp = self._client.request(method, path, **kwargs)
|
||||||
@@ -48,7 +48,26 @@ class PlatformBackend(Backend):
|
|||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
if resp.status_code == 204:
|
if resp.status_code == 204:
|
||||||
return {}
|
return {}
|
||||||
return resp.json()
|
data = resp.json()
|
||||||
|
|
||||||
|
# Pull the unclaimed-Agent-Mode notice out of the body (or the header
|
||||||
|
# fallback for endpoints that return non-dict / non-dict-leading
|
||||||
|
# payloads) and stash it for end-of-command surfacing.
|
||||||
|
notice = None
|
||||||
|
if isinstance(data, dict) and "mem0_notice" in data:
|
||||||
|
notice = data.pop("mem0_notice")
|
||||||
|
elif (
|
||||||
|
isinstance(data, list)
|
||||||
|
and data
|
||||||
|
and isinstance(data[0], dict)
|
||||||
|
and "mem0_notice" in data[0]
|
||||||
|
):
|
||||||
|
notice = data[0].pop("mem0_notice")
|
||||||
|
if notice is None:
|
||||||
|
notice = resp.headers.get("X-Mem0-Notice-Message") or None
|
||||||
|
capture_notice(notice)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
def add(
|
def add(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from mem0_cli.branding import (
|
|||||||
BRAND_COLOR,
|
BRAND_COLOR,
|
||||||
DIM_COLOR,
|
DIM_COLOR,
|
||||||
print_error,
|
print_error,
|
||||||
print_info,
|
|
||||||
print_success,
|
print_success,
|
||||||
)
|
)
|
||||||
from mem0_cli.config import Mem0Config, save_config
|
from mem0_cli.config import Mem0Config, save_config
|
||||||
@@ -81,7 +80,13 @@ def bootstrap_via_backend(
|
|||||||
save_config(config)
|
save_config(config)
|
||||||
|
|
||||||
print_success(console, f"Agent Mode active. Default user_id: {envelope['default_user_id']}")
|
print_success(console, f"Agent Mode active. Default user_id: {envelope['default_user_id']}")
|
||||||
console.print(f" [{DIM_COLOR}]To claim this account later: {envelope.get('claim_command', 'mem0 init --email <your-email>')}[/]")
|
notice = envelope.get("mem0_notice")
|
||||||
|
if notice:
|
||||||
|
console.print(f"\n[yellow]🔔 {notice}[/yellow]\n")
|
||||||
|
else:
|
||||||
|
# Fallback if the backend hasn't deployed the unified notice yet.
|
||||||
|
claim_cmd = envelope.get("claim_command", "mem0 init --email <your-email>")
|
||||||
|
console.print(f" [{DIM_COLOR}]To claim this account later: {claim_cmd}[/]")
|
||||||
|
|
||||||
|
|
||||||
def claim_via_otp(config: Mem0Config, *, email: str, code: str | None = None) -> None:
|
def claim_via_otp(config: Mem0Config, *, email: str, code: str | None = None) -> None:
|
||||||
@@ -161,7 +166,9 @@ def claim_via_otp(config: Mem0Config, *, email: str, code: str | None = None) ->
|
|||||||
code_str = ""
|
code_str = ""
|
||||||
print_error(err_console, f"Claim failed: {detail}")
|
print_error(err_console, f"Claim failed: {detail}")
|
||||||
if code_str == "email_already_claimed":
|
if code_str == "email_already_claimed":
|
||||||
console.print(f" [{DIM_COLOR}]Tip: this email already has a Mem0 account. Sign in there and run `mem0 link <key>` to attach this agent.[/]")
|
console.print(
|
||||||
|
f" [{DIM_COLOR}]Tip: this email already has a Mem0 account. Sign in there and run `mem0 link <key>` to attach this agent.[/]"
|
||||||
|
)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
body = verify.json()
|
body = verify.json()
|
||||||
|
|||||||
@@ -229,6 +229,16 @@ def format_json_envelope(
|
|||||||
if error:
|
if error:
|
||||||
envelope["error"] = error
|
envelope["error"] = error
|
||||||
envelope["data"] = data
|
envelope["data"] = data
|
||||||
|
|
||||||
|
# If the platform flagged this as an unclaimed Agent Mode account, surface
|
||||||
|
# the notice inside the JSON envelope so an agent consuming the output
|
||||||
|
# sees it without needing to inspect HTTP headers.
|
||||||
|
from mem0_cli.state import take_notice
|
||||||
|
|
||||||
|
notice = take_notice()
|
||||||
|
if notice:
|
||||||
|
envelope["mem0_notice"] = notice
|
||||||
|
|
||||||
console.print_json(json.dumps(envelope, default=str))
|
console.print_json(json.dumps(envelope, default=str))
|
||||||
|
|
||||||
|
|
||||||
@@ -323,6 +333,15 @@ def format_agent_envelope(
|
|||||||
if count is not None:
|
if count is not None:
|
||||||
envelope["count"] = count
|
envelope["count"] = count
|
||||||
envelope["data"] = sanitize_agent_data(command, data)
|
envelope["data"] = sanitize_agent_data(command, data)
|
||||||
|
|
||||||
|
# Surface the unclaimed-Agent-Mode notice (if any) in the envelope so an
|
||||||
|
# agent reading the JSON output sees it without inspecting HTTP headers.
|
||||||
|
from mem0_cli.state import take_notice
|
||||||
|
|
||||||
|
notice = take_notice()
|
||||||
|
if notice:
|
||||||
|
envelope["mem0_notice"] = notice
|
||||||
|
|
||||||
console.print_json(json.dumps(envelope, default=str))
|
console.print_json(json.dumps(envelope, default=str))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
_agent_mode: bool = False
|
_agent_mode: bool = False
|
||||||
_current_command: str = ""
|
_current_command: str = ""
|
||||||
|
_pending_notice: str = ""
|
||||||
|
|
||||||
|
|
||||||
def is_agent_mode() -> bool:
|
def is_agent_mode() -> bool:
|
||||||
@@ -22,3 +23,23 @@ def get_current_command() -> str:
|
|||||||
def set_current_command(name: str) -> None:
|
def set_current_command(name: str) -> None:
|
||||||
global _current_command
|
global _current_command
|
||||||
_current_command = name
|
_current_command = name
|
||||||
|
|
||||||
|
|
||||||
|
def capture_notice(notice: str | None) -> None:
|
||||||
|
"""Stash a Mem0 backend notice for end-of-command surfacing.
|
||||||
|
|
||||||
|
Called from the platform backend after each response so the notice can
|
||||||
|
be printed once per command (regardless of how many sub-requests fired).
|
||||||
|
Last-write-wins is fine — the message text is identical across requests.
|
||||||
|
"""
|
||||||
|
global _pending_notice
|
||||||
|
if notice:
|
||||||
|
_pending_notice = notice
|
||||||
|
|
||||||
|
|
||||||
|
def take_notice() -> str:
|
||||||
|
"""Return and clear the pending notice."""
|
||||||
|
global _pending_notice
|
||||||
|
msg = _pending_notice
|
||||||
|
_pending_notice = ""
|
||||||
|
return msg
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
"""Parity tests for `mem0 init --agent` (Agent Mode bootstrap).
|
||||||
|
|
||||||
|
Mirror of ``cli/node/tests/agent-mode.test.ts`` — both files MUST stay in
|
||||||
|
sync so that the Python and Node CLIs expose an identical surface for the
|
||||||
|
Agent Mode entrypoint. If you add a flag here, add the same assertion on
|
||||||
|
the Node side (and vice versa).
|
||||||
|
|
||||||
|
Network-bound bootstrap is covered by the platform-side E2E suite
|
||||||
|
(``backend/tests/e2e/test_05_agent_mode.py``); these tests only verify
|
||||||
|
the CLI surface that ships in the binary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[mKJHABCDfsu]")
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_ansi(text: str) -> str:
|
||||||
|
return _ANSI_RE.sub("", text)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(args: list[str], home_dir: str | None = None) -> subprocess.CompletedProcess:
|
||||||
|
env = os.environ.copy()
|
||||||
|
for key in list(env.keys()):
|
||||||
|
if key.startswith("MEM0_"):
|
||||||
|
del env[key]
|
||||||
|
env.pop("FORCE_COLOR", None)
|
||||||
|
if home_dir:
|
||||||
|
env["HOME"] = home_dir
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-m", "mem0_cli", *args],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=env,
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
return subprocess.CompletedProcess(
|
||||||
|
args=result.args,
|
||||||
|
returncode=result.returncode,
|
||||||
|
stdout=_strip_ansi(result.stdout),
|
||||||
|
stderr=_strip_ansi(result.stderr),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def clean_home(tmp_path):
|
||||||
|
return str(tmp_path)
|
||||||
|
|
||||||
|
|
||||||
|
class TestInitFlagSurface:
|
||||||
|
"""`mem0 init --help` must expose the Agent Mode flags."""
|
||||||
|
|
||||||
|
def test_init_help_lists_agent_flag(self):
|
||||||
|
result = _run(["init", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "--agent" in result.stdout
|
||||||
|
|
||||||
|
def test_init_help_describes_agent_mode(self):
|
||||||
|
result = _run(["init", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
# Description must mention what --agent actually does so an agent
|
||||||
|
# reading the help can self-discover the bootstrap entrypoint.
|
||||||
|
assert "Agent Mode" in result.stdout or "unattended" in result.stdout.lower()
|
||||||
|
|
||||||
|
def test_init_help_lists_source_flag(self):
|
||||||
|
result = _run(["init", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "--source" in result.stdout
|
||||||
|
|
||||||
|
def test_init_help_lists_email_and_code(self):
|
||||||
|
# Claim flow flags must remain present alongside Agent Mode flags.
|
||||||
|
result = _run(["init", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "--email" in result.stdout
|
||||||
|
assert "--code" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
class TestArgvPreprocessing:
|
||||||
|
"""`--agent` on `init` must reach init_cmd, not be eaten by the global preprocessor.
|
||||||
|
|
||||||
|
Regression for the bug where the top-level `--agent` JSON-alias was
|
||||||
|
stripped from ``sys.argv`` before Typer could bind it to the init
|
||||||
|
subcommand, making ``mem0 init --agent`` indistinguishable from a
|
||||||
|
plain ``mem0 init`` (interactive wizard).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_init_with_agent_reaches_subcommand(self, clean_home):
|
||||||
|
# We can't hit a real backend in unit tests, so we point the CLI at
|
||||||
|
# a guaranteed-dead URL and assert the failure is the bootstrap
|
||||||
|
# request failing — proving the --agent flag was honored and the
|
||||||
|
# bootstrap branch ran, not the interactive wizard.
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-m", "mem0_cli", "init", "--agent"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env={
|
||||||
|
**{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
|
||||||
|
"HOME": clean_home,
|
||||||
|
"MEM0_BASE_URL": "http://127.0.0.1:1", # blackhole
|
||||||
|
"FORCE_COLOR": "0",
|
||||||
|
},
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
combined = _strip_ansi(result.stdout + result.stderr).lower()
|
||||||
|
# Either we got a connection/network error from the bootstrap POST,
|
||||||
|
# or the CLI surfaced an Agent Mode-specific failure message.
|
||||||
|
assert (
|
||||||
|
"agent" in combined
|
||||||
|
or "connect" in combined
|
||||||
|
or "network" in combined
|
||||||
|
or "fetch" in combined
|
||||||
|
or "bootstrap" in combined
|
||||||
|
), f"Expected bootstrap attempt, got: {combined!r}"
|
||||||
|
|
||||||
|
|
||||||
|
class TestJsonEnvelopeParity:
|
||||||
|
"""`mem0 init --agent --json` should produce a JSON envelope on success.
|
||||||
|
|
||||||
|
Without a live backend we can only assert the failure shape: when the
|
||||||
|
backend is unreachable, the CLI must still exit non-zero AND not crash
|
||||||
|
on a Python traceback (which would mean we leaked an exception past
|
||||||
|
the agent-mode handler).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_init_agent_json_no_traceback_on_network_failure(self, clean_home):
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-m", "mem0_cli", "init", "--agent", "--json"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env={
|
||||||
|
**{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
|
||||||
|
"HOME": clean_home,
|
||||||
|
"MEM0_BASE_URL": "http://127.0.0.1:1",
|
||||||
|
"FORCE_COLOR": "0",
|
||||||
|
},
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
combined = _strip_ansi(result.stdout + result.stderr)
|
||||||
|
assert "Traceback (most recent call last)" not in combined
|
||||||
|
assert result.returncode != 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestInitInCommandList:
|
||||||
|
"""`mem0 --help` must list `init` so agents walking the top-level help
|
||||||
|
can discover the Agent Mode entrypoint without prior knowledge."""
|
||||||
|
|
||||||
|
def test_top_level_help_lists_init(self):
|
||||||
|
result = _run(["--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "init" in result.stdout
|
||||||
@@ -11,6 +11,12 @@
|
|||||||
- Live MCP server: https://mcp.mem0.ai (see `platform/mem0-mcp`).
|
- Live MCP server: https://mcp.mem0.ai (see `platform/mem0-mcp`).
|
||||||
- Source repo: https://github.com/mem0ai/mem0
|
- Source repo: https://github.com/mem0ai/mem0
|
||||||
|
|
||||||
|
### Don't have a Mem0 API key?
|
||||||
|
|
||||||
|
Run: `mem0 init --agent --json`
|
||||||
|
|
||||||
|
This mints an evaluation key in <5 seconds — no email, no dashboard, no OTP required. Use it immediately against the Platform API or MCP server. When the human is ready, claim with `mem0 init --email <your-email>` to transfer ownership (memories preserved, same API key, no agent disruption).
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
- Python SDK: `pip install mem0ai`
|
- Python SDK: `pip install mem0ai`
|
||||||
|
|||||||
@@ -2,6 +2,18 @@
|
|||||||
|
|
||||||
Add persistent memory to your AI workflows. Store, retrieve, and manage memories across sessions using the Mem0 Platform. Works with **Claude Code** (CLI), **Claude Cowork** (desktop app), **Cursor**, and **Codex**.
|
Add persistent memory to your AI workflows. Store, retrieve, and manage memories across sessions using the Mem0 Platform. Works with **Claude Code** (CLI), **Claude Cowork** (desktop app), **Cursor**, and **Codex**.
|
||||||
|
|
||||||
|
## Quick path for agents
|
||||||
|
|
||||||
|
If you're an AI agent setting up Mem0 autonomously (no human dashboard signup available), run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 init --agent --json
|
||||||
|
```
|
||||||
|
|
||||||
|
This mints an evaluation API key in <5 seconds — no email, no OTP, no browser. The key is written to your config and exported as `MEM0_API_KEY` for the current shell. Use it immediately against the Mem0 Platform or the MCP server. The human owner can later run `mem0 init --email <their-email>` to claim ownership: memories transfer, the same API key keeps working, and the agent isn't disrupted.
|
||||||
|
|
||||||
|
Humans setting up Mem0 by hand should continue with Step 1 below.
|
||||||
|
|
||||||
## Step 1: Set your API key
|
## Step 1: Set your API key
|
||||||
|
|
||||||
> **You must complete this step before installing the plugin.**
|
> **You must complete this step before installing the plugin.**
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export MEM0_API_KEY="m0-your-api-key"
|
|||||||
|
|
||||||
Get an API key at: https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=mem0-plugin-skill
|
Get an API key at: https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=mem0-plugin-skill
|
||||||
|
|
||||||
|
> **Don't have a `MEM0_API_KEY`?** Run `mem0 init --agent --json` (after `pip install mem0-cli` or `npm install -g @mem0/cli`) to mint an evaluation key without email or dashboard. The human can claim later with `mem0 init --email <your-email>`.
|
||||||
|
|
||||||
## Step 2: Initialize the client
|
## Step 2: Initialize the client
|
||||||
|
|
||||||
**Python:**
|
**Python:**
|
||||||
|
|||||||
@@ -19,6 +19,18 @@ openclaw --version
|
|||||||
|------------------|----------------|
|
|------------------|----------------|
|
||||||
| `>= 2026.4.25` | Fully supported |
|
| `>= 2026.4.25` | Fully supported |
|
||||||
|
|
||||||
|
## Quick path for agents
|
||||||
|
|
||||||
|
If you're an AI agent setting up Mem0 autonomously (no human dashboard signup), mint an evaluation API key in <5 seconds:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 init --agent --json
|
||||||
|
```
|
||||||
|
|
||||||
|
No email, no OTP, no browser. The key is written to your Mem0 CLI config and exported as `MEM0_API_KEY` — pass it to `openclaw mem0 init --api-key $MEM0_API_KEY --user-id <id>` to wire OpenClaw up immediately. The human owner can later run `mem0 init --email <their-email>` to claim ownership: memories transfer, same API key keeps working, no agent disruption.
|
||||||
|
|
||||||
|
Humans should follow the Quick Start below.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### Platform (Mem0 Cloud)
|
### Platform (Mem0 Cloud)
|
||||||
|
|||||||
@@ -38,7 +38,21 @@ Both packages install a `mem0` binary with identical commands, options, and outp
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
**Interactive wizard:**
|
**Autonomous Mem0 setup for agents (no email, no dashboard):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 init --agent --json
|
||||||
|
```
|
||||||
|
|
||||||
|
This mints an evaluation API key in <5 seconds. Use it immediately. Print this line to the human:
|
||||||
|
|
||||||
|
> Mem0 memory is running in Agent Mode. Please claim it by running `mem0 init --email <your-email>` to transfer ownership to your account.
|
||||||
|
|
||||||
|
Never commit API keys, `.env`, or `~/.mem0/config.json`.
|
||||||
|
|
||||||
|
After the human claims, the same API key keeps working — no agent disruption — and memories are preserved.
|
||||||
|
|
||||||
|
**Interactive wizard (for humans):**
|
||||||
```bash
|
```bash
|
||||||
mem0 init
|
mem0 init
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -248,8 +248,8 @@ Bake the choice into the goal doc in step 5. Do not re-decide later.
|
|||||||
| OSS (default LLM) | `OPENAI_API_KEY` | https://platform.openai.com/api-keys |
|
| OSS (default LLM) | `OPENAI_API_KEY` | https://platform.openai.com/api-keys |
|
||||||
|
|
||||||
If present in env → continue.
|
If present in env → continue.
|
||||||
If missing → **interactive mode** asks; **CI mode** (`MEM0_INTEGRATE_CI=1`)
|
If `MEM0_API_KEY` is missing AND the track is **Platform** → **default to Agent Mode**: run `mem0 init --agent --json` (after `pip install mem0-cli` or `npm install -g @mem0/cli`) to mint an evaluation key without email/dashboard friction. Cache the key to `.env` (with user consent) and continue. Tell the user to claim later with `mem0 init --email <their-email>` — same key, no agent disruption.
|
||||||
exits with code 2 and the name of the missing key.
|
If missing AND **CI mode** (`MEM0_INTEGRATE_CI=1`) → exit with code 2 and the name of the missing key.
|
||||||
|
|
||||||
Never echo key values into `trace.jsonl`. Persist to `.env` only with
|
Never echo key values into `trace.jsonl`. Persist to `.env` only with
|
||||||
explicit user consent, and append `.env` to `.gitignore` if not already there.
|
explicit user consent, and append `.env` to `.gitignore` if not already there.
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export MEM0_API_KEY="m0-your-api-key"
|
|||||||
|
|
||||||
Get an API key at: https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=skill-mem0
|
Get an API key at: https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=skill-mem0
|
||||||
|
|
||||||
|
> **Don't have a `MEM0_API_KEY`?** Run `mem0 init --agent --json` (after `pip install mem0-cli` or `npm install -g @mem0/cli`) to mint an evaluation key without email or dashboard. The human can claim later with `mem0 init --email <your-email>`.
|
||||||
|
|
||||||
## Step 2: Initialize the client
|
## Step 2: Initialize the client
|
||||||
|
|
||||||
**Python:**
|
**Python:**
|
||||||
|
|||||||
Reference in New Issue
Block a user