fix(cli): fix critical crashes, improve error messages, UX and validation in Node.js and Python SDKs (#4636)

This commit is contained in:
Saket Aryan
2026-03-31 20:28:48 +05:30
committed by GitHub
parent 93bd4e248c
commit 9bebcf45f7
14 changed files with 198 additions and 43 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
"mem0": "./dist/index.js"
},
"scripts": {
"build": "tsup src/index.ts --format esm --dts --clean",
"build": "tsup",
"dev": "tsx src/index.ts",
"test": "vitest run",
"test:watch": "vitest",
+8 -3
View File
@@ -58,15 +58,20 @@ export class PlatformBackend implements Backend {
if (resp.status === 400) {
let detail: string;
try {
const body = await resp.json();
detail = (body as Record<string, string>).detail ?? resp.statusText;
const body = await resp.json() as Record<string, unknown>;
detail = (body.detail ?? body.message ?? JSON.stringify(body)) as string ?? resp.statusText;
} catch {
detail = resp.statusText;
}
throw new APIError(path, detail);
}
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
let detail: string = resp.statusText;
try {
const body = await resp.json() as Record<string, unknown>;
detail = (body.detail ?? body.message ?? resp.statusText) as string;
} catch { /* ignore */ }
throw new Error(`HTTP ${resp.status}: ${detail}`);
}
if (resp.status === 204) {
return {};
+3 -7
View File
@@ -4,10 +4,6 @@
import chalk from "chalk";
import ora, { type Ora } from "ora";
import { createRequire } from "node:module";
const _require = createRequire(import.meta.url);
const PKG_VERSION: string = _require("../package.json").version;
export const LOGO = `
███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
@@ -48,7 +44,7 @@ export function printBanner(): void {
const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2))
const logoLines = LOGO.trimEnd().split("\n");
const tagline = ` ${TAGLINE}`;
const subtitle = `Node.js SDK · v${PKG_VERSION}`;
const subtitle = `Node.js SDK · v${__CLI_VERSION__}`;
const contentLines = ["", ...logoLines, "", tagline, ""];
// Compute inner width from longest content line + padding both sides
@@ -90,7 +86,7 @@ export function printWarning(message: string): void {
}
export function printInfo(message: string): void {
console.log(`${brand(sym("◆", "*"))} ${message}`);
console.error(`${brand(sym("◆", "*"))} ${message}`);
}
export function printScope(ids: Record<string, string | undefined>): void {
@@ -102,7 +98,7 @@ export function printScope(ids: Record<string, string | undefined>): void {
}
}
if (parts.length > 0) {
console.log(` ${dim(`Scope: ${parts.join(", ")}`)}`);
console.error(` ${dim(`Scope: ${parts.join(", ")}`)}`);
}
}
+47 -2
View File
@@ -8,6 +8,7 @@ import type { Backend } from "../backend/base.js";
import {
formatAddResult,
formatJson,
formatJsonEnvelope,
formatMemoriesTable,
formatMemoriesText,
formatSingleMemory,
@@ -60,11 +61,27 @@ export async function cmdAdd(
content = fs.readFileSync(0, "utf-8").trim();
}
if (content !== undefined && content.trim() === "") {
printError("Content cannot be empty.");
process.exit(1);
}
if (!content && !msgs) {
printError("No content provided. Pass text, --messages, --file, or pipe via stdin.");
process.exit(1);
}
// Validate --expires
if (opts.expires) {
if (!/^\d{4}-\d{2}-\d{2}$/.test(opts.expires)) {
printError("Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).");
process.exit(1);
}
if (new Date(opts.expires) <= new Date()) {
printError("--expires date must be in the future.");
process.exit(1);
}
}
let meta: Record<string, unknown> | undefined;
if (opts.metadata) {
try {
@@ -155,6 +172,15 @@ export async function cmdSearch(
const fieldList = opts.fields ? opts.fields.split(",").map((f) => f.trim()) : undefined;
if (opts.topK < 1) {
printError("--top-k must be >= 1.");
process.exit(1);
}
if (opts.threshold < 0 || opts.threshold > 1) {
printError("--threshold must be between 0.0 and 1.0.");
process.exit(1);
}
const start = performance.now();
let results: Record<string, unknown>[];
try {
@@ -179,11 +205,13 @@ export async function cmdSearch(
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "quiet") return;
if (opts.output === "json") {
formatJson(results);
} else if (opts.output === "table") {
if (results.length > 0) {
formatMemoriesTable(results);
formatMemoriesTable(results, { showScore: true });
printResultSummary({ count: results.length, durationSecs: elapsed, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } });
} else {
console.log();
@@ -236,6 +264,15 @@ export async function cmdList(
output: string;
},
): Promise<void> {
if (opts.pageSize < 1) {
printError("--page-size must be >= 1.");
process.exit(1);
}
if (opts.page < 1) {
printError("--page must be >= 1.");
process.exit(1);
}
const start = performance.now();
let results: Record<string, unknown>[];
try {
@@ -259,8 +296,15 @@ export async function cmdList(
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "quiet") return;
if (opts.output === "json") {
formatJson(results);
formatJsonEnvelope({
command: "list",
data: results,
count: results.length,
scope: { user_id: opts.userId, agent_id: opts.agentId },
});
} else if (opts.output === "table") {
if (results.length > 0) {
formatMemoriesTable(results);
@@ -372,6 +416,7 @@ export async function cmdDeleteAll(
// 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;
}
+1 -5
View File
@@ -3,7 +3,6 @@
*/
import fs from "node:fs";
import { createRequire } from "node:module";
import { printError, printSuccess, timedStatus, colors } from "../branding.js";
import type { Backend } from "../backend/base.js";
import { formatJsonEnvelope } from "../output.js";
@@ -11,9 +10,6 @@ import boxen from "boxen";
const { brand, dim, success, error: errorColor } = colors;
const _require = createRequire(import.meta.url);
const VERSION: string = _require("../../package.json").version;
export async function cmdStatus(
backend: Backend,
opts: { userId?: string; agentId?: string; output?: string } = {},
@@ -73,7 +69,7 @@ export async function cmdStatus(
}
export function cmdVersion(): void {
console.log(` ${brand("◆ Mem0")} CLI v${VERSION}`);
console.log(` ${brand("◆ Mem0")} CLI v${__CLI_VERSION__}`);
}
export async function cmdImport(
+8
View File
@@ -132,6 +132,14 @@ const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
"defaults.app_id": ["defaults", "appId"],
"defaults.run_id": ["defaults", "runId"],
"defaults.enable_graph": ["defaults", "enableGraph"],
// Short-form aliases
"api_key": ["platform", "apiKey"],
"base_url": ["platform", "baseUrl"],
"user_id": ["defaults", "userId"],
"agent_id": ["defaults", "agentId"],
"app_id": ["defaults", "appId"],
"run_id": ["defaults", "runId"],
"enable_graph": ["defaults", "enableGraph"],
};
export function getNestedValue(config: Mem0Config, dottedKey: string): unknown {
+2
View File
@@ -0,0 +1,2 @@
/** Injected by tsup at build time from package.json version field. */
declare const __CLI_VERSION__: string;
+14 -8
View File
@@ -7,7 +7,6 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createRequire } from "node:module";
import { Command } from "commander";
import type { Mem0Config } from "./config.js";
import { loadConfig } from "./config.js";
@@ -15,9 +14,6 @@ import { getBackend, type Backend } from "./backend/index.js";
import { printError, colors } from "./branding.js";
import { richFormatHelp } from "./help.js";
const _require = createRequire(import.meta.url);
const VERSION: string = _require("../package.json").version;
const program = new Command();
// ── Helpers ──────────────────────────────────────────────────────────────
@@ -95,10 +91,10 @@ function resolveGraph(
program
.name("mem0")
.description(`◆ Mem0 CLI v${VERSION} · Node.js SDK\n\nThe Memory Layer for AI Agents`)
.description(`◆ Mem0 CLI v${__CLI_VERSION__} · Node.js SDK\n\nThe Memory Layer for AI Agents`)
.option("--version", "Show version and exit.")
.on("option:version", () => {
console.log(` ${colors.brand("◆ Mem0")} CLI v${VERSION}`);
console.log(` ${colors.brand("◆ Mem0")} CLI v${__CLI_VERSION__}`);
process.exit(0);
})
.usage("<command> [options]")
@@ -476,11 +472,11 @@ program
const spec = JSON.parse(fs.readFileSync(specPath, "utf-8"));
console.log(JSON.stringify(spec, null, 2));
} else {
console.log(JSON.stringify({ name: "mem0", version: VERSION, description: "The Memory Layer for AI Agents" }, null, 2));
console.log(JSON.stringify({ name: "mem0", version: __CLI_VERSION__, description: "The Memory Layer for AI Agents" }, null, 2));
}
} else {
const { brand: b } = colors;
console.log(`${b("◆ Mem0 CLI")} v${VERSION} · Node.js SDK\n The Memory Layer for AI Agents\n`);
console.log(`${b("◆ Mem0 CLI")} v${__CLI_VERSION__} · Node.js SDK\n The Memory Layer for AI Agents\n`);
console.log("Usage: mem0 <command> [OPTIONS]\n");
console.log("Commands:");
console.log(" add Add a memory from text, messages, file, or stdin");
@@ -501,6 +497,16 @@ program
}
});
// ── Version ───────────────────────────────────────────────────────────────
program
.command("version")
.description("Show version.")
.action(async () => {
const { cmdVersion } = await import("./commands/utils.js");
cmdVersion();
});
// ── Entrypoint ────────────────────────────────────────────────────────────
program.parse();
+20 -6
View File
@@ -52,16 +52,22 @@ export function formatMemoriesText(
}
}
export function formatMemoriesTable(memories: Record<string, unknown>[]): void {
export function formatMemoriesTable(memories: Record<string, unknown>[], opts: { showScore?: boolean } = {}): void {
const head = opts.showScore
? [accent("ID"), accent("Score"), accent("Memory"), accent("Category"), accent("Created")]
: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")];
const colWidths = opts.showScore
? [38, 8, 40, 16, 14]
: [38, 40, 16, 14];
const table = new Table({
head: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")],
colWidths: [12, 52, 16, 14],
head,
colWidths,
wordWrap: true,
style: { head: [], border: [] },
});
for (const mem of memories) {
const memId = ((mem.id as string) ?? "").slice(0, 8);
const memId = ((mem.id as string) ?? "");
let memoryText = (mem.memory ?? mem.text ?? "") as string;
if (memoryText.length > 60) {
memoryText = memoryText.slice(0, 57) + "...";
@@ -69,10 +75,18 @@ export function formatMemoriesTable(memories: Record<string, unknown>[]): void {
const categories = mem.categories;
const cat =
Array.isArray(categories) && categories.length > 0
? (categories[0] as string)
? categories.length > 1
? `${categories[0]} (+${categories.length - 1})`
: (categories[0] as string)
: "—";
const created = formatDate(mem.created_at as string | undefined) ?? "—";
table.push([dim(memId), memoryText, cat, created]);
if (opts.showScore) {
const score = mem.score as number | undefined;
const scoreStr = score !== undefined ? score.toFixed(2) : "—";
table.push([dim(memId), scoreStr, memoryText, cat, created]);
} else {
table.push([dim(memId), memoryText, cat, created]);
}
}
console.log();
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from 'tsup';
import { createRequire } from 'node:module';
const _require = createRequire(import.meta.url);
const pkg = _require('./package.json');
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
clean: true,
define: {
__CLI_VERSION__: JSON.stringify(pkg.version),
},
});