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
+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;
}