diff --git a/.github/workflows/cli-node-ci.yml b/.github/workflows/cli-node-ci.yml new file mode 100644 index 000000000..c4ebb4dc8 --- /dev/null +++ b/.github/workflows/cli-node-ci.yml @@ -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) diff --git a/.github/workflows/cli-python-ci.yml b/.github/workflows/cli-python-ci.yml new file mode 100644 index 000000000..1fbe6bf0e --- /dev/null +++ b/.github/workflows/cli-python-ci.yml @@ -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) diff --git a/cli/node/src/backend/base.ts b/cli/node/src/backend/base.ts index e2774f5db..a573a5974 100644 --- a/cli/node/src/backend/base.ts +++ b/cli/node/src/backend/base.ts @@ -6,110 +6,119 @@ import type { Mem0Config } from "../config.js"; import { PlatformBackend } from "./platform.js"; export interface AddOptions { - userId?: string; - agentId?: string; - appId?: string; - runId?: string; - metadata?: Record; - immutable?: boolean; - infer?: boolean; - expires?: string; - categories?: string[]; - enableGraph?: boolean; + userId?: string; + agentId?: string; + appId?: string; + runId?: string; + metadata?: Record; + immutable?: boolean; + infer?: boolean; + expires?: string; + categories?: string[]; + enableGraph?: boolean; } export interface SearchOptions { - userId?: string; - agentId?: string; - appId?: string; - runId?: string; - topK?: number; - threshold?: number; - rerank?: boolean; - keyword?: boolean; - filters?: Record; - fields?: string[]; - enableGraph?: boolean; + userId?: string; + agentId?: string; + appId?: string; + runId?: string; + topK?: number; + threshold?: number; + rerank?: boolean; + keyword?: boolean; + filters?: Record; + fields?: string[]; + enableGraph?: boolean; } export interface ListOptions { - userId?: string; - agentId?: string; - appId?: string; - runId?: string; - page?: number; - pageSize?: number; - category?: string; - after?: string; - before?: string; - enableGraph?: boolean; + userId?: string; + agentId?: string; + appId?: string; + runId?: string; + page?: number; + pageSize?: number; + category?: string; + after?: string; + before?: string; + enableGraph?: boolean; } export interface DeleteOptions { - all?: boolean; - userId?: string; - agentId?: string; - appId?: string; - runId?: string; + all?: boolean; + userId?: string; + agentId?: string; + appId?: string; + runId?: string; } export interface EntityIds { - userId?: string; - agentId?: string; - appId?: string; - runId?: string; + userId?: string; + agentId?: string; + appId?: string; + runId?: string; } export interface Backend { - add( - content?: string, - messages?: Record[], - opts?: AddOptions, - ): Promise>; + add( + content?: string, + messages?: Record[], + opts?: AddOptions, + ): Promise>; - search(query: string, opts?: SearchOptions): Promise[]>; + search( + query: string, + opts?: SearchOptions, + ): Promise[]>; - get(memoryId: string): Promise>; + get(memoryId: string): Promise>; - listMemories(opts?: ListOptions): Promise[]>; + listMemories(opts?: ListOptions): Promise[]>; - update( - memoryId: string, - content?: string, - metadata?: Record, - ): Promise>; + update( + memoryId: string, + content?: string, + metadata?: Record, + ): Promise>; - delete(memoryId?: string, opts?: DeleteOptions): Promise>; + delete( + memoryId?: string, + opts?: DeleteOptions, + ): Promise>; - deleteEntities(opts: EntityIds): Promise>; + deleteEntities(opts: EntityIds): Promise>; - status(opts?: { userId?: string; agentId?: string }): Promise>; + status(opts?: { userId?: string; agentId?: string }): Promise< + Record + >; - entities(entityType: string): Promise[]>; + entities(entityType: string): Promise[]>; } export class AuthError extends Error { - constructor(message = "Authentication failed. Your API key may be invalid or expired.") { - super(message); - this.name = "AuthError"; - } + constructor( + message = "Authentication failed. Your API key may be invalid or expired.", + ) { + super(message); + this.name = "AuthError"; + } } export class NotFoundError extends Error { - constructor(path: string) { - super(`Resource not found: ${path}`); - this.name = "NotFoundError"; - } + constructor(path: string) { + super(`Resource not found: ${path}`); + this.name = "NotFoundError"; + } } export class APIError extends Error { - constructor(path: string, detail: string) { - super(`Bad request to ${path}: ${detail}`); - this.name = "APIError"; - } + constructor(path: string, detail: string) { + super(`Bad request to ${path}: ${detail}`); + this.name = "APIError"; + } } export function getBackend(config: Mem0Config): Backend { - return new PlatformBackend(config.platform); + return new PlatformBackend(config.platform); } - diff --git a/cli/node/src/backend/index.ts b/cli/node/src/backend/index.ts index 74e896bc3..d22e20b95 100644 --- a/cli/node/src/backend/index.ts +++ b/cli/node/src/backend/index.ts @@ -3,5 +3,12 @@ */ export { getBackend } from "./base.js"; -export type { Backend, AddOptions, SearchOptions, ListOptions, DeleteOptions, EntityIds } from "./base.js"; +export type { + Backend, + AddOptions, + SearchOptions, + ListOptions, + DeleteOptions, + EntityIds, +} from "./base.js"; export { AuthError, NotFoundError, APIError } from "./base.js"; diff --git a/cli/node/src/backend/platform.ts b/cli/node/src/backend/platform.ts index cdef78b35..8256bc5c0 100644 --- a/cli/node/src/backend/platform.ts +++ b/cli/node/src/backend/platform.ts @@ -4,305 +4,335 @@ import type { PlatformConfig } from "../config.js"; import { - type AddOptions, - APIError, - AuthError, - type Backend, - type DeleteOptions, - type EntityIds, - type ListOptions, - NotFoundError, - type SearchOptions, + APIError, + type AddOptions, + AuthError, + type Backend, + type DeleteOptions, + type EntityIds, + type ListOptions, + NotFoundError, + type SearchOptions, } from "./base.js"; export class PlatformBackend implements Backend { - private baseUrl: string; - private headers: Record; + private baseUrl: string; + private headers: Record; - constructor(config: PlatformConfig) { - this.baseUrl = config.baseUrl.replace(/\/+$/, ""); - this.headers = { - Authorization: `Token ${config.apiKey}`, - "Content-Type": "application/json", - }; - } + constructor(config: PlatformConfig) { + this.baseUrl = config.baseUrl.replace(/\/+$/, ""); + this.headers = { + Authorization: `Token ${config.apiKey}`, + "Content-Type": "application/json", + }; + } - private async _request( - method: string, - path: string, - opts?: { json?: unknown; params?: Record }, - ): Promise { - let url = `${this.baseUrl}${path}`; - if (opts?.params) { - const qs = new URLSearchParams(opts.params).toString(); - url += `?${qs}`; - } + private async _request( + method: string, + path: string, + opts?: { json?: unknown; params?: Record }, + ): Promise { + let url = `${this.baseUrl}${path}`; + if (opts?.params) { + const qs = new URLSearchParams(opts.params).toString(); + url += `?${qs}`; + } - const fetchOpts: RequestInit = { - method, - headers: this.headers, - signal: AbortSignal.timeout(30_000), - }; - if (opts?.json) { - fetchOpts.body = JSON.stringify(opts.json); - } + const fetchOpts: RequestInit = { + method, + headers: this.headers, + signal: AbortSignal.timeout(30_000), + }; + if (opts?.json) { + fetchOpts.body = JSON.stringify(opts.json); + } - const resp = await fetch(url, fetchOpts); + const resp = await fetch(url, fetchOpts); - if (resp.status === 401) { - throw new AuthError(); - } - if (resp.status === 404) { - throw new NotFoundError(path); - } - if (resp.status === 400) { - let detail: string; - try { - const body = await resp.json() as Record; - detail = (body.detail ?? body.message ?? JSON.stringify(body)) as string ?? resp.statusText; - } catch { - detail = resp.statusText; - } - throw new APIError(path, detail); - } - if (!resp.ok) { - let detail: string = resp.statusText; - try { - const body = await resp.json() as Record; - detail = (body.detail ?? body.message ?? resp.statusText) as string; - } catch { /* ignore */ } - throw new Error(`HTTP ${resp.status}: ${detail}`); - } - if (resp.status === 204) { - return {}; - } - return resp.json(); - } + if (resp.status === 401) { + throw new AuthError(); + } + if (resp.status === 404) { + throw new NotFoundError(path); + } + if (resp.status === 400) { + let detail: string; + try { + const body = (await resp.json()) as Record; + detail = + ((body.detail ?? body.message ?? JSON.stringify(body)) as string) ?? + resp.statusText; + } catch { + detail = resp.statusText; + } + throw new APIError(path, detail); + } + if (!resp.ok) { + let detail: string = resp.statusText; + try { + const body = (await resp.json()) as Record; + detail = (body.detail ?? body.message ?? resp.statusText) as string; + } catch { + /* ignore */ + } + throw new Error(`HTTP ${resp.status}: ${detail}`); + } + if (resp.status === 204) { + return {}; + } + return resp.json(); + } - async add( - content?: string, - messages?: Record[], - opts: AddOptions = {}, - ): Promise> { - const payload: Record = {}; + async add( + content?: string, + messages?: Record[], + opts: AddOptions = {}, + ): Promise> { + const payload: Record = {}; - if (messages) { - payload.messages = messages; - } else if (content) { - payload.messages = [{ role: "user", content }]; - } + if (messages) { + payload.messages = messages; + } else if (content) { + payload.messages = [{ role: "user", content }]; + } - if (opts.userId) payload.user_id = opts.userId; - if (opts.agentId) payload.agent_id = opts.agentId; - if (opts.appId) payload.app_id = opts.appId; - if (opts.runId) payload.run_id = opts.runId; - if (opts.metadata) payload.metadata = opts.metadata; - if (opts.immutable) payload.immutable = true; - if (opts.infer === false) payload.infer = false; - if (opts.expires) payload.expiration_date = opts.expires; - if (opts.categories) payload.categories = opts.categories; - if (opts.enableGraph) payload.enable_graph = true; + if (opts.userId) payload.user_id = opts.userId; + if (opts.agentId) payload.agent_id = opts.agentId; + if (opts.appId) payload.app_id = opts.appId; + if (opts.runId) payload.run_id = opts.runId; + if (opts.metadata) payload.metadata = opts.metadata; + if (opts.immutable) payload.immutable = true; + if (opts.infer === false) payload.infer = false; + if (opts.expires) payload.expiration_date = opts.expires; + if (opts.categories) payload.categories = opts.categories; + if (opts.enableGraph) payload.enable_graph = true; - return (await this._request("POST", "/v1/memories/", { json: payload })) as Record< - string, - unknown - >; - } + return (await this._request("POST", "/v1/memories/", { + json: payload, + })) as Record; + } - private _buildFilters(opts: { - userId?: string; - agentId?: string; - appId?: string; - runId?: string; - extraFilters?: Record; - }): Record | undefined { - // If caller passed a pre-built filter structure, use it directly - if (opts.extraFilters && ("AND" in opts.extraFilters || "OR" in opts.extraFilters)) { - return opts.extraFilters; - } + private _buildFilters(opts: { + userId?: string; + agentId?: string; + appId?: string; + runId?: string; + extraFilters?: Record; + }): Record | undefined { + // If caller passed a pre-built filter structure, use it directly + if ( + opts.extraFilters && + ("AND" in opts.extraFilters || "OR" in opts.extraFilters) + ) { + return opts.extraFilters; + } - const andConditions: Record[] = []; - if (opts.userId) andConditions.push({ user_id: opts.userId }); - if (opts.agentId) andConditions.push({ agent_id: opts.agentId }); - if (opts.appId) andConditions.push({ app_id: opts.appId }); - if (opts.runId) andConditions.push({ run_id: opts.runId }); + const andConditions: Record[] = []; + if (opts.userId) andConditions.push({ user_id: opts.userId }); + if (opts.agentId) andConditions.push({ agent_id: opts.agentId }); + if (opts.appId) andConditions.push({ app_id: opts.appId }); + if (opts.runId) andConditions.push({ run_id: opts.runId }); - if (opts.extraFilters) { - for (const [k, v] of Object.entries(opts.extraFilters)) { - andConditions.push({ [k]: v }); - } - } + if (opts.extraFilters) { + for (const [k, v] of Object.entries(opts.extraFilters)) { + andConditions.push({ [k]: v }); + } + } - if (andConditions.length === 1) return andConditions[0]; - if (andConditions.length > 1) return { AND: andConditions }; - return undefined; - } + if (andConditions.length === 1) return andConditions[0]; + if (andConditions.length > 1) return { AND: andConditions }; + return undefined; + } - async search(query: string, opts: SearchOptions = {}): Promise[]> { - const payload: Record = { - query, - top_k: opts.topK ?? 10, - threshold: opts.threshold ?? 0.3, - }; + async search( + query: string, + opts: SearchOptions = {}, + ): Promise[]> { + const payload: Record = { + query, + top_k: opts.topK ?? 10, + threshold: opts.threshold ?? 0.3, + }; - const apiFilters = this._buildFilters({ - userId: opts.userId, - agentId: opts.agentId, - appId: opts.appId, - runId: opts.runId, - extraFilters: opts.filters, - }); - if (apiFilters) payload.filters = apiFilters; - if (opts.rerank) payload.rerank = true; - if (opts.keyword) payload.keyword_search = true; - if (opts.fields) payload.fields = opts.fields; - if (opts.enableGraph) payload.enable_graph = true; + const apiFilters = this._buildFilters({ + userId: opts.userId, + agentId: opts.agentId, + appId: opts.appId, + runId: opts.runId, + extraFilters: opts.filters, + }); + if (apiFilters) payload.filters = apiFilters; + if (opts.rerank) payload.rerank = true; + if (opts.keyword) payload.keyword_search = true; + if (opts.fields) payload.fields = opts.fields; + if (opts.enableGraph) payload.enable_graph = true; - const result = (await this._request("POST", "/v2/memories/search/", { - json: payload, - })) as unknown; - if (Array.isArray(result)) return result; - const obj = result as Record; - return (obj.results ?? obj.memories ?? []) as Record[]; - } + const result = (await this._request("POST", "/v2/memories/search/", { + json: payload, + })) as unknown; + if (Array.isArray(result)) return result; + const obj = result as Record; + return (obj.results ?? obj.memories ?? []) as Record[]; + } - async get(memoryId: string): Promise> { - return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record; - } + async get(memoryId: string): Promise> { + return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record< + string, + unknown + >; + } - async listMemories(opts: ListOptions = {}): Promise[]> { - const payload: Record = {}; - const params: Record = { - page: String(opts.page ?? 1), - page_size: String(opts.pageSize ?? 100), - }; + async listMemories( + opts: ListOptions = {}, + ): Promise[]> { + const payload: Record = {}; + const params: Record = { + page: String(opts.page ?? 1), + page_size: String(opts.pageSize ?? 100), + }; - const extra: Record = {}; - if (opts.category) { - extra.categories = { contains: opts.category }; - } - if (opts.after) { - extra.created_at = { ...(extra.created_at as Record | undefined), gte: opts.after }; - } - if (opts.before) { - extra.created_at = { ...(extra.created_at as Record | undefined), lte: opts.before }; - } + const extra: Record = {}; + if (opts.category) { + extra.categories = { contains: opts.category }; + } + if (opts.after) { + extra.created_at = { + ...(extra.created_at as Record | undefined), + gte: opts.after, + }; + } + if (opts.before) { + extra.created_at = { + ...(extra.created_at as Record | undefined), + lte: opts.before, + }; + } - const apiFilters = this._buildFilters({ - userId: opts.userId, - agentId: opts.agentId, - appId: opts.appId, - runId: opts.runId, - extraFilters: Object.keys(extra).length > 0 ? extra : undefined, - }); - if (apiFilters) payload.filters = apiFilters; - if (opts.enableGraph) payload.enable_graph = true; + const apiFilters = this._buildFilters({ + userId: opts.userId, + agentId: opts.agentId, + appId: opts.appId, + runId: opts.runId, + extraFilters: Object.keys(extra).length > 0 ? extra : undefined, + }); + if (apiFilters) payload.filters = apiFilters; + if (opts.enableGraph) payload.enable_graph = true; - const result = (await this._request("POST", "/v2/memories/", { json: payload, params })) as unknown; - if (Array.isArray(result)) return result; - const obj = result as Record; - return (obj.results ?? obj.memories ?? []) as Record[]; - } + const result = (await this._request("POST", "/v2/memories/", { + json: payload, + params, + })) as unknown; + if (Array.isArray(result)) return result; + const obj = result as Record; + return (obj.results ?? obj.memories ?? []) as Record[]; + } - async update( - memoryId: string, - content?: string, - metadata?: Record, - ): Promise> { - const payload: Record = {}; - if (content) payload.text = content; - if (metadata) payload.metadata = metadata; - return (await this._request("PUT", `/v1/memories/${memoryId}/`, { - json: payload, - })) as Record; - } + async update( + memoryId: string, + content?: string, + metadata?: Record, + ): Promise> { + const payload: Record = {}; + if (content) payload.text = content; + if (metadata) payload.metadata = metadata; + return (await this._request("PUT", `/v1/memories/${memoryId}/`, { + json: payload, + })) as Record; + } - async delete( - memoryId?: string, - opts: DeleteOptions = {}, - ): Promise> { - if (opts.all) { - const params: Record = {}; - if (opts.userId) params.user_id = opts.userId; - if (opts.agentId) params.agent_id = opts.agentId; - if (opts.appId) params.app_id = opts.appId; - if (opts.runId) params.run_id = opts.runId; - return (await this._request("DELETE", "/v1/memories/", { params })) as Record< - string, - unknown - >; - } - if (memoryId) { - return (await this._request("DELETE", `/v1/memories/${memoryId}/`)) as Record< - string, - unknown - >; - } - throw new Error("Either memoryId or --all is required"); - } + async delete( + memoryId?: string, + opts: DeleteOptions = {}, + ): Promise> { + if (opts.all) { + const params: Record = {}; + if (opts.userId) params.user_id = opts.userId; + if (opts.agentId) params.agent_id = opts.agentId; + if (opts.appId) params.app_id = opts.appId; + if (opts.runId) params.run_id = opts.runId; + return (await this._request("DELETE", "/v1/memories/", { + params, + })) as Record; + } + if (memoryId) { + return (await this._request( + "DELETE", + `/v1/memories/${memoryId}/`, + )) as Record; + } + throw new Error("Either memoryId or --all is required"); + } - async deleteEntities(opts: EntityIds): Promise> { - const params: Record = {}; - if (opts.userId) params.user_id = opts.userId; - if (opts.agentId) params.agent_id = opts.agentId; - if (opts.appId) params.app_id = opts.appId; - if (opts.runId) params.run_id = opts.runId; - if (Object.keys(params).length === 0) { - throw new Error("At least one entity ID is required for deleteEntities."); - } - return (await this._request("DELETE", "/v1/entities/", { params })) as Record< - string, - unknown - >; - } + async deleteEntities(opts: EntityIds): Promise> { + const params: Record = {}; + if (opts.userId) params.user_id = opts.userId; + if (opts.agentId) params.agent_id = opts.agentId; + if (opts.appId) params.app_id = opts.appId; + if (opts.runId) params.run_id = opts.runId; + if (Object.keys(params).length === 0) { + throw new Error("At least one entity ID is required for deleteEntities."); + } + return (await this._request("DELETE", "/v1/entities/", { + params, + })) as Record; + } - async status( - opts: { userId?: string; agentId?: string } = {}, - ): Promise> { - try { - if (opts.userId || opts.agentId) { - const payload: Record = {}; - const statusParams: Record = { page: "1", page_size: "1" }; - const apiFilters = this._buildFilters({ - userId: opts.userId, - agentId: opts.agentId, - }); - if (apiFilters) payload.filters = apiFilters; - await this._request("POST", "/v2/memories/", { json: payload, params: statusParams }); - } else { - await this._request("GET", "/v1/entities/"); - } - return { connected: true, backend: "platform", base_url: this.baseUrl }; - } catch (e) { - return { - connected: false, - backend: "platform", - error: e instanceof Error ? e.message : String(e), - }; - } - } + async status( + opts: { userId?: string; agentId?: string } = {}, + ): Promise> { + try { + if (opts.userId || opts.agentId) { + const payload: Record = {}; + const statusParams: Record = { + page: "1", + page_size: "1", + }; + const apiFilters = this._buildFilters({ + userId: opts.userId, + agentId: opts.agentId, + }); + if (apiFilters) payload.filters = apiFilters; + await this._request("POST", "/v2/memories/", { + json: payload, + params: statusParams, + }); + } else { + await this._request("GET", "/v1/entities/"); + } + return { connected: true, backend: "platform", base_url: this.baseUrl }; + } catch (e) { + return { + connected: false, + backend: "platform", + error: e instanceof Error ? e.message : String(e), + }; + } + } - async entities(entityType: string): Promise[]> { - const result = (await this._request("GET", "/v1/entities/")) as unknown; - let items: Record[]; - if (Array.isArray(result)) { - items = result; - } else { - items = ((result as Record).results ?? []) as Record[]; - } + async entities(entityType: string): Promise[]> { + const result = (await this._request("GET", "/v1/entities/")) as unknown; + let items: Record[]; + if (Array.isArray(result)) { + items = result; + } else { + items = ((result as Record).results ?? []) as Record< + string, + unknown + >[]; + } - const typeMap: Record = { - users: "user", - agents: "agent", - apps: "app", - runs: "run", - }; - const targetType = typeMap[entityType]; - if (targetType) { - items = items.filter( - (e) => (e.type as string | undefined)?.toLowerCase() === targetType, - ); - } - return items; - } + const typeMap: Record = { + users: "user", + agents: "agent", + apps: "app", + runs: "run", + }; + const targetType = typeMap[entityType]; + if (targetType) { + items = items.filter( + (e) => (e.type as string | undefined)?.toLowerCase() === targetType, + ); + } + return items; + } } diff --git a/cli/node/src/branding.ts b/cli/node/src/branding.ts index e60c5e3be..4025184b0 100644 --- a/cli/node/src/branding.ts +++ b/cli/node/src/branding.ts @@ -4,6 +4,7 @@ import chalk from "chalk"; import ora, { type Ora } from "ora"; +import { CLI_VERSION } from "./version.js"; export const LOGO = ` ███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗ @@ -36,75 +37,78 @@ const dim = chalk.hex(DIM_COLOR); * plain-text for piped/non-TTY or NO_COLOR environments. */ export function sym(fancy: string, plain: string): string { - if (!process.stdout.isTTY || process.env.NO_COLOR) return plain; - return fancy; + if (!process.stdout.isTTY || process.env.NO_COLOR) return plain; + return fancy; } export function printBanner(): void { - const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2)) - const logoLines = LOGO.trimEnd().split("\n"); - const tagline = ` ${TAGLINE}`; - const subtitle = `Node.js SDK · v${__CLI_VERSION__}`; - const contentLines = ["", ...logoLines, "", tagline, ""]; + const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2)) + const logoLines = LOGO.trimEnd().split("\n"); + const tagline = ` ${TAGLINE}`; + const subtitle = `Node.js SDK · v${CLI_VERSION}`; + const contentLines = ["", ...logoLines, "", tagline, ""]; - // Compute inner width from longest content line + padding both sides - const maxContent = Math.max(...contentLines.map((l) => l.length)); - const innerWidth = maxContent + pad * 2; - const totalWidth = innerWidth + 2; // + 2 for │ borders + // Compute inner width from longest content line + padding both sides + const maxContent = Math.max(...contentLines.map((l) => l.length)); + const innerWidth = maxContent + pad * 2; + const totalWidth = innerWidth + 2; // + 2 for │ borders - const topBorder = brand(`╭${"─".repeat(totalWidth - 2)}╮`); - const subtitleFill = totalWidth - 2 - subtitle.length - 3; // 3 = "─ " before subtitle + "─" after - const bottomBorder = brand(`╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`); + const topBorder = brand(`╭${"─".repeat(totalWidth - 2)}╮`); + const subtitleFill = totalWidth - 2 - subtitle.length - 3; // 3 = "─ " before subtitle + "─" after + const bottomBorder = brand( + `╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`, + ); - const body = contentLines.map((line) => { - const rightPad = innerWidth - pad - line.length; - return `${brand("│")}${" ".repeat(pad)}${brand.bold(line)}${" ".repeat(Math.max(rightPad, 0))}${brand("│")}`; - }); - // Re-color tagline line with accent instead of brand.bold - const taglineIdx = body.length - 2; // second-to-last (before trailing empty line) - const taglineRightPad = innerWidth - pad - tagline.length; - body[taglineIdx] = `${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`; + const body = contentLines.map((line) => { + const rightPad = innerWidth - pad - line.length; + return `${brand("│")}${" ".repeat(pad)}${brand.bold(line)}${" ".repeat(Math.max(rightPad, 0))}${brand("│")}`; + }); + // Re-color tagline line with accent instead of brand.bold + const taglineIdx = body.length - 2; // second-to-last (before trailing empty line) + const taglineRightPad = innerWidth - pad - tagline.length; + body[taglineIdx] = + `${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`; - console.log(topBorder); - for (const line of body) console.log(line); - console.log(bottomBorder); + console.log(topBorder); + for (const line of body) console.log(line); + console.log(bottomBorder); } export function printSuccess(message: string): void { - console.log(`${success(sym("✓", "[ok]"))} ${message}`); + console.log(`${success(sym("✓", "[ok]"))} ${message}`); } export function printError(message: string, hint?: string): void { - console.error(`${error(sym("✗", "[error]") + " Error:")} ${message}`); - if (hint) { - console.error(` ${dim(hint)}`); - } + console.error(`${error(`${sym("✗", "[error]")} Error:`)} ${message}`); + if (hint) { + console.error(` ${dim(hint)}`); + } } export function printWarning(message: string): void { - console.error(`${warning(sym("⚠", "[warn]"))} ${message}`); + console.error(`${warning(sym("⚠", "[warn]"))} ${message}`); } export function printInfo(message: string): void { - console.error(`${brand(sym("◆", "*"))} ${message}`); + console.error(`${brand(sym("◆", "*"))} ${message}`); } export function printScope(ids: Record): void { - const parts: string[] = []; - for (const [key, val] of Object.entries(ids)) { - if (val) { - const label = key.replace(/_/g, " ").replace("id", "ID").trim(); - parts.push(`${label}=${val}`); - } - } - if (parts.length > 0) { - console.error(` ${dim(`Scope: ${parts.join(", ")}`)}`); - } + const parts: string[] = []; + for (const [key, val] of Object.entries(ids)) { + if (val) { + const label = key.replace(/_/g, " ").replace("id", "ID").trim(); + parts.push(`${label}=${val}`); + } + } + if (parts.length > 0) { + console.error(` ${dim(`Scope: ${parts.join(", ")}`)}`); + } } export interface TimedStatusContext { - successMsg: string; - errorMsg: string; + successMsg: string; + errorMsg: string; } /** @@ -112,29 +116,33 @@ export interface TimedStatusContext { * Equivalent to Python's timed_status context manager. */ export async function timedStatus( - message: string, - fn: (ctx: TimedStatusContext) => Promise, + message: string, + fn: (ctx: TimedStatusContext) => Promise, ): Promise { - const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" }; - const spinner = ora({ text: dim(message), color: "magenta", stream: process.stderr }).start(); - const start = performance.now(); + const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" }; + const spinner = ora({ + text: dim(message), + color: "magenta", + stream: process.stderr, + }).start(); + const start = performance.now(); - try { - const result = await fn(ctx); - const elapsed = ((performance.now() - start) / 1000).toFixed(2); - spinner.stop(); - if (ctx.successMsg) { - console.error(`${success("✓")} ${ctx.successMsg} (${elapsed}s)`); - } - return result; - } catch (err) { - const elapsed = ((performance.now() - start) / 1000).toFixed(2); - spinner.stop(); - if (ctx.errorMsg) { - console.error(`${error("✗ Error:")} ${ctx.errorMsg} (${elapsed}s)`); - } - throw err; - } + try { + const result = await fn(ctx); + const elapsed = ((performance.now() - start) / 1000).toFixed(2); + spinner.stop(); + if (ctx.successMsg) { + console.error(`${success("✓")} ${ctx.successMsg} (${elapsed}s)`); + } + return result; + } catch (err) { + const elapsed = ((performance.now() - start) / 1000).toFixed(2); + spinner.stop(); + if (ctx.errorMsg) { + console.error(`${error("✗ Error:")} ${ctx.errorMsg} (${elapsed}s)`); + } + throw err; + } } /** Format helpers using brand colors for external use. */ diff --git a/cli/node/src/commands/config.ts b/cli/node/src/commands/config.ts index f45616bb9..797509ef2 100644 --- a/cli/node/src/commands/config.ts +++ b/cli/node/src/commands/config.ts @@ -3,88 +3,91 @@ */ import Table from "cli-table3"; -import { printError, printSuccess, colors } from "../branding.js"; +import { colors, printError, printSuccess } from "../branding.js"; import { - getNestedValue, - loadConfig, - redactKey, - saveConfig, - setNestedValue, + getNestedValue, + loadConfig, + redactKey, + saveConfig, + setNestedValue, } from "../config.js"; import { formatJsonEnvelope } from "../output.js"; const { brand, accent, dim } = colors; export function cmdConfigShow(opts: { output?: string } = {}): void { - const config = loadConfig(); + const config = loadConfig(); - if (opts.output === "json") { - formatJsonEnvelope({ - command: "config show", - data: { - defaults: { - user_id: config.defaults.userId || null, - agent_id: config.defaults.agentId || null, - app_id: config.defaults.appId || null, - run_id: config.defaults.runId || null, - enable_graph: config.defaults.enableGraph, - }, - platform: { - api_key: redactKey(config.platform.apiKey), - base_url: config.platform.baseUrl, - }, - }, - }); - return; - } + if (opts.output === "json") { + formatJsonEnvelope({ + command: "config show", + data: { + defaults: { + user_id: config.defaults.userId || null, + agent_id: config.defaults.agentId || null, + app_id: config.defaults.appId || null, + run_id: config.defaults.runId || null, + enable_graph: config.defaults.enableGraph, + }, + platform: { + api_key: redactKey(config.platform.apiKey), + base_url: config.platform.baseUrl, + }, + }, + }); + return; + } - console.log(); - console.log(` ${brand("◆ mem0 Configuration")}\n`); + console.log(); + console.log(` ${brand("◆ mem0 Configuration")}\n`); - const table = new Table({ - head: [accent("Key"), accent("Value")], - style: { head: [], border: [] }, - }); + const table = new Table({ + head: [accent("Key"), accent("Value")], + style: { head: [], border: [] }, + }); - // Defaults - table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]); - table.push(["defaults.agent_id", config.defaults.agentId || dim("(not set)")]); - table.push(["defaults.app_id", config.defaults.appId || dim("(not set)")]); - table.push(["defaults.run_id", config.defaults.runId || dim("(not set)")]); - table.push(["defaults.enable_graph", String(config.defaults.enableGraph)]); - table.push(["", ""]); + // Defaults + table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]); + table.push([ + "defaults.agent_id", + config.defaults.agentId || dim("(not set)"), + ]); + table.push(["defaults.app_id", config.defaults.appId || dim("(not set)")]); + table.push(["defaults.run_id", config.defaults.runId || dim("(not set)")]); + table.push(["defaults.enable_graph", String(config.defaults.enableGraph)]); + table.push(["", ""]); - // Platform - table.push(["platform.api_key", redactKey(config.platform.apiKey)]); - table.push(["platform.base_url", config.platform.baseUrl]); + // Platform + table.push(["platform.api_key", redactKey(config.platform.apiKey)]); + table.push(["platform.base_url", config.platform.baseUrl]); - console.log(table.toString()); - console.log(); + console.log(table.toString()); + console.log(); } export function cmdConfigGet(key: string): void { - const config = loadConfig(); - const value = getNestedValue(config, key); + const config = loadConfig(); + const value = getNestedValue(config, key); - if (value === undefined) { - printError(`Unknown config key: ${key}`); - } else { - // Redact secrets - if (key.includes("api_key") || key.split(".").pop() === "key") { - console.log(redactKey(String(value))); - } else { - console.log(String(value)); - } - } + if (value === undefined) { + printError(`Unknown config key: ${key}`); + } else { + // Redact secrets + if (key.includes("api_key") || key.split(".").pop() === "key") { + console.log(redactKey(String(value))); + } else { + console.log(String(value)); + } + } } export function cmdConfigSet(key: string, value: string): void { - const config = loadConfig(); - if (setNestedValue(config, key, value)) { - saveConfig(config); - const display = key.includes("key") ? redactKey(value) : value; - printSuccess(`${key} = ${display}`); - } else { - printError(`Unknown config key: ${key}`); - } + const config = loadConfig(); + if (setNestedValue(config, key, value)) { + saveConfig(config); + const display = key.includes("key") ? redactKey(value) : value; + printSuccess(`${key} = ${display}`); + } else { + printError(`Unknown config key: ${key}`); + } } diff --git a/cli/node/src/commands/entities.ts b/cli/node/src/commands/entities.ts index 612be8f2a..35e002711 100644 --- a/cli/node/src/commands/entities.ts +++ b/cli/node/src/commands/entities.ts @@ -4,8 +4,14 @@ import readline from "node:readline"; import Table from "cli-table3"; -import { printError, printInfo, printSuccess, timedStatus, colors } from "../branding.js"; import type { Backend } from "../backend/base.js"; +import { + colors, + printError, + printInfo, + printSuccess, + timedStatus, +} from "../branding.js"; import { formatJson } from "../output.js"; const { brand, accent, dim } = colors; @@ -13,127 +19,138 @@ const { brand, accent, dim } = colors; const VALID_TYPES = new Set(["users", "agents", "apps", "runs"]); export async function cmdEntitiesList( - backend: Backend, - entityType: string, - opts: { output: string }, + backend: Backend, + entityType: string, + opts: { output: string }, ): Promise { - if (!VALID_TYPES.has(entityType)) { - printError(`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`); - process.exit(1); - } + if (!VALID_TYPES.has(entityType)) { + printError( + `Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`, + ); + process.exit(1); + } - const start = performance.now(); - let results: Record[]; - try { - results = await timedStatus(`Fetching ${entityType}...`, async () => { - return backend.entities(entityType); - }); - } catch (e) { - printError( - e instanceof Error ? e.message : String(e), - "This feature may require the mem0 Platform.", - ); - process.exit(1); - } - const elapsed = (performance.now() - start) / 1000; + const start = performance.now(); + let results: Record[]; + try { + results = await timedStatus(`Fetching ${entityType}...`, async () => { + return backend.entities(entityType); + }); + } catch (e) { + printError( + e instanceof Error ? e.message : String(e), + "This feature may require the mem0 Platform.", + ); + process.exit(1); + } + const elapsed = (performance.now() - start) / 1000; - if (opts.output === "json") { - formatJson(results); - return; - } + if (opts.output === "json") { + formatJson(results); + return; + } - if (!results.length) { - printInfo(`No ${entityType} found.`); - return; - } + if (!results.length) { + printInfo(`No ${entityType} found.`); + return; + } - const table = new Table({ - head: [accent("Name / ID"), accent("Created")], - style: { head: [], border: [] }, - }); + const table = new Table({ + head: [accent("Name / ID"), accent("Created")], + style: { head: [], border: [] }, + }); - for (const entity of results) { - const name = String(entity.name ?? entity.id ?? "—"); - const created = String(entity.created_at ?? "—").slice(0, 10); - table.push([name, created]); - } + for (const entity of results) { + const name = String(entity.name ?? entity.id ?? "—"); + const created = String(entity.created_at ?? "—").slice(0, 10); + table.push([name, created]); + } - console.log(); - console.log(table.toString()); - console.log(` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`); - console.log(); + console.log(); + console.log(table.toString()); + console.log( + ` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`, + ); + console.log(); } export async function cmdEntitiesDelete( - backend: Backend, - opts: { - userId?: string; - agentId?: string; - appId?: string; - runId?: string; - dryRun?: boolean; - force: boolean; - output: string; - }, + backend: Backend, + opts: { + userId?: string; + agentId?: string; + appId?: string; + runId?: string; + dryRun?: boolean; + force: boolean; + output: string; + }, ): Promise { - if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) { - printError("Provide at least one of --user-id, --agent-id, --app-id, --run-id."); - process.exit(1); - } + if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) { + printError( + "Provide at least one of --user-id, --agent-id, --app-id, --run-id.", + ); + process.exit(1); + } - if (opts.dryRun) { - const scopeParts: string[] = []; - if (opts.userId) scopeParts.push(`user=${opts.userId}`); - if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`); - if (opts.appId) scopeParts.push(`app=${opts.appId}`); - if (opts.runId) scopeParts.push(`run=${opts.runId}`); - printInfo(`Would delete entity ${scopeParts.join(", ")} and all its memories.`); - printInfo("No changes made."); - return; - } + if (opts.dryRun) { + const scopeParts: string[] = []; + if (opts.userId) scopeParts.push(`user=${opts.userId}`); + if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`); + if (opts.appId) scopeParts.push(`app=${opts.appId}`); + if (opts.runId) scopeParts.push(`run=${opts.runId}`); + printInfo( + `Would delete entity ${scopeParts.join(", ")} and all its memories.`, + ); + printInfo("No changes made."); + return; + } - if (!opts.force) { - const scopeParts: string[] = []; - if (opts.userId) scopeParts.push(`user=${opts.userId}`); - if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`); - if (opts.appId) scopeParts.push(`app=${opts.appId}`); - if (opts.runId) scopeParts.push(`run=${opts.runId}`); - const scope = scopeParts.join(", "); + if (!opts.force) { + const scopeParts: string[] = []; + if (opts.userId) scopeParts.push(`user=${opts.userId}`); + if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`); + if (opts.appId) scopeParts.push(`app=${opts.appId}`); + if (opts.runId) scopeParts.push(`run=${opts.runId}`); + const scope = scopeParts.join(", "); - const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); - const answer = await new Promise((resolve) => { - rl.question( - `\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `, - resolve, - ); - }); - rl.close(); - if (answer.toLowerCase() !== "y") { - printInfo("Cancelled."); - process.exit(0); - } - } + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + const answer = await new Promise((resolve) => { + rl.question( + `\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `, + resolve, + ); + }); + rl.close(); + if (answer.toLowerCase() !== "y") { + printInfo("Cancelled."); + process.exit(0); + } + } - const start = performance.now(); - let result: Record; - try { - result = await timedStatus("Deleting entity...", async () => { - return backend.deleteEntities({ - userId: opts.userId, - agentId: opts.agentId, - appId: opts.appId, - runId: opts.runId, - }); - }); - } catch (e) { - printError(e instanceof Error ? e.message : String(e)); - process.exit(1); - } - const elapsed = (performance.now() - start) / 1000; + const start = performance.now(); + let result: Record; + try { + result = await timedStatus("Deleting entity...", async () => { + return backend.deleteEntities({ + userId: opts.userId, + agentId: opts.agentId, + appId: opts.appId, + runId: opts.runId, + }); + }); + } catch (e) { + printError(e instanceof Error ? e.message : String(e)); + process.exit(1); + } + const elapsed = (performance.now() - start) / 1000; - if (opts.output === "json") { - formatJson(result); - } else if (opts.output !== "quiet") { - printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`); - } + if (opts.output === "json") { + formatJson(result); + } else if (opts.output !== "quiet") { + printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`); + } } diff --git a/cli/node/src/commands/init.ts b/cli/node/src/commands/init.ts index 1ace32d77..76f7cd8be 100644 --- a/cli/node/src/commands/init.ts +++ b/cli/node/src/commands/init.ts @@ -3,355 +3,381 @@ */ import readline from "node:readline"; -import { - printBanner, - printError, - printInfo, - printSuccess, - colors, -} from "../branding.js"; -import { type Mem0Config, createDefaultConfig, loadConfig, saveConfig, DEFAULT_BASE_URL } from "../config.js"; import { PlatformBackend } from "../backend/platform.js"; +import { + colors, + printBanner, + printError, + printInfo, + printSuccess, +} from "../branding.js"; +import { + DEFAULT_BASE_URL, + type Mem0Config, + createDefaultConfig, + loadConfig, + saveConfig, +} from "../config.js"; const { brand, dim } = colors; const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/; function validateEmail(email: string): void { - if (!EMAIL_RE.test(email)) { - printError(`Invalid email address: ${JSON.stringify(email)}`); - process.exit(1); - } + if (!EMAIL_RE.test(email)) { + printError(`Invalid email address: ${JSON.stringify(email)}`); + process.exit(1); + } } async function emailLogin( - email: string, - code: string | undefined, - baseUrl: string, + email: string, + code: string | undefined, + baseUrl: string, ): Promise> { - const url = baseUrl.replace(/\/+$/, ""); + const url = baseUrl.replace(/\/+$/, ""); + let codeValue = code; - if (!code) { - const resp = await fetch(`${url}/api/v1/auth/email_code/`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email }), - signal: AbortSignal.timeout(30_000), - }); - if (resp.status === 429) { - printError("Too many attempts. Try again in a few minutes."); - process.exit(1); - } - if (!resp.ok) { - let detail: string; - try { - const body = (await resp.json()) as Record; - detail = (body["error"] ?? body["detail"] ?? resp.statusText) as string; - } catch { - detail = resp.statusText; - } - printError(`Failed to send code: ${detail}`); - process.exit(1); - } + if (!codeValue) { + const resp = await fetch(`${url}/api/v1/auth/email_code/`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email }), + signal: AbortSignal.timeout(30_000), + }); + if (resp.status === 429) { + printError("Too many attempts. Try again in a few minutes."); + process.exit(1); + } + if (!resp.ok) { + let detail: string; + try { + const body = (await resp.json()) as Record; + detail = (body.error ?? body.detail ?? resp.statusText) as string; + } catch { + detail = resp.statusText; + } + printError(`Failed to send code: ${detail}`); + process.exit(1); + } - printSuccess("Verification code sent! Check your email."); + printSuccess("Verification code sent! Check your email."); - if (!process.stdin.isTTY) { - printError( - "No --code provided and terminal is non-interactive.", - "Run: mem0 init --email --code ", - ); - process.exit(1); - } + if (!process.stdin.isTTY) { + printError( + "No --code provided and terminal is non-interactive.", + "Run: mem0 init --email --code ", + ); + process.exit(1); + } - console.log(); - const entered = await promptLine(` ${brand("Verification Code")}`); - if (!entered) { - printError("Code is required."); - process.exit(1); - } - code = entered; - } + console.log(); + const entered = await promptLine(` ${brand("Verification Code")}`); + if (!entered) { + printError("Code is required."); + process.exit(1); + } + codeValue = entered; + } - const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email, code: code.trim() }), - signal: AbortSignal.timeout(30_000), - }); - if (verifyResp.status === 429) { - printError("Too many attempts. Try again in a few minutes."); - process.exit(1); - } - if (!verifyResp.ok) { - let detail: string; - try { - const body = (await verifyResp.json()) as Record; - detail = (body["error"] ?? body["detail"] ?? verifyResp.statusText) as string; - } catch { - detail = verifyResp.statusText; - } - printError(`Verification failed: ${detail}`); - process.exit(1); - } + const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, code: codeValue.trim() }), + signal: AbortSignal.timeout(30_000), + }); + if (verifyResp.status === 429) { + printError("Too many attempts. Try again in a few minutes."); + process.exit(1); + } + if (!verifyResp.ok) { + let detail: string; + try { + const body = (await verifyResp.json()) as Record; + detail = (body.error ?? body.detail ?? verifyResp.statusText) as string; + } catch { + detail = verifyResp.statusText; + } + printError(`Verification failed: ${detail}`); + process.exit(1); + } - return verifyResp.json() as Promise>; + return verifyResp.json() as Promise>; } function promptSecret(label: string): Promise { - return new Promise((resolve, reject) => { - process.stdout.write(label); + return new Promise((resolve, reject) => { + process.stdout.write(label); - if (process.stdin.isTTY) { - process.stdin.setRawMode(true); - } - process.stdin.resume(); - process.stdin.setEncoding("utf-8"); + if (process.stdin.isTTY) { + process.stdin.setRawMode(true); + } + process.stdin.resume(); + process.stdin.setEncoding("utf-8"); - const chars: string[] = []; + const chars: string[] = []; - const onData = (key: string) => { - for (const ch of key) { - if (ch === "\r" || ch === "\n") { - cleanup(); - process.stdout.write("\n"); - resolve(chars.join("")); - return; - } - if (ch === "\x03") { - cleanup(); - reject(new Error("Interrupted")); - return; - } - if (ch === "\x7f" || ch === "\x08") { - // backspace - if (chars.length > 0) { - chars.pop(); - process.stdout.write("\b \b"); - } - } else if (ch === "\x15") { - // Ctrl+U — clear line - process.stdout.write("\b \b".repeat(chars.length)); - chars.length = 0; - } else if (ch >= " ") { - chars.push(ch); - process.stdout.write("*"); - } - } - }; + const onData = (key: string) => { + for (const ch of key) { + if (ch === "\r" || ch === "\n") { + cleanup(); + process.stdout.write("\n"); + resolve(chars.join("")); + return; + } + if (ch === "\x03") { + cleanup(); + reject(new Error("Interrupted")); + return; + } + if (ch === "\x7f" || ch === "\x08") { + // backspace + if (chars.length > 0) { + chars.pop(); + process.stdout.write("\b \b"); + } + } else if (ch === "\x15") { + // Ctrl+U — clear line + process.stdout.write("\b \b".repeat(chars.length)); + chars.length = 0; + } else if (ch >= " ") { + chars.push(ch); + process.stdout.write("*"); + } + } + }; - const cleanup = () => { - process.stdin.removeListener("data", onData); - if (process.stdin.isTTY) { - process.stdin.setRawMode(false); - } - process.stdin.pause(); - }; + const cleanup = () => { + process.stdin.removeListener("data", onData); + if (process.stdin.isTTY) { + process.stdin.setRawMode(false); + } + process.stdin.pause(); + }; - process.stdin.on("data", onData); - }); + process.stdin.on("data", onData); + }); } function promptLine(label: string, defaultValue?: string): Promise { - const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); - const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `; - return new Promise((resolve) => { - rl.question(prompt, (answer) => { - rl.close(); - resolve(answer.trim() || defaultValue || ""); - }); - }); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `; + return new Promise((resolve) => { + rl.question(prompt, (answer) => { + rl.close(); + resolve(answer.trim() || defaultValue || ""); + }); + }); } async function setupPlatform(config: Mem0Config): Promise { - console.log(); - console.log(` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys")}`); - console.log(); + console.log(); + console.log( + ` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys")}`, + ); + console.log(); - process.stdout.write(` ${brand("API Key")}: `); - const apiKey = await promptSecret(""); - if (!apiKey) { - printError("API key is required."); - process.exit(1); - } - config.platform.apiKey = apiKey; + process.stdout.write(` ${brand("API Key")}: `); + const apiKey = await promptSecret(""); + if (!apiKey) { + printError("API key is required."); + process.exit(1); + } + config.platform.apiKey = apiKey; } async function setupDefaults(config: Mem0Config): Promise { - console.log(); - printInfo("Set default entity IDs (press Enter to skip).\n"); + console.log(); + printInfo("Set default entity IDs (press Enter to skip).\n"); - const userId = await promptLine(` ${brand("Default User ID")} ${dim("(recommended)")}`, "mem0-cli"); - if (userId) config.defaults.userId = userId; + const userId = await promptLine( + ` ${brand("Default User ID")} ${dim("(recommended)")}`, + "mem0-cli", + ); + if (userId) config.defaults.userId = userId; } async function validatePlatform(config: Mem0Config): Promise { - console.log(); - printInfo("Validating connection..."); - try { - const backend = new PlatformBackend(config.platform); - const status = await backend.status({ - userId: config.defaults.userId || undefined, - agentId: config.defaults.agentId || undefined, - }); - if (status.connected) { - printSuccess("Connected to mem0 Platform!"); - } else { - printError( - `Could not connect: ${status.error ?? "Unknown error"}`, - "Check your API key and try again.", - ); - } - } catch (e) { - printError(`Connection test failed: ${e instanceof Error ? e.message : e}`); - } + console.log(); + printInfo("Validating connection..."); + try { + const backend = new PlatformBackend(config.platform); + const status = await backend.status({ + userId: config.defaults.userId || undefined, + agentId: config.defaults.agentId || undefined, + }); + if (status.connected) { + printSuccess("Connected to mem0 Platform!"); + } else { + printError( + `Could not connect: ${status.error ?? "Unknown error"}`, + "Check your API key and try again.", + ); + } + } catch (e) { + printError(`Connection test failed: ${e instanceof Error ? e.message : e}`); + } } export async function runInit( - opts: { apiKey?: string; userId?: string; email?: string; code?: string } = {}, + opts: { + apiKey?: string; + userId?: string; + email?: string; + code?: string; + } = {}, ): Promise { - const config = createDefaultConfig(); - const savedConfig = loadConfig(); - const baseUrl = - process.env["MEM0_BASE_URL"] || savedConfig.platform.baseUrl || DEFAULT_BASE_URL; + const config = createDefaultConfig(); + const savedConfig = loadConfig(); + const baseUrl = + process.env.MEM0_BASE_URL || + savedConfig.platform.baseUrl || + DEFAULT_BASE_URL; - // Guards - if (opts.code && !opts.email) { - printError("--code requires --email."); - process.exit(1); - } - if (opts.email && opts.apiKey) { - printError("Cannot use both --api-key and --email."); - process.exit(1); - } + // Guards + if (opts.code && !opts.email) { + printError("--code requires --email."); + process.exit(1); + } + if (opts.email && opts.apiKey) { + printError("Cannot use both --api-key and --email."); + process.exit(1); + } - // ── Email login flow ────────────────────────────────────────────────────── - if (opts.email) { - const email = opts.email.trim().toLowerCase(); - validateEmail(email); + // ── Email login flow ────────────────────────────────────────────────────── + if (opts.email) { + const email = opts.email.trim().toLowerCase(); + validateEmail(email); - printBanner(); - console.log(); - printInfo(`Logging in as ${email}...\n`); + printBanner(); + console.log(); + printInfo(`Logging in as ${email}...\n`); - const result = await emailLogin(email, opts.code, baseUrl); + const result = await emailLogin(email, opts.code, baseUrl); - const apiKeyVal = result["api_key"] as string | undefined; - if (!apiKeyVal) { - printError("Auth succeeded but no API key was returned. Contact support."); - process.exit(1); - } + const apiKeyVal = result.api_key as string | undefined; + if (!apiKeyVal) { + printError( + "Auth succeeded but no API key was returned. Contact support.", + ); + process.exit(1); + } - config.platform.apiKey = apiKeyVal; - config.platform.baseUrl = baseUrl; - config.defaults.userId = opts.userId || "mem0-cli"; + config.platform.apiKey = apiKeyVal; + config.platform.baseUrl = baseUrl; + config.defaults.userId = opts.userId || "mem0-cli"; - saveConfig(config); - console.log(); - printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json"); - console.log(); - console.log(` ${dim("Get started:")}`); - console.log(` ${dim(' mem0 add "I prefer dark mode"')}`); - console.log(` ${dim(' mem0 search "preferences"')}`); - console.log(); - return; - } + saveConfig(config); + console.log(); + printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json"); + console.log(); + console.log(` ${dim("Get started:")}`); + console.log(` ${dim(' mem0 add "I prefer dark mode"')}`); + console.log(` ${dim(' mem0 search "preferences"')}`); + console.log(); + return; + } - // ── API key flow ────────────────────────────────────────────────────────── + // ── API key flow ────────────────────────────────────────────────────────── - // Non-interactive: both flags provided - if (opts.apiKey && opts.userId) { - config.platform.apiKey = opts.apiKey; - config.defaults.userId = opts.userId; - await validatePlatform(config); - saveConfig(config); - printSuccess("Configuration saved to ~/.mem0/config.json"); - return; - } + // Non-interactive: both flags provided + if (opts.apiKey && opts.userId) { + config.platform.apiKey = opts.apiKey; + config.defaults.userId = opts.userId; + await validatePlatform(config); + saveConfig(config); + printSuccess("Configuration saved to ~/.mem0/config.json"); + return; + } - // Non-TTY without full flags: error with usage hint - if (!process.stdin.isTTY && (!opts.apiKey || !opts.userId)) { - printError( - "Non-interactive terminal detected and missing required flags.", - "Usage: mem0 init --api-key --user-id ", - ); - process.exit(1); - } + // Non-TTY without full flags: error with usage hint + if (!process.stdin.isTTY && (!opts.apiKey || !opts.userId)) { + printError( + "Non-interactive terminal detected and missing required flags.", + "Usage: mem0 init --api-key --user-id ", + ); + process.exit(1); + } - printBanner(); - console.log(); - printInfo("Welcome! Let's set up your mem0 CLI.\n"); + printBanner(); + console.log(); + printInfo("Welcome! Let's set up your mem0 CLI.\n"); - // Use provided API key or prompt - if (opts.apiKey) { - config.platform.apiKey = opts.apiKey; - } else { - console.log(` ${brand("How would you like to authenticate?")}`); - console.log(` ${dim("1.")} Login with email ${dim("(recommended)")}`); - console.log(` ${dim("2.")} Enter API key manually`); - console.log(); + // Use provided API key or prompt + if (opts.apiKey) { + config.platform.apiKey = opts.apiKey; + } else { + console.log(` ${brand("How would you like to authenticate?")}`); + console.log(` ${dim("1.")} Login with email ${dim("(recommended)")}`); + console.log(` ${dim("2.")} Enter API key manually`); + console.log(); - const choice = await promptLine(` ${brand("Choose")} [1/2]`, "1"); + const choice = await promptLine(` ${brand("Choose")} [1/2]`, "1"); - if (choice === "1") { - console.log(); - const emailAddr = await promptLine(` ${brand("Email")}`); - if (!emailAddr) { - printError("Email is required."); - process.exit(1); - } + if (choice === "1") { + console.log(); + const emailAddr = await promptLine(` ${brand("Email")}`); + if (!emailAddr) { + printError("Email is required."); + process.exit(1); + } - const email = emailAddr.trim().toLowerCase(); - validateEmail(email); - printInfo(`Logging in as ${email}...\n`); + const email = emailAddr.trim().toLowerCase(); + validateEmail(email); + printInfo(`Logging in as ${email}...\n`); - const result = await emailLogin(email, undefined, baseUrl); + const result = await emailLogin(email, undefined, baseUrl); - const apiKeyVal = result["api_key"] as string | undefined; - if (!apiKeyVal) { - printError("Auth succeeded but no API key was returned. Contact support."); - process.exit(1); - } + const apiKeyVal = result.api_key as string | undefined; + if (!apiKeyVal) { + printError( + "Auth succeeded but no API key was returned. Contact support.", + ); + process.exit(1); + } - config.platform.apiKey = apiKeyVal; - config.platform.baseUrl = baseUrl; - config.defaults.userId = opts.userId || "mem0-cli"; + config.platform.apiKey = apiKeyVal; + config.platform.baseUrl = baseUrl; + config.defaults.userId = opts.userId || "mem0-cli"; - saveConfig(config); - console.log(); - printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json"); - console.log(); - console.log(` ${dim("Get started:")}`); - console.log(` ${dim(' mem0 add "I prefer dark mode"')}`); - console.log(` ${dim(' mem0 search "preferences"')}`); - console.log(); - return; - } + saveConfig(config); + console.log(); + printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json"); + console.log(); + console.log(` ${dim("Get started:")}`); + console.log(` ${dim(' mem0 add "I prefer dark mode"')}`); + console.log(` ${dim(' mem0 search "preferences"')}`); + console.log(); + return; + } - // choice === "2": fall through to API key prompt - await setupPlatform(config); - } + // choice === "2": fall through to API key prompt + await setupPlatform(config); + } - // Use provided user ID or prompt - if (opts.userId) { - config.defaults.userId = opts.userId; - } else { - await setupDefaults(config); - } + // Use provided user ID or prompt + if (opts.userId) { + config.defaults.userId = opts.userId; + } else { + await setupDefaults(config); + } - await validatePlatform(config); + await validatePlatform(config); - saveConfig(config); - console.log(); - printSuccess("Configuration saved to ~/.mem0/config.json"); - console.log(); - console.log(` ${dim("Get started:")}`); - if (config.defaults.userId) { - console.log(` ${dim(' mem0 add "I prefer dark mode"')}`); - console.log(` ${dim(' mem0 search "preferences"')}`); - } else { - console.log(` ${dim(' mem0 add "I prefer dark mode" --user-id alice')}`); - console.log(` ${dim(' mem0 search "preferences" --user-id alice')}`); - } - console.log(); + saveConfig(config); + console.log(); + printSuccess("Configuration saved to ~/.mem0/config.json"); + console.log(); + console.log(` ${dim("Get started:")}`); + if (config.defaults.userId) { + console.log(` ${dim(' mem0 add "I prefer dark mode"')}`); + console.log(` ${dim(' mem0 search "preferences"')}`); + } else { + console.log(` ${dim(' mem0 add "I prefer dark mode" --user-id alice')}`); + console.log(` ${dim(' mem0 search "preferences" --user-id alice')}`); + } + console.log(); } diff --git a/cli/node/src/commands/memory.ts b/cli/node/src/commands/memory.ts index 95065f98e..997260670 100644 --- a/cli/node/src/commands/memory.ts +++ b/cli/node/src/commands/memory.ts @@ -3,530 +3,598 @@ */ import fs from "node:fs"; -import { printError, printInfo, printScope, printSuccess, timedStatus } from "../branding.js"; import type { Backend } from "../backend/base.js"; import { - formatAddResult, - formatJson, - formatJsonEnvelope, - formatMemoriesTable, - formatMemoriesText, - formatSingleMemory, - printResultSummary, + printError, + printInfo, + printScope, + printSuccess, + timedStatus, +} from "../branding.js"; +import { + formatAddResult, + formatJson, + formatJsonEnvelope, + formatMemoriesTable, + formatMemoriesText, + formatSingleMemory, + printResultSummary, } from "../output.js"; export async function cmdAdd( - backend: Backend, - text: string | undefined, - opts: { - userId?: string; - agentId?: string; - appId?: string; - runId?: string; - messages?: string; - file?: string; - metadata?: string; - immutable: boolean; - noInfer: boolean; - expires?: string; - categories?: string; - enableGraph: boolean; - output: string; - }, + backend: Backend, + text: string | undefined, + opts: { + userId?: string; + agentId?: string; + appId?: string; + runId?: string; + messages?: string; + file?: string; + metadata?: string; + immutable: boolean; + noInfer: boolean; + expires?: string; + categories?: string; + enableGraph: boolean; + output: string; + }, ): Promise { - let msgs: Record[] | undefined; - let content = text; + let msgs: Record[] | undefined; + let content = text; - // Read from file - if (opts.file) { - try { - const raw = fs.readFileSync(opts.file, "utf-8"); - msgs = JSON.parse(raw); - } catch (e) { - printError(`Failed to read file: ${e instanceof Error ? e.message : e}`); - process.exit(1); - } - } - // Parse messages JSON - else if (opts.messages) { - try { - msgs = JSON.parse(opts.messages); - } catch (e) { - printError(`Invalid JSON in --messages: ${e instanceof Error ? e.message : e}`); - process.exit(1); - } - } - // Read from stdin if piped - else if (!content && !process.stdin.isTTY) { - content = fs.readFileSync(0, "utf-8").trim(); - } + // Read from file + if (opts.file) { + try { + const raw = fs.readFileSync(opts.file, "utf-8"); + msgs = JSON.parse(raw); + } catch (e) { + printError(`Failed to read file: ${e instanceof Error ? e.message : e}`); + process.exit(1); + } + } + // Parse messages JSON + else if (opts.messages) { + try { + msgs = JSON.parse(opts.messages); + } catch (e) { + printError( + `Invalid JSON in --messages: ${e instanceof Error ? e.message : e}`, + ); + process.exit(1); + } + } + // Read from stdin if piped + else if (!content && !process.stdin.isTTY) { + content = fs.readFileSync(0, "utf-8").trim(); + } - if (content !== undefined && content.trim() === "") { - printError("Content cannot be empty."); - process.exit(1); - } - if (!content && !msgs) { - printError("No content provided. Pass text, --messages, --file, or pipe via stdin."); - process.exit(1); - } + if (content !== undefined && content.trim() === "") { + printError("Content cannot be empty."); + process.exit(1); + } + if (!content && !msgs) { + printError( + "No content provided. Pass text, --messages, --file, or pipe via stdin.", + ); + process.exit(1); + } - // Validate --expires - if (opts.expires) { - if (!/^\d{4}-\d{2}-\d{2}$/.test(opts.expires)) { - printError("Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31)."); - process.exit(1); - } - if (new Date(opts.expires) <= new Date()) { - printError("--expires date must be in the future."); - process.exit(1); - } - } + // Validate --expires + if (opts.expires) { + if (!/^\d{4}-\d{2}-\d{2}$/.test(opts.expires)) { + printError( + "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).", + ); + process.exit(1); + } + if (new Date(opts.expires) <= new Date()) { + printError("--expires date must be in the future."); + process.exit(1); + } + } - let meta: Record | undefined; - if (opts.metadata) { - try { - meta = JSON.parse(opts.metadata); - } catch { - printError("Invalid JSON in --metadata."); - process.exit(1); - } - } + let meta: Record | undefined; + if (opts.metadata) { + try { + meta = JSON.parse(opts.metadata); + } catch { + printError("Invalid JSON in --metadata."); + process.exit(1); + } + } - let cats: string[] | undefined; - if (opts.categories) { - try { - cats = JSON.parse(opts.categories); - } catch { - cats = opts.categories.split(",").map((c) => c.trim()); - } - } + let cats: string[] | undefined; + if (opts.categories) { + try { + cats = JSON.parse(opts.categories); + } catch { + cats = opts.categories.split(",").map((c) => c.trim()); + } + } - let result: Record; - try { - result = await timedStatus("Adding memory...", async () => { - return backend.add(content ?? undefined, msgs, { - userId: opts.userId, - agentId: opts.agentId, - appId: opts.appId, - runId: opts.runId, - metadata: meta, - immutable: opts.immutable, - infer: !opts.noInfer, - expires: opts.expires, - categories: cats, - enableGraph: opts.enableGraph, - }); - }); - } catch (e) { - printError(e instanceof Error ? e.message : String(e)); - process.exit(1); - } + let result: Record; + try { + result = await timedStatus("Adding memory...", async () => { + return backend.add(content ?? undefined, msgs, { + userId: opts.userId, + agentId: opts.agentId, + appId: opts.appId, + runId: opts.runId, + metadata: meta, + immutable: opts.immutable, + infer: !opts.noInfer, + expires: opts.expires, + categories: cats, + enableGraph: opts.enableGraph, + }); + }); + } catch (e) { + printError(e instanceof Error ? e.message : String(e)); + process.exit(1); + } - if (opts.output === "quiet") return; + if (opts.output === "quiet") return; - if (opts.output === "json") { - formatAddResult(result, opts.output); - return; - } + if (opts.output === "json") { + formatAddResult(result, opts.output); + return; + } - console.log(); - printScope({ user_id: opts.userId, agent_id: opts.agentId, app_id: opts.appId, run_id: opts.runId }); - const results = Array.isArray(result) ? result : ((result.results as unknown[]) ?? [result]); - const count = results.length; - printSuccess(`Memory processed — ${count} memor${count === 1 ? "y" : "ies"} extracted`); - formatAddResult(result, opts.output); + console.log(); + printScope({ + user_id: opts.userId, + agent_id: opts.agentId, + app_id: opts.appId, + run_id: opts.runId, + }); + const results = Array.isArray(result) + ? result + : ((result.results as unknown[]) ?? [result]); + const count = results.length; + printSuccess( + `Memory processed — ${count} memor${count === 1 ? "y" : "ies"} extracted`, + ); + formatAddResult(result, opts.output); } export async function cmdSearch( - backend: Backend, - query: string | undefined, - opts: { - userId?: string; - agentId?: string; - appId?: string; - runId?: string; - topK: number; - threshold: number; - rerank: boolean; - keyword: boolean; - filterJson?: string; - fields?: string; - enableGraph: boolean; - output: string; - }, + backend: Backend, + query: string | undefined, + opts: { + userId?: string; + agentId?: string; + appId?: string; + runId?: string; + topK: number; + threshold: number; + rerank: boolean; + keyword: boolean; + filterJson?: string; + fields?: string; + enableGraph: boolean; + output: string; + }, ): Promise { - if (!query) { - printError("No query provided. Pass a query argument or pipe via stdin."); - process.exit(1); - } + if (!query) { + printError("No query provided. Pass a query argument or pipe via stdin."); + process.exit(1); + } - let filters: Record | undefined; - if (opts.filterJson) { - try { - filters = JSON.parse(opts.filterJson); - } catch { - printError("Invalid JSON in --filter."); - process.exit(1); - } - } + let filters: Record | undefined; + if (opts.filterJson) { + try { + filters = JSON.parse(opts.filterJson); + } catch { + printError("Invalid JSON in --filter."); + process.exit(1); + } + } - const fieldList = opts.fields ? opts.fields.split(",").map((f) => f.trim()) : undefined; + const fieldList = opts.fields + ? opts.fields.split(",").map((f) => f.trim()) + : undefined; - if (opts.topK < 1) { - printError("--top-k must be >= 1."); - process.exit(1); - } - if (opts.threshold < 0 || opts.threshold > 1) { - printError("--threshold must be between 0.0 and 1.0."); - process.exit(1); - } + if (opts.topK < 1) { + printError("--top-k must be >= 1."); + process.exit(1); + } + if (opts.threshold < 0 || opts.threshold > 1) { + printError("--threshold must be between 0.0 and 1.0."); + process.exit(1); + } - const start = performance.now(); - let results: Record[]; - try { - results = await timedStatus("Searching memories...", async () => { - return backend.search(query!, { - userId: opts.userId, - agentId: opts.agentId, - appId: opts.appId, - runId: opts.runId, - topK: opts.topK, - threshold: opts.threshold, - rerank: opts.rerank, - keyword: opts.keyword, - filters, - fields: fieldList, - enableGraph: opts.enableGraph, - }); - }); - } catch (e) { - printError(e instanceof Error ? e.message : String(e)); - process.exit(1); - } - const elapsed = (performance.now() - start) / 1000; + const start = performance.now(); + let results: Record[]; + try { + results = await timedStatus("Searching memories...", async () => { + // biome-ignore lint/style/noNonNullAssertion: guarded by process.exit above + return backend.search(query!, { + userId: opts.userId, + agentId: opts.agentId, + appId: opts.appId, + runId: opts.runId, + topK: opts.topK, + threshold: opts.threshold, + rerank: opts.rerank, + keyword: opts.keyword, + filters, + fields: fieldList, + enableGraph: opts.enableGraph, + }); + }); + } catch (e) { + printError(e instanceof Error ? e.message : String(e)); + process.exit(1); + } + const elapsed = (performance.now() - start) / 1000; - if (opts.output === "quiet") return; + if (opts.output === "quiet") return; - if (opts.output === "json") { - formatJson(results); - } else if (opts.output === "table") { - if (results.length > 0) { - formatMemoriesTable(results, { showScore: true }); - printResultSummary({ count: results.length, durationSecs: elapsed, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } }); - } else { - console.log(); - printInfo("No memories found matching your query."); - console.log(); - } - } else { - if (results.length > 0) { - formatMemoriesText(results); - printResultSummary({ count: results.length, durationSecs: elapsed, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } }); - } else { - console.log(); - printInfo("No memories found matching your query."); - console.log(); - } - } + if (opts.output === "json") { + formatJson(results); + } else if (opts.output === "table") { + if (results.length > 0) { + formatMemoriesTable(results, { showScore: true }); + printResultSummary({ + count: results.length, + durationSecs: elapsed, + scopeIds: { user_id: opts.userId, agent_id: opts.agentId }, + }); + } else { + console.log(); + printInfo("No memories found matching your query."); + console.log(); + } + } else { + if (results.length > 0) { + formatMemoriesText(results); + printResultSummary({ + count: results.length, + durationSecs: elapsed, + scopeIds: { user_id: opts.userId, agent_id: opts.agentId }, + }); + } else { + console.log(); + printInfo("No memories found matching your query."); + console.log(); + } + } } export async function cmdGet( - backend: Backend, - memoryId: string, - opts: { output: string }, + backend: Backend, + memoryId: string, + opts: { output: string }, ): Promise { - let result: Record; - try { - result = await timedStatus("Fetching memory...", async () => { - return backend.get(memoryId); - }); - } catch (e) { - printError(e instanceof Error ? e.message : String(e)); - process.exit(1); - } + let result: Record; + try { + result = await timedStatus("Fetching memory...", async () => { + return backend.get(memoryId); + }); + } catch (e) { + printError(e instanceof Error ? e.message : String(e)); + process.exit(1); + } - formatSingleMemory(result, opts.output); + formatSingleMemory(result, opts.output); } export async function cmdList( - backend: Backend, - opts: { - userId?: string; - agentId?: string; - appId?: string; - runId?: string; - page: number; - pageSize: number; - category?: string; - after?: string; - before?: string; - enableGraph: boolean; - output: string; - }, + backend: Backend, + opts: { + userId?: string; + agentId?: string; + appId?: string; + runId?: string; + page: number; + pageSize: number; + category?: string; + after?: string; + before?: string; + enableGraph: boolean; + output: string; + }, ): Promise { - if (opts.pageSize < 1) { - printError("--page-size must be >= 1."); - process.exit(1); - } - if (opts.page < 1) { - printError("--page must be >= 1."); - process.exit(1); - } + if (opts.pageSize < 1) { + printError("--page-size must be >= 1."); + process.exit(1); + } + if (opts.page < 1) { + printError("--page must be >= 1."); + process.exit(1); + } - const start = performance.now(); - let results: Record[]; - try { - results = await timedStatus("Listing memories...", async () => { - return backend.listMemories({ - userId: opts.userId, - agentId: opts.agentId, - appId: opts.appId, - runId: opts.runId, - page: opts.page, - pageSize: opts.pageSize, - category: opts.category, - after: opts.after, - before: opts.before, - enableGraph: opts.enableGraph, - }); - }); - } catch (e) { - printError(e instanceof Error ? e.message : String(e)); - process.exit(1); - } - const elapsed = (performance.now() - start) / 1000; + const start = performance.now(); + let results: Record[]; + try { + results = await timedStatus("Listing memories...", async () => { + return backend.listMemories({ + userId: opts.userId, + agentId: opts.agentId, + appId: opts.appId, + runId: opts.runId, + page: opts.page, + pageSize: opts.pageSize, + category: opts.category, + after: opts.after, + before: opts.before, + enableGraph: opts.enableGraph, + }); + }); + } catch (e) { + printError(e instanceof Error ? e.message : String(e)); + process.exit(1); + } + const elapsed = (performance.now() - start) / 1000; - if (opts.output === "quiet") return; + if (opts.output === "quiet") return; - if (opts.output === "json") { - formatJsonEnvelope({ - command: "list", - data: results, - count: results.length, - scope: { user_id: opts.userId, agent_id: opts.agentId }, - }); - } else if (opts.output === "table") { - if (results.length > 0) { - formatMemoriesTable(results); - printResultSummary({ count: results.length, durationSecs: elapsed, page: opts.page, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } }); - } else { - console.log(); - printInfo("No memories found."); - console.log(); - } - } else { - if (results.length > 0) { - formatMemoriesText(results, "memories"); - printResultSummary({ count: results.length, durationSecs: elapsed, page: opts.page, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } }); - } else { - console.log(); - printInfo("No memories found."); - console.log(); - } - } + if (opts.output === "json") { + formatJsonEnvelope({ + command: "list", + data: results, + count: results.length, + scope: { user_id: opts.userId, agent_id: opts.agentId }, + }); + } else if (opts.output === "table") { + if (results.length > 0) { + formatMemoriesTable(results); + printResultSummary({ + count: results.length, + durationSecs: elapsed, + page: opts.page, + scopeIds: { user_id: opts.userId, agent_id: opts.agentId }, + }); + } else { + console.log(); + printInfo("No memories found."); + console.log(); + } + } else { + if (results.length > 0) { + formatMemoriesText(results, "memories"); + printResultSummary({ + count: results.length, + durationSecs: elapsed, + page: opts.page, + scopeIds: { user_id: opts.userId, agent_id: opts.agentId }, + }); + } else { + console.log(); + printInfo("No memories found."); + console.log(); + } + } } export async function cmdUpdate( - backend: Backend, - memoryId: string, - text: string | undefined, - opts: { metadata?: string; output: string }, + backend: Backend, + memoryId: string, + text: string | undefined, + opts: { metadata?: string; output: string }, ): Promise { - let meta: Record | undefined; - if (opts.metadata) { - try { - meta = JSON.parse(opts.metadata); - } catch { - printError("Invalid JSON in --metadata."); - process.exit(1); - } - } + let meta: Record | undefined; + if (opts.metadata) { + try { + meta = JSON.parse(opts.metadata); + } catch { + printError("Invalid JSON in --metadata."); + process.exit(1); + } + } - const start = performance.now(); - let result: Record; - try { - result = await timedStatus("Updating memory...", async () => { - return backend.update(memoryId, text, meta); - }); - } catch (e) { - printError(e instanceof Error ? e.message : String(e)); - process.exit(1); - } - const elapsed = (performance.now() - start) / 1000; + const start = performance.now(); + let result: Record; + try { + result = await timedStatus("Updating memory...", async () => { + return backend.update(memoryId, text, meta); + }); + } catch (e) { + printError(e instanceof Error ? e.message : String(e)); + process.exit(1); + } + const elapsed = (performance.now() - start) / 1000; - if (opts.output === "json") { - formatJson(result); - } else if (opts.output !== "quiet") { - printSuccess(`Memory ${memoryId.slice(0, 8)} updated (${elapsed.toFixed(2)}s)`); - } + if (opts.output === "json") { + formatJson(result); + } else if (opts.output !== "quiet") { + printSuccess( + `Memory ${memoryId.slice(0, 8)} updated (${elapsed.toFixed(2)}s)`, + ); + } } export async function cmdDelete( - backend: Backend, - memoryId: string, - opts: { output: string; dryRun?: boolean; force?: boolean }, + backend: Backend, + memoryId: string, + opts: { output: string; dryRun?: boolean; force?: boolean }, ): Promise { - if (opts.dryRun) { - let mem: Record; - try { - mem = await backend.get(memoryId); - } catch (e) { - printError(e instanceof Error ? e.message : String(e)); - process.exit(1); - } - const text = (mem.memory ?? mem.text ?? "") as string; - printInfo(`Would delete memory ${memoryId.slice(0, 8)}: ${text}`); - printInfo("No changes made."); - return; - } + if (opts.dryRun) { + let mem: Record; + try { + mem = await backend.get(memoryId); + } catch (e) { + printError(e instanceof Error ? e.message : String(e)); + process.exit(1); + } + const text = (mem.memory ?? mem.text ?? "") as string; + printInfo(`Would delete memory ${memoryId.slice(0, 8)}: ${text}`); + printInfo("No changes made."); + return; + } - const start = performance.now(); - let result: Record; - try { - result = await timedStatus("Deleting...", async () => { - return backend.delete(memoryId); - }); - } catch (e) { - printError(e instanceof Error ? e.message : String(e)); - process.exit(1); - } - const elapsed = (performance.now() - start) / 1000; + const start = performance.now(); + let result: Record; + try { + result = await timedStatus("Deleting...", async () => { + return backend.delete(memoryId); + }); + } catch (e) { + printError(e instanceof Error ? e.message : String(e)); + process.exit(1); + } + const elapsed = (performance.now() - start) / 1000; - if (opts.output === "json") { - formatJson(result); - } else if (opts.output !== "quiet") { - printSuccess(`Memory ${memoryId.slice(0, 8)} deleted (${elapsed.toFixed(2)}s)`); - } + if (opts.output === "json") { + formatJson(result); + } else if (opts.output !== "quiet") { + printSuccess( + `Memory ${memoryId.slice(0, 8)} deleted (${elapsed.toFixed(2)}s)`, + ); + } } export async function cmdDeleteAll( - backend: Backend, - opts: { - force: boolean; - dryRun?: boolean; - all?: boolean; - userId?: string; - agentId?: string; - appId?: string; - runId?: string; - output: string; - }, + backend: Backend, + opts: { + force: boolean; + dryRun?: boolean; + all?: boolean; + userId?: string; + agentId?: string; + appId?: string; + runId?: string; + output: string; + }, ): Promise { - if (opts.all) { - // Project-wide wipe using wildcard entity IDs - if (opts.dryRun) { - printInfo("Would delete ALL memories project-wide."); - printInfo("Run without --dry-run to see the actual count of deleted memories."); - printInfo("No changes made."); - return; - } + if (opts.all) { + // Project-wide wipe using wildcard entity IDs + if (opts.dryRun) { + printInfo("Would delete ALL memories project-wide."); + printInfo( + "Run without --dry-run to see the actual count of deleted memories.", + ); + printInfo("No changes made."); + return; + } - if (!opts.force) { - const readline = await import("node:readline"); - const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); - const answer = await new Promise((resolve) => { - rl.question(`\n \u26a0 Delete ALL memories across the ENTIRE project? This cannot be undone. [y/N] `, resolve); - }); - rl.close(); - if (answer.toLowerCase() !== "y") { - printInfo("Cancelled."); - process.exit(0); - } - } + if (!opts.force) { + const readline = await import("node:readline"); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + const answer = await new Promise((resolve) => { + rl.question( + "\n \u26a0 Delete ALL memories across the ENTIRE project? This cannot be undone. [y/N] ", + resolve, + ); + }); + rl.close(); + if (answer.toLowerCase() !== "y") { + printInfo("Cancelled."); + process.exit(0); + } + } - const start = performance.now(); - let result: Record; - try { - result = await timedStatus("Deleting all memories project-wide...", async () => { - return backend.delete(undefined, { - all: true, - userId: "*", - agentId: "*", - appId: "*", - runId: "*", - }); - }); - } catch (e) { - printError(e instanceof Error ? e.message : String(e)); - process.exit(1); - } - const elapsed = (performance.now() - start) / 1000; + const start = performance.now(); + let result: Record; + try { + result = await timedStatus( + "Deleting all memories project-wide...", + async () => { + return backend.delete(undefined, { + all: true, + userId: "*", + agentId: "*", + appId: "*", + runId: "*", + }); + }, + ); + } catch (e) { + printError(e instanceof Error ? e.message : String(e)); + process.exit(1); + } + const elapsed = (performance.now() - start) / 1000; - if (opts.output === "json") { - formatJson(result); - } else if (opts.output !== "quiet") { - if (result.message) { - printInfo("Deletion started. Memories will be removed in the background."); - } else { - printSuccess(`All project memories deleted (${elapsed.toFixed(2)}s)`); - } - } - return; - } + if (opts.output === "json") { + formatJson(result); + } else if (opts.output !== "quiet") { + if (result.message) { + printInfo( + "Deletion started. Memories will be removed in the background.", + ); + } else { + printSuccess(`All project memories deleted (${elapsed.toFixed(2)}s)`); + } + } + return; + } - if (opts.dryRun) { - let memories: Record[]; - try { - memories = await backend.listMemories({ - userId: opts.userId, - agentId: opts.agentId, - appId: opts.appId, - runId: opts.runId, - }); - } catch (e) { - printError(e instanceof Error ? e.message : String(e)); - process.exit(1); - } - printInfo(`Would delete ${memories.length} memories.`); - printInfo("No changes made."); - return; - } + if (opts.dryRun) { + let memories: Record[]; + try { + memories = await backend.listMemories({ + userId: opts.userId, + agentId: opts.agentId, + appId: opts.appId, + runId: opts.runId, + }); + } catch (e) { + printError(e instanceof Error ? e.message : String(e)); + process.exit(1); + } + printInfo(`Would delete ${memories.length} memories.`); + printInfo("No changes made."); + return; + } - if (!opts.force) { - const scopeParts: string[] = []; - if (opts.userId) scopeParts.push(`user=${opts.userId}`); - if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`); - if (opts.appId) scopeParts.push(`app=${opts.appId}`); - if (opts.runId) scopeParts.push(`run=${opts.runId}`); - const scope = scopeParts.length > 0 ? scopeParts.join(", ") : "ALL entities"; + if (!opts.force) { + const scopeParts: string[] = []; + if (opts.userId) scopeParts.push(`user=${opts.userId}`); + if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`); + if (opts.appId) scopeParts.push(`app=${opts.appId}`); + if (opts.runId) scopeParts.push(`run=${opts.runId}`); + const scope = + scopeParts.length > 0 ? scopeParts.join(", ") : "ALL entities"; - const readline = await import("node:readline"); - const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); - const answer = await new Promise((resolve) => { - rl.question(`\n \u26a0 Delete ALL memories for ${scope}? This cannot be undone. [y/N] `, resolve); - }); - rl.close(); - if (answer.toLowerCase() !== "y") { - printInfo("Cancelled."); - process.exit(0); - } - } + const readline = await import("node:readline"); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + const answer = await new Promise((resolve) => { + rl.question( + `\n \u26a0 Delete ALL memories for ${scope}? This cannot be undone. [y/N] `, + resolve, + ); + }); + rl.close(); + if (answer.toLowerCase() !== "y") { + printInfo("Cancelled."); + process.exit(0); + } + } - const start = performance.now(); - let result: Record; - try { - result = await timedStatus("Deleting all memories...", async () => { - return backend.delete(undefined, { - all: true, - userId: opts.userId, - agentId: opts.agentId, - appId: opts.appId, - runId: opts.runId, - }); - }); - } catch (e) { - printError(e instanceof Error ? e.message : String(e)); - process.exit(1); - } - const elapsed = (performance.now() - start) / 1000; + const start = performance.now(); + let result: Record; + try { + result = await timedStatus("Deleting all memories...", async () => { + return backend.delete(undefined, { + all: true, + userId: opts.userId, + agentId: opts.agentId, + appId: opts.appId, + runId: opts.runId, + }); + }); + } catch (e) { + printError(e instanceof Error ? e.message : String(e)); + process.exit(1); + } + const elapsed = (performance.now() - start) / 1000; - if (opts.output === "json") { - formatJson(result); - } else if (opts.output !== "quiet") { - if (result.message) { - printInfo("Deletion started. Memories will be removed in the background."); - } else { - printSuccess(`All matching memories deleted (${elapsed.toFixed(2)}s)`); - } - } + if (opts.output === "json") { + formatJson(result); + } else if (opts.output !== "quiet") { + if (result.message) { + printInfo( + "Deletion started. Memories will be removed in the background.", + ); + } else { + printSuccess(`All matching memories deleted (${elapsed.toFixed(2)}s)`); + } + } } diff --git a/cli/node/src/commands/utils.ts b/cli/node/src/commands/utils.ts index 07297d89e..8243ea520 100644 --- a/cli/node/src/commands/utils.ts +++ b/cli/node/src/commands/utils.ts @@ -3,133 +3,143 @@ */ import fs from "node:fs"; -import { printError, printSuccess, timedStatus, colors } from "../branding.js"; -import type { Backend } from "../backend/base.js"; -import { formatJsonEnvelope } from "../output.js"; import boxen from "boxen"; +import type { Backend } from "../backend/base.js"; +import { colors, printError, printSuccess, timedStatus } from "../branding.js"; +import { formatJsonEnvelope } from "../output.js"; +import { CLI_VERSION } from "../version.js"; const { brand, dim, success, error: errorColor } = colors; export async function cmdStatus( - backend: Backend, - opts: { userId?: string; agentId?: string; output?: string } = {}, + backend: Backend, + opts: { userId?: string; agentId?: string; output?: string } = {}, ): Promise { - const start = performance.now(); - let result: Record; - try { - result = await timedStatus("Checking connection...", async () => { - return backend.status({ userId: opts.userId, agentId: opts.agentId }); - }); - } catch (e) { - result = { connected: false, error: e instanceof Error ? e.message : String(e) }; - } - const elapsed = (performance.now() - start) / 1000; + const start = performance.now(); + let result: Record; + try { + result = await timedStatus("Checking connection...", async () => { + return backend.status({ userId: opts.userId, agentId: opts.agentId }); + }); + } catch (e) { + result = { + connected: false, + error: e instanceof Error ? e.message : String(e), + }; + } + const elapsed = (performance.now() - start) / 1000; - if (opts.output === "json") { - formatJsonEnvelope({ - command: "status", - data: { - connected: result.connected, - backend: result.backend ?? null, - base_url: result.base_url ?? null, - latency_ms: Math.round(elapsed * 1000), - }, - durationMs: Math.round(elapsed * 1000), - }); - return; - } + if (opts.output === "json") { + formatJsonEnvelope({ + command: "status", + data: { + connected: result.connected, + backend: result.backend ?? null, + base_url: result.base_url ?? null, + latency_ms: Math.round(elapsed * 1000), + }, + durationMs: Math.round(elapsed * 1000), + }); + return; + } - const lines: string[] = []; - if (result.connected) { - lines.push(` ${success("\u25cf")} Connected`); - } else { - lines.push(` ${errorColor("\u25cf")} Disconnected`); - } + const lines: string[] = []; + if (result.connected) { + lines.push(` ${success("\u25cf")} Connected`); + } else { + lines.push(` ${errorColor("\u25cf")} Disconnected`); + } - lines.push(` ${dim("Backend:")} ${result.backend ?? "?"}`); - if (result.base_url) { - lines.push(` ${dim("API URL:")} ${result.base_url}`); - } - if (result.error) { - lines.push(` ${errorColor("Error:")} ${result.error}`); - } - lines.push(` ${dim("Latency:")} ${elapsed.toFixed(2)}s`); + lines.push(` ${dim("Backend:")} ${result.backend ?? "?"}`); + if (result.base_url) { + lines.push(` ${dim("API URL:")} ${result.base_url}`); + } + if (result.error) { + lines.push(` ${errorColor("Error:")} ${result.error}`); + } + lines.push(` ${dim("Latency:")} ${elapsed.toFixed(2)}s`); - const content = lines.join("\n"); - console.log(); - console.log( - boxen(content, { - title: brand("Connection Status"), - titleAlignment: "left", - borderColor: "magenta", - padding: 1, - }), - ); - console.log(); + const content = lines.join("\n"); + console.log(); + console.log( + boxen(content, { + title: brand("Connection Status"), + titleAlignment: "left", + borderColor: "magenta", + padding: 1, + }), + ); + console.log(); } export function cmdVersion(): void { - console.log(` ${brand("◆ Mem0")} CLI v${__CLI_VERSION__}`); + console.log(` ${brand("◆ Mem0")} CLI v${CLI_VERSION}`); } export async function cmdImport( - backend: Backend, - filePath: string, - opts: { userId?: string; agentId?: string; output?: string }, + backend: Backend, + filePath: string, + opts: { userId?: string; agentId?: string; output?: string }, ): Promise { - let data: Record[]; - try { - const raw = fs.readFileSync(filePath, "utf-8"); - const parsed = JSON.parse(raw); - data = Array.isArray(parsed) ? parsed : [parsed]; - } catch (e) { - printError(`Failed to read file: ${e instanceof Error ? e.message : e}`); - process.exit(1); - } + let data: Record[]; + try { + const raw = fs.readFileSync(filePath, "utf-8"); + const parsed = JSON.parse(raw); + data = Array.isArray(parsed) ? parsed : [parsed]; + } catch (e) { + printError(`Failed to read file: ${e instanceof Error ? e.message : e}`); + process.exit(1); + } - let added = 0; - let failed = 0; - const start = performance.now(); + let added = 0; + let failed = 0; + const start = performance.now(); - for (let i = 0; i < data.length; i++) { - const item = data[i]; - const content = (item.memory ?? item.text ?? item.content ?? "") as string; - if (!content) { - failed++; - continue; - } + for (let i = 0; i < data.length; i++) { + const item = data[i]; + const content = (item.memory ?? item.text ?? item.content ?? "") as string; + if (!content) { + failed++; + continue; + } - try { - await backend.add(content, undefined, { - userId: opts.userId ?? (item.user_id as string | undefined), - agentId: opts.agentId ?? (item.agent_id as string | undefined), - metadata: item.metadata as Record | undefined, - }); - added++; - } catch { - failed++; - } + try { + await backend.add(content, undefined, { + userId: opts.userId ?? (item.user_id as string | undefined), + agentId: opts.agentId ?? (item.agent_id as string | undefined), + metadata: item.metadata as Record | undefined, + }); + added++; + } catch { + failed++; + } - // Simple progress indicator - if ((i + 1) % 10 === 0 || i === data.length - 1) { - process.stdout.write(`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`); - } - } + // Simple progress indicator + if ((i + 1) % 10 === 0 || i === data.length - 1) { + process.stdout.write( + `\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`, + ); + } + } - const elapsed = (performance.now() - start) / 1000; - console.log(); // Clear progress line + const elapsed = (performance.now() - start) / 1000; + console.log(); // Clear progress line - if (opts.output === "json") { - formatJsonEnvelope({ - command: "import", - data: { added, failed, duration_s: parseFloat(elapsed.toFixed(2)) }, - durationMs: Math.round(elapsed * 1000), - }); - return; - } + if (opts.output === "json") { + formatJsonEnvelope({ + command: "import", + data: { + added, + failed, + duration_s: Number.parseFloat(elapsed.toFixed(2)), + }, + durationMs: Math.round(elapsed * 1000), + }); + return; + } - printSuccess(`Imported ${added} memories (${elapsed.toFixed(2)}s)`); - if (failed > 0) { - printError(`${failed} memories failed to import.`); - } + printSuccess(`Imported ${added} memories (${elapsed.toFixed(2)}s)`); + if (failed > 0) { + printError(`${failed} memories failed to import.`); + } } diff --git a/cli/node/src/config.ts b/cli/node/src/config.ts index 91ef29353..e16221d7c 100644 --- a/cli/node/src/config.ts +++ b/cli/node/src/config.ts @@ -18,150 +18,158 @@ export const DEFAULT_BASE_URL = "https://api.mem0.ai"; export const CONFIG_VERSION = 1; export interface PlatformConfig { - apiKey: string; - baseUrl: string; + apiKey: string; + baseUrl: string; } export interface DefaultsConfig { - userId: string; - agentId: string; - appId: string; - runId: string; - enableGraph: boolean; + userId: string; + agentId: string; + appId: string; + runId: string; + enableGraph: boolean; } export interface Mem0Config { - version: number; - defaults: DefaultsConfig; - platform: PlatformConfig; + version: number; + defaults: DefaultsConfig; + platform: PlatformConfig; } export function createDefaultConfig(): Mem0Config { - return { - version: CONFIG_VERSION, - defaults: { - userId: "", - agentId: "", - appId: "", - runId: "", - enableGraph: false, - }, - platform: { - apiKey: "", - baseUrl: DEFAULT_BASE_URL, - }, - }; + return { + version: CONFIG_VERSION, + defaults: { + userId: "", + agentId: "", + appId: "", + runId: "", + enableGraph: false, + }, + platform: { + apiKey: "", + baseUrl: DEFAULT_BASE_URL, + }, + }; } export function ensureConfigDir(): string { - fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 }); - return CONFIG_DIR; + fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 }); + return CONFIG_DIR; } export function loadConfig(): Mem0Config { - const config = createDefaultConfig(); + const config = createDefaultConfig(); - if (fs.existsSync(CONFIG_FILE)) { - const raw = fs.readFileSync(CONFIG_FILE, "utf-8"); - const data = JSON.parse(raw); + if (fs.existsSync(CONFIG_FILE)) { + const raw = fs.readFileSync(CONFIG_FILE, "utf-8"); + const data = JSON.parse(raw); - config.version = data.version ?? CONFIG_VERSION; + config.version = data.version ?? CONFIG_VERSION; - const plat = data.platform ?? {}; - config.platform.apiKey = plat.api_key ?? ""; - config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL; + const plat = data.platform ?? {}; + config.platform.apiKey = plat.api_key ?? ""; + config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL; - const defaults = data.defaults ?? {}; - config.defaults.userId = defaults.user_id ?? ""; - config.defaults.agentId = defaults.agent_id ?? ""; - config.defaults.appId = defaults.app_id ?? ""; - config.defaults.runId = defaults.run_id ?? ""; - config.defaults.enableGraph = defaults.enable_graph ?? false; - } + const defaults = data.defaults ?? {}; + config.defaults.userId = defaults.user_id ?? ""; + config.defaults.agentId = defaults.agent_id ?? ""; + config.defaults.appId = defaults.app_id ?? ""; + config.defaults.runId = defaults.run_id ?? ""; + config.defaults.enableGraph = defaults.enable_graph ?? false; + } - // Environment variable overrides - if (process.env.MEM0_API_KEY) config.platform.apiKey = process.env.MEM0_API_KEY; - if (process.env.MEM0_BASE_URL) config.platform.baseUrl = process.env.MEM0_BASE_URL; - if (process.env.MEM0_USER_ID) config.defaults.userId = process.env.MEM0_USER_ID; - if (process.env.MEM0_AGENT_ID) config.defaults.agentId = process.env.MEM0_AGENT_ID; - if (process.env.MEM0_APP_ID) config.defaults.appId = process.env.MEM0_APP_ID; - if (process.env.MEM0_RUN_ID) config.defaults.runId = process.env.MEM0_RUN_ID; - if (process.env.MEM0_ENABLE_GRAPH) { - config.defaults.enableGraph = ["true", "1", "yes"].includes( - process.env.MEM0_ENABLE_GRAPH.toLowerCase(), - ); - } + // Environment variable overrides + if (process.env.MEM0_API_KEY) + config.platform.apiKey = process.env.MEM0_API_KEY; + if (process.env.MEM0_BASE_URL) + config.platform.baseUrl = process.env.MEM0_BASE_URL; + if (process.env.MEM0_USER_ID) + config.defaults.userId = process.env.MEM0_USER_ID; + if (process.env.MEM0_AGENT_ID) + config.defaults.agentId = process.env.MEM0_AGENT_ID; + if (process.env.MEM0_APP_ID) config.defaults.appId = process.env.MEM0_APP_ID; + if (process.env.MEM0_RUN_ID) config.defaults.runId = process.env.MEM0_RUN_ID; + if (process.env.MEM0_ENABLE_GRAPH) { + config.defaults.enableGraph = ["true", "1", "yes"].includes( + process.env.MEM0_ENABLE_GRAPH.toLowerCase(), + ); + } - return config; + return config; } export function saveConfig(config: Mem0Config): void { - ensureConfigDir(); + ensureConfigDir(); - const data = { - version: config.version, - defaults: { - user_id: config.defaults.userId, - agent_id: config.defaults.agentId, - app_id: config.defaults.appId, - run_id: config.defaults.runId, - enable_graph: config.defaults.enableGraph, - }, - platform: { - api_key: config.platform.apiKey, - base_url: config.platform.baseUrl, - }, - }; + const data = { + version: config.version, + defaults: { + user_id: config.defaults.userId, + agent_id: config.defaults.agentId, + app_id: config.defaults.appId, + run_id: config.defaults.runId, + enable_graph: config.defaults.enableGraph, + }, + platform: { + api_key: config.platform.apiKey, + base_url: config.platform.baseUrl, + }, + }; - fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2)); - fs.chmodSync(CONFIG_FILE, 0o600); + fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2)); + fs.chmodSync(CONFIG_FILE, 0o600); } export function redactKey(key: string): string { - if (!key) return "(not set)"; - if (key.length <= 8) return key.slice(0, 2) + "***"; - return key.slice(0, 4) + "..." + key.slice(-4); + if (!key) return "(not set)"; + if (key.length <= 8) return `${key.slice(0, 2)}***`; + return `${key.slice(0, 4)}...${key.slice(-4)}`; } /** Key map from dotted config path to the config object fields. */ const KEY_MAP: Record = { - "platform.api_key": ["platform", "apiKey"], - "platform.base_url": ["platform", "baseUrl"], - "defaults.user_id": ["defaults", "userId"], - "defaults.agent_id": ["defaults", "agentId"], - "defaults.app_id": ["defaults", "appId"], - "defaults.run_id": ["defaults", "runId"], - "defaults.enable_graph": ["defaults", "enableGraph"], - // Short-form aliases - "api_key": ["platform", "apiKey"], - "base_url": ["platform", "baseUrl"], - "user_id": ["defaults", "userId"], - "agent_id": ["defaults", "agentId"], - "app_id": ["defaults", "appId"], - "run_id": ["defaults", "runId"], - "enable_graph": ["defaults", "enableGraph"], + "platform.api_key": ["platform", "apiKey"], + "platform.base_url": ["platform", "baseUrl"], + "defaults.user_id": ["defaults", "userId"], + "defaults.agent_id": ["defaults", "agentId"], + "defaults.app_id": ["defaults", "appId"], + "defaults.run_id": ["defaults", "runId"], + "defaults.enable_graph": ["defaults", "enableGraph"], + // Short-form aliases + api_key: ["platform", "apiKey"], + base_url: ["platform", "baseUrl"], + user_id: ["defaults", "userId"], + agent_id: ["defaults", "agentId"], + app_id: ["defaults", "appId"], + run_id: ["defaults", "runId"], + enable_graph: ["defaults", "enableGraph"], }; export function getNestedValue(config: Mem0Config, dottedKey: string): unknown { - const mapping = KEY_MAP[dottedKey]; - if (!mapping) return undefined; - const [section, field] = mapping; - return (config[section] as unknown as Record)[field]; + const mapping = KEY_MAP[dottedKey]; + if (!mapping) return undefined; + const [section, field] = mapping; + return (config[section] as unknown as Record)[field]; } -export function setNestedValue(config: Mem0Config, dottedKey: string, value: string): boolean { - const mapping = KEY_MAP[dottedKey]; - if (!mapping) return false; - const [section, field] = mapping; - const obj = config[section] as unknown as Record; +export function setNestedValue( + config: Mem0Config, + dottedKey: string, + value: string, +): boolean { + const mapping = KEY_MAP[dottedKey]; + if (!mapping) return false; + const [section, field] = mapping; + const obj = config[section] as unknown as Record; - const current = obj[field]; - if (typeof current === "boolean") { - obj[field] = ["true", "1", "yes"].includes(value.toLowerCase()); - } else if (typeof current === "number") { - obj[field] = parseInt(value, 10); - } else { - obj[field] = value; - } - return true; + const current = obj[field]; + if (typeof current === "boolean") { + obj[field] = ["true", "1", "yes"].includes(value.toLowerCase()); + } else if (typeof current === "number") { + obj[field] = Number.parseInt(value, 10); + } else { + obj[field] = value; + } + return true; } diff --git a/cli/node/src/globals.d.ts b/cli/node/src/globals.d.ts index d51a5c52d..32a5c75cd 100644 --- a/cli/node/src/globals.d.ts +++ b/cli/node/src/globals.d.ts @@ -1,2 +1,2 @@ -/** Injected by tsup at build time from package.json version field. */ -declare const __CLI_VERSION__: string; +/** Injected by tsup at build time from package.json version field. Undefined in dev/test. */ +declare const __CLI_VERSION__: string | undefined; diff --git a/cli/node/src/help.ts b/cli/node/src/help.ts index 3dd3a491c..ad1971a63 100644 --- a/cli/node/src/help.ts +++ b/cli/node/src/help.ts @@ -4,40 +4,40 @@ */ import chalk from "chalk"; -import type { Command, Help, Option, Argument } from "commander"; +import type { Argument, Command, Help, Option } from "commander"; // Colors imported from chalk directly to match Typer/Rich defaults // ── Colors (matching Typer/Rich defaults) ──────────────────────────────── -const cyanBold = chalk.cyan.bold; // option flags, command names -const greenBold = chalk.green.bold; // switch flags (boolean --force etc) -const yellowBold = chalk.yellow.bold; // metavar -const yellow = chalk.yellow; // "Usage:" label -const bold = chalk.bold; // command name in usage -const dim = chalk.dim; // defaults, descriptions -const dimBorder = chalk.dim; // panel borders +const cyanBold = chalk.cyan.bold; // option flags, command names +const greenBold = chalk.green.bold; // switch flags (boolean --force etc) +const yellowBold = chalk.yellow.bold; // metavar +const yellow = chalk.yellow; // "Usage:" label +const bold = chalk.bold; // command name in usage +const dim = chalk.dim; // defaults, descriptions +const dimBorder = chalk.dim; // panel borders // ── Strip ANSI ─────────────────────────────────────────────────────────── -// eslint-disable-next-line no-control-regex +// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequence is intentional const ANSI_RE = /\x1b\[[0-9;]*m/g; function stripAnsi(str: string): number { - return str.replace(ANSI_RE, "").length; + return str.replace(ANSI_RE, "").length; } // ── Command display order (matches Python CLI) ────────────────────────── /** Commands grouped into panels, matching Python CLI's rich_help_panel. */ const COMMAND_GROUPS: { panel: string; commands: string[] }[] = [ - { - panel: "Memory", - commands: ["add", "search", "get", "list", "update", "delete"], - }, - { - panel: "Management", - commands: ["init", "status", "import", "help", "entity", "config"], - }, + { + panel: "Memory", + commands: ["add", "search", "get", "list", "update", "delete"], + }, + { + panel: "Management", + commands: ["init", "status", "import", "help", "entity", "config"], + }, ]; /** Flat order derived from COMMAND_GROUPS. */ @@ -46,88 +46,88 @@ const COMMAND_ORDER: string[] = COMMAND_GROUPS.flatMap((g) => g.commands); // ── Option-to-panel mapping (derived from Python's rich_help_panel) ───── const OPTION_PANELS: Record> = { - add: { - "--user-id": "Scope", - "--agent-id": "Scope", - "--app-id": "Scope", - "--run-id": "Scope", - "--output": "Output", - "--api-key": "Connection", - "--base-url": "Connection", - }, - search: { - "--user-id": "Scope", - "--agent-id": "Scope", - "--app-id": "Scope", - "--run-id": "Scope", - "--top-k": "Search", - "--threshold": "Search", - "--rerank": "Search", - "--keyword": "Search", - "--filter": "Search", - "--fields": "Search", - "--graph": "Search", - "--no-graph": "Search", - "--output": "Output", - "--api-key": "Connection", - "--base-url": "Connection", - }, - get: { - "--output": "Output", - "--api-key": "Connection", - "--base-url": "Connection", - }, - list: { - "--user-id": "Scope", - "--agent-id": "Scope", - "--app-id": "Scope", - "--run-id": "Scope", - "--page": "Pagination", - "--page-size": "Pagination", - "--category": "Filters", - "--after": "Filters", - "--before": "Filters", - "--graph": "Filters", - "--no-graph": "Filters", - "--output": "Output", - "--api-key": "Connection", - "--base-url": "Connection", - }, - update: { - "--output": "Output", - "--api-key": "Connection", - "--base-url": "Connection", - }, - delete: { - "--user-id": "Scope", - "--agent-id": "Scope", - "--app-id": "Scope", - "--run-id": "Scope", - "--output": "Output", - "--api-key": "Connection", - "--base-url": "Connection", - }, - status: { - "--output": "Output", - "--api-key": "Connection", - "--base-url": "Connection", - }, - import: { - "--user-id": "Scope", - "--agent-id": "Scope", - "--output": "Output", - "--api-key": "Connection", - "--base-url": "Connection", - }, + add: { + "--user-id": "Scope", + "--agent-id": "Scope", + "--app-id": "Scope", + "--run-id": "Scope", + "--output": "Output", + "--api-key": "Connection", + "--base-url": "Connection", + }, + search: { + "--user-id": "Scope", + "--agent-id": "Scope", + "--app-id": "Scope", + "--run-id": "Scope", + "--top-k": "Search", + "--threshold": "Search", + "--rerank": "Search", + "--keyword": "Search", + "--filter": "Search", + "--fields": "Search", + "--graph": "Search", + "--no-graph": "Search", + "--output": "Output", + "--api-key": "Connection", + "--base-url": "Connection", + }, + get: { + "--output": "Output", + "--api-key": "Connection", + "--base-url": "Connection", + }, + list: { + "--user-id": "Scope", + "--agent-id": "Scope", + "--app-id": "Scope", + "--run-id": "Scope", + "--page": "Pagination", + "--page-size": "Pagination", + "--category": "Filters", + "--after": "Filters", + "--before": "Filters", + "--graph": "Filters", + "--no-graph": "Filters", + "--output": "Output", + "--api-key": "Connection", + "--base-url": "Connection", + }, + update: { + "--output": "Output", + "--api-key": "Connection", + "--base-url": "Connection", + }, + delete: { + "--user-id": "Scope", + "--agent-id": "Scope", + "--app-id": "Scope", + "--run-id": "Scope", + "--output": "Output", + "--api-key": "Connection", + "--base-url": "Connection", + }, + status: { + "--output": "Output", + "--api-key": "Connection", + "--base-url": "Connection", + }, + import: { + "--user-id": "Scope", + "--agent-id": "Scope", + "--output": "Output", + "--api-key": "Connection", + "--base-url": "Connection", + }, }; const PANEL_ORDER: string[] = [ - "Scope", - "Search", - "Pagination", - "Filters", - "Output", - "Connection", + "Scope", + "Search", + "Pagination", + "Filters", + "Output", + "Connection", ]; // ── Panel rendering ───────────────────────────────────────────────────── @@ -141,234 +141,238 @@ const PANEL_ORDER: string[] = [ * ╰────────────────────────────────╯ * ``` */ -function renderPanel( - title: string, - rows: string[], - width: number, -): string { - if (rows.length === 0) return ""; +function renderPanel(title: string, rows: string[], width: number): string { + if (rows.length === 0) return ""; - // Inner width is total width minus the two border chars - const inner = width - 2; + // Inner width is total width minus the two border chars + const inner = width - 2; - // Top border: ╭─ Title ─...─╮ - const titleStr = ` ${title} `; - const fillLen = Math.max(0, inner - 1 - titleStr.length); - const topLine = - dimBorder("╭─") + - dimBorder(titleStr) + - dimBorder("─".repeat(fillLen)) + - dimBorder("╮"); + // Top border: ╭─ Title ─...─╮ + const titleStr = ` ${title} `; + const fillLen = Math.max(0, inner - 1 - titleStr.length); + const topLine = + dimBorder("╭─") + + dimBorder(titleStr) + + dimBorder("─".repeat(fillLen)) + + dimBorder("╮"); - // Bottom border: ╰─...─╯ - const bottomLine = dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯"); + // Bottom border: ╰─...─╯ + const bottomLine = + dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯"); - // Content rows - const contentLines = rows.map((row) => { - const visLen = stripAnsi(row); - const pad = Math.max(0, inner - 1 - visLen); - return dimBorder("│") + " " + row + " ".repeat(pad) + dimBorder("│"); - }); + // Content rows + const contentLines = rows.map((row) => { + const visLen = stripAnsi(row); + const pad = Math.max(0, inner - 1 - visLen); + return `${dimBorder("│")} ${row}${" ".repeat(pad)}${dimBorder("│")}`; + }); - return [topLine, ...contentLines, bottomLine].join("\n"); + return [topLine, ...contentLines, bottomLine].join("\n"); } // ── Format an option term (short + long) ──────────────────────────────── function formatOptionTerm(opt: Option): string { - const parts: string[] = []; - if (opt.short) parts.push(opt.short); - if (opt.long) parts.push(opt.long); - let term = parts.join(", "); + const parts: string[] = []; + if (opt.short) parts.push(opt.short); + if (opt.long) parts.push(opt.long); + let term = parts.join(", "); - // Append value placeholder for non-boolean options - if (opt.flags) { - const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/); - if (match) { - term += " " + match[0]; - } - } - return term; + // Append value placeholder for non-boolean options + if (opt.flags) { + const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/); + if (match) { + term += ` ${match[0]}`; + } + } + return term; } // ── Get the long flag name for panel lookup ───────────────────────────── function getLongFlag(opt: Option): string { - if (opt.long) return opt.long; - return opt.short || ""; + if (opt.long) return opt.long; + return opt.short || ""; } // ── Format a default value ────────────────────────────────────────────── function formatDefault(opt: Option): string { - if (opt.defaultValue !== undefined && opt.defaultValue !== false) { - return dim(` [default: ${opt.defaultValue}]`); - } - return ""; + if (opt.defaultValue !== undefined && opt.defaultValue !== false) { + return dim(` [default: ${opt.defaultValue}]`); + } + return ""; } // ── The main help formatter ───────────────────────────────────────────── export function richFormatHelp(cmd: Command, helper: Help): string { - const width = process.stdout.columns || 80; - const lines: string[] = []; + const width = process.stdout.columns || 80; + const lines: string[] = []; - const isRoot = !cmd.parent; + const isRoot = !cmd.parent; - // ── Usage line ── - const usage = helper.commandUsage(cmd); - lines.push(""); - if (isRoot) { - // Root: "Usage: mem0 [options]" — yellow, [options] bold - lines.push(` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("")} ${bold("[options]")}`); - } else { - // Subcommands: split into command path (bold) and args (yellow) - const usageParts = usage.split(" "); - const cmdPath: string[] = []; - const argParts: string[] = []; - let pastCmd = false; - for (const part of usageParts) { - if (!pastCmd && !part.startsWith("[") && !part.startsWith("<")) { - cmdPath.push(part); - } else { - pastCmd = true; - argParts.push(part); - } - } - lines.push(` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`); - } - lines.push(""); + // ── Usage line ── + const usage = helper.commandUsage(cmd); + lines.push(""); + if (isRoot) { + // Root: "Usage: mem0 [options]" — yellow, [options] bold + lines.push( + ` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("")} ${bold("[options]")}`, + ); + } else { + // Subcommands: split into command path (bold) and args (yellow) + const usageParts = usage.split(" "); + const cmdPath: string[] = []; + const argParts: string[] = []; + let pastCmd = false; + for (const part of usageParts) { + if (!pastCmd && !part.startsWith("[") && !part.startsWith("<")) { + cmdPath.push(part); + } else { + pastCmd = true; + argParts.push(part); + } + } + lines.push( + ` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`, + ); + } + lines.push(""); - // ── Description ── - const desc = helper.commandDescription(cmd); - if (desc) { - // Split multi-line descriptions (e.g., title + tagline) - const descLines = desc.split("\n"); - for (let i = 0; i < descLines.length; i++) { - const dLine = descLines[i]; - // First line is the title, subsequent non-empty lines are tagline (dimmed) - if (i === 0 || dLine.trim() === "") { - lines.push(` ${dLine}`); - } else { - lines.push(` ${dim(dLine)}`); - } - } - lines.push(""); - } + // ── Description ── + const desc = helper.commandDescription(cmd); + if (desc) { + // Split multi-line descriptions (e.g., title + tagline) + const descLines = desc.split("\n"); + for (let i = 0; i < descLines.length; i++) { + const dLine = descLines[i]; + // First line is the title, subsequent non-empty lines are tagline (dimmed) + if (i === 0 || dLine.trim() === "") { + lines.push(` ${dLine}`); + } else { + lines.push(` ${dim(dLine)}`); + } + } + lines.push(""); + } - // ── Arguments panel (subcommands only) ── - if (!isRoot) { - const visibleArgs = helper.visibleArguments(cmd); - if (visibleArgs.length > 0) { - const maxLen = Math.max(...visibleArgs.map((a: Argument) => a.name().length)); - const argRows = visibleArgs.map((a: Argument) => { - const name = cyanBold(a.name().padEnd(maxLen)); - const description = helper.argumentDescription(a); - return ` ${name} ${description}`; - }); - const panel = renderPanel("Arguments", argRows, width); - if (panel) lines.push(panel); - } - } + // ── Arguments panel (subcommands only) ── + if (!isRoot) { + const visibleArgs = helper.visibleArguments(cmd); + if (visibleArgs.length > 0) { + const maxLen = Math.max( + ...visibleArgs.map((a: Argument) => a.name().length), + ); + const argRows = visibleArgs.map((a: Argument) => { + const name = cyanBold(a.name().padEnd(maxLen)); + const description = helper.argumentDescription(a); + return ` ${name} ${description}`; + }); + const panel = renderPanel("Arguments", argRows, width); + if (panel) lines.push(panel); + } + } - // ── Collect options (grouped into panels for subcommands) ── - const visibleOpts = helper.visibleOptions(cmd); - const cmdName = cmd.name(); - const panelMap = (!isRoot && OPTION_PANELS[cmdName]) ? OPTION_PANELS[cmdName] : {}; + // ── Collect options (grouped into panels for subcommands) ── + const visibleOpts = helper.visibleOptions(cmd); + const cmdName = cmd.name(); + const panelMap = + !isRoot && OPTION_PANELS[cmdName] ? OPTION_PANELS[cmdName] : {}; - const grouped: Record = { Options: [] }; - for (const panelName of PANEL_ORDER) { - grouped[panelName] = []; - } + const grouped: Record = { Options: [] }; + for (const panelName of PANEL_ORDER) { + grouped[panelName] = []; + } - for (const opt of visibleOpts) { - const flag = getLongFlag(opt); - const panel = panelMap[flag]; - if (panel && PANEL_ORDER.includes(panel)) { - grouped[panel].push(opt); - } else { - grouped["Options"].push(opt); - } - } + for (const opt of visibleOpts) { + const flag = getLongFlag(opt); + const panel = panelMap[flag]; + if (panel && PANEL_ORDER.includes(panel)) { + grouped[panel].push(opt); + } else { + grouped.Options.push(opt); + } + } - // ── Collect commands ── - const visibleCmds = helper.visibleCommands(cmd); + // ── Collect commands ── + const visibleCmds = helper.visibleCommands(cmd); - if (isRoot) { - // ROOT: Options first, then command groups (matches Python/Typer ordering) - if (grouped["Options"].length > 0) { - const optRows = formatOptionRows(grouped["Options"]); - const panel = renderPanel("Options", optRows, width); - if (panel) lines.push(panel); - } - if (visibleCmds.length > 0) { - const cmdMap = new Map(visibleCmds.map((c) => [c.name(), c])); - for (const group of COMMAND_GROUPS) { - const groupCmds = group.commands - .map((name) => cmdMap.get(name)) - .filter((c): c is Command => c !== undefined); - if (groupCmds.length === 0) continue; - const maxLen = Math.max(...groupCmds.map((c) => c.name().length)); - const cmdRows = groupCmds.map((c) => { - const name = cyanBold(c.name().padEnd(maxLen)); - const description = helper.subcommandDescription(c); - return ` ${name} ${description}`; - }); - const panel = renderPanel(group.panel, cmdRows, width); - if (panel) lines.push(panel); - } - } - } else { - // SUBCOMMANDS: Options/panels first, then sub-subcommands - const panelSequence = ["Options", ...PANEL_ORDER]; - for (const panelName of panelSequence) { - const opts = grouped[panelName]; - if (opts && opts.length > 0) { - const optRows = formatOptionRows(opts); - const panel = renderPanel(panelName, optRows, width); - if (panel) lines.push(panel); - } - } - // Sub-subcommands (e.g., config show/get/set, entity list/delete) - if (visibleCmds.length > 0) { - const maxLen = Math.max(...visibleCmds.map((c) => c.name().length)); - const cmdRows = visibleCmds.map((c) => { - const name = cyanBold(c.name().padEnd(maxLen)); - const description = helper.subcommandDescription(c); - return ` ${name} ${description}`; - }); - const panel = renderPanel("Commands", cmdRows, width); - if (panel) lines.push(panel); - } - } + if (isRoot) { + // ROOT: Options first, then command groups (matches Python/Typer ordering) + if (grouped.Options.length > 0) { + const optRows = formatOptionRows(grouped.Options); + const panel = renderPanel("Options", optRows, width); + if (panel) lines.push(panel); + } + if (visibleCmds.length > 0) { + const cmdMap = new Map(visibleCmds.map((c) => [c.name(), c])); + for (const group of COMMAND_GROUPS) { + const groupCmds = group.commands + .map((name) => cmdMap.get(name)) + .filter((c): c is Command => c !== undefined); + if (groupCmds.length === 0) continue; + const maxLen = Math.max(...groupCmds.map((c) => c.name().length)); + const cmdRows = groupCmds.map((c) => { + const name = cyanBold(c.name().padEnd(maxLen)); + const description = helper.subcommandDescription(c); + return ` ${name} ${description}`; + }); + const panel = renderPanel(group.panel, cmdRows, width); + if (panel) lines.push(panel); + } + } + } else { + // SUBCOMMANDS: Options/panels first, then sub-subcommands + const panelSequence = ["Options", ...PANEL_ORDER]; + for (const panelName of panelSequence) { + const opts = grouped[panelName]; + if (opts && opts.length > 0) { + const optRows = formatOptionRows(opts); + const panel = renderPanel(panelName, optRows, width); + if (panel) lines.push(panel); + } + } + // Sub-subcommands (e.g., config show/get/set, entity list/delete) + if (visibleCmds.length > 0) { + const maxLen = Math.max(...visibleCmds.map((c) => c.name().length)); + const cmdRows = visibleCmds.map((c) => { + const name = cyanBold(c.name().padEnd(maxLen)); + const description = helper.subcommandDescription(c); + return ` ${name} ${description}`; + }); + const panel = renderPanel("Commands", cmdRows, width); + if (panel) lines.push(panel); + } + } - lines.push(""); - return lines.join("\n"); + lines.push(""); + return lines.join("\n"); } // ── Format option rows with aligned columns ───────────────────────────── function formatOptionRows(opts: Option[]): string[] { - const terms = opts.map((o) => formatOptionTerm(o)); - const maxTermLen = Math.max(...terms.map((t) => t.length)); + const terms = opts.map((o) => formatOptionTerm(o)); + const maxTermLen = Math.max(...terms.map((t) => t.length)); - return opts.map((opt, i) => { - const term = cyanBold(terms[i].padEnd(maxTermLen)); - const desc = opt.description || ""; - const def = formatDefault(opt); - return ` ${term} ${desc}${def}`; - }); + return opts.map((opt, i) => { + const term = cyanBold(terms[i].padEnd(maxTermLen)); + const desc = opt.description || ""; + const def = formatDefault(opt); + return ` ${term} ${desc}${def}`; + }); } // ── Sort commands by COMMAND_ORDER ────────────────────────────────────── function sortCommands(cmds: Command[]): Command[] { - return [...cmds].sort((a, b) => { - const ai = COMMAND_ORDER.indexOf(a.name()); - const bi = COMMAND_ORDER.indexOf(b.name()); - // Unknown commands go to end, preserving original order - const aIdx = ai === -1 ? COMMAND_ORDER.length : ai; - const bIdx = bi === -1 ? COMMAND_ORDER.length : bi; - return aIdx - bIdx; - }); + return [...cmds].sort((a, b) => { + const ai = COMMAND_ORDER.indexOf(a.name()); + const bi = COMMAND_ORDER.indexOf(b.name()); + // Unknown commands go to end, preserving original order + const aIdx = ai === -1 ? COMMAND_ORDER.length : ai; + const bIdx = bi === -1 ? COMMAND_ORDER.length : bi; + return aIdx - bIdx; + }); } diff --git a/cli/node/src/index.ts b/cli/node/src/index.ts index 4c0c7b8e9..8201eac00 100644 --- a/cli/node/src/index.ts +++ b/cli/node/src/index.ts @@ -8,38 +8,39 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { Command } from "commander"; +import { type Backend, getBackend } from "./backend/index.js"; +import { colors, printError } from "./branding.js"; import type { Mem0Config } from "./config.js"; import { loadConfig } from "./config.js"; -import { getBackend, type Backend } from "./backend/index.js"; -import { printError, colors } from "./branding.js"; import { richFormatHelp } from "./help.js"; +import { CLI_VERSION } from "./version.js"; const program = new Command(); // ── Helpers ────────────────────────────────────────────────────────────── function getBackendAndConfig( - apiKey?: string, - baseUrl?: string, + apiKey?: string, + baseUrl?: string, ): { backend: Backend; config: Mem0Config } { - const config = loadConfig(); + const config = loadConfig(); - if (apiKey) config.platform.apiKey = apiKey; - if (baseUrl) config.platform.baseUrl = baseUrl; + if (apiKey) config.platform.apiKey = apiKey; + if (baseUrl) config.platform.baseUrl = baseUrl; - if (!config.platform.apiKey) { - printError( - "No API key configured.", - "Run 'mem0 init' or set MEM0_API_KEY environment variable.", - ); - process.exit(1); - } + if (!config.platform.apiKey) { + printError( + "No API key configured.", + "Run 'mem0 init' or set MEM0_API_KEY environment variable.", + ); + process.exit(1); + } - return { backend: getBackend(config), config }; + return { backend: getBackend(config), config }; } function getBackendOnly(apiKey?: string, baseUrl?: string): Backend { - return getBackendAndConfig(apiKey, baseUrl).backend; + return getBackendAndConfig(apiKey, baseUrl).backend; } /** @@ -50,462 +51,580 @@ function getBackendOnly(apiKey?: string, baseUrl?: string): Backend { * If no explicit IDs, fall back to all configured defaults. */ function resolveIds( - config: Mem0Config, - opts: { - userId?: string; - agentId?: string; - appId?: string; - runId?: string; - }, + config: Mem0Config, + opts: { + userId?: string; + agentId?: string; + appId?: string; + runId?: string; + }, ): { userId?: string; agentId?: string; appId?: string; runId?: string } { - const hasExplicit = !!(opts.userId || opts.agentId || opts.appId || opts.runId); - if (hasExplicit) { - return { - userId: opts.userId || undefined, - agentId: opts.agentId || undefined, - appId: opts.appId || undefined, - runId: opts.runId || undefined, - }; - } - return { - userId: config.defaults.userId || undefined, - agentId: config.defaults.agentId || undefined, - appId: config.defaults.appId || undefined, - runId: config.defaults.runId || undefined, - }; + const hasExplicit = !!( + opts.userId || + opts.agentId || + opts.appId || + opts.runId + ); + if (hasExplicit) { + return { + userId: opts.userId || undefined, + agentId: opts.agentId || undefined, + appId: opts.appId || undefined, + runId: opts.runId || undefined, + }; + } + return { + userId: config.defaults.userId || undefined, + agentId: config.defaults.agentId || undefined, + appId: config.defaults.appId || undefined, + runId: config.defaults.runId || undefined, + }; } /** * Resolve graph tri-state: --no-graph > --graph > config default. */ function resolveGraph( - config: Mem0Config, - opts: { graph?: boolean; noGraph?: boolean }, + config: Mem0Config, + opts: { graph?: boolean; noGraph?: boolean }, ): boolean { - if (opts.noGraph) return false; - if (opts.graph) return true; - return config.defaults.enableGraph; + if (opts.noGraph) return false; + if (opts.graph) return true; + return config.defaults.enableGraph; } // ── Main program ────────────────────────────────────────────────────────── program - .name("mem0") - .description(`◆ Mem0 CLI v${__CLI_VERSION__} · Node.js SDK\n\nThe Memory Layer for AI Agents`) - .option("--version", "Show version and exit.") - .on("option:version", () => { - console.log(` ${colors.brand("◆ Mem0")} CLI v${__CLI_VERSION__}`); - process.exit(0); - }) - .usage(" [options]") - .helpOption("--help", "Show this message and exit.") - .addHelpCommand(false) - .configureHelp({ formatHelp: richFormatHelp }); + .name("mem0") + .description( + `◆ Mem0 CLI v${CLI_VERSION} · Node.js SDK\n\nThe Memory Layer for AI Agents`, + ) + .option("--version", "Show version and exit.") + .on("option:version", () => { + console.log(` ${colors.brand("◆ Mem0")} CLI v${CLI_VERSION}`); + process.exit(0); + }) + .usage(" [options]") + .helpOption("--help", "Show this message and exit.") + .addHelpCommand(false) + .configureHelp({ formatHelp: richFormatHelp }); // ── Init ────────────────────────────────────────────────────────────────── program - .command("init") - .description("Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).") - .option("--api-key ", "API key (skip prompt).") - .option("-u, --user-id ", "Default user ID (skip prompt).") - .option("--email ", "Login via email verification code.") - .option("--code ", "Verification code (use with --email for non-interactive login).") - .addHelpText( - "after", - "\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice\n $ mem0 init --email you@example.com\n $ mem0 init --email you@example.com --code 123456", - ) - .action(async (opts) => { - const { runInit } = await import("./commands/init.js"); - await runInit({ apiKey: opts.apiKey, userId: opts.userId, email: opts.email, code: opts.code }); - }); + .command("init") + .description( + "Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).", + ) + .option("--api-key ", "API key (skip prompt).") + .option("-u, --user-id ", "Default user ID (skip prompt).") + .option("--email ", "Login via email verification code.") + .option( + "--code ", + "Verification code (use with --email for non-interactive login).", + ) + .addHelpText( + "after", + "\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice\n $ mem0 init --email you@example.com\n $ mem0 init --email you@example.com --code 123456", + ) + .action(async (opts) => { + const { runInit } = await import("./commands/init.js"); + await runInit({ + apiKey: opts.apiKey, + userId: opts.userId, + email: opts.email, + code: opts.code, + }); + }); // ── Memory: add ─────────────────────────────────────────────────────────── program - .command("add [text]") - .description("Add a memory from text, messages, file, or stdin.") - .option("-u, --user-id ", "Scope to user.") - .option("--agent-id ", "Scope to agent.") - .option("--app-id ", "Scope to app.") - .option("--run-id ", "Scope to run.") - .option("--messages ", "Conversation messages as JSON.") - .option("-f, --file ", "Read messages from JSON file.") - .option("-m, --metadata ", "Custom metadata as JSON.") - .option("--immutable", "Prevent future updates.", false) - .option("--no-infer", "Skip inference, store raw.") - .option("--expires ", "Expiration date (YYYY-MM-DD).") - .option("--categories ", "Categories (JSON array or comma-separated).") - .option("--graph", "Enable graph memory extraction.", false) - .option("--no-graph", "Disable graph memory extraction.") - .option("-o, --output ", "Output format: text, json, quiet.", "text") - .option("--api-key ", "Override API key.") - .option("--base-url ", "Override API base URL.") - .addHelpText("after", '\nExamples:\n $ mem0 add "I prefer dark mode" --user-id alice\n $ echo "text" | mem0 add -u alice\n $ mem0 add --file msgs.json -u alice -o json') - .action(async (text, opts) => { - const { cmdAdd } = await import("./commands/memory.js"); - const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl); - const ids = resolveIds(config, opts); - const enableGraph = resolveGraph(config, opts); - await cmdAdd(backend, text, { ...ids, ...opts, enableGraph }); - }); + .command("add [text]") + .description("Add a memory from text, messages, file, or stdin.") + .option("-u, --user-id ", "Scope to user.") + .option("--agent-id ", "Scope to agent.") + .option("--app-id ", "Scope to app.") + .option("--run-id ", "Scope to run.") + .option("--messages ", "Conversation messages as JSON.") + .option("-f, --file ", "Read messages from JSON file.") + .option("-m, --metadata ", "Custom metadata as JSON.") + .option("--immutable", "Prevent future updates.", false) + .option("--no-infer", "Skip inference, store raw.") + .option("--expires ", "Expiration date (YYYY-MM-DD).") + .option("--categories ", "Categories (JSON array or comma-separated).") + .option("--graph", "Enable graph memory extraction.", false) + .option("--no-graph", "Disable graph memory extraction.") + .option("-o, --output ", "Output format: text, json, quiet.", "text") + .option("--api-key ", "Override API key.") + .option("--base-url ", "Override API base URL.") + .addHelpText( + "after", + '\nExamples:\n $ mem0 add "I prefer dark mode" --user-id alice\n $ echo "text" | mem0 add -u alice\n $ mem0 add --file msgs.json -u alice -o json', + ) + .action(async (text, opts) => { + const { cmdAdd } = await import("./commands/memory.js"); + const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl); + const ids = resolveIds(config, opts); + const enableGraph = resolveGraph(config, opts); + await cmdAdd(backend, text, { ...ids, ...opts, enableGraph }); + }); // ── Memory: search ──────────────────────────────────────────────────────── program - .command("search [query]") - .description("Search memories by semantic query.") - .option("-u, --user-id ", "Filter by user.") - .option("--agent-id ", "Filter by agent.") - .option("--app-id ", "Filter by app.") - .option("--run-id ", "Filter by run.") - .option("-k, --top-k ", "Number of results.", (v) => parseInt(v), 10) - .option("--threshold ", "Minimum similarity score.", (v) => parseFloat(v), 0.3) - .option("--rerank", "Enable reranking (Platform only).", false) - .option("--keyword", "Use keyword search.", false) - .option("--filter ", "Advanced filter expression (JSON).") - .option("--fields ", "Specific fields to return (comma-separated).") - .option("--graph", "Enable graph in search.", false) - .option("--no-graph", "Disable graph in search.") - .option("-o, --output ", "Output: text, json, table.", "text") - .option("--api-key ", "Override API key.") - .option("--base-url ", "Override API base URL.") - .addHelpText("after", '\nExamples:\n $ mem0 search "preferences" --user-id alice\n $ mem0 search "tools" -u alice -o json -k 5\n $ echo "preferences" | mem0 search -u alice') - .action(async (query, opts) => { - let resolvedQuery = query; - if (!resolvedQuery && !process.stdin.isTTY) { - resolvedQuery = fs.readFileSync(0, "utf-8").trim(); - } - if (!resolvedQuery) { - printError("No query provided. Pass a query argument or pipe via stdin."); - process.exit(1); - } - const { cmdSearch } = await import("./commands/memory.js"); - const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl); - const ids = resolveIds(config, opts); - const enableGraph = resolveGraph(config, opts); - await cmdSearch(backend, resolvedQuery, { - ...ids, - topK: opts.topK, - threshold: opts.threshold, - rerank: opts.rerank, - keyword: opts.keyword, - filterJson: opts.filter, - fields: opts.fields, - enableGraph, - output: opts.output, - }); - }); + .command("search [query]") + .description("Search memories by semantic query.") + .option("-u, --user-id ", "Filter by user.") + .option("--agent-id ", "Filter by agent.") + .option("--app-id ", "Filter by app.") + .option("--run-id ", "Filter by run.") + .option( + "-k, --top-k ", + "Number of results.", + (v) => Number.parseInt(v), + 10, + ) + .option( + "--threshold ", + "Minimum similarity score.", + (v) => Number.parseFloat(v), + 0.3, + ) + .option("--rerank", "Enable reranking (Platform only).", false) + .option("--keyword", "Use keyword search.", false) + .option("--filter ", "Advanced filter expression (JSON).") + .option("--fields ", "Specific fields to return (comma-separated).") + .option("--graph", "Enable graph in search.", false) + .option("--no-graph", "Disable graph in search.") + .option("-o, --output ", "Output: text, json, table.", "text") + .option("--api-key ", "Override API key.") + .option("--base-url ", "Override API base URL.") + .addHelpText( + "after", + '\nExamples:\n $ mem0 search "preferences" --user-id alice\n $ mem0 search "tools" -u alice -o json -k 5\n $ echo "preferences" | mem0 search -u alice', + ) + .action(async (query, opts) => { + let resolvedQuery = query; + if (!resolvedQuery && !process.stdin.isTTY) { + resolvedQuery = fs.readFileSync(0, "utf-8").trim(); + } + if (!resolvedQuery) { + printError("No query provided. Pass a query argument or pipe via stdin."); + process.exit(1); + } + const { cmdSearch } = await import("./commands/memory.js"); + const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl); + const ids = resolveIds(config, opts); + const enableGraph = resolveGraph(config, opts); + await cmdSearch(backend, resolvedQuery, { + ...ids, + topK: opts.topK, + threshold: opts.threshold, + rerank: opts.rerank, + keyword: opts.keyword, + filterJson: opts.filter, + fields: opts.fields, + enableGraph, + output: opts.output, + }); + }); // ── Memory: get ─────────────────────────────────────────────────────────── program - .command("get ") - .description("Get a specific memory by ID.") - .option("-o, --output ", "Output: text, json.", "text") - .option("--api-key ", "Override API key.") - .option("--base-url ", "Override API base URL.") - .addHelpText("after", "\nExamples:\n $ mem0 get abc-123-def-456\n $ mem0 get abc-123-def-456 -o json") - .action(async (memoryId, opts) => { - const { cmdGet } = await import("./commands/memory.js"); - const backend = getBackendOnly(opts.apiKey, opts.baseUrl); - await cmdGet(backend, memoryId, { output: opts.output }); - }); + .command("get ") + .description("Get a specific memory by ID.") + .option("-o, --output ", "Output: text, json.", "text") + .option("--api-key ", "Override API key.") + .option("--base-url ", "Override API base URL.") + .addHelpText( + "after", + "\nExamples:\n $ mem0 get abc-123-def-456\n $ mem0 get abc-123-def-456 -o json", + ) + .action(async (memoryId, opts) => { + const { cmdGet } = await import("./commands/memory.js"); + const backend = getBackendOnly(opts.apiKey, opts.baseUrl); + await cmdGet(backend, memoryId, { output: opts.output }); + }); // ── Memory: list ────────────────────────────────────────────────────────── program - .command("list") - .description("List memories with optional filters.") - .option("-u, --user-id ", "Filter by user.") - .option("--agent-id ", "Filter by agent.") - .option("--app-id ", "Filter by app.") - .option("--run-id ", "Filter by run.") - .option("--page ", "Page number.", (v) => parseInt(v), 1) - .option("--page-size ", "Results per page.", (v) => parseInt(v), 100) - .option("--category ", "Filter by category.") - .option("--after ", "Created after (YYYY-MM-DD).") - .option("--before ", "Created before (YYYY-MM-DD).") - .option("--graph", "Enable graph in listing.", false) - .option("--no-graph", "Disable graph in listing.") - .option("-o, --output ", "Output: text, json, table.", "table") - .option("--api-key ", "Override API key.") - .option("--base-url ", "Override API base URL.") - .addHelpText("after", "\nExamples:\n $ mem0 list -u alice\n $ mem0 list --category prefs --after 2024-01-01 -o json") - .action(async (opts) => { - const { cmdList } = await import("./commands/memory.js"); - const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl); - const ids = resolveIds(config, opts); - const enableGraph = resolveGraph(config, opts); - await cmdList(backend, { - ...ids, - page: opts.page, - pageSize: opts.pageSize, - category: opts.category, - after: opts.after, - before: opts.before, - enableGraph, - output: opts.output, - }); - }); + .command("list") + .description("List memories with optional filters.") + .option("-u, --user-id ", "Filter by user.") + .option("--agent-id ", "Filter by agent.") + .option("--app-id ", "Filter by app.") + .option("--run-id ", "Filter by run.") + .option("--page ", "Page number.", (v) => Number.parseInt(v), 1) + .option( + "--page-size ", + "Results per page.", + (v) => Number.parseInt(v), + 100, + ) + .option("--category ", "Filter by category.") + .option("--after ", "Created after (YYYY-MM-DD).") + .option("--before ", "Created before (YYYY-MM-DD).") + .option("--graph", "Enable graph in listing.", false) + .option("--no-graph", "Disable graph in listing.") + .option("-o, --output ", "Output: text, json, table.", "table") + .option("--api-key ", "Override API key.") + .option("--base-url ", "Override API base URL.") + .addHelpText( + "after", + "\nExamples:\n $ mem0 list -u alice\n $ mem0 list --category prefs --after 2024-01-01 -o json", + ) + .action(async (opts) => { + const { cmdList } = await import("./commands/memory.js"); + const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl); + const ids = resolveIds(config, opts); + const enableGraph = resolveGraph(config, opts); + await cmdList(backend, { + ...ids, + page: opts.page, + pageSize: opts.pageSize, + category: opts.category, + after: opts.after, + before: opts.before, + enableGraph, + output: opts.output, + }); + }); // ── Memory: update ──────────────────────────────────────────────────────── program - .command("update [text]") - .description("Update a memory's text or metadata.") - .option("-m, --metadata ", "Update metadata (JSON).") - .option("-o, --output ", "Output: text, json, quiet.", "text") - .option("--api-key ", "Override API key.") - .option("--base-url ", "Override API base URL.") - .addHelpText("after", `\nExamples:\n $ mem0 update abc-123 "new text"\n $ mem0 update abc-123 --metadata '{"key":"val"}'\n $ echo "new text" | mem0 update abc-123`) - .action(async (memoryId, text, opts) => { - let resolvedText = text; - if (!resolvedText && !opts.metadata && !process.stdin.isTTY) { - resolvedText = fs.readFileSync(0, "utf-8").trim(); - } - const { cmdUpdate } = await import("./commands/memory.js"); - const backend = getBackendOnly(opts.apiKey, opts.baseUrl); - await cmdUpdate(backend, memoryId, resolvedText, { metadata: opts.metadata, output: opts.output }); - }); + .command("update [text]") + .description("Update a memory's text or metadata.") + .option("-m, --metadata ", "Update metadata (JSON).") + .option("-o, --output ", "Output: text, json, quiet.", "text") + .option("--api-key ", "Override API key.") + .option("--base-url ", "Override API base URL.") + .addHelpText( + "after", + `\nExamples:\n $ mem0 update abc-123 "new text"\n $ mem0 update abc-123 --metadata '{"key":"val"}'\n $ echo "new text" | mem0 update abc-123`, + ) + .action(async (memoryId, text, opts) => { + let resolvedText = text; + if (!resolvedText && !opts.metadata && !process.stdin.isTTY) { + resolvedText = fs.readFileSync(0, "utf-8").trim(); + } + const { cmdUpdate } = await import("./commands/memory.js"); + const backend = getBackendOnly(opts.apiKey, opts.baseUrl); + await cmdUpdate(backend, memoryId, resolvedText, { + metadata: opts.metadata, + output: opts.output, + }); + }); // ── Memory: delete (consolidated) ───────────────────────────────────────── program - .command("delete [memoryId]") - .description("Delete a memory, all memories matching a scope, or an entity.") - .option("--all", "Delete all memories matching scope filters.", false) - .option("--entity", "Delete the entity itself and all its memories (cascade).", false) - .option("--project", "With --all: delete ALL memories project-wide.", false) - .option("--dry-run", "Show what would be deleted without deleting.", false) - .option("--force", "Skip confirmation.", false) - .option("-u, --user-id ", "Scope to user.") - .option("--agent-id ", "Scope to agent.") - .option("--app-id ", "Scope to app.") - .option("--run-id ", "Scope to run.") - .option("-o, --output ", "Output: text, json, quiet.", "text") - .option("--api-key ", "Override API key.") - .option("--base-url ", "Override API base URL.") - .addHelpText("after", [ - "\nExamples:", - " $ mem0 delete abc-123-def-456 # single memory", - " $ mem0 delete --all -u alice --force # all memories for user", - " $ mem0 delete --all --project --force # project-wide wipe", - " $ mem0 delete --entity -u alice --force # entity + all its memories", - ].join("\n")) - .action(async (memoryId, opts) => { - // ── Mutual-exclusion checks ── - if (memoryId && opts.all) { - printError("Cannot combine with --all. Use one or the other."); - process.exit(1); - } - if (memoryId && opts.entity) { - printError("Cannot combine with --entity. Use one or the other."); - process.exit(1); - } - if (opts.all && opts.entity) { - printError("Cannot combine --all with --entity. Use one or the other."); - process.exit(1); - } - if (!memoryId && !opts.all && !opts.entity) { - printError( - "Specify a memory ID, --all, or --entity.\n" + - " mem0 delete Delete a single memory\n" + - " mem0 delete --all [scope] Delete all memories matching scope\n" + - " mem0 delete --entity [scope] Delete an entity and all its memories", - ); - process.exit(1); - } + .command("delete [memoryId]") + .description("Delete a memory, all memories matching a scope, or an entity.") + .option("--all", "Delete all memories matching scope filters.", false) + .option( + "--entity", + "Delete the entity itself and all its memories (cascade).", + false, + ) + .option("--project", "With --all: delete ALL memories project-wide.", false) + .option("--dry-run", "Show what would be deleted without deleting.", false) + .option("--force", "Skip confirmation.", false) + .option("-u, --user-id ", "Scope to user.") + .option("--agent-id ", "Scope to agent.") + .option("--app-id ", "Scope to app.") + .option("--run-id ", "Scope to run.") + .option("-o, --output ", "Output: text, json, quiet.", "text") + .option("--api-key ", "Override API key.") + .option("--base-url ", "Override API base URL.") + .addHelpText( + "after", + [ + "\nExamples:", + " $ mem0 delete abc-123-def-456 # single memory", + " $ mem0 delete --all -u alice --force # all memories for user", + " $ mem0 delete --all --project --force # project-wide wipe", + " $ mem0 delete --entity -u alice --force # entity + all its memories", + ].join("\n"), + ) + .action(async (memoryId, opts) => { + // ── Mutual-exclusion checks ── + if (memoryId && opts.all) { + printError("Cannot combine with --all. Use one or the other."); + process.exit(1); + } + if (memoryId && opts.entity) { + printError( + "Cannot combine with --entity. Use one or the other.", + ); + process.exit(1); + } + if (opts.all && opts.entity) { + printError("Cannot combine --all with --entity. Use one or the other."); + process.exit(1); + } + if (!memoryId && !opts.all && !opts.entity) { + printError( + "Specify a memory ID, --all, or --entity.\n" + + " mem0 delete Delete a single memory\n" + + " mem0 delete --all [scope] Delete all memories matching scope\n" + + " mem0 delete --entity [scope] Delete an entity and all its memories", + ); + process.exit(1); + } - // ── Dispatch: single memory ── - if (memoryId) { - const { cmdDelete } = await import("./commands/memory.js"); - const backend = getBackendOnly(opts.apiKey, opts.baseUrl); - await cmdDelete(backend, memoryId, { output: opts.output, dryRun: opts.dryRun, force: opts.force }); - return; - } + // ── Dispatch: single memory ── + if (memoryId) { + const { cmdDelete } = await import("./commands/memory.js"); + const backend = getBackendOnly(opts.apiKey, opts.baseUrl); + await cmdDelete(backend, memoryId, { + output: opts.output, + dryRun: opts.dryRun, + force: opts.force, + }); + return; + } - // ── Dispatch: --all ── - if (opts.all) { - const { cmdDeleteAll } = await import("./commands/memory.js"); - const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl); - const ids = opts.project - ? { userId: undefined, agentId: undefined, appId: undefined, runId: undefined } - : resolveIds(config, opts); - await cmdDeleteAll(backend, { force: opts.force, dryRun: opts.dryRun, all: opts.project, ...ids, output: opts.output }); - return; - } + // ── Dispatch: --all ── + if (opts.all) { + const { cmdDeleteAll } = await import("./commands/memory.js"); + const { backend, config } = getBackendAndConfig( + opts.apiKey, + opts.baseUrl, + ); + const ids = opts.project + ? { + userId: undefined, + agentId: undefined, + appId: undefined, + runId: undefined, + } + : resolveIds(config, opts); + await cmdDeleteAll(backend, { + force: opts.force, + dryRun: opts.dryRun, + all: opts.project, + ...ids, + output: opts.output, + }); + return; + } - // ── Dispatch: --entity ── - if (opts.entity) { - const { cmdEntitiesDelete } = await import("./commands/entities.js"); - const backend = getBackendOnly(opts.apiKey, opts.baseUrl); - await cmdEntitiesDelete(backend, opts); - return; - } - }); + // ── Dispatch: --entity ── + if (opts.entity) { + const { cmdEntitiesDelete } = await import("./commands/entities.js"); + const backend = getBackendOnly(opts.apiKey, opts.baseUrl); + await cmdEntitiesDelete(backend, opts); + return; + } + }); // ── Config subcommands ──────────────────────────────────────────────────── const configCmd = program - .command("config") - .description("Manage mem0 configuration.") - .addHelpCommand(false); + .command("config") + .description("Manage mem0 configuration.") + .addHelpCommand(false); configCmd - .command("show") - .description("Display current configuration (secrets redacted).") - .option("-o, --output ", "Output: text, json.", "text") - .addHelpText("after", "\nExamples:\n $ mem0 config show\n $ mem0 config show -o json") - .action(async (opts) => { - const { cmdConfigShow } = await import("./commands/config.js"); - cmdConfigShow({ output: opts.output }); - }); + .command("show") + .description("Display current configuration (secrets redacted).") + .option("-o, --output ", "Output: text, json.", "text") + .addHelpText( + "after", + "\nExamples:\n $ mem0 config show\n $ mem0 config show -o json", + ) + .action(async (opts) => { + const { cmdConfigShow } = await import("./commands/config.js"); + cmdConfigShow({ output: opts.output }); + }); configCmd - .command("get ") - .description("Get a configuration value.") - .addHelpText("after", "\nExamples:\n $ mem0 config get platform.api_key\n $ mem0 config get defaults.user_id") - .action(async (key) => { - const { cmdConfigGet } = await import("./commands/config.js"); - cmdConfigGet(key); - }); + .command("get ") + .description("Get a configuration value.") + .addHelpText( + "after", + "\nExamples:\n $ mem0 config get platform.api_key\n $ mem0 config get defaults.user_id", + ) + .action(async (key) => { + const { cmdConfigGet } = await import("./commands/config.js"); + cmdConfigGet(key); + }); configCmd - .command("set ") - .description("Set a configuration value.") - .addHelpText("after", "\nExamples:\n $ mem0 config set defaults.user_id alice\n $ mem0 config set platform.base_url https://api.mem0.ai") - .action(async (key, value) => { - const { cmdConfigSet } = await import("./commands/config.js"); - cmdConfigSet(key, value); - }); + .command("set ") + .description("Set a configuration value.") + .addHelpText( + "after", + "\nExamples:\n $ mem0 config set defaults.user_id alice\n $ mem0 config set platform.base_url https://api.mem0.ai", + ) + .action(async (key, value) => { + const { cmdConfigSet } = await import("./commands/config.js"); + cmdConfigSet(key, value); + }); // ── Entity subcommand group ─────────────────────────────────────────────── const entityCmd = program - .command("entity") - .description("Manage entities.") - .addHelpCommand(false) - .configureHelp({ formatHelp: richFormatHelp }); + .command("entity") + .description("Manage entities.") + .addHelpCommand(false) + .configureHelp({ formatHelp: richFormatHelp }); entityCmd - .command("list ") - .description("List all entities of a given type.") - .option("-o, --output ", "Output: table, json.", "table") - .option("--api-key ", "Override API key.") - .option("--base-url ", "Override API base URL.") - .addHelpText("after", "\nExamples:\n $ mem0 entity list users\n $ mem0 entity list agents -o json") - .action(async (entityType, opts) => { - const { cmdEntitiesList } = await import("./commands/entities.js"); - const backend = getBackendOnly(opts.apiKey, opts.baseUrl); - await cmdEntitiesList(backend, entityType, { output: opts.output }); - }); + .command("list ") + .description("List all entities of a given type.") + .option("-o, --output ", "Output: table, json.", "table") + .option("--api-key ", "Override API key.") + .option("--base-url ", "Override API base URL.") + .addHelpText( + "after", + "\nExamples:\n $ mem0 entity list users\n $ mem0 entity list agents -o json", + ) + .action(async (entityType, opts) => { + const { cmdEntitiesList } = await import("./commands/entities.js"); + const backend = getBackendOnly(opts.apiKey, opts.baseUrl); + await cmdEntitiesList(backend, entityType, { output: opts.output }); + }); entityCmd - .command("delete") - .description("Delete an entity and ALL its memories (cascade).") - .option("--dry-run", "Show what would be deleted without deleting.", false) - .option("-u, --user-id ", "Scope to user.") - .option("--agent-id ", "Scope to agent.") - .option("--app-id ", "Scope to app.") - .option("--run-id ", "Scope to run.") - .option("--force", "Skip confirmation.", false) - .option("-o, --output ", "Output: text, json, quiet.", "text") - .option("--api-key ", "Override API key.") - .option("--base-url ", "Override API base URL.") - .addHelpText("after", "\nExamples:\n $ mem0 entity delete --user-id alice --force\n $ mem0 entity delete --user-id alice --dry-run") - .action(async (opts) => { - const { cmdEntitiesDelete } = await import("./commands/entities.js"); - const backend = getBackendOnly(opts.apiKey, opts.baseUrl); - await cmdEntitiesDelete(backend, opts); - }); + .command("delete") + .description("Delete an entity and ALL its memories (cascade).") + .option("--dry-run", "Show what would be deleted without deleting.", false) + .option("-u, --user-id ", "Scope to user.") + .option("--agent-id ", "Scope to agent.") + .option("--app-id ", "Scope to app.") + .option("--run-id ", "Scope to run.") + .option("--force", "Skip confirmation.", false) + .option("-o, --output ", "Output: text, json, quiet.", "text") + .option("--api-key ", "Override API key.") + .option("--base-url ", "Override API base URL.") + .addHelpText( + "after", + "\nExamples:\n $ mem0 entity delete --user-id alice --force\n $ mem0 entity delete --user-id alice --dry-run", + ) + .action(async (opts) => { + const { cmdEntitiesDelete } = await import("./commands/entities.js"); + const backend = getBackendOnly(opts.apiKey, opts.baseUrl); + await cmdEntitiesDelete(backend, opts); + }); // ── Utility commands ────────────────────────────────────────────────────── program - .command("status") - .description("Check connectivity and authentication.") - .option("-o, --output ", "Output: text, json.", "text") - .option("--api-key ", "Override API key.") - .option("--base-url ", "Override API base URL.") - .addHelpText("after", "\nExamples:\n $ mem0 status\n $ mem0 status -o json") - .action(async (opts) => { - const { cmdStatus } = await import("./commands/utils.js"); - const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl); - await cmdStatus(backend, { - userId: config.defaults.userId || undefined, - agentId: config.defaults.agentId || undefined, - output: opts.output, - }); - }); - + .command("status") + .description("Check connectivity and authentication.") + .option("-o, --output ", "Output: text, json.", "text") + .option("--api-key ", "Override API key.") + .option("--base-url ", "Override API base URL.") + .addHelpText("after", "\nExamples:\n $ mem0 status\n $ mem0 status -o json") + .action(async (opts) => { + const { cmdStatus } = await import("./commands/utils.js"); + const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl); + await cmdStatus(backend, { + userId: config.defaults.userId || undefined, + agentId: config.defaults.agentId || undefined, + output: opts.output, + }); + }); program - .command("import ") - .description("Import memories from a JSON file.") - .option("-u, --user-id ", "Override user ID.") - .option("--agent-id ", "Override agent ID.") - .option("-o, --output ", "Output: text, json.", "text") - .option("--api-key ", "Override API key.") - .option("--base-url ", "Override API base URL.") - .addHelpText("after", "\nExamples:\n $ mem0 import data.json --user-id alice\n $ mem0 import data.json -u alice -o json") - .action(async (filePath, opts) => { - const { cmdImport } = await import("./commands/utils.js"); - const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl); - const ids = resolveIds(config, opts); - await cmdImport(backend, filePath, { userId: ids.userId, agentId: ids.agentId, output: opts.output }); - }); + .command("import ") + .description("Import memories from a JSON file.") + .option("-u, --user-id ", "Override user ID.") + .option("--agent-id ", "Override agent ID.") + .option("-o, --output ", "Output: text, json.", "text") + .option("--api-key ", "Override API key.") + .option("--base-url ", "Override API base URL.") + .addHelpText( + "after", + "\nExamples:\n $ mem0 import data.json --user-id alice\n $ mem0 import data.json -u alice -o json", + ) + .action(async (filePath, opts) => { + const { cmdImport } = await import("./commands/utils.js"); + const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl); + const ids = resolveIds(config, opts); + await cmdImport(backend, filePath, { + userId: ids.userId, + agentId: ids.agentId, + output: opts.output, + }); + }); // ── Help (machine-readable) ────────────────────────────────────────────── program - .command("help") - .description("Show help. Use --json for machine-readable output (for LLM agents).") - .option("--json", "Output machine-readable JSON for LLM agents.", false) - .addHelpText("after", "\nExamples:\n $ mem0 help\n $ mem0 help --json") - .action((opts) => { - if (opts.json) { - // Load spec from parent directory - const __dirname = path.dirname(fileURLToPath(import.meta.url)); - const specPath = path.join(__dirname, "..", "..", "cli-spec.json"); - if (fs.existsSync(specPath)) { - const spec = JSON.parse(fs.readFileSync(specPath, "utf-8")); - console.log(JSON.stringify(spec, null, 2)); - } else { - console.log(JSON.stringify({ name: "mem0", version: __CLI_VERSION__, description: "The Memory Layer for AI Agents" }, null, 2)); - } - } else { - const { brand: b } = colors; - console.log(`${b("◆ Mem0 CLI")} v${__CLI_VERSION__} · Node.js SDK\n The Memory Layer for AI Agents\n`); - console.log("Usage: mem0 [OPTIONS]\n"); - console.log("Commands:"); - console.log(" add Add a memory from text, messages, file, or stdin"); - console.log(" search Search memories by semantic query"); - console.log(" get Get a specific memory by ID"); - console.log(" list List memories with optional filters"); - console.log(" update Update a memory's text or metadata"); - console.log(" delete Delete a memory, all memories, or an entity"); - console.log(" import Import memories from a JSON file"); - console.log(" config Manage configuration (show, get, set)"); - console.log(" entity Manage entities (list, delete)"); - console.log(" init Interactive setup wizard"); - console.log(" status Check connectivity and authentication"); - console.log(); - console.log(" mem0 --help Get help for a command"); - console.log(" mem0 help --json Machine-readable help (for LLM agents)"); - console.log(); - } - }); + .command("help") + .description( + "Show help. Use --json for machine-readable output (for LLM agents).", + ) + .option("--json", "Output machine-readable JSON for LLM agents.", false) + .addHelpText("after", "\nExamples:\n $ mem0 help\n $ mem0 help --json") + .action((opts) => { + if (opts.json) { + // Load spec from parent directory + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const specPath = path.join(__dirname, "..", "..", "cli-spec.json"); + if (fs.existsSync(specPath)) { + const spec = JSON.parse(fs.readFileSync(specPath, "utf-8")); + console.log(JSON.stringify(spec, null, 2)); + } else { + console.log( + JSON.stringify( + { + name: "mem0", + version: CLI_VERSION, + description: "The Memory Layer for AI Agents", + }, + null, + 2, + ), + ); + } + } else { + const { brand: b } = colors; + console.log( + `${b("◆ Mem0 CLI")} v${CLI_VERSION} · Node.js SDK\n The Memory Layer for AI Agents\n`, + ); + console.log("Usage: mem0 [OPTIONS]\n"); + console.log("Commands:"); + console.log( + " add Add a memory from text, messages, file, or stdin", + ); + console.log(" search Search memories by semantic query"); + console.log(" get Get a specific memory by ID"); + console.log(" list List memories with optional filters"); + console.log(" update Update a memory's text or metadata"); + console.log( + " delete Delete a memory, all memories, or an entity", + ); + console.log(" import Import memories from a JSON file"); + console.log(" config Manage configuration (show, get, set)"); + console.log(" entity Manage entities (list, delete)"); + console.log(" init Interactive setup wizard"); + console.log(" status Check connectivity and authentication"); + console.log(); + console.log(" mem0 --help Get help for a command"); + console.log( + " mem0 help --json Machine-readable help (for LLM agents)", + ); + console.log(); + } + }); // ── Version ─────────────────────────────────────────────────────────────── program - .command("version") - .description("Show version.") - .action(async () => { - const { cmdVersion } = await import("./commands/utils.js"); - cmdVersion(); - }); + .command("version") + .description("Show version.") + .action(async () => { + const { cmdVersion } = await import("./commands/utils.js"); + cmdVersion(); + }); // ── Entrypoint ──────────────────────────────────────────────────────────── diff --git a/cli/node/src/output.ts b/cli/node/src/output.ts index aa35e16cc..a3cff7b10 100644 --- a/cli/node/src/output.ts +++ b/cli/node/src/output.ts @@ -2,243 +2,259 @@ * Output formatting for mem0 CLI — text, JSON, table, quiet modes. */ -import Table from "cli-table3"; import boxen from "boxen"; +import Table from "cli-table3"; import { colors, sym } from "./branding.js"; const { brand, accent, success, error: errorColor, dim } = colors; function formatDate(dtStr?: string): string | undefined { - if (!dtStr) return undefined; - try { - const dt = new Date(dtStr.replace("Z", "+00:00")); - return dt.toISOString().slice(0, 10); - } catch { - return dtStr?.slice(0, 10); - } + if (!dtStr) return undefined; + try { + const dt = new Date(dtStr.replace("Z", "+00:00")); + return dt.toISOString().slice(0, 10); + } catch { + return dtStr?.slice(0, 10); + } } export function formatMemoriesText( - memories: Record[], - title = "memories", + memories: Record[], + title = "memories", ): void { - const count = memories.length; - console.log(`\n${brand(`Found ${count} ${title}:`)}\n`); + const count = memories.length; + console.log(`\n${brand(`Found ${count} ${title}:`)}\n`); - for (let i = 0; i < memories.length; i++) { - const mem = memories[i]; - const memoryText = (mem.memory ?? mem.text ?? "") as string; - const memId = ((mem.id as string) ?? "").slice(0, 8); - const score = mem.score as number | undefined; - const created = formatDate(mem.created_at as string | undefined); - let category: string | undefined; - const cats = mem.categories; - if (Array.isArray(cats)) { - category = cats[0] as string | undefined; - } + for (let i = 0; i < memories.length; i++) { + const mem = memories[i]; + const memoryText = (mem.memory ?? mem.text ?? "") as string; + const memId = ((mem.id as string) ?? "").slice(0, 8); + const score = mem.score as number | undefined; + const created = formatDate(mem.created_at as string | undefined); + let category: string | undefined; + const cats = mem.categories; + if (Array.isArray(cats)) { + category = cats[0] as string | undefined; + } - console.log(` ${i + 1}. ${memoryText}`); + console.log(` ${i + 1}. ${memoryText}`); - const details: string[] = []; - if (score !== undefined) details.push(`Score: ${score.toFixed(2)}`); - if (memId) details.push(`ID: ${memId}`); - if (created) details.push(`Created: ${created}`); - if (category) details.push(`Category: ${category}`); + const details: string[] = []; + if (score !== undefined) details.push(`Score: ${score.toFixed(2)}`); + if (memId) details.push(`ID: ${memId}`); + if (created) details.push(`Created: ${created}`); + if (category) details.push(`Category: ${category}`); - if (details.length > 0) { - console.log(` ${dim(details.join(" · "))}`); - } - console.log(); - } + if (details.length > 0) { + console.log(` ${dim(details.join(" · "))}`); + } + console.log(); + } } -export function formatMemoriesTable(memories: Record[], opts: { showScore?: boolean } = {}): void { - const head = opts.showScore - ? [accent("ID"), accent("Score"), accent("Memory"), accent("Category"), accent("Created")] - : [accent("ID"), accent("Memory"), accent("Category"), accent("Created")]; - const colWidths = opts.showScore - ? [38, 8, 40, 16, 14] - : [38, 40, 16, 14]; - const table = new Table({ - head, - colWidths, - wordWrap: true, - style: { head: [], border: [] }, - }); +export function formatMemoriesTable( + memories: Record[], + opts: { showScore?: boolean } = {}, +): void { + const head = opts.showScore + ? [ + accent("ID"), + accent("Score"), + accent("Memory"), + accent("Category"), + accent("Created"), + ] + : [accent("ID"), accent("Memory"), accent("Category"), accent("Created")]; + const colWidths = opts.showScore ? [38, 8, 40, 16, 14] : [38, 40, 16, 14]; + const table = new Table({ + head, + colWidths, + wordWrap: true, + style: { head: [], border: [] }, + }); - for (const mem of memories) { - const memId = ((mem.id as string) ?? ""); - let memoryText = (mem.memory ?? mem.text ?? "") as string; - if (memoryText.length > 60) { - memoryText = memoryText.slice(0, 57) + "..."; - } - const categories = mem.categories; - const cat = - Array.isArray(categories) && categories.length > 0 - ? categories.length > 1 - ? `${categories[0]} (+${categories.length - 1})` - : (categories[0] as string) - : "—"; - const created = formatDate(mem.created_at as string | undefined) ?? "—"; - if (opts.showScore) { - const score = mem.score as number | undefined; - const scoreStr = score !== undefined ? score.toFixed(2) : "—"; - table.push([dim(memId), scoreStr, memoryText, cat, created]); - } else { - table.push([dim(memId), memoryText, cat, created]); - } - } + for (const mem of memories) { + const memId = (mem.id as string) ?? ""; + let memoryText = (mem.memory ?? mem.text ?? "") as string; + if (memoryText.length > 60) { + memoryText = `${memoryText.slice(0, 57)}...`; + } + const categories = mem.categories; + const cat = + Array.isArray(categories) && categories.length > 0 + ? categories.length > 1 + ? `${categories[0]} (+${categories.length - 1})` + : (categories[0] as string) + : "—"; + const created = formatDate(mem.created_at as string | undefined) ?? "—"; + if (opts.showScore) { + const score = mem.score as number | undefined; + const scoreStr = score !== undefined ? score.toFixed(2) : "—"; + table.push([dim(memId), scoreStr, memoryText, cat, created]); + } else { + table.push([dim(memId), memoryText, cat, created]); + } + } - console.log(); - console.log(table.toString()); - console.log(); + console.log(); + console.log(table.toString()); + console.log(); } export function formatJson(data: unknown): void { - console.log(JSON.stringify(data, null, 2)); + console.log(JSON.stringify(data, null, 2)); } export function formatSingleMemory( - mem: Record, - output = "text", + mem: Record, + output = "text", ): void { - if (output === "json") { - formatJson(mem); - return; - } + if (output === "json") { + formatJson(mem); + return; + } - const memoryText = (mem.memory ?? mem.text ?? "") as string; - const memId = (mem.id ?? "") as string; + const memoryText = (mem.memory ?? mem.text ?? "") as string; + const memId = (mem.id ?? "") as string; - const lines: string[] = []; - lines.push(` ${memoryText}`); - lines.push(""); + const lines: string[] = []; + lines.push(` ${memoryText}`); + lines.push(""); - if (memId) lines.push(` ${dim("ID:")} ${memId}`); - const created = formatDate(mem.created_at as string | undefined); - if (created) lines.push(` ${dim("Created:")} ${created}`); - const updated = formatDate(mem.updated_at as string | undefined); - if (updated) lines.push(` ${dim("Updated:")} ${updated}`); - const meta = mem.metadata; - if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`); - const categories = mem.categories; - if (categories) { - const catStr = Array.isArray(categories) ? categories.join(", ") : String(categories); - lines.push(` ${dim("Categories:")} ${catStr}`); - } + if (memId) lines.push(` ${dim("ID:")} ${memId}`); + const created = formatDate(mem.created_at as string | undefined); + if (created) lines.push(` ${dim("Created:")} ${created}`); + const updated = formatDate(mem.updated_at as string | undefined); + if (updated) lines.push(` ${dim("Updated:")} ${updated}`); + const meta = mem.metadata; + if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`); + const categories = mem.categories; + if (categories) { + const catStr = Array.isArray(categories) + ? categories.join(", ") + : String(categories); + lines.push(` ${dim("Categories:")} ${catStr}`); + } - const content = lines.join("\n"); - console.log(); - console.log( - boxen(content, { - title: brand("Memory"), - titleAlignment: "left", - borderColor: "magenta", - padding: 1, - }), - ); - console.log(); + const content = lines.join("\n"); + console.log(); + console.log( + boxen(content, { + title: brand("Memory"), + titleAlignment: "left", + borderColor: "magenta", + padding: 1, + }), + ); + console.log(); } export function formatAddResult( - result: Record | Record[], - output = "text", + result: Record | Record[], + output = "text", ): void { - if (output === "json") { - formatJson(result); - return; - } - if (output === "quiet") return; + if (output === "json") { + formatJson(result); + return; + } + if (output === "quiet") return; - const results: Record[] = Array.isArray(result) - ? result - : ((result.results as Record[]) ?? [result]); + const results: Record[] = Array.isArray(result) + ? result + : ((result.results as Record[]) ?? [result]); - if (!results.length) { - console.log(` ${dim("No memories extracted.")}`); - return; - } + if (!results.length) { + console.log(` ${dim("No memories extracted.")}`); + return; + } - console.log(); - for (const r of results) { - // Detect async PENDING response - if (r.status === "PENDING") { - const eventId = ((r.event_id as string) ?? "").slice(0, 8); - const icon = accent(sym("⧗", "...")); - const parts = [` ${icon} ${dim("Queued".padEnd(10))}`, "Processing in background"]; - if (eventId) parts.push(dim(`(event ${eventId})`)); - console.log(parts.join(" ")); - continue; - } + console.log(); + for (const r of results) { + // Detect async PENDING response + if (r.status === "PENDING") { + const eventId = ((r.event_id as string) ?? "").slice(0, 8); + const icon = accent(sym("⧗", "...")); + const parts = [ + ` ${icon} ${dim("Queued".padEnd(10))}`, + "Processing in background", + ]; + if (eventId) parts.push(dim(`(event ${eventId})`)); + console.log(parts.join(" ")); + continue; + } - const event = (r.event ?? "ADD") as string; - const memory = (r.memory ?? r.text ?? r.content ?? r.data ?? "") as string; - const memId = ((r.id as string) ?? (r.memory_id as string) ?? "").slice(0, 8); + const event = (r.event ?? "ADD") as string; + const memory = (r.memory ?? r.text ?? r.content ?? r.data ?? "") as string; + const memId = ((r.id as string) ?? (r.memory_id as string) ?? "").slice( + 0, + 8, + ); - let icon: string; - let label: string; - if (event === "ADD") { - icon = success("+"); - label = "Added"; - } else if (event === "UPDATE") { - icon = accent("~"); - label = "Updated"; - } else if (event === "DELETE") { - icon = errorColor("-"); - label = "Deleted"; - } else if (event === "NOOP") { - icon = dim("·"); - label = "No change"; - } else { - icon = dim("?"); - label = event; - } + let icon: string; + let label: string; + if (event === "ADD") { + icon = success("+"); + label = "Added"; + } else if (event === "UPDATE") { + icon = accent("~"); + label = "Updated"; + } else if (event === "DELETE") { + icon = errorColor("-"); + label = "Deleted"; + } else if (event === "NOOP") { + icon = dim("·"); + label = "No change"; + } else { + icon = dim("?"); + label = event; + } - const parts = [` ${icon} ${dim(label.padEnd(10))}`]; - if (memory) parts.push(memory); - if (memId) parts.push(dim(`(${memId})`)); - console.log(parts.join(" ")); - } - console.log(); + const parts = [` ${icon} ${dim(label.padEnd(10))}`]; + if (memory) parts.push(memory); + if (memId) parts.push(dim(`(${memId})`)); + console.log(parts.join(" ")); + } + console.log(); } export function formatJsonEnvelope(opts: { - command: string; - data: unknown; - durationMs?: number; - scope?: Record; - count?: number; - status?: string; - error?: string; + command: string; + data: unknown; + durationMs?: number; + scope?: Record; + count?: number; + status?: string; + error?: string; }): void { - const envelope: Record = { - status: opts.status ?? "success", - command: opts.command, - }; - if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs; - if (opts.scope !== undefined) envelope.scope = opts.scope; - if (opts.count !== undefined) envelope.count = opts.count; - if (opts.error) envelope.error = opts.error; - envelope.data = opts.data; - console.log(JSON.stringify(envelope, null, 2)); + const envelope: Record = { + status: opts.status ?? "success", + command: opts.command, + }; + if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs; + if (opts.scope !== undefined) envelope.scope = opts.scope; + if (opts.count !== undefined) envelope.count = opts.count; + if (opts.error) envelope.error = opts.error; + envelope.data = opts.data; + console.log(JSON.stringify(envelope, null, 2)); } export function printResultSummary(opts: { - count: number; - durationSecs?: number; - page?: number; - scopeIds?: Record; + count: number; + durationSecs?: number; + page?: number; + scopeIds?: Record; }): void { - const parts = [`${opts.count} result${opts.count !== 1 ? "s" : ""}`]; - if (opts.page !== undefined) parts.push(`page ${opts.page}`); - if (opts.scopeIds) { - const scopeParts = Object.entries(opts.scopeIds) - .filter(([, v]) => v) - .map(([k, v]) => `${k.replace(/_/g, " ")}=${v}`); - if (scopeParts.length > 0) parts.push(scopeParts.join(", ")); - } - if (opts.durationSecs !== undefined) parts.push(`${opts.durationSecs.toFixed(2)}s`); + const parts = [`${opts.count} result${opts.count !== 1 ? "s" : ""}`]; + if (opts.page !== undefined) parts.push(`page ${opts.page}`); + if (opts.scopeIds) { + const scopeParts = Object.entries(opts.scopeIds) + .filter(([, v]) => v) + .map(([k, v]) => `${k.replace(/_/g, " ")}=${v}`); + if (scopeParts.length > 0) parts.push(scopeParts.join(", ")); + } + if (opts.durationSecs !== undefined) + parts.push(`${opts.durationSecs.toFixed(2)}s`); - console.log(` ${dim(parts.join(" · "))}`); - console.log(); + console.log(` ${dim(parts.join(" · "))}`); + console.log(); } diff --git a/cli/node/src/version.ts b/cli/node/src/version.ts new file mode 100644 index 000000000..bcbbde35a --- /dev/null +++ b/cli/node/src/version.ts @@ -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; diff --git a/cli/node/tests/branding.test.ts b/cli/node/tests/branding.test.ts index efa2f4759..9e93b3423 100644 --- a/cli/node/tests/branding.test.ts +++ b/cli/node/tests/branding.test.ts @@ -80,19 +80,19 @@ describe("printWarning", () => { describe("printInfo", () => { it("prints info message", () => { printInfo("Important note"); - expect(output).toContain("Important note"); + expect(errOutput).toContain("Important note"); }); }); describe("printScope", () => { it("prints scope when IDs present", () => { printScope({ user_id: "alice", agent_id: "bot" }); - expect(output).toContain("alice"); - expect(output).toContain("bot"); + expect(errOutput).toContain("alice"); + expect(errOutput).toContain("bot"); }); it("prints nothing when no IDs", () => { printScope({}); - expect(output).toBe(""); + expect(errOutput).toBe(""); }); }); diff --git a/cli/node/tests/commands.test.ts b/cli/node/tests/commands.test.ts index 5432798d4..f049c8bdf 100644 --- a/cli/node/tests/commands.test.ts +++ b/cli/node/tests/commands.test.ts @@ -125,7 +125,7 @@ describe("cmdSearch", () => { enableGraph: false, output: "text", }); - expect(output).toContain("No memories found"); + expect(errOutput).toContain("No memories found"); }); }); @@ -166,7 +166,7 @@ describe("cmdList", () => { enableGraph: false, output: "text", }); - expect(output).toContain("No memories found"); + expect(errOutput).toContain("No memories found"); }); }); diff --git a/cli/node/vitest.config.ts b/cli/node/vitest.config.ts new file mode 100644 index 000000000..36cc7df04 --- /dev/null +++ b/cli/node/vitest.config.ts @@ -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), + }, +}); diff --git a/cli/python/src/mem0_cli/app.py b/cli/python/src/mem0_cli/app.py index 5352525b4..1363af9ed 100644 --- a/cli/python/src/mem0_cli/app.py +++ b/cli/python/src/mem0_cli/app.py @@ -253,8 +253,12 @@ def search( help="Specific fields to return (comma-separated).", rich_help_panel="Search", ), - graph: bool = typer.Option(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"), + graph: bool = typer.Option( + 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( "text", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output" ), @@ -372,8 +376,12 @@ def list_cmd( before: str | None = typer.Option( 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"), - no_graph: bool = typer.Option(False, "--no-graph", help="Disable graph in listing.", rich_help_panel="Filters"), + graph: bool = typer.Option( + 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( "table", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output" ), @@ -463,11 +471,19 @@ def update( @app.command(rich_help_panel="Memory") 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."), - entity: bool = typer.Option(False, "--entity", help="Delete the entity itself and all its memories (cascade)."), - 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."), + entity: bool = typer.Option( + False, "--entity", help="Delete the entity itself and all its memories (cascade)." + ), + 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."), user_id: str | None = typer.Option( 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( None, "--agent-id", help="Agent ID.", rich_help_panel="Scope" ), - app_id: str | None = typer.Option( - 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" - ), + app_id: str | None = typer.Option(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"), 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( "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") def init( 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."), - 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: """Interactive setup wizard for mem0 CLI. @@ -747,7 +765,6 @@ def status( ) - @app.command("import", rich_help_panel="Management") def import_cmd( file_path: str = typer.Argument(..., help="JSON file to import."), @@ -1018,6 +1035,7 @@ def help( def version() -> None: """Show version and exit.""" from mem0_cli.commands.utils import cmd_version + cmd_version() diff --git a/cli/python/src/mem0_cli/backend/base.py b/cli/python/src/mem0_cli/backend/base.py index 29394f9e5..7a48429dc 100644 --- a/cli/python/src/mem0_cli/backend/base.py +++ b/cli/python/src/mem0_cli/backend/base.py @@ -105,7 +105,6 @@ class Backend(ABC): def entities(self, entity_type: str) -> list[dict]: ... - def get_backend(config: Mem0Config) -> Backend: """Return the Platform backend.""" from mem0_cli.backend.platform import PlatformBackend diff --git a/cli/python/src/mem0_cli/backend/platform.py b/cli/python/src/mem0_cli/backend/platform.py index 737d0483a..452c33ad1 100644 --- a/cli/python/src/mem0_cli/backend/platform.py +++ b/cli/python/src/mem0_cli/backend/platform.py @@ -312,7 +312,6 @@ class PlatformBackend(Backend): return items - class AuthError(Exception): pass diff --git a/cli/python/src/mem0_cli/commands/entities.py b/cli/python/src/mem0_cli/commands/entities.py index 83caf3471..29c0980f2 100644 --- a/cli/python/src/mem0_cli/commands/entities.py +++ b/cli/python/src/mem0_cli/commands/entities.py @@ -28,7 +28,9 @@ def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> Non """List entities of a given type.""" valid_types = {"users", "agents", "apps", "runs"} 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) _start = _time.perf_counter() @@ -76,7 +78,9 @@ def cmd_entities_delete( ) -> None: """Delete an entity and all its memories (cascade delete).""" 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) if dry_run: diff --git a/cli/python/src/mem0_cli/commands/init_cmd.py b/cli/python/src/mem0_cli/commands/init_cmd.py index 03ec3fa2f..48570a2f5 100644 --- a/cli/python/src/mem0_cli/commands/init_cmd.py +++ b/cli/python/src/mem0_cli/commands/init_cmd.py @@ -265,7 +265,9 @@ def run_init( api_key_val = result.get("api_key") 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) config.platform.api_key = api_key_val config.platform.base_url = base_url diff --git a/cli/python/src/mem0_cli/commands/memory.py b/cli/python/src/mem0_cli/commands/memory.py index 9d5e201fc..3f4a775e2 100644 --- a/cli/python/src/mem0_cli/commands/memory.py +++ b/cli/python/src/mem0_cli/commands/memory.py @@ -98,10 +98,14 @@ def cmd_add( # Validate --expires if expires: import re - if not re.match(r'^\d{4}-\d{2}-\d{2}$', expires): - print_error(err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).") + + if not re.match(r"^\d{4}-\d{2}-\d{2}$", expires): + print_error( + err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31)." + ) raise typer.Exit(1) from datetime import date + if date.fromisoformat(expires) <= date.today(): print_error(err_console, "--expires date must be in the future.") raise typer.Exit(1) @@ -290,6 +294,7 @@ def cmd_list( if output == "json": from mem0_cli.output import format_json_envelope + format_json_envelope( console, command="list", diff --git a/cli/python/src/mem0_cli/commands/utils.py b/cli/python/src/mem0_cli/commands/utils.py index 3fa7ba57c..ef03906e3 100644 --- a/cli/python/src/mem0_cli/commands/utils.py +++ b/cli/python/src/mem0_cli/commands/utils.py @@ -110,7 +110,9 @@ def cmd_import( added = 0 failed = 0 _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", ""))) if not content: failed += 1 diff --git a/cli/python/src/mem0_cli/output.py b/cli/python/src/mem0_cli/output.py index 09156c55a..fc773f919 100644 --- a/cli/python/src/mem0_cli/output.py +++ b/cli/python/src/mem0_cli/output.py @@ -49,7 +49,9 @@ def format_memories_text(console: Console, memories: list[dict], title: str = "m 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.""" table = Table( border_style=BRAND_COLOR, @@ -71,7 +73,11 @@ def format_memories_table(console: Console, memories: list[dict], *, show_score: memory_text = memory_text[:57] + "..." categories = mem.get("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: cat = "—" created = _format_date(mem.get("created_at")) or "—" diff --git a/cli/python/tests/test_cli_integration.py b/cli/python/tests/test_cli_integration.py index 79bda00df..2189c5da6 100644 --- a/cli/python/tests/test_cli_integration.py +++ b/cli/python/tests/test_cli_integration.py @@ -8,11 +8,19 @@ boundaries). from __future__ import annotations import os +import re import subprocess import sys 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( args: list[str], @@ -28,22 +36,35 @@ def _run( reads config from ``/.mem0/config.json`` instead of the user's real config. This is critical for tests that 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() # Strip all MEM0_ env vars so tests start clean for key in list(env.keys()): if key.startswith("MEM0_"): del env[key] + env.pop("FORCE_COLOR", None) if home_dir: env["HOME"] = home_dir if env_override: env.update(env_override) - return subprocess.run( + result = subprocess.run( [sys.executable, "-m", "mem0_cli", *args], capture_output=True, text=True, env=env, ) + return subprocess.CompletedProcess( + args=result.args, + returncode=result.returncode, + stdout=_strip_ansi(result.stdout), + stderr=_strip_ansi(result.stderr), + ) @pytest.fixture @@ -67,7 +88,6 @@ class TestCLIIntegration: assert result.returncode == 0 assert "0.1.0" in result.stdout - def test_add_help(self): result = _run(["add", "--help"]) assert result.returncode == 0 @@ -161,7 +181,12 @@ class TestCLIIsolated: ) assert result.returncode != 0 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): """config show with no config should still work.""" diff --git a/cli/python/tests/test_commands.py b/cli/python/tests/test_commands.py index eb655513e..1e064f5b8 100644 --- a/cli/python/tests/test_commands.py +++ b/cli/python/tests/test_commands.py @@ -62,7 +62,7 @@ class TestAddCommand: metadata=None, immutable=False, no_infer=False, - expires=None, + expires=None, categories=None, output="text", ) @@ -88,7 +88,7 @@ class TestAddCommand: metadata=None, immutable=False, no_infer=False, - expires=None, + expires=None, categories=None, output="text", ) @@ -113,7 +113,7 @@ class TestAddCommand: metadata='{"source": "test"}', immutable=False, no_infer=False, - expires=None, + expires=None, categories=None, output="text", ) @@ -139,7 +139,7 @@ class TestAddCommand: metadata=None, immutable=False, no_infer=False, - expires=None, + expires=None, categories=None, output="json", ) @@ -165,7 +165,7 @@ class TestAddCommand: metadata=None, immutable=False, no_infer=False, - expires=None, + expires=None, categories=None, output="quiet", ) @@ -196,7 +196,7 @@ class TestAddCommand: metadata=None, immutable=False, no_infer=False, - expires=None, + expires=None, categories=None, output="text", ) @@ -221,7 +221,7 @@ class TestAddCommand: metadata="not-json", immutable=False, no_infer=False, - expires=None, + expires=None, categories=None, output="text", ) @@ -247,7 +247,7 @@ class TestAddCommand: metadata=None, immutable=False, no_infer=False, - expires=None, + expires=None, categories=None, output="text", ) @@ -272,7 +272,7 @@ class TestAddCommand: metadata=None, immutable=False, no_infer=False, - expires=None, + expires=None, categories="health,prefs", output="text", )