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
+100
View File
@@ -0,0 +1,100 @@
name: CLI Node CI
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'cli/node/**'
- '.github/workflows/cli-node-ci.yml'
pull_request:
paths:
- 'cli/node/**'
- '.github/workflows/cli-node-ci.yml'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: cli/node/pnpm-lock.yaml
- name: Install dependencies
working-directory: cli/node
run: pnpm install --frozen-lockfile
- name: Lint
working-directory: cli/node
run: pnpm run lint
- name: Type check
working-directory: cli/node
run: pnpm run typecheck
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
cache-dependency-path: cli/node/pnpm-lock.yaml
- name: Install dependencies
working-directory: cli/node
run: pnpm install --frozen-lockfile
- name: Run tests
working-directory: cli/node
run: pnpm run test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: cli/node/pnpm-lock.yaml
- name: Install dependencies
working-directory: cli/node
run: pnpm install --frozen-lockfile
- name: Build
working-directory: cli/node
run: pnpm run build
- name: Verify dist output
run: |
test -f cli/node/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
+79
View File
@@ -0,0 +1,79 @@
name: CLI Python CI
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'cli/python/**'
- '.github/workflows/cli-python-ci.yml'
pull_request:
paths:
- 'cli/python/**'
- '.github/workflows/cli-python-ci.yml'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dev dependencies
working-directory: cli/python
run: pip install -e ".[dev]"
- name: Lint with ruff
working-directory: cli/python
run: ruff check .
- name: Check formatting
working-directory: cli/python
run: ruff format --check .
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dev dependencies
working-directory: cli/python
run: pip install -e ".[dev]"
- name: Run tests
working-directory: cli/python
run: pytest
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Hatch
run: pip install hatch
- name: Build
working-directory: cli/python
run: hatch build --clean
- name: Verify dist output
run: |
ls cli/python/dist/*.whl || (echo "Wheel file missing" && exit 1)
ls cli/python/dist/*.tar.gz || (echo "Source dist missing" && exit 1)
+80 -71
View File
@@ -6,110 +6,119 @@ import type { Mem0Config } from "../config.js";
import { PlatformBackend } from "./platform.js"; import { PlatformBackend } from "./platform.js";
export interface AddOptions { export interface AddOptions {
userId?: string; userId?: string;
agentId?: string; agentId?: string;
appId?: string; appId?: string;
runId?: string; runId?: string;
metadata?: Record<string, unknown>; metadata?: Record<string, unknown>;
immutable?: boolean; immutable?: boolean;
infer?: boolean; infer?: boolean;
expires?: string; expires?: string;
categories?: string[]; categories?: string[];
enableGraph?: boolean; enableGraph?: boolean;
} }
export interface SearchOptions { export interface SearchOptions {
userId?: string; userId?: string;
agentId?: string; agentId?: string;
appId?: string; appId?: string;
runId?: string; runId?: string;
topK?: number; topK?: number;
threshold?: number; threshold?: number;
rerank?: boolean; rerank?: boolean;
keyword?: boolean; keyword?: boolean;
filters?: Record<string, unknown>; filters?: Record<string, unknown>;
fields?: string[]; fields?: string[];
enableGraph?: boolean; enableGraph?: boolean;
} }
export interface ListOptions { export interface ListOptions {
userId?: string; userId?: string;
agentId?: string; agentId?: string;
appId?: string; appId?: string;
runId?: string; runId?: string;
page?: number; page?: number;
pageSize?: number; pageSize?: number;
category?: string; category?: string;
after?: string; after?: string;
before?: string; before?: string;
enableGraph?: boolean; enableGraph?: boolean;
} }
export interface DeleteOptions { export interface DeleteOptions {
all?: boolean; all?: boolean;
userId?: string; userId?: string;
agentId?: string; agentId?: string;
appId?: string; appId?: string;
runId?: string; runId?: string;
} }
export interface EntityIds { export interface EntityIds {
userId?: string; userId?: string;
agentId?: string; agentId?: string;
appId?: string; appId?: string;
runId?: string; runId?: string;
} }
export interface Backend { export interface Backend {
add( add(
content?: string, content?: string,
messages?: Record<string, unknown>[], messages?: Record<string, unknown>[],
opts?: AddOptions, opts?: AddOptions,
): Promise<Record<string, unknown>>; ): 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( update(
memoryId: string, memoryId: string,
content?: string, content?: string,
metadata?: Record<string, unknown>, metadata?: Record<string, unknown>,
): Promise<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 { export class AuthError extends Error {
constructor(message = "Authentication failed. Your API key may be invalid or expired.") { constructor(
super(message); message = "Authentication failed. Your API key may be invalid or expired.",
this.name = "AuthError"; ) {
} super(message);
this.name = "AuthError";
}
} }
export class NotFoundError extends Error { export class NotFoundError extends Error {
constructor(path: string) { constructor(path: string) {
super(`Resource not found: ${path}`); super(`Resource not found: ${path}`);
this.name = "NotFoundError"; this.name = "NotFoundError";
} }
} }
export class APIError extends Error { export class APIError extends Error {
constructor(path: string, detail: string) { constructor(path: string, detail: string) {
super(`Bad request to ${path}: ${detail}`); super(`Bad request to ${path}: ${detail}`);
this.name = "APIError"; this.name = "APIError";
} }
} }
export function getBackend(config: Mem0Config): Backend { 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 { 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"; export { AuthError, NotFoundError, APIError } from "./base.js";
+301 -271
View File
@@ -4,305 +4,335 @@
import type { PlatformConfig } from "../config.js"; import type { PlatformConfig } from "../config.js";
import { import {
type AddOptions, APIError,
APIError, type AddOptions,
AuthError, AuthError,
type Backend, type Backend,
type DeleteOptions, type DeleteOptions,
type EntityIds, type EntityIds,
type ListOptions, type ListOptions,
NotFoundError, NotFoundError,
type SearchOptions, type SearchOptions,
} from "./base.js"; } from "./base.js";
export class PlatformBackend implements Backend { export class PlatformBackend implements Backend {
private baseUrl: string; private baseUrl: string;
private headers: Record<string, string>; private headers: Record<string, string>;
constructor(config: PlatformConfig) { constructor(config: PlatformConfig) {
this.baseUrl = config.baseUrl.replace(/\/+$/, ""); this.baseUrl = config.baseUrl.replace(/\/+$/, "");
this.headers = { this.headers = {
Authorization: `Token ${config.apiKey}`, Authorization: `Token ${config.apiKey}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
} }
private async _request( private async _request(
method: string, method: string,
path: string, path: string,
opts?: { json?: unknown; params?: Record<string, string> }, opts?: { json?: unknown; params?: Record<string, string> },
): Promise<unknown> { ): Promise<unknown> {
let url = `${this.baseUrl}${path}`; let url = `${this.baseUrl}${path}`;
if (opts?.params) { if (opts?.params) {
const qs = new URLSearchParams(opts.params).toString(); const qs = new URLSearchParams(opts.params).toString();
url += `?${qs}`; url += `?${qs}`;
} }
const fetchOpts: RequestInit = { const fetchOpts: RequestInit = {
method, method,
headers: this.headers, headers: this.headers,
signal: AbortSignal.timeout(30_000), signal: AbortSignal.timeout(30_000),
}; };
if (opts?.json) { if (opts?.json) {
fetchOpts.body = JSON.stringify(opts.json); fetchOpts.body = JSON.stringify(opts.json);
} }
const resp = await fetch(url, fetchOpts); const resp = await fetch(url, fetchOpts);
if (resp.status === 401) { if (resp.status === 401) {
throw new AuthError(); throw new AuthError();
} }
if (resp.status === 404) { if (resp.status === 404) {
throw new NotFoundError(path); throw new NotFoundError(path);
} }
if (resp.status === 400) { if (resp.status === 400) {
let detail: string; let detail: string;
try { try {
const body = await resp.json() as Record<string, unknown>; const body = (await resp.json()) as Record<string, unknown>;
detail = (body.detail ?? body.message ?? JSON.stringify(body)) as string ?? resp.statusText; detail =
} catch { ((body.detail ?? body.message ?? JSON.stringify(body)) as string) ??
detail = resp.statusText; resp.statusText;
} } catch {
throw new APIError(path, detail); detail = resp.statusText;
} }
if (!resp.ok) { throw new APIError(path, detail);
let detail: string = resp.statusText; }
try { if (!resp.ok) {
const body = await resp.json() as Record<string, unknown>; let detail: string = resp.statusText;
detail = (body.detail ?? body.message ?? resp.statusText) as string; try {
} catch { /* ignore */ } const body = (await resp.json()) as Record<string, unknown>;
throw new Error(`HTTP ${resp.status}: ${detail}`); detail = (body.detail ?? body.message ?? resp.statusText) as string;
} } catch {
if (resp.status === 204) { /* ignore */
return {}; }
} throw new Error(`HTTP ${resp.status}: ${detail}`);
return resp.json(); }
} if (resp.status === 204) {
return {};
}
return resp.json();
}
async add( async add(
content?: string, content?: string,
messages?: Record<string, unknown>[], messages?: Record<string, unknown>[],
opts: AddOptions = {}, opts: AddOptions = {},
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
const payload: Record<string, unknown> = {}; const payload: Record<string, unknown> = {};
if (messages) { if (messages) {
payload.messages = messages; payload.messages = messages;
} else if (content) { } else if (content) {
payload.messages = [{ role: "user", content }]; payload.messages = [{ role: "user", content }];
} }
if (opts.userId) payload.user_id = opts.userId; if (opts.userId) payload.user_id = opts.userId;
if (opts.agentId) payload.agent_id = opts.agentId; if (opts.agentId) payload.agent_id = opts.agentId;
if (opts.appId) payload.app_id = opts.appId; if (opts.appId) payload.app_id = opts.appId;
if (opts.runId) payload.run_id = opts.runId; if (opts.runId) payload.run_id = opts.runId;
if (opts.metadata) payload.metadata = opts.metadata; if (opts.metadata) payload.metadata = opts.metadata;
if (opts.immutable) payload.immutable = true; if (opts.immutable) payload.immutable = true;
if (opts.infer === false) payload.infer = false; if (opts.infer === false) payload.infer = false;
if (opts.expires) payload.expiration_date = opts.expires; if (opts.expires) payload.expiration_date = opts.expires;
if (opts.categories) payload.categories = opts.categories; if (opts.categories) payload.categories = opts.categories;
if (opts.enableGraph) payload.enable_graph = true; if (opts.enableGraph) payload.enable_graph = true;
return (await this._request("POST", "/v1/memories/", { json: payload })) as Record< return (await this._request("POST", "/v1/memories/", {
string, json: payload,
unknown })) as Record<string, unknown>;
>; }
}
private _buildFilters(opts: { private _buildFilters(opts: {
userId?: string; userId?: string;
agentId?: string; agentId?: string;
appId?: string; appId?: string;
runId?: string; runId?: string;
extraFilters?: Record<string, unknown>; extraFilters?: Record<string, unknown>;
}): Record<string, unknown> | undefined { }): Record<string, unknown> | undefined {
// If caller passed a pre-built filter structure, use it directly // If caller passed a pre-built filter structure, use it directly
if (opts.extraFilters && ("AND" in opts.extraFilters || "OR" in opts.extraFilters)) { if (
return opts.extraFilters; opts.extraFilters &&
} ("AND" in opts.extraFilters || "OR" in opts.extraFilters)
) {
return opts.extraFilters;
}
const andConditions: Record<string, unknown>[] = []; const andConditions: Record<string, unknown>[] = [];
if (opts.userId) andConditions.push({ user_id: opts.userId }); if (opts.userId) andConditions.push({ user_id: opts.userId });
if (opts.agentId) andConditions.push({ agent_id: opts.agentId }); if (opts.agentId) andConditions.push({ agent_id: opts.agentId });
if (opts.appId) andConditions.push({ app_id: opts.appId }); if (opts.appId) andConditions.push({ app_id: opts.appId });
if (opts.runId) andConditions.push({ run_id: opts.runId }); if (opts.runId) andConditions.push({ run_id: opts.runId });
if (opts.extraFilters) { if (opts.extraFilters) {
for (const [k, v] of Object.entries(opts.extraFilters)) { for (const [k, v] of Object.entries(opts.extraFilters)) {
andConditions.push({ [k]: v }); andConditions.push({ [k]: v });
} }
} }
if (andConditions.length === 1) return andConditions[0]; if (andConditions.length === 1) return andConditions[0];
if (andConditions.length > 1) return { AND: andConditions }; if (andConditions.length > 1) return { AND: andConditions };
return undefined; return undefined;
} }
async search(query: string, opts: SearchOptions = {}): Promise<Record<string, unknown>[]> { async search(
const payload: Record<string, unknown> = { query: string,
query, opts: SearchOptions = {},
top_k: opts.topK ?? 10, ): Promise<Record<string, unknown>[]> {
threshold: opts.threshold ?? 0.3, const payload: Record<string, unknown> = {
}; query,
top_k: opts.topK ?? 10,
threshold: opts.threshold ?? 0.3,
};
const apiFilters = this._buildFilters({ const apiFilters = this._buildFilters({
userId: opts.userId, userId: opts.userId,
agentId: opts.agentId, agentId: opts.agentId,
appId: opts.appId, appId: opts.appId,
runId: opts.runId, runId: opts.runId,
extraFilters: opts.filters, extraFilters: opts.filters,
}); });
if (apiFilters) payload.filters = apiFilters; if (apiFilters) payload.filters = apiFilters;
if (opts.rerank) payload.rerank = true; if (opts.rerank) payload.rerank = true;
if (opts.keyword) payload.keyword_search = true; if (opts.keyword) payload.keyword_search = true;
if (opts.fields) payload.fields = opts.fields; if (opts.fields) payload.fields = opts.fields;
if (opts.enableGraph) payload.enable_graph = true; if (opts.enableGraph) payload.enable_graph = true;
const result = (await this._request("POST", "/v2/memories/search/", { const result = (await this._request("POST", "/v2/memories/search/", {
json: payload, json: payload,
})) as unknown; })) as unknown;
if (Array.isArray(result)) return result; if (Array.isArray(result)) return result;
const obj = result as Record<string, unknown>; const obj = result as Record<string, unknown>;
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[]; return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
} }
async get(memoryId: string): Promise<Record<string, unknown>> { async get(memoryId: string): Promise<Record<string, unknown>> {
return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record<string, unknown>; return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record<
} string,
unknown
>;
}
async listMemories(opts: ListOptions = {}): Promise<Record<string, unknown>[]> { async listMemories(
const payload: Record<string, unknown> = {}; opts: ListOptions = {},
const params: Record<string, string> = { ): Promise<Record<string, unknown>[]> {
page: String(opts.page ?? 1), const payload: Record<string, unknown> = {};
page_size: String(opts.pageSize ?? 100), const params: Record<string, string> = {
}; page: String(opts.page ?? 1),
page_size: String(opts.pageSize ?? 100),
};
const extra: Record<string, unknown> = {}; const extra: Record<string, unknown> = {};
if (opts.category) { if (opts.category) {
extra.categories = { contains: opts.category }; extra.categories = { contains: opts.category };
} }
if (opts.after) { if (opts.after) {
extra.created_at = { ...(extra.created_at as Record<string, unknown> | undefined), gte: opts.after }; extra.created_at = {
} ...(extra.created_at as Record<string, unknown> | undefined),
if (opts.before) { gte: opts.after,
extra.created_at = { ...(extra.created_at as Record<string, unknown> | undefined), lte: opts.before }; };
} }
if (opts.before) {
extra.created_at = {
...(extra.created_at as Record<string, unknown> | undefined),
lte: opts.before,
};
}
const apiFilters = this._buildFilters({ const apiFilters = this._buildFilters({
userId: opts.userId, userId: opts.userId,
agentId: opts.agentId, agentId: opts.agentId,
appId: opts.appId, appId: opts.appId,
runId: opts.runId, runId: opts.runId,
extraFilters: Object.keys(extra).length > 0 ? extra : undefined, extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
}); });
if (apiFilters) payload.filters = apiFilters; if (apiFilters) payload.filters = apiFilters;
if (opts.enableGraph) payload.enable_graph = true; if (opts.enableGraph) payload.enable_graph = true;
const result = (await this._request("POST", "/v2/memories/", { json: payload, params })) as unknown; const result = (await this._request("POST", "/v2/memories/", {
if (Array.isArray(result)) return result; json: payload,
const obj = result as Record<string, unknown>; params,
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[]; })) 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( async update(
memoryId: string, memoryId: string,
content?: string, content?: string,
metadata?: Record<string, unknown>, metadata?: Record<string, unknown>,
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
const payload: Record<string, unknown> = {}; const payload: Record<string, unknown> = {};
if (content) payload.text = content; if (content) payload.text = content;
if (metadata) payload.metadata = metadata; if (metadata) payload.metadata = metadata;
return (await this._request("PUT", `/v1/memories/${memoryId}/`, { return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
json: payload, json: payload,
})) as Record<string, unknown>; })) as Record<string, unknown>;
} }
async delete( async delete(
memoryId?: string, memoryId?: string,
opts: DeleteOptions = {}, opts: DeleteOptions = {},
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
if (opts.all) { if (opts.all) {
const params: Record<string, string> = {}; const params: Record<string, string> = {};
if (opts.userId) params.user_id = opts.userId; if (opts.userId) params.user_id = opts.userId;
if (opts.agentId) params.agent_id = opts.agentId; if (opts.agentId) params.agent_id = opts.agentId;
if (opts.appId) params.app_id = opts.appId; if (opts.appId) params.app_id = opts.appId;
if (opts.runId) params.run_id = opts.runId; if (opts.runId) params.run_id = opts.runId;
return (await this._request("DELETE", "/v1/memories/", { params })) as Record< return (await this._request("DELETE", "/v1/memories/", {
string, params,
unknown })) as Record<string, unknown>;
>; }
} if (memoryId) {
if (memoryId) { return (await this._request(
return (await this._request("DELETE", `/v1/memories/${memoryId}/`)) as Record< "DELETE",
string, `/v1/memories/${memoryId}/`,
unknown )) as Record<string, unknown>;
>; }
} throw new Error("Either memoryId or --all is required");
throw new Error("Either memoryId or --all is required"); }
}
async deleteEntities(opts: EntityIds): Promise<Record<string, unknown>> { async deleteEntities(opts: EntityIds): Promise<Record<string, unknown>> {
const params: Record<string, string> = {}; const params: Record<string, string> = {};
if (opts.userId) params.user_id = opts.userId; if (opts.userId) params.user_id = opts.userId;
if (opts.agentId) params.agent_id = opts.agentId; if (opts.agentId) params.agent_id = opts.agentId;
if (opts.appId) params.app_id = opts.appId; if (opts.appId) params.app_id = opts.appId;
if (opts.runId) params.run_id = opts.runId; if (opts.runId) params.run_id = opts.runId;
if (Object.keys(params).length === 0) { if (Object.keys(params).length === 0) {
throw new Error("At least one entity ID is required for deleteEntities."); throw new Error("At least one entity ID is required for deleteEntities.");
} }
return (await this._request("DELETE", "/v1/entities/", { params })) as Record< return (await this._request("DELETE", "/v1/entities/", {
string, params,
unknown })) as Record<string, unknown>;
>; }
}
async status( async status(
opts: { userId?: string; agentId?: string } = {}, opts: { userId?: string; agentId?: string } = {},
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
try { try {
if (opts.userId || opts.agentId) { if (opts.userId || opts.agentId) {
const payload: Record<string, unknown> = {}; const payload: Record<string, unknown> = {};
const statusParams: Record<string, string> = { page: "1", page_size: "1" }; const statusParams: Record<string, string> = {
const apiFilters = this._buildFilters({ page: "1",
userId: opts.userId, page_size: "1",
agentId: opts.agentId, };
}); const apiFilters = this._buildFilters({
if (apiFilters) payload.filters = apiFilters; userId: opts.userId,
await this._request("POST", "/v2/memories/", { json: payload, params: statusParams }); agentId: opts.agentId,
} else { });
await this._request("GET", "/v1/entities/"); if (apiFilters) payload.filters = apiFilters;
} await this._request("POST", "/v2/memories/", {
return { connected: true, backend: "platform", base_url: this.baseUrl }; json: payload,
} catch (e) { params: statusParams,
return { });
connected: false, } else {
backend: "platform", await this._request("GET", "/v1/entities/");
error: e instanceof Error ? e.message : String(e), }
}; 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>[]> { async entities(entityType: string): Promise<Record<string, unknown>[]> {
const result = (await this._request("GET", "/v1/entities/")) as unknown; const result = (await this._request("GET", "/v1/entities/")) as unknown;
let items: Record<string, unknown>[]; let items: Record<string, unknown>[];
if (Array.isArray(result)) { if (Array.isArray(result)) {
items = result; items = result;
} else { } else {
items = ((result as Record<string, unknown>).results ?? []) as Record<string, unknown>[]; items = ((result as Record<string, unknown>).results ?? []) as Record<
} string,
unknown
>[];
}
const typeMap: Record<string, string> = { const typeMap: Record<string, string> = {
users: "user", users: "user",
agents: "agent", agents: "agent",
apps: "app", apps: "app",
runs: "run", runs: "run",
}; };
const targetType = typeMap[entityType]; const targetType = typeMap[entityType];
if (targetType) { if (targetType) {
items = items.filter( items = items.filter(
(e) => (e.type as string | undefined)?.toLowerCase() === targetType, (e) => (e.type as string | undefined)?.toLowerCase() === targetType,
); );
} }
return items; return items;
} }
} }
+73 -65
View File
@@ -4,6 +4,7 @@
import chalk from "chalk"; import chalk from "chalk";
import ora, { type Ora } from "ora"; import ora, { type Ora } from "ora";
import { CLI_VERSION } from "./version.js";
export const LOGO = ` export const LOGO = `
███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗ ███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
@@ -36,75 +37,78 @@ const dim = chalk.hex(DIM_COLOR);
* plain-text for piped/non-TTY or NO_COLOR environments. * plain-text for piped/non-TTY or NO_COLOR environments.
*/ */
export function sym(fancy: string, plain: string): string { export function sym(fancy: string, plain: string): string {
if (!process.stdout.isTTY || process.env.NO_COLOR) return plain; if (!process.stdout.isTTY || process.env.NO_COLOR) return plain;
return fancy; return fancy;
} }
export function printBanner(): void { export function printBanner(): void {
const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2)) const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2))
const logoLines = LOGO.trimEnd().split("\n"); const logoLines = LOGO.trimEnd().split("\n");
const tagline = ` ${TAGLINE}`; const tagline = ` ${TAGLINE}`;
const subtitle = `Node.js SDK · v${__CLI_VERSION__}`; const subtitle = `Node.js SDK · v${CLI_VERSION}`;
const contentLines = ["", ...logoLines, "", tagline, ""]; const contentLines = ["", ...logoLines, "", tagline, ""];
// Compute inner width from longest content line + padding both sides // Compute inner width from longest content line + padding both sides
const maxContent = Math.max(...contentLines.map((l) => l.length)); const maxContent = Math.max(...contentLines.map((l) => l.length));
const innerWidth = maxContent + pad * 2; const innerWidth = maxContent + pad * 2;
const totalWidth = innerWidth + 2; // + 2 for │ borders const totalWidth = innerWidth + 2; // + 2 for │ borders
const topBorder = brand(`╭${"─".repeat(totalWidth - 2)}╮`); const topBorder = brand(`╭${"─".repeat(totalWidth - 2)}╮`);
const subtitleFill = totalWidth - 2 - subtitle.length - 3; // 3 = "─ " before subtitle + "─" after const subtitleFill = totalWidth - 2 - subtitle.length - 3; // 3 = "─ " before subtitle + "─" after
const bottomBorder = brand(`╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`); const bottomBorder = brand(
`╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`,
);
const body = contentLines.map((line) => { const body = contentLines.map((line) => {
const rightPad = innerWidth - pad - line.length; const rightPad = innerWidth - pad - line.length;
return `${brand("│")}${" ".repeat(pad)}${brand.bold(line)}${" ".repeat(Math.max(rightPad, 0))}${brand("│")}`; return `${brand("│")}${" ".repeat(pad)}${brand.bold(line)}${" ".repeat(Math.max(rightPad, 0))}${brand("│")}`;
}); });
// Re-color tagline line with accent instead of brand.bold // Re-color tagline line with accent instead of brand.bold
const taglineIdx = body.length - 2; // second-to-last (before trailing empty line) const taglineIdx = body.length - 2; // second-to-last (before trailing empty line)
const taglineRightPad = innerWidth - pad - tagline.length; const taglineRightPad = innerWidth - pad - tagline.length;
body[taglineIdx] = `${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`; body[taglineIdx] =
`${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`;
console.log(topBorder); console.log(topBorder);
for (const line of body) console.log(line); for (const line of body) console.log(line);
console.log(bottomBorder); console.log(bottomBorder);
} }
export function printSuccess(message: string): void { 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 { export function printError(message: string, hint?: string): void {
console.error(`${error(sym("✗", "[error]") + " Error:")} ${message}`); console.error(`${error(`${sym("✗", "[error]")} Error:`)} ${message}`);
if (hint) { if (hint) {
console.error(` ${dim(hint)}`); console.error(` ${dim(hint)}`);
} }
} }
export function printWarning(message: string): void { export function printWarning(message: string): void {
console.error(`${warning(sym("⚠", "[warn]"))} ${message}`); console.error(`${warning(sym("⚠", "[warn]"))} ${message}`);
} }
export function printInfo(message: string): void { 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 { export function printScope(ids: Record<string, string | undefined>): void {
const parts: string[] = []; const parts: string[] = [];
for (const [key, val] of Object.entries(ids)) { for (const [key, val] of Object.entries(ids)) {
if (val) { if (val) {
const label = key.replace(/_/g, " ").replace("id", "ID").trim(); const label = key.replace(/_/g, " ").replace("id", "ID").trim();
parts.push(`${label}=${val}`); parts.push(`${label}=${val}`);
} }
} }
if (parts.length > 0) { if (parts.length > 0) {
console.error(` ${dim(`Scope: ${parts.join(", ")}`)}`); console.error(` ${dim(`Scope: ${parts.join(", ")}`)}`);
} }
} }
export interface TimedStatusContext { export interface TimedStatusContext {
successMsg: string; successMsg: string;
errorMsg: string; errorMsg: string;
} }
/** /**
@@ -112,29 +116,33 @@ export interface TimedStatusContext {
* Equivalent to Python's timed_status context manager. * Equivalent to Python's timed_status context manager.
*/ */
export async function timedStatus<T>( export async function timedStatus<T>(
message: string, message: string,
fn: (ctx: TimedStatusContext) => Promise<T>, fn: (ctx: TimedStatusContext) => Promise<T>,
): Promise<T> { ): Promise<T> {
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" }; const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
const spinner = ora({ text: dim(message), color: "magenta", stream: process.stderr }).start(); const spinner = ora({
const start = performance.now(); text: dim(message),
color: "magenta",
stream: process.stderr,
}).start();
const start = performance.now();
try { try {
const result = await fn(ctx); const result = await fn(ctx);
const elapsed = ((performance.now() - start) / 1000).toFixed(2); const elapsed = ((performance.now() - start) / 1000).toFixed(2);
spinner.stop(); spinner.stop();
if (ctx.successMsg) { if (ctx.successMsg) {
console.error(`${success("✓")} ${ctx.successMsg} (${elapsed}s)`); console.error(`${success("✓")} ${ctx.successMsg} (${elapsed}s)`);
} }
return result; return result;
} catch (err) { } catch (err) {
const elapsed = ((performance.now() - start) / 1000).toFixed(2); const elapsed = ((performance.now() - start) / 1000).toFixed(2);
spinner.stop(); spinner.stop();
if (ctx.errorMsg) { if (ctx.errorMsg) {
console.error(`${error("✗ Error:")} ${ctx.errorMsg} (${elapsed}s)`); console.error(`${error("✗ Error:")} ${ctx.errorMsg} (${elapsed}s)`);
} }
throw err; throw err;
} }
} }
/** Format helpers using brand colors for external use. */ /** Format helpers using brand colors for external use. */
+67 -64
View File
@@ -3,88 +3,91 @@
*/ */
import Table from "cli-table3"; import Table from "cli-table3";
import { printError, printSuccess, colors } from "../branding.js"; import { colors, printError, printSuccess } from "../branding.js";
import { import {
getNestedValue, getNestedValue,
loadConfig, loadConfig,
redactKey, redactKey,
saveConfig, saveConfig,
setNestedValue, setNestedValue,
} from "../config.js"; } from "../config.js";
import { formatJsonEnvelope } from "../output.js"; import { formatJsonEnvelope } from "../output.js";
const { brand, accent, dim } = colors; const { brand, accent, dim } = colors;
export function cmdConfigShow(opts: { output?: string } = {}): void { export function cmdConfigShow(opts: { output?: string } = {}): void {
const config = loadConfig(); const config = loadConfig();
if (opts.output === "json") { if (opts.output === "json") {
formatJsonEnvelope({ formatJsonEnvelope({
command: "config show", command: "config show",
data: { data: {
defaults: { defaults: {
user_id: config.defaults.userId || null, user_id: config.defaults.userId || null,
agent_id: config.defaults.agentId || null, agent_id: config.defaults.agentId || null,
app_id: config.defaults.appId || null, app_id: config.defaults.appId || null,
run_id: config.defaults.runId || null, run_id: config.defaults.runId || null,
enable_graph: config.defaults.enableGraph, enable_graph: config.defaults.enableGraph,
}, },
platform: { platform: {
api_key: redactKey(config.platform.apiKey), api_key: redactKey(config.platform.apiKey),
base_url: config.platform.baseUrl, base_url: config.platform.baseUrl,
}, },
}, },
}); });
return; return;
} }
console.log(); console.log();
console.log(` ${brand("◆ mem0 Configuration")}\n`); console.log(` ${brand("◆ mem0 Configuration")}\n`);
const table = new Table({ const table = new Table({
head: [accent("Key"), accent("Value")], head: [accent("Key"), accent("Value")],
style: { head: [], border: [] }, style: { head: [], border: [] },
}); });
// Defaults // Defaults
table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]); table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]);
table.push(["defaults.agent_id", config.defaults.agentId || dim("(not set)")]); table.push([
table.push(["defaults.app_id", config.defaults.appId || dim("(not set)")]); "defaults.agent_id",
table.push(["defaults.run_id", config.defaults.runId || dim("(not set)")]); config.defaults.agentId || dim("(not set)"),
table.push(["defaults.enable_graph", String(config.defaults.enableGraph)]); ]);
table.push(["", ""]); 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 // Platform
table.push(["platform.api_key", redactKey(config.platform.apiKey)]); table.push(["platform.api_key", redactKey(config.platform.apiKey)]);
table.push(["platform.base_url", config.platform.baseUrl]); table.push(["platform.base_url", config.platform.baseUrl]);
console.log(table.toString()); console.log(table.toString());
console.log(); console.log();
} }
export function cmdConfigGet(key: string): void { export function cmdConfigGet(key: string): void {
const config = loadConfig(); const config = loadConfig();
const value = getNestedValue(config, key); const value = getNestedValue(config, key);
if (value === undefined) { if (value === undefined) {
printError(`Unknown config key: ${key}`); printError(`Unknown config key: ${key}`);
} else { } else {
// Redact secrets // Redact secrets
if (key.includes("api_key") || key.split(".").pop() === "key") { if (key.includes("api_key") || key.split(".").pop() === "key") {
console.log(redactKey(String(value))); console.log(redactKey(String(value)));
} else { } else {
console.log(String(value)); console.log(String(value));
} }
} }
} }
export function cmdConfigSet(key: string, value: string): void { export function cmdConfigSet(key: string, value: string): void {
const config = loadConfig(); const config = loadConfig();
if (setNestedValue(config, key, value)) { if (setNestedValue(config, key, value)) {
saveConfig(config); saveConfig(config);
const display = key.includes("key") ? redactKey(value) : value; const display = key.includes("key") ? redactKey(value) : value;
printSuccess(`${key} = ${display}`); printSuccess(`${key} = ${display}`);
} else { } else {
printError(`Unknown config key: ${key}`); printError(`Unknown config key: ${key}`);
} }
} }
+125 -108
View File
@@ -4,8 +4,14 @@
import readline from "node:readline"; import readline from "node:readline";
import Table from "cli-table3"; import Table from "cli-table3";
import { printError, printInfo, printSuccess, timedStatus, colors } from "../branding.js";
import type { Backend } from "../backend/base.js"; import type { Backend } from "../backend/base.js";
import {
colors,
printError,
printInfo,
printSuccess,
timedStatus,
} from "../branding.js";
import { formatJson } from "../output.js"; import { formatJson } from "../output.js";
const { brand, accent, dim } = colors; const { brand, accent, dim } = colors;
@@ -13,127 +19,138 @@ const { brand, accent, dim } = colors;
const VALID_TYPES = new Set(["users", "agents", "apps", "runs"]); const VALID_TYPES = new Set(["users", "agents", "apps", "runs"]);
export async function cmdEntitiesList( export async function cmdEntitiesList(
backend: Backend, backend: Backend,
entityType: string, entityType: string,
opts: { output: string }, opts: { output: string },
): Promise<void> { ): Promise<void> {
if (!VALID_TYPES.has(entityType)) { if (!VALID_TYPES.has(entityType)) {
printError(`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`); printError(
process.exit(1); `Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`,
} );
process.exit(1);
}
const start = performance.now(); const start = performance.now();
let results: Record<string, unknown>[]; let results: Record<string, unknown>[];
try { try {
results = await timedStatus(`Fetching ${entityType}...`, async () => { results = await timedStatus(`Fetching ${entityType}...`, async () => {
return backend.entities(entityType); return backend.entities(entityType);
}); });
} catch (e) { } catch (e) {
printError( printError(
e instanceof Error ? e.message : String(e), e instanceof Error ? e.message : String(e),
"This feature may require the mem0 Platform.", "This feature may require the mem0 Platform.",
); );
process.exit(1); process.exit(1);
} }
const elapsed = (performance.now() - start) / 1000; const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") { if (opts.output === "json") {
formatJson(results); formatJson(results);
return; return;
} }
if (!results.length) { if (!results.length) {
printInfo(`No ${entityType} found.`); printInfo(`No ${entityType} found.`);
return; return;
} }
const table = new Table({ const table = new Table({
head: [accent("Name / ID"), accent("Created")], head: [accent("Name / ID"), accent("Created")],
style: { head: [], border: [] }, style: { head: [], border: [] },
}); });
for (const entity of results) { for (const entity of results) {
const name = String(entity.name ?? entity.id ?? "—"); const name = String(entity.name ?? entity.id ?? "—");
const created = String(entity.created_at ?? "—").slice(0, 10); const created = String(entity.created_at ?? "—").slice(0, 10);
table.push([name, created]); table.push([name, created]);
} }
console.log(); console.log();
console.log(table.toString()); console.log(table.toString());
console.log(` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`); console.log(
console.log(); ` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`,
);
console.log();
} }
export async function cmdEntitiesDelete( export async function cmdEntitiesDelete(
backend: Backend, backend: Backend,
opts: { opts: {
userId?: string; userId?: string;
agentId?: string; agentId?: string;
appId?: string; appId?: string;
runId?: string; runId?: string;
dryRun?: boolean; dryRun?: boolean;
force: boolean; force: boolean;
output: string; output: string;
}, },
): Promise<void> { ): Promise<void> {
if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) { if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) {
printError("Provide at least one of --user-id, --agent-id, --app-id, --run-id."); printError(
process.exit(1); "Provide at least one of --user-id, --agent-id, --app-id, --run-id.",
} );
process.exit(1);
}
if (opts.dryRun) { if (opts.dryRun) {
const scopeParts: string[] = []; const scopeParts: string[] = [];
if (opts.userId) scopeParts.push(`user=${opts.userId}`); if (opts.userId) scopeParts.push(`user=${opts.userId}`);
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`); if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
if (opts.appId) scopeParts.push(`app=${opts.appId}`); if (opts.appId) scopeParts.push(`app=${opts.appId}`);
if (opts.runId) scopeParts.push(`run=${opts.runId}`); if (opts.runId) scopeParts.push(`run=${opts.runId}`);
printInfo(`Would delete entity ${scopeParts.join(", ")} and all its memories.`); printInfo(
printInfo("No changes made."); `Would delete entity ${scopeParts.join(", ")} and all its memories.`,
return; );
} printInfo("No changes made.");
return;
}
if (!opts.force) { if (!opts.force) {
const scopeParts: string[] = []; const scopeParts: string[] = [];
if (opts.userId) scopeParts.push(`user=${opts.userId}`); if (opts.userId) scopeParts.push(`user=${opts.userId}`);
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`); if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
if (opts.appId) scopeParts.push(`app=${opts.appId}`); if (opts.appId) scopeParts.push(`app=${opts.appId}`);
if (opts.runId) scopeParts.push(`run=${opts.runId}`); if (opts.runId) scopeParts.push(`run=${opts.runId}`);
const scope = scopeParts.join(", "); const scope = scopeParts.join(", ");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const rl = readline.createInterface({
const answer = await new Promise<string>((resolve) => { input: process.stdin,
rl.question( output: process.stdout,
`\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `, });
resolve, const answer = await new Promise<string>((resolve) => {
); rl.question(
}); `\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `,
rl.close(); resolve,
if (answer.toLowerCase() !== "y") { );
printInfo("Cancelled."); });
process.exit(0); rl.close();
} if (answer.toLowerCase() !== "y") {
} printInfo("Cancelled.");
process.exit(0);
}
}
const start = performance.now(); const start = performance.now();
let result: Record<string, unknown>; let result: Record<string, unknown>;
try { try {
result = await timedStatus("Deleting entity...", async () => { result = await timedStatus("Deleting entity...", async () => {
return backend.deleteEntities({ return backend.deleteEntities({
userId: opts.userId, userId: opts.userId,
agentId: opts.agentId, agentId: opts.agentId,
appId: opts.appId, appId: opts.appId,
runId: opts.runId, runId: opts.runId,
}); });
}); });
} catch (e) { } catch (e) {
printError(e instanceof Error ? e.message : String(e)); printError(e instanceof Error ? e.message : String(e));
process.exit(1); process.exit(1);
} }
const elapsed = (performance.now() - start) / 1000; const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") { if (opts.output === "json") {
formatJson(result); formatJson(result);
} else if (opts.output !== "quiet") { } else if (opts.output !== "quiet") {
printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`); printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`);
} }
} }
+311 -285
View File
@@ -3,355 +3,381 @@
*/ */
import readline from "node:readline"; 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 { 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 { brand, dim } = colors;
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/; const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
function validateEmail(email: string): void { function validateEmail(email: string): void {
if (!EMAIL_RE.test(email)) { if (!EMAIL_RE.test(email)) {
printError(`Invalid email address: ${JSON.stringify(email)}`); printError(`Invalid email address: ${JSON.stringify(email)}`);
process.exit(1); process.exit(1);
} }
} }
async function emailLogin( async function emailLogin(
email: string, email: string,
code: string | undefined, code: string | undefined,
baseUrl: string, baseUrl: string,
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
const url = baseUrl.replace(/\/+$/, ""); const url = baseUrl.replace(/\/+$/, "");
let codeValue = code;
if (!code) { if (!codeValue) {
const resp = await fetch(`${url}/api/v1/auth/email_code/`, { const resp = await fetch(`${url}/api/v1/auth/email_code/`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }), body: JSON.stringify({ email }),
signal: AbortSignal.timeout(30_000), signal: AbortSignal.timeout(30_000),
}); });
if (resp.status === 429) { if (resp.status === 429) {
printError("Too many attempts. Try again in a few minutes."); printError("Too many attempts. Try again in a few minutes.");
process.exit(1); process.exit(1);
} }
if (!resp.ok) { if (!resp.ok) {
let detail: string; let detail: string;
try { try {
const body = (await resp.json()) as Record<string, unknown>; const body = (await resp.json()) as Record<string, unknown>;
detail = (body["error"] ?? body["detail"] ?? resp.statusText) as string; detail = (body.error ?? body.detail ?? resp.statusText) as string;
} catch { } catch {
detail = resp.statusText; detail = resp.statusText;
} }
printError(`Failed to send code: ${detail}`); printError(`Failed to send code: ${detail}`);
process.exit(1); process.exit(1);
} }
printSuccess("Verification code sent! Check your email."); printSuccess("Verification code sent! Check your email.");
if (!process.stdin.isTTY) { if (!process.stdin.isTTY) {
printError( printError(
"No --code provided and terminal is non-interactive.", "No --code provided and terminal is non-interactive.",
"Run: mem0 init --email <email> --code <code>", "Run: mem0 init --email <email> --code <code>",
); );
process.exit(1); process.exit(1);
} }
console.log(); console.log();
const entered = await promptLine(` ${brand("Verification Code")}`); const entered = await promptLine(` ${brand("Verification Code")}`);
if (!entered) { if (!entered) {
printError("Code is required."); printError("Code is required.");
process.exit(1); process.exit(1);
} }
code = entered; codeValue = entered;
} }
const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, { const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, code: code.trim() }), body: JSON.stringify({ email, code: codeValue.trim() }),
signal: AbortSignal.timeout(30_000), signal: AbortSignal.timeout(30_000),
}); });
if (verifyResp.status === 429) { if (verifyResp.status === 429) {
printError("Too many attempts. Try again in a few minutes."); printError("Too many attempts. Try again in a few minutes.");
process.exit(1); process.exit(1);
} }
if (!verifyResp.ok) { if (!verifyResp.ok) {
let detail: string; let detail: string;
try { try {
const body = (await verifyResp.json()) as Record<string, unknown>; const body = (await verifyResp.json()) as Record<string, unknown>;
detail = (body["error"] ?? body["detail"] ?? verifyResp.statusText) as string; detail = (body.error ?? body.detail ?? verifyResp.statusText) as string;
} catch { } catch {
detail = verifyResp.statusText; detail = verifyResp.statusText;
} }
printError(`Verification failed: ${detail}`); printError(`Verification failed: ${detail}`);
process.exit(1); 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> { function promptSecret(label: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
process.stdout.write(label); process.stdout.write(label);
if (process.stdin.isTTY) { if (process.stdin.isTTY) {
process.stdin.setRawMode(true); process.stdin.setRawMode(true);
} }
process.stdin.resume(); process.stdin.resume();
process.stdin.setEncoding("utf-8"); process.stdin.setEncoding("utf-8");
const chars: string[] = []; const chars: string[] = [];
const onData = (key: string) => { const onData = (key: string) => {
for (const ch of key) { for (const ch of key) {
if (ch === "\r" || ch === "\n") { if (ch === "\r" || ch === "\n") {
cleanup(); cleanup();
process.stdout.write("\n"); process.stdout.write("\n");
resolve(chars.join("")); resolve(chars.join(""));
return; return;
} }
if (ch === "\x03") { if (ch === "\x03") {
cleanup(); cleanup();
reject(new Error("Interrupted")); reject(new Error("Interrupted"));
return; return;
} }
if (ch === "\x7f" || ch === "\x08") { if (ch === "\x7f" || ch === "\x08") {
// backspace // backspace
if (chars.length > 0) { if (chars.length > 0) {
chars.pop(); chars.pop();
process.stdout.write("\b \b"); process.stdout.write("\b \b");
} }
} else if (ch === "\x15") { } else if (ch === "\x15") {
// Ctrl+U — clear line // Ctrl+U — clear line
process.stdout.write("\b \b".repeat(chars.length)); process.stdout.write("\b \b".repeat(chars.length));
chars.length = 0; chars.length = 0;
} else if (ch >= " ") { } else if (ch >= " ") {
chars.push(ch); chars.push(ch);
process.stdout.write("*"); process.stdout.write("*");
} }
} }
}; };
const cleanup = () => { const cleanup = () => {
process.stdin.removeListener("data", onData); process.stdin.removeListener("data", onData);
if (process.stdin.isTTY) { if (process.stdin.isTTY) {
process.stdin.setRawMode(false); process.stdin.setRawMode(false);
} }
process.stdin.pause(); process.stdin.pause();
}; };
process.stdin.on("data", onData); process.stdin.on("data", onData);
}); });
} }
function promptLine(label: string, defaultValue?: string): Promise<string> { function promptLine(label: string, defaultValue?: string): Promise<string> {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const rl = readline.createInterface({
const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `; input: process.stdin,
return new Promise((resolve) => { output: process.stdout,
rl.question(prompt, (answer) => { });
rl.close(); const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `;
resolve(answer.trim() || defaultValue || ""); return new Promise((resolve) => {
}); rl.question(prompt, (answer) => {
}); rl.close();
resolve(answer.trim() || defaultValue || "");
});
});
} }
async function setupPlatform(config: Mem0Config): Promise<void> { async function setupPlatform(config: Mem0Config): Promise<void> {
console.log(); console.log();
console.log(` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys")}`); 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")}: `); process.stdout.write(` ${brand("API Key")}: `);
const apiKey = await promptSecret(""); const apiKey = await promptSecret("");
if (!apiKey) { if (!apiKey) {
printError("API key is required."); printError("API key is required.");
process.exit(1); process.exit(1);
} }
config.platform.apiKey = apiKey; config.platform.apiKey = apiKey;
} }
async function setupDefaults(config: Mem0Config): Promise<void> { async function setupDefaults(config: Mem0Config): Promise<void> {
console.log(); console.log();
printInfo("Set default entity IDs (press Enter to skip).\n"); printInfo("Set default entity IDs (press Enter to skip).\n");
const userId = await promptLine(` ${brand("Default User ID")} ${dim("(recommended)")}`, "mem0-cli"); const userId = await promptLine(
if (userId) config.defaults.userId = userId; ` ${brand("Default User ID")} ${dim("(recommended)")}`,
"mem0-cli",
);
if (userId) config.defaults.userId = userId;
} }
async function validatePlatform(config: Mem0Config): Promise<void> { async function validatePlatform(config: Mem0Config): Promise<void> {
console.log(); console.log();
printInfo("Validating connection..."); printInfo("Validating connection...");
try { try {
const backend = new PlatformBackend(config.platform); const backend = new PlatformBackend(config.platform);
const status = await backend.status({ const status = await backend.status({
userId: config.defaults.userId || undefined, userId: config.defaults.userId || undefined,
agentId: config.defaults.agentId || undefined, agentId: config.defaults.agentId || undefined,
}); });
if (status.connected) { if (status.connected) {
printSuccess("Connected to mem0 Platform!"); printSuccess("Connected to mem0 Platform!");
} else { } else {
printError( printError(
`Could not connect: ${status.error ?? "Unknown error"}`, `Could not connect: ${status.error ?? "Unknown error"}`,
"Check your API key and try again.", "Check your API key and try again.",
); );
} }
} catch (e) { } catch (e) {
printError(`Connection test failed: ${e instanceof Error ? e.message : e}`); printError(`Connection test failed: ${e instanceof Error ? e.message : e}`);
} }
} }
export async function runInit( export async function runInit(
opts: { apiKey?: string; userId?: string; email?: string; code?: string } = {}, opts: {
apiKey?: string;
userId?: string;
email?: string;
code?: string;
} = {},
): Promise<void> { ): Promise<void> {
const config = createDefaultConfig(); const config = createDefaultConfig();
const savedConfig = loadConfig(); const savedConfig = loadConfig();
const baseUrl = const baseUrl =
process.env["MEM0_BASE_URL"] || savedConfig.platform.baseUrl || DEFAULT_BASE_URL; process.env.MEM0_BASE_URL ||
savedConfig.platform.baseUrl ||
DEFAULT_BASE_URL;
// Guards // Guards
if (opts.code && !opts.email) { if (opts.code && !opts.email) {
printError("--code requires --email."); printError("--code requires --email.");
process.exit(1); process.exit(1);
} }
if (opts.email && opts.apiKey) { if (opts.email && opts.apiKey) {
printError("Cannot use both --api-key and --email."); printError("Cannot use both --api-key and --email.");
process.exit(1); process.exit(1);
} }
// ── Email login flow ────────────────────────────────────────────────────── // ── Email login flow ──────────────────────────────────────────────────────
if (opts.email) { if (opts.email) {
const email = opts.email.trim().toLowerCase(); const email = opts.email.trim().toLowerCase();
validateEmail(email); validateEmail(email);
printBanner(); printBanner();
console.log(); console.log();
printInfo(`Logging in as ${email}...\n`); 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; const apiKeyVal = result.api_key as string | undefined;
if (!apiKeyVal) { if (!apiKeyVal) {
printError("Auth succeeded but no API key was returned. Contact support."); printError(
process.exit(1); "Auth succeeded but no API key was returned. Contact support.",
} );
process.exit(1);
}
config.platform.apiKey = apiKeyVal; config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl; config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli"; config.defaults.userId = opts.userId || "mem0-cli";
saveConfig(config); saveConfig(config);
console.log(); console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json"); printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log(); console.log();
console.log(` ${dim("Get started:")}`); console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`); console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`); console.log(` ${dim(' mem0 search "preferences"')}`);
console.log(); console.log();
return; return;
} }
// ── API key flow ────────────────────────────────────────────────────────── // ── API key flow ──────────────────────────────────────────────────────────
// Non-interactive: both flags provided // Non-interactive: both flags provided
if (opts.apiKey && opts.userId) { if (opts.apiKey && opts.userId) {
config.platform.apiKey = opts.apiKey; config.platform.apiKey = opts.apiKey;
config.defaults.userId = opts.userId; config.defaults.userId = opts.userId;
await validatePlatform(config); await validatePlatform(config);
saveConfig(config); saveConfig(config);
printSuccess("Configuration saved to ~/.mem0/config.json"); printSuccess("Configuration saved to ~/.mem0/config.json");
return; return;
} }
// Non-TTY without full flags: error with usage hint // Non-TTY without full flags: error with usage hint
if (!process.stdin.isTTY && (!opts.apiKey || !opts.userId)) { if (!process.stdin.isTTY && (!opts.apiKey || !opts.userId)) {
printError( printError(
"Non-interactive terminal detected and missing required flags.", "Non-interactive terminal detected and missing required flags.",
"Usage: mem0 init --api-key <key> --user-id <id>", "Usage: mem0 init --api-key <key> --user-id <id>",
); );
process.exit(1); process.exit(1);
} }
printBanner(); printBanner();
console.log(); console.log();
printInfo("Welcome! Let's set up your mem0 CLI.\n"); printInfo("Welcome! Let's set up your mem0 CLI.\n");
// Use provided API key or prompt // Use provided API key or prompt
if (opts.apiKey) { if (opts.apiKey) {
config.platform.apiKey = opts.apiKey; config.platform.apiKey = opts.apiKey;
} else { } else {
console.log(` ${brand("How would you like to authenticate?")}`); console.log(` ${brand("How would you like to authenticate?")}`);
console.log(` ${dim("1.")} Login with email ${dim("(recommended)")}`); console.log(` ${dim("1.")} Login with email ${dim("(recommended)")}`);
console.log(` ${dim("2.")} Enter API key manually`); console.log(` ${dim("2.")} Enter API key manually`);
console.log(); console.log();
const choice = await promptLine(` ${brand("Choose")} [1/2]`, "1"); const choice = await promptLine(` ${brand("Choose")} [1/2]`, "1");
if (choice === "1") { if (choice === "1") {
console.log(); console.log();
const emailAddr = await promptLine(` ${brand("Email")}`); const emailAddr = await promptLine(` ${brand("Email")}`);
if (!emailAddr) { if (!emailAddr) {
printError("Email is required."); printError("Email is required.");
process.exit(1); process.exit(1);
} }
const email = emailAddr.trim().toLowerCase(); const email = emailAddr.trim().toLowerCase();
validateEmail(email); validateEmail(email);
printInfo(`Logging in as ${email}...\n`); 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; const apiKeyVal = result.api_key as string | undefined;
if (!apiKeyVal) { if (!apiKeyVal) {
printError("Auth succeeded but no API key was returned. Contact support."); printError(
process.exit(1); "Auth succeeded but no API key was returned. Contact support.",
} );
process.exit(1);
}
config.platform.apiKey = apiKeyVal; config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl; config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli"; config.defaults.userId = opts.userId || "mem0-cli";
saveConfig(config); saveConfig(config);
console.log(); console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json"); printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log(); console.log();
console.log(` ${dim("Get started:")}`); console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`); console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`); console.log(` ${dim(' mem0 search "preferences"')}`);
console.log(); console.log();
return; return;
} }
// choice === "2": fall through to API key prompt // choice === "2": fall through to API key prompt
await setupPlatform(config); await setupPlatform(config);
} }
// Use provided user ID or prompt // Use provided user ID or prompt
if (opts.userId) { if (opts.userId) {
config.defaults.userId = opts.userId; config.defaults.userId = opts.userId;
} else { } else {
await setupDefaults(config); await setupDefaults(config);
} }
await validatePlatform(config); await validatePlatform(config);
saveConfig(config); saveConfig(config);
console.log(); console.log();
printSuccess("Configuration saved to ~/.mem0/config.json"); printSuccess("Configuration saved to ~/.mem0/config.json");
console.log(); console.log();
console.log(` ${dim("Get started:")}`); console.log(` ${dim("Get started:")}`);
if (config.defaults.userId) { if (config.defaults.userId) {
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`); console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`); console.log(` ${dim(' mem0 search "preferences"')}`);
} else { } else {
console.log(` ${dim(' mem0 add "I prefer dark mode" --user-id alice')}`); console.log(` ${dim(' mem0 add "I prefer dark mode" --user-id alice')}`);
console.log(` ${dim(' mem0 search "preferences" --user-id alice')}`); console.log(` ${dim(' mem0 search "preferences" --user-id alice')}`);
} }
console.log(); 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 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 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; const { brand, dim, success, error: errorColor } = colors;
export async function cmdStatus( export async function cmdStatus(
backend: Backend, backend: Backend,
opts: { userId?: string; agentId?: string; output?: string } = {}, opts: { userId?: string; agentId?: string; output?: string } = {},
): Promise<void> { ): Promise<void> {
const start = performance.now(); const start = performance.now();
let result: Record<string, unknown>; let result: Record<string, unknown>;
try { try {
result = await timedStatus("Checking connection...", async () => { result = await timedStatus("Checking connection...", async () => {
return backend.status({ userId: opts.userId, agentId: opts.agentId }); return backend.status({ userId: opts.userId, agentId: opts.agentId });
}); });
} catch (e) { } catch (e) {
result = { connected: false, error: e instanceof Error ? e.message : String(e) }; result = {
} connected: false,
const elapsed = (performance.now() - start) / 1000; error: e instanceof Error ? e.message : String(e),
};
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") { if (opts.output === "json") {
formatJsonEnvelope({ formatJsonEnvelope({
command: "status", command: "status",
data: { data: {
connected: result.connected, connected: result.connected,
backend: result.backend ?? null, backend: result.backend ?? null,
base_url: result.base_url ?? null, base_url: result.base_url ?? null,
latency_ms: Math.round(elapsed * 1000), latency_ms: Math.round(elapsed * 1000),
}, },
durationMs: Math.round(elapsed * 1000), durationMs: Math.round(elapsed * 1000),
}); });
return; return;
} }
const lines: string[] = []; const lines: string[] = [];
if (result.connected) { if (result.connected) {
lines.push(` ${success("\u25cf")} Connected`); lines.push(` ${success("\u25cf")} Connected`);
} else { } else {
lines.push(` ${errorColor("\u25cf")} Disconnected`); lines.push(` ${errorColor("\u25cf")} Disconnected`);
} }
lines.push(` ${dim("Backend:")} ${result.backend ?? "?"}`); lines.push(` ${dim("Backend:")} ${result.backend ?? "?"}`);
if (result.base_url) { if (result.base_url) {
lines.push(` ${dim("API URL:")} ${result.base_url}`); lines.push(` ${dim("API URL:")} ${result.base_url}`);
} }
if (result.error) { if (result.error) {
lines.push(` ${errorColor("Error:")} ${result.error}`); lines.push(` ${errorColor("Error:")} ${result.error}`);
} }
lines.push(` ${dim("Latency:")} ${elapsed.toFixed(2)}s`); lines.push(` ${dim("Latency:")} ${elapsed.toFixed(2)}s`);
const content = lines.join("\n"); const content = lines.join("\n");
console.log(); console.log();
console.log( console.log(
boxen(content, { boxen(content, {
title: brand("Connection Status"), title: brand("Connection Status"),
titleAlignment: "left", titleAlignment: "left",
borderColor: "magenta", borderColor: "magenta",
padding: 1, padding: 1,
}), }),
); );
console.log(); console.log();
} }
export function cmdVersion(): void { export function cmdVersion(): void {
console.log(` ${brand("◆ Mem0")} CLI v${__CLI_VERSION__}`); console.log(` ${brand("◆ Mem0")} CLI v${CLI_VERSION}`);
} }
export async function cmdImport( export async function cmdImport(
backend: Backend, backend: Backend,
filePath: string, filePath: string,
opts: { userId?: string; agentId?: string; output?: string }, opts: { userId?: string; agentId?: string; output?: string },
): Promise<void> { ): Promise<void> {
let data: Record<string, unknown>[]; let data: Record<string, unknown>[];
try { try {
const raw = fs.readFileSync(filePath, "utf-8"); const raw = fs.readFileSync(filePath, "utf-8");
const parsed = JSON.parse(raw); const parsed = JSON.parse(raw);
data = Array.isArray(parsed) ? parsed : [parsed]; data = Array.isArray(parsed) ? parsed : [parsed];
} catch (e) { } catch (e) {
printError(`Failed to read file: ${e instanceof Error ? e.message : e}`); printError(`Failed to read file: ${e instanceof Error ? e.message : e}`);
process.exit(1); process.exit(1);
} }
let added = 0; let added = 0;
let failed = 0; let failed = 0;
const start = performance.now(); const start = performance.now();
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
const item = data[i]; const item = data[i];
const content = (item.memory ?? item.text ?? item.content ?? "") as string; const content = (item.memory ?? item.text ?? item.content ?? "") as string;
if (!content) { if (!content) {
failed++; failed++;
continue; continue;
} }
try { try {
await backend.add(content, undefined, { await backend.add(content, undefined, {
userId: opts.userId ?? (item.user_id as string | undefined), userId: opts.userId ?? (item.user_id as string | undefined),
agentId: opts.agentId ?? (item.agent_id as string | undefined), agentId: opts.agentId ?? (item.agent_id as string | undefined),
metadata: item.metadata as Record<string, unknown> | undefined, metadata: item.metadata as Record<string, unknown> | undefined,
}); });
added++; added++;
} catch { } catch {
failed++; failed++;
} }
// Simple progress indicator // Simple progress indicator
if ((i + 1) % 10 === 0 || i === data.length - 1) { if ((i + 1) % 10 === 0 || i === data.length - 1) {
process.stdout.write(`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`); process.stdout.write(
} `\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`,
} );
}
}
const elapsed = (performance.now() - start) / 1000; const elapsed = (performance.now() - start) / 1000;
console.log(); // Clear progress line console.log(); // Clear progress line
if (opts.output === "json") { if (opts.output === "json") {
formatJsonEnvelope({ formatJsonEnvelope({
command: "import", command: "import",
data: { added, failed, duration_s: parseFloat(elapsed.toFixed(2)) }, data: {
durationMs: Math.round(elapsed * 1000), added,
}); failed,
return; duration_s: Number.parseFloat(elapsed.toFixed(2)),
} },
durationMs: Math.round(elapsed * 1000),
});
return;
}
printSuccess(`Imported ${added} memories (${elapsed.toFixed(2)}s)`); printSuccess(`Imported ${added} memories (${elapsed.toFixed(2)}s)`);
if (failed > 0) { if (failed > 0) {
printError(`${failed} memories failed to import.`); 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 const CONFIG_VERSION = 1;
export interface PlatformConfig { export interface PlatformConfig {
apiKey: string; apiKey: string;
baseUrl: string; baseUrl: string;
} }
export interface DefaultsConfig { export interface DefaultsConfig {
userId: string; userId: string;
agentId: string; agentId: string;
appId: string; appId: string;
runId: string; runId: string;
enableGraph: boolean; enableGraph: boolean;
} }
export interface Mem0Config { export interface Mem0Config {
version: number; version: number;
defaults: DefaultsConfig; defaults: DefaultsConfig;
platform: PlatformConfig; platform: PlatformConfig;
} }
export function createDefaultConfig(): Mem0Config { export function createDefaultConfig(): Mem0Config {
return { return {
version: CONFIG_VERSION, version: CONFIG_VERSION,
defaults: { defaults: {
userId: "", userId: "",
agentId: "", agentId: "",
appId: "", appId: "",
runId: "", runId: "",
enableGraph: false, enableGraph: false,
}, },
platform: { platform: {
apiKey: "", apiKey: "",
baseUrl: DEFAULT_BASE_URL, baseUrl: DEFAULT_BASE_URL,
}, },
}; };
} }
export function ensureConfigDir(): string { export function ensureConfigDir(): string {
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 }); fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
return CONFIG_DIR; return CONFIG_DIR;
} }
export function loadConfig(): Mem0Config { export function loadConfig(): Mem0Config {
const config = createDefaultConfig(); const config = createDefaultConfig();
if (fs.existsSync(CONFIG_FILE)) { if (fs.existsSync(CONFIG_FILE)) {
const raw = fs.readFileSync(CONFIG_FILE, "utf-8"); const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
const data = JSON.parse(raw); const data = JSON.parse(raw);
config.version = data.version ?? CONFIG_VERSION; config.version = data.version ?? CONFIG_VERSION;
const plat = data.platform ?? {}; const plat = data.platform ?? {};
config.platform.apiKey = plat.api_key ?? ""; config.platform.apiKey = plat.api_key ?? "";
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL; config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
const defaults = data.defaults ?? {}; const defaults = data.defaults ?? {};
config.defaults.userId = defaults.user_id ?? ""; config.defaults.userId = defaults.user_id ?? "";
config.defaults.agentId = defaults.agent_id ?? ""; config.defaults.agentId = defaults.agent_id ?? "";
config.defaults.appId = defaults.app_id ?? ""; config.defaults.appId = defaults.app_id ?? "";
config.defaults.runId = defaults.run_id ?? ""; config.defaults.runId = defaults.run_id ?? "";
config.defaults.enableGraph = defaults.enable_graph ?? false; config.defaults.enableGraph = defaults.enable_graph ?? false;
} }
// Environment variable overrides // Environment variable overrides
if (process.env.MEM0_API_KEY) config.platform.apiKey = process.env.MEM0_API_KEY; if (process.env.MEM0_API_KEY)
if (process.env.MEM0_BASE_URL) config.platform.baseUrl = process.env.MEM0_BASE_URL; config.platform.apiKey = process.env.MEM0_API_KEY;
if (process.env.MEM0_USER_ID) config.defaults.userId = process.env.MEM0_USER_ID; if (process.env.MEM0_BASE_URL)
if (process.env.MEM0_AGENT_ID) config.defaults.agentId = process.env.MEM0_AGENT_ID; config.platform.baseUrl = process.env.MEM0_BASE_URL;
if (process.env.MEM0_APP_ID) config.defaults.appId = process.env.MEM0_APP_ID; if (process.env.MEM0_USER_ID)
if (process.env.MEM0_RUN_ID) config.defaults.runId = process.env.MEM0_RUN_ID; config.defaults.userId = process.env.MEM0_USER_ID;
if (process.env.MEM0_ENABLE_GRAPH) { if (process.env.MEM0_AGENT_ID)
config.defaults.enableGraph = ["true", "1", "yes"].includes( config.defaults.agentId = process.env.MEM0_AGENT_ID;
process.env.MEM0_ENABLE_GRAPH.toLowerCase(), 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 { export function saveConfig(config: Mem0Config): void {
ensureConfigDir(); ensureConfigDir();
const data = { const data = {
version: config.version, version: config.version,
defaults: { defaults: {
user_id: config.defaults.userId, user_id: config.defaults.userId,
agent_id: config.defaults.agentId, agent_id: config.defaults.agentId,
app_id: config.defaults.appId, app_id: config.defaults.appId,
run_id: config.defaults.runId, run_id: config.defaults.runId,
enable_graph: config.defaults.enableGraph, enable_graph: config.defaults.enableGraph,
}, },
platform: { platform: {
api_key: config.platform.apiKey, api_key: config.platform.apiKey,
base_url: config.platform.baseUrl, base_url: config.platform.baseUrl,
}, },
}; };
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2)); fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
fs.chmodSync(CONFIG_FILE, 0o600); fs.chmodSync(CONFIG_FILE, 0o600);
} }
export function redactKey(key: string): string { export function redactKey(key: string): string {
if (!key) return "(not set)"; if (!key) return "(not set)";
if (key.length <= 8) return key.slice(0, 2) + "***"; if (key.length <= 8) return `${key.slice(0, 2)}***`;
return key.slice(0, 4) + "..." + key.slice(-4); return `${key.slice(0, 4)}...${key.slice(-4)}`;
} }
/** Key map from dotted config path to the config object fields. */ /** Key map from dotted config path to the config object fields. */
const KEY_MAP: Record<string, [keyof Mem0Config, string]> = { const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
"platform.api_key": ["platform", "apiKey"], "platform.api_key": ["platform", "apiKey"],
"platform.base_url": ["platform", "baseUrl"], "platform.base_url": ["platform", "baseUrl"],
"defaults.user_id": ["defaults", "userId"], "defaults.user_id": ["defaults", "userId"],
"defaults.agent_id": ["defaults", "agentId"], "defaults.agent_id": ["defaults", "agentId"],
"defaults.app_id": ["defaults", "appId"], "defaults.app_id": ["defaults", "appId"],
"defaults.run_id": ["defaults", "runId"], "defaults.run_id": ["defaults", "runId"],
"defaults.enable_graph": ["defaults", "enableGraph"], "defaults.enable_graph": ["defaults", "enableGraph"],
// Short-form aliases // Short-form aliases
"api_key": ["platform", "apiKey"], api_key: ["platform", "apiKey"],
"base_url": ["platform", "baseUrl"], base_url: ["platform", "baseUrl"],
"user_id": ["defaults", "userId"], user_id: ["defaults", "userId"],
"agent_id": ["defaults", "agentId"], agent_id: ["defaults", "agentId"],
"app_id": ["defaults", "appId"], app_id: ["defaults", "appId"],
"run_id": ["defaults", "runId"], run_id: ["defaults", "runId"],
"enable_graph": ["defaults", "enableGraph"], enable_graph: ["defaults", "enableGraph"],
}; };
export function getNestedValue(config: Mem0Config, dottedKey: string): unknown { export function getNestedValue(config: Mem0Config, dottedKey: string): unknown {
const mapping = KEY_MAP[dottedKey]; const mapping = KEY_MAP[dottedKey];
if (!mapping) return undefined; if (!mapping) return undefined;
const [section, field] = mapping; const [section, field] = mapping;
return (config[section] as unknown as Record<string, unknown>)[field]; return (config[section] as unknown as Record<string, unknown>)[field];
} }
export function setNestedValue(config: Mem0Config, dottedKey: string, value: string): boolean { export function setNestedValue(
const mapping = KEY_MAP[dottedKey]; config: Mem0Config,
if (!mapping) return false; dottedKey: string,
const [section, field] = mapping; value: string,
const obj = config[section] as unknown as Record<string, unknown>; ): 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]; const current = obj[field];
if (typeof current === "boolean") { if (typeof current === "boolean") {
obj[field] = ["true", "1", "yes"].includes(value.toLowerCase()); obj[field] = ["true", "1", "yes"].includes(value.toLowerCase());
} else if (typeof current === "number") { } else if (typeof current === "number") {
obj[field] = parseInt(value, 10); obj[field] = Number.parseInt(value, 10);
} else { } else {
obj[field] = value; obj[field] = value;
} }
return true; return true;
} }
+2 -2
View File
@@ -1,2 +1,2 @@
/** Injected by tsup at build time from package.json version field. */ /** Injected by tsup at build time from package.json version field. Undefined in dev/test. */
declare const __CLI_VERSION__: string; declare const __CLI_VERSION__: string | undefined;
+284 -280
View File
@@ -4,40 +4,40 @@
*/ */
import chalk from "chalk"; 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 imported from chalk directly to match Typer/Rich defaults
// ── Colors (matching Typer/Rich defaults) ──────────────────────────────── // ── Colors (matching Typer/Rich defaults) ────────────────────────────────
const cyanBold = chalk.cyan.bold; // option flags, command names const cyanBold = chalk.cyan.bold; // option flags, command names
const greenBold = chalk.green.bold; // switch flags (boolean --force etc) const greenBold = chalk.green.bold; // switch flags (boolean --force etc)
const yellowBold = chalk.yellow.bold; // metavar <value> const yellowBold = chalk.yellow.bold; // metavar <value>
const yellow = chalk.yellow; // "Usage:" label const yellow = chalk.yellow; // "Usage:" label
const bold = chalk.bold; // command name in usage const bold = chalk.bold; // command name in usage
const dim = chalk.dim; // defaults, descriptions const dim = chalk.dim; // defaults, descriptions
const dimBorder = chalk.dim; // panel borders const dimBorder = chalk.dim; // panel borders
// ── Strip ANSI ─────────────────────────────────────────────────────────── // ── 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; const ANSI_RE = /\x1b\[[0-9;]*m/g;
function stripAnsi(str: string): number { function stripAnsi(str: string): number {
return str.replace(ANSI_RE, "").length; return str.replace(ANSI_RE, "").length;
} }
// ── Command display order (matches Python CLI) ────────────────────────── // ── Command display order (matches Python CLI) ──────────────────────────
/** Commands grouped into panels, matching Python CLI's rich_help_panel. */ /** Commands grouped into panels, matching Python CLI's rich_help_panel. */
const COMMAND_GROUPS: { panel: string; commands: string[] }[] = [ const COMMAND_GROUPS: { panel: string; commands: string[] }[] = [
{ {
panel: "Memory", panel: "Memory",
commands: ["add", "search", "get", "list", "update", "delete"], commands: ["add", "search", "get", "list", "update", "delete"],
}, },
{ {
panel: "Management", panel: "Management",
commands: ["init", "status", "import", "help", "entity", "config"], commands: ["init", "status", "import", "help", "entity", "config"],
}, },
]; ];
/** Flat order derived from COMMAND_GROUPS. */ /** 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) ───── // ── Option-to-panel mapping (derived from Python's rich_help_panel) ─────
const OPTION_PANELS: Record<string, Record<string, string>> = { const OPTION_PANELS: Record<string, Record<string, string>> = {
add: { add: {
"--user-id": "Scope", "--user-id": "Scope",
"--agent-id": "Scope", "--agent-id": "Scope",
"--app-id": "Scope", "--app-id": "Scope",
"--run-id": "Scope", "--run-id": "Scope",
"--output": "Output", "--output": "Output",
"--api-key": "Connection", "--api-key": "Connection",
"--base-url": "Connection", "--base-url": "Connection",
}, },
search: { search: {
"--user-id": "Scope", "--user-id": "Scope",
"--agent-id": "Scope", "--agent-id": "Scope",
"--app-id": "Scope", "--app-id": "Scope",
"--run-id": "Scope", "--run-id": "Scope",
"--top-k": "Search", "--top-k": "Search",
"--threshold": "Search", "--threshold": "Search",
"--rerank": "Search", "--rerank": "Search",
"--keyword": "Search", "--keyword": "Search",
"--filter": "Search", "--filter": "Search",
"--fields": "Search", "--fields": "Search",
"--graph": "Search", "--graph": "Search",
"--no-graph": "Search", "--no-graph": "Search",
"--output": "Output", "--output": "Output",
"--api-key": "Connection", "--api-key": "Connection",
"--base-url": "Connection", "--base-url": "Connection",
}, },
get: { get: {
"--output": "Output", "--output": "Output",
"--api-key": "Connection", "--api-key": "Connection",
"--base-url": "Connection", "--base-url": "Connection",
}, },
list: { list: {
"--user-id": "Scope", "--user-id": "Scope",
"--agent-id": "Scope", "--agent-id": "Scope",
"--app-id": "Scope", "--app-id": "Scope",
"--run-id": "Scope", "--run-id": "Scope",
"--page": "Pagination", "--page": "Pagination",
"--page-size": "Pagination", "--page-size": "Pagination",
"--category": "Filters", "--category": "Filters",
"--after": "Filters", "--after": "Filters",
"--before": "Filters", "--before": "Filters",
"--graph": "Filters", "--graph": "Filters",
"--no-graph": "Filters", "--no-graph": "Filters",
"--output": "Output", "--output": "Output",
"--api-key": "Connection", "--api-key": "Connection",
"--base-url": "Connection", "--base-url": "Connection",
}, },
update: { update: {
"--output": "Output", "--output": "Output",
"--api-key": "Connection", "--api-key": "Connection",
"--base-url": "Connection", "--base-url": "Connection",
}, },
delete: { delete: {
"--user-id": "Scope", "--user-id": "Scope",
"--agent-id": "Scope", "--agent-id": "Scope",
"--app-id": "Scope", "--app-id": "Scope",
"--run-id": "Scope", "--run-id": "Scope",
"--output": "Output", "--output": "Output",
"--api-key": "Connection", "--api-key": "Connection",
"--base-url": "Connection", "--base-url": "Connection",
}, },
status: { status: {
"--output": "Output", "--output": "Output",
"--api-key": "Connection", "--api-key": "Connection",
"--base-url": "Connection", "--base-url": "Connection",
}, },
import: { import: {
"--user-id": "Scope", "--user-id": "Scope",
"--agent-id": "Scope", "--agent-id": "Scope",
"--output": "Output", "--output": "Output",
"--api-key": "Connection", "--api-key": "Connection",
"--base-url": "Connection", "--base-url": "Connection",
}, },
}; };
const PANEL_ORDER: string[] = [ const PANEL_ORDER: string[] = [
"Scope", "Scope",
"Search", "Search",
"Pagination", "Pagination",
"Filters", "Filters",
"Output", "Output",
"Connection", "Connection",
]; ];
// ── Panel rendering ───────────────────────────────────────────────────── // ── Panel rendering ─────────────────────────────────────────────────────
@@ -141,234 +141,238 @@ const PANEL_ORDER: string[] = [
* ╰────────────────────────────────╯ * ╰────────────────────────────────╯
* ``` * ```
*/ */
function renderPanel( function renderPanel(title: string, rows: string[], width: number): string {
title: string, if (rows.length === 0) return "";
rows: string[],
width: number,
): string {
if (rows.length === 0) return "";
// Inner width is total width minus the two border chars // Inner width is total width minus the two border chars
const inner = width - 2; const inner = width - 2;
// Top border: ╭─ Title ─...─╮ // Top border: ╭─ Title ─...─╮
const titleStr = ` ${title} `; const titleStr = ` ${title} `;
const fillLen = Math.max(0, inner - 1 - titleStr.length); const fillLen = Math.max(0, inner - 1 - titleStr.length);
const topLine = const topLine =
dimBorder("╭─") + dimBorder("╭─") +
dimBorder(titleStr) + dimBorder(titleStr) +
dimBorder("─".repeat(fillLen)) + dimBorder("─".repeat(fillLen)) +
dimBorder("╮"); dimBorder("╮");
// Bottom border: ╰─...─╯ // Bottom border: ╰─...─╯
const bottomLine = dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯"); const bottomLine =
dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯");
// Content rows // Content rows
const contentLines = rows.map((row) => { const contentLines = rows.map((row) => {
const visLen = stripAnsi(row); const visLen = stripAnsi(row);
const pad = Math.max(0, inner - 1 - visLen); const pad = Math.max(0, inner - 1 - visLen);
return dimBorder("│") + " " + row + " ".repeat(pad) + dimBorder("│"); return `${dimBorder("│")} ${row}${" ".repeat(pad)}${dimBorder("│")}`;
}); });
return [topLine, ...contentLines, bottomLine].join("\n"); return [topLine, ...contentLines, bottomLine].join("\n");
} }
// ── Format an option term (short + long) ──────────────────────────────── // ── Format an option term (short + long) ────────────────────────────────
function formatOptionTerm(opt: Option): string { function formatOptionTerm(opt: Option): string {
const parts: string[] = []; const parts: string[] = [];
if (opt.short) parts.push(opt.short); if (opt.short) parts.push(opt.short);
if (opt.long) parts.push(opt.long); if (opt.long) parts.push(opt.long);
let term = parts.join(", "); let term = parts.join(", ");
// Append value placeholder for non-boolean options // Append value placeholder for non-boolean options
if (opt.flags) { if (opt.flags) {
const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/); const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/);
if (match) { if (match) {
term += " " + match[0]; term += ` ${match[0]}`;
} }
} }
return term; return term;
} }
// ── Get the long flag name for panel lookup ───────────────────────────── // ── Get the long flag name for panel lookup ─────────────────────────────
function getLongFlag(opt: Option): string { function getLongFlag(opt: Option): string {
if (opt.long) return opt.long; if (opt.long) return opt.long;
return opt.short || ""; return opt.short || "";
} }
// ── Format a default value ────────────────────────────────────────────── // ── Format a default value ──────────────────────────────────────────────
function formatDefault(opt: Option): string { function formatDefault(opt: Option): string {
if (opt.defaultValue !== undefined && opt.defaultValue !== false) { if (opt.defaultValue !== undefined && opt.defaultValue !== false) {
return dim(` [default: ${opt.defaultValue}]`); return dim(` [default: ${opt.defaultValue}]`);
} }
return ""; return "";
} }
// ── The main help formatter ───────────────────────────────────────────── // ── The main help formatter ─────────────────────────────────────────────
export function richFormatHelp(cmd: Command, helper: Help): string { export function richFormatHelp(cmd: Command, helper: Help): string {
const width = process.stdout.columns || 80; const width = process.stdout.columns || 80;
const lines: string[] = []; const lines: string[] = [];
const isRoot = !cmd.parent; const isRoot = !cmd.parent;
// ── Usage line ── // ── Usage line ──
const usage = helper.commandUsage(cmd); const usage = helper.commandUsage(cmd);
lines.push(""); lines.push("");
if (isRoot) { if (isRoot) {
// Root: "Usage: mem0 <command> [options]" — <command> yellow, [options] bold // Root: "Usage: mem0 <command> [options]" — <command> yellow, [options] bold
lines.push(` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`); lines.push(
} else { ` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`,
// Subcommands: split into command path (bold) and args (yellow) );
const usageParts = usage.split(" "); } else {
const cmdPath: string[] = []; // Subcommands: split into command path (bold) and args (yellow)
const argParts: string[] = []; const usageParts = usage.split(" ");
let pastCmd = false; const cmdPath: string[] = [];
for (const part of usageParts) { const argParts: string[] = [];
if (!pastCmd && !part.startsWith("[") && !part.startsWith("<")) { let pastCmd = false;
cmdPath.push(part); for (const part of usageParts) {
} else { if (!pastCmd && !part.startsWith("[") && !part.startsWith("<")) {
pastCmd = true; cmdPath.push(part);
argParts.push(part); } else {
} pastCmd = true;
} argParts.push(part);
lines.push(` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`); }
} }
lines.push(""); lines.push(
` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`,
);
}
lines.push("");
// ── Description ── // ── Description ──
const desc = helper.commandDescription(cmd); const desc = helper.commandDescription(cmd);
if (desc) { if (desc) {
// Split multi-line descriptions (e.g., title + tagline) // Split multi-line descriptions (e.g., title + tagline)
const descLines = desc.split("\n"); const descLines = desc.split("\n");
for (let i = 0; i < descLines.length; i++) { for (let i = 0; i < descLines.length; i++) {
const dLine = descLines[i]; const dLine = descLines[i];
// First line is the title, subsequent non-empty lines are tagline (dimmed) // First line is the title, subsequent non-empty lines are tagline (dimmed)
if (i === 0 || dLine.trim() === "") { if (i === 0 || dLine.trim() === "") {
lines.push(` ${dLine}`); lines.push(` ${dLine}`);
} else { } else {
lines.push(` ${dim(dLine)}`); lines.push(` ${dim(dLine)}`);
} }
} }
lines.push(""); lines.push("");
} }
// ── Arguments panel (subcommands only) ── // ── Arguments panel (subcommands only) ──
if (!isRoot) { if (!isRoot) {
const visibleArgs = helper.visibleArguments(cmd); const visibleArgs = helper.visibleArguments(cmd);
if (visibleArgs.length > 0) { if (visibleArgs.length > 0) {
const maxLen = Math.max(...visibleArgs.map((a: Argument) => a.name().length)); const maxLen = Math.max(
const argRows = visibleArgs.map((a: Argument) => { ...visibleArgs.map((a: Argument) => a.name().length),
const name = cyanBold(a.name().padEnd(maxLen)); );
const description = helper.argumentDescription(a); const argRows = visibleArgs.map((a: Argument) => {
return ` ${name} ${description}`; const name = cyanBold(a.name().padEnd(maxLen));
}); const description = helper.argumentDescription(a);
const panel = renderPanel("Arguments", argRows, width); return ` ${name} ${description}`;
if (panel) lines.push(panel); });
} const panel = renderPanel("Arguments", argRows, width);
} if (panel) lines.push(panel);
}
}
// ── Collect options (grouped into panels for subcommands) ── // ── Collect options (grouped into panels for subcommands) ──
const visibleOpts = helper.visibleOptions(cmd); const visibleOpts = helper.visibleOptions(cmd);
const cmdName = cmd.name(); const cmdName = cmd.name();
const panelMap = (!isRoot && OPTION_PANELS[cmdName]) ? OPTION_PANELS[cmdName] : {}; const panelMap =
!isRoot && OPTION_PANELS[cmdName] ? OPTION_PANELS[cmdName] : {};
const grouped: Record<string, Option[]> = { Options: [] }; const grouped: Record<string, Option[]> = { Options: [] };
for (const panelName of PANEL_ORDER) { for (const panelName of PANEL_ORDER) {
grouped[panelName] = []; grouped[panelName] = [];
} }
for (const opt of visibleOpts) { for (const opt of visibleOpts) {
const flag = getLongFlag(opt); const flag = getLongFlag(opt);
const panel = panelMap[flag]; const panel = panelMap[flag];
if (panel && PANEL_ORDER.includes(panel)) { if (panel && PANEL_ORDER.includes(panel)) {
grouped[panel].push(opt); grouped[panel].push(opt);
} else { } else {
grouped["Options"].push(opt); grouped.Options.push(opt);
} }
} }
// ── Collect commands ── // ── Collect commands ──
const visibleCmds = helper.visibleCommands(cmd); const visibleCmds = helper.visibleCommands(cmd);
if (isRoot) { if (isRoot) {
// ROOT: Options first, then command groups (matches Python/Typer ordering) // ROOT: Options first, then command groups (matches Python/Typer ordering)
if (grouped["Options"].length > 0) { if (grouped.Options.length > 0) {
const optRows = formatOptionRows(grouped["Options"]); const optRows = formatOptionRows(grouped.Options);
const panel = renderPanel("Options", optRows, width); const panel = renderPanel("Options", optRows, width);
if (panel) lines.push(panel); if (panel) lines.push(panel);
} }
if (visibleCmds.length > 0) { if (visibleCmds.length > 0) {
const cmdMap = new Map(visibleCmds.map((c) => [c.name(), c])); const cmdMap = new Map(visibleCmds.map((c) => [c.name(), c]));
for (const group of COMMAND_GROUPS) { for (const group of COMMAND_GROUPS) {
const groupCmds = group.commands const groupCmds = group.commands
.map((name) => cmdMap.get(name)) .map((name) => cmdMap.get(name))
.filter((c): c is Command => c !== undefined); .filter((c): c is Command => c !== undefined);
if (groupCmds.length === 0) continue; if (groupCmds.length === 0) continue;
const maxLen = Math.max(...groupCmds.map((c) => c.name().length)); const maxLen = Math.max(...groupCmds.map((c) => c.name().length));
const cmdRows = groupCmds.map((c) => { const cmdRows = groupCmds.map((c) => {
const name = cyanBold(c.name().padEnd(maxLen)); const name = cyanBold(c.name().padEnd(maxLen));
const description = helper.subcommandDescription(c); const description = helper.subcommandDescription(c);
return ` ${name} ${description}`; return ` ${name} ${description}`;
}); });
const panel = renderPanel(group.panel, cmdRows, width); const panel = renderPanel(group.panel, cmdRows, width);
if (panel) lines.push(panel); if (panel) lines.push(panel);
} }
} }
} else { } else {
// SUBCOMMANDS: Options/panels first, then sub-subcommands // SUBCOMMANDS: Options/panels first, then sub-subcommands
const panelSequence = ["Options", ...PANEL_ORDER]; const panelSequence = ["Options", ...PANEL_ORDER];
for (const panelName of panelSequence) { for (const panelName of panelSequence) {
const opts = grouped[panelName]; const opts = grouped[panelName];
if (opts && opts.length > 0) { if (opts && opts.length > 0) {
const optRows = formatOptionRows(opts); const optRows = formatOptionRows(opts);
const panel = renderPanel(panelName, optRows, width); const panel = renderPanel(panelName, optRows, width);
if (panel) lines.push(panel); if (panel) lines.push(panel);
} }
} }
// Sub-subcommands (e.g., config show/get/set, entity list/delete) // Sub-subcommands (e.g., config show/get/set, entity list/delete)
if (visibleCmds.length > 0) { if (visibleCmds.length > 0) {
const maxLen = Math.max(...visibleCmds.map((c) => c.name().length)); const maxLen = Math.max(...visibleCmds.map((c) => c.name().length));
const cmdRows = visibleCmds.map((c) => { const cmdRows = visibleCmds.map((c) => {
const name = cyanBold(c.name().padEnd(maxLen)); const name = cyanBold(c.name().padEnd(maxLen));
const description = helper.subcommandDescription(c); const description = helper.subcommandDescription(c);
return ` ${name} ${description}`; return ` ${name} ${description}`;
}); });
const panel = renderPanel("Commands", cmdRows, width); const panel = renderPanel("Commands", cmdRows, width);
if (panel) lines.push(panel); if (panel) lines.push(panel);
} }
} }
lines.push(""); lines.push("");
return lines.join("\n"); return lines.join("\n");
} }
// ── Format option rows with aligned columns ───────────────────────────── // ── Format option rows with aligned columns ─────────────────────────────
function formatOptionRows(opts: Option[]): string[] { function formatOptionRows(opts: Option[]): string[] {
const terms = opts.map((o) => formatOptionTerm(o)); const terms = opts.map((o) => formatOptionTerm(o));
const maxTermLen = Math.max(...terms.map((t) => t.length)); const maxTermLen = Math.max(...terms.map((t) => t.length));
return opts.map((opt, i) => { return opts.map((opt, i) => {
const term = cyanBold(terms[i].padEnd(maxTermLen)); const term = cyanBold(terms[i].padEnd(maxTermLen));
const desc = opt.description || ""; const desc = opt.description || "";
const def = formatDefault(opt); const def = formatDefault(opt);
return ` ${term} ${desc}${def}`; return ` ${term} ${desc}${def}`;
}); });
} }
// ── Sort commands by COMMAND_ORDER ────────────────────────────────────── // ── Sort commands by COMMAND_ORDER ──────────────────────────────────────
function sortCommands(cmds: Command[]): Command[] { function sortCommands(cmds: Command[]): Command[] {
return [...cmds].sort((a, b) => { return [...cmds].sort((a, b) => {
const ai = COMMAND_ORDER.indexOf(a.name()); const ai = COMMAND_ORDER.indexOf(a.name());
const bi = COMMAND_ORDER.indexOf(b.name()); const bi = COMMAND_ORDER.indexOf(b.name());
// Unknown commands go to end, preserving original order // Unknown commands go to end, preserving original order
const aIdx = ai === -1 ? COMMAND_ORDER.length : ai; const aIdx = ai === -1 ? COMMAND_ORDER.length : ai;
const bIdx = bi === -1 ? COMMAND_ORDER.length : bi; const bIdx = bi === -1 ? COMMAND_ORDER.length : bi;
return aIdx - bIdx; 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. * Output formatting for mem0 CLI — text, JSON, table, quiet modes.
*/ */
import Table from "cli-table3";
import boxen from "boxen"; import boxen from "boxen";
import Table from "cli-table3";
import { colors, sym } from "./branding.js"; import { colors, sym } from "./branding.js";
const { brand, accent, success, error: errorColor, dim } = colors; const { brand, accent, success, error: errorColor, dim } = colors;
function formatDate(dtStr?: string): string | undefined { function formatDate(dtStr?: string): string | undefined {
if (!dtStr) return undefined; if (!dtStr) return undefined;
try { try {
const dt = new Date(dtStr.replace("Z", "+00:00")); const dt = new Date(dtStr.replace("Z", "+00:00"));
return dt.toISOString().slice(0, 10); return dt.toISOString().slice(0, 10);
} catch { } catch {
return dtStr?.slice(0, 10); return dtStr?.slice(0, 10);
} }
} }
export function formatMemoriesText( export function formatMemoriesText(
memories: Record<string, unknown>[], memories: Record<string, unknown>[],
title = "memories", title = "memories",
): void { ): void {
const count = memories.length; const count = memories.length;
console.log(`\n${brand(`Found ${count} ${title}:`)}\n`); console.log(`\n${brand(`Found ${count} ${title}:`)}\n`);
for (let i = 0; i < memories.length; i++) { for (let i = 0; i < memories.length; i++) {
const mem = memories[i]; const mem = memories[i];
const memoryText = (mem.memory ?? mem.text ?? "") as string; const memoryText = (mem.memory ?? mem.text ?? "") as string;
const memId = ((mem.id as string) ?? "").slice(0, 8); const memId = ((mem.id as string) ?? "").slice(0, 8);
const score = mem.score as number | undefined; const score = mem.score as number | undefined;
const created = formatDate(mem.created_at as string | undefined); const created = formatDate(mem.created_at as string | undefined);
let category: string | undefined; let category: string | undefined;
const cats = mem.categories; const cats = mem.categories;
if (Array.isArray(cats)) { if (Array.isArray(cats)) {
category = cats[0] as string | undefined; category = cats[0] as string | undefined;
} }
console.log(` ${i + 1}. ${memoryText}`); console.log(` ${i + 1}. ${memoryText}`);
const details: string[] = []; const details: string[] = [];
if (score !== undefined) details.push(`Score: ${score.toFixed(2)}`); if (score !== undefined) details.push(`Score: ${score.toFixed(2)}`);
if (memId) details.push(`ID: ${memId}`); if (memId) details.push(`ID: ${memId}`);
if (created) details.push(`Created: ${created}`); if (created) details.push(`Created: ${created}`);
if (category) details.push(`Category: ${category}`); if (category) details.push(`Category: ${category}`);
if (details.length > 0) { if (details.length > 0) {
console.log(` ${dim(details.join(" · "))}`); console.log(` ${dim(details.join(" · "))}`);
} }
console.log(); console.log();
} }
} }
export function formatMemoriesTable(memories: Record<string, unknown>[], opts: { showScore?: boolean } = {}): void { export function formatMemoriesTable(
const head = opts.showScore memories: Record<string, unknown>[],
? [accent("ID"), accent("Score"), accent("Memory"), accent("Category"), accent("Created")] opts: { showScore?: boolean } = {},
: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")]; ): void {
const colWidths = opts.showScore const head = opts.showScore
? [38, 8, 40, 16, 14] ? [
: [38, 40, 16, 14]; accent("ID"),
const table = new Table({ accent("Score"),
head, accent("Memory"),
colWidths, accent("Category"),
wordWrap: true, accent("Created"),
style: { head: [], border: [] }, ]
}); : [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) { for (const mem of memories) {
const memId = ((mem.id as string) ?? ""); const memId = (mem.id as string) ?? "";
let memoryText = (mem.memory ?? mem.text ?? "") as string; let memoryText = (mem.memory ?? mem.text ?? "") as string;
if (memoryText.length > 60) { if (memoryText.length > 60) {
memoryText = memoryText.slice(0, 57) + "..."; memoryText = `${memoryText.slice(0, 57)}...`;
} }
const categories = mem.categories; const categories = mem.categories;
const cat = const cat =
Array.isArray(categories) && categories.length > 0 Array.isArray(categories) && categories.length > 0
? categories.length > 1 ? categories.length > 1
? `${categories[0]} (+${categories.length - 1})` ? `${categories[0]} (+${categories.length - 1})`
: (categories[0] as string) : (categories[0] as string)
: "—"; : "—";
const created = formatDate(mem.created_at as string | undefined) ?? "—"; const created = formatDate(mem.created_at as string | undefined) ?? "—";
if (opts.showScore) { if (opts.showScore) {
const score = mem.score as number | undefined; const score = mem.score as number | undefined;
const scoreStr = score !== undefined ? score.toFixed(2) : "—"; const scoreStr = score !== undefined ? score.toFixed(2) : "—";
table.push([dim(memId), scoreStr, memoryText, cat, created]); table.push([dim(memId), scoreStr, memoryText, cat, created]);
} else { } else {
table.push([dim(memId), memoryText, cat, created]); table.push([dim(memId), memoryText, cat, created]);
} }
} }
console.log(); console.log();
console.log(table.toString()); console.log(table.toString());
console.log(); console.log();
} }
export function formatJson(data: unknown): void { export function formatJson(data: unknown): void {
console.log(JSON.stringify(data, null, 2)); console.log(JSON.stringify(data, null, 2));
} }
export function formatSingleMemory( export function formatSingleMemory(
mem: Record<string, unknown>, mem: Record<string, unknown>,
output = "text", output = "text",
): void { ): void {
if (output === "json") { if (output === "json") {
formatJson(mem); formatJson(mem);
return; return;
} }
const memoryText = (mem.memory ?? mem.text ?? "") as string; const memoryText = (mem.memory ?? mem.text ?? "") as string;
const memId = (mem.id ?? "") as string; const memId = (mem.id ?? "") as string;
const lines: string[] = []; const lines: string[] = [];
lines.push(` ${memoryText}`); lines.push(` ${memoryText}`);
lines.push(""); lines.push("");
if (memId) lines.push(` ${dim("ID:")} ${memId}`); if (memId) lines.push(` ${dim("ID:")} ${memId}`);
const created = formatDate(mem.created_at as string | undefined); const created = formatDate(mem.created_at as string | undefined);
if (created) lines.push(` ${dim("Created:")} ${created}`); if (created) lines.push(` ${dim("Created:")} ${created}`);
const updated = formatDate(mem.updated_at as string | undefined); const updated = formatDate(mem.updated_at as string | undefined);
if (updated) lines.push(` ${dim("Updated:")} ${updated}`); if (updated) lines.push(` ${dim("Updated:")} ${updated}`);
const meta = mem.metadata; const meta = mem.metadata;
if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`); if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`);
const categories = mem.categories; const categories = mem.categories;
if (categories) { if (categories) {
const catStr = Array.isArray(categories) ? categories.join(", ") : String(categories); const catStr = Array.isArray(categories)
lines.push(` ${dim("Categories:")} ${catStr}`); ? categories.join(", ")
} : String(categories);
lines.push(` ${dim("Categories:")} ${catStr}`);
}
const content = lines.join("\n"); const content = lines.join("\n");
console.log(); console.log();
console.log( console.log(
boxen(content, { boxen(content, {
title: brand("Memory"), title: brand("Memory"),
titleAlignment: "left", titleAlignment: "left",
borderColor: "magenta", borderColor: "magenta",
padding: 1, padding: 1,
}), }),
); );
console.log(); console.log();
} }
export function formatAddResult( export function formatAddResult(
result: Record<string, unknown> | Record<string, unknown>[], result: Record<string, unknown> | Record<string, unknown>[],
output = "text", output = "text",
): void { ): void {
if (output === "json") { if (output === "json") {
formatJson(result); formatJson(result);
return; return;
} }
if (output === "quiet") return; if (output === "quiet") return;
const results: Record<string, unknown>[] = Array.isArray(result) const results: Record<string, unknown>[] = Array.isArray(result)
? result ? result
: ((result.results as Record<string, unknown>[]) ?? [result]); : ((result.results as Record<string, unknown>[]) ?? [result]);
if (!results.length) { if (!results.length) {
console.log(` ${dim("No memories extracted.")}`); console.log(` ${dim("No memories extracted.")}`);
return; return;
} }
console.log(); console.log();
for (const r of results) { for (const r of results) {
// Detect async PENDING response // Detect async PENDING response
if (r.status === "PENDING") { if (r.status === "PENDING") {
const eventId = ((r.event_id as string) ?? "").slice(0, 8); const eventId = ((r.event_id as string) ?? "").slice(0, 8);
const icon = accent(sym("⧗", "...")); const icon = accent(sym("⧗", "..."));
const parts = [` ${icon} ${dim("Queued".padEnd(10))}`, "Processing in background"]; const parts = [
if (eventId) parts.push(dim(`(event ${eventId})`)); ` ${icon} ${dim("Queued".padEnd(10))}`,
console.log(parts.join(" ")); "Processing in background",
continue; ];
} if (eventId) parts.push(dim(`(event ${eventId})`));
console.log(parts.join(" "));
continue;
}
const event = (r.event ?? "ADD") as string; const event = (r.event ?? "ADD") as string;
const memory = (r.memory ?? r.text ?? r.content ?? r.data ?? "") 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 memId = ((r.id as string) ?? (r.memory_id as string) ?? "").slice(
0,
8,
);
let icon: string; let icon: string;
let label: string; let label: string;
if (event === "ADD") { if (event === "ADD") {
icon = success("+"); icon = success("+");
label = "Added"; label = "Added";
} else if (event === "UPDATE") { } else if (event === "UPDATE") {
icon = accent("~"); icon = accent("~");
label = "Updated"; label = "Updated";
} else if (event === "DELETE") { } else if (event === "DELETE") {
icon = errorColor("-"); icon = errorColor("-");
label = "Deleted"; label = "Deleted";
} else if (event === "NOOP") { } else if (event === "NOOP") {
icon = dim("·"); icon = dim("·");
label = "No change"; label = "No change";
} else { } else {
icon = dim("?"); icon = dim("?");
label = event; label = event;
} }
const parts = [` ${icon} ${dim(label.padEnd(10))}`]; const parts = [` ${icon} ${dim(label.padEnd(10))}`];
if (memory) parts.push(memory); if (memory) parts.push(memory);
if (memId) parts.push(dim(`(${memId})`)); if (memId) parts.push(dim(`(${memId})`));
console.log(parts.join(" ")); console.log(parts.join(" "));
} }
console.log(); console.log();
} }
export function formatJsonEnvelope(opts: { export function formatJsonEnvelope(opts: {
command: string; command: string;
data: unknown; data: unknown;
durationMs?: number; durationMs?: number;
scope?: Record<string, string | undefined>; scope?: Record<string, string | undefined>;
count?: number; count?: number;
status?: string; status?: string;
error?: string; error?: string;
}): void { }): void {
const envelope: Record<string, unknown> = { const envelope: Record<string, unknown> = {
status: opts.status ?? "success", status: opts.status ?? "success",
command: opts.command, command: opts.command,
}; };
if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs; if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs;
if (opts.scope !== undefined) envelope.scope = opts.scope; if (opts.scope !== undefined) envelope.scope = opts.scope;
if (opts.count !== undefined) envelope.count = opts.count; if (opts.count !== undefined) envelope.count = opts.count;
if (opts.error) envelope.error = opts.error; if (opts.error) envelope.error = opts.error;
envelope.data = opts.data; envelope.data = opts.data;
console.log(JSON.stringify(envelope, null, 2)); console.log(JSON.stringify(envelope, null, 2));
} }
export function printResultSummary(opts: { export function printResultSummary(opts: {
count: number; count: number;
durationSecs?: number; durationSecs?: number;
page?: number; page?: number;
scopeIds?: Record<string, string | undefined>; scopeIds?: Record<string, string | undefined>;
}): void { }): void {
const parts = [`${opts.count} result${opts.count !== 1 ? "s" : ""}`]; const parts = [`${opts.count} result${opts.count !== 1 ? "s" : ""}`];
if (opts.page !== undefined) parts.push(`page ${opts.page}`); if (opts.page !== undefined) parts.push(`page ${opts.page}`);
if (opts.scopeIds) { if (opts.scopeIds) {
const scopeParts = Object.entries(opts.scopeIds) const scopeParts = Object.entries(opts.scopeIds)
.filter(([, v]) => v) .filter(([, v]) => v)
.map(([k, v]) => `${k.replace(/_/g, " ")}=${v}`); .map(([k, v]) => `${k.replace(/_/g, " ")}=${v}`);
if (scopeParts.length > 0) parts.push(scopeParts.join(", ")); if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
} }
if (opts.durationSecs !== undefined) parts.push(`${opts.durationSecs.toFixed(2)}s`); if (opts.durationSecs !== undefined)
parts.push(`${opts.durationSecs.toFixed(2)}s`);
console.log(` ${dim(parts.join(" · "))}`); console.log(` ${dim(parts.join(" · "))}`);
console.log(); 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", () => { describe("printInfo", () => {
it("prints info message", () => { it("prints info message", () => {
printInfo("Important note"); printInfo("Important note");
expect(output).toContain("Important note"); expect(errOutput).toContain("Important note");
}); });
}); });
describe("printScope", () => { describe("printScope", () => {
it("prints scope when IDs present", () => { it("prints scope when IDs present", () => {
printScope({ user_id: "alice", agent_id: "bot" }); printScope({ user_id: "alice", agent_id: "bot" });
expect(output).toContain("alice"); expect(errOutput).toContain("alice");
expect(output).toContain("bot"); expect(errOutput).toContain("bot");
}); });
it("prints nothing when no IDs", () => { it("prints nothing when no IDs", () => {
printScope({}); printScope({});
expect(output).toBe(""); expect(errOutput).toBe("");
}); });
}); });
+2 -2
View File
@@ -125,7 +125,7 @@ describe("cmdSearch", () => {
enableGraph: false, enableGraph: false,
output: "text", output: "text",
}); });
expect(output).toContain("No memories found"); expect(errOutput).toContain("No memories found");
}); });
}); });
@@ -166,7 +166,7 @@ describe("cmdList", () => {
enableGraph: false, enableGraph: false,
output: "text", 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),
},
});
+36 -18
View File
@@ -253,8 +253,12 @@ def search(
help="Specific fields to return (comma-separated).", help="Specific fields to return (comma-separated).",
rich_help_panel="Search", rich_help_panel="Search",
), ),
graph: bool = typer.Option(False, "--graph", help="Enable graph in search.", rich_help_panel="Search"), graph: bool = typer.Option(
no_graph: bool = typer.Option(False, "--no-graph", help="Disable graph in search.", rich_help_panel="Search"), False, "--graph", help="Enable graph in search.", rich_help_panel="Search"
),
no_graph: bool = typer.Option(
False, "--no-graph", help="Disable graph in search.", rich_help_panel="Search"
),
output: str = typer.Option( output: str = typer.Option(
"text", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output" "text", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
), ),
@@ -372,8 +376,12 @@ def list_cmd(
before: str | None = typer.Option( before: str | None = typer.Option(
None, "--before", help="Created before (YYYY-MM-DD).", rich_help_panel="Filters" None, "--before", help="Created before (YYYY-MM-DD).", rich_help_panel="Filters"
), ),
graph: bool = typer.Option(False, "--graph", help="Enable graph in listing.", rich_help_panel="Filters"), graph: bool = typer.Option(
no_graph: bool = typer.Option(False, "--no-graph", help="Disable graph in listing.", rich_help_panel="Filters"), False, "--graph", help="Enable graph in listing.", rich_help_panel="Filters"
),
no_graph: bool = typer.Option(
False, "--no-graph", help="Disable graph in listing.", rich_help_panel="Filters"
),
output: str = typer.Option( output: str = typer.Option(
"table", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output" "table", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
), ),
@@ -463,11 +471,19 @@ def update(
@app.command(rich_help_panel="Memory") @app.command(rich_help_panel="Memory")
def delete( def delete(
memory_id: str | None = typer.Argument(None, help="Memory ID to delete (omit when using --all or --entity)."), memory_id: str | None = typer.Argument(
None, help="Memory ID to delete (omit when using --all or --entity)."
),
all_: bool = typer.Option(False, "--all", help="Delete all memories matching scope filters."), all_: bool = typer.Option(False, "--all", help="Delete all memories matching scope filters."),
entity: bool = typer.Option(False, "--entity", help="Delete the entity itself and all its memories (cascade)."), entity: bool = typer.Option(
project: bool = typer.Option(False, "--project", help="With --all: delete ALL memories project-wide."), False, "--entity", help="Delete the entity itself and all its memories (cascade)."
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting."), ),
project: bool = typer.Option(
False, "--project", help="With --all: delete ALL memories project-wide."
),
dry_run: bool = typer.Option(
False, "--dry-run", help="Show what would be deleted without deleting."
),
force: bool = typer.Option(False, "--force", help="Skip confirmation."), force: bool = typer.Option(False, "--force", help="Skip confirmation."),
user_id: str | None = typer.Option( user_id: str | None = typer.Option(
None, "--user-id", "-u", help="Scope to user.", rich_help_panel="Scope" None, "--user-id", "-u", help="Scope to user.", rich_help_panel="Scope"
@@ -641,14 +657,12 @@ def entity_delete(
agent_id: str | None = typer.Option( agent_id: str | None = typer.Option(
None, "--agent-id", help="Agent ID.", rich_help_panel="Scope" None, "--agent-id", help="Agent ID.", rich_help_panel="Scope"
), ),
app_id: str | None = typer.Option( app_id: str | None = typer.Option(None, "--app-id", help="App ID.", rich_help_panel="Scope"),
None, "--app-id", help="App ID.", rich_help_panel="Scope" run_id: str | None = typer.Option(None, "--run-id", help="Run ID.", rich_help_panel="Scope"),
),
run_id: str | None = typer.Option(
None, "--run-id", help="Run ID.", rich_help_panel="Scope"
),
force: bool = typer.Option(False, "--force", help="Skip confirmation."), force: bool = typer.Option(False, "--force", help="Skip confirmation."),
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting."), dry_run: bool = typer.Option(
False, "--dry-run", help="Show what would be deleted without deleting."
),
output: str = typer.Option( output: str = typer.Option(
"text", "--output", "-o", help="Output: text, json, quiet.", rich_help_panel="Output" "text", "--output", "-o", help="Output: text, json, quiet.", rich_help_panel="Output"
), ),
@@ -694,9 +708,13 @@ app.add_typer(entity_app, name="entity", rich_help_panel="Management")
@app.command(rich_help_panel="Management") @app.command(rich_help_panel="Management")
def init( def init(
api_key: str | None = typer.Option(None, "--api-key", help="API key (skip prompt)."), api_key: str | None = typer.Option(None, "--api-key", help="API key (skip prompt)."),
user_id: str | None = typer.Option(None, "--user-id", "-u", help="Default user ID (skip prompt)."), user_id: str | None = typer.Option(
None, "--user-id", "-u", help="Default user ID (skip prompt)."
),
email: str | None = typer.Option(None, "--email", help="Login via email verification code."), email: str | None = typer.Option(None, "--email", help="Login via email verification code."),
code: str | None = typer.Option(None, "--code", help="Verification code (use with --email for non-interactive login)."), code: str | None = typer.Option(
None, "--code", help="Verification code (use with --email for non-interactive login)."
),
) -> None: ) -> None:
"""Interactive setup wizard for mem0 CLI. """Interactive setup wizard for mem0 CLI.
@@ -747,7 +765,6 @@ def status(
) )
@app.command("import", rich_help_panel="Management") @app.command("import", rich_help_panel="Management")
def import_cmd( def import_cmd(
file_path: str = typer.Argument(..., help="JSON file to import."), file_path: str = typer.Argument(..., help="JSON file to import."),
@@ -1018,6 +1035,7 @@ def help(
def version() -> None: def version() -> None:
"""Show version and exit.""" """Show version and exit."""
from mem0_cli.commands.utils import cmd_version from mem0_cli.commands.utils import cmd_version
cmd_version() cmd_version()
-1
View File
@@ -105,7 +105,6 @@ class Backend(ABC):
def entities(self, entity_type: str) -> list[dict]: ... def entities(self, entity_type: str) -> list[dict]: ...
def get_backend(config: Mem0Config) -> Backend: def get_backend(config: Mem0Config) -> Backend:
"""Return the Platform backend.""" """Return the Platform backend."""
from mem0_cli.backend.platform import PlatformBackend from mem0_cli.backend.platform import PlatformBackend
@@ -312,7 +312,6 @@ class PlatformBackend(Backend):
return items return items
class AuthError(Exception): class AuthError(Exception):
pass pass
+6 -2
View File
@@ -28,7 +28,9 @@ def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> Non
"""List entities of a given type.""" """List entities of a given type."""
valid_types = {"users", "agents", "apps", "runs"} valid_types = {"users", "agents", "apps", "runs"}
if entity_type not in valid_types: if entity_type not in valid_types:
print_error(err_console, f"Invalid entity type: {entity_type}. Use: {', '.join(valid_types)}") print_error(
err_console, f"Invalid entity type: {entity_type}. Use: {', '.join(valid_types)}"
)
raise typer.Exit(1) raise typer.Exit(1)
_start = _time.perf_counter() _start = _time.perf_counter()
@@ -76,7 +78,9 @@ def cmd_entities_delete(
) -> None: ) -> None:
"""Delete an entity and all its memories (cascade delete).""" """Delete an entity and all its memories (cascade delete)."""
if not any([user_id, agent_id, app_id, run_id]): if not any([user_id, agent_id, app_id, run_id]):
print_error(err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id.") print_error(
err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id."
)
raise typer.Exit(1) raise typer.Exit(1)
if dry_run: if dry_run:
+3 -1
View File
@@ -265,7 +265,9 @@ def run_init(
api_key_val = result.get("api_key") api_key_val = result.get("api_key")
if not api_key_val: if not api_key_val:
print_error(err_console, "Auth succeeded but no API key was returned. Contact support.") print_error(
err_console, "Auth succeeded but no API key was returned. Contact support."
)
raise typer.Exit(1) raise typer.Exit(1)
config.platform.api_key = api_key_val config.platform.api_key = api_key_val
config.platform.base_url = base_url config.platform.base_url = base_url
+7 -2
View File
@@ -98,10 +98,14 @@ def cmd_add(
# Validate --expires # Validate --expires
if expires: if expires:
import re import re
if not re.match(r'^\d{4}-\d{2}-\d{2}$', expires):
print_error(err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).") if not re.match(r"^\d{4}-\d{2}-\d{2}$", expires):
print_error(
err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31)."
)
raise typer.Exit(1) raise typer.Exit(1)
from datetime import date from datetime import date
if date.fromisoformat(expires) <= date.today(): if date.fromisoformat(expires) <= date.today():
print_error(err_console, "--expires date must be in the future.") print_error(err_console, "--expires date must be in the future.")
raise typer.Exit(1) raise typer.Exit(1)
@@ -290,6 +294,7 @@ def cmd_list(
if output == "json": if output == "json":
from mem0_cli.output import format_json_envelope from mem0_cli.output import format_json_envelope
format_json_envelope( format_json_envelope(
console, console,
command="list", command="list",
+3 -1
View File
@@ -110,7 +110,9 @@ def cmd_import(
added = 0 added = 0
failed = 0 failed = 0
_start = _time.perf_counter() _start = _time.perf_counter()
for item in track(data, description=f"[{DIM_COLOR}]Importing memories...[/]", console=err_console): for item in track(
data, description=f"[{DIM_COLOR}]Importing memories...[/]", console=err_console
):
content = item.get("memory", item.get("text", item.get("content", ""))) content = item.get("memory", item.get("text", item.get("content", "")))
if not content: if not content:
failed += 1 failed += 1
+8 -2
View File
@@ -49,7 +49,9 @@ def format_memories_text(console: Console, memories: list[dict], title: str = "m
console.print() console.print()
def format_memories_table(console: Console, memories: list[dict], *, show_score: bool = False) -> None: def format_memories_table(
console: Console, memories: list[dict], *, show_score: bool = False
) -> None:
"""Render memories in a rich table.""" """Render memories in a rich table."""
table = Table( table = Table(
border_style=BRAND_COLOR, border_style=BRAND_COLOR,
@@ -71,7 +73,11 @@ def format_memories_table(console: Console, memories: list[dict], *, show_score:
memory_text = memory_text[:57] + "..." memory_text = memory_text[:57] + "..."
categories = mem.get("categories", []) categories = mem.get("categories", [])
if isinstance(categories, list) and categories: if isinstance(categories, list) and categories:
cat = categories[0] if len(categories) == 1 else f"{categories[0]} (+{len(categories) - 1})" cat = (
categories[0]
if len(categories) == 1
else f"{categories[0]} (+{len(categories) - 1})"
)
else: else:
cat = "—" cat = "—"
created = _format_date(mem.get("created_at")) or "—" created = _format_date(mem.get("created_at")) or "—"
+28 -3
View File
@@ -8,11 +8,19 @@ boundaries).
from __future__ import annotations from __future__ import annotations
import os import os
import re
import subprocess import subprocess
import sys import sys
import pytest import pytest
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[mKJHABCDfsu]")
def _strip_ansi(text: str) -> str:
"""Remove ANSI escape codes so substring checks work regardless of color mode."""
return _ANSI_RE.sub("", text)
def _run( def _run(
args: list[str], args: list[str],
@@ -28,22 +36,35 @@ def _run(
reads config from ``<home_dir>/.mem0/config.json`` instead reads config from ``<home_dir>/.mem0/config.json`` instead
of the user's real config. This is critical for tests that of the user's real config. This is critical for tests that
depend on a clean (no API key) or custom config state. depend on a clean (no API key) or custom config state.
Returns a CompletedProcess whose stdout/stderr have ANSI escape codes
stripped. GitHub Actions sets FORCE_COLOR=1 which causes Rich/Typer to
fragment option names like --user-id into separately-styled ANSI segments,
making plain ``in`` checks fail. Stripping here is version-agnostic and
ensures all assertions see the same plain text regardless of terminal env.
""" """
env = os.environ.copy() env = os.environ.copy()
# Strip all MEM0_ env vars so tests start clean # Strip all MEM0_ env vars so tests start clean
for key in list(env.keys()): for key in list(env.keys()):
if key.startswith("MEM0_"): if key.startswith("MEM0_"):
del env[key] del env[key]
env.pop("FORCE_COLOR", None)
if home_dir: if home_dir:
env["HOME"] = home_dir env["HOME"] = home_dir
if env_override: if env_override:
env.update(env_override) env.update(env_override)
return subprocess.run( result = subprocess.run(
[sys.executable, "-m", "mem0_cli", *args], [sys.executable, "-m", "mem0_cli", *args],
capture_output=True, capture_output=True,
text=True, text=True,
env=env, env=env,
) )
return subprocess.CompletedProcess(
args=result.args,
returncode=result.returncode,
stdout=_strip_ansi(result.stdout),
stderr=_strip_ansi(result.stderr),
)
@pytest.fixture @pytest.fixture
@@ -67,7 +88,6 @@ class TestCLIIntegration:
assert result.returncode == 0 assert result.returncode == 0
assert "0.1.0" in result.stdout assert "0.1.0" in result.stdout
def test_add_help(self): def test_add_help(self):
result = _run(["add", "--help"]) result = _run(["add", "--help"])
assert result.returncode == 0 assert result.returncode == 0
@@ -161,7 +181,12 @@ class TestCLIIsolated:
) )
assert result.returncode != 0 assert result.returncode != 0
combined = result.stderr + result.stdout combined = result.stderr + result.stdout
assert "memory ID" in combined.lower() or "--all" in combined or "--entity" in combined or "Error" in combined assert (
"memory ID" in combined.lower()
or "--all" in combined
or "--entity" in combined
or "Error" in combined
)
def test_config_show_clean(self, clean_home): def test_config_show_clean(self, clean_home):
"""config show with no config should still work.""" """config show with no config should still work."""
+9 -9
View File
@@ -62,7 +62,7 @@ class TestAddCommand:
metadata=None, metadata=None,
immutable=False, immutable=False,
no_infer=False, no_infer=False,
expires=None, expires=None,
categories=None, categories=None,
output="text", output="text",
) )
@@ -88,7 +88,7 @@ class TestAddCommand:
metadata=None, metadata=None,
immutable=False, immutable=False,
no_infer=False, no_infer=False,
expires=None, expires=None,
categories=None, categories=None,
output="text", output="text",
) )
@@ -113,7 +113,7 @@ class TestAddCommand:
metadata='{"source": "test"}', metadata='{"source": "test"}',
immutable=False, immutable=False,
no_infer=False, no_infer=False,
expires=None, expires=None,
categories=None, categories=None,
output="text", output="text",
) )
@@ -139,7 +139,7 @@ class TestAddCommand:
metadata=None, metadata=None,
immutable=False, immutable=False,
no_infer=False, no_infer=False,
expires=None, expires=None,
categories=None, categories=None,
output="json", output="json",
) )
@@ -165,7 +165,7 @@ class TestAddCommand:
metadata=None, metadata=None,
immutable=False, immutable=False,
no_infer=False, no_infer=False,
expires=None, expires=None,
categories=None, categories=None,
output="quiet", output="quiet",
) )
@@ -196,7 +196,7 @@ class TestAddCommand:
metadata=None, metadata=None,
immutable=False, immutable=False,
no_infer=False, no_infer=False,
expires=None, expires=None,
categories=None, categories=None,
output="text", output="text",
) )
@@ -221,7 +221,7 @@ class TestAddCommand:
metadata="not-json", metadata="not-json",
immutable=False, immutable=False,
no_infer=False, no_infer=False,
expires=None, expires=None,
categories=None, categories=None,
output="text", output="text",
) )
@@ -247,7 +247,7 @@ class TestAddCommand:
metadata=None, metadata=None,
immutable=False, immutable=False,
no_infer=False, no_infer=False,
expires=None, expires=None,
categories=None, categories=None,
output="text", output="text",
) )
@@ -272,7 +272,7 @@ class TestAddCommand:
metadata=None, metadata=None,
immutable=False, immutable=False,
no_infer=False, no_infer=False,
expires=None, expires=None,
categories="health,prefs", categories="health,prefs",
output="text", output="text",
) )