feat(cli): add event commands, --json/--agent flag, agent output sanitization, and edge-case hardening for CLI SDKs (#4649)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Saket Aryan
2026-04-01 21:04:59 +05:30
committed by GitHub
parent 713dba5d0a
commit 2acf9571b3
35 changed files with 2941 additions and 341 deletions
+175 -15
View File
@@ -28,7 +28,7 @@ mem0 CLI is the official command-line interface for [mem0](https://mem0.ai) -- t
### Who is it for?
- Developers integrating mem0 into their workflows
- AI agents that need persistent memory (the CLI is designed with `--output json` and `help --json` specifically for machine consumption)
- AI agents that need persistent memory (the CLI is designed with `--json`/`--agent` global flags and `help --json` specifically for machine consumption)
- DevOps/CI pipelines that need to manage memories programmatically
### Project Structure
@@ -79,6 +79,7 @@ Apache-2.0
│ ├── init_cmd.py # run_init (interactive wizard)
│ ├── config_cmd.py # cmd_config_show, cmd_config_get, cmd_config_set
│ ├── entities.py # cmd_entities_list, cmd_entities_delete
│ ├── events_cmd.py # cmd_event_list, cmd_event_status
│ └── utils.py # cmd_status, cmd_version, cmd_import
└── node/
├── package.json # Node package config (tsup build)
@@ -88,6 +89,7 @@ Apache-2.0
├── config.ts # Config loading/saving, env var overrides
├── branding.ts # Colors, icons, banner, timedStatus, print helpers
├── output.ts # Output formatting (text, json, table, quiet)
├── state.ts # Agent mode flag (setAgentMode, isAgentMode)
├── help.ts # Rich-style help formatter (panels, command ordering)
├── backend/
│ ├── index.ts # Re-exports
@@ -98,6 +100,7 @@ Apache-2.0
├── init.ts # runInit (interactive wizard)
├── config.ts # cmdConfigShow, cmdConfigGet, cmdConfigSet
├── entities.ts # cmdEntitiesList, cmdEntitiesDelete
├── events.ts # cmdEventList, cmdEventStatus
└── utils.ts # cmdStatus, cmdVersion, cmdImport
```
@@ -155,9 +158,15 @@ Interactive setup wizard for mem0 CLI.
| `-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). |
| `--force` | bool | No | false | Overwrite existing config without confirmation. |
**Behavior:**
*Existing config protection:*
- If `~/.mem0/config.json` exists with an API key, the CLI warns and asks for confirmation before overwriting.
- In non-TTY mode, this is a hard error unless `--force` is passed.
- `--force` skips the confirmation in both TTY and non-TTY modes.
*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).
@@ -175,6 +184,7 @@ Interactive setup wizard for mem0 CLI.
```bash
mem0 init
mem0 init --api-key m0-xxx --user-id alice
mem0 init --api-key m0-xxx --user-id alice --force
mem0 init --email alice@company.com
mem0 init --email alice@company.com --code 482901
```
@@ -700,7 +710,114 @@ mem0 entity delete --user-id alice --dry-run
---
### 3.14 `status`
### 3.14 `event list`
List recent background processing events.
| Property | Value |
|------------------|-------|
| Usage | `mem0 event list [OPTIONS]` |
| needsBackend | Yes |
| needsConfig | Yes |
| resolveIds | No |
| resolveGraph | No |
| confirmDangerous | No |
| Output formats | text (table), json |
| Default output | table |
| API endpoint | `GET /v1/events/` |
**Options:**
| Flag | Type | Default | Panel | Help |
|----------------|--------|---------|------------|------|
| `-o, --output` | string | "table" | Output | Output: text, json. |
| `--api-key` | string | - | Connection | Override API key. |
| `--base-url` | string | - | Connection | Override API base URL. |
**Behavior:** Fetches all background events for the project. Displays as a table with columns: Event ID (first 8 chars), Type, Status (color-coded), Latency, Created. Status values: `PENDING` (accent), `SUCCEEDED` (green), `FAILED` (red), `PROCESSING` (yellow).
**JSON output envelope:**
```json
{
"status": "success",
"command": "event list",
"count": 3,
"duration_ms": 87,
"data": [
{ "id": "evt-abc", "event_type": "ADD", "status": "SUCCEEDED", "latency": 412.0, "created_at": "2026-01-01T10:00:00Z" }
]
}
```
**Examples:**
```bash
mem0 event list
mem0 event list --output json
```
---
### 3.15 `event status`
Get the status and results of a specific background event.
| Property | Value |
|------------------|-------|
| Usage | `mem0 event status <event_id> [OPTIONS]` |
| needsBackend | Yes |
| needsConfig | Yes |
| resolveIds | No |
| resolveGraph | No |
| confirmDangerous | No |
| Output formats | text, json |
| Default output | text |
| API endpoint | `GET /v1/events/{event_id}/` |
**Arguments:**
| Name | Type | Required | Help |
|------------|--------|----------|------|
| `event_id` | string | Yes | Event ID to inspect. |
**Options:**
| Flag | Type | Default | Panel | Help |
|----------------|--------|---------|------------|------|
| `-o, --output` | string | "text" | Output | Output: text, json. |
| `--api-key` | string | - | Connection | Override API key. |
| `--base-url` | string | - | Connection | Override API base URL. |
**Behavior:** Fetches the event by ID and displays: Event ID, Type, Status (color-coded), Latency, Created, Updated, and a numbered list of result memories (event type, memory text, user_id, truncated memory ID). Displayed in a boxed panel (text) or JSON envelope.
**JSON output envelope:**
```json
{
"status": "success",
"command": "event status",
"duration_ms": 65,
"data": {
"id": "evt-abc",
"event_type": "ADD",
"status": "SUCCEEDED",
"latency": 412.0,
"created_at": "2026-01-01T10:00:00Z",
"updated_at": "2026-01-01T10:00:01Z",
"results": [
{ "id": "mem-xyz", "event": "ADD", "user_id": "alice", "memory": "User prefers dark mode" }
]
}
}
```
**Examples:**
```bash
mem0 event status evt-abc-123
mem0 event status evt-abc-123 --output json
```
---
### 3.16 `status`
Check connectivity and authentication.
@@ -721,20 +838,19 @@ Check connectivity and authentication.
| `--api-key` | string | - | Connection | Override API key. |
| `--base-url` | string | - | Connection | Override API base URL. |
**Behavior:** If config has a default `user_id` or `agent_id`, validates by making a minimal `POST /v2/memories/` with `page=1&page_size=1`. Otherwise validates via `GET /v1/entities/`. Displays connection status in a boxed panel (text) or JSON envelope.
**Behavior:** Validates connectivity by calling `GET /v1/ping/`. Displays connection status in a boxed panel (text) or JSON envelope. The ping endpoint is lightweight and does not require any entity scope.
**JSON output:**
```json
{
"status": "success",
"command": "status",
"duration_ms": 112,
"data": {
"connected": true,
"backend": "platform",
"base_url": "https://api.mem0.ai",
"latency_ms": 245
},
"duration_ms": 245
"base_url": "https://api.mem0.ai"
}
}
```
@@ -746,7 +862,7 @@ mem0 status -o json
---
### 3.15 `help`
### 3.17 `help`
Show help. Use `--json` for machine-readable output (for LLM agents).
@@ -806,6 +922,9 @@ The auth header name is `Authorization` and the scheme is `Token` (not Bearer).
| Delete all | `DELETE` | `/v1/memories/` | - | entity ID params |
| List entities | `GET` | `/v1/entities/` | - | - |
| Delete entities | `DELETE` | `/v1/entities/` | - | entity ID params |
| List events | `GET` | `/v1/events/` | - | - |
| Get event | `GET` | `/v1/events/{event_id}/` | - | - |
| Ping (status) | `GET` | `/v1/ping/` | - | - |
### How Filters Are Built (`_buildFilters` / `_build_filters`)
@@ -1108,6 +1227,8 @@ For `PENDING` events, displays "Processing in background" with the event ID.
### 7.1 Supported Modes Per Command
All commands also support `agent` mode via the global `--json`/`--agent` flag, which wraps output in a structured JSON envelope with sanitized fields.
| Command | text | json | table | quiet |
|----------------|------|------|-------|-------|
| add | Y | Y | - | Y |
@@ -1122,12 +1243,16 @@ For `PENDING` events, displays "Processing in background" with the event ID.
| config set | (success msg) | - | - | - |
| entity list | - | Y | Y (default) | - |
| entity delete | Y | Y | - | Y |
| event list | Y (table) | Y | - | - |
| event status | Y | Y | - | - |
| status | Y | Y | - | - |
| help | Y | Y (--json) | - | - |
### 7.2 JSON Envelope Format (`formatJsonEnvelope`)
### 7.2 JSON Envelope Format
Used by `config show`, `status`, and `import` for structured JSON output:
There are two related envelope formats:
**`formatJsonEnvelope`** — used by `config show`, `status`, and `import` for `--output json`:
```json
{
@@ -1141,14 +1266,39 @@ Used by `config show`, `status`, and `import` for structured JSON output:
}
```
**`formatAgentEnvelope`** — used by all commands in agent mode (`--json`/`--agent`). Same structure, but `data` is passed through `sanitizeAgentData(command, data)` to project only the most relevant fields:
| Command | Fields in `data` |
|---------------|-----------------|
| add | `[{id, memory, event}]` or `[{status, event_id}]` for PENDING |
| search | `[{id, memory, score, created_at, categories}]` |
| list | `[{id, memory, created_at, categories}]` |
| get | `{id, memory, created_at, updated_at, categories, metadata}` |
| update | `{id, memory}` |
| delete | (raw API response) |
| entity list | `[{name, type, count}]` |
| event list | `[{id, event_type, status, latency, created_at}]` |
| event status | `{id, event_type, status, latency, created_at, updated_at, results: [{id, event, user_id, memory}]}` |
| status/config/import | (pass-through) |
Error envelopes (on non-zero exit):
```json
{
"status": "error",
"command": "<command_name>",
"error": "Authentication failed. Your API key may be invalid or expired.",
"data": null
}
```
Fields:
- `status`: Always `"success"` (errors go to stderr before exit).
- `command`: The command name (e.g. `"status"`, `"config show"`, `"import"`).
- `status`: `"success"` or `"error"`.
- `command`: The command name.
- `duration_ms`: Optional, elapsed time in milliseconds.
- `scope`: Optional, active entity scope.
- `scope`: Optional, active entity scope (omitted if empty).
- `count`: Optional, result count.
- `error`: Optional, error message string.
- `data`: The primary payload.
- `error`: Only present when `status` is `"error"`.
- `data`: The primary payload (sanitized in agent mode).
### 7.3 Text Output
@@ -1219,6 +1369,16 @@ Destructive commands (`delete --all`, `delete --entity`, `entity delete`) requir
- CI/CD pipelines
- Scripting
### Why `--json`/`--agent` global flags exist
The `--json` and `--agent` flags (aliases of each other) activate agent mode globally. When set:
1. All output becomes a structured JSON envelope (`{status, command, duration_ms, scope, count, data}`).
2. The `data` field is sanitized via `sanitizeAgentData` — only the most relevant fields are included per command, reducing noise for agents parsing the output.
3. All human-readable output (spinners, colors, banners, timing lines) is suppressed.
4. Errors are emitted as JSON to stdout with a non-zero exit code, not to stderr as text.
This is distinct from `--output json`, which returns the raw API response without sanitization.
### Why `--output json` is on every command
Every data-returning command supports `--output json` (or `--json` for `help`). This enables machine consumption by AI agents and scripts. JSON output goes to stdout while human-readable spinners/timing go to stderr, so piping `mem0 list -o json | jq .` works cleanly.
+38 -6
View File
@@ -2,6 +2,8 @@
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. Works with the Mem0 Platform API. Available in Python and Node.js.
> **For AI agents:** pass `--agent` (or `--json`) on any command for structured JSON output purpose-built for tool loops — sanitized fields, no colors or spinners, errors as JSON. See [Agent mode](#agent-mode) below.
## Installation
```bash
@@ -55,12 +57,45 @@ mem0 delete <memory-id>
| `mem0 delete` | Delete a memory, all memories for a scope, or an entity |
| `mem0 import` | Bulk import memories from a JSON file |
| `mem0 config` | View or modify CLI configuration |
| `mem0 entities` | List or delete entities (users, agents, apps) |
| `mem0 entity` | List or delete entities (users, agents, apps, runs) |
| `mem0 event` | Inspect background processing events (bulk deletes, large add jobs) |
| `mem0 status` | Verify API connection and display current project |
| `mem0 version` | Print the CLI version |
Run `mem0 <command> --help` for detailed usage on any command.
## Agent mode
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
```bash
mem0 --agent search "user preferences" --user-id alice
mem0 --agent add "User prefers dark mode" --user-id alice
mem0 --agent list --user-id alice
```
Every command returns the same envelope shape:
```json
{
"status": "success",
"command": "search",
"duration_ms": 134,
"scope": { "user_id": "alice" },
"count": 2,
"data": [
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] }
]
}
```
What agent mode does differently from `--output json`:
- **Sanitized `data`**: only the fields an agent needs (id, memory, score, etc.) — no internal API noise
- **No human output**: spinners, colors, and banners are suppressed entirely
- **Errors as JSON**: errors go to stdout as `{"status": "error", "command": "...", "error": "..."}` with a non-zero exit code
Use `mem0 help --json` to get the full command tree as JSON — useful for agents that need to self-discover available commands.
## Output formats
Control how results are displayed with `--output`:
@@ -68,13 +103,10 @@ Control how results are displayed with `--output`:
| Format | Description |
|--------|-------------|
| `text` | Human-readable with colors and formatting (default) |
| `json` | Structured JSON for piping to `jq` or agent consumption |
| `json` | Structured JSON for piping to `jq` (raw API response) |
| `table` | Tabular format (default for `list`) |
| `quiet` | Minimal — just IDs or status codes |
```bash
mem0 search "preferences" --user-id alice --output json | jq '.data.results[].memory'
```
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
## Environment variables
+2 -1
View File
@@ -515,7 +515,8 @@
{ "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": "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." }
]
},
{
+4
View File
@@ -94,6 +94,10 @@ export interface Backend {
>;
entities(entityType: string): Promise<Record<string, unknown>[]>;
listEvents(): Promise<Record<string, unknown>[]>;
getEvent(eventId: string): Promise<Record<string, unknown>>;
}
export class AuthError extends Error {
+35 -27
View File
@@ -264,41 +264,33 @@ export class PlatformBackend implements Backend {
}
async deleteEntities(opts: EntityIds): Promise<Record<string, unknown>> {
const params: Record<string, string> = {};
if (opts.userId) params.user_id = opts.userId;
if (opts.agentId) params.agent_id = opts.agentId;
if (opts.appId) params.app_id = opts.appId;
if (opts.runId) params.run_id = opts.runId;
if (Object.keys(params).length === 0) {
// v2 endpoint: DELETE /v2/entities/{entity_type}/{entity_id}/
const typeMap: [string, string | undefined][] = [
["user", opts.userId],
["agent", opts.agentId],
["app", opts.appId],
["run", opts.runId],
];
const entities = typeMap.filter(([, v]) => v) as [string, string][];
if (entities.length === 0) {
throw new Error("At least one entity ID is required for deleteEntities.");
}
return (await this._request("DELETE", "/v1/entities/", {
params,
})) as Record<string, unknown>;
// Delete each provided entity via the v2 path-based endpoint
let result: Record<string, unknown> = {};
for (const [entityType, entityId] of entities) {
result = (await this._request(
"DELETE",
`/v2/entities/${entityType}/${entityId}/`,
)) as Record<string, unknown>;
}
return result;
}
async status(
opts: { userId?: string; agentId?: string } = {},
): Promise<Record<string, unknown>> {
try {
if (opts.userId || opts.agentId) {
const payload: Record<string, unknown> = {};
const statusParams: Record<string, string> = {
page: "1",
page_size: "1",
};
const apiFilters = this._buildFilters({
userId: opts.userId,
agentId: opts.agentId,
});
if (apiFilters) payload.filters = apiFilters;
await this._request("POST", "/v2/memories/", {
json: payload,
params: statusParams,
});
} else {
await this._request("GET", "/v1/entities/");
}
await this._request("GET", "/v1/ping/");
return { connected: true, backend: "platform", base_url: this.baseUrl };
} catch (e) {
return {
@@ -335,4 +327,20 @@ export class PlatformBackend implements Backend {
}
return items;
}
async listEvents(): Promise<Record<string, unknown>[]> {
const result = (await this._request("GET", "/v1/events/")) as unknown;
if (Array.isArray(result)) return result;
return ((result as Record<string, unknown>).results ?? []) as Record<
string,
unknown
>[];
}
async getEvent(eventId: string): Promise<Record<string, unknown>> {
return (await this._request("GET", `/v1/event/${eventId}/`)) as Record<
string,
unknown
>;
}
}
+28 -5
View File
@@ -4,6 +4,7 @@
import chalk from "chalk";
import ora, { type Ora } from "ora";
import { getCurrentCommand, isAgentMode } from "./state.js";
import { CLI_VERSION } from "./version.js";
export const LOGO = `
@@ -42,6 +43,7 @@ export function sym(fancy: string, plain: string): string {
}
export function printBanner(): void {
if (isAgentMode()) return;
const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2))
const logoLines = LOGO.trimEnd().split("\n");
const tagline = ` ${TAGLINE}`;
@@ -75,13 +77,29 @@ export function printBanner(): void {
}
export function printSuccess(message: string): void {
if (isAgentMode()) return;
console.log(`${success(sym("✓", "[ok]"))} ${message}`);
}
export function printError(message: string, hint?: string): void {
if (isAgentMode()) {
const envelope = {
status: "error",
command: getCurrentCommand(),
error: message,
data: null,
};
console.log(JSON.stringify(envelope));
return;
}
console.error(`${error(`${sym("✗", "[error]")} Error:`)} ${message}`);
if (hint) {
console.error(` ${dim(hint)}`);
const resolvedHint =
hint ??
(message.includes("Authentication failed")
? `Run ${brand("mem0 init")} to reconfigure your API key · https://app.mem0.ai/dashboard/api-keys`
: undefined);
if (resolvedHint) {
console.error(` ${dim(resolvedHint)}`);
}
}
@@ -90,15 +108,16 @@ export function printWarning(message: string): void {
}
export function printInfo(message: string): void {
if (isAgentMode()) return;
console.error(`${brand(sym("◆", "*"))} ${message}`);
}
export function printScope(ids: Record<string, string | undefined>): void {
if (isAgentMode()) return;
const parts: string[] = [];
for (const [key, val] of Object.entries(ids)) {
if (val) {
const label = key.replace(/_/g, " ").replace("id", "ID").trim();
parts.push(`${label}=${val}`);
parts.push(`${key}=${val}`);
}
}
if (parts.length > 0) {
@@ -119,6 +138,10 @@ export async function timedStatus<T>(
message: string,
fn: (ctx: TimedStatusContext) => Promise<T>,
): Promise<T> {
if (isAgentMode()) {
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
return fn(ctx);
}
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
const spinner = ora({
text: dim(message),
@@ -139,7 +162,7 @@ export async function timedStatus<T>(
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
spinner.stop();
if (ctx.errorMsg) {
console.error(`${error("✗ Error:")} ${ctx.errorMsg} (${elapsed}s)`);
printError(`${ctx.errorMsg} (${elapsed}s)`);
}
throw err;
}
+25 -7
View File
@@ -11,15 +11,17 @@ import {
saveConfig,
setNestedValue,
} from "../config.js";
import { formatJsonEnvelope } from "../output.js";
import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js";
import { isAgentMode, setCurrentCommand } from "../state.js";
const { brand, accent, dim } = colors;
export function cmdConfigShow(opts: { output?: string } = {}): void {
setCurrentCommand("config show");
const config = loadConfig();
if (opts.output === "json") {
formatJsonEnvelope({
if (opts.output === "agent" || opts.output === "json") {
formatAgentEnvelope({
command: "config show",
data: {
defaults: {
@@ -66,6 +68,7 @@ export function cmdConfigShow(opts: { output?: string } = {}): void {
}
export function cmdConfigGet(key: string): void {
setCurrentCommand("config get");
const config = loadConfig();
const value = getNestedValue(config, key);
@@ -73,20 +76,35 @@ export function cmdConfigGet(key: string): void {
printError(`Unknown config key: ${key}`);
} else {
// Redact secrets
if (key.includes("api_key") || key.split(".").pop() === "key") {
console.log(redactKey(String(value)));
const displayValue =
key.includes("api_key") || key.split(".").pop() === "key"
? redactKey(String(value))
: String(value);
if (isAgentMode()) {
formatAgentEnvelope({
command: "config get",
data: { key, value: displayValue },
});
} else {
console.log(String(value));
console.log(displayValue);
}
}
}
export function cmdConfigSet(key: string, value: string): void {
setCurrentCommand("config set");
const config = loadConfig();
if (setNestedValue(config, key, value)) {
saveConfig(config);
const display = key.includes("key") ? redactKey(value) : value;
printSuccess(`${key} = ${display}`);
if (isAgentMode()) {
formatAgentEnvelope({
command: "config set",
data: { key, value: display },
});
} else {
printSuccess(`${key} = ${display}`);
}
} else {
printError(`Unknown config key: ${key}`);
}
+31 -19
View File
@@ -12,7 +12,8 @@ import {
printSuccess,
timedStatus,
} from "../branding.js";
import { formatJson } from "../output.js";
import { formatAgentEnvelope, formatJson } from "../output.js";
import { setCurrentCommand } from "../state.js";
const { brand, accent, dim } = colors;
@@ -23,6 +24,7 @@ export async function cmdEntitiesList(
entityType: string,
opts: { output: string },
): Promise<void> {
setCurrentCommand("entity list");
if (!VALID_TYPES.has(entityType)) {
printError(
`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`,
@@ -45,8 +47,13 @@ export async function cmdEntitiesList(
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJson(results);
if (opts.output === "agent" || opts.output === "json") {
formatAgentEnvelope({
command: "entity list",
data: results,
count: results.length,
durationMs: Math.round(elapsed * 1000),
});
return;
}
@@ -86,6 +93,12 @@ export async function cmdEntitiesDelete(
output: string;
},
): Promise<void> {
setCurrentCommand("entity delete");
const { isAgentMode } = await import("../state.js");
if (isAgentMode() && !opts.force) {
printError("Destructive operation requires --force in agent mode.");
process.exit(1);
}
if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) {
printError(
"Provide at least one of --user-id, --agent-id, --app-id, --run-id.",
@@ -93,27 +106,20 @@ export async function cmdEntitiesDelete(
process.exit(1);
}
const scopeParts: string[] = [];
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
const scope = scopeParts.join(", ");
if (opts.dryRun) {
const scopeParts: string[] = [];
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
printInfo(
`Would delete entity ${scopeParts.join(", ")} and all its memories.`,
);
printInfo(`Would delete entity ${scope} and all its memories.`);
printInfo("No changes made.");
return;
}
if (!opts.force) {
const scopeParts: string[] = [];
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
const scope = scopeParts.join(", ");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
@@ -148,7 +154,13 @@ export async function cmdEntitiesDelete(
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
if (opts.output === "agent") {
formatAgentEnvelope({
command: "entity delete",
data: { deleted: true },
durationMs: Math.round(elapsed * 1000),
});
} else if (opts.output === "json") {
formatJson(result);
} else if (opts.output !== "quiet") {
printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`);
+169
View File
@@ -0,0 +1,169 @@
/**
* Event commands: list and status.
*/
import boxen from "boxen";
import Table from "cli-table3";
import type { Backend } from "../backend/base.js";
import { colors, printError, printInfo, timedStatus } from "../branding.js";
import { formatAgentEnvelope, formatJson } from "../output.js";
import { setCurrentCommand } from "../state.js";
const { brand, accent, success, error: errorColor, warning, dim } = colors;
function statusStyled(status: string): string {
switch (status.toUpperCase()) {
case "SUCCEEDED":
return success("SUCCEEDED");
case "PENDING":
return accent("PENDING");
case "FAILED":
return errorColor("FAILED");
case "PROCESSING":
return warning("PROCESSING");
default:
return status;
}
}
export async function cmdEventList(
backend: Backend,
opts: { output: string },
): Promise<void> {
setCurrentCommand("event list");
const start = performance.now();
let results: Record<string, unknown>[];
try {
results = await timedStatus("Fetching events...", async () => {
return backend.listEvents();
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "agent" || opts.output === "json") {
formatAgentEnvelope({
command: "event list",
data: results,
count: results.length,
durationMs: Math.round(elapsed * 1000),
});
return;
}
if (results.length === 0) {
console.log();
printInfo("No events found.");
console.log();
return;
}
const table = new Table({
head: [
accent("Event ID"),
accent("Type"),
accent("Status"),
accent("Latency"),
accent("Created"),
],
colWidths: [12, 14, 14, 10, 22],
wordWrap: true,
style: { head: [], border: [] },
});
for (const ev of results) {
const evId = String(ev.id ?? "").slice(0, 8);
const evType = String(ev.event_type ?? "—");
const status = String(ev.status ?? "—");
const latency = ev.latency as number | undefined;
const latencyStr = latency !== undefined ? `${Math.round(latency)}ms` : "—";
const created = String(ev.created_at ?? "—")
.slice(0, 19)
.replace("T", " ");
table.push([dim(evId), evType, statusStyled(status), latencyStr, created]);
}
console.log();
console.log(table.toString());
console.log(
` ${dim(`${results.length} event${results.length !== 1 ? "s" : ""}`)}`,
);
console.log();
}
export async function cmdEventStatus(
backend: Backend,
eventId: string,
opts: { output: string },
): Promise<void> {
setCurrentCommand("event status");
const start = performance.now();
let ev: Record<string, unknown>;
try {
ev = await timedStatus("Fetching event...", async () => {
return backend.getEvent(eventId);
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "agent" || opts.output === "json") {
formatAgentEnvelope({
command: "event status",
data: ev,
durationMs: Math.round(elapsed * 1000),
});
return;
}
const status = String(ev.status ?? "—");
const evType = String(ev.event_type ?? "—");
const latency = ev.latency as number | undefined;
const latencyStr = latency !== undefined ? `${Math.round(latency)}ms` : "—";
const created = String(ev.created_at ?? "—")
.slice(0, 19)
.replace("T", " ");
const updated = String(ev.updated_at ?? "—")
.slice(0, 19)
.replace("T", " ");
const results = ev.results as Record<string, unknown>[] | undefined;
const lines: string[] = [];
lines.push(` ${dim("Event ID:")} ${eventId}`);
lines.push(` ${dim("Type:")} ${evType}`);
lines.push(` ${dim("Status:")} ${statusStyled(status)}`);
lines.push(` ${dim("Latency:")} ${latencyStr}`);
lines.push(` ${dim("Created:")} ${created}`);
lines.push(` ${dim("Updated:")} ${updated}`);
if (results && results.length > 0) {
lines.push("");
lines.push(` ${dim(`Results (${results.length}):`)}`);
for (const r of results) {
const memId = String(r.id ?? "").slice(0, 8);
const data = r.data as Record<string, unknown> | undefined;
const memory = data?.memory ? String(data.memory) : "";
const evName = String(r.event ?? "");
const user = String(r.user_id ?? "");
let detail = `${evName} ${memory}`;
if (user) detail += ` ${dim(`(user_id=${user})`)}`;
lines.push(` ${success("·")} ${detail} ${dim(`(${memId})`)}`);
}
}
const content = lines.join("\n");
console.log();
console.log(
boxen(content, {
title: brand("Event Status"),
titleAlignment: "left",
borderColor: "magenta",
padding: 1,
}),
);
console.log();
}
+58 -13
View File
@@ -2,6 +2,7 @@
* mem0 init — interactive setup wizard.
*/
import fs from "node:fs";
import readline from "node:readline";
import { PlatformBackend } from "../backend/platform.js";
import {
@@ -12,10 +13,12 @@ import {
printSuccess,
} from "../branding.js";
import {
CONFIG_FILE,
DEFAULT_BASE_URL,
type Mem0Config,
createDefaultConfig,
loadConfig,
redactKey,
saveConfig,
} from "../config.js";
@@ -193,9 +196,10 @@ async function setupDefaults(config: Mem0Config): Promise<void> {
console.log();
printInfo("Set default entity IDs (press Enter to skip).\n");
const _systemUser = process.env.USER || process.env.USERNAME || "mem0-cli";
const userId = await promptLine(
` ${brand("Default User ID")} ${dim("(recommended)")}`,
"mem0-cli",
_systemUser,
);
if (userId) config.defaults.userId = userId;
}
@@ -214,7 +218,7 @@ async function validatePlatform(config: Mem0Config): Promise<void> {
} else {
printError(
`Could not connect: ${status.error ?? "Unknown error"}`,
"Check your API key and try again.",
"Visit https://app.mem0.ai/dashboard/api-keys to get a new key, or run mem0 init again.",
);
}
} catch (e) {
@@ -228,6 +232,7 @@ export async function runInit(
userId?: string;
email?: string;
code?: string;
force?: boolean;
} = {},
): Promise<void> {
const config = createDefaultConfig();
@@ -247,6 +252,40 @@ export async function runInit(
process.exit(1);
}
// Warn if an existing config with an API key would be overwritten
if (
!opts.force &&
fs.existsSync(CONFIG_FILE) &&
savedConfig.platform.apiKey
) {
console.log(
`\n ${brand("Existing configuration found")} ${dim(`(API key: ${redactKey(savedConfig.platform.apiKey)})`)}`,
);
if (process.stdin.isTTY) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const answer = await new Promise<string>((resolve) => {
rl.question(
" Overwrite existing config? This cannot be undone. [y/N] ",
resolve,
);
});
rl.close();
if (answer.toLowerCase() !== "y") {
printInfo("Cancelled. Use --force to skip this check.");
process.exit(0);
}
} else {
printError(
"Existing config would be overwritten.",
"Use --force to overwrite.",
);
process.exit(1);
}
}
// ── Email login flow ──────────────────────────────────────────────────────
if (opts.email) {
const email = opts.email.trim().toLowerCase();
@@ -268,7 +307,8 @@ export async function runInit(
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli";
config.defaults.userId =
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
saveConfig(config);
console.log();
@@ -283,6 +323,19 @@ export async function runInit(
// ── API key flow ──────────────────────────────────────────────────────────
// Non-TTY: resolve defaults so partial flags work in pipelines / CI
if (!process.stdin.isTTY) {
if (!opts.apiKey) {
printError(
"Non-interactive terminal detected and --api-key is required.",
"Usage: mem0 init --api-key <key> [--user-id <id>]",
);
process.exit(1);
}
opts.userId =
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
}
// Non-interactive: both flags provided
if (opts.apiKey && opts.userId) {
config.platform.apiKey = opts.apiKey;
@@ -293,15 +346,6 @@ export async function runInit(
return;
}
// Non-TTY without full flags: error with usage hint
if (!process.stdin.isTTY && (!opts.apiKey || !opts.userId)) {
printError(
"Non-interactive terminal detected and missing required flags.",
"Usage: mem0 init --api-key <key> --user-id <id>",
);
process.exit(1);
}
printBanner();
console.log();
printInfo("Welcome! Let's set up your mem0 CLI.\n");
@@ -341,7 +385,8 @@ export async function runInit(
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli";
config.defaults.userId =
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
saveConfig(config);
console.log();
+136 -27
View File
@@ -13,6 +13,7 @@ import {
} from "../branding.js";
import {
formatAddResult,
formatAgentEnvelope,
formatJson,
formatJsonEnvelope,
formatMemoriesTable,
@@ -20,6 +21,18 @@ import {
formatSingleMemory,
printResultSummary,
} from "../output.js";
import { isAgentMode, setCurrentCommand } from "../state.js";
/** True only when stdin is an actual pipe or file redirect — never in agent mode. */
function _stdinIsPiped(): boolean {
if (isAgentMode()) return false;
try {
const stat = fs.fstatSync(0);
return stat.isFIFO() || stat.isFile();
} catch {
return false;
}
}
export async function cmdAdd(
backend: Backend,
@@ -40,6 +53,7 @@ export async function cmdAdd(
output: string;
},
): Promise<void> {
setCurrentCommand("add");
let msgs: Record<string, unknown>[] | undefined;
let content = text;
@@ -64,8 +78,8 @@ export async function cmdAdd(
process.exit(1);
}
}
// Read from stdin if piped
else if (!content && !process.stdin.isTTY) {
// Read from stdin only if stdin is an actual pipe or file redirect
else if (!content && _stdinIsPiped()) {
content = fs.readFileSync(0, "utf-8").trim();
}
@@ -136,8 +150,43 @@ export async function cmdAdd(
if (opts.output === "quiet") return;
// Deduplicate PENDING entries sharing the same event_id across all output modes
const rawResults: Record<string, unknown>[] = Array.isArray(result)
? result
: ((result.results as Record<string, unknown>[]) ?? [result]);
const seenEvents = new Set<string>();
const deduped: Record<string, unknown>[] = [];
for (const r of rawResults) {
if (r.status === "PENDING") {
const eid = (r.event_id as string) ?? "";
if (eid && seenEvents.has(eid)) continue;
if (eid) seenEvents.add(eid);
}
deduped.push(r);
}
// Write back so downstream formatters see deduplicated data
const dedupedResult: Record<string, unknown> = Array.isArray(result)
? (deduped as unknown as Record<string, unknown>)
: { ...result, results: deduped };
if (opts.output === "agent") {
const scope: Record<string, string | undefined> = {
user_id: opts.userId,
agent_id: opts.agentId,
app_id: opts.appId,
run_id: opts.runId,
};
formatAgentEnvelope({
command: "add",
data: deduped,
scope,
count: deduped.length,
});
return;
}
if (opts.output === "json") {
formatAddResult(result, opts.output);
formatAddResult(dedupedResult, opts.output);
return;
}
@@ -148,14 +197,18 @@ export async function cmdAdd(
app_id: opts.appId,
run_id: opts.runId,
});
const results = Array.isArray(result)
? result
: ((result.results as unknown[]) ?? [result]);
const count = results.length;
printSuccess(
`Memory processed — ${count} memor${count === 1 ? "y" : "ies"} extracted`,
);
formatAddResult(result, opts.output);
const count = deduped.length;
const allPending = count > 0 && deduped.every((r) => r.status === "PENDING");
if (allPending) {
printSuccess(
`Memory queued — ${count} event${count !== 1 ? "s" : ""} pending`,
);
} else {
printSuccess(
`Memory processed — ${count} memor${count === 1 ? "y" : "ies"} extracted`,
);
}
formatAddResult(dedupedResult, opts.output);
}
export async function cmdSearch(
@@ -176,6 +229,7 @@ export async function cmdSearch(
output: string;
},
): Promise<void> {
setCurrentCommand("search");
if (!query) {
printError("No query provided. Pass a query argument or pipe via stdin.");
process.exit(1);
@@ -231,6 +285,23 @@ export async function cmdSearch(
if (opts.output === "quiet") return;
if (opts.output === "agent") {
const scope: Record<string, string | undefined> = {
user_id: opts.userId,
agent_id: opts.agentId,
app_id: opts.appId,
run_id: opts.runId,
};
formatAgentEnvelope({
command: "search",
data: results,
scope,
count: results.length,
durationMs: Math.round(elapsed * 1000),
});
return;
}
if (opts.output === "json") {
formatJson(results);
} else if (opts.output === "table") {
@@ -267,6 +338,7 @@ export async function cmdGet(
memoryId: string,
opts: { output: string },
): Promise<void> {
setCurrentCommand("get");
let result: Record<string, unknown>;
try {
result = await timedStatus("Fetching memory...", async () => {
@@ -277,7 +349,11 @@ export async function cmdGet(
process.exit(1);
}
formatSingleMemory(result, opts.output);
if (opts.output === "agent") {
formatAgentEnvelope({ command: "get", data: result });
} else {
formatSingleMemory(result, opts.output);
}
}
export async function cmdList(
@@ -296,6 +372,7 @@ export async function cmdList(
output: string;
},
): Promise<void> {
setCurrentCommand("list");
if (opts.pageSize < 1) {
printError("--page-size must be >= 1.");
process.exit(1);
@@ -330,12 +407,19 @@ export async function cmdList(
if (opts.output === "quiet") return;
if (opts.output === "json") {
formatJsonEnvelope({
if (opts.output === "agent" || opts.output === "json") {
const scope: Record<string, string | undefined> = {
user_id: opts.userId,
agent_id: opts.agentId,
app_id: opts.appId,
run_id: opts.runId,
};
formatAgentEnvelope({
command: "list",
data: results,
scope,
count: results.length,
scope: { user_id: opts.userId, agent_id: opts.agentId },
durationMs: Math.round(elapsed * 1000),
});
} else if (opts.output === "table") {
if (results.length > 0) {
@@ -374,6 +458,7 @@ export async function cmdUpdate(
text: string | undefined,
opts: { metadata?: string; output: string },
): Promise<void> {
setCurrentCommand("update");
let meta: Record<string, unknown> | undefined;
if (opts.metadata) {
try {
@@ -396,7 +481,13 @@ export async function cmdUpdate(
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
if (opts.output === "agent") {
formatAgentEnvelope({
command: "update",
data: result,
durationMs: Math.round(elapsed * 1000),
});
} else if (opts.output === "json") {
formatJson(result);
} else if (opts.output !== "quiet") {
printSuccess(
@@ -410,6 +501,7 @@ export async function cmdDelete(
memoryId: string,
opts: { output: string; dryRun?: boolean; force?: boolean },
): Promise<void> {
setCurrentCommand("delete");
if (opts.dryRun) {
let mem: Record<string, unknown>;
try {
@@ -436,7 +528,13 @@ export async function cmdDelete(
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
if (opts.output === "agent") {
formatAgentEnvelope({
command: "delete",
data: { id: memoryId, deleted: true },
durationMs: Math.round(elapsed * 1000),
});
} else if (opts.output === "json") {
formatJson(result);
} else if (opts.output !== "quiet") {
printSuccess(
@@ -458,16 +556,15 @@ export async function cmdDeleteAll(
output: string;
},
): Promise<void> {
setCurrentCommand("delete-all");
const { isAgentMode } = await import("../state.js");
if (isAgentMode() && !opts.force) {
printError("Destructive operation requires --force in agent mode.");
process.exit(1);
}
if (opts.all) {
// Project-wide wipe using wildcard entity IDs
if (opts.dryRun) {
printInfo("Would delete ALL memories project-wide.");
printInfo(
"Run without --dry-run to see the actual count of deleted memories.",
);
printInfo("No changes made.");
return;
}
// Note: --dry-run is ignored here because the API has no count-before-delete endpoint.
if (!opts.force) {
const readline = await import("node:readline");
@@ -509,7 +606,13 @@ export async function cmdDeleteAll(
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
if (opts.output === "agent") {
formatAgentEnvelope({
command: "delete-all",
data: result,
durationMs: Math.round(elapsed * 1000),
});
} else if (opts.output === "json") {
formatJson(result);
} else if (opts.output !== "quiet") {
if (result.message) {
@@ -586,7 +689,13 @@ export async function cmdDeleteAll(
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
if (opts.output === "agent") {
formatAgentEnvelope({
command: "delete-all",
data: result,
durationMs: Math.round(elapsed * 1000),
});
} else if (opts.output === "json") {
formatJson(result);
} else if (opts.output !== "quiet") {
if (result.message) {
+17 -7
View File
@@ -6,7 +6,8 @@ import fs from "node:fs";
import boxen from "boxen";
import type { Backend } from "../backend/base.js";
import { colors, printError, printSuccess, timedStatus } from "../branding.js";
import { formatJsonEnvelope } from "../output.js";
import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js";
import { setCurrentCommand } from "../state.js";
import { CLI_VERSION } from "../version.js";
const { brand, dim, success, error: errorColor } = colors;
@@ -15,6 +16,7 @@ export async function cmdStatus(
backend: Backend,
opts: { userId?: string; agentId?: string; output?: string } = {},
): Promise<void> {
setCurrentCommand("status");
const start = performance.now();
let result: Record<string, unknown>;
try {
@@ -29,14 +31,13 @@ export async function cmdStatus(
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJsonEnvelope({
if (opts.output === "agent" || opts.output === "json") {
formatAgentEnvelope({
command: "status",
data: {
connected: result.connected,
backend: result.backend ?? null,
base_url: result.base_url ?? null,
latency_ms: Math.round(elapsed * 1000),
},
durationMs: Math.round(elapsed * 1000),
});
@@ -56,6 +57,15 @@ export async function cmdStatus(
}
if (result.error) {
lines.push(` ${errorColor("Error:")} ${result.error}`);
if (String(result.error).includes("Authentication failed")) {
lines.push("");
lines.push(
` ${dim("Run")} ${brand("mem0 init")} ${dim("to reconfigure your API key")}`,
);
lines.push(
` ${dim("Get a key at")} ${brand("https://app.mem0.ai/dashboard/api-keys")}`,
);
}
}
lines.push(` ${dim("Latency:")} ${elapsed.toFixed(2)}s`);
@@ -81,6 +91,7 @@ export async function cmdImport(
filePath: string,
opts: { userId?: string; agentId?: string; output?: string },
): Promise<void> {
setCurrentCommand("import");
let data: Record<string, unknown>[];
try {
const raw = fs.readFileSync(filePath, "utf-8");
@@ -125,13 +136,12 @@ export async function cmdImport(
const elapsed = (performance.now() - start) / 1000;
console.log(); // Clear progress line
if (opts.output === "json") {
formatJsonEnvelope({
if (opts.output === "agent" || opts.output === "json") {
formatAgentEnvelope({
command: "import",
data: {
added,
failed,
duration_s: Number.parseFloat(elapsed.toFixed(2)),
},
durationMs: Math.round(elapsed * 1000),
});
+1 -1
View File
@@ -36,7 +36,7 @@ const COMMAND_GROUPS: { panel: string; commands: string[] }[] = [
},
{
panel: "Management",
commands: ["init", "status", "import", "help", "entity", "config"],
commands: ["init", "status", "import", "help", "entity", "event", "config"],
},
];
+109 -29
View File
@@ -13,6 +13,7 @@ import { colors, printError } from "./branding.js";
import type { Mem0Config } from "./config.js";
import { loadConfig } from "./config.js";
import { richFormatHelp } from "./help.js";
import { setAgentMode } from "./state.js";
import { CLI_VERSION } from "./version.js";
const program = new Command();
@@ -43,6 +44,13 @@ function getBackendOnly(apiKey?: string, baseUrl?: string): Backend {
return getBackendAndConfig(apiKey, baseUrl).backend;
}
function checkAgentMode(): boolean {
const rootOpts = program.opts();
const isAgent = !!(rootOpts.json || rootOpts.agent);
if (isAgent) setAgentMode(true);
return isAgent;
}
/**
* Resolve entity IDs: CLI flag > config default > undefined.
*
@@ -105,6 +113,11 @@ program
console.log(` ${colors.brand("◆ Mem0")} CLI v${CLI_VERSION}`);
process.exit(0);
})
.option("--json", "Output as JSON for agent/programmatic use.")
.option(
"--agent",
"Output as JSON for agent/programmatic use. (alias: --json)",
)
.usage("<command> [options]")
.helpOption("--help", "Show this message and exit.")
.addHelpCommand(false)
@@ -114,9 +127,7 @@ program
program
.command("init")
.description(
"Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
)
.description("Interactive setup wizard for mem0 CLI.")
.option("--api-key <key>", "API key (skip prompt).")
.option("-u, --user-id <id>", "Default user ID (skip prompt).")
.option("--email <email>", "Login via email verification code.")
@@ -124,6 +135,7 @@ program
"--code <code>",
"Verification code (use with --email for non-interactive login).",
)
.option("--force", "Overwrite existing config without confirmation.", false)
.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",
@@ -135,6 +147,7 @@ program
userId: opts.userId,
email: opts.email,
code: opts.code,
force: opts.force,
});
});
@@ -165,17 +178,21 @@ program
)
.action(async (text, opts) => {
const { cmdAdd } = await import("./commands/memory.js");
const isAgent = checkAgentMode();
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
const ids = resolveIds(config, opts);
const enableGraph = resolveGraph(config, opts);
await cmdAdd(backend, text, { ...ids, ...opts, enableGraph });
const output = isAgent ? "agent" : opts.output;
await cmdAdd(backend, text, { ...ids, ...opts, enableGraph, output });
});
// ── Memory: search ────────────────────────────────────────────────────────
program
.command("search [query]")
.description("Search memories by semantic query.")
.description(
"Query your memory store — semantic, keyword, or hybrid retrieval.",
)
.option("-u, --user-id <id>", "Filter by user.")
.option("--agent-id <id>", "Filter by agent.")
.option("--app-id <id>", "Filter by app.")
@@ -215,9 +232,11 @@ program
process.exit(1);
}
const { cmdSearch } = await import("./commands/memory.js");
const isAgent = checkAgentMode();
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
const ids = resolveIds(config, opts);
const enableGraph = resolveGraph(config, opts);
const output = isAgent ? "agent" : opts.output;
await cmdSearch(backend, resolvedQuery, {
...ids,
topK: opts.topK,
@@ -227,7 +246,7 @@ program
filterJson: opts.filter,
fields: opts.fields,
enableGraph,
output: opts.output,
output,
});
});
@@ -245,8 +264,10 @@ program
)
.action(async (memoryId, opts) => {
const { cmdGet } = await import("./commands/memory.js");
const isAgent = checkAgentMode();
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
await cmdGet(backend, memoryId, { output: opts.output });
const output = isAgent ? "agent" : opts.output;
await cmdGet(backend, memoryId, { output });
});
// ── Memory: list ──────────────────────────────────────────────────────────
@@ -279,9 +300,11 @@ program
)
.action(async (opts) => {
const { cmdList } = await import("./commands/memory.js");
const isAgent = checkAgentMode();
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
const ids = resolveIds(config, opts);
const enableGraph = resolveGraph(config, opts);
const output = isAgent ? "agent" : opts.output;
await cmdList(backend, {
...ids,
page: opts.page,
@@ -290,7 +313,7 @@ program
after: opts.after,
before: opts.before,
enableGraph,
output: opts.output,
output,
});
});
@@ -313,10 +336,12 @@ program
resolvedText = fs.readFileSync(0, "utf-8").trim();
}
const { cmdUpdate } = await import("./commands/memory.js");
const isAgent = checkAgentMode();
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
const output = isAgent ? "agent" : opts.output;
await cmdUpdate(backend, memoryId, resolvedText, {
metadata: opts.metadata,
output: opts.output,
output,
});
});
@@ -352,6 +377,8 @@ program
].join("\n"),
)
.action(async (memoryId, opts) => {
const isAgent = checkAgentMode();
const output = isAgent ? "agent" : opts.output;
// ── Mutual-exclusion checks ──
if (memoryId && opts.all) {
printError("Cannot combine <memoryId> with --all. Use one or the other.");
@@ -382,7 +409,7 @@ program
const { cmdDelete } = await import("./commands/memory.js");
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
await cmdDelete(backend, memoryId, {
output: opts.output,
output,
dryRun: opts.dryRun,
force: opts.force,
});
@@ -409,7 +436,7 @@ program
dryRun: opts.dryRun,
all: opts.project,
...ids,
output: opts.output,
output,
});
return;
}
@@ -418,7 +445,7 @@ program
if (opts.entity) {
const { cmdEntitiesDelete } = await import("./commands/entities.js");
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
await cmdEntitiesDelete(backend, opts);
await cmdEntitiesDelete(backend, { ...opts, output });
return;
}
});
@@ -440,7 +467,9 @@ configCmd
)
.action(async (opts) => {
const { cmdConfigShow } = await import("./commands/config.js");
cmdConfigShow({ output: opts.output });
const isAgent = checkAgentMode();
const output = isAgent ? "agent" : opts.output;
cmdConfigShow({ output });
});
configCmd
@@ -452,6 +481,7 @@ configCmd
)
.action(async (key) => {
const { cmdConfigGet } = await import("./commands/config.js");
checkAgentMode();
cmdConfigGet(key);
});
@@ -464,6 +494,7 @@ configCmd
)
.action(async (key, value) => {
const { cmdConfigSet } = await import("./commands/config.js");
checkAgentMode();
cmdConfigSet(key, value);
});
@@ -487,8 +518,10 @@ entityCmd
)
.action(async (entityType, opts) => {
const { cmdEntitiesList } = await import("./commands/entities.js");
const isAgent = checkAgentMode();
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
await cmdEntitiesList(backend, entityType, { output: opts.output });
const output = isAgent ? "agent" : opts.output;
await cmdEntitiesList(backend, entityType, { output });
});
entityCmd
@@ -509,8 +542,54 @@ entityCmd
)
.action(async (opts) => {
const { cmdEntitiesDelete } = await import("./commands/entities.js");
const isAgent = checkAgentMode();
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
await cmdEntitiesDelete(backend, opts);
const output = isAgent ? "agent" : opts.output;
await cmdEntitiesDelete(backend, { ...opts, output });
});
// ── Event subcommands ─────────────────────────────────────────────────────
const eventCmd = program
.command("event")
.description("Inspect background processing events.")
.addHelpCommand(false)
.configureHelp({ formatHelp: richFormatHelp });
eventCmd
.command("list")
.description("List recent background processing events.")
.option("-o, --output <format>", "Output: table, json.", "table")
.option("--api-key <key>", "Override API key.")
.option("--base-url <url>", "Override API base URL.")
.addHelpText(
"after",
"\nExamples:\n $ mem0 event list\n $ mem0 event list -o json",
)
.action(async (opts) => {
const { cmdEventList } = await import("./commands/events.js");
const isAgent = checkAgentMode();
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
const output = isAgent ? "agent" : opts.output;
await cmdEventList(backend, { output });
});
eventCmd
.command("status <eventId>")
.description("Check the status of a specific background event.")
.option("-o, --output <format>", "Output: text, json.", "text")
.option("--api-key <key>", "Override API key.")
.option("--base-url <url>", "Override API base URL.")
.addHelpText(
"after",
"\nExamples:\n $ mem0 event status <event-id>\n $ mem0 event status <event-id> -o json",
)
.action(async (eventId, opts) => {
const { cmdEventStatus } = await import("./commands/events.js");
const isAgent = checkAgentMode();
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
const output = isAgent ? "agent" : opts.output;
await cmdEventStatus(backend, eventId, { output });
});
// ── Utility commands ──────────────────────────────────────────────────────
@@ -524,11 +603,13 @@ program
.addHelpText("after", "\nExamples:\n $ mem0 status\n $ mem0 status -o json")
.action(async (opts) => {
const { cmdStatus } = await import("./commands/utils.js");
const isAgent = checkAgentMode();
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
const output = isAgent ? "agent" : opts.output;
await cmdStatus(backend, {
userId: config.defaults.userId || undefined,
agentId: config.defaults.agentId || undefined,
output: opts.output,
output,
});
});
@@ -546,12 +627,14 @@ program
)
.action(async (filePath, opts) => {
const { cmdImport } = await import("./commands/utils.js");
const isAgent = checkAgentMode();
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
const ids = resolveIds(config, opts);
const output = isAgent ? "agent" : opts.output;
await cmdImport(backend, filePath, {
userId: ids.userId,
agentId: ids.agentId,
output: opts.output,
output,
});
});
@@ -565,7 +648,9 @@ program
.option("--json", "Output machine-readable JSON for LLM agents.", false)
.addHelpText("after", "\nExamples:\n $ mem0 help\n $ mem0 help --json")
.action((opts) => {
if (opts.json) {
// opts.json is set when `mem0 help --json` is used (subcommand flag).
// program.opts().json is set when the root --json global flag was used first.
if (opts.json || program.opts().json) {
// Load spec from parent directory
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const specPath = path.join(__dirname, "..", "..", "cli-spec.json");
@@ -595,7 +680,9 @@ program
console.log(
" add Add a memory from text, messages, file, or stdin",
);
console.log(" search Search memories by semantic query");
console.log(
" search Query your memory store (semantic, keyword, hybrid)",
);
console.log(" get Get a specific memory by ID");
console.log(" list List memories with optional filters");
console.log(" update Update a memory's text or metadata");
@@ -605,6 +692,9 @@ program
console.log(" import Import memories from a JSON file");
console.log(" config Manage configuration (show, get, set)");
console.log(" entity Manage entities (list, delete)");
console.log(
" event Inspect background events (list, status)",
);
console.log(" init Interactive setup wizard");
console.log(" status Check connectivity and authentication");
console.log();
@@ -616,16 +706,6 @@ program
}
});
// ── Version ───────────────────────────────────────────────────────────────
program
.command("version")
.description("Show version.")
.action(async () => {
const { cmdVersion } = await import("./commands/utils.js");
cmdVersion();
});
// ── Entrypoint ────────────────────────────────────────────────────────────
program.parse();
+124 -3
View File
@@ -169,17 +169,26 @@ export function formatAddResult(
}
console.log();
const seenPendingEvents = new Set<string>();
for (const r of results) {
// Detect async PENDING response
if (r.status === "PENDING") {
const eventId = ((r.event_id as string) ?? "").slice(0, 8);
const eventId = (r.event_id as string) ?? "";
// Deduplicate PENDING entries with the same event_id
if (eventId && seenPendingEvents.has(eventId)) continue;
if (eventId) seenPendingEvents.add(eventId);
const icon = accent(sym("⧗", "..."));
const parts = [
` ${icon} ${dim("Queued".padEnd(10))}`,
"Processing in background",
];
if (eventId) parts.push(dim(`(event ${eventId})`));
console.log(parts.join(" "));
if (eventId) {
console.log(` ${dim(` event_id: ${eventId}`)}`);
console.log(
` ${dim(` → Check status: mem0 event status ${eventId}`)}`,
);
}
continue;
}
@@ -238,6 +247,118 @@ export function formatJsonEnvelope(opts: {
console.log(JSON.stringify(envelope, null, 2));
}
function pick(
obj: Record<string, unknown>,
keys: string[],
): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const key of keys) {
if (key in obj) result[key] = obj[key];
}
return result;
}
export function sanitizeAgentData(command: string, data: unknown): unknown {
if (data === null || data === undefined) return data;
switch (command) {
case "add": {
const items = Array.isArray(data) ? data : [data];
return items.map((item) => {
const r = item as Record<string, unknown>;
if (r.status === "PENDING") return pick(r, ["status", "event_id"]);
return pick(r, ["id", "memory", "event"]);
});
}
case "search":
return (data as Record<string, unknown>[]).map((r) =>
pick(r, ["id", "memory", "score", "created_at", "categories"]),
);
case "list":
return (data as Record<string, unknown>[]).map((r) =>
pick(r, ["id", "memory", "created_at", "categories"]),
);
case "get": {
const r = data as Record<string, unknown>;
return pick(r, [
"id",
"memory",
"created_at",
"updated_at",
"categories",
"metadata",
]);
}
case "update": {
const r = data as Record<string, unknown>;
return pick(r, ["id", "memory"]);
}
case "delete":
case "delete-all":
case "entity delete":
return data;
case "entity list":
return (data as Record<string, unknown>[]).map((r) => ({
name: (r.name ?? r.id) as string,
...pick(r, ["type", "count"]),
}));
case "event list":
return (data as Record<string, unknown>[]).map((r) =>
pick(r, ["id", "event_type", "status", "latency", "created_at"]),
);
case "event status": {
const ev = data as Record<string, unknown>;
const rawResults =
(ev.results as Record<string, unknown>[] | undefined) ?? [];
const sanitizedResults = rawResults.map((r) => {
const nested = r.data as Record<string, unknown> | undefined;
return {
id: r.id,
event: r.event,
user_id: r.user_id,
memory: nested?.memory ?? null,
};
});
return {
...pick(ev, [
"id",
"event_type",
"status",
"latency",
"created_at",
"updated_at",
]),
results: sanitizedResults,
};
}
default:
return data;
}
}
export function formatAgentEnvelope(opts: {
command: string;
data: unknown;
durationMs?: number;
scope?: Record<string, string | undefined>;
count?: number;
}): void {
const envelope: Record<string, unknown> = {
status: "success",
command: opts.command,
};
if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs;
if (opts.scope) {
const filtered = Object.fromEntries(
Object.entries(opts.scope).filter(([, v]) => v),
);
if (Object.keys(filtered).length > 0) envelope.scope = filtered;
}
if (opts.count !== undefined) envelope.count = opts.count;
envelope.data = sanitizeAgentData(opts.command, opts.data);
console.log(JSON.stringify(envelope, null, 2));
}
export function printResultSummary(opts: {
count: number;
durationSecs?: number;
@@ -249,7 +370,7 @@ export function printResultSummary(opts: {
if (opts.scopeIds) {
const scopeParts = Object.entries(opts.scopeIds)
.filter(([, v]) => v)
.map(([k, v]) => `${k.replace(/_/g, " ")}=${v}`);
.map(([k, v]) => `${k}=${v}`);
if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
}
if (opts.durationSecs !== undefined)
+23
View File
@@ -0,0 +1,23 @@
/**
* Agent mode state — set by the root program option handler,
* read by commands and branding functions.
*/
let _agentMode = false;
let _currentCommand = "";
export function isAgentMode(): boolean {
return _agentMode;
}
export function setAgentMode(val: boolean): void {
_agentMode = val;
}
export function getCurrentCommand(): string {
return _currentCommand;
}
export function setCurrentCommand(name: string): void {
_currentCommand = name;
}
+220
View File
@@ -5,6 +5,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { createMockBackend } from "./setup.js";
import type { Backend } from "../src/backend/base.js";
import { setAgentMode } from "../src/state.js";
let mockBackend: Backend;
@@ -31,6 +32,7 @@ import { afterEach } from "vitest";
afterEach(() => {
console.log = originalLog;
console.error = originalError;
setAgentMode(false);
});
describe("cmdAdd", () => {
@@ -84,6 +86,59 @@ describe("cmdAdd", () => {
});
});
describe("cmdAdd deduplicates PENDING", () => {
const DUPLICATE_PENDING = {
results: [
{ status: "PENDING", event_id: "evt-dup" },
{ status: "PENDING", event_id: "evt-dup" },
],
};
it("text shows one pending block", async () => {
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
noInfer: false,
enableGraph: false,
output: "text",
});
expect(output.match(/Queued/g)?.length).toBe(1);
});
it("json shows one pending entry", async () => {
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
noInfer: false,
enableGraph: false,
output: "json",
});
const data = JSON.parse(output);
const pending = data.results.filter((r: Record<string, unknown>) => r.status === "PENDING");
expect(pending).toHaveLength(1);
});
it("agent shows one pending entry", async () => {
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
setAgentMode(true);
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
noInfer: false,
enableGraph: false,
output: "agent",
});
const data = JSON.parse(output);
expect(data.count).toBe(1);
expect(data.data).toHaveLength(1);
});
});
describe("cmdSearch", () => {
it("searches and shows results in text mode", async () => {
const { cmdSearch } = await import("../src/commands/memory.js");
@@ -219,3 +274,168 @@ describe("cmdEntitiesList", () => {
expect(output).toContain("alice");
});
});
describe("cmdEventList", () => {
it("lists events in table mode", async () => {
const { cmdEventList } = await import("../src/commands/events.js");
await cmdEventList(mockBackend, { output: "table" });
expect(output).toContain("evt-abc-");
expect(output).toContain("ADD");
expect(output).toContain("SUCCEEDED");
});
it("lists events in json mode", async () => {
const { cmdEventList } = await import("../src/commands/events.js");
await cmdEventList(mockBackend, { output: "json" });
expect(output).toContain("evt-abc-123-def-456");
expect(output).toContain("evt-def-456-ghi-789");
});
it("shows empty message when no events", async () => {
(mockBackend.listEvents as ReturnType<typeof vi.fn>).mockResolvedValueOnce([]);
const { cmdEventList } = await import("../src/commands/events.js");
await cmdEventList(mockBackend, { output: "table" });
expect((output + errOutput).toLowerCase()).toContain("no events");
});
});
describe("cmdEventStatus", () => {
it("shows event details in text mode", async () => {
const { cmdEventStatus } = await import("../src/commands/events.js");
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "text" });
expect(output).toContain("evt-abc-123-def-456");
expect(output).toContain("SUCCEEDED");
});
it("shows event details in json mode", async () => {
const { cmdEventStatus } = await import("../src/commands/events.js");
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "json" });
expect(output).toContain("evt-abc-123-def-456");
expect(output).toContain("ADD");
});
});
describe("agent mode", () => {
it("cmdAdd outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "test preference", {
userId: "alice",
immutable: false,
noInfer: false,
enableGraph: false,
output: "agent",
});
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("add");
expect(parsed.data).toBeDefined();
expect(parsed.scope).toMatchObject({ user_id: "alice" });
expect(Object.keys(parsed.data[0]).sort()).toEqual(["event", "id", "memory"].sort());
});
it("cmdSearch outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdSearch } = await import("../src/commands/memory.js");
await cmdSearch(mockBackend, "preferences", {
userId: "alice",
topK: 10,
threshold: 0.3,
rerank: false,
keyword: false,
enableGraph: false,
output: "agent",
});
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("search");
expect(Array.isArray(parsed.data)).toBe(true);
expect(parsed.count).toBe(2);
const keys = Object.keys(parsed.data[0]);
expect(keys).toContain("id");
expect(keys).toContain("memory");
expect(keys).toContain("score");
expect(keys).toContain("created_at");
expect(keys).toContain("categories");
expect(keys).not.toContain("user_id");
expect(keys).not.toContain("agent_id");
});
it("cmdList outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdList } = await import("../src/commands/memory.js");
await cmdList(mockBackend, {
userId: "alice",
page: 1,
pageSize: 100,
enableGraph: false,
output: "agent",
});
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("list");
expect(Array.isArray(parsed.data)).toBe(true);
expect(parsed.count).toBe(2);
expect(Object.keys(parsed.data[0]).sort()).toEqual(["categories", "created_at", "id", "memory"]);
});
it("cmdGet outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdGet } = await import("../src/commands/memory.js");
await cmdGet(mockBackend, "abc-123-def-456", { output: "agent" });
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("get");
expect(parsed.data).toBeDefined();
expect(parsed.data).toMatchObject({ id: "abc-123-def-456" });
expect(Object.keys(parsed.data)).not.toContain("user_id");
});
it("cmdUpdate outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdUpdate } = await import("../src/commands/memory.js");
await cmdUpdate(mockBackend, "abc-123", "Updated text", { output: "agent" });
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("update");
expect(parsed.data).toBeDefined();
});
it("cmdDelete outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdDelete } = await import("../src/commands/memory.js");
await cmdDelete(mockBackend, "abc-123", { output: "agent" });
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("delete");
expect(parsed.data).toBeDefined();
});
it("cmdEventList outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdEventList } = await import("../src/commands/events.js");
await cmdEventList(mockBackend, { output: "agent" });
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("event list");
expect(Array.isArray(parsed.data)).toBe(true);
expect(parsed.count).toBe(2);
expect(Object.keys(parsed.data[0]).sort()).toEqual(
["created_at", "event_type", "id", "latency", "status"],
);
expect(Object.keys(parsed.data[0])).not.toContain("updated_at");
});
it("cmdEventStatus outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdEventStatus } = await import("../src/commands/events.js");
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "agent" });
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("event status");
expect(parsed.data).toBeDefined();
expect(parsed.data).toMatchObject({ id: "evt-abc-123-def-456" });
expect(parsed.data.results[0]).toHaveProperty("memory");
expect(parsed.data.results[0]).not.toHaveProperty("data");
});
});
+83
View File
@@ -10,6 +10,7 @@ import {
formatSingleMemory,
formatAddResult,
printResultSummary,
sanitizeAgentData,
} from "../src/output.js";
let output: string;
@@ -98,6 +99,18 @@ describe("formatAddResult", () => {
});
expect(output).toContain("Queued");
});
it("deduplicates PENDING entries with same event_id", () => {
formatAddResult({
results: [
{ status: "PENDING", event_id: "evt-dup" },
{ status: "PENDING", event_id: "evt-dup" },
],
});
// Should show only one PENDING block despite two entries with same event_id
expect(output.match(/Queued/g)?.length).toBe(1);
expect(output.match(/evt-dup/g)?.length).toBe(2); // event_id line + status hint line
});
});
describe("printResultSummary", () => {
@@ -113,3 +126,73 @@ describe("printResultSummary", () => {
expect(output).not.toContain("results");
});
});
describe("sanitizeAgentData", () => {
it("projects add results", () => {
const raw = [{ id: "abc", memory: "test", event: "ADD", metadata: { x: 1 }, categories: ["a"] }];
const result = sanitizeAgentData("add", raw) as Record<string, unknown>[];
expect(result).toEqual([{ id: "abc", memory: "test", event: "ADD" }]);
});
it("passes through PENDING add items", () => {
const raw = [{ status: "PENDING", event_id: "evt-123", noise: "x" }];
const result = sanitizeAgentData("add", raw) as Record<string, unknown>[];
expect(result).toEqual([{ status: "PENDING", event_id: "evt-123" }]);
});
it("projects search results", () => {
const raw = [{ id: "abc", memory: "test", score: 0.9, created_at: "2026-01-01", categories: ["a"], user_id: "u1" }];
const result = sanitizeAgentData("search", raw) as Record<string, unknown>[];
expect(result[0]).not.toHaveProperty("user_id");
expect(result[0]).toHaveProperty("score");
});
it("projects list results", () => {
const raw = [{ id: "abc", memory: "test", created_at: "2026-01-01", categories: ["a"], user_id: "u1" }];
const result = sanitizeAgentData("list", raw) as Record<string, unknown>[];
expect(Object.keys(result[0]).sort()).toEqual(["categories", "created_at", "id", "memory"]);
});
it("projects get result", () => {
const raw = { id: "abc", memory: "test", created_at: "2026-01-01", updated_at: "2026-01-02", categories: ["a"], metadata: { k: "v" }, user_id: "u1" };
const result = sanitizeAgentData("get", raw) as Record<string, unknown>;
expect(result).not.toHaveProperty("user_id");
expect(result).toHaveProperty("metadata");
});
it("projects update result", () => {
const raw = { id: "abc", memory: "updated", extra: "noise" };
const result = sanitizeAgentData("update", raw);
expect(result).toEqual({ id: "abc", memory: "updated" });
});
it("projects event list results", () => {
const raw = [{ id: "evt-1", event_type: "ADD", status: "SUCCEEDED", graph_status: null, latency: 100, created_at: "2026-01-01", updated_at: "2026-01-02" }];
const result = sanitizeAgentData("event list", raw) as Record<string, unknown>[];
expect(result[0]).not.toHaveProperty("updated_at");
expect(result[0]).not.toHaveProperty("graph_status");
});
it("flattens event status results", () => {
const raw = {
id: "evt-1", event_type: "ADD", status: "SUCCEEDED",
latency: 100, created_at: "2026-01-01", updated_at: "2026-01-02",
results: [{ id: "mem-1", event: "ADD", user_id: "alice", data: { memory: "dark mode" } }],
};
const result = sanitizeAgentData("event status", raw) as Record<string, unknown>;
const firstResult = (result.results as Record<string, unknown>[])[0];
expect(firstResult).toHaveProperty("memory", "dark mode");
expect(firstResult).not.toHaveProperty("data");
});
it("passes through status/config/import commands unchanged", () => {
const data = { key: "value", other: "stuff" };
for (const cmd of ["status", "import", "config show", "config get", "config set"]) {
expect(sanitizeAgentData(cmd, data)).toEqual(data);
}
});
it("handles null data", () => {
expect(sanitizeAgentData("add", null)).toBeNull();
});
});
+37
View File
@@ -71,5 +71,42 @@ export function createMockBackend(): Backend {
{ name: "alice", count: 5 },
{ name: "bob", count: 3 },
]),
listEvents: vi.fn().mockResolvedValue([
{
id: "evt-abc-123-def-456",
event_type: "ADD",
status: "SUCCEEDED",
graph_status: null,
latency: 1234.5,
created_at: "2026-04-01T10:00:00Z",
updated_at: "2026-04-01T10:00:01Z",
},
{
id: "evt-def-456-ghi-789",
event_type: "SEARCH",
status: "PENDING",
graph_status: null,
latency: null,
created_at: "2026-04-01T10:01:00Z",
updated_at: "2026-04-01T10:01:00Z",
},
]),
getEvent: vi.fn().mockResolvedValue({
id: "evt-abc-123-def-456",
event_type: "ADD",
status: "SUCCEEDED",
graph_status: "SUCCEEDED",
latency: 1234.5,
created_at: "2026-04-01T10:00:00Z",
updated_at: "2026-04-01T10:00:01Z",
results: [
{
id: "mem-abc-123",
event: "ADD",
user_id: "alice",
data: { memory: "User prefers dark mode" },
},
],
}),
};
}
+137 -15
View File
@@ -3,6 +3,8 @@
from __future__ import annotations
import json as _json
import os
import stat as _stat_mod
import sys
from pathlib import Path
@@ -43,7 +45,14 @@ entity_app = typer.Typer(
no_args_is_help=True,
rich_markup_mode="rich",
)
# entity_app registered after Memory commands to control panel ordering
event_app = typer.Typer(
name="event",
help="Inspect background processing events.",
no_args_is_help=True,
rich_markup_mode="rich",
)
# entity_app and event_app registered after Memory commands to control panel ordering
# ── Helpers ───────────────────────────────────────────────────────────────
@@ -114,9 +123,22 @@ def _resolve_ids(
}
def _stdin_is_piped() -> bool:
"""Return True only when stdin is an actual pipe or file redirect — not a bare open fd."""
from mem0_cli.state import is_agent_mode
if is_agent_mode():
return False
try:
mode = os.fstat(sys.stdin.fileno()).st_mode
return _stat_mod.S_ISFIFO(mode) or _stat_mod.S_ISREG(mode)
except Exception:
return False
def _read_stdin() -> str | None:
"""Read from stdin if it is piped (not a TTY)."""
if not sys.stdin.isatty():
"""Read from stdin if it is an actual pipe or file redirect (not a TTY, not agent mode)."""
if _stdin_is_piped():
return sys.stdin.read().strip() or None
return None
@@ -128,7 +150,18 @@ def _read_stdin() -> str | None:
def main_callback(
ctx: typer.Context,
version: bool = typer.Option(False, "--version", help="Show version and exit."),
json_agent: bool = typer.Option(
False,
"--json",
"--agent",
help="Output as JSON for agent/programmatic use.",
is_eager=False,
),
) -> None:
if json_agent:
from mem0_cli.state import set_agent_mode
set_agent_mode(True)
if version:
from mem0_cli.commands.utils import cmd_version
@@ -273,7 +306,7 @@ def search(
None, "--base-url", help="Override API base URL.", rich_help_panel="Connection"
),
) -> None:
"""Search memories by semantic query.
"""Query your memory store — semantic, keyword, or hybrid retrieval.
Examples:
mem0 search "preferences" --user-id alice
@@ -702,6 +735,70 @@ def entity_delete(
app.add_typer(entity_app, name="entity", rich_help_panel="Management")
# ── Event subcommands ─────────────────────────────────────────────────────
@event_app.command("list")
def event_list(
output: str = typer.Option(
"table", "--output", "-o", help="Output: table, json.", rich_help_panel="Output"
),
api_key: str | None = typer.Option(
None,
"--api-key",
help="Override API key.",
envvar="MEM0_API_KEY",
rich_help_panel="Connection",
),
base_url: str | None = typer.Option(
None, "--base-url", help="Override API base URL.", rich_help_panel="Connection"
),
) -> None:
"""List recent background processing events.
Examples:
mem0 event list
mem0 event list -o json
"""
from mem0_cli.commands.events_cmd import cmd_event_list
backend = _get_backend(api_key, base_url)
cmd_event_list(backend, output=output)
@event_app.command("status")
def event_status(
event_id: str = typer.Argument(..., help="Event ID to inspect."),
output: str = typer.Option(
"text", "--output", "-o", help="Output: text, json.", rich_help_panel="Output"
),
api_key: str | None = typer.Option(
None,
"--api-key",
help="Override API key.",
envvar="MEM0_API_KEY",
rich_help_panel="Connection",
),
base_url: str | None = typer.Option(
None, "--base-url", help="Override API base URL.", rich_help_panel="Connection"
),
) -> None:
"""Check the status of a specific background event.
Examples:
mem0 event status <event-id>
mem0 event status <event-id> -o json
"""
from mem0_cli.commands.events_cmd import cmd_event_status
backend = _get_backend(api_key, base_url)
cmd_event_status(backend, event_id, output=output)
# ── Event subgroup ──
app.add_typer(event_app, name="event", rich_help_panel="Management")
# ── Management commands ───────────────────────────────────────────────────
@@ -715,6 +812,9 @@ def init(
code: str | None = typer.Option(
None, "--code", help="Verification code (use with --email for non-interactive login)."
),
force: bool = typer.Option(
False, "--force", help="Overwrite existing config without confirmation."
),
) -> None:
"""Interactive setup wizard for mem0 CLI.
@@ -726,7 +826,7 @@ def init(
"""
from mem0_cli.commands.init_cmd import run_init
run_init(api_key=api_key, user_id=user_id, email=email, code=code)
run_init(api_key=api_key, user_id=user_id, email=email, code=code, force=force)
# (entity_app registered at module level, below sub-group definitions)
@@ -831,7 +931,7 @@ def _build_help_json() -> dict:
},
},
"search": {
"description": "Search memories by semantic query.",
"description": "Query your memory store — semantic, keyword, or hybrid retrieval.",
"usage": "mem0 search <query> [OPTIONS]",
"arguments": {"query": {"description": "Search query.", "required": False}},
"options": {
@@ -935,6 +1035,24 @@ def _build_help_json() -> dict:
"value": {"description": "Value to set.", "required": True},
},
},
"event": {
"description": "Inspect background processing events.",
"subcommands": {
"list": {
"description": "List recent background processing events.",
"usage": "mem0 event list [OPTIONS]",
"options": {"--output, -o": "Output format: table, json."},
},
"status": {
"description": "Check the status of a specific background event.",
"usage": "mem0 event status <event_id> [OPTIONS]",
"arguments": {
"event_id": {"description": "Event ID to inspect.", "required": True}
},
"options": {"--output, -o": "Output format: text, json."},
},
},
},
"entity": {
"description": "Manage entities.",
"subcommands": {
@@ -986,6 +1104,7 @@ def _build_help_json() -> dict:
"global_options": {
"--api-key": "Override API key (env: MEM0_API_KEY).",
"--base-url": "Override API base URL.",
"--json / --agent": "Output as JSON for agent/programmatic use.",
"--help": "Show help for a command.",
"--version": "Show version and exit.",
},
@@ -1015,7 +1134,7 @@ def help(
console.print("Usage: mem0 <command> [OPTIONS]\n")
console.print("[bold]Commands:[/]")
console.print(" add Add a memory from text, messages, file, or stdin")
console.print(" search Search memories by semantic query")
console.print(" search Query your memory store (semantic, keyword, hybrid)")
console.print(" get Get a specific memory by ID")
console.print(" list List memories with optional filters")
console.print(" update Update a memory's text or metadata")
@@ -1023,6 +1142,7 @@ def help(
console.print(" import Import memories from a JSON file")
console.print(" config Manage configuration (show, get, set)")
console.print(" entity Manage entities (list, delete)")
console.print(" event Inspect background events (list, status)")
console.print(" init Interactive setup wizard")
console.print(" status Check connectivity and authentication")
console.print()
@@ -1031,14 +1151,6 @@ def help(
console.print()
@app.command(rich_help_panel="Utility")
def version() -> None:
"""Show version and exit."""
from mem0_cli.commands.utils import cmd_version
cmd_version()
# Register config subgroup here so it appears after help in Management panel
app.add_typer(config_app, name="config", rich_help_panel="Management")
@@ -1047,4 +1159,14 @@ app.add_typer(config_app, name="config", rich_help_panel="Management")
def main() -> None:
import sys
# Allow --json/--agent anywhere in the command line (not just before subcommand).
_json_flags = {"--json", "--agent"}
if any(a in _json_flags for a in sys.argv[1:]):
from mem0_cli.state import set_agent_mode
set_agent_mode(True)
sys.argv = [sys.argv[0]] + [a for a in sys.argv[1:] if a not in _json_flags]
app()
+6
View File
@@ -104,6 +104,12 @@ class Backend(ABC):
@abstractmethod
def entities(self, entity_type: str) -> list[dict]: ...
@abstractmethod
def list_events(self) -> list[dict]: ...
@abstractmethod
def get_event(self, event_id: str) -> dict: ...
def get_backend(config: Mem0Config) -> Backend:
"""Return the Platform backend."""
+23 -23
View File
@@ -265,18 +265,21 @@ class PlatformBackend(Backend):
app_id: str | None = None,
run_id: str | None = None,
) -> dict:
params: dict[str, str] = {}
if user_id:
params["user_id"] = user_id
if agent_id:
params["agent_id"] = agent_id
if app_id:
params["app_id"] = app_id
if run_id:
params["run_id"] = run_id
if not params:
# v2 endpoint: DELETE /v2/entities/{entity_type}/{entity_id}/
type_map = {
"user": user_id,
"agent": agent_id,
"app": app_id,
"run": run_id,
}
entities = {t: v for t, v in type_map.items() if v}
if not entities:
raise ValueError("At least one entity ID is required for delete_entities.")
return self._request("DELETE", "/v1/entities/", params=params)
# Delete each provided entity via the v2 path-based endpoint
result: dict = {}
for entity_type, entity_id in entities.items():
result = self._request("DELETE", f"/v2/entities/{entity_type}/{entity_id}/")
return result
def status(
self,
@@ -284,19 +287,9 @@ class PlatformBackend(Backend):
user_id: str | None = None,
agent_id: str | None = None,
) -> dict[str, Any]:
"""Check connectivity by making a lightweight API call."""
"""Check connectivity using the ping endpoint."""
try:
# If entity IDs are available, validate with a minimal memories list
if user_id or agent_id:
payload: dict[str, Any] = {}
params = {"page": "1", "page_size": "1"}
api_filters = self._build_filters(user_id=user_id, agent_id=agent_id)
if api_filters:
payload["filters"] = api_filters
self._request("POST", "/v2/memories/", json=payload, params=params)
else:
# No entity IDs — use entities endpoint to validate API key
self._request("GET", "/v1/entities/")
self._request("GET", "/v1/ping/")
return {"connected": True, "backend": "platform", "base_url": self.base_url}
except Exception as e:
return {"connected": False, "backend": "platform", "error": str(e)}
@@ -311,6 +304,13 @@ class PlatformBackend(Backend):
items = [e for e in items if e.get("type", "").lower() == target_type]
return items
def list_events(self) -> list[dict]:
result = self._request("GET", "/v1/events/")
return result if isinstance(result, list) else result.get("results", [])
def get_event(self, event_id: str) -> dict:
return self._request("GET", f"/v1/event/{event_id}/")
class AuthError(Exception):
pass
+48 -2
View File
@@ -43,6 +43,10 @@ def _sym(fancy: str, plain: str) -> str:
def print_banner(console: Console) -> None:
"""Print the mem0 welcome banner."""
from mem0_cli.state import is_agent_mode
if is_agent_mode():
return
logo_text = Text(LOGO, style=f"bold {BRAND_COLOR}")
tagline = Text(f" {TAGLINE}\n", style=f"{ACCENT_COLOR}")
@@ -61,11 +65,28 @@ def print_banner(console: Console) -> None:
def print_success(console: Console, message: str) -> None:
from mem0_cli.state import is_agent_mode
if is_agent_mode():
return
sym = _sym("✓", "[ok]")
console.print(f"[{SUCCESS_COLOR}]{sym}[/] {message}")
def print_error(console: Console, message: str, hint: str | None = None) -> None:
from mem0_cli.state import get_current_command, is_agent_mode
if is_agent_mode():
import json as _json
envelope = {
"status": "error",
"command": get_current_command(),
"error": message,
"data": None,
}
print(_json.dumps(envelope))
return
sym = _sym("✗", "[error]")
console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {message}")
if hint:
@@ -73,11 +94,19 @@ def print_error(console: Console, message: str, hint: str | None = None) -> None
def print_warning(console: Console, message: str) -> None:
from mem0_cli.state import is_agent_mode
if is_agent_mode():
return
sym = _sym("⚠", "[warn]")
console.print(f"[{WARNING_COLOR}]{sym}[/] {message}")
def print_info(console: Console, message: str) -> None:
from mem0_cli.state import is_agent_mode
if is_agent_mode():
return
sym = _sym("◆", "*")
console.print(f"[{BRAND_COLOR}]{sym}[/] {message}")
@@ -89,7 +118,9 @@ def timed_status(console: Console, message: str):
The spinner and timing output are sent to stderr (via ``_err``) so they
never contaminate machine-readable stdout. The *console* parameter is
kept for backward compatibility but is not used for spinner output.
In agent mode the spinner is suppressed entirely.
"""
from mem0_cli.state import is_agent_mode
class _Ctx:
def __init__(self):
@@ -97,6 +128,13 @@ def timed_status(console: Console, message: str):
self.error_msg = ""
ctx = _Ctx()
if is_agent_mode():
try:
yield ctx
except Exception:
raise
return
start = time.perf_counter()
try:
with Status(f"[{DIM_COLOR}]{message}[/]", console=_err):
@@ -105,6 +143,11 @@ def timed_status(console: Console, message: str):
elapsed = time.perf_counter() - start
if ctx.error_msg:
print_error(_err, f"{ctx.error_msg} ({elapsed:.2f}s)")
if "Authentication failed" in ctx.error_msg:
_err.print(
f" [{DIM_COLOR}]Run [bold]mem0 init[/bold] to reconfigure your API key"
f" · [bold]https://app.mem0.ai/dashboard/api-keys[/bold][/]"
)
raise
else:
elapsed = time.perf_counter() - start
@@ -114,11 +157,14 @@ def timed_status(console: Console, message: str):
def print_scope(console: Console, **ids: str | None) -> None:
"""Show active entity scope if any IDs are set."""
from mem0_cli.state import is_agent_mode
if is_agent_mode():
return
parts = []
for key, val in ids.items():
if val:
label = key.replace("_", " ").replace("id", "ID").strip()
parts.append(f"{label}={val}")
parts.append(f"{key}={val}")
if parts:
scope_str = ", ".join(parts)
console.print(f" [{DIM_COLOR}]Scope: {scope_str}[/]")
+33 -9
View File
@@ -20,12 +20,17 @@ err_console = Console(stderr=True)
def cmd_config_show(*, output: str = "text") -> None:
"""Display current configuration (secrets redacted)."""
from mem0_cli.output import format_json_envelope
from mem0_cli.output import format_agent_envelope
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("config show")
if is_agent_mode():
output = "agent"
config = load_config()
if output == "json":
format_json_envelope(
if output in ("json", "agent"):
format_agent_envelope(
console,
command="config show",
data={
@@ -84,25 +89,44 @@ def cmd_config_show(*, output: str = "text") -> None:
def cmd_config_get(key: str) -> None:
"""Get a config value."""
from mem0_cli.output import format_agent_envelope
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("config get")
config = load_config()
value = get_nested_value(config, key)
if value is None:
print_error(err_console, f"Unknown config key: {key}")
return
display_value = (
redact_key(str(value)) if ("api_key" in key or "key" in key.split(".")[-1:]) else str(value)
)
if is_agent_mode():
format_agent_envelope(
console, command="config get", data={"key": key, "value": display_value}
)
else:
# Redact secrets
if "api_key" in key or "key" in key.split(".")[-1:]:
console.print(redact_key(str(value)))
else:
console.print(str(value))
console.print(display_value)
def cmd_config_set(key: str, value: str) -> None:
"""Set a config value."""
from mem0_cli.output import format_agent_envelope
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("config set")
config = load_config()
if set_nested_value(config, key, value):
save_config(config)
display = redact_key(value) if "key" in key else value
print_success(console, f"{key} = {display}")
if is_agent_mode():
format_agent_envelope(
console, command="config set", data={"key": key, "value": display}
)
else:
print_success(console, f"{key} = {display}")
else:
print_error(err_console, f"Unknown config key: {key}")
+56 -25
View File
@@ -18,7 +18,7 @@ from mem0_cli.branding import (
print_success,
timed_status,
)
from mem0_cli.output import format_json
from mem0_cli.output import format_agent_envelope, format_json
console = Console()
err_console = Console(stderr=True)
@@ -26,6 +26,11 @@ err_console = Console(stderr=True)
def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> None:
"""List entities of a given type."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("entity list")
if is_agent_mode():
output = "agent"
valid_types = {"users", "agents", "apps", "runs"}
if entity_type not in valid_types:
print_error(
@@ -42,6 +47,16 @@ def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> Non
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "agent":
format_agent_envelope(
console,
command="entity list",
data=results,
count=len(results),
duration_ms=int(_elapsed * 1000),
)
return
if output == "json":
format_json(console, results)
return
@@ -77,41 +92,39 @@ def cmd_entities_delete(
output: str,
) -> None:
"""Delete an entity and all its memories (cascade delete)."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("entity delete")
if is_agent_mode():
output = "agent"
if not force:
print_error(err_console, "Destructive operation requires --force in agent mode.")
raise typer.Exit(1)
if not any([user_id, agent_id, app_id, run_id]):
print_error(
err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id."
)
raise typer.Exit(1)
scope_parts = []
if user_id:
scope_parts.append(f"user={user_id}")
if agent_id:
scope_parts.append(f"agent={agent_id}")
if app_id:
scope_parts.append(f"app={app_id}")
if run_id:
scope_parts.append(f"run={run_id}")
scope_str = ", ".join(scope_parts)
if dry_run:
scope_parts = []
if user_id:
scope_parts.append(f"user={user_id}")
if agent_id:
scope_parts.append(f"agent={agent_id}")
if app_id:
scope_parts.append(f"app={app_id}")
if run_id:
scope_parts.append(f"run={run_id}")
scope = ", ".join(scope_parts)
print_info(console, f"Would delete entity {scope} and all its memories.")
print_info(console, f"Would delete entity {scope_str} and all its memories.")
print_info(console, "No changes made (dry run).")
return
if not force:
scope_parts = []
if user_id:
scope_parts.append(f"user={user_id}")
if agent_id:
scope_parts.append(f"agent={agent_id}")
if app_id:
scope_parts.append(f"app={app_id}")
if run_id:
scope_parts.append(f"run={run_id}")
scope = ", ".join(scope_parts)
confirm = typer.confirm(
f"\n \u26a0 Delete entity {scope} AND all its memories? This cannot be undone."
f"\n \u26a0 Delete entity {scope_str} AND all its memories? This cannot be undone."
)
if not confirm:
print_info(console, "Cancelled.")
@@ -131,7 +144,25 @@ def cmd_entities_delete(
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
scope = {
k: v
for k, v in {
"user_id": user_id,
"agent_id": agent_id,
"app_id": app_id,
"run_id": run_id,
}.items()
if v
}
if output == "agent":
format_agent_envelope(
console,
command="entity delete",
data={"deleted": True},
scope=scope or None,
duration_ms=int(_elapsed * 1000),
)
elif output == "json":
format_json(console, result)
elif output != "quiet":
print_success(console, f"Entity deleted with all memories ({_elapsed:.2f}s)")
@@ -0,0 +1,176 @@
"""Event commands: list and status."""
from __future__ import annotations
import typer
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from mem0_cli.backend.base import Backend
from mem0_cli.branding import (
ACCENT_COLOR,
BRAND_COLOR,
DIM_COLOR,
ERROR_COLOR,
SUCCESS_COLOR,
WARNING_COLOR,
print_info,
timed_status,
)
from mem0_cli.output import format_agent_envelope, format_json
console = Console()
err_console = Console(stderr=True)
_STATUS_STYLE = {
"SUCCEEDED": f"[{SUCCESS_COLOR}]SUCCEEDED[/]",
"PENDING": f"[{ACCENT_COLOR}]PENDING[/]",
"FAILED": f"[{ERROR_COLOR}]FAILED[/]",
"PROCESSING": f"[{WARNING_COLOR}]PROCESSING[/]",
}
def _status_styled(status: str) -> str:
return _STATUS_STYLE.get(status.upper(), status)
def cmd_event_list(backend: Backend, *, output: str = "table") -> None:
"""List recent background events."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("event list")
if is_agent_mode():
output = "agent"
import time as _time
_start = _time.perf_counter()
with timed_status(err_console, "Fetching events...") as _ts:
try:
results = backend.list_events()
except Exception as e:
_ts.error_msg = str(e)
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "agent":
format_agent_envelope(
console,
command="event list",
data=results,
count=len(results),
duration_ms=int(_elapsed * 1000),
)
return
if output == "json":
format_json(console, results)
return
if not results:
console.print()
print_info(console, "No events found.")
console.print()
return
table = Table(
border_style=BRAND_COLOR,
header_style=f"bold {ACCENT_COLOR}",
row_styles=["", "dim"],
padding=(0, 1),
)
table.add_column("Event ID", style="dim", max_width=10, no_wrap=True)
table.add_column("Type", max_width=14)
table.add_column("Status", max_width=12)
table.add_column("Latency", max_width=10, justify="right")
table.add_column("Created", max_width=20)
for ev in results:
ev_id = str(ev.get("id", ""))[:8]
ev_type = str(ev.get("event_type", "—"))
status = str(ev.get("status", "—"))
latency = ev.get("latency")
latency_str = f"{latency:.0f}ms" if isinstance(latency, (int, float)) else "—"
created = str(ev.get("created_at", "—"))[:19].replace("T", " ")
table.add_row(ev_id, ev_type, _status_styled(status), latency_str, created)
console.print()
console.print(table)
console.print(f" [{DIM_COLOR}]{len(results)} event{'s' if len(results) != 1 else ''}[/]")
console.print()
def cmd_event_status(backend: Backend, event_id: str, *, output: str = "text") -> None:
"""Get the status of a specific background event."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("event status")
if is_agent_mode():
output = "agent"
import time as _time
_start = _time.perf_counter()
with timed_status(err_console, "Fetching event...") as _ts:
try:
ev = backend.get_event(event_id)
except Exception as e:
_ts.error_msg = str(e)
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "agent":
format_agent_envelope(
console,
command="event status",
data=ev,
duration_ms=int(_elapsed * 1000),
)
return
if output == "json":
format_json(console, ev)
return
status = str(ev.get("status", "—"))
ev_type = str(ev.get("event_type", "—"))
latency = ev.get("latency")
latency_str = f"{latency:.0f}ms" if isinstance(latency, (int, float)) else "—"
created = str(ev.get("created_at", "—"))[:19].replace("T", " ")
updated = str(ev.get("updated_at", "—"))[:19].replace("T", " ")
results = ev.get("results")
lines = []
lines.append(f" [{DIM_COLOR}]Event ID:[/] {event_id}")
lines.append(f" [{DIM_COLOR}]Type:[/] {ev_type}")
lines.append(f" [{DIM_COLOR}]Status:[/] {_status_styled(status)}")
lines.append(f" [{DIM_COLOR}]Latency:[/] {latency_str}")
lines.append(f" [{DIM_COLOR}]Created:[/] {created}")
lines.append(f" [{DIM_COLOR}]Updated:[/] {updated}")
if results:
lines.append("")
lines.append(f" [{DIM_COLOR}]Results ({len(results)}):[/]")
for r in results:
mem_id = str(r.get("id", ""))[:8]
data = r.get("data", {})
memory = data.get("memory", "") if isinstance(data, dict) else str(data)
ev_name = str(r.get("event", ""))
user = str(r.get("user_id", ""))
detail = f"{ev_name} {memory}"
if user:
detail += f" [{DIM_COLOR}](user_id={user})[/]"
lines.append(f" [{SUCCESS_COLOR}]·[/] {detail} [{DIM_COLOR}]({mem_id})[/]")
content = "\n".join(lines)
panel = Panel(
content,
title=f"[{BRAND_COLOR}]Event Status[/]",
title_align="left",
border_style=BRAND_COLOR,
padding=(1, 1),
)
console.print()
console.print(panel)
console.print()
+45 -14
View File
@@ -19,7 +19,7 @@ from mem0_cli.branding import (
print_info,
print_success,
)
from mem0_cli.config import DEFAULT_BASE_URL, Mem0Config, save_config
from mem0_cli.config import CONFIG_FILE, DEFAULT_BASE_URL, Mem0Config, load_config, save_config
console = Console()
err_console = Console(stderr=True)
@@ -169,6 +169,7 @@ def run_init(
user_id: str | None = None,
email: str | None = None,
code: str | None = None,
force: bool = False,
) -> None:
"""Interactive setup wizard for mem0 CLI.
@@ -184,6 +185,29 @@ def run_init(
print_error(err_console, "--code requires --email.")
raise typer.Exit(1)
# Warn if an existing config with an API key would be overwritten
if not force and CONFIG_FILE.exists():
existing = load_config()
if existing.platform.api_key:
from mem0_cli.config import redact_key
console.print(
f"\n [{BRAND_COLOR}]Existing configuration found[/] "
f"[{DIM_COLOR}](API key: {redact_key(existing.platform.api_key)})[/]"
)
if sys.stdin.isatty():
confirm = typer.confirm(" Overwrite existing config? This cannot be undone.")
if not confirm:
print_info(console, "Cancelled. Use --force to skip this check.")
raise typer.Exit(0)
else:
print_error(
err_console,
"Existing config would be overwritten.",
hint="Use --force to overwrite.",
)
raise typer.Exit(1)
# ── Email login flow ──────────────────────────────────────────────
if email:
if api_key:
@@ -205,7 +229,9 @@ def run_init(
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"
config.defaults.user_id = (
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
)
save_config(config)
@@ -220,6 +246,17 @@ def run_init(
# ── API key flow (existing) ───────────────────────────────────────
# Non-TTY: resolve defaults so partial flags work in pipelines / CI
if not sys.stdin.isatty():
if not api_key:
print_error(
err_console,
"Non-interactive terminal detected and --api-key is required.",
hint="Run: mem0 init --api-key <key> [--user-id <id>]",
)
raise typer.Exit(1)
user_id = user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
# Fully non-interactive when both flags provided
if api_key and user_id:
config.platform.api_key = api_key
@@ -229,15 +266,6 @@ def run_init(
print_success(console, "Configuration saved to ~/.mem0/config.json")
return
# Non-TTY without full flags -> error
if not sys.stdin.isatty() and (not api_key or not user_id):
print_error(
err_console,
"Non-interactive terminal detected and required flags missing.",
hint="Run: mem0 init --api-key <key> --user-id <id>",
)
raise typer.Exit(1)
print_banner(console)
console.print()
print_info(console, "Welcome! Let's set up your mem0 CLI.\n")
@@ -271,7 +299,9 @@ def run_init(
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"
config.defaults.user_id = (
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
)
save_config(config)
@@ -331,9 +361,10 @@ def _setup_defaults(config: Mem0Config) -> None:
console.print()
print_info(console, "Set default entity IDs (press Enter to skip).\n")
_default_user = os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
user_id = Prompt.ask(
f" [{BRAND_COLOR}]Default User ID[/] [{DIM_COLOR}](recommended)[/]",
default="mem0-cli",
default=_default_user,
)
if user_id:
config.defaults.user_id = user_id
@@ -357,7 +388,7 @@ def _validate_platform(config: Mem0Config) -> None:
print_error(
err_console,
f"Could not connect: {status.get('error', 'Unknown error')}",
hint="Check your API key and try again.",
hint="Visit https://app.mem0.ai/dashboard/api-keys to get a new key, then run mem0 init again.",
)
except Exception as e:
print_error(err_console, f"Connection test failed: {e}")
+188 -23
View File
@@ -3,6 +3,8 @@
from __future__ import annotations
import json
import os
import stat as _stat_mod
import sys
import time as _time
from pathlib import Path
@@ -20,6 +22,7 @@ from mem0_cli.branding import (
)
from mem0_cli.output import (
format_add_result,
format_agent_envelope,
format_json,
format_memories_table,
format_memories_text,
@@ -31,6 +34,19 @@ console = Console()
err_console = Console(stderr=True)
def _stdin_is_piped() -> bool:
"""Return True only when stdin is an actual pipe or file redirect."""
from mem0_cli.state import is_agent_mode
if is_agent_mode():
return False
try:
mode = os.fstat(sys.stdin.fileno()).st_mode
return _stat_mod.S_ISFIFO(mode) or _stat_mod.S_ISREG(mode)
except Exception:
return False
def cmd_add(
backend: Backend,
text: str | None,
@@ -50,6 +66,11 @@ def cmd_add(
output: str = "text",
) -> None:
"""Add a memory."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("add")
if is_agent_mode():
output = "agent"
msgs = None
content = text
@@ -70,8 +91,8 @@ def cmd_add(
print_error(err_console, f"Invalid JSON in --messages: {e}")
raise typer.Exit(1) from None
# Read from stdin if no text and stdin is piped
elif not content and not sys.stdin.isatty():
# Read from stdin only if stdin is an actual pipe or file redirect
elif not content and _stdin_is_piped():
content = sys.stdin.read().strip()
if not content and not msgs:
@@ -133,18 +154,61 @@ def cmd_add(
if output == "quiet":
return
# Deduplicate PENDING entries sharing the same event_id across all output modes
results_list = result if isinstance(result, list) else result.get("results", [result])
seen_events: set[str] = set()
deduped: list[dict] = []
for r in results_list:
if r.get("status") == "PENDING":
eid = r.get("event_id", "")
if eid and eid in seen_events:
continue
if eid:
seen_events.add(eid)
deduped.append(r)
# Write back so downstream formatters see deduplicated data
if isinstance(result, dict) and "results" in result:
result = {**result, "results": deduped}
else:
result = deduped
if output == "agent":
scope = {
k: v
for k, v in {
"user_id": user_id,
"agent_id": agent_id,
"app_id": app_id,
"run_id": run_id,
}.items()
if v
}
format_agent_envelope(
console,
command="add",
data=deduped,
scope=scope or None,
count=len(deduped),
)
return
if output == "json":
format_add_result(console, result, output)
return
console.print()
print_scope(console, user_id=user_id, agent_id=agent_id, app_id=app_id, run_id=run_id)
# Count results
results = result if isinstance(result, list) else result.get("results", [result])
count = len(results) if results else 0
print_success(
console, f"Memory processed — {count} memor{'y' if count == 1 else 'ies'} extracted"
)
count = len(deduped)
all_pending = count > 0 and all(r.get("status") == "PENDING" for r in deduped)
if all_pending:
print_success(
console,
f"Memory queued — {count} event{'s' if count != 1 else ''} pending",
)
else:
print_success(
console, f"Memory processed — {count} memor{'y' if count == 1 else 'ies'} extracted"
)
format_add_result(console, result, output)
@@ -166,6 +230,11 @@ def cmd_search(
output: str = "text",
) -> None:
"""Search memories."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("search")
if is_agent_mode():
output = "agent"
filters = None
if filter_json:
try:
@@ -210,6 +279,27 @@ def cmd_search(
if output == "quiet":
return
if output == "agent":
scope = {
k: v
for k, v in {
"user_id": user_id,
"agent_id": agent_id,
"app_id": app_id,
"run_id": run_id,
}.items()
if v
}
format_agent_envelope(
console,
command="search",
data=results,
scope=scope or None,
count=len(results),
duration_ms=int(_elapsed * 1000),
)
return
if output == "json":
format_json(console, results)
elif output == "table":
@@ -236,6 +326,11 @@ def cmd_search(
def cmd_get(backend: Backend, memory_id: str, *, output: str) -> None:
"""Get a specific memory by ID."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("get")
if is_agent_mode():
output = "agent"
with timed_status(err_console, "Fetching memory...") as _ts:
try:
result = backend.get(memory_id)
@@ -243,7 +338,10 @@ def cmd_get(backend: Backend, memory_id: str, *, output: str) -> None:
print_error(err_console, str(e))
raise typer.Exit(1) from None
format_single_memory(console, result, output)
if output == "agent":
format_agent_envelope(console, command="get", data=result)
else:
format_single_memory(console, result, output)
def cmd_list(
@@ -262,6 +360,11 @@ def cmd_list(
output: str = "table",
) -> None:
"""List memories."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("list")
if is_agent_mode():
output = "agent"
if page_size < 1:
print_error(err_console, "--page-size must be >= 1.")
raise typer.Exit(1)
@@ -292,15 +395,24 @@ def cmd_list(
if output == "quiet":
return
if output == "json":
from mem0_cli.output import format_json_envelope
format_json_envelope(
if output in ("json", "agent"):
scope = {
k: v
for k, v in {
"user_id": user_id,
"agent_id": agent_id,
"app_id": app_id,
"run_id": run_id,
}.items()
if v
}
format_agent_envelope(
console,
command="list",
data=results,
scope=scope or None,
count=len(results),
scope={k: v for k, v in {"user_id": user_id, "agent_id": agent_id}.items() if v},
duration_ms=int(_elapsed * 1000),
)
elif output == "table":
if results:
@@ -343,6 +455,11 @@ def cmd_update(
output: str,
) -> None:
"""Update a memory."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("update")
if is_agent_mode():
output = "agent"
meta = None
if metadata:
try:
@@ -360,7 +477,14 @@ def cmd_update(
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
if output == "agent":
format_agent_envelope(
console,
command="update",
data=result,
duration_ms=int(_elapsed * 1000),
)
elif output == "json":
format_json(console, result)
elif output != "quiet":
print_success(console, f"Memory {memory_id[:8]} updated ({_elapsed:.2f}s)")
@@ -375,6 +499,11 @@ def cmd_delete(
output: str,
) -> None:
"""Delete a single memory by ID."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("delete")
if is_agent_mode():
output = "agent"
if dry_run:
# Fetch and display what would be deleted
try:
@@ -395,7 +524,14 @@ def cmd_delete(
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
if output == "agent":
format_agent_envelope(
console,
command="delete",
data={"id": memory_id, "deleted": True},
duration_ms=int(_elapsed * 1000),
)
elif output == "json":
format_json(console, result)
elif output != "quiet":
print_success(console, f"Memory {memory_id[:8]} deleted ({_elapsed:.2f}s)")
@@ -414,13 +550,17 @@ def cmd_delete_all(
output: str,
) -> None:
"""Delete all memories matching a scope."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("delete-all")
if is_agent_mode():
output = "agent"
if not force:
print_error(err_console, "Destructive operation requires --force in agent mode.")
raise typer.Exit(1)
if all_:
# Project-wide wipe using wildcard entity IDs
if dry_run:
print_info(console, "Would delete ALL memories project-wide.")
print_info(console, "Run without --dry-run to see the actual count.")
print_info(console, "No changes made (dry run).")
return
# Note: --dry-run is ignored here because the API has no count-before-delete endpoint.
if not force:
confirm = typer.confirm(
@@ -445,7 +585,14 @@ def cmd_delete_all(
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
if output == "agent":
format_agent_envelope(
console,
command="delete-all",
data={"deleted": True, "scope": "project"},
duration_ms=int(_elapsed * 1000),
)
elif output == "json":
format_json(console, result)
elif output != "quiet":
if isinstance(result, dict) and "message" in result:
@@ -503,7 +650,25 @@ def cmd_delete_all(
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
scope = {
k: v
for k, v in {
"user_id": user_id,
"agent_id": agent_id,
"app_id": app_id,
"run_id": run_id,
}.items()
if v
}
if output == "agent":
format_agent_envelope(
console,
command="delete-all",
data={"deleted": True},
scope=scope or None,
duration_ms=int(_elapsed * 1000),
)
elif output == "json":
format_json(console, result)
elif output != "quiet":
if isinstance(result, dict) and "message" in result:
+27 -8
View File
@@ -35,22 +35,26 @@ def cmd_status(
output: str = "text",
) -> None:
"""Check connectivity and auth."""
from mem0_cli.output import format_json_envelope
from mem0_cli.output import format_agent_envelope
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("status")
if is_agent_mode():
output = "agent"
_start = _time.perf_counter()
with timed_status(err_console, "Checking connection...") as _ts:
result = backend.status(user_id=user_id, agent_id=agent_id)
_elapsed = _time.perf_counter() - _start
if output == "json":
format_json_envelope(
if output in ("json", "agent"):
format_agent_envelope(
console,
command="status",
data={
"connected": result.get("connected", False),
"backend": result.get("backend", "?"),
"base_url": result.get("base_url", ""),
"latency_ms": int(_elapsed * 1000),
},
duration_ms=int(_elapsed * 1000),
)
@@ -67,6 +71,14 @@ def cmd_status(
lines.append(f" [{DIM_COLOR}]API URL:[/] {result['base_url']}")
if result.get("error"):
lines.append(f" [{ERROR_COLOR}]Error:[/] {result['error']}")
if "Authentication failed" in str(result["error"]):
lines.append("")
lines.append(
f" [{DIM_COLOR}]Run [bold]mem0 init[/bold] to reconfigure your API key[/]"
)
lines.append(
f" [{DIM_COLOR}]Get a key at [bold]https://app.mem0.ai/dashboard/api-keys[/bold][/]"
)
lines.append(f" [{DIM_COLOR}]Latency:[/] {_elapsed:.2f}s")
content = "\n".join(lines)
@@ -96,7 +108,12 @@ def cmd_import(
output: str = "text",
) -> None:
"""Import memories from a JSON file."""
from mem0_cli.output import format_json_envelope
from mem0_cli.output import format_agent_envelope
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("import")
if is_agent_mode():
output = "agent"
try:
data = json.loads(Path(file_path).read_text())
@@ -129,11 +146,13 @@ def cmd_import(
failed += 1
_elapsed = _time.perf_counter() - _start
if output == "json":
format_json_envelope(
if output in ("json", "agent"):
scope = {k: v for k, v in {"user_id": user_id, "agent_id": agent_id}.items() if v}
format_agent_envelope(
console,
command="import",
data={"added": added, "failed": failed, "duration_s": round(_elapsed, 2)},
data={"added": added, "failed": failed},
scope=scope or None,
duration_ms=int(_elapsed * 1000),
)
return
+105 -4
View File
@@ -155,16 +155,23 @@ def format_add_result(console: Console, result: dict | list, output: str = "text
return
console.print()
seen_pending_events: set[str] = set()
for r in results:
# Detect async PENDING response from Platform API
if r.get("status") == "PENDING":
event_id = r.get("event_id", "")[:8]
event_id = r.get("event_id", "")
# Deduplicate PENDING entries with the same event_id
if event_id and event_id in seen_pending_events:
continue
if event_id:
seen_pending_events.add(event_id)
icon = f"[{ACCENT_COLOR}]{_sym('⧗', '...')}[/]"
parts = [f" {icon} [{DIM_COLOR}]{'Queued':<10}[/]"]
parts.append("[white]Processing in background[/]")
if event_id:
parts.append(f"[{DIM_COLOR}](event {event_id})[/]")
console.print(" ".join(parts))
if event_id:
console.print(f" [{DIM_COLOR}] event_id: {event_id}[/]")
console.print(f" [{DIM_COLOR}] → Check status: mem0 event status {event_id}[/]")
continue
event = r.get("event", "ADD")
@@ -225,6 +232,100 @@ def format_json_envelope(
console.print_json(json.dumps(envelope, default=str))
def sanitize_agent_data(command: str, data: Any) -> Any:
"""Project API response data to minimal relevant fields for agent consumption."""
def pick(obj: dict, keys: list) -> dict:
return {k: obj[k] for k in keys if k in obj}
if data is None:
return data
if command == "add":
items = data if isinstance(data, list) else [data]
result = []
for item in items:
if item.get("status") == "PENDING":
result.append(pick(item, ["status", "event_id"]))
else:
result.append(pick(item, ["id", "memory", "event"]))
return result
if command == "search":
return [pick(r, ["id", "memory", "score", "created_at", "categories"]) for r in data]
if command == "list":
return [pick(r, ["id", "memory", "created_at", "categories"]) for r in data]
if command == "get":
return pick(data, ["id", "memory", "created_at", "updated_at", "categories", "metadata"])
if command == "update":
return pick(data, ["id", "memory"])
if command in ("delete", "delete-all", "entity delete"):
return data
if command == "entity list":
result = []
for r in data:
item = pick(r, ["type", "count"])
item["name"] = r.get("name") or r.get("id", "")
result.append(item)
return result
if command == "event list":
return [pick(r, ["id", "event_type", "status", "latency", "created_at"]) for r in data]
if command == "event status":
ev = data
raw_results = ev.get("results") or []
sanitized_results = []
for r in raw_results:
nested = r.get("data") or {}
memory = nested.get("memory") if isinstance(nested, dict) else None
sanitized_results.append(
{
"id": r.get("id"),
"event": r.get("event"),
"user_id": r.get("user_id"),
"memory": memory,
}
)
result = pick(ev, ["id", "event_type", "status", "latency", "created_at", "updated_at"])
result["results"] = sanitized_results
return result
# Pass-through: status, import, config show/get/set
return data
def format_agent_envelope(
console: Console,
*,
command: str,
data: Any,
duration_ms: int | None = None,
scope: dict | None = None,
count: int | None = None,
) -> None:
"""Output structured JSON envelope for agent/programmatic use (--json/--agent mode)."""
envelope: dict[str, Any] = {
"status": "success",
"command": command,
}
if duration_ms is not None:
envelope["duration_ms"] = duration_ms
if scope:
filtered = {k: v for k, v in scope.items() if v}
if filtered:
envelope["scope"] = filtered
if count is not None:
envelope["count"] = count
envelope["data"] = sanitize_agent_data(command, data)
console.print_json(json.dumps(envelope, default=str))
def print_result_summary(
console: Console,
count: int,
@@ -237,7 +338,7 @@ def print_result_summary(
parts = [f"{count} result{'s' if count != 1 else ''}"]
if page is not None:
parts.append(f"page {page}")
scope_parts = [f"{k.replace('_', ' ')}={v}" for k, v in scope_ids.items() if v]
scope_parts = [f"{k}={v}" for k, v in scope_ids.items() if v]
if scope_parts:
parts.append(", ".join(scope_parts))
if duration_secs is not None:
+24
View File
@@ -0,0 +1,24 @@
"""Agent mode state — set by the root callback, read by commands and branding."""
from __future__ import annotations
_agent_mode: bool = False
_current_command: str = ""
def is_agent_mode() -> bool:
return _agent_mode
def set_agent_mode(val: bool) -> None:
global _agent_mode
_agent_mode = val
def get_current_command() -> str:
return _current_command
def set_current_command(name: str) -> None:
global _current_command
_current_command = name
+37
View File
@@ -96,6 +96,43 @@ def mock_backend():
{"name": "alice", "count": 5},
{"name": "bob", "count": 3},
]
backend.list_events.return_value = [
{
"id": "evt-abc-123-def-456",
"event_type": "ADD",
"status": "SUCCEEDED",
"graph_status": None,
"latency": 1234.5,
"created_at": "2026-04-01T10:00:00Z",
"updated_at": "2026-04-01T10:00:01Z",
},
{
"id": "evt-def-456-ghi-789",
"event_type": "SEARCH",
"status": "PENDING",
"graph_status": None,
"latency": None,
"created_at": "2026-04-01T10:01:00Z",
"updated_at": "2026-04-01T10:01:00Z",
},
]
backend.get_event.return_value = {
"id": "evt-abc-123-def-456",
"event_type": "ADD",
"status": "SUCCEEDED",
"graph_status": "SUCCEEDED",
"latency": 1234.5,
"created_at": "2026-04-01T10:00:00Z",
"updated_at": "2026-04-01T10:00:01Z",
"results": [
{
"id": "mem-abc-123",
"event": "ADD",
"user_id": "alice",
"data": {"memory": "User prefers dark mode"},
}
],
}
return backend
+487 -42
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import json
import typing
from io import StringIO
from unittest.mock import patch
@@ -16,6 +17,7 @@ from mem0_cli.commands.config_cmd import (
cmd_config_show,
)
from mem0_cli.commands.entities import cmd_entities_delete, cmd_entities_list
from mem0_cli.commands.events_cmd import cmd_event_list, cmd_event_status
from mem0_cli.commands.memory import (
cmd_add,
cmd_delete,
@@ -176,30 +178,28 @@ class TestAddCommand:
def test_add_no_content_exits(self, mock_backend):
console, _buf = _make_console()
err_console, _err_buf = _make_err_console()
# Patch stdin.isatty to return True so it doesn't try to read stdin
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
patch("mem0_cli.commands.memory.sys") as mock_sys,
patch("mem0_cli.commands.memory._stdin_is_piped", return_value=False),
pytest.raises((SystemExit, ClickExit)),
):
mock_sys.stdin.isatty.return_value = True
with pytest.raises((SystemExit, ClickExit)):
cmd_add(
mock_backend,
None,
user_id="alice",
agent_id=None,
app_id=None,
run_id=None,
messages=None,
file=None,
metadata=None,
immutable=False,
no_infer=False,
expires=None,
categories=None,
output="text",
)
cmd_add(
mock_backend,
None,
user_id="alice",
agent_id=None,
app_id=None,
run_id=None,
messages=None,
file=None,
metadata=None,
immutable=False,
no_infer=False,
expires=None,
categories=None,
output="text",
)
def test_add_invalid_metadata_json(self, mock_backend):
console, _buf = _make_console()
@@ -279,6 +279,66 @@ class TestAddCommand:
mock_backend.add.assert_called_once()
class TestAddDeduplicatesPending:
"""Ensure duplicate PENDING entries with the same event_id are collapsed."""
DUPLICATE_PENDING: typing.ClassVar[dict] = {
"results": [
{"status": "PENDING", "event_id": "evt-dup"},
{"status": "PENDING", "event_id": "evt-dup"},
]
}
def _run_add(self, mock_backend, output):
mock_backend.add.return_value = self.DUPLICATE_PENDING
console, buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
):
cmd_add(
mock_backend,
"test",
user_id="alice",
agent_id=None,
app_id=None,
run_id=None,
messages=None,
file=None,
metadata=None,
immutable=False,
no_infer=False,
expires=None,
categories=None,
output=output,
)
return buf.getvalue()
def test_text_shows_one_pending(self, mock_backend):
raw = self._run_add(mock_backend, "text")
assert raw.count("Queued") == 1
def test_json_shows_one_pending(self, mock_backend):
raw = self._run_add(mock_backend, "json")
data = json.loads(raw)
results = data.get("results", data)
pending = [r for r in results if r.get("status") == "PENDING"]
assert len(pending) == 1
def test_agent_shows_one_pending(self, mock_backend):
from mem0_cli.state import set_agent_mode
set_agent_mode(True)
try:
raw = self._run_add(mock_backend, "agent")
finally:
set_agent_mode(False)
data = json.loads(raw)
assert data["count"] == 1
assert len(data["data"]) == 1
class TestSearchCommand:
def test_search_text(self, mock_backend):
console, buf = _make_console()
@@ -618,28 +678,6 @@ class TestDeleteAllCommand:
run_id="*",
)
def test_delete_all_project_wide_dry_run(self, mock_backend):
console, buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
):
cmd_delete_all(
mock_backend,
force=True,
all_=True,
dry_run=True,
user_id=None,
agent_id=None,
app_id=None,
run_id=None,
output="text",
)
output = buf.getvalue()
assert "project-wide" in output.lower()
mock_backend.delete.assert_not_called()
def test_delete_all_project_wide_async_response(self, mock_backend):
mock_backend.delete.return_value = {"message": "Memories deletion started..."}
console, buf = _make_console()
@@ -888,6 +926,28 @@ class TestEntitiesDeleteCommand:
output = buf.getvalue()
assert "deleted" in output.lower()
def test_delete_entity_agent_id(self, mock_backend):
console, buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.entities.console", console),
patch("mem0_cli.commands.entities.err_console", err_console),
):
cmd_entities_delete(
mock_backend,
user_id=None,
agent_id="bot1",
app_id=None,
run_id=None,
force=True,
output="text",
)
mock_backend.delete_entities.assert_called_once_with(
user_id=None, agent_id="bot1", app_id=None, run_id=None
)
output = buf.getvalue()
assert "deleted" in output.lower()
def test_delete_entity_no_id_exits(self, mock_backend):
console, _buf = _make_console()
err_console, _err_buf = _make_err_console()
@@ -1024,3 +1084,388 @@ class TestEnableGraph:
)
call_kwargs = mock_backend.list_memories.call_args
assert call_kwargs.kwargs.get("enable_graph") is True
class TestEventCommands:
def test_event_list_table(self, mock_backend):
console, buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.events_cmd.console", console),
patch("mem0_cli.commands.events_cmd.err_console", err_console),
):
cmd_event_list(mock_backend, output="table")
out = buf.getvalue()
assert "evt-abc-" in out
assert "ADD" in out
assert "SUCCEEDED" in out
def test_event_list_json(self, mock_backend):
console, buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.events_cmd.console", console),
patch("mem0_cli.commands.events_cmd.err_console", err_console),
):
cmd_event_list(mock_backend, output="json")
out = buf.getvalue()
assert "evt-abc-123-def-456" in out
assert "evt-def-456-ghi-789" in out
def test_event_list_empty(self, mock_backend):
mock_backend.list_events.return_value = []
console, buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.events_cmd.console", console),
patch("mem0_cli.commands.events_cmd.err_console", err_console),
):
cmd_event_list(mock_backend, output="table")
out = buf.getvalue()
assert "No events" in out
def test_event_status_text(self, mock_backend):
console, buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.events_cmd.console", console),
patch("mem0_cli.commands.events_cmd.err_console", err_console),
):
cmd_event_status(mock_backend, "evt-abc-123-def-456", output="text")
out = buf.getvalue()
assert "evt-abc-123-def-456" in out
assert "SUCCEEDED" in out
def test_event_status_json(self, mock_backend):
console, buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.events_cmd.console", console),
patch("mem0_cli.commands.events_cmd.err_console", err_console),
):
cmd_event_status(mock_backend, "evt-abc-123-def-456", output="json")
out = buf.getvalue()
assert "evt-abc-123-def-456" in out
assert "ADD" in out
class TestAgentMode:
"""Tests for --json/--agent mode: structured JSON envelope output."""
def setup_method(self):
"""Enable agent mode before each test."""
from mem0_cli.state import set_agent_mode
set_agent_mode(True)
def teardown_method(self):
"""Reset agent mode after each test."""
from mem0_cli.state import set_agent_mode
set_agent_mode(False)
# ── add ──────────────────────────────────────────────────────────────────
def test_add_agent_mode_envelope(self, mock_backend):
console, buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
):
cmd_add(
mock_backend,
"I prefer dark mode",
user_id="alice",
agent_id=None,
app_id=None,
run_id=None,
messages=None,
file=None,
metadata=None,
immutable=False,
no_infer=False,
expires=None,
categories=None,
output="text", # will be overridden to "agent"
)
raw = buf.getvalue()
data = json.loads(raw)
assert data["status"] == "success"
assert data["command"] == "add"
assert "data" in data
assert isinstance(data["data"], list)
assert data["count"] == 1
assert set(data["data"][0].keys()) == {"id", "memory", "event"}
def test_add_agent_mode_scope(self, mock_backend):
console, buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
):
cmd_add(
mock_backend,
"test",
user_id="bob",
agent_id="agent1",
app_id=None,
run_id=None,
messages=None,
file=None,
metadata=None,
immutable=False,
no_infer=False,
expires=None,
categories=None,
output="text",
)
data = json.loads(buf.getvalue())
assert data["scope"]["user_id"] == "bob"
assert data["scope"]["agent_id"] == "agent1"
# ── search ───────────────────────────────────────────────────────────────
def test_search_agent_mode_envelope(self, mock_backend):
console, buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
):
cmd_search(
mock_backend,
"dark mode",
user_id="alice",
agent_id=None,
app_id=None,
run_id=None,
top_k=10,
threshold=0.3,
rerank=False,
keyword=False,
filter_json=None,
fields=None,
output="text",
)
data = json.loads(buf.getvalue())
assert data["status"] == "success"
assert data["command"] == "search"
assert isinstance(data["data"], list)
assert data["count"] == 2
assert "duration_ms" in data
assert set(data["data"][0].keys()) == {"id", "memory", "score", "created_at", "categories"}
# ── list ─────────────────────────────────────────────────────────────────
def test_list_agent_mode_envelope(self, mock_backend):
console, buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
):
cmd_list(
mock_backend,
user_id="alice",
agent_id=None,
app_id=None,
run_id=None,
page=1,
page_size=100,
category=None,
after=None,
before=None,
output="table", # will be overridden to "agent"
)
data = json.loads(buf.getvalue())
assert data["status"] == "success"
assert data["command"] == "list"
assert isinstance(data["data"], list)
assert data["count"] == 2
assert data["scope"]["user_id"] == "alice"
assert set(data["data"][0].keys()) == {"id", "memory", "created_at", "categories"}
# ── get ──────────────────────────────────────────────────────────────────
def test_get_agent_mode_envelope(self, mock_backend):
console, buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
):
cmd_get(mock_backend, "abc-123-def-456", output="text")
data = json.loads(buf.getvalue())
assert data["status"] == "success"
assert data["command"] == "get"
assert isinstance(data["data"], dict)
assert data["data"]["id"] == "abc-123-def-456"
assert "memory" in data["data"]
assert set(data["data"].keys()) >= {"id", "memory"}
# ── update ───────────────────────────────────────────────────────────────
def test_update_agent_mode_envelope(self, mock_backend):
console, buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
):
cmd_update(mock_backend, "abc-123", "Updated content", metadata=None, output="text")
data = json.loads(buf.getvalue())
assert data["status"] == "success"
assert data["command"] == "update"
assert isinstance(data["data"], dict)
assert "memory" in data["data"]
assert "duration_ms" in data
# ── delete ───────────────────────────────────────────────────────────────
def test_delete_agent_mode_envelope(self, mock_backend):
console, buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
):
cmd_delete(mock_backend, "abc-123-def-456", output="text")
data = json.loads(buf.getvalue())
assert data["status"] == "success"
assert data["command"] == "delete"
assert data["data"]["id"] == "abc-123-def-456"
assert data["data"]["deleted"] is True
assert "duration_ms" in data
# ── event list ───────────────────────────────────────────────────────────
def test_event_list_agent_mode_envelope(self, mock_backend):
console, buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.events_cmd.console", console),
patch("mem0_cli.commands.events_cmd.err_console", err_console),
):
cmd_event_list(mock_backend, output="table")
data = json.loads(buf.getvalue())
assert data["status"] == "success"
assert data["command"] == "event list"
assert isinstance(data["data"], list)
assert data["count"] == 2
assert "duration_ms" in data
assert set(data["data"][0].keys()) == {
"id",
"event_type",
"status",
"latency",
"created_at",
}
# ── event status ─────────────────────────────────────────────────────────
def test_event_status_agent_mode_envelope(self, mock_backend):
console, buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.events_cmd.console", console),
patch("mem0_cli.commands.events_cmd.err_console", err_console),
):
cmd_event_status(mock_backend, "evt-abc-123-def-456", output="text")
data = json.loads(buf.getvalue())
assert data["status"] == "success"
assert data["command"] == "event status"
assert isinstance(data["data"], dict)
assert data["data"]["id"] == "evt-abc-123-def-456"
assert "duration_ms" in data
assert set(data["data"]["results"][0].keys()) == {"id", "event", "user_id", "memory"}
assert "data" not in data["data"]["results"][0]
# ── error handling ───────────────────────────────────────────────────────
def test_error_in_agent_mode_produces_json_to_stdout(self, mock_backend):
"""Errors in agent mode must emit a JSON envelope to stdout, not stderr."""
from io import StringIO
mock_backend.get.side_effect = Exception("Memory not found")
console, _buf = _make_console()
err_console, _err_buf = _make_err_console()
captured_stdout = StringIO()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
patch("sys.stdout", captured_stdout),
pytest.raises((SystemExit, ClickExit)),
):
cmd_get(mock_backend, "bad-id", output="text")
stdout_output = captured_stdout.getvalue()
# The error JSON envelope must be on stdout
error_data = json.loads(stdout_output)
assert error_data["status"] == "error"
assert "error" in error_data
assert error_data["data"] is None
def test_branding_suppressed_in_agent_mode(self, mock_backend):
"""Scope line and success message must be absent in agent mode output."""
console, buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
):
cmd_add(
mock_backend,
"branding test",
user_id="alice",
agent_id=None,
app_id=None,
run_id=None,
messages=None,
file=None,
metadata=None,
immutable=False,
no_infer=False,
expires=None,
categories=None,
output="text",
)
output = buf.getvalue()
# Must be valid JSON only — no human-readable branding
data = json.loads(output)
assert data["status"] == "success"
# "Scope:" and "Memory processed" must NOT appear in the raw output
assert "Scope:" not in output
assert "Memory processed" not in output
assert "spinner" not in output.lower()
def test_no_spinner_in_agent_mode(self, mock_backend):
"""timed_status must not emit spinner output in agent mode."""
err_buf = StringIO()
err_console_buf = Console(file=err_buf, force_terminal=False, no_color=True, width=120)
console, _buf = _make_console()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console_buf),
):
cmd_search(
mock_backend,
"query",
user_id="alice",
agent_id=None,
app_id=None,
run_id=None,
top_k=5,
threshold=0.3,
rerank=False,
keyword=False,
filter_json=None,
fields=None,
output="text",
)
# The err_buf captures what would have been spinner/timing noise
# In agent mode it should be empty (no status lines printed)
err_output = err_buf.getvalue()
assert "Searching" not in err_output
+146
View File
@@ -11,6 +11,7 @@ from mem0_cli.output import (
format_memories_table,
format_memories_text,
format_single_memory,
sanitize_agent_data,
)
@@ -129,8 +130,153 @@ class TestAddResult:
output = buf.getvalue()
assert output.strip() == ""
def test_format_add_result_deduplicates_pending_by_event_id(self):
console, buf = _make_console()
result = {
"results": [
{"status": "PENDING", "event_id": "evt-dup"},
{"status": "PENDING", "event_id": "evt-dup"},
]
}
format_add_result(console, result, "text")
output = buf.getvalue()
# Should show only one PENDING block despite two entries with same event_id
assert output.count("evt-dup") == 2 # event_id line + status hint line
assert output.count("Queued") == 1
def test_format_add_result_empty(self):
console, buf = _make_console()
format_add_result(console, {"results": []}, "text")
output = buf.getvalue()
assert "No memories extracted" in output
class TestSanitizeAgentData:
def test_add_projects_fields(self):
raw = [
{
"id": "abc",
"memory": "test",
"event": "ADD",
"metadata": {"x": 1},
"categories": ["a"],
}
]
result = sanitize_agent_data("add", raw)
assert result == [{"id": "abc", "memory": "test", "event": "ADD"}]
def test_add_pending_passthrough(self):
raw = [{"status": "PENDING", "event_id": "evt-123", "metadata": "noise"}]
result = sanitize_agent_data("add", raw)
assert result == [{"status": "PENDING", "event_id": "evt-123"}]
def test_search_projects_fields(self):
raw = [
{
"id": "abc",
"memory": "test",
"score": 0.9,
"created_at": "2026-01-01",
"categories": ["a"],
"user_id": "u1",
"agent_id": None,
}
]
result = sanitize_agent_data("search", raw)
assert result == [
{
"id": "abc",
"memory": "test",
"score": 0.9,
"created_at": "2026-01-01",
"categories": ["a"],
}
]
def test_list_projects_fields(self):
raw = [
{
"id": "abc",
"memory": "test",
"created_at": "2026-01-01",
"categories": ["a"],
"user_id": "u1",
}
]
result = sanitize_agent_data("list", raw)
assert result == [
{"id": "abc", "memory": "test", "created_at": "2026-01-01", "categories": ["a"]}
]
def test_get_projects_fields(self):
raw = {
"id": "abc",
"memory": "test",
"created_at": "2026-01-01",
"updated_at": "2026-01-02",
"categories": ["a"],
"metadata": {"k": "v"},
"user_id": "u1",
}
result = sanitize_agent_data("get", raw)
assert "user_id" not in result
assert "id" in result and "memory" in result
def test_update_projects_fields(self):
raw = {"id": "abc", "memory": "updated", "extra": "noise"}
result = sanitize_agent_data("update", raw)
assert result == {"id": "abc", "memory": "updated"}
def test_event_list_projects_fields(self):
raw = [
{
"id": "evt-1",
"event_type": "ADD",
"status": "SUCCEEDED",
"graph_status": None,
"latency": 100.0,
"created_at": "2026-01-01",
"updated_at": "2026-01-02",
}
]
result = sanitize_agent_data("event list", raw)
assert result == [
{
"id": "evt-1",
"event_type": "ADD",
"status": "SUCCEEDED",
"latency": 100.0,
"created_at": "2026-01-01",
}
]
assert "updated_at" not in result[0]
assert "graph_status" not in result[0]
def test_event_status_flattens_results(self):
raw = {
"id": "evt-1",
"event_type": "ADD",
"status": "SUCCEEDED",
"latency": 100.0,
"created_at": "2026-01-01",
"updated_at": "2026-01-02",
"results": [
{"id": "mem-1", "event": "ADD", "user_id": "alice", "data": {"memory": "dark mode"}}
],
}
result = sanitize_agent_data("event status", raw)
assert result["results"][0] == {
"id": "mem-1",
"event": "ADD",
"user_id": "alice",
"memory": "dark mode",
}
assert "data" not in result["results"][0]
def test_passthrough_commands(self):
for cmd in ("status", "import", "config show", "config get", "config set"):
data = {"key": "value", "other": "stuff"}
assert sanitize_agent_data(cmd, data) == data
def test_none_data(self):
assert sanitize_agent_data("add", None) is None
+88 -16
View File
@@ -1,6 +1,6 @@
---
title: CLI
description: "Manage memories from your terminal. Available for Node.js and Python."
description: "Manage memories from your terminal — built for both humans and AI agents."
icon: "terminal"
iconType: "solid"
---
@@ -9,6 +9,10 @@ The mem0 CLI lets you add, search, list, update, and delete memories directly fr
Both implementations provide identical behavior — same commands, same options, same output formats.
<Tip>
**Built for AI agents.** Pass `--agent` (or `--json`) as a global flag on any command to get structured JSON output optimized for programmatic consumption — sanitized fields, no colors or spinners, and errors as JSON too. Drop it into any agent tool loop with zero extra parsing.
</Tip>
## Installation
<CodeGroup>
@@ -74,8 +78,23 @@ Interactive setup wizard. Prompts for your API key and default user ID.
```bash
mem0 init
mem0 init --api-key m0-xxx --user-id alice
mem0 init --email alice@company.com
```
If an existing configuration is detected, the CLI will ask for confirmation before overwriting. Use `--force` to skip the prompt (useful in CI/CD pipelines).
```bash
mem0 init --api-key m0-xxx --user-id alice --force
```
| Flag | Description |
|------|-------------|
| `--api-key` | API key (skip prompt) |
| `-u, --user-id` | Default user ID (skip prompt) |
| `--email` | Login via email verification code |
| `--code` | Verification code (use with `--email` for non-interactive login) |
| `--force` | Overwrite existing config without confirmation |
### `mem0 add`
Add a memory from text, a JSON messages array, a file, or stdin.
@@ -202,16 +221,32 @@ mem0 config get api_key # Get a specific value
mem0 config set user_id bob # Set a value
```
### `mem0 entities`
### `mem0 entity`
List or delete entities (users, agents, apps).
List or delete entities (users, agents, apps, runs).
```bash
mem0 entities list
mem0 entities list --type agent --output json
mem0 entities delete --user-id alice --force
mem0 entity list users
mem0 entity list agents --output json
mem0 entity delete --user-id alice --force
```
### `mem0 event`
Inspect background processing events created by async operations (e.g. bulk deletes, large add jobs).
```bash
# List recent events
mem0 event list
# Check the status of a specific event
mem0 event status <event-id>
```
| Flag | Description |
|------|-------------|
| `-o, --output` | Output format: `text`, `json` |
### `mem0 status`
Verify your API connection and display the current project.
@@ -238,6 +273,7 @@ All commands support the `--output` flag to control how results are displayed:
| `json` | Structured JSON, suitable for piping to `jq` or consumption by AI agents |
| `table` | Tabular format (default for `list`) |
| `quiet` | Minimal output — just IDs or status codes |
| `agent` | Structured JSON envelope with sanitized fields — set automatically by `--json`/`--agent` |
Example with JSON output:
@@ -247,20 +283,54 @@ mem0 search "user preferences" --user-id alice --output json | jq '.data.results
## Use with AI agents
The CLI is designed to be used by AI agents and automation tools. Two features make this straightforward:
The CLI is purpose-built for use inside AI agent tool loops. Pass `--agent` or `--json` as a global flag on **any** command to activate agent mode:
- **`--output json`** returns structured data that agents can parse directly
- **`mem0 help --json`** returns the full command tree as JSON, so agents can discover available commands programmatically
- Every command outputs a consistent JSON envelope: `{"status", "command", "duration_ms", "scope", "count", "data"}`
- The `data` field contains only the fields that matter — IDs, memory text, scores, categories. Noisy API fields are stripped.
- All human-readable output is suppressed: no spinners, no colors, no banners.
- Errors are returned as JSON to stdout with a non-zero exit code, so your agent can catch them the same way as successes.
```bash
# Agent adds a memory
mem0 add "User prefers concise responses" --user-id user-42 --output quiet
# Agent searches memories and parses results
mem0 search "response preferences" --user-id user-42 --output json
# Drop --agent on any command and get clean, parseable JSON
mem0 --agent search "response preferences" --user-id user-42
mem0 --agent add "User prefers concise responses" --user-id user-42
mem0 --agent list --user-id user-42
mem0 --agent delete --all --user-id user-42 --force
```
For non-interactive environments (CI, agent runtimes), use `mem0 init --api-key --user-id` or set the `MEM0_API_KEY` environment variable to skip interactive prompts.
<CodeGroup>
```json Output: mem0 --agent search "dark mode" --user-id alice
{
"status": "success",
"command": "search",
"duration_ms": 134,
"scope": { "user_id": "alice" },
"count": 2,
"data": [
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] },
{ "id": "def-456", "memory": "User uses vim keybindings", "score": 0.81, "created_at": "2026-01-10", "categories": ["tools"] }
]
}
```
```json Output: mem0 --agent add "Likes concise answers" --user-id alice
{
"status": "success",
"command": "add",
"duration_ms": 210,
"data": [
{ "id": "ghi-789", "memory": "Likes concise answers", "event": "ADD" }
]
}
```
</CodeGroup>
Two other agent-friendly features:
- **`--output json`** returns structured data without sanitization — useful when you want the full raw API response
- **`mem0 help --json`** returns the complete command tree as JSON, so agents can self-discover available commands and options
For non-interactive environments (CI, agent runtimes), set credentials via `mem0 init --api-key m0-xxx --user-id alice --force` or the `MEM0_API_KEY` environment variable.
## Environment variables
@@ -278,10 +348,12 @@ Environment variables take precedence over values in the config file, which take
## Global flags
These flags are available on all commands that interact with the API:
These flags are available on all commands:
| Flag | Description |
|------|-------------|
| `--json` | Enable agent mode: structured JSON envelope output, no colors or spinners |
| `--agent` | Alias for `--json` |
| `--api-key` | Override the configured API key for this request |
| `--base-url` | Override the configured API base URL for this request |
| `-o, --output` | Set the output format |