fix(cli): fix critical crashes, improve error messages, UX and validation in Node.js and Python SDKs (#4636)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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(", ")}`)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
/** Injected by tsup at build time from package.json version field. */
|
||||
declare const __CLI_VERSION__: string;
|
||||
+14
-8
@@ -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
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
});
|
||||
@@ -281,8 +281,8 @@ def search(
|
||||
# STEP 7: stdin fallback for query
|
||||
if query is None:
|
||||
query = _read_stdin()
|
||||
if query is None:
|
||||
print_error(err_console, "No query provided. Pass a query argument or pipe via stdin.")
|
||||
if not query or not query.strip():
|
||||
print_error(err_console, "Search query cannot be empty.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
backend, config = _get_backend_and_config(api_key, base_url)
|
||||
@@ -1014,6 +1014,13 @@ 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")
|
||||
|
||||
|
||||
@@ -95,6 +95,17 @@ def cmd_add(
|
||||
except json.JSONDecodeError:
|
||||
cats = [c.strip() for c in categories.split(",")]
|
||||
|
||||
# Validate --expires
|
||||
if expires:
|
||||
import re
|
||||
if not re.match(r'^\d{4}-\d{2}-\d{2}$', expires):
|
||||
print_error(err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).")
|
||||
raise typer.Exit(1)
|
||||
from datetime import date
|
||||
if date.fromisoformat(expires) <= date.today():
|
||||
print_error(err_console, "--expires date must be in the future.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
with timed_status(err_console, "Adding memory...") as ts:
|
||||
try:
|
||||
result = backend.add(
|
||||
@@ -113,7 +124,6 @@ def cmd_add(
|
||||
)
|
||||
except Exception as e:
|
||||
ts.error_msg = str(e)
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
if output == "quiet":
|
||||
@@ -164,6 +174,13 @@ def cmd_search(
|
||||
if fields:
|
||||
field_list = [f.strip() for f in fields.split(",")]
|
||||
|
||||
if top_k < 1:
|
||||
print_error(err_console, "--top-k must be >= 1.")
|
||||
raise typer.Exit(1)
|
||||
if not (0.0 <= threshold <= 1.0):
|
||||
print_error(err_console, "--threshold must be between 0.0 and 1.0.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Searching memories...") as _ts:
|
||||
try:
|
||||
@@ -186,11 +203,14 @@ def cmd_search(
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "quiet":
|
||||
return
|
||||
|
||||
if output == "json":
|
||||
format_json(console, results)
|
||||
elif output == "table":
|
||||
if results:
|
||||
format_memories_table(console, results)
|
||||
format_memories_table(console, results, show_score=True)
|
||||
print_result_summary(
|
||||
console, len(results), duration_secs=_elapsed, user_id=user_id, agent_id=agent_id
|
||||
)
|
||||
@@ -238,6 +258,13 @@ def cmd_list(
|
||||
output: str = "table",
|
||||
) -> None:
|
||||
"""List memories."""
|
||||
if page_size < 1:
|
||||
print_error(err_console, "--page-size must be >= 1.")
|
||||
raise typer.Exit(1)
|
||||
if page < 1:
|
||||
print_error(err_console, "--page must be >= 1.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Listing memories...") as _ts:
|
||||
try:
|
||||
@@ -258,8 +285,18 @@ def cmd_list(
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "quiet":
|
||||
return
|
||||
|
||||
if output == "json":
|
||||
format_json(console, results)
|
||||
from mem0_cli.output import format_json_envelope
|
||||
format_json_envelope(
|
||||
console,
|
||||
command="list",
|
||||
data=results,
|
||||
count=len(results),
|
||||
scope={k: v for k, v in {"user_id": user_id, "agent_id": agent_id}.items() if v},
|
||||
)
|
||||
elif output == "table":
|
||||
if results:
|
||||
format_memories_table(console, results)
|
||||
@@ -376,6 +413,7 @@ def cmd_delete_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
|
||||
|
||||
|
||||
@@ -45,6 +45,17 @@ class Mem0Config:
|
||||
platform: PlatformConfig = field(default_factory=PlatformConfig)
|
||||
|
||||
|
||||
SHORT_KEY_ALIASES: dict[str, str] = {
|
||||
"api_key": "platform.api_key",
|
||||
"base_url": "platform.base_url",
|
||||
"user_id": "defaults.user_id",
|
||||
"agent_id": "defaults.agent_id",
|
||||
"app_id": "defaults.app_id",
|
||||
"run_id": "defaults.run_id",
|
||||
"enable_graph": "defaults.enable_graph",
|
||||
}
|
||||
|
||||
|
||||
def ensure_config_dir() -> Path:
|
||||
"""Create ~/.mem0 directory with secure permissions if it doesn't exist."""
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
@@ -140,7 +151,8 @@ def redact_key(key: str) -> str:
|
||||
|
||||
|
||||
def get_nested_value(config: Mem0Config, dotted_key: str) -> Any:
|
||||
"""Get a config value by dotted path, e.g. 'platform.api_key'."""
|
||||
"""Get a config value by dotted path, e.g. 'platform.api_key' or short form 'api_key'."""
|
||||
dotted_key = SHORT_KEY_ALIASES.get(dotted_key, dotted_key)
|
||||
parts = dotted_key.split(".")
|
||||
obj: Any = config
|
||||
for part in parts:
|
||||
@@ -153,6 +165,7 @@ def get_nested_value(config: Mem0Config, dotted_key: str) -> Any:
|
||||
|
||||
def set_nested_value(config: Mem0Config, dotted_key: str, value: str) -> bool:
|
||||
"""Set a config value by dotted path. Returns True on success."""
|
||||
dotted_key = SHORT_KEY_ALIASES.get(dotted_key, dotted_key)
|
||||
parts = dotted_key.split(".")
|
||||
obj: Any = config
|
||||
for part in parts[:-1]:
|
||||
|
||||
@@ -49,7 +49,7 @@ def format_memories_text(console: Console, memories: list[dict], title: str = "m
|
||||
console.print()
|
||||
|
||||
|
||||
def format_memories_table(console: Console, memories: list[dict]) -> None:
|
||||
def format_memories_table(console: Console, memories: list[dict], *, show_score: bool = False) -> None:
|
||||
"""Render memories in a rich table."""
|
||||
table = Table(
|
||||
border_style=BRAND_COLOR,
|
||||
@@ -57,20 +57,30 @@ def format_memories_table(console: Console, memories: list[dict]) -> None:
|
||||
row_styles=["", "dim"],
|
||||
padding=(0, 1),
|
||||
)
|
||||
table.add_column("ID", style="dim", max_width=10)
|
||||
table.add_column("ID", style="dim", max_width=38, no_wrap=True)
|
||||
if show_score:
|
||||
table.add_column("Score", max_width=7, justify="right")
|
||||
table.add_column("Memory", max_width=50, no_wrap=False)
|
||||
table.add_column("Category", max_width=14)
|
||||
table.add_column("Created", max_width=12)
|
||||
|
||||
for mem in memories:
|
||||
mem_id = mem.get("id", "")[:8]
|
||||
mem_id = mem.get("id", "")
|
||||
memory_text = mem.get("memory", mem.get("text", ""))
|
||||
if len(memory_text) > 60:
|
||||
memory_text = memory_text[:57] + "..."
|
||||
categories = mem.get("categories", [])
|
||||
cat = categories[0] if isinstance(categories, list) and categories else "—"
|
||||
if isinstance(categories, list) and categories:
|
||||
cat = categories[0] if len(categories) == 1 else f"{categories[0]} (+{len(categories) - 1})"
|
||||
else:
|
||||
cat = "—"
|
||||
created = _format_date(mem.get("created_at")) or "—"
|
||||
table.add_row(mem_id, memory_text, cat, created)
|
||||
if show_score:
|
||||
score = mem.get("score")
|
||||
score_str = f"{score:.2f}" if score is not None else "—"
|
||||
table.add_row(mem_id, score_str, memory_text, cat, created)
|
||||
else:
|
||||
table.add_row(mem_id, memory_text, cat, created)
|
||||
|
||||
console.print()
|
||||
console.print(table)
|
||||
|
||||
Reference in New Issue
Block a user