555e282cc4
ci / changelog_check (push) Waiting to run
ci / check_changes (push) Waiting to run
ci / build_mem0 (3.10) (push) Blocked by required conditions
ci / build_mem0 (3.11) (push) Blocked by required conditions
ci / build_mem0 (3.12) (push) Blocked by required conditions
CLI Node CI / lint (push) Waiting to run
CLI Node CI / test (20) (push) Waiting to run
CLI Node CI / test (22) (push) Waiting to run
CLI Node CI / build (push) Waiting to run
CLI Python CI / lint (push) Waiting to run
CLI Python CI / test (3.10) (push) Waiting to run
CLI Python CI / test (3.11) (push) Waiting to run
CLI Python CI / test (3.12) (push) Waiting to run
CLI Python CI / build (push) Waiting to run
openclaw checks / lint (push) Waiting to run
openclaw checks / test (20) (push) Waiting to run
openclaw checks / test (22) (push) Waiting to run
openclaw checks / build (push) Waiting to run
opencode-plugin checks / build (push) Waiting to run
pi-agent-plugin checks / lint (push) Waiting to run
pi-agent-plugin checks / test (20) (push) Waiting to run
pi-agent-plugin checks / test (22) (push) Waiting to run
pi-agent-plugin checks / build (push) Waiting to run
TypeScript SDK CI / check_changes (push) Waiting to run
TypeScript SDK CI / changelog_check (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (22) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (22) (push) Blocked by required conditions
398 lines
11 KiB
TypeScript
398 lines
11 KiB
TypeScript
/**
|
|
* Output formatting for mem0 CLI — text, JSON, table, quiet modes.
|
|
*/
|
|
|
|
import boxen from "boxen";
|
|
import Table from "cli-table3";
|
|
import { colors, sym } from "./branding.js";
|
|
import { takeNotice } from "./state.js";
|
|
|
|
const { brand, accent, success, error: errorColor, dim } = colors;
|
|
|
|
function formatDate(dtStr?: string): string | undefined {
|
|
if (!dtStr) return undefined;
|
|
try {
|
|
const dt = new Date(dtStr.replace("Z", "+00:00"));
|
|
return dt.toISOString().slice(0, 10);
|
|
} catch {
|
|
return dtStr?.slice(0, 10);
|
|
}
|
|
}
|
|
|
|
export function formatMemoriesText(
|
|
memories: Record<string, unknown>[],
|
|
title = "memories",
|
|
): void {
|
|
const count = memories.length;
|
|
console.log(`\n${brand(`Found ${count} ${title}:`)}\n`);
|
|
|
|
for (let i = 0; i < memories.length; i++) {
|
|
const mem = memories[i];
|
|
const memoryText = (mem.memory ?? mem.text ?? "") as string;
|
|
const memId = ((mem.id as string) ?? "").slice(0, 8);
|
|
const score = mem.score as number | undefined;
|
|
const created = formatDate(mem.created_at as string | undefined);
|
|
let category: string | undefined;
|
|
const cats = mem.categories;
|
|
if (Array.isArray(cats)) {
|
|
category = cats[0] as string | undefined;
|
|
}
|
|
|
|
console.log(` ${i + 1}. ${memoryText}`);
|
|
|
|
const details: string[] = [];
|
|
if (score !== undefined) details.push(`Score: ${score.toFixed(2)}`);
|
|
if (memId) details.push(`ID: ${memId}`);
|
|
if (created) details.push(`Created: ${created}`);
|
|
if (category) details.push(`Category: ${category}`);
|
|
|
|
if (details.length > 0) {
|
|
console.log(` ${dim(details.join(" · "))}`);
|
|
}
|
|
console.log();
|
|
}
|
|
}
|
|
|
|
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,
|
|
colWidths,
|
|
wordWrap: true,
|
|
style: { head: [], border: [] },
|
|
});
|
|
|
|
for (const mem of memories) {
|
|
const memId = (mem.id as string) ?? "";
|
|
let memoryText = (mem.memory ?? mem.text ?? "") as string;
|
|
if (memoryText.length > 60) {
|
|
memoryText = `${memoryText.slice(0, 57)}...`;
|
|
}
|
|
const categories = mem.categories;
|
|
const cat =
|
|
Array.isArray(categories) && categories.length > 0
|
|
? categories.length > 1
|
|
? `${categories[0]} (+${categories.length - 1})`
|
|
: (categories[0] as string)
|
|
: "—";
|
|
const created = formatDate(mem.created_at as string | undefined) ?? "—";
|
|
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();
|
|
console.log(table.toString());
|
|
console.log();
|
|
}
|
|
|
|
export function formatJson(data: unknown): void {
|
|
console.log(JSON.stringify(data, null, 2));
|
|
}
|
|
|
|
export function formatSingleMemory(
|
|
mem: Record<string, unknown>,
|
|
output = "text",
|
|
): void {
|
|
if (output === "json") {
|
|
formatJson(mem);
|
|
return;
|
|
}
|
|
|
|
const memoryText = (mem.memory ?? mem.text ?? "") as string;
|
|
const memId = (mem.id ?? "") as string;
|
|
|
|
const lines: string[] = [];
|
|
lines.push(` ${memoryText}`);
|
|
lines.push("");
|
|
|
|
if (memId) lines.push(` ${dim("ID:")} ${memId}`);
|
|
const created = formatDate(mem.created_at as string | undefined);
|
|
if (created) lines.push(` ${dim("Created:")} ${created}`);
|
|
const updated = formatDate(mem.updated_at as string | undefined);
|
|
if (updated) lines.push(` ${dim("Updated:")} ${updated}`);
|
|
const meta = mem.metadata;
|
|
if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`);
|
|
const categories = mem.categories;
|
|
if (categories) {
|
|
const catStr = Array.isArray(categories)
|
|
? categories.join(", ")
|
|
: String(categories);
|
|
lines.push(` ${dim("Categories:")} ${catStr}`);
|
|
}
|
|
|
|
const content = lines.join("\n");
|
|
console.log();
|
|
console.log(
|
|
boxen(content, {
|
|
title: brand("Memory"),
|
|
titleAlignment: "left",
|
|
borderColor: "magenta",
|
|
padding: 1,
|
|
}),
|
|
);
|
|
console.log();
|
|
}
|
|
|
|
export function formatAddResult(
|
|
result: Record<string, unknown> | Record<string, unknown>[],
|
|
output = "text",
|
|
): void {
|
|
if (output === "json") {
|
|
formatJson(result);
|
|
return;
|
|
}
|
|
if (output === "quiet") return;
|
|
|
|
const results: Record<string, unknown>[] = Array.isArray(result)
|
|
? result
|
|
: ((result.results as Record<string, unknown>[]) ?? [result]);
|
|
|
|
if (!results.length) {
|
|
console.log(` ${dim("No memories extracted.")}`);
|
|
return;
|
|
}
|
|
|
|
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) ?? "";
|
|
// 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",
|
|
];
|
|
console.log(parts.join(" "));
|
|
if (eventId) {
|
|
console.log(` ${dim(` event_id: ${eventId}`)}`);
|
|
console.log(
|
|
` ${dim(` → Check status: mem0 event status ${eventId}`)}`,
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const event = (r.event ?? "ADD") as string;
|
|
const memory = (r.memory ?? r.text ?? r.content ?? r.data ?? "") as string;
|
|
const memId = ((r.id as string) ?? (r.memory_id as string) ?? "").slice(
|
|
0,
|
|
8,
|
|
);
|
|
|
|
let icon: string;
|
|
let label: string;
|
|
if (event === "ADD") {
|
|
icon = success("+");
|
|
label = "Added";
|
|
} else if (event === "UPDATE") {
|
|
icon = accent("~");
|
|
label = "Updated";
|
|
} else if (event === "DELETE") {
|
|
icon = errorColor("-");
|
|
label = "Deleted";
|
|
} else if (event === "NOOP") {
|
|
icon = dim("·");
|
|
label = "No change";
|
|
} else {
|
|
icon = dim("?");
|
|
label = event;
|
|
}
|
|
|
|
const parts = [` ${icon} ${dim(label.padEnd(10))}`];
|
|
if (memory) parts.push(memory);
|
|
if (memId) parts.push(dim(`(${memId})`));
|
|
console.log(parts.join(" "));
|
|
}
|
|
console.log();
|
|
}
|
|
|
|
export function formatJsonEnvelope(opts: {
|
|
command: string;
|
|
data: unknown;
|
|
durationMs?: number;
|
|
scope?: Record<string, string | undefined>;
|
|
count?: number;
|
|
status?: string;
|
|
error?: string;
|
|
}): void {
|
|
const envelope: Record<string, unknown> = {
|
|
status: opts.status ?? "success",
|
|
command: opts.command,
|
|
};
|
|
if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs;
|
|
if (opts.scope !== undefined) envelope.scope = opts.scope;
|
|
if (opts.count !== undefined) envelope.count = opts.count;
|
|
if (opts.error) envelope.error = opts.error;
|
|
envelope.data = opts.data;
|
|
|
|
// If the platform flagged this as an unclaimed Agent Mode account, surface
|
|
// the notice inside the JSON envelope so an agent consuming the output
|
|
// sees it without needing to inspect HTTP headers.
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
const { takeNotice } = require("./state.js");
|
|
const notice = takeNotice();
|
|
if (notice) envelope.mem0_notice = notice;
|
|
|
|
console.log(JSON.stringify(envelope, null, 2));
|
|
}
|
|
|
|
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);
|
|
|
|
// Surface the unclaimed-Agent-Mode notice (if any) in the envelope so an
|
|
// agent reading the JSON output sees it without inspecting HTTP headers.
|
|
const notice = takeNotice();
|
|
if (notice) envelope.mem0_notice = notice;
|
|
|
|
console.log(JSON.stringify(envelope, null, 2));
|
|
}
|
|
|
|
export function printResultSummary(opts: {
|
|
count: number;
|
|
durationSecs?: number;
|
|
page?: number;
|
|
scopeIds?: Record<string, string | undefined>;
|
|
}): void {
|
|
const parts = [`${opts.count} result${opts.count !== 1 ? "s" : ""}`];
|
|
if (opts.page !== undefined) parts.push(`page ${opts.page}`);
|
|
if (opts.scopeIds) {
|
|
const scopeParts = Object.entries(opts.scopeIds)
|
|
.filter(([, v]) => v)
|
|
.map(([k, v]) => `${k}=${v}`);
|
|
if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
|
|
}
|
|
if (opts.durationSecs !== undefined)
|
|
parts.push(`${opts.durationSecs.toFixed(2)}s`);
|
|
|
|
console.log(` ${dim(parts.join(" · "))}`);
|
|
console.log();
|
|
}
|