ci(cli): add CI pipelines for Node and Python CLI SDKs (#4640)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Saket Aryan
2026-03-31 22:30:31 +05:30
committed by GitHub
parent 9bebcf45f7
commit 32c1ccba5b
30 changed files with 3042 additions and 2457 deletions
+80 -71
View File
@@ -6,110 +6,119 @@ import type { Mem0Config } from "../config.js";
import { PlatformBackend } from "./platform.js";
export interface AddOptions {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
metadata?: Record<string, unknown>;
immutable?: boolean;
infer?: boolean;
expires?: string;
categories?: string[];
enableGraph?: boolean;
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
metadata?: Record<string, unknown>;
immutable?: boolean;
infer?: boolean;
expires?: string;
categories?: string[];
enableGraph?: boolean;
}
export interface SearchOptions {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
topK?: number;
threshold?: number;
rerank?: boolean;
keyword?: boolean;
filters?: Record<string, unknown>;
fields?: string[];
enableGraph?: boolean;
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
topK?: number;
threshold?: number;
rerank?: boolean;
keyword?: boolean;
filters?: Record<string, unknown>;
fields?: string[];
enableGraph?: boolean;
}
export interface ListOptions {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
page?: number;
pageSize?: number;
category?: string;
after?: string;
before?: string;
enableGraph?: boolean;
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
page?: number;
pageSize?: number;
category?: string;
after?: string;
before?: string;
enableGraph?: boolean;
}
export interface DeleteOptions {
all?: boolean;
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
all?: boolean;
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
}
export interface EntityIds {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
}
export interface Backend {
add(
content?: string,
messages?: Record<string, unknown>[],
opts?: AddOptions,
): Promise<Record<string, unknown>>;
add(
content?: string,
messages?: Record<string, unknown>[],
opts?: AddOptions,
): Promise<Record<string, unknown>>;
search(query: string, opts?: SearchOptions): Promise<Record<string, unknown>[]>;
search(
query: string,
opts?: SearchOptions,
): Promise<Record<string, unknown>[]>;
get(memoryId: string): Promise<Record<string, unknown>>;
get(memoryId: string): Promise<Record<string, unknown>>;
listMemories(opts?: ListOptions): Promise<Record<string, unknown>[]>;
listMemories(opts?: ListOptions): Promise<Record<string, unknown>[]>;
update(
memoryId: string,
content?: string,
metadata?: Record<string, unknown>,
): Promise<Record<string, unknown>>;
update(
memoryId: string,
content?: string,
metadata?: Record<string, unknown>,
): Promise<Record<string, unknown>>;
delete(memoryId?: string, opts?: DeleteOptions): Promise<Record<string, unknown>>;
delete(
memoryId?: string,
opts?: DeleteOptions,
): Promise<Record<string, unknown>>;
deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>;
deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>;
status(opts?: { userId?: string; agentId?: string }): Promise<Record<string, unknown>>;
status(opts?: { userId?: string; agentId?: string }): Promise<
Record<string, unknown>
>;
entities(entityType: string): Promise<Record<string, unknown>[]>;
entities(entityType: string): Promise<Record<string, unknown>[]>;
}
export class AuthError extends Error {
constructor(message = "Authentication failed. Your API key may be invalid or expired.") {
super(message);
this.name = "AuthError";
}
constructor(
message = "Authentication failed. Your API key may be invalid or expired.",
) {
super(message);
this.name = "AuthError";
}
}
export class NotFoundError extends Error {
constructor(path: string) {
super(`Resource not found: ${path}`);
this.name = "NotFoundError";
}
constructor(path: string) {
super(`Resource not found: ${path}`);
this.name = "NotFoundError";
}
}
export class APIError extends Error {
constructor(path: string, detail: string) {
super(`Bad request to ${path}: ${detail}`);
this.name = "APIError";
}
constructor(path: string, detail: string) {
super(`Bad request to ${path}: ${detail}`);
this.name = "APIError";
}
}
export function getBackend(config: Mem0Config): Backend {
return new PlatformBackend(config.platform);
return new PlatformBackend(config.platform);
}
+8 -1
View File
@@ -3,5 +3,12 @@
*/
export { getBackend } from "./base.js";
export type { Backend, AddOptions, SearchOptions, ListOptions, DeleteOptions, EntityIds } from "./base.js";
export type {
Backend,
AddOptions,
SearchOptions,
ListOptions,
DeleteOptions,
EntityIds,
} from "./base.js";
export { AuthError, NotFoundError, APIError } from "./base.js";
+301 -271
View File
@@ -4,305 +4,335 @@
import type { PlatformConfig } from "../config.js";
import {
type AddOptions,
APIError,
AuthError,
type Backend,
type DeleteOptions,
type EntityIds,
type ListOptions,
NotFoundError,
type SearchOptions,
APIError,
type AddOptions,
AuthError,
type Backend,
type DeleteOptions,
type EntityIds,
type ListOptions,
NotFoundError,
type SearchOptions,
} from "./base.js";
export class PlatformBackend implements Backend {
private baseUrl: string;
private headers: Record<string, string>;
private baseUrl: string;
private headers: Record<string, string>;
constructor(config: PlatformConfig) {
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
this.headers = {
Authorization: `Token ${config.apiKey}`,
"Content-Type": "application/json",
};
}
constructor(config: PlatformConfig) {
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
this.headers = {
Authorization: `Token ${config.apiKey}`,
"Content-Type": "application/json",
};
}
private async _request(
method: string,
path: string,
opts?: { json?: unknown; params?: Record<string, string> },
): Promise<unknown> {
let url = `${this.baseUrl}${path}`;
if (opts?.params) {
const qs = new URLSearchParams(opts.params).toString();
url += `?${qs}`;
}
private async _request(
method: string,
path: string,
opts?: { json?: unknown; params?: Record<string, string> },
): Promise<unknown> {
let url = `${this.baseUrl}${path}`;
if (opts?.params) {
const qs = new URLSearchParams(opts.params).toString();
url += `?${qs}`;
}
const fetchOpts: RequestInit = {
method,
headers: this.headers,
signal: AbortSignal.timeout(30_000),
};
if (opts?.json) {
fetchOpts.body = JSON.stringify(opts.json);
}
const fetchOpts: RequestInit = {
method,
headers: this.headers,
signal: AbortSignal.timeout(30_000),
};
if (opts?.json) {
fetchOpts.body = JSON.stringify(opts.json);
}
const resp = await fetch(url, fetchOpts);
const resp = await fetch(url, fetchOpts);
if (resp.status === 401) {
throw new AuthError();
}
if (resp.status === 404) {
throw new NotFoundError(path);
}
if (resp.status === 400) {
let detail: string;
try {
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) {
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 {};
}
return resp.json();
}
if (resp.status === 401) {
throw new AuthError();
}
if (resp.status === 404) {
throw new NotFoundError(path);
}
if (resp.status === 400) {
let detail: string;
try {
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) {
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 {};
}
return resp.json();
}
async add(
content?: string,
messages?: Record<string, unknown>[],
opts: AddOptions = {},
): Promise<Record<string, unknown>> {
const payload: Record<string, unknown> = {};
async add(
content?: string,
messages?: Record<string, unknown>[],
opts: AddOptions = {},
): Promise<Record<string, unknown>> {
const payload: Record<string, unknown> = {};
if (messages) {
payload.messages = messages;
} else if (content) {
payload.messages = [{ role: "user", content }];
}
if (messages) {
payload.messages = messages;
} else if (content) {
payload.messages = [{ role: "user", content }];
}
if (opts.userId) payload.user_id = opts.userId;
if (opts.agentId) payload.agent_id = opts.agentId;
if (opts.appId) payload.app_id = opts.appId;
if (opts.runId) payload.run_id = opts.runId;
if (opts.metadata) payload.metadata = opts.metadata;
if (opts.immutable) payload.immutable = true;
if (opts.infer === false) payload.infer = false;
if (opts.expires) payload.expiration_date = opts.expires;
if (opts.categories) payload.categories = opts.categories;
if (opts.enableGraph) payload.enable_graph = true;
if (opts.userId) payload.user_id = opts.userId;
if (opts.agentId) payload.agent_id = opts.agentId;
if (opts.appId) payload.app_id = opts.appId;
if (opts.runId) payload.run_id = opts.runId;
if (opts.metadata) payload.metadata = opts.metadata;
if (opts.immutable) payload.immutable = true;
if (opts.infer === false) payload.infer = false;
if (opts.expires) payload.expiration_date = opts.expires;
if (opts.categories) payload.categories = opts.categories;
if (opts.enableGraph) payload.enable_graph = true;
return (await this._request("POST", "/v1/memories/", { json: payload })) as Record<
string,
unknown
>;
}
return (await this._request("POST", "/v1/memories/", {
json: payload,
})) as Record<string, unknown>;
}
private _buildFilters(opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
extraFilters?: Record<string, unknown>;
}): Record<string, unknown> | undefined {
// If caller passed a pre-built filter structure, use it directly
if (opts.extraFilters && ("AND" in opts.extraFilters || "OR" in opts.extraFilters)) {
return opts.extraFilters;
}
private _buildFilters(opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
extraFilters?: Record<string, unknown>;
}): Record<string, unknown> | undefined {
// If caller passed a pre-built filter structure, use it directly
if (
opts.extraFilters &&
("AND" in opts.extraFilters || "OR" in opts.extraFilters)
) {
return opts.extraFilters;
}
const andConditions: Record<string, unknown>[] = [];
if (opts.userId) andConditions.push({ user_id: opts.userId });
if (opts.agentId) andConditions.push({ agent_id: opts.agentId });
if (opts.appId) andConditions.push({ app_id: opts.appId });
if (opts.runId) andConditions.push({ run_id: opts.runId });
const andConditions: Record<string, unknown>[] = [];
if (opts.userId) andConditions.push({ user_id: opts.userId });
if (opts.agentId) andConditions.push({ agent_id: opts.agentId });
if (opts.appId) andConditions.push({ app_id: opts.appId });
if (opts.runId) andConditions.push({ run_id: opts.runId });
if (opts.extraFilters) {
for (const [k, v] of Object.entries(opts.extraFilters)) {
andConditions.push({ [k]: v });
}
}
if (opts.extraFilters) {
for (const [k, v] of Object.entries(opts.extraFilters)) {
andConditions.push({ [k]: v });
}
}
if (andConditions.length === 1) return andConditions[0];
if (andConditions.length > 1) return { AND: andConditions };
return undefined;
}
if (andConditions.length === 1) return andConditions[0];
if (andConditions.length > 1) return { AND: andConditions };
return undefined;
}
async search(query: string, opts: SearchOptions = {}): Promise<Record<string, unknown>[]> {
const payload: Record<string, unknown> = {
query,
top_k: opts.topK ?? 10,
threshold: opts.threshold ?? 0.3,
};
async search(
query: string,
opts: SearchOptions = {},
): Promise<Record<string, unknown>[]> {
const payload: Record<string, unknown> = {
query,
top_k: opts.topK ?? 10,
threshold: opts.threshold ?? 0.3,
};
const apiFilters = this._buildFilters({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
extraFilters: opts.filters,
});
if (apiFilters) payload.filters = apiFilters;
if (opts.rerank) payload.rerank = true;
if (opts.keyword) payload.keyword_search = true;
if (opts.fields) payload.fields = opts.fields;
if (opts.enableGraph) payload.enable_graph = true;
const apiFilters = this._buildFilters({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
extraFilters: opts.filters,
});
if (apiFilters) payload.filters = apiFilters;
if (opts.rerank) payload.rerank = true;
if (opts.keyword) payload.keyword_search = true;
if (opts.fields) payload.fields = opts.fields;
if (opts.enableGraph) payload.enable_graph = true;
const result = (await this._request("POST", "/v2/memories/search/", {
json: payload,
})) as unknown;
if (Array.isArray(result)) return result;
const obj = result as Record<string, unknown>;
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
}
const result = (await this._request("POST", "/v2/memories/search/", {
json: payload,
})) as unknown;
if (Array.isArray(result)) return result;
const obj = result as Record<string, unknown>;
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
}
async get(memoryId: string): Promise<Record<string, unknown>> {
return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record<string, unknown>;
}
async get(memoryId: string): Promise<Record<string, unknown>> {
return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record<
string,
unknown
>;
}
async listMemories(opts: ListOptions = {}): Promise<Record<string, unknown>[]> {
const payload: Record<string, unknown> = {};
const params: Record<string, string> = {
page: String(opts.page ?? 1),
page_size: String(opts.pageSize ?? 100),
};
async listMemories(
opts: ListOptions = {},
): Promise<Record<string, unknown>[]> {
const payload: Record<string, unknown> = {};
const params: Record<string, string> = {
page: String(opts.page ?? 1),
page_size: String(opts.pageSize ?? 100),
};
const extra: Record<string, unknown> = {};
if (opts.category) {
extra.categories = { contains: opts.category };
}
if (opts.after) {
extra.created_at = { ...(extra.created_at as Record<string, unknown> | undefined), gte: opts.after };
}
if (opts.before) {
extra.created_at = { ...(extra.created_at as Record<string, unknown> | undefined), lte: opts.before };
}
const extra: Record<string, unknown> = {};
if (opts.category) {
extra.categories = { contains: opts.category };
}
if (opts.after) {
extra.created_at = {
...(extra.created_at as Record<string, unknown> | undefined),
gte: opts.after,
};
}
if (opts.before) {
extra.created_at = {
...(extra.created_at as Record<string, unknown> | undefined),
lte: opts.before,
};
}
const apiFilters = this._buildFilters({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
});
if (apiFilters) payload.filters = apiFilters;
if (opts.enableGraph) payload.enable_graph = true;
const apiFilters = this._buildFilters({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
});
if (apiFilters) payload.filters = apiFilters;
if (opts.enableGraph) payload.enable_graph = true;
const result = (await this._request("POST", "/v2/memories/", { json: payload, params })) as unknown;
if (Array.isArray(result)) return result;
const obj = result as Record<string, unknown>;
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
}
const result = (await this._request("POST", "/v2/memories/", {
json: payload,
params,
})) as unknown;
if (Array.isArray(result)) return result;
const obj = result as Record<string, unknown>;
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
}
async update(
memoryId: string,
content?: string,
metadata?: Record<string, unknown>,
): Promise<Record<string, unknown>> {
const payload: Record<string, unknown> = {};
if (content) payload.text = content;
if (metadata) payload.metadata = metadata;
return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
json: payload,
})) as Record<string, unknown>;
}
async update(
memoryId: string,
content?: string,
metadata?: Record<string, unknown>,
): Promise<Record<string, unknown>> {
const payload: Record<string, unknown> = {};
if (content) payload.text = content;
if (metadata) payload.metadata = metadata;
return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
json: payload,
})) as Record<string, unknown>;
}
async delete(
memoryId?: string,
opts: DeleteOptions = {},
): Promise<Record<string, unknown>> {
if (opts.all) {
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;
return (await this._request("DELETE", "/v1/memories/", { params })) as Record<
string,
unknown
>;
}
if (memoryId) {
return (await this._request("DELETE", `/v1/memories/${memoryId}/`)) as Record<
string,
unknown
>;
}
throw new Error("Either memoryId or --all is required");
}
async delete(
memoryId?: string,
opts: DeleteOptions = {},
): Promise<Record<string, unknown>> {
if (opts.all) {
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;
return (await this._request("DELETE", "/v1/memories/", {
params,
})) as Record<string, unknown>;
}
if (memoryId) {
return (await this._request(
"DELETE",
`/v1/memories/${memoryId}/`,
)) as Record<string, unknown>;
}
throw new Error("Either memoryId or --all is required");
}
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) {
throw new Error("At least one entity ID is required for deleteEntities.");
}
return (await this._request("DELETE", "/v1/entities/", { params })) as Record<
string,
unknown
>;
}
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) {
throw new Error("At least one entity ID is required for deleteEntities.");
}
return (await this._request("DELETE", "/v1/entities/", {
params,
})) as Record<string, unknown>;
}
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/");
}
return { connected: true, backend: "platform", base_url: this.baseUrl };
} catch (e) {
return {
connected: false,
backend: "platform",
error: e instanceof Error ? e.message : String(e),
};
}
}
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/");
}
return { connected: true, backend: "platform", base_url: this.baseUrl };
} catch (e) {
return {
connected: false,
backend: "platform",
error: e instanceof Error ? e.message : String(e),
};
}
}
async entities(entityType: string): Promise<Record<string, unknown>[]> {
const result = (await this._request("GET", "/v1/entities/")) as unknown;
let items: Record<string, unknown>[];
if (Array.isArray(result)) {
items = result;
} else {
items = ((result as Record<string, unknown>).results ?? []) as Record<string, unknown>[];
}
async entities(entityType: string): Promise<Record<string, unknown>[]> {
const result = (await this._request("GET", "/v1/entities/")) as unknown;
let items: Record<string, unknown>[];
if (Array.isArray(result)) {
items = result;
} else {
items = ((result as Record<string, unknown>).results ?? []) as Record<
string,
unknown
>[];
}
const typeMap: Record<string, string> = {
users: "user",
agents: "agent",
apps: "app",
runs: "run",
};
const targetType = typeMap[entityType];
if (targetType) {
items = items.filter(
(e) => (e.type as string | undefined)?.toLowerCase() === targetType,
);
}
return items;
}
const typeMap: Record<string, string> = {
users: "user",
agents: "agent",
apps: "app",
runs: "run",
};
const targetType = typeMap[entityType];
if (targetType) {
items = items.filter(
(e) => (e.type as string | undefined)?.toLowerCase() === targetType,
);
}
return items;
}
}
+73 -65
View File
@@ -4,6 +4,7 @@
import chalk from "chalk";
import ora, { type Ora } from "ora";
import { CLI_VERSION } from "./version.js";
export const LOGO = `
███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
@@ -36,75 +37,78 @@ const dim = chalk.hex(DIM_COLOR);
* plain-text for piped/non-TTY or NO_COLOR environments.
*/
export function sym(fancy: string, plain: string): string {
if (!process.stdout.isTTY || process.env.NO_COLOR) return plain;
return fancy;
if (!process.stdout.isTTY || process.env.NO_COLOR) return plain;
return fancy;
}
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${__CLI_VERSION__}`;
const contentLines = ["", ...logoLines, "", tagline, ""];
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${CLI_VERSION}`;
const contentLines = ["", ...logoLines, "", tagline, ""];
// Compute inner width from longest content line + padding both sides
const maxContent = Math.max(...contentLines.map((l) => l.length));
const innerWidth = maxContent + pad * 2;
const totalWidth = innerWidth + 2; // + 2 for │ borders
// Compute inner width from longest content line + padding both sides
const maxContent = Math.max(...contentLines.map((l) => l.length));
const innerWidth = maxContent + pad * 2;
const totalWidth = innerWidth + 2; // + 2 for │ borders
const topBorder = brand(`╭${"─".repeat(totalWidth - 2)}╮`);
const subtitleFill = totalWidth - 2 - subtitle.length - 3; // 3 = "─ " before subtitle + "─" after
const bottomBorder = brand(`╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`);
const topBorder = brand(`╭${"─".repeat(totalWidth - 2)}╮`);
const subtitleFill = totalWidth - 2 - subtitle.length - 3; // 3 = "─ " before subtitle + "─" after
const bottomBorder = brand(
`╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`,
);
const body = contentLines.map((line) => {
const rightPad = innerWidth - pad - line.length;
return `${brand("│")}${" ".repeat(pad)}${brand.bold(line)}${" ".repeat(Math.max(rightPad, 0))}${brand("│")}`;
});
// Re-color tagline line with accent instead of brand.bold
const taglineIdx = body.length - 2; // second-to-last (before trailing empty line)
const taglineRightPad = innerWidth - pad - tagline.length;
body[taglineIdx] = `${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`;
const body = contentLines.map((line) => {
const rightPad = innerWidth - pad - line.length;
return `${brand("│")}${" ".repeat(pad)}${brand.bold(line)}${" ".repeat(Math.max(rightPad, 0))}${brand("│")}`;
});
// Re-color tagline line with accent instead of brand.bold
const taglineIdx = body.length - 2; // second-to-last (before trailing empty line)
const taglineRightPad = innerWidth - pad - tagline.length;
body[taglineIdx] =
`${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`;
console.log(topBorder);
for (const line of body) console.log(line);
console.log(bottomBorder);
console.log(topBorder);
for (const line of body) console.log(line);
console.log(bottomBorder);
}
export function printSuccess(message: string): void {
console.log(`${success(sym("✓", "[ok]"))} ${message}`);
console.log(`${success(sym("✓", "[ok]"))} ${message}`);
}
export function printError(message: string, hint?: string): void {
console.error(`${error(sym("✗", "[error]") + " Error:")} ${message}`);
if (hint) {
console.error(` ${dim(hint)}`);
}
console.error(`${error(`${sym("✗", "[error]")} Error:`)} ${message}`);
if (hint) {
console.error(` ${dim(hint)}`);
}
}
export function printWarning(message: string): void {
console.error(`${warning(sym("⚠", "[warn]"))} ${message}`);
console.error(`${warning(sym("⚠", "[warn]"))} ${message}`);
}
export function printInfo(message: string): void {
console.error(`${brand(sym("◆", "*"))} ${message}`);
console.error(`${brand(sym("◆", "*"))} ${message}`);
}
export function printScope(ids: Record<string, string | undefined>): void {
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}`);
}
}
if (parts.length > 0) {
console.error(` ${dim(`Scope: ${parts.join(", ")}`)}`);
}
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}`);
}
}
if (parts.length > 0) {
console.error(` ${dim(`Scope: ${parts.join(", ")}`)}`);
}
}
export interface TimedStatusContext {
successMsg: string;
errorMsg: string;
successMsg: string;
errorMsg: string;
}
/**
@@ -112,29 +116,33 @@ export interface TimedStatusContext {
* Equivalent to Python's timed_status context manager.
*/
export async function timedStatus<T>(
message: string,
fn: (ctx: TimedStatusContext) => Promise<T>,
message: string,
fn: (ctx: TimedStatusContext) => Promise<T>,
): Promise<T> {
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
const spinner = ora({ text: dim(message), color: "magenta", stream: process.stderr }).start();
const start = performance.now();
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
const spinner = ora({
text: dim(message),
color: "magenta",
stream: process.stderr,
}).start();
const start = performance.now();
try {
const result = await fn(ctx);
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
spinner.stop();
if (ctx.successMsg) {
console.error(`${success("✓")} ${ctx.successMsg} (${elapsed}s)`);
}
return result;
} catch (err) {
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
spinner.stop();
if (ctx.errorMsg) {
console.error(`${error("✗ Error:")} ${ctx.errorMsg} (${elapsed}s)`);
}
throw err;
}
try {
const result = await fn(ctx);
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
spinner.stop();
if (ctx.successMsg) {
console.error(`${success("✓")} ${ctx.successMsg} (${elapsed}s)`);
}
return result;
} catch (err) {
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
spinner.stop();
if (ctx.errorMsg) {
console.error(`${error("✗ Error:")} ${ctx.errorMsg} (${elapsed}s)`);
}
throw err;
}
}
/** Format helpers using brand colors for external use. */
+67 -64
View File
@@ -3,88 +3,91 @@
*/
import Table from "cli-table3";
import { printError, printSuccess, colors } from "../branding.js";
import { colors, printError, printSuccess } from "../branding.js";
import {
getNestedValue,
loadConfig,
redactKey,
saveConfig,
setNestedValue,
getNestedValue,
loadConfig,
redactKey,
saveConfig,
setNestedValue,
} from "../config.js";
import { formatJsonEnvelope } from "../output.js";
const { brand, accent, dim } = colors;
export function cmdConfigShow(opts: { output?: string } = {}): void {
const config = loadConfig();
const config = loadConfig();
if (opts.output === "json") {
formatJsonEnvelope({
command: "config show",
data: {
defaults: {
user_id: config.defaults.userId || null,
agent_id: config.defaults.agentId || null,
app_id: config.defaults.appId || null,
run_id: config.defaults.runId || null,
enable_graph: config.defaults.enableGraph,
},
platform: {
api_key: redactKey(config.platform.apiKey),
base_url: config.platform.baseUrl,
},
},
});
return;
}
if (opts.output === "json") {
formatJsonEnvelope({
command: "config show",
data: {
defaults: {
user_id: config.defaults.userId || null,
agent_id: config.defaults.agentId || null,
app_id: config.defaults.appId || null,
run_id: config.defaults.runId || null,
enable_graph: config.defaults.enableGraph,
},
platform: {
api_key: redactKey(config.platform.apiKey),
base_url: config.platform.baseUrl,
},
},
});
return;
}
console.log();
console.log(` ${brand("◆ mem0 Configuration")}\n`);
console.log();
console.log(` ${brand("◆ mem0 Configuration")}\n`);
const table = new Table({
head: [accent("Key"), accent("Value")],
style: { head: [], border: [] },
});
const table = new Table({
head: [accent("Key"), accent("Value")],
style: { head: [], border: [] },
});
// Defaults
table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]);
table.push(["defaults.agent_id", config.defaults.agentId || dim("(not set)")]);
table.push(["defaults.app_id", config.defaults.appId || dim("(not set)")]);
table.push(["defaults.run_id", config.defaults.runId || dim("(not set)")]);
table.push(["defaults.enable_graph", String(config.defaults.enableGraph)]);
table.push(["", ""]);
// Defaults
table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]);
table.push([
"defaults.agent_id",
config.defaults.agentId || dim("(not set)"),
]);
table.push(["defaults.app_id", config.defaults.appId || dim("(not set)")]);
table.push(["defaults.run_id", config.defaults.runId || dim("(not set)")]);
table.push(["defaults.enable_graph", String(config.defaults.enableGraph)]);
table.push(["", ""]);
// Platform
table.push(["platform.api_key", redactKey(config.platform.apiKey)]);
table.push(["platform.base_url", config.platform.baseUrl]);
// Platform
table.push(["platform.api_key", redactKey(config.platform.apiKey)]);
table.push(["platform.base_url", config.platform.baseUrl]);
console.log(table.toString());
console.log();
console.log(table.toString());
console.log();
}
export function cmdConfigGet(key: string): void {
const config = loadConfig();
const value = getNestedValue(config, key);
const config = loadConfig();
const value = getNestedValue(config, key);
if (value === undefined) {
printError(`Unknown config key: ${key}`);
} else {
// Redact secrets
if (key.includes("api_key") || key.split(".").pop() === "key") {
console.log(redactKey(String(value)));
} else {
console.log(String(value));
}
}
if (value === undefined) {
printError(`Unknown config key: ${key}`);
} else {
// Redact secrets
if (key.includes("api_key") || key.split(".").pop() === "key") {
console.log(redactKey(String(value)));
} else {
console.log(String(value));
}
}
}
export function cmdConfigSet(key: string, value: string): void {
const config = loadConfig();
if (setNestedValue(config, key, value)) {
saveConfig(config);
const display = key.includes("key") ? redactKey(value) : value;
printSuccess(`${key} = ${display}`);
} else {
printError(`Unknown config key: ${key}`);
}
const config = loadConfig();
if (setNestedValue(config, key, value)) {
saveConfig(config);
const display = key.includes("key") ? redactKey(value) : value;
printSuccess(`${key} = ${display}`);
} else {
printError(`Unknown config key: ${key}`);
}
}
+125 -108
View File
@@ -4,8 +4,14 @@
import readline from "node:readline";
import Table from "cli-table3";
import { printError, printInfo, printSuccess, timedStatus, colors } from "../branding.js";
import type { Backend } from "../backend/base.js";
import {
colors,
printError,
printInfo,
printSuccess,
timedStatus,
} from "../branding.js";
import { formatJson } from "../output.js";
const { brand, accent, dim } = colors;
@@ -13,127 +19,138 @@ const { brand, accent, dim } = colors;
const VALID_TYPES = new Set(["users", "agents", "apps", "runs"]);
export async function cmdEntitiesList(
backend: Backend,
entityType: string,
opts: { output: string },
backend: Backend,
entityType: string,
opts: { output: string },
): Promise<void> {
if (!VALID_TYPES.has(entityType)) {
printError(`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`);
process.exit(1);
}
if (!VALID_TYPES.has(entityType)) {
printError(
`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`,
);
process.exit(1);
}
const start = performance.now();
let results: Record<string, unknown>[];
try {
results = await timedStatus(`Fetching ${entityType}...`, async () => {
return backend.entities(entityType);
});
} catch (e) {
printError(
e instanceof Error ? e.message : String(e),
"This feature may require the mem0 Platform.",
);
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
const start = performance.now();
let results: Record<string, unknown>[];
try {
results = await timedStatus(`Fetching ${entityType}...`, async () => {
return backend.entities(entityType);
});
} catch (e) {
printError(
e instanceof Error ? e.message : String(e),
"This feature may require the mem0 Platform.",
);
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJson(results);
return;
}
if (opts.output === "json") {
formatJson(results);
return;
}
if (!results.length) {
printInfo(`No ${entityType} found.`);
return;
}
if (!results.length) {
printInfo(`No ${entityType} found.`);
return;
}
const table = new Table({
head: [accent("Name / ID"), accent("Created")],
style: { head: [], border: [] },
});
const table = new Table({
head: [accent("Name / ID"), accent("Created")],
style: { head: [], border: [] },
});
for (const entity of results) {
const name = String(entity.name ?? entity.id ?? "—");
const created = String(entity.created_at ?? "—").slice(0, 10);
table.push([name, created]);
}
for (const entity of results) {
const name = String(entity.name ?? entity.id ?? "—");
const created = String(entity.created_at ?? "—").slice(0, 10);
table.push([name, created]);
}
console.log();
console.log(table.toString());
console.log(` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`);
console.log();
console.log();
console.log(table.toString());
console.log(
` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`,
);
console.log();
}
export async function cmdEntitiesDelete(
backend: Backend,
opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
dryRun?: boolean;
force: boolean;
output: string;
},
backend: Backend,
opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
dryRun?: boolean;
force: boolean;
output: string;
},
): Promise<void> {
if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) {
printError("Provide at least one of --user-id, --agent-id, --app-id, --run-id.");
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.",
);
process.exit(1);
}
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("No changes made.");
return;
}
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("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(", ");
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 });
const answer = await new Promise<string>((resolve) => {
rl.question(
`\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `,
resolve,
);
});
rl.close();
if (answer.toLowerCase() !== "y") {
printInfo("Cancelled.");
process.exit(0);
}
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const answer = await new Promise<string>((resolve) => {
rl.question(
`\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `,
resolve,
);
});
rl.close();
if (answer.toLowerCase() !== "y") {
printInfo("Cancelled.");
process.exit(0);
}
}
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Deleting entity...", async () => {
return backend.deleteEntities({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
});
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Deleting entity...", async () => {
return backend.deleteEntities({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
});
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJson(result);
} else if (opts.output !== "quiet") {
printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`);
}
if (opts.output === "json") {
formatJson(result);
} else if (opts.output !== "quiet") {
printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`);
}
}
+311 -285
View File
@@ -3,355 +3,381 @@
*/
import readline from "node:readline";
import {
printBanner,
printError,
printInfo,
printSuccess,
colors,
} from "../branding.js";
import { type Mem0Config, createDefaultConfig, loadConfig, saveConfig, DEFAULT_BASE_URL } from "../config.js";
import { PlatformBackend } from "../backend/platform.js";
import {
colors,
printBanner,
printError,
printInfo,
printSuccess,
} from "../branding.js";
import {
DEFAULT_BASE_URL,
type Mem0Config,
createDefaultConfig,
loadConfig,
saveConfig,
} from "../config.js";
const { brand, dim } = colors;
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
function validateEmail(email: string): void {
if (!EMAIL_RE.test(email)) {
printError(`Invalid email address: ${JSON.stringify(email)}`);
process.exit(1);
}
if (!EMAIL_RE.test(email)) {
printError(`Invalid email address: ${JSON.stringify(email)}`);
process.exit(1);
}
}
async function emailLogin(
email: string,
code: string | undefined,
baseUrl: string,
email: string,
code: string | undefined,
baseUrl: string,
): Promise<Record<string, unknown>> {
const url = baseUrl.replace(/\/+$/, "");
const url = baseUrl.replace(/\/+$/, "");
let codeValue = code;
if (!code) {
const resp = await fetch(`${url}/api/v1/auth/email_code/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(30_000),
});
if (resp.status === 429) {
printError("Too many attempts. Try again in a few minutes.");
process.exit(1);
}
if (!resp.ok) {
let detail: string;
try {
const body = (await resp.json()) as Record<string, unknown>;
detail = (body["error"] ?? body["detail"] ?? resp.statusText) as string;
} catch {
detail = resp.statusText;
}
printError(`Failed to send code: ${detail}`);
process.exit(1);
}
if (!codeValue) {
const resp = await fetch(`${url}/api/v1/auth/email_code/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(30_000),
});
if (resp.status === 429) {
printError("Too many attempts. Try again in a few minutes.");
process.exit(1);
}
if (!resp.ok) {
let detail: string;
try {
const body = (await resp.json()) as Record<string, unknown>;
detail = (body.error ?? body.detail ?? resp.statusText) as string;
} catch {
detail = resp.statusText;
}
printError(`Failed to send code: ${detail}`);
process.exit(1);
}
printSuccess("Verification code sent! Check your email.");
printSuccess("Verification code sent! Check your email.");
if (!process.stdin.isTTY) {
printError(
"No --code provided and terminal is non-interactive.",
"Run: mem0 init --email <email> --code <code>",
);
process.exit(1);
}
if (!process.stdin.isTTY) {
printError(
"No --code provided and terminal is non-interactive.",
"Run: mem0 init --email <email> --code <code>",
);
process.exit(1);
}
console.log();
const entered = await promptLine(` ${brand("Verification Code")}`);
if (!entered) {
printError("Code is required.");
process.exit(1);
}
code = entered;
}
console.log();
const entered = await promptLine(` ${brand("Verification Code")}`);
if (!entered) {
printError("Code is required.");
process.exit(1);
}
codeValue = entered;
}
const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, code: code.trim() }),
signal: AbortSignal.timeout(30_000),
});
if (verifyResp.status === 429) {
printError("Too many attempts. Try again in a few minutes.");
process.exit(1);
}
if (!verifyResp.ok) {
let detail: string;
try {
const body = (await verifyResp.json()) as Record<string, unknown>;
detail = (body["error"] ?? body["detail"] ?? verifyResp.statusText) as string;
} catch {
detail = verifyResp.statusText;
}
printError(`Verification failed: ${detail}`);
process.exit(1);
}
const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, code: codeValue.trim() }),
signal: AbortSignal.timeout(30_000),
});
if (verifyResp.status === 429) {
printError("Too many attempts. Try again in a few minutes.");
process.exit(1);
}
if (!verifyResp.ok) {
let detail: string;
try {
const body = (await verifyResp.json()) as Record<string, unknown>;
detail = (body.error ?? body.detail ?? verifyResp.statusText) as string;
} catch {
detail = verifyResp.statusText;
}
printError(`Verification failed: ${detail}`);
process.exit(1);
}
return verifyResp.json() as Promise<Record<string, unknown>>;
return verifyResp.json() as Promise<Record<string, unknown>>;
}
function promptSecret(label: string): Promise<string> {
return new Promise((resolve, reject) => {
process.stdout.write(label);
return new Promise((resolve, reject) => {
process.stdout.write(label);
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
process.stdin.resume();
process.stdin.setEncoding("utf-8");
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
process.stdin.resume();
process.stdin.setEncoding("utf-8");
const chars: string[] = [];
const chars: string[] = [];
const onData = (key: string) => {
for (const ch of key) {
if (ch === "\r" || ch === "\n") {
cleanup();
process.stdout.write("\n");
resolve(chars.join(""));
return;
}
if (ch === "\x03") {
cleanup();
reject(new Error("Interrupted"));
return;
}
if (ch === "\x7f" || ch === "\x08") {
// backspace
if (chars.length > 0) {
chars.pop();
process.stdout.write("\b \b");
}
} else if (ch === "\x15") {
// Ctrl+U — clear line
process.stdout.write("\b \b".repeat(chars.length));
chars.length = 0;
} else if (ch >= " ") {
chars.push(ch);
process.stdout.write("*");
}
}
};
const onData = (key: string) => {
for (const ch of key) {
if (ch === "\r" || ch === "\n") {
cleanup();
process.stdout.write("\n");
resolve(chars.join(""));
return;
}
if (ch === "\x03") {
cleanup();
reject(new Error("Interrupted"));
return;
}
if (ch === "\x7f" || ch === "\x08") {
// backspace
if (chars.length > 0) {
chars.pop();
process.stdout.write("\b \b");
}
} else if (ch === "\x15") {
// Ctrl+U — clear line
process.stdout.write("\b \b".repeat(chars.length));
chars.length = 0;
} else if (ch >= " ") {
chars.push(ch);
process.stdout.write("*");
}
}
};
const cleanup = () => {
process.stdin.removeListener("data", onData);
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
process.stdin.pause();
};
const cleanup = () => {
process.stdin.removeListener("data", onData);
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
process.stdin.pause();
};
process.stdin.on("data", onData);
});
process.stdin.on("data", onData);
});
}
function promptLine(label: string, defaultValue?: string): Promise<string> {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `;
return new Promise((resolve) => {
rl.question(prompt, (answer) => {
rl.close();
resolve(answer.trim() || defaultValue || "");
});
});
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `;
return new Promise((resolve) => {
rl.question(prompt, (answer) => {
rl.close();
resolve(answer.trim() || defaultValue || "");
});
});
}
async function setupPlatform(config: Mem0Config): Promise<void> {
console.log();
console.log(` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys")}`);
console.log();
console.log();
console.log(
` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys")}`,
);
console.log();
process.stdout.write(` ${brand("API Key")}: `);
const apiKey = await promptSecret("");
if (!apiKey) {
printError("API key is required.");
process.exit(1);
}
config.platform.apiKey = apiKey;
process.stdout.write(` ${brand("API Key")}: `);
const apiKey = await promptSecret("");
if (!apiKey) {
printError("API key is required.");
process.exit(1);
}
config.platform.apiKey = apiKey;
}
async function setupDefaults(config: Mem0Config): Promise<void> {
console.log();
printInfo("Set default entity IDs (press Enter to skip).\n");
console.log();
printInfo("Set default entity IDs (press Enter to skip).\n");
const userId = await promptLine(` ${brand("Default User ID")} ${dim("(recommended)")}`, "mem0-cli");
if (userId) config.defaults.userId = userId;
const userId = await promptLine(
` ${brand("Default User ID")} ${dim("(recommended)")}`,
"mem0-cli",
);
if (userId) config.defaults.userId = userId;
}
async function validatePlatform(config: Mem0Config): Promise<void> {
console.log();
printInfo("Validating connection...");
try {
const backend = new PlatformBackend(config.platform);
const status = await backend.status({
userId: config.defaults.userId || undefined,
agentId: config.defaults.agentId || undefined,
});
if (status.connected) {
printSuccess("Connected to mem0 Platform!");
} else {
printError(
`Could not connect: ${status.error ?? "Unknown error"}`,
"Check your API key and try again.",
);
}
} catch (e) {
printError(`Connection test failed: ${e instanceof Error ? e.message : e}`);
}
console.log();
printInfo("Validating connection...");
try {
const backend = new PlatformBackend(config.platform);
const status = await backend.status({
userId: config.defaults.userId || undefined,
agentId: config.defaults.agentId || undefined,
});
if (status.connected) {
printSuccess("Connected to mem0 Platform!");
} else {
printError(
`Could not connect: ${status.error ?? "Unknown error"}`,
"Check your API key and try again.",
);
}
} catch (e) {
printError(`Connection test failed: ${e instanceof Error ? e.message : e}`);
}
}
export async function runInit(
opts: { apiKey?: string; userId?: string; email?: string; code?: string } = {},
opts: {
apiKey?: string;
userId?: string;
email?: string;
code?: string;
} = {},
): Promise<void> {
const config = createDefaultConfig();
const savedConfig = loadConfig();
const baseUrl =
process.env["MEM0_BASE_URL"] || savedConfig.platform.baseUrl || DEFAULT_BASE_URL;
const config = createDefaultConfig();
const savedConfig = loadConfig();
const baseUrl =
process.env.MEM0_BASE_URL ||
savedConfig.platform.baseUrl ||
DEFAULT_BASE_URL;
// Guards
if (opts.code && !opts.email) {
printError("--code requires --email.");
process.exit(1);
}
if (opts.email && opts.apiKey) {
printError("Cannot use both --api-key and --email.");
process.exit(1);
}
// Guards
if (opts.code && !opts.email) {
printError("--code requires --email.");
process.exit(1);
}
if (opts.email && opts.apiKey) {
printError("Cannot use both --api-key and --email.");
process.exit(1);
}
// ── Email login flow ──────────────────────────────────────────────────────
if (opts.email) {
const email = opts.email.trim().toLowerCase();
validateEmail(email);
// ── Email login flow ──────────────────────────────────────────────────────
if (opts.email) {
const email = opts.email.trim().toLowerCase();
validateEmail(email);
printBanner();
console.log();
printInfo(`Logging in as ${email}...\n`);
printBanner();
console.log();
printInfo(`Logging in as ${email}...\n`);
const result = await emailLogin(email, opts.code, baseUrl);
const result = await emailLogin(email, opts.code, baseUrl);
const apiKeyVal = result["api_key"] as string | undefined;
if (!apiKeyVal) {
printError("Auth succeeded but no API key was returned. Contact support.");
process.exit(1);
}
const apiKeyVal = result.api_key as string | undefined;
if (!apiKeyVal) {
printError(
"Auth succeeded but no API key was returned. Contact support.",
);
process.exit(1);
}
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli";
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli";
saveConfig(config);
console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
console.log();
return;
}
saveConfig(config);
console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
console.log();
return;
}
// ── API key flow ──────────────────────────────────────────────────────────
// ── API key flow ──────────────────────────────────────────────────────────
// Non-interactive: both flags provided
if (opts.apiKey && opts.userId) {
config.platform.apiKey = opts.apiKey;
config.defaults.userId = opts.userId;
await validatePlatform(config);
saveConfig(config);
printSuccess("Configuration saved to ~/.mem0/config.json");
return;
}
// Non-interactive: both flags provided
if (opts.apiKey && opts.userId) {
config.platform.apiKey = opts.apiKey;
config.defaults.userId = opts.userId;
await validatePlatform(config);
saveConfig(config);
printSuccess("Configuration saved to ~/.mem0/config.json");
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);
}
// 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");
printBanner();
console.log();
printInfo("Welcome! Let's set up your mem0 CLI.\n");
// Use provided API key or prompt
if (opts.apiKey) {
config.platform.apiKey = opts.apiKey;
} else {
console.log(` ${brand("How would you like to authenticate?")}`);
console.log(` ${dim("1.")} Login with email ${dim("(recommended)")}`);
console.log(` ${dim("2.")} Enter API key manually`);
console.log();
// Use provided API key or prompt
if (opts.apiKey) {
config.platform.apiKey = opts.apiKey;
} else {
console.log(` ${brand("How would you like to authenticate?")}`);
console.log(` ${dim("1.")} Login with email ${dim("(recommended)")}`);
console.log(` ${dim("2.")} Enter API key manually`);
console.log();
const choice = await promptLine(` ${brand("Choose")} [1/2]`, "1");
const choice = await promptLine(` ${brand("Choose")} [1/2]`, "1");
if (choice === "1") {
console.log();
const emailAddr = await promptLine(` ${brand("Email")}`);
if (!emailAddr) {
printError("Email is required.");
process.exit(1);
}
if (choice === "1") {
console.log();
const emailAddr = await promptLine(` ${brand("Email")}`);
if (!emailAddr) {
printError("Email is required.");
process.exit(1);
}
const email = emailAddr.trim().toLowerCase();
validateEmail(email);
printInfo(`Logging in as ${email}...\n`);
const email = emailAddr.trim().toLowerCase();
validateEmail(email);
printInfo(`Logging in as ${email}...\n`);
const result = await emailLogin(email, undefined, baseUrl);
const result = await emailLogin(email, undefined, baseUrl);
const apiKeyVal = result["api_key"] as string | undefined;
if (!apiKeyVal) {
printError("Auth succeeded but no API key was returned. Contact support.");
process.exit(1);
}
const apiKeyVal = result.api_key as string | undefined;
if (!apiKeyVal) {
printError(
"Auth succeeded but no API key was returned. Contact support.",
);
process.exit(1);
}
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli";
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli";
saveConfig(config);
console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
console.log();
return;
}
saveConfig(config);
console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
console.log();
return;
}
// choice === "2": fall through to API key prompt
await setupPlatform(config);
}
// choice === "2": fall through to API key prompt
await setupPlatform(config);
}
// Use provided user ID or prompt
if (opts.userId) {
config.defaults.userId = opts.userId;
} else {
await setupDefaults(config);
}
// Use provided user ID or prompt
if (opts.userId) {
config.defaults.userId = opts.userId;
} else {
await setupDefaults(config);
}
await validatePlatform(config);
await validatePlatform(config);
saveConfig(config);
console.log();
printSuccess("Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
if (config.defaults.userId) {
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
} else {
console.log(` ${dim(' mem0 add "I prefer dark mode" --user-id alice')}`);
console.log(` ${dim(' mem0 search "preferences" --user-id alice')}`);
}
console.log();
saveConfig(config);
console.log();
printSuccess("Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
if (config.defaults.userId) {
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
} else {
console.log(` ${dim(' mem0 add "I prefer dark mode" --user-id alice')}`);
console.log(` ${dim(' mem0 search "preferences" --user-id alice')}`);
}
console.log();
}
File diff suppressed because it is too large Load Diff
+115 -105
View File
@@ -3,133 +3,143 @@
*/
import fs from "node:fs";
import { printError, printSuccess, timedStatus, colors } from "../branding.js";
import type { Backend } from "../backend/base.js";
import { formatJsonEnvelope } from "../output.js";
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 { CLI_VERSION } from "../version.js";
const { brand, dim, success, error: errorColor } = colors;
export async function cmdStatus(
backend: Backend,
opts: { userId?: string; agentId?: string; output?: string } = {},
backend: Backend,
opts: { userId?: string; agentId?: string; output?: string } = {},
): Promise<void> {
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Checking connection...", async () => {
return backend.status({ userId: opts.userId, agentId: opts.agentId });
});
} catch (e) {
result = { connected: false, error: e instanceof Error ? e.message : String(e) };
}
const elapsed = (performance.now() - start) / 1000;
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Checking connection...", async () => {
return backend.status({ userId: opts.userId, agentId: opts.agentId });
});
} catch (e) {
result = {
connected: false,
error: e instanceof Error ? e.message : String(e),
};
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJsonEnvelope({
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),
});
return;
}
if (opts.output === "json") {
formatJsonEnvelope({
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),
});
return;
}
const lines: string[] = [];
if (result.connected) {
lines.push(` ${success("\u25cf")} Connected`);
} else {
lines.push(` ${errorColor("\u25cf")} Disconnected`);
}
const lines: string[] = [];
if (result.connected) {
lines.push(` ${success("\u25cf")} Connected`);
} else {
lines.push(` ${errorColor("\u25cf")} Disconnected`);
}
lines.push(` ${dim("Backend:")} ${result.backend ?? "?"}`);
if (result.base_url) {
lines.push(` ${dim("API URL:")} ${result.base_url}`);
}
if (result.error) {
lines.push(` ${errorColor("Error:")} ${result.error}`);
}
lines.push(` ${dim("Latency:")} ${elapsed.toFixed(2)}s`);
lines.push(` ${dim("Backend:")} ${result.backend ?? "?"}`);
if (result.base_url) {
lines.push(` ${dim("API URL:")} ${result.base_url}`);
}
if (result.error) {
lines.push(` ${errorColor("Error:")} ${result.error}`);
}
lines.push(` ${dim("Latency:")} ${elapsed.toFixed(2)}s`);
const content = lines.join("\n");
console.log();
console.log(
boxen(content, {
title: brand("Connection Status"),
titleAlignment: "left",
borderColor: "magenta",
padding: 1,
}),
);
console.log();
const content = lines.join("\n");
console.log();
console.log(
boxen(content, {
title: brand("Connection Status"),
titleAlignment: "left",
borderColor: "magenta",
padding: 1,
}),
);
console.log();
}
export function cmdVersion(): void {
console.log(` ${brand("◆ Mem0")} CLI v${__CLI_VERSION__}`);
console.log(` ${brand("◆ Mem0")} CLI v${CLI_VERSION}`);
}
export async function cmdImport(
backend: Backend,
filePath: string,
opts: { userId?: string; agentId?: string; output?: string },
backend: Backend,
filePath: string,
opts: { userId?: string; agentId?: string; output?: string },
): Promise<void> {
let data: Record<string, unknown>[];
try {
const raw = fs.readFileSync(filePath, "utf-8");
const parsed = JSON.parse(raw);
data = Array.isArray(parsed) ? parsed : [parsed];
} catch (e) {
printError(`Failed to read file: ${e instanceof Error ? e.message : e}`);
process.exit(1);
}
let data: Record<string, unknown>[];
try {
const raw = fs.readFileSync(filePath, "utf-8");
const parsed = JSON.parse(raw);
data = Array.isArray(parsed) ? parsed : [parsed];
} catch (e) {
printError(`Failed to read file: ${e instanceof Error ? e.message : e}`);
process.exit(1);
}
let added = 0;
let failed = 0;
const start = performance.now();
let added = 0;
let failed = 0;
const start = performance.now();
for (let i = 0; i < data.length; i++) {
const item = data[i];
const content = (item.memory ?? item.text ?? item.content ?? "") as string;
if (!content) {
failed++;
continue;
}
for (let i = 0; i < data.length; i++) {
const item = data[i];
const content = (item.memory ?? item.text ?? item.content ?? "") as string;
if (!content) {
failed++;
continue;
}
try {
await backend.add(content, undefined, {
userId: opts.userId ?? (item.user_id as string | undefined),
agentId: opts.agentId ?? (item.agent_id as string | undefined),
metadata: item.metadata as Record<string, unknown> | undefined,
});
added++;
} catch {
failed++;
}
try {
await backend.add(content, undefined, {
userId: opts.userId ?? (item.user_id as string | undefined),
agentId: opts.agentId ?? (item.agent_id as string | undefined),
metadata: item.metadata as Record<string, unknown> | undefined,
});
added++;
} catch {
failed++;
}
// Simple progress indicator
if ((i + 1) % 10 === 0 || i === data.length - 1) {
process.stdout.write(`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`);
}
}
// Simple progress indicator
if ((i + 1) % 10 === 0 || i === data.length - 1) {
process.stdout.write(
`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`,
);
}
}
const elapsed = (performance.now() - start) / 1000;
console.log(); // Clear progress line
const elapsed = (performance.now() - start) / 1000;
console.log(); // Clear progress line
if (opts.output === "json") {
formatJsonEnvelope({
command: "import",
data: { added, failed, duration_s: parseFloat(elapsed.toFixed(2)) },
durationMs: Math.round(elapsed * 1000),
});
return;
}
if (opts.output === "json") {
formatJsonEnvelope({
command: "import",
data: {
added,
failed,
duration_s: Number.parseFloat(elapsed.toFixed(2)),
},
durationMs: Math.round(elapsed * 1000),
});
return;
}
printSuccess(`Imported ${added} memories (${elapsed.toFixed(2)}s)`);
if (failed > 0) {
printError(`${failed} memories failed to import.`);
}
printSuccess(`Imported ${added} memories (${elapsed.toFixed(2)}s)`);
if (failed > 0) {
printError(`${failed} memories failed to import.`);
}
}
+115 -107
View File
@@ -18,150 +18,158 @@ export const DEFAULT_BASE_URL = "https://api.mem0.ai";
export const CONFIG_VERSION = 1;
export interface PlatformConfig {
apiKey: string;
baseUrl: string;
apiKey: string;
baseUrl: string;
}
export interface DefaultsConfig {
userId: string;
agentId: string;
appId: string;
runId: string;
enableGraph: boolean;
userId: string;
agentId: string;
appId: string;
runId: string;
enableGraph: boolean;
}
export interface Mem0Config {
version: number;
defaults: DefaultsConfig;
platform: PlatformConfig;
version: number;
defaults: DefaultsConfig;
platform: PlatformConfig;
}
export function createDefaultConfig(): Mem0Config {
return {
version: CONFIG_VERSION,
defaults: {
userId: "",
agentId: "",
appId: "",
runId: "",
enableGraph: false,
},
platform: {
apiKey: "",
baseUrl: DEFAULT_BASE_URL,
},
};
return {
version: CONFIG_VERSION,
defaults: {
userId: "",
agentId: "",
appId: "",
runId: "",
enableGraph: false,
},
platform: {
apiKey: "",
baseUrl: DEFAULT_BASE_URL,
},
};
}
export function ensureConfigDir(): string {
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
return CONFIG_DIR;
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
return CONFIG_DIR;
}
export function loadConfig(): Mem0Config {
const config = createDefaultConfig();
const config = createDefaultConfig();
if (fs.existsSync(CONFIG_FILE)) {
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
const data = JSON.parse(raw);
if (fs.existsSync(CONFIG_FILE)) {
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
const data = JSON.parse(raw);
config.version = data.version ?? CONFIG_VERSION;
config.version = data.version ?? CONFIG_VERSION;
const plat = data.platform ?? {};
config.platform.apiKey = plat.api_key ?? "";
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
const plat = data.platform ?? {};
config.platform.apiKey = plat.api_key ?? "";
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
const defaults = data.defaults ?? {};
config.defaults.userId = defaults.user_id ?? "";
config.defaults.agentId = defaults.agent_id ?? "";
config.defaults.appId = defaults.app_id ?? "";
config.defaults.runId = defaults.run_id ?? "";
config.defaults.enableGraph = defaults.enable_graph ?? false;
}
const defaults = data.defaults ?? {};
config.defaults.userId = defaults.user_id ?? "";
config.defaults.agentId = defaults.agent_id ?? "";
config.defaults.appId = defaults.app_id ?? "";
config.defaults.runId = defaults.run_id ?? "";
config.defaults.enableGraph = defaults.enable_graph ?? false;
}
// Environment variable overrides
if (process.env.MEM0_API_KEY) config.platform.apiKey = process.env.MEM0_API_KEY;
if (process.env.MEM0_BASE_URL) config.platform.baseUrl = process.env.MEM0_BASE_URL;
if (process.env.MEM0_USER_ID) config.defaults.userId = process.env.MEM0_USER_ID;
if (process.env.MEM0_AGENT_ID) config.defaults.agentId = process.env.MEM0_AGENT_ID;
if (process.env.MEM0_APP_ID) config.defaults.appId = process.env.MEM0_APP_ID;
if (process.env.MEM0_RUN_ID) config.defaults.runId = process.env.MEM0_RUN_ID;
if (process.env.MEM0_ENABLE_GRAPH) {
config.defaults.enableGraph = ["true", "1", "yes"].includes(
process.env.MEM0_ENABLE_GRAPH.toLowerCase(),
);
}
// Environment variable overrides
if (process.env.MEM0_API_KEY)
config.platform.apiKey = process.env.MEM0_API_KEY;
if (process.env.MEM0_BASE_URL)
config.platform.baseUrl = process.env.MEM0_BASE_URL;
if (process.env.MEM0_USER_ID)
config.defaults.userId = process.env.MEM0_USER_ID;
if (process.env.MEM0_AGENT_ID)
config.defaults.agentId = process.env.MEM0_AGENT_ID;
if (process.env.MEM0_APP_ID) config.defaults.appId = process.env.MEM0_APP_ID;
if (process.env.MEM0_RUN_ID) config.defaults.runId = process.env.MEM0_RUN_ID;
if (process.env.MEM0_ENABLE_GRAPH) {
config.defaults.enableGraph = ["true", "1", "yes"].includes(
process.env.MEM0_ENABLE_GRAPH.toLowerCase(),
);
}
return config;
return config;
}
export function saveConfig(config: Mem0Config): void {
ensureConfigDir();
ensureConfigDir();
const data = {
version: config.version,
defaults: {
user_id: config.defaults.userId,
agent_id: config.defaults.agentId,
app_id: config.defaults.appId,
run_id: config.defaults.runId,
enable_graph: config.defaults.enableGraph,
},
platform: {
api_key: config.platform.apiKey,
base_url: config.platform.baseUrl,
},
};
const data = {
version: config.version,
defaults: {
user_id: config.defaults.userId,
agent_id: config.defaults.agentId,
app_id: config.defaults.appId,
run_id: config.defaults.runId,
enable_graph: config.defaults.enableGraph,
},
platform: {
api_key: config.platform.apiKey,
base_url: config.platform.baseUrl,
},
};
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
fs.chmodSync(CONFIG_FILE, 0o600);
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
fs.chmodSync(CONFIG_FILE, 0o600);
}
export function redactKey(key: string): string {
if (!key) return "(not set)";
if (key.length <= 8) return key.slice(0, 2) + "***";
return key.slice(0, 4) + "..." + key.slice(-4);
if (!key) return "(not set)";
if (key.length <= 8) return `${key.slice(0, 2)}***`;
return `${key.slice(0, 4)}...${key.slice(-4)}`;
}
/** Key map from dotted config path to the config object fields. */
const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
"platform.api_key": ["platform", "apiKey"],
"platform.base_url": ["platform", "baseUrl"],
"defaults.user_id": ["defaults", "userId"],
"defaults.agent_id": ["defaults", "agentId"],
"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"],
"platform.api_key": ["platform", "apiKey"],
"platform.base_url": ["platform", "baseUrl"],
"defaults.user_id": ["defaults", "userId"],
"defaults.agent_id": ["defaults", "agentId"],
"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 {
const mapping = KEY_MAP[dottedKey];
if (!mapping) return undefined;
const [section, field] = mapping;
return (config[section] as unknown as Record<string, unknown>)[field];
const mapping = KEY_MAP[dottedKey];
if (!mapping) return undefined;
const [section, field] = mapping;
return (config[section] as unknown as Record<string, unknown>)[field];
}
export function setNestedValue(config: Mem0Config, dottedKey: string, value: string): boolean {
const mapping = KEY_MAP[dottedKey];
if (!mapping) return false;
const [section, field] = mapping;
const obj = config[section] as unknown as Record<string, unknown>;
export function setNestedValue(
config: Mem0Config,
dottedKey: string,
value: string,
): boolean {
const mapping = KEY_MAP[dottedKey];
if (!mapping) return false;
const [section, field] = mapping;
const obj = config[section] as unknown as Record<string, unknown>;
const current = obj[field];
if (typeof current === "boolean") {
obj[field] = ["true", "1", "yes"].includes(value.toLowerCase());
} else if (typeof current === "number") {
obj[field] = parseInt(value, 10);
} else {
obj[field] = value;
}
return true;
const current = obj[field];
if (typeof current === "boolean") {
obj[field] = ["true", "1", "yes"].includes(value.toLowerCase());
} else if (typeof current === "number") {
obj[field] = Number.parseInt(value, 10);
} else {
obj[field] = value;
}
return true;
}
+2 -2
View File
@@ -1,2 +1,2 @@
/** Injected by tsup at build time from package.json version field. */
declare const __CLI_VERSION__: string;
/** Injected by tsup at build time from package.json version field. Undefined in dev/test. */
declare const __CLI_VERSION__: string | undefined;
+284 -280
View File
@@ -4,40 +4,40 @@
*/
import chalk from "chalk";
import type { Command, Help, Option, Argument } from "commander";
import type { Argument, Command, Help, Option } from "commander";
// Colors imported from chalk directly to match Typer/Rich defaults
// ── Colors (matching Typer/Rich defaults) ────────────────────────────────
const cyanBold = chalk.cyan.bold; // option flags, command names
const greenBold = chalk.green.bold; // switch flags (boolean --force etc)
const yellowBold = chalk.yellow.bold; // metavar <value>
const yellow = chalk.yellow; // "Usage:" label
const bold = chalk.bold; // command name in usage
const dim = chalk.dim; // defaults, descriptions
const dimBorder = chalk.dim; // panel borders
const cyanBold = chalk.cyan.bold; // option flags, command names
const greenBold = chalk.green.bold; // switch flags (boolean --force etc)
const yellowBold = chalk.yellow.bold; // metavar <value>
const yellow = chalk.yellow; // "Usage:" label
const bold = chalk.bold; // command name in usage
const dim = chalk.dim; // defaults, descriptions
const dimBorder = chalk.dim; // panel borders
// ── Strip ANSI ───────────────────────────────────────────────────────────
// eslint-disable-next-line no-control-regex
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequence is intentional
const ANSI_RE = /\x1b\[[0-9;]*m/g;
function stripAnsi(str: string): number {
return str.replace(ANSI_RE, "").length;
return str.replace(ANSI_RE, "").length;
}
// ── Command display order (matches Python CLI) ──────────────────────────
/** Commands grouped into panels, matching Python CLI's rich_help_panel. */
const COMMAND_GROUPS: { panel: string; commands: string[] }[] = [
{
panel: "Memory",
commands: ["add", "search", "get", "list", "update", "delete"],
},
{
panel: "Management",
commands: ["init", "status", "import", "help", "entity", "config"],
},
{
panel: "Memory",
commands: ["add", "search", "get", "list", "update", "delete"],
},
{
panel: "Management",
commands: ["init", "status", "import", "help", "entity", "config"],
},
];
/** Flat order derived from COMMAND_GROUPS. */
@@ -46,88 +46,88 @@ const COMMAND_ORDER: string[] = COMMAND_GROUPS.flatMap((g) => g.commands);
// ── Option-to-panel mapping (derived from Python's rich_help_panel) ─────
const OPTION_PANELS: Record<string, Record<string, string>> = {
add: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
search: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--top-k": "Search",
"--threshold": "Search",
"--rerank": "Search",
"--keyword": "Search",
"--filter": "Search",
"--fields": "Search",
"--graph": "Search",
"--no-graph": "Search",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
get: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
list: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--page": "Pagination",
"--page-size": "Pagination",
"--category": "Filters",
"--after": "Filters",
"--before": "Filters",
"--graph": "Filters",
"--no-graph": "Filters",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
update: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
delete: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
status: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
import: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
add: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
search: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--top-k": "Search",
"--threshold": "Search",
"--rerank": "Search",
"--keyword": "Search",
"--filter": "Search",
"--fields": "Search",
"--graph": "Search",
"--no-graph": "Search",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
get: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
list: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--page": "Pagination",
"--page-size": "Pagination",
"--category": "Filters",
"--after": "Filters",
"--before": "Filters",
"--graph": "Filters",
"--no-graph": "Filters",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
update: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
delete: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
status: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
import: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
};
const PANEL_ORDER: string[] = [
"Scope",
"Search",
"Pagination",
"Filters",
"Output",
"Connection",
"Scope",
"Search",
"Pagination",
"Filters",
"Output",
"Connection",
];
// ── Panel rendering ─────────────────────────────────────────────────────
@@ -141,234 +141,238 @@ const PANEL_ORDER: string[] = [
* ╰────────────────────────────────╯
* ```
*/
function renderPanel(
title: string,
rows: string[],
width: number,
): string {
if (rows.length === 0) return "";
function renderPanel(title: string, rows: string[], width: number): string {
if (rows.length === 0) return "";
// Inner width is total width minus the two border chars
const inner = width - 2;
// Inner width is total width minus the two border chars
const inner = width - 2;
// Top border: ╭─ Title ─...─╮
const titleStr = ` ${title} `;
const fillLen = Math.max(0, inner - 1 - titleStr.length);
const topLine =
dimBorder("╭─") +
dimBorder(titleStr) +
dimBorder("─".repeat(fillLen)) +
dimBorder("╮");
// Top border: ╭─ Title ─...─╮
const titleStr = ` ${title} `;
const fillLen = Math.max(0, inner - 1 - titleStr.length);
const topLine =
dimBorder("╭─") +
dimBorder(titleStr) +
dimBorder("─".repeat(fillLen)) +
dimBorder("╮");
// Bottom border: ╰─...─╯
const bottomLine = dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯");
// Bottom border: ╰─...─╯
const bottomLine =
dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯");
// Content rows
const contentLines = rows.map((row) => {
const visLen = stripAnsi(row);
const pad = Math.max(0, inner - 1 - visLen);
return dimBorder("│") + " " + row + " ".repeat(pad) + dimBorder("│");
});
// Content rows
const contentLines = rows.map((row) => {
const visLen = stripAnsi(row);
const pad = Math.max(0, inner - 1 - visLen);
return `${dimBorder("│")} ${row}${" ".repeat(pad)}${dimBorder("│")}`;
});
return [topLine, ...contentLines, bottomLine].join("\n");
return [topLine, ...contentLines, bottomLine].join("\n");
}
// ── Format an option term (short + long) ────────────────────────────────
function formatOptionTerm(opt: Option): string {
const parts: string[] = [];
if (opt.short) parts.push(opt.short);
if (opt.long) parts.push(opt.long);
let term = parts.join(", ");
const parts: string[] = [];
if (opt.short) parts.push(opt.short);
if (opt.long) parts.push(opt.long);
let term = parts.join(", ");
// Append value placeholder for non-boolean options
if (opt.flags) {
const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/);
if (match) {
term += " " + match[0];
}
}
return term;
// Append value placeholder for non-boolean options
if (opt.flags) {
const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/);
if (match) {
term += ` ${match[0]}`;
}
}
return term;
}
// ── Get the long flag name for panel lookup ─────────────────────────────
function getLongFlag(opt: Option): string {
if (opt.long) return opt.long;
return opt.short || "";
if (opt.long) return opt.long;
return opt.short || "";
}
// ── Format a default value ──────────────────────────────────────────────
function formatDefault(opt: Option): string {
if (opt.defaultValue !== undefined && opt.defaultValue !== false) {
return dim(` [default: ${opt.defaultValue}]`);
}
return "";
if (opt.defaultValue !== undefined && opt.defaultValue !== false) {
return dim(` [default: ${opt.defaultValue}]`);
}
return "";
}
// ── The main help formatter ─────────────────────────────────────────────
export function richFormatHelp(cmd: Command, helper: Help): string {
const width = process.stdout.columns || 80;
const lines: string[] = [];
const width = process.stdout.columns || 80;
const lines: string[] = [];
const isRoot = !cmd.parent;
const isRoot = !cmd.parent;
// ── Usage line ──
const usage = helper.commandUsage(cmd);
lines.push("");
if (isRoot) {
// Root: "Usage: mem0 <command> [options]" — <command> yellow, [options] bold
lines.push(` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`);
} else {
// Subcommands: split into command path (bold) and args (yellow)
const usageParts = usage.split(" ");
const cmdPath: string[] = [];
const argParts: string[] = [];
let pastCmd = false;
for (const part of usageParts) {
if (!pastCmd && !part.startsWith("[") && !part.startsWith("<")) {
cmdPath.push(part);
} else {
pastCmd = true;
argParts.push(part);
}
}
lines.push(` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`);
}
lines.push("");
// ── Usage line ──
const usage = helper.commandUsage(cmd);
lines.push("");
if (isRoot) {
// Root: "Usage: mem0 <command> [options]" — <command> yellow, [options] bold
lines.push(
` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`,
);
} else {
// Subcommands: split into command path (bold) and args (yellow)
const usageParts = usage.split(" ");
const cmdPath: string[] = [];
const argParts: string[] = [];
let pastCmd = false;
for (const part of usageParts) {
if (!pastCmd && !part.startsWith("[") && !part.startsWith("<")) {
cmdPath.push(part);
} else {
pastCmd = true;
argParts.push(part);
}
}
lines.push(
` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`,
);
}
lines.push("");
// ── Description ──
const desc = helper.commandDescription(cmd);
if (desc) {
// Split multi-line descriptions (e.g., title + tagline)
const descLines = desc.split("\n");
for (let i = 0; i < descLines.length; i++) {
const dLine = descLines[i];
// First line is the title, subsequent non-empty lines are tagline (dimmed)
if (i === 0 || dLine.trim() === "") {
lines.push(` ${dLine}`);
} else {
lines.push(` ${dim(dLine)}`);
}
}
lines.push("");
}
// ── Description ──
const desc = helper.commandDescription(cmd);
if (desc) {
// Split multi-line descriptions (e.g., title + tagline)
const descLines = desc.split("\n");
for (let i = 0; i < descLines.length; i++) {
const dLine = descLines[i];
// First line is the title, subsequent non-empty lines are tagline (dimmed)
if (i === 0 || dLine.trim() === "") {
lines.push(` ${dLine}`);
} else {
lines.push(` ${dim(dLine)}`);
}
}
lines.push("");
}
// ── Arguments panel (subcommands only) ──
if (!isRoot) {
const visibleArgs = helper.visibleArguments(cmd);
if (visibleArgs.length > 0) {
const maxLen = Math.max(...visibleArgs.map((a: Argument) => a.name().length));
const argRows = visibleArgs.map((a: Argument) => {
const name = cyanBold(a.name().padEnd(maxLen));
const description = helper.argumentDescription(a);
return ` ${name} ${description}`;
});
const panel = renderPanel("Arguments", argRows, width);
if (panel) lines.push(panel);
}
}
// ── Arguments panel (subcommands only) ──
if (!isRoot) {
const visibleArgs = helper.visibleArguments(cmd);
if (visibleArgs.length > 0) {
const maxLen = Math.max(
...visibleArgs.map((a: Argument) => a.name().length),
);
const argRows = visibleArgs.map((a: Argument) => {
const name = cyanBold(a.name().padEnd(maxLen));
const description = helper.argumentDescription(a);
return ` ${name} ${description}`;
});
const panel = renderPanel("Arguments", argRows, width);
if (panel) lines.push(panel);
}
}
// ── Collect options (grouped into panels for subcommands) ──
const visibleOpts = helper.visibleOptions(cmd);
const cmdName = cmd.name();
const panelMap = (!isRoot && OPTION_PANELS[cmdName]) ? OPTION_PANELS[cmdName] : {};
// ── Collect options (grouped into panels for subcommands) ──
const visibleOpts = helper.visibleOptions(cmd);
const cmdName = cmd.name();
const panelMap =
!isRoot && OPTION_PANELS[cmdName] ? OPTION_PANELS[cmdName] : {};
const grouped: Record<string, Option[]> = { Options: [] };
for (const panelName of PANEL_ORDER) {
grouped[panelName] = [];
}
const grouped: Record<string, Option[]> = { Options: [] };
for (const panelName of PANEL_ORDER) {
grouped[panelName] = [];
}
for (const opt of visibleOpts) {
const flag = getLongFlag(opt);
const panel = panelMap[flag];
if (panel && PANEL_ORDER.includes(panel)) {
grouped[panel].push(opt);
} else {
grouped["Options"].push(opt);
}
}
for (const opt of visibleOpts) {
const flag = getLongFlag(opt);
const panel = panelMap[flag];
if (panel && PANEL_ORDER.includes(panel)) {
grouped[panel].push(opt);
} else {
grouped.Options.push(opt);
}
}
// ── Collect commands ──
const visibleCmds = helper.visibleCommands(cmd);
// ── Collect commands ──
const visibleCmds = helper.visibleCommands(cmd);
if (isRoot) {
// ROOT: Options first, then command groups (matches Python/Typer ordering)
if (grouped["Options"].length > 0) {
const optRows = formatOptionRows(grouped["Options"]);
const panel = renderPanel("Options", optRows, width);
if (panel) lines.push(panel);
}
if (visibleCmds.length > 0) {
const cmdMap = new Map(visibleCmds.map((c) => [c.name(), c]));
for (const group of COMMAND_GROUPS) {
const groupCmds = group.commands
.map((name) => cmdMap.get(name))
.filter((c): c is Command => c !== undefined);
if (groupCmds.length === 0) continue;
const maxLen = Math.max(...groupCmds.map((c) => c.name().length));
const cmdRows = groupCmds.map((c) => {
const name = cyanBold(c.name().padEnd(maxLen));
const description = helper.subcommandDescription(c);
return ` ${name} ${description}`;
});
const panel = renderPanel(group.panel, cmdRows, width);
if (panel) lines.push(panel);
}
}
} else {
// SUBCOMMANDS: Options/panels first, then sub-subcommands
const panelSequence = ["Options", ...PANEL_ORDER];
for (const panelName of panelSequence) {
const opts = grouped[panelName];
if (opts && opts.length > 0) {
const optRows = formatOptionRows(opts);
const panel = renderPanel(panelName, optRows, width);
if (panel) lines.push(panel);
}
}
// Sub-subcommands (e.g., config show/get/set, entity list/delete)
if (visibleCmds.length > 0) {
const maxLen = Math.max(...visibleCmds.map((c) => c.name().length));
const cmdRows = visibleCmds.map((c) => {
const name = cyanBold(c.name().padEnd(maxLen));
const description = helper.subcommandDescription(c);
return ` ${name} ${description}`;
});
const panel = renderPanel("Commands", cmdRows, width);
if (panel) lines.push(panel);
}
}
if (isRoot) {
// ROOT: Options first, then command groups (matches Python/Typer ordering)
if (grouped.Options.length > 0) {
const optRows = formatOptionRows(grouped.Options);
const panel = renderPanel("Options", optRows, width);
if (panel) lines.push(panel);
}
if (visibleCmds.length > 0) {
const cmdMap = new Map(visibleCmds.map((c) => [c.name(), c]));
for (const group of COMMAND_GROUPS) {
const groupCmds = group.commands
.map((name) => cmdMap.get(name))
.filter((c): c is Command => c !== undefined);
if (groupCmds.length === 0) continue;
const maxLen = Math.max(...groupCmds.map((c) => c.name().length));
const cmdRows = groupCmds.map((c) => {
const name = cyanBold(c.name().padEnd(maxLen));
const description = helper.subcommandDescription(c);
return ` ${name} ${description}`;
});
const panel = renderPanel(group.panel, cmdRows, width);
if (panel) lines.push(panel);
}
}
} else {
// SUBCOMMANDS: Options/panels first, then sub-subcommands
const panelSequence = ["Options", ...PANEL_ORDER];
for (const panelName of panelSequence) {
const opts = grouped[panelName];
if (opts && opts.length > 0) {
const optRows = formatOptionRows(opts);
const panel = renderPanel(panelName, optRows, width);
if (panel) lines.push(panel);
}
}
// Sub-subcommands (e.g., config show/get/set, entity list/delete)
if (visibleCmds.length > 0) {
const maxLen = Math.max(...visibleCmds.map((c) => c.name().length));
const cmdRows = visibleCmds.map((c) => {
const name = cyanBold(c.name().padEnd(maxLen));
const description = helper.subcommandDescription(c);
return ` ${name} ${description}`;
});
const panel = renderPanel("Commands", cmdRows, width);
if (panel) lines.push(panel);
}
}
lines.push("");
return lines.join("\n");
lines.push("");
return lines.join("\n");
}
// ── Format option rows with aligned columns ─────────────────────────────
function formatOptionRows(opts: Option[]): string[] {
const terms = opts.map((o) => formatOptionTerm(o));
const maxTermLen = Math.max(...terms.map((t) => t.length));
const terms = opts.map((o) => formatOptionTerm(o));
const maxTermLen = Math.max(...terms.map((t) => t.length));
return opts.map((opt, i) => {
const term = cyanBold(terms[i].padEnd(maxTermLen));
const desc = opt.description || "";
const def = formatDefault(opt);
return ` ${term} ${desc}${def}`;
});
return opts.map((opt, i) => {
const term = cyanBold(terms[i].padEnd(maxTermLen));
const desc = opt.description || "";
const def = formatDefault(opt);
return ` ${term} ${desc}${def}`;
});
}
// ── Sort commands by COMMAND_ORDER ──────────────────────────────────────
function sortCommands(cmds: Command[]): Command[] {
return [...cmds].sort((a, b) => {
const ai = COMMAND_ORDER.indexOf(a.name());
const bi = COMMAND_ORDER.indexOf(b.name());
// Unknown commands go to end, preserving original order
const aIdx = ai === -1 ? COMMAND_ORDER.length : ai;
const bIdx = bi === -1 ? COMMAND_ORDER.length : bi;
return aIdx - bIdx;
});
return [...cmds].sort((a, b) => {
const ai = COMMAND_ORDER.indexOf(a.name());
const bi = COMMAND_ORDER.indexOf(b.name());
// Unknown commands go to end, preserving original order
const aIdx = ai === -1 ? COMMAND_ORDER.length : ai;
const bIdx = bi === -1 ? COMMAND_ORDER.length : bi;
return aIdx - bIdx;
});
}
+515 -396
View File
File diff suppressed because it is too large Load Diff
+207 -191
View File
@@ -2,243 +2,259 @@
* Output formatting for mem0 CLI — text, JSON, table, quiet modes.
*/
import Table from "cli-table3";
import boxen from "boxen";
import Table from "cli-table3";
import { colors, sym } from "./branding.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);
}
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",
memories: Record<string, unknown>[],
title = "memories",
): void {
const count = memories.length;
console.log(`\n${brand(`Found ${count} ${title}:`)}\n`);
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;
}
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}`);
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}`);
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();
}
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: [] },
});
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]);
}
}
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();
console.log();
console.log(table.toString());
console.log();
}
export function formatJson(data: unknown): void {
console.log(JSON.stringify(data, null, 2));
console.log(JSON.stringify(data, null, 2));
}
export function formatSingleMemory(
mem: Record<string, unknown>,
output = "text",
mem: Record<string, unknown>,
output = "text",
): void {
if (output === "json") {
formatJson(mem);
return;
}
if (output === "json") {
formatJson(mem);
return;
}
const memoryText = (mem.memory ?? mem.text ?? "") as string;
const memId = (mem.id ?? "") as string;
const memoryText = (mem.memory ?? mem.text ?? "") as string;
const memId = (mem.id ?? "") as string;
const lines: string[] = [];
lines.push(` ${memoryText}`);
lines.push("");
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}`);
}
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();
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",
result: Record<string, unknown> | Record<string, unknown>[],
output = "text",
): void {
if (output === "json") {
formatJson(result);
return;
}
if (output === "quiet") return;
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]);
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;
}
if (!results.length) {
console.log(` ${dim("No memories extracted.")}`);
return;
}
console.log();
for (const r of results) {
// Detect async PENDING response
if (r.status === "PENDING") {
const eventId = ((r.event_id as string) ?? "").slice(0, 8);
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(" "));
continue;
}
console.log();
for (const r of results) {
// Detect async PENDING response
if (r.status === "PENDING") {
const eventId = ((r.event_id as string) ?? "").slice(0, 8);
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(" "));
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);
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;
}
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();
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;
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;
console.log(JSON.stringify(envelope, null, 2));
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;
console.log(JSON.stringify(envelope, null, 2));
}
export function printResultSummary(opts: {
count: number;
durationSecs?: number;
page?: number;
scopeIds?: Record<string, string | undefined>;
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.replace(/_/g, " ")}=${v}`);
if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
}
if (opts.durationSecs !== undefined) parts.push(`${opts.durationSecs.toFixed(2)}s`);
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.replace(/_/g, " ")}=${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();
console.log(` ${dim(parts.join(" · "))}`);
console.log();
}
+10
View File
@@ -0,0 +1,10 @@
import { createRequire } from "node:module";
// __CLI_VERSION__ is replaced at build time by tsup (see tsup.config.ts).
// When running via tsx in dev/test mode, fall back to reading package.json.
// typeof is safe to use on undeclared identifiers — it returns 'undefined' without throwing.
export const CLI_VERSION: string =
typeof __CLI_VERSION__ !== "undefined"
? (__CLI_VERSION__ as string)
: (createRequire(import.meta.url)("../package.json") as { version: string })
.version;
+4 -4
View File
@@ -80,19 +80,19 @@ describe("printWarning", () => {
describe("printInfo", () => {
it("prints info message", () => {
printInfo("Important note");
expect(output).toContain("Important note");
expect(errOutput).toContain("Important note");
});
});
describe("printScope", () => {
it("prints scope when IDs present", () => {
printScope({ user_id: "alice", agent_id: "bot" });
expect(output).toContain("alice");
expect(output).toContain("bot");
expect(errOutput).toContain("alice");
expect(errOutput).toContain("bot");
});
it("prints nothing when no IDs", () => {
printScope({});
expect(output).toBe("");
expect(errOutput).toBe("");
});
});
+2 -2
View File
@@ -125,7 +125,7 @@ describe("cmdSearch", () => {
enableGraph: false,
output: "text",
});
expect(output).toContain("No memories found");
expect(errOutput).toContain("No memories found");
});
});
@@ -166,7 +166,7 @@ describe("cmdList", () => {
enableGraph: false,
output: "text",
});
expect(output).toContain("No memories found");
expect(errOutput).toContain("No memories found");
});
});
+11
View File
@@ -0,0 +1,11 @@
import { createRequire } from "node:module";
import { defineConfig } from "vitest/config";
const _require = createRequire(import.meta.url);
const pkg = _require("./package.json") as { version: string };
export default defineConfig({
define: {
__CLI_VERSION__: JSON.stringify(pkg.version),
},
});