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:
Mgeeeek
2026-05-14 02:11:39 +05:30
parent 1c92c466c4
commit a2516269cc
22 changed files with 583 additions and 35 deletions
+34 -2
View File
@@ -3,7 +3,7 @@
*/
import type { PlatformConfig } from "../config.js";
import { isAgentMode } from "../state.js";
import { captureNotice, isAgentMode } from "../state.js";
import { CLI_VERSION } from "../version.js";
import {
APIError,
@@ -90,7 +90,39 @@ export class PlatformBackend implements Backend {
if (resp.status === 204) {
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(
+32 -9
View File
@@ -21,13 +21,17 @@ export interface BootstrapEnvelope {
mcp_url?: string;
smoke_test_url?: string;
claim_command?: string;
mem0_notice?: string;
}
export async function bootstrapViaBackend(
config: Mem0Config,
{ source }: { source?: string | null } = {},
): 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> = {};
if (source) body.source = source;
@@ -43,7 +47,9 @@ export async function bootstrapViaBackend(
signal: AbortSignal.timeout(30_000),
});
} 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);
}
@@ -79,10 +85,16 @@ export async function bootstrapViaBackend(
config.defaults.userId = envelope.default_user_id;
saveConfig(config);
printSuccess(`Agent Mode active. Default user_id: ${envelope.default_user_id}`);
console.log(
` ${dim(`To claim this account later: ${envelope.claim_command ?? "mem0 init --email <your-email>"}`)}`,
printSuccess(
`Agent Mode active. Default user_id: ${envelope.default_user_id}`,
);
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,
{ email, code }: { email: string; code?: string },
): 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) {
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);
}
@@ -163,7 +180,10 @@ export async function claimViaOtp(
let detail: string = verifyResp.statusText;
let errCode = "";
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.code) errCode = errBody.code;
} catch {
@@ -178,7 +198,10 @@ export async function claimViaOtp(
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) {
printError(`Unexpected verify response: ${JSON.stringify(body)}`);
process.exit(1);
+11 -5
View File
@@ -258,7 +258,10 @@ export async function runInit(
const { isAgentMode } = await import("../state.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 caller = detectAgentCaller();
if (caller) props.agent_caller = caller;
@@ -286,7 +289,12 @@ export async function runInit(
}
// ── 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();
validateEmail(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
// users would get surprised by a silent shadow signup.
const agentCtx =
opts.agent === true ||
isAgentMode() ||
detectAgentCaller() !== null;
opts.agent === true || isAgentMode() || detectAgentCaller() !== null;
if (!opts.apiKey && !opts.email && agentCtx) {
await bootstrapViaBackend(config, { source: opts.source ?? null });
fireInit("agent");
+23 -4
View File
@@ -13,7 +13,7 @@ import { colors, printError, printWarning } from "./branding.js";
import type { Mem0Config } from "./config.js";
import { loadConfig, saveConfig } from "./config.js";
import { richFormatHelp } from "./help.js";
import { setAgentMode } from "./state.js";
import { isAgentMode, setAgentMode, takeNotice } from "./state.js";
import { captureEvent } from "./telemetry.js";
import { CLI_VERSION } from "./version.js";
@@ -197,8 +197,15 @@ program
"Verification code (use with --email for non-interactive login).",
)
.option("--force", "Overwrite existing config without confirmation.", false)
.option("--agent", "Bootstrap an unattended Agent Mode account (no email required).", false)
.option("--source <channel>", "Channel attribution for signup (e.g. github, hn, ph).")
.option(
"--agent",
"Bootstrap an unattended Agent Mode account (no email required).",
false,
)
.option(
"--source <channel>",
"Channel attribution for signup (e.g. github, hn, ph).",
)
.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\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 ────────────────────────────────────────────────────────────
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();
});
+16
View File
@@ -5,6 +5,7 @@
import boxen from "boxen";
import Table from "cli-table3";
import { colors, sym } from "./branding.js";
import { takeNotice } from "./state.js";
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.error) envelope.error = opts.error;
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));
}
@@ -356,6 +366,12 @@ export function formatAgentEnvelope(opts: {
}
if (opts.count !== undefined) envelope.count = opts.count;
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));
}
+17
View File
@@ -5,6 +5,7 @@
let _agentMode = false;
let _currentCommand = "";
let _pendingNotice = "";
export function isAgentMode(): boolean {
return _agentMode;
@@ -21,3 +22,19 @@ export function getCurrentCommand(): string {
export function setCurrentCommand(name: string): void {
_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;
}