ci(cli): add CI pipelines for Node and Python CLI SDKs (#4640)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
9bebcf45f7
commit
32c1ccba5b
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -67,7 +67,10 @@ export interface Backend {
|
|||||||
opts?: AddOptions,
|
opts?: AddOptions,
|
||||||
): Promise<Record<string, unknown>>;
|
): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
search(query: string, opts?: SearchOptions): Promise<Record<string, unknown>[]>;
|
search(
|
||||||
|
query: string,
|
||||||
|
opts?: SearchOptions,
|
||||||
|
): Promise<Record<string, unknown>[]>;
|
||||||
|
|
||||||
get(memoryId: string): Promise<Record<string, unknown>>;
|
get(memoryId: string): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
@@ -79,17 +82,24 @@ export interface Backend {
|
|||||||
metadata?: Record<string, unknown>,
|
metadata?: Record<string, unknown>,
|
||||||
): Promise<Record<string, unknown>>;
|
): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
delete(memoryId?: string, opts?: DeleteOptions): Promise<Record<string, unknown>>;
|
delete(
|
||||||
|
memoryId?: string,
|
||||||
|
opts?: DeleteOptions,
|
||||||
|
): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>;
|
deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
status(opts?: { userId?: string; agentId?: string }): Promise<Record<string, unknown>>;
|
status(opts?: { userId?: string; agentId?: string }): Promise<
|
||||||
|
Record<string, unknown>
|
||||||
|
>;
|
||||||
|
|
||||||
entities(entityType: string): Promise<Record<string, unknown>[]>;
|
entities(entityType: string): Promise<Record<string, unknown>[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthError extends Error {
|
export class AuthError extends Error {
|
||||||
constructor(message = "Authentication failed. Your API key may be invalid or expired.") {
|
constructor(
|
||||||
|
message = "Authentication failed. Your API key may be invalid or expired.",
|
||||||
|
) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = "AuthError";
|
this.name = "AuthError";
|
||||||
}
|
}
|
||||||
@@ -112,4 +122,3 @@ export class APIError extends Error {
|
|||||||
export function getBackend(config: Mem0Config): Backend {
|
export function getBackend(config: Mem0Config): Backend {
|
||||||
return new PlatformBackend(config.platform);
|
return new PlatformBackend(config.platform);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export { getBackend } from "./base.js";
|
export { getBackend } from "./base.js";
|
||||||
export type { Backend, AddOptions, SearchOptions, ListOptions, DeleteOptions, EntityIds } from "./base.js";
|
export type {
|
||||||
|
Backend,
|
||||||
|
AddOptions,
|
||||||
|
SearchOptions,
|
||||||
|
ListOptions,
|
||||||
|
DeleteOptions,
|
||||||
|
EntityIds,
|
||||||
|
} from "./base.js";
|
||||||
export { AuthError, NotFoundError, APIError } from "./base.js";
|
export { AuthError, NotFoundError, APIError } from "./base.js";
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
import type { PlatformConfig } from "../config.js";
|
import type { PlatformConfig } from "../config.js";
|
||||||
import {
|
import {
|
||||||
type AddOptions,
|
|
||||||
APIError,
|
APIError,
|
||||||
|
type AddOptions,
|
||||||
AuthError,
|
AuthError,
|
||||||
type Backend,
|
type Backend,
|
||||||
type DeleteOptions,
|
type DeleteOptions,
|
||||||
@@ -58,8 +58,10 @@ export class PlatformBackend implements Backend {
|
|||||||
if (resp.status === 400) {
|
if (resp.status === 400) {
|
||||||
let detail: string;
|
let detail: string;
|
||||||
try {
|
try {
|
||||||
const body = await resp.json() as Record<string, unknown>;
|
const body = (await resp.json()) as Record<string, unknown>;
|
||||||
detail = (body.detail ?? body.message ?? JSON.stringify(body)) as string ?? resp.statusText;
|
detail =
|
||||||
|
((body.detail ?? body.message ?? JSON.stringify(body)) as string) ??
|
||||||
|
resp.statusText;
|
||||||
} catch {
|
} catch {
|
||||||
detail = resp.statusText;
|
detail = resp.statusText;
|
||||||
}
|
}
|
||||||
@@ -68,9 +70,11 @@ export class PlatformBackend implements Backend {
|
|||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
let detail: string = resp.statusText;
|
let detail: string = resp.statusText;
|
||||||
try {
|
try {
|
||||||
const body = await resp.json() as Record<string, unknown>;
|
const body = (await resp.json()) as Record<string, unknown>;
|
||||||
detail = (body.detail ?? body.message ?? resp.statusText) as string;
|
detail = (body.detail ?? body.message ?? resp.statusText) as string;
|
||||||
} catch { /* ignore */ }
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
throw new Error(`HTTP ${resp.status}: ${detail}`);
|
throw new Error(`HTTP ${resp.status}: ${detail}`);
|
||||||
}
|
}
|
||||||
if (resp.status === 204) {
|
if (resp.status === 204) {
|
||||||
@@ -103,10 +107,9 @@ export class PlatformBackend implements Backend {
|
|||||||
if (opts.categories) payload.categories = opts.categories;
|
if (opts.categories) payload.categories = opts.categories;
|
||||||
if (opts.enableGraph) payload.enable_graph = true;
|
if (opts.enableGraph) payload.enable_graph = true;
|
||||||
|
|
||||||
return (await this._request("POST", "/v1/memories/", { json: payload })) as Record<
|
return (await this._request("POST", "/v1/memories/", {
|
||||||
string,
|
json: payload,
|
||||||
unknown
|
})) as Record<string, unknown>;
|
||||||
>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _buildFilters(opts: {
|
private _buildFilters(opts: {
|
||||||
@@ -117,7 +120,10 @@ export class PlatformBackend implements Backend {
|
|||||||
extraFilters?: Record<string, unknown>;
|
extraFilters?: Record<string, unknown>;
|
||||||
}): Record<string, unknown> | undefined {
|
}): Record<string, unknown> | undefined {
|
||||||
// If caller passed a pre-built filter structure, use it directly
|
// If caller passed a pre-built filter structure, use it directly
|
||||||
if (opts.extraFilters && ("AND" in opts.extraFilters || "OR" in opts.extraFilters)) {
|
if (
|
||||||
|
opts.extraFilters &&
|
||||||
|
("AND" in opts.extraFilters || "OR" in opts.extraFilters)
|
||||||
|
) {
|
||||||
return opts.extraFilters;
|
return opts.extraFilters;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +144,10 @@ export class PlatformBackend implements Backend {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query: string, opts: SearchOptions = {}): Promise<Record<string, unknown>[]> {
|
async search(
|
||||||
|
query: string,
|
||||||
|
opts: SearchOptions = {},
|
||||||
|
): Promise<Record<string, unknown>[]> {
|
||||||
const payload: Record<string, unknown> = {
|
const payload: Record<string, unknown> = {
|
||||||
query,
|
query,
|
||||||
top_k: opts.topK ?? 10,
|
top_k: opts.topK ?? 10,
|
||||||
@@ -167,10 +176,15 @@ export class PlatformBackend implements Backend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get(memoryId: string): Promise<Record<string, unknown>> {
|
async get(memoryId: string): Promise<Record<string, unknown>> {
|
||||||
return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record<string, unknown>;
|
return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async listMemories(opts: ListOptions = {}): Promise<Record<string, unknown>[]> {
|
async listMemories(
|
||||||
|
opts: ListOptions = {},
|
||||||
|
): Promise<Record<string, unknown>[]> {
|
||||||
const payload: Record<string, unknown> = {};
|
const payload: Record<string, unknown> = {};
|
||||||
const params: Record<string, string> = {
|
const params: Record<string, string> = {
|
||||||
page: String(opts.page ?? 1),
|
page: String(opts.page ?? 1),
|
||||||
@@ -182,10 +196,16 @@ export class PlatformBackend implements Backend {
|
|||||||
extra.categories = { contains: opts.category };
|
extra.categories = { contains: opts.category };
|
||||||
}
|
}
|
||||||
if (opts.after) {
|
if (opts.after) {
|
||||||
extra.created_at = { ...(extra.created_at as Record<string, unknown> | undefined), gte: opts.after };
|
extra.created_at = {
|
||||||
|
...(extra.created_at as Record<string, unknown> | undefined),
|
||||||
|
gte: opts.after,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (opts.before) {
|
if (opts.before) {
|
||||||
extra.created_at = { ...(extra.created_at as Record<string, unknown> | undefined), lte: opts.before };
|
extra.created_at = {
|
||||||
|
...(extra.created_at as Record<string, unknown> | undefined),
|
||||||
|
lte: opts.before,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiFilters = this._buildFilters({
|
const apiFilters = this._buildFilters({
|
||||||
@@ -198,7 +218,10 @@ export class PlatformBackend implements Backend {
|
|||||||
if (apiFilters) payload.filters = apiFilters;
|
if (apiFilters) payload.filters = apiFilters;
|
||||||
if (opts.enableGraph) payload.enable_graph = true;
|
if (opts.enableGraph) payload.enable_graph = true;
|
||||||
|
|
||||||
const result = (await this._request("POST", "/v2/memories/", { json: payload, params })) as unknown;
|
const result = (await this._request("POST", "/v2/memories/", {
|
||||||
|
json: payload,
|
||||||
|
params,
|
||||||
|
})) as unknown;
|
||||||
if (Array.isArray(result)) return result;
|
if (Array.isArray(result)) return result;
|
||||||
const obj = result as Record<string, unknown>;
|
const obj = result as Record<string, unknown>;
|
||||||
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
|
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
|
||||||
@@ -227,16 +250,15 @@ export class PlatformBackend implements Backend {
|
|||||||
if (opts.agentId) params.agent_id = opts.agentId;
|
if (opts.agentId) params.agent_id = opts.agentId;
|
||||||
if (opts.appId) params.app_id = opts.appId;
|
if (opts.appId) params.app_id = opts.appId;
|
||||||
if (opts.runId) params.run_id = opts.runId;
|
if (opts.runId) params.run_id = opts.runId;
|
||||||
return (await this._request("DELETE", "/v1/memories/", { params })) as Record<
|
return (await this._request("DELETE", "/v1/memories/", {
|
||||||
string,
|
params,
|
||||||
unknown
|
})) as Record<string, unknown>;
|
||||||
>;
|
|
||||||
}
|
}
|
||||||
if (memoryId) {
|
if (memoryId) {
|
||||||
return (await this._request("DELETE", `/v1/memories/${memoryId}/`)) as Record<
|
return (await this._request(
|
||||||
string,
|
"DELETE",
|
||||||
unknown
|
`/v1/memories/${memoryId}/`,
|
||||||
>;
|
)) as Record<string, unknown>;
|
||||||
}
|
}
|
||||||
throw new Error("Either memoryId or --all is required");
|
throw new Error("Either memoryId or --all is required");
|
||||||
}
|
}
|
||||||
@@ -250,10 +272,9 @@ export class PlatformBackend implements Backend {
|
|||||||
if (Object.keys(params).length === 0) {
|
if (Object.keys(params).length === 0) {
|
||||||
throw new Error("At least one entity ID is required for deleteEntities.");
|
throw new Error("At least one entity ID is required for deleteEntities.");
|
||||||
}
|
}
|
||||||
return (await this._request("DELETE", "/v1/entities/", { params })) as Record<
|
return (await this._request("DELETE", "/v1/entities/", {
|
||||||
string,
|
params,
|
||||||
unknown
|
})) as Record<string, unknown>;
|
||||||
>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async status(
|
async status(
|
||||||
@@ -262,13 +283,19 @@ export class PlatformBackend implements Backend {
|
|||||||
try {
|
try {
|
||||||
if (opts.userId || opts.agentId) {
|
if (opts.userId || opts.agentId) {
|
||||||
const payload: Record<string, unknown> = {};
|
const payload: Record<string, unknown> = {};
|
||||||
const statusParams: Record<string, string> = { page: "1", page_size: "1" };
|
const statusParams: Record<string, string> = {
|
||||||
|
page: "1",
|
||||||
|
page_size: "1",
|
||||||
|
};
|
||||||
const apiFilters = this._buildFilters({
|
const apiFilters = this._buildFilters({
|
||||||
userId: opts.userId,
|
userId: opts.userId,
|
||||||
agentId: opts.agentId,
|
agentId: opts.agentId,
|
||||||
});
|
});
|
||||||
if (apiFilters) payload.filters = apiFilters;
|
if (apiFilters) payload.filters = apiFilters;
|
||||||
await this._request("POST", "/v2/memories/", { json: payload, params: statusParams });
|
await this._request("POST", "/v2/memories/", {
|
||||||
|
json: payload,
|
||||||
|
params: statusParams,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
await this._request("GET", "/v1/entities/");
|
await this._request("GET", "/v1/entities/");
|
||||||
}
|
}
|
||||||
@@ -288,7 +315,10 @@ export class PlatformBackend implements Backend {
|
|||||||
if (Array.isArray(result)) {
|
if (Array.isArray(result)) {
|
||||||
items = result;
|
items = result;
|
||||||
} else {
|
} else {
|
||||||
items = ((result as Record<string, unknown>).results ?? []) as Record<string, unknown>[];
|
items = ((result as Record<string, unknown>).results ?? []) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeMap: Record<string, string> = {
|
const typeMap: Record<string, string> = {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import ora, { type Ora } from "ora";
|
import ora, { type Ora } from "ora";
|
||||||
|
import { CLI_VERSION } from "./version.js";
|
||||||
|
|
||||||
export const LOGO = `
|
export const LOGO = `
|
||||||
███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
|
███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
|
||||||
@@ -44,7 +45,7 @@ export function printBanner(): void {
|
|||||||
const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2))
|
const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2))
|
||||||
const logoLines = LOGO.trimEnd().split("\n");
|
const logoLines = LOGO.trimEnd().split("\n");
|
||||||
const tagline = ` ${TAGLINE}`;
|
const tagline = ` ${TAGLINE}`;
|
||||||
const subtitle = `Node.js SDK · v${__CLI_VERSION__}`;
|
const subtitle = `Node.js SDK · v${CLI_VERSION}`;
|
||||||
const contentLines = ["", ...logoLines, "", tagline, ""];
|
const contentLines = ["", ...logoLines, "", tagline, ""];
|
||||||
|
|
||||||
// Compute inner width from longest content line + padding both sides
|
// Compute inner width from longest content line + padding both sides
|
||||||
@@ -54,7 +55,9 @@ export function printBanner(): void {
|
|||||||
|
|
||||||
const topBorder = brand(`╭${"─".repeat(totalWidth - 2)}╮`);
|
const topBorder = brand(`╭${"─".repeat(totalWidth - 2)}╮`);
|
||||||
const subtitleFill = totalWidth - 2 - subtitle.length - 3; // 3 = "─ " before subtitle + "─" after
|
const subtitleFill = totalWidth - 2 - subtitle.length - 3; // 3 = "─ " before subtitle + "─" after
|
||||||
const bottomBorder = brand(`╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`);
|
const bottomBorder = brand(
|
||||||
|
`╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`,
|
||||||
|
);
|
||||||
|
|
||||||
const body = contentLines.map((line) => {
|
const body = contentLines.map((line) => {
|
||||||
const rightPad = innerWidth - pad - line.length;
|
const rightPad = innerWidth - pad - line.length;
|
||||||
@@ -63,7 +66,8 @@ export function printBanner(): void {
|
|||||||
// Re-color tagline line with accent instead of brand.bold
|
// Re-color tagline line with accent instead of brand.bold
|
||||||
const taglineIdx = body.length - 2; // second-to-last (before trailing empty line)
|
const taglineIdx = body.length - 2; // second-to-last (before trailing empty line)
|
||||||
const taglineRightPad = innerWidth - pad - tagline.length;
|
const taglineRightPad = innerWidth - pad - tagline.length;
|
||||||
body[taglineIdx] = `${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`;
|
body[taglineIdx] =
|
||||||
|
`${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`;
|
||||||
|
|
||||||
console.log(topBorder);
|
console.log(topBorder);
|
||||||
for (const line of body) console.log(line);
|
for (const line of body) console.log(line);
|
||||||
@@ -75,7 +79,7 @@ export function printSuccess(message: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function printError(message: string, hint?: string): void {
|
export function printError(message: string, hint?: string): void {
|
||||||
console.error(`${error(sym("✗", "[error]") + " Error:")} ${message}`);
|
console.error(`${error(`${sym("✗", "[error]")} Error:`)} ${message}`);
|
||||||
if (hint) {
|
if (hint) {
|
||||||
console.error(` ${dim(hint)}`);
|
console.error(` ${dim(hint)}`);
|
||||||
}
|
}
|
||||||
@@ -116,7 +120,11 @@ export async function timedStatus<T>(
|
|||||||
fn: (ctx: TimedStatusContext) => Promise<T>,
|
fn: (ctx: TimedStatusContext) => Promise<T>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
|
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
|
||||||
const spinner = ora({ text: dim(message), color: "magenta", stream: process.stderr }).start();
|
const spinner = ora({
|
||||||
|
text: dim(message),
|
||||||
|
color: "magenta",
|
||||||
|
stream: process.stderr,
|
||||||
|
}).start();
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Table from "cli-table3";
|
import Table from "cli-table3";
|
||||||
import { printError, printSuccess, colors } from "../branding.js";
|
import { colors, printError, printSuccess } from "../branding.js";
|
||||||
import {
|
import {
|
||||||
getNestedValue,
|
getNestedValue,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
@@ -48,7 +48,10 @@ export function cmdConfigShow(opts: { output?: string } = {}): void {
|
|||||||
|
|
||||||
// Defaults
|
// Defaults
|
||||||
table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]);
|
table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]);
|
||||||
table.push(["defaults.agent_id", config.defaults.agentId || dim("(not set)")]);
|
table.push([
|
||||||
|
"defaults.agent_id",
|
||||||
|
config.defaults.agentId || dim("(not set)"),
|
||||||
|
]);
|
||||||
table.push(["defaults.app_id", config.defaults.appId || 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.run_id", config.defaults.runId || dim("(not set)")]);
|
||||||
table.push(["defaults.enable_graph", String(config.defaults.enableGraph)]);
|
table.push(["defaults.enable_graph", String(config.defaults.enableGraph)]);
|
||||||
|
|||||||
@@ -4,8 +4,14 @@
|
|||||||
|
|
||||||
import readline from "node:readline";
|
import readline from "node:readline";
|
||||||
import Table from "cli-table3";
|
import Table from "cli-table3";
|
||||||
import { printError, printInfo, printSuccess, timedStatus, colors } from "../branding.js";
|
|
||||||
import type { Backend } from "../backend/base.js";
|
import type { Backend } from "../backend/base.js";
|
||||||
|
import {
|
||||||
|
colors,
|
||||||
|
printError,
|
||||||
|
printInfo,
|
||||||
|
printSuccess,
|
||||||
|
timedStatus,
|
||||||
|
} from "../branding.js";
|
||||||
import { formatJson } from "../output.js";
|
import { formatJson } from "../output.js";
|
||||||
|
|
||||||
const { brand, accent, dim } = colors;
|
const { brand, accent, dim } = colors;
|
||||||
@@ -18,7 +24,9 @@ export async function cmdEntitiesList(
|
|||||||
opts: { output: string },
|
opts: { output: string },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!VALID_TYPES.has(entityType)) {
|
if (!VALID_TYPES.has(entityType)) {
|
||||||
printError(`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`);
|
printError(
|
||||||
|
`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`,
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +68,9 @@ export async function cmdEntitiesList(
|
|||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
console.log(table.toString());
|
console.log(table.toString());
|
||||||
console.log(` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`);
|
console.log(
|
||||||
|
` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`,
|
||||||
|
);
|
||||||
console.log();
|
console.log();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +87,9 @@ export async function cmdEntitiesDelete(
|
|||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) {
|
if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) {
|
||||||
printError("Provide at least one of --user-id, --agent-id, --app-id, --run-id.");
|
printError(
|
||||||
|
"Provide at least one of --user-id, --agent-id, --app-id, --run-id.",
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +99,9 @@ export async function cmdEntitiesDelete(
|
|||||||
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
|
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
|
||||||
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
|
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
|
||||||
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
||||||
printInfo(`Would delete entity ${scopeParts.join(", ")} and all its memories.`);
|
printInfo(
|
||||||
|
`Would delete entity ${scopeParts.join(", ")} and all its memories.`,
|
||||||
|
);
|
||||||
printInfo("No changes made.");
|
printInfo("No changes made.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -100,7 +114,10 @@ export async function cmdEntitiesDelete(
|
|||||||
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
||||||
const scope = scopeParts.join(", ");
|
const scope = scopeParts.join(", ");
|
||||||
|
|
||||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
const answer = await new Promise<string>((resolve) => {
|
const answer = await new Promise<string>((resolve) => {
|
||||||
rl.question(
|
rl.question(
|
||||||
`\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `,
|
`\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `,
|
||||||
|
|||||||
@@ -3,15 +3,21 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import readline from "node:readline";
|
import readline from "node:readline";
|
||||||
|
import { PlatformBackend } from "../backend/platform.js";
|
||||||
import {
|
import {
|
||||||
|
colors,
|
||||||
printBanner,
|
printBanner,
|
||||||
printError,
|
printError,
|
||||||
printInfo,
|
printInfo,
|
||||||
printSuccess,
|
printSuccess,
|
||||||
colors,
|
|
||||||
} from "../branding.js";
|
} from "../branding.js";
|
||||||
import { type Mem0Config, createDefaultConfig, loadConfig, saveConfig, DEFAULT_BASE_URL } from "../config.js";
|
import {
|
||||||
import { PlatformBackend } from "../backend/platform.js";
|
DEFAULT_BASE_URL,
|
||||||
|
type Mem0Config,
|
||||||
|
createDefaultConfig,
|
||||||
|
loadConfig,
|
||||||
|
saveConfig,
|
||||||
|
} from "../config.js";
|
||||||
|
|
||||||
const { brand, dim } = colors;
|
const { brand, dim } = colors;
|
||||||
|
|
||||||
@@ -30,8 +36,9 @@ async function emailLogin(
|
|||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
): Promise<Record<string, unknown>> {
|
): Promise<Record<string, unknown>> {
|
||||||
const url = baseUrl.replace(/\/+$/, "");
|
const url = baseUrl.replace(/\/+$/, "");
|
||||||
|
let codeValue = code;
|
||||||
|
|
||||||
if (!code) {
|
if (!codeValue) {
|
||||||
const resp = await fetch(`${url}/api/v1/auth/email_code/`, {
|
const resp = await fetch(`${url}/api/v1/auth/email_code/`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -46,7 +53,7 @@ async function emailLogin(
|
|||||||
let detail: string;
|
let detail: string;
|
||||||
try {
|
try {
|
||||||
const body = (await resp.json()) as Record<string, unknown>;
|
const body = (await resp.json()) as Record<string, unknown>;
|
||||||
detail = (body["error"] ?? body["detail"] ?? resp.statusText) as string;
|
detail = (body.error ?? body.detail ?? resp.statusText) as string;
|
||||||
} catch {
|
} catch {
|
||||||
detail = resp.statusText;
|
detail = resp.statusText;
|
||||||
}
|
}
|
||||||
@@ -70,13 +77,13 @@ async function emailLogin(
|
|||||||
printError("Code is required.");
|
printError("Code is required.");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
code = entered;
|
codeValue = entered;
|
||||||
}
|
}
|
||||||
|
|
||||||
const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, {
|
const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ email, code: code.trim() }),
|
body: JSON.stringify({ email, code: codeValue.trim() }),
|
||||||
signal: AbortSignal.timeout(30_000),
|
signal: AbortSignal.timeout(30_000),
|
||||||
});
|
});
|
||||||
if (verifyResp.status === 429) {
|
if (verifyResp.status === 429) {
|
||||||
@@ -87,7 +94,7 @@ async function emailLogin(
|
|||||||
let detail: string;
|
let detail: string;
|
||||||
try {
|
try {
|
||||||
const body = (await verifyResp.json()) as Record<string, unknown>;
|
const body = (await verifyResp.json()) as Record<string, unknown>;
|
||||||
detail = (body["error"] ?? body["detail"] ?? verifyResp.statusText) as string;
|
detail = (body.error ?? body.detail ?? verifyResp.statusText) as string;
|
||||||
} catch {
|
} catch {
|
||||||
detail = verifyResp.statusText;
|
detail = verifyResp.statusText;
|
||||||
}
|
}
|
||||||
@@ -153,7 +160,10 @@ function promptSecret(label: string): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function promptLine(label: string, defaultValue?: string): Promise<string> {
|
function promptLine(label: string, defaultValue?: string): Promise<string> {
|
||||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `;
|
const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `;
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
rl.question(prompt, (answer) => {
|
rl.question(prompt, (answer) => {
|
||||||
@@ -165,7 +175,9 @@ function promptLine(label: string, defaultValue?: string): Promise<string> {
|
|||||||
|
|
||||||
async function setupPlatform(config: Mem0Config): Promise<void> {
|
async function setupPlatform(config: Mem0Config): Promise<void> {
|
||||||
console.log();
|
console.log();
|
||||||
console.log(` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys")}`);
|
console.log(
|
||||||
|
` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys")}`,
|
||||||
|
);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
process.stdout.write(` ${brand("API Key")}: `);
|
process.stdout.write(` ${brand("API Key")}: `);
|
||||||
@@ -181,7 +193,10 @@ async function setupDefaults(config: Mem0Config): Promise<void> {
|
|||||||
console.log();
|
console.log();
|
||||||
printInfo("Set default entity IDs (press Enter to skip).\n");
|
printInfo("Set default entity IDs (press Enter to skip).\n");
|
||||||
|
|
||||||
const userId = await promptLine(` ${brand("Default User ID")} ${dim("(recommended)")}`, "mem0-cli");
|
const userId = await promptLine(
|
||||||
|
` ${brand("Default User ID")} ${dim("(recommended)")}`,
|
||||||
|
"mem0-cli",
|
||||||
|
);
|
||||||
if (userId) config.defaults.userId = userId;
|
if (userId) config.defaults.userId = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,12 +223,19 @@ async function validatePlatform(config: Mem0Config): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runInit(
|
export async function runInit(
|
||||||
opts: { apiKey?: string; userId?: string; email?: string; code?: string } = {},
|
opts: {
|
||||||
|
apiKey?: string;
|
||||||
|
userId?: string;
|
||||||
|
email?: string;
|
||||||
|
code?: string;
|
||||||
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const config = createDefaultConfig();
|
const config = createDefaultConfig();
|
||||||
const savedConfig = loadConfig();
|
const savedConfig = loadConfig();
|
||||||
const baseUrl =
|
const baseUrl =
|
||||||
process.env["MEM0_BASE_URL"] || savedConfig.platform.baseUrl || DEFAULT_BASE_URL;
|
process.env.MEM0_BASE_URL ||
|
||||||
|
savedConfig.platform.baseUrl ||
|
||||||
|
DEFAULT_BASE_URL;
|
||||||
|
|
||||||
// Guards
|
// Guards
|
||||||
if (opts.code && !opts.email) {
|
if (opts.code && !opts.email) {
|
||||||
@@ -236,9 +258,11 @@ export async function runInit(
|
|||||||
|
|
||||||
const result = await emailLogin(email, opts.code, baseUrl);
|
const result = await emailLogin(email, opts.code, baseUrl);
|
||||||
|
|
||||||
const apiKeyVal = result["api_key"] as string | undefined;
|
const apiKeyVal = result.api_key as string | undefined;
|
||||||
if (!apiKeyVal) {
|
if (!apiKeyVal) {
|
||||||
printError("Auth succeeded but no API key was returned. Contact support.");
|
printError(
|
||||||
|
"Auth succeeded but no API key was returned. Contact support.",
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,9 +331,11 @@ export async function runInit(
|
|||||||
|
|
||||||
const result = await emailLogin(email, undefined, baseUrl);
|
const result = await emailLogin(email, undefined, baseUrl);
|
||||||
|
|
||||||
const apiKeyVal = result["api_key"] as string | undefined;
|
const apiKeyVal = result.api_key as string | undefined;
|
||||||
if (!apiKeyVal) {
|
if (!apiKeyVal) {
|
||||||
printError("Auth succeeded but no API key was returned. Contact support.");
|
printError(
|
||||||
|
"Auth succeeded but no API key was returned. Contact support.",
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { printError, printInfo, printScope, printSuccess, timedStatus } from "../branding.js";
|
|
||||||
import type { Backend } from "../backend/base.js";
|
import type { Backend } from "../backend/base.js";
|
||||||
|
import {
|
||||||
|
printError,
|
||||||
|
printInfo,
|
||||||
|
printScope,
|
||||||
|
printSuccess,
|
||||||
|
timedStatus,
|
||||||
|
} from "../branding.js";
|
||||||
import {
|
import {
|
||||||
formatAddResult,
|
formatAddResult,
|
||||||
formatJson,
|
formatJson,
|
||||||
@@ -52,7 +58,9 @@ export async function cmdAdd(
|
|||||||
try {
|
try {
|
||||||
msgs = JSON.parse(opts.messages);
|
msgs = JSON.parse(opts.messages);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
printError(`Invalid JSON in --messages: ${e instanceof Error ? e.message : e}`);
|
printError(
|
||||||
|
`Invalid JSON in --messages: ${e instanceof Error ? e.message : e}`,
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,14 +74,18 @@ export async function cmdAdd(
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
if (!content && !msgs) {
|
if (!content && !msgs) {
|
||||||
printError("No content provided. Pass text, --messages, --file, or pipe via stdin.");
|
printError(
|
||||||
|
"No content provided. Pass text, --messages, --file, or pipe via stdin.",
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate --expires
|
// Validate --expires
|
||||||
if (opts.expires) {
|
if (opts.expires) {
|
||||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(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).");
|
printError(
|
||||||
|
"Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).",
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
if (new Date(opts.expires) <= new Date()) {
|
if (new Date(opts.expires) <= new Date()) {
|
||||||
@@ -130,10 +142,19 @@ export async function cmdAdd(
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
printScope({ user_id: opts.userId, agent_id: opts.agentId, app_id: opts.appId, run_id: opts.runId });
|
printScope({
|
||||||
const results = Array.isArray(result) ? result : ((result.results as unknown[]) ?? [result]);
|
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;
|
const count = results.length;
|
||||||
printSuccess(`Memory processed — ${count} memor${count === 1 ? "y" : "ies"} extracted`);
|
printSuccess(
|
||||||
|
`Memory processed — ${count} memor${count === 1 ? "y" : "ies"} extracted`,
|
||||||
|
);
|
||||||
formatAddResult(result, opts.output);
|
formatAddResult(result, opts.output);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +191,9 @@ export async function cmdSearch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
if (opts.topK < 1) {
|
||||||
printError("--top-k must be >= 1.");
|
printError("--top-k must be >= 1.");
|
||||||
@@ -185,6 +208,7 @@ export async function cmdSearch(
|
|||||||
let results: Record<string, unknown>[];
|
let results: Record<string, unknown>[];
|
||||||
try {
|
try {
|
||||||
results = await timedStatus("Searching memories...", async () => {
|
results = await timedStatus("Searching memories...", async () => {
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: guarded by process.exit above
|
||||||
return backend.search(query!, {
|
return backend.search(query!, {
|
||||||
userId: opts.userId,
|
userId: opts.userId,
|
||||||
agentId: opts.agentId,
|
agentId: opts.agentId,
|
||||||
@@ -212,7 +236,11 @@ export async function cmdSearch(
|
|||||||
} else if (opts.output === "table") {
|
} else if (opts.output === "table") {
|
||||||
if (results.length > 0) {
|
if (results.length > 0) {
|
||||||
formatMemoriesTable(results, { showScore: true });
|
formatMemoriesTable(results, { showScore: true });
|
||||||
printResultSummary({ count: results.length, durationSecs: elapsed, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } });
|
printResultSummary({
|
||||||
|
count: results.length,
|
||||||
|
durationSecs: elapsed,
|
||||||
|
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log();
|
console.log();
|
||||||
printInfo("No memories found matching your query.");
|
printInfo("No memories found matching your query.");
|
||||||
@@ -221,7 +249,11 @@ export async function cmdSearch(
|
|||||||
} else {
|
} else {
|
||||||
if (results.length > 0) {
|
if (results.length > 0) {
|
||||||
formatMemoriesText(results);
|
formatMemoriesText(results);
|
||||||
printResultSummary({ count: results.length, durationSecs: elapsed, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } });
|
printResultSummary({
|
||||||
|
count: results.length,
|
||||||
|
durationSecs: elapsed,
|
||||||
|
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log();
|
console.log();
|
||||||
printInfo("No memories found matching your query.");
|
printInfo("No memories found matching your query.");
|
||||||
@@ -308,7 +340,12 @@ export async function cmdList(
|
|||||||
} else if (opts.output === "table") {
|
} else if (opts.output === "table") {
|
||||||
if (results.length > 0) {
|
if (results.length > 0) {
|
||||||
formatMemoriesTable(results);
|
formatMemoriesTable(results);
|
||||||
printResultSummary({ count: results.length, durationSecs: elapsed, page: opts.page, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } });
|
printResultSummary({
|
||||||
|
count: results.length,
|
||||||
|
durationSecs: elapsed,
|
||||||
|
page: opts.page,
|
||||||
|
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log();
|
console.log();
|
||||||
printInfo("No memories found.");
|
printInfo("No memories found.");
|
||||||
@@ -317,7 +354,12 @@ export async function cmdList(
|
|||||||
} else {
|
} else {
|
||||||
if (results.length > 0) {
|
if (results.length > 0) {
|
||||||
formatMemoriesText(results, "memories");
|
formatMemoriesText(results, "memories");
|
||||||
printResultSummary({ count: results.length, durationSecs: elapsed, page: opts.page, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } });
|
printResultSummary({
|
||||||
|
count: results.length,
|
||||||
|
durationSecs: elapsed,
|
||||||
|
page: opts.page,
|
||||||
|
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log();
|
console.log();
|
||||||
printInfo("No memories found.");
|
printInfo("No memories found.");
|
||||||
@@ -357,7 +399,9 @@ export async function cmdUpdate(
|
|||||||
if (opts.output === "json") {
|
if (opts.output === "json") {
|
||||||
formatJson(result);
|
formatJson(result);
|
||||||
} else if (opts.output !== "quiet") {
|
} else if (opts.output !== "quiet") {
|
||||||
printSuccess(`Memory ${memoryId.slice(0, 8)} updated (${elapsed.toFixed(2)}s)`);
|
printSuccess(
|
||||||
|
`Memory ${memoryId.slice(0, 8)} updated (${elapsed.toFixed(2)}s)`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +439,9 @@ export async function cmdDelete(
|
|||||||
if (opts.output === "json") {
|
if (opts.output === "json") {
|
||||||
formatJson(result);
|
formatJson(result);
|
||||||
} else if (opts.output !== "quiet") {
|
} else if (opts.output !== "quiet") {
|
||||||
printSuccess(`Memory ${memoryId.slice(0, 8)} deleted (${elapsed.toFixed(2)}s)`);
|
printSuccess(
|
||||||
|
`Memory ${memoryId.slice(0, 8)} deleted (${elapsed.toFixed(2)}s)`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,16 +462,24 @@ export async function cmdDeleteAll(
|
|||||||
// Project-wide wipe using wildcard entity IDs
|
// Project-wide wipe using wildcard entity IDs
|
||||||
if (opts.dryRun) {
|
if (opts.dryRun) {
|
||||||
printInfo("Would delete ALL memories project-wide.");
|
printInfo("Would delete ALL memories project-wide.");
|
||||||
printInfo("Run without --dry-run to see the actual count of deleted memories.");
|
printInfo(
|
||||||
|
"Run without --dry-run to see the actual count of deleted memories.",
|
||||||
|
);
|
||||||
printInfo("No changes made.");
|
printInfo("No changes made.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!opts.force) {
|
if (!opts.force) {
|
||||||
const readline = await import("node:readline");
|
const readline = await import("node:readline");
|
||||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
const answer = await new Promise<string>((resolve) => {
|
const answer = await new Promise<string>((resolve) => {
|
||||||
rl.question(`\n \u26a0 Delete ALL memories across the ENTIRE project? This cannot be undone. [y/N] `, resolve);
|
rl.question(
|
||||||
|
"\n \u26a0 Delete ALL memories across the ENTIRE project? This cannot be undone. [y/N] ",
|
||||||
|
resolve,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
rl.close();
|
rl.close();
|
||||||
if (answer.toLowerCase() !== "y") {
|
if (answer.toLowerCase() !== "y") {
|
||||||
@@ -437,7 +491,9 @@ export async function cmdDeleteAll(
|
|||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
let result: Record<string, unknown>;
|
let result: Record<string, unknown>;
|
||||||
try {
|
try {
|
||||||
result = await timedStatus("Deleting all memories project-wide...", async () => {
|
result = await timedStatus(
|
||||||
|
"Deleting all memories project-wide...",
|
||||||
|
async () => {
|
||||||
return backend.delete(undefined, {
|
return backend.delete(undefined, {
|
||||||
all: true,
|
all: true,
|
||||||
userId: "*",
|
userId: "*",
|
||||||
@@ -445,7 +501,8 @@ export async function cmdDeleteAll(
|
|||||||
appId: "*",
|
appId: "*",
|
||||||
runId: "*",
|
runId: "*",
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
printError(e instanceof Error ? e.message : String(e));
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -456,7 +513,9 @@ export async function cmdDeleteAll(
|
|||||||
formatJson(result);
|
formatJson(result);
|
||||||
} else if (opts.output !== "quiet") {
|
} else if (opts.output !== "quiet") {
|
||||||
if (result.message) {
|
if (result.message) {
|
||||||
printInfo("Deletion started. Memories will be removed in the background.");
|
printInfo(
|
||||||
|
"Deletion started. Memories will be removed in the background.",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
printSuccess(`All project memories deleted (${elapsed.toFixed(2)}s)`);
|
printSuccess(`All project memories deleted (${elapsed.toFixed(2)}s)`);
|
||||||
}
|
}
|
||||||
@@ -488,12 +547,19 @@ export async function cmdDeleteAll(
|
|||||||
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
|
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
|
||||||
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
|
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
|
||||||
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
||||||
const scope = scopeParts.length > 0 ? scopeParts.join(", ") : "ALL entities";
|
const scope =
|
||||||
|
scopeParts.length > 0 ? scopeParts.join(", ") : "ALL entities";
|
||||||
|
|
||||||
const readline = await import("node:readline");
|
const readline = await import("node:readline");
|
||||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
const answer = await new Promise<string>((resolve) => {
|
const answer = await new Promise<string>((resolve) => {
|
||||||
rl.question(`\n \u26a0 Delete ALL memories for ${scope}? This cannot be undone. [y/N] `, resolve);
|
rl.question(
|
||||||
|
`\n \u26a0 Delete ALL memories for ${scope}? This cannot be undone. [y/N] `,
|
||||||
|
resolve,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
rl.close();
|
rl.close();
|
||||||
if (answer.toLowerCase() !== "y") {
|
if (answer.toLowerCase() !== "y") {
|
||||||
@@ -524,7 +590,9 @@ export async function cmdDeleteAll(
|
|||||||
formatJson(result);
|
formatJson(result);
|
||||||
} else if (opts.output !== "quiet") {
|
} else if (opts.output !== "quiet") {
|
||||||
if (result.message) {
|
if (result.message) {
|
||||||
printInfo("Deletion started. Memories will be removed in the background.");
|
printInfo(
|
||||||
|
"Deletion started. Memories will be removed in the background.",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
printSuccess(`All matching memories deleted (${elapsed.toFixed(2)}s)`);
|
printSuccess(`All matching memories deleted (${elapsed.toFixed(2)}s)`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { printError, printSuccess, timedStatus, colors } from "../branding.js";
|
|
||||||
import type { Backend } from "../backend/base.js";
|
|
||||||
import { formatJsonEnvelope } from "../output.js";
|
|
||||||
import boxen from "boxen";
|
import boxen from "boxen";
|
||||||
|
import type { Backend } from "../backend/base.js";
|
||||||
|
import { colors, printError, printSuccess, timedStatus } from "../branding.js";
|
||||||
|
import { formatJsonEnvelope } from "../output.js";
|
||||||
|
import { CLI_VERSION } from "../version.js";
|
||||||
|
|
||||||
const { brand, dim, success, error: errorColor } = colors;
|
const { brand, dim, success, error: errorColor } = colors;
|
||||||
|
|
||||||
@@ -21,7 +22,10 @@ export async function cmdStatus(
|
|||||||
return backend.status({ userId: opts.userId, agentId: opts.agentId });
|
return backend.status({ userId: opts.userId, agentId: opts.agentId });
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result = { connected: false, error: e instanceof Error ? e.message : String(e) };
|
result = {
|
||||||
|
connected: false,
|
||||||
|
error: e instanceof Error ? e.message : String(e),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const elapsed = (performance.now() - start) / 1000;
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
@@ -69,7 +73,7 @@ export async function cmdStatus(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function cmdVersion(): void {
|
export function cmdVersion(): void {
|
||||||
console.log(` ${brand("◆ Mem0")} CLI v${__CLI_VERSION__}`);
|
console.log(` ${brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cmdImport(
|
export async function cmdImport(
|
||||||
@@ -112,7 +116,9 @@ export async function cmdImport(
|
|||||||
|
|
||||||
// Simple progress indicator
|
// Simple progress indicator
|
||||||
if ((i + 1) % 10 === 0 || i === data.length - 1) {
|
if ((i + 1) % 10 === 0 || i === data.length - 1) {
|
||||||
process.stdout.write(`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`);
|
process.stdout.write(
|
||||||
|
`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +128,11 @@ export async function cmdImport(
|
|||||||
if (opts.output === "json") {
|
if (opts.output === "json") {
|
||||||
formatJsonEnvelope({
|
formatJsonEnvelope({
|
||||||
command: "import",
|
command: "import",
|
||||||
data: { added, failed, duration_s: parseFloat(elapsed.toFixed(2)) },
|
data: {
|
||||||
|
added,
|
||||||
|
failed,
|
||||||
|
duration_s: Number.parseFloat(elapsed.toFixed(2)),
|
||||||
|
},
|
||||||
durationMs: Math.round(elapsed * 1000),
|
durationMs: Math.round(elapsed * 1000),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|||||||
+23
-15
@@ -80,10 +80,14 @@ export function loadConfig(): Mem0Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Environment variable overrides
|
// Environment variable overrides
|
||||||
if (process.env.MEM0_API_KEY) config.platform.apiKey = process.env.MEM0_API_KEY;
|
if (process.env.MEM0_API_KEY)
|
||||||
if (process.env.MEM0_BASE_URL) config.platform.baseUrl = process.env.MEM0_BASE_URL;
|
config.platform.apiKey = process.env.MEM0_API_KEY;
|
||||||
if (process.env.MEM0_USER_ID) config.defaults.userId = process.env.MEM0_USER_ID;
|
if (process.env.MEM0_BASE_URL)
|
||||||
if (process.env.MEM0_AGENT_ID) config.defaults.agentId = process.env.MEM0_AGENT_ID;
|
config.platform.baseUrl = process.env.MEM0_BASE_URL;
|
||||||
|
if (process.env.MEM0_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_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_RUN_ID) config.defaults.runId = process.env.MEM0_RUN_ID;
|
||||||
if (process.env.MEM0_ENABLE_GRAPH) {
|
if (process.env.MEM0_ENABLE_GRAPH) {
|
||||||
@@ -119,8 +123,8 @@ export function saveConfig(config: Mem0Config): void {
|
|||||||
|
|
||||||
export function redactKey(key: string): string {
|
export function redactKey(key: string): string {
|
||||||
if (!key) return "(not set)";
|
if (!key) return "(not set)";
|
||||||
if (key.length <= 8) return key.slice(0, 2) + "***";
|
if (key.length <= 8) return `${key.slice(0, 2)}***`;
|
||||||
return key.slice(0, 4) + "..." + key.slice(-4);
|
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Key map from dotted config path to the config object fields. */
|
/** Key map from dotted config path to the config object fields. */
|
||||||
@@ -133,13 +137,13 @@ const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
|
|||||||
"defaults.run_id": ["defaults", "runId"],
|
"defaults.run_id": ["defaults", "runId"],
|
||||||
"defaults.enable_graph": ["defaults", "enableGraph"],
|
"defaults.enable_graph": ["defaults", "enableGraph"],
|
||||||
// Short-form aliases
|
// Short-form aliases
|
||||||
"api_key": ["platform", "apiKey"],
|
api_key: ["platform", "apiKey"],
|
||||||
"base_url": ["platform", "baseUrl"],
|
base_url: ["platform", "baseUrl"],
|
||||||
"user_id": ["defaults", "userId"],
|
user_id: ["defaults", "userId"],
|
||||||
"agent_id": ["defaults", "agentId"],
|
agent_id: ["defaults", "agentId"],
|
||||||
"app_id": ["defaults", "appId"],
|
app_id: ["defaults", "appId"],
|
||||||
"run_id": ["defaults", "runId"],
|
run_id: ["defaults", "runId"],
|
||||||
"enable_graph": ["defaults", "enableGraph"],
|
enable_graph: ["defaults", "enableGraph"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getNestedValue(config: Mem0Config, dottedKey: string): unknown {
|
export function getNestedValue(config: Mem0Config, dottedKey: string): unknown {
|
||||||
@@ -149,7 +153,11 @@ export function getNestedValue(config: Mem0Config, dottedKey: string): unknown {
|
|||||||
return (config[section] as unknown as Record<string, unknown>)[field];
|
return (config[section] as unknown as Record<string, unknown>)[field];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setNestedValue(config: Mem0Config, dottedKey: string, value: string): boolean {
|
export function setNestedValue(
|
||||||
|
config: Mem0Config,
|
||||||
|
dottedKey: string,
|
||||||
|
value: string,
|
||||||
|
): boolean {
|
||||||
const mapping = KEY_MAP[dottedKey];
|
const mapping = KEY_MAP[dottedKey];
|
||||||
if (!mapping) return false;
|
if (!mapping) return false;
|
||||||
const [section, field] = mapping;
|
const [section, field] = mapping;
|
||||||
@@ -159,7 +167,7 @@ export function setNestedValue(config: Mem0Config, dottedKey: string, value: str
|
|||||||
if (typeof current === "boolean") {
|
if (typeof current === "boolean") {
|
||||||
obj[field] = ["true", "1", "yes"].includes(value.toLowerCase());
|
obj[field] = ["true", "1", "yes"].includes(value.toLowerCase());
|
||||||
} else if (typeof current === "number") {
|
} else if (typeof current === "number") {
|
||||||
obj[field] = parseInt(value, 10);
|
obj[field] = Number.parseInt(value, 10);
|
||||||
} else {
|
} else {
|
||||||
obj[field] = value;
|
obj[field] = value;
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
/** Injected by tsup at build time from package.json version field. */
|
/** Injected by tsup at build time from package.json version field. Undefined in dev/test. */
|
||||||
declare const __CLI_VERSION__: string;
|
declare const __CLI_VERSION__: string | undefined;
|
||||||
|
|||||||
+21
-17
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import type { Command, Help, Option, Argument } from "commander";
|
import type { Argument, Command, Help, Option } from "commander";
|
||||||
// Colors imported from chalk directly to match Typer/Rich defaults
|
// Colors imported from chalk directly to match Typer/Rich defaults
|
||||||
|
|
||||||
// ── Colors (matching Typer/Rich defaults) ────────────────────────────────
|
// ── Colors (matching Typer/Rich defaults) ────────────────────────────────
|
||||||
@@ -19,7 +19,7 @@ const dimBorder = chalk.dim; // panel borders
|
|||||||
|
|
||||||
// ── Strip ANSI ───────────────────────────────────────────────────────────
|
// ── Strip ANSI ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// eslint-disable-next-line no-control-regex
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequence is intentional
|
||||||
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
||||||
|
|
||||||
function stripAnsi(str: string): number {
|
function stripAnsi(str: string): number {
|
||||||
@@ -141,11 +141,7 @@ const PANEL_ORDER: string[] = [
|
|||||||
* ╰────────────────────────────────╯
|
* ╰────────────────────────────────╯
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
function renderPanel(
|
function renderPanel(title: string, rows: string[], width: number): string {
|
||||||
title: string,
|
|
||||||
rows: string[],
|
|
||||||
width: number,
|
|
||||||
): string {
|
|
||||||
if (rows.length === 0) return "";
|
if (rows.length === 0) return "";
|
||||||
|
|
||||||
// Inner width is total width minus the two border chars
|
// Inner width is total width minus the two border chars
|
||||||
@@ -161,13 +157,14 @@ function renderPanel(
|
|||||||
dimBorder("╮");
|
dimBorder("╮");
|
||||||
|
|
||||||
// Bottom border: ╰─...─╯
|
// Bottom border: ╰─...─╯
|
||||||
const bottomLine = dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯");
|
const bottomLine =
|
||||||
|
dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯");
|
||||||
|
|
||||||
// Content rows
|
// Content rows
|
||||||
const contentLines = rows.map((row) => {
|
const contentLines = rows.map((row) => {
|
||||||
const visLen = stripAnsi(row);
|
const visLen = stripAnsi(row);
|
||||||
const pad = Math.max(0, inner - 1 - visLen);
|
const pad = Math.max(0, inner - 1 - visLen);
|
||||||
return dimBorder("│") + " " + row + " ".repeat(pad) + dimBorder("│");
|
return `${dimBorder("│")} ${row}${" ".repeat(pad)}${dimBorder("│")}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
return [topLine, ...contentLines, bottomLine].join("\n");
|
return [topLine, ...contentLines, bottomLine].join("\n");
|
||||||
@@ -185,7 +182,7 @@ function formatOptionTerm(opt: Option): string {
|
|||||||
if (opt.flags) {
|
if (opt.flags) {
|
||||||
const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/);
|
const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/);
|
||||||
if (match) {
|
if (match) {
|
||||||
term += " " + match[0];
|
term += ` ${match[0]}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return term;
|
return term;
|
||||||
@@ -220,7 +217,9 @@ export function richFormatHelp(cmd: Command, helper: Help): string {
|
|||||||
lines.push("");
|
lines.push("");
|
||||||
if (isRoot) {
|
if (isRoot) {
|
||||||
// Root: "Usage: mem0 <command> [options]" — <command> yellow, [options] bold
|
// Root: "Usage: mem0 <command> [options]" — <command> yellow, [options] bold
|
||||||
lines.push(` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`);
|
lines.push(
|
||||||
|
` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Subcommands: split into command path (bold) and args (yellow)
|
// Subcommands: split into command path (bold) and args (yellow)
|
||||||
const usageParts = usage.split(" ");
|
const usageParts = usage.split(" ");
|
||||||
@@ -235,7 +234,9 @@ export function richFormatHelp(cmd: Command, helper: Help): string {
|
|||||||
argParts.push(part);
|
argParts.push(part);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines.push(` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`);
|
lines.push(
|
||||||
|
` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
@@ -260,7 +261,9 @@ export function richFormatHelp(cmd: Command, helper: Help): string {
|
|||||||
if (!isRoot) {
|
if (!isRoot) {
|
||||||
const visibleArgs = helper.visibleArguments(cmd);
|
const visibleArgs = helper.visibleArguments(cmd);
|
||||||
if (visibleArgs.length > 0) {
|
if (visibleArgs.length > 0) {
|
||||||
const maxLen = Math.max(...visibleArgs.map((a: Argument) => a.name().length));
|
const maxLen = Math.max(
|
||||||
|
...visibleArgs.map((a: Argument) => a.name().length),
|
||||||
|
);
|
||||||
const argRows = visibleArgs.map((a: Argument) => {
|
const argRows = visibleArgs.map((a: Argument) => {
|
||||||
const name = cyanBold(a.name().padEnd(maxLen));
|
const name = cyanBold(a.name().padEnd(maxLen));
|
||||||
const description = helper.argumentDescription(a);
|
const description = helper.argumentDescription(a);
|
||||||
@@ -274,7 +277,8 @@ export function richFormatHelp(cmd: Command, helper: Help): string {
|
|||||||
// ── Collect options (grouped into panels for subcommands) ──
|
// ── Collect options (grouped into panels for subcommands) ──
|
||||||
const visibleOpts = helper.visibleOptions(cmd);
|
const visibleOpts = helper.visibleOptions(cmd);
|
||||||
const cmdName = cmd.name();
|
const cmdName = cmd.name();
|
||||||
const panelMap = (!isRoot && OPTION_PANELS[cmdName]) ? OPTION_PANELS[cmdName] : {};
|
const panelMap =
|
||||||
|
!isRoot && OPTION_PANELS[cmdName] ? OPTION_PANELS[cmdName] : {};
|
||||||
|
|
||||||
const grouped: Record<string, Option[]> = { Options: [] };
|
const grouped: Record<string, Option[]> = { Options: [] };
|
||||||
for (const panelName of PANEL_ORDER) {
|
for (const panelName of PANEL_ORDER) {
|
||||||
@@ -287,7 +291,7 @@ export function richFormatHelp(cmd: Command, helper: Help): string {
|
|||||||
if (panel && PANEL_ORDER.includes(panel)) {
|
if (panel && PANEL_ORDER.includes(panel)) {
|
||||||
grouped[panel].push(opt);
|
grouped[panel].push(opt);
|
||||||
} else {
|
} else {
|
||||||
grouped["Options"].push(opt);
|
grouped.Options.push(opt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,8 +300,8 @@ export function richFormatHelp(cmd: Command, helper: Help): string {
|
|||||||
|
|
||||||
if (isRoot) {
|
if (isRoot) {
|
||||||
// ROOT: Options first, then command groups (matches Python/Typer ordering)
|
// ROOT: Options first, then command groups (matches Python/Typer ordering)
|
||||||
if (grouped["Options"].length > 0) {
|
if (grouped.Options.length > 0) {
|
||||||
const optRows = formatOptionRows(grouped["Options"]);
|
const optRows = formatOptionRows(grouped.Options);
|
||||||
const panel = renderPanel("Options", optRows, width);
|
const panel = renderPanel("Options", optRows, width);
|
||||||
if (panel) lines.push(panel);
|
if (panel) lines.push(panel);
|
||||||
}
|
}
|
||||||
|
|||||||
+159
-40
@@ -8,11 +8,12 @@ import fs from "node:fs";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { Command } from "commander";
|
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 type { Mem0Config } from "./config.js";
|
||||||
import { loadConfig } 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 { richFormatHelp } from "./help.js";
|
||||||
|
import { CLI_VERSION } from "./version.js";
|
||||||
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
@@ -58,7 +59,12 @@ function resolveIds(
|
|||||||
runId?: string;
|
runId?: string;
|
||||||
},
|
},
|
||||||
): { 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);
|
const hasExplicit = !!(
|
||||||
|
opts.userId ||
|
||||||
|
opts.agentId ||
|
||||||
|
opts.appId ||
|
||||||
|
opts.runId
|
||||||
|
);
|
||||||
if (hasExplicit) {
|
if (hasExplicit) {
|
||||||
return {
|
return {
|
||||||
userId: opts.userId || undefined,
|
userId: opts.userId || undefined,
|
||||||
@@ -91,10 +97,12 @@ function resolveGraph(
|
|||||||
|
|
||||||
program
|
program
|
||||||
.name("mem0")
|
.name("mem0")
|
||||||
.description(`◆ Mem0 CLI v${__CLI_VERSION__} · Node.js SDK\n\nThe Memory Layer for AI Agents`)
|
.description(
|
||||||
|
`◆ Mem0 CLI v${CLI_VERSION} · Node.js SDK\n\nThe Memory Layer for AI Agents`,
|
||||||
|
)
|
||||||
.option("--version", "Show version and exit.")
|
.option("--version", "Show version and exit.")
|
||||||
.on("option:version", () => {
|
.on("option:version", () => {
|
||||||
console.log(` ${colors.brand("◆ Mem0")} CLI v${__CLI_VERSION__}`);
|
console.log(` ${colors.brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
})
|
})
|
||||||
.usage("<command> [options]")
|
.usage("<command> [options]")
|
||||||
@@ -106,18 +114,28 @@ program
|
|||||||
|
|
||||||
program
|
program
|
||||||
.command("init")
|
.command("init")
|
||||||
.description("Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).")
|
.description(
|
||||||
|
"Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
|
||||||
|
)
|
||||||
.option("--api-key <key>", "API key (skip prompt).")
|
.option("--api-key <key>", "API key (skip prompt).")
|
||||||
.option("-u, --user-id <id>", "Default user ID (skip prompt).")
|
.option("-u, --user-id <id>", "Default user ID (skip prompt).")
|
||||||
.option("--email <email>", "Login via email verification code.")
|
.option("--email <email>", "Login via email verification code.")
|
||||||
.option("--code <code>", "Verification code (use with --email for non-interactive login).")
|
.option(
|
||||||
|
"--code <code>",
|
||||||
|
"Verification code (use with --email for non-interactive login).",
|
||||||
|
)
|
||||||
.addHelpText(
|
.addHelpText(
|
||||||
"after",
|
"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",
|
"\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) => {
|
.action(async (opts) => {
|
||||||
const { runInit } = await import("./commands/init.js");
|
const { runInit } = await import("./commands/init.js");
|
||||||
await runInit({ apiKey: opts.apiKey, userId: opts.userId, email: opts.email, code: opts.code });
|
await runInit({
|
||||||
|
apiKey: opts.apiKey,
|
||||||
|
userId: opts.userId,
|
||||||
|
email: opts.email,
|
||||||
|
code: opts.code,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Memory: add ───────────────────────────────────────────────────────────
|
// ── Memory: add ───────────────────────────────────────────────────────────
|
||||||
@@ -141,7 +159,10 @@ program
|
|||||||
.option("-o, --output <format>", "Output format: text, json, quiet.", "text")
|
.option("-o, --output <format>", "Output format: text, json, quiet.", "text")
|
||||||
.option("--api-key <key>", "Override API key.")
|
.option("--api-key <key>", "Override API key.")
|
||||||
.option("--base-url <url>", "Override API base URL.")
|
.option("--base-url <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')
|
.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) => {
|
.action(async (text, opts) => {
|
||||||
const { cmdAdd } = await import("./commands/memory.js");
|
const { cmdAdd } = await import("./commands/memory.js");
|
||||||
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
||||||
@@ -159,8 +180,18 @@ program
|
|||||||
.option("--agent-id <id>", "Filter by agent.")
|
.option("--agent-id <id>", "Filter by agent.")
|
||||||
.option("--app-id <id>", "Filter by app.")
|
.option("--app-id <id>", "Filter by app.")
|
||||||
.option("--run-id <id>", "Filter by run.")
|
.option("--run-id <id>", "Filter by run.")
|
||||||
.option("-k, --top-k <n>", "Number of results.", (v) => parseInt(v), 10)
|
.option(
|
||||||
.option("--threshold <n>", "Minimum similarity score.", (v) => parseFloat(v), 0.3)
|
"-k, --top-k <n>",
|
||||||
|
"Number of results.",
|
||||||
|
(v) => Number.parseInt(v),
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
.option(
|
||||||
|
"--threshold <n>",
|
||||||
|
"Minimum similarity score.",
|
||||||
|
(v) => Number.parseFloat(v),
|
||||||
|
0.3,
|
||||||
|
)
|
||||||
.option("--rerank", "Enable reranking (Platform only).", false)
|
.option("--rerank", "Enable reranking (Platform only).", false)
|
||||||
.option("--keyword", "Use keyword search.", false)
|
.option("--keyword", "Use keyword search.", false)
|
||||||
.option("--filter <json>", "Advanced filter expression (JSON).")
|
.option("--filter <json>", "Advanced filter expression (JSON).")
|
||||||
@@ -170,7 +201,10 @@ program
|
|||||||
.option("-o, --output <format>", "Output: text, json, table.", "text")
|
.option("-o, --output <format>", "Output: text, json, table.", "text")
|
||||||
.option("--api-key <key>", "Override API key.")
|
.option("--api-key <key>", "Override API key.")
|
||||||
.option("--base-url <url>", "Override API base URL.")
|
.option("--base-url <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')
|
.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) => {
|
.action(async (query, opts) => {
|
||||||
let resolvedQuery = query;
|
let resolvedQuery = query;
|
||||||
if (!resolvedQuery && !process.stdin.isTTY) {
|
if (!resolvedQuery && !process.stdin.isTTY) {
|
||||||
@@ -205,7 +239,10 @@ program
|
|||||||
.option("-o, --output <format>", "Output: text, json.", "text")
|
.option("-o, --output <format>", "Output: text, json.", "text")
|
||||||
.option("--api-key <key>", "Override API key.")
|
.option("--api-key <key>", "Override API key.")
|
||||||
.option("--base-url <url>", "Override API base URL.")
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
.addHelpText("after", "\nExamples:\n $ mem0 get abc-123-def-456\n $ mem0 get abc-123-def-456 -o json")
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 get abc-123-def-456\n $ mem0 get abc-123-def-456 -o json",
|
||||||
|
)
|
||||||
.action(async (memoryId, opts) => {
|
.action(async (memoryId, opts) => {
|
||||||
const { cmdGet } = await import("./commands/memory.js");
|
const { cmdGet } = await import("./commands/memory.js");
|
||||||
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
@@ -221,8 +258,13 @@ program
|
|||||||
.option("--agent-id <id>", "Filter by agent.")
|
.option("--agent-id <id>", "Filter by agent.")
|
||||||
.option("--app-id <id>", "Filter by app.")
|
.option("--app-id <id>", "Filter by app.")
|
||||||
.option("--run-id <id>", "Filter by run.")
|
.option("--run-id <id>", "Filter by run.")
|
||||||
.option("--page <n>", "Page number.", (v) => parseInt(v), 1)
|
.option("--page <n>", "Page number.", (v) => Number.parseInt(v), 1)
|
||||||
.option("--page-size <n>", "Results per page.", (v) => parseInt(v), 100)
|
.option(
|
||||||
|
"--page-size <n>",
|
||||||
|
"Results per page.",
|
||||||
|
(v) => Number.parseInt(v),
|
||||||
|
100,
|
||||||
|
)
|
||||||
.option("--category <name>", "Filter by category.")
|
.option("--category <name>", "Filter by category.")
|
||||||
.option("--after <date>", "Created after (YYYY-MM-DD).")
|
.option("--after <date>", "Created after (YYYY-MM-DD).")
|
||||||
.option("--before <date>", "Created before (YYYY-MM-DD).")
|
.option("--before <date>", "Created before (YYYY-MM-DD).")
|
||||||
@@ -231,7 +273,10 @@ program
|
|||||||
.option("-o, --output <format>", "Output: text, json, table.", "table")
|
.option("-o, --output <format>", "Output: text, json, table.", "table")
|
||||||
.option("--api-key <key>", "Override API key.")
|
.option("--api-key <key>", "Override API key.")
|
||||||
.option("--base-url <url>", "Override API base URL.")
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
.addHelpText("after", "\nExamples:\n $ mem0 list -u alice\n $ mem0 list --category prefs --after 2024-01-01 -o json")
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 list -u alice\n $ mem0 list --category prefs --after 2024-01-01 -o json",
|
||||||
|
)
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
const { cmdList } = await import("./commands/memory.js");
|
const { cmdList } = await import("./commands/memory.js");
|
||||||
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
||||||
@@ -258,7 +303,10 @@ program
|
|||||||
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
||||||
.option("--api-key <key>", "Override API key.")
|
.option("--api-key <key>", "Override API key.")
|
||||||
.option("--base-url <url>", "Override API base URL.")
|
.option("--base-url <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`)
|
.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) => {
|
.action(async (memoryId, text, opts) => {
|
||||||
let resolvedText = text;
|
let resolvedText = text;
|
||||||
if (!resolvedText && !opts.metadata && !process.stdin.isTTY) {
|
if (!resolvedText && !opts.metadata && !process.stdin.isTTY) {
|
||||||
@@ -266,7 +314,10 @@ program
|
|||||||
}
|
}
|
||||||
const { cmdUpdate } = await import("./commands/memory.js");
|
const { cmdUpdate } = await import("./commands/memory.js");
|
||||||
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
await cmdUpdate(backend, memoryId, resolvedText, { metadata: opts.metadata, output: opts.output });
|
await cmdUpdate(backend, memoryId, resolvedText, {
|
||||||
|
metadata: opts.metadata,
|
||||||
|
output: opts.output,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Memory: delete (consolidated) ─────────────────────────────────────────
|
// ── Memory: delete (consolidated) ─────────────────────────────────────────
|
||||||
@@ -275,7 +326,11 @@ program
|
|||||||
.command("delete [memoryId]")
|
.command("delete [memoryId]")
|
||||||
.description("Delete a memory, all memories matching a scope, or an entity.")
|
.description("Delete a memory, all memories matching a scope, or an entity.")
|
||||||
.option("--all", "Delete all memories matching scope filters.", false)
|
.option("--all", "Delete all memories matching scope filters.", false)
|
||||||
.option("--entity", "Delete the entity itself and all its memories (cascade).", false)
|
.option(
|
||||||
|
"--entity",
|
||||||
|
"Delete the entity itself and all its memories (cascade).",
|
||||||
|
false,
|
||||||
|
)
|
||||||
.option("--project", "With --all: delete ALL memories project-wide.", false)
|
.option("--project", "With --all: delete ALL memories project-wide.", false)
|
||||||
.option("--dry-run", "Show what would be deleted without deleting.", false)
|
.option("--dry-run", "Show what would be deleted without deleting.", false)
|
||||||
.option("--force", "Skip confirmation.", false)
|
.option("--force", "Skip confirmation.", false)
|
||||||
@@ -286,13 +341,16 @@ program
|
|||||||
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
||||||
.option("--api-key <key>", "Override API key.")
|
.option("--api-key <key>", "Override API key.")
|
||||||
.option("--base-url <url>", "Override API base URL.")
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
.addHelpText("after", [
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
[
|
||||||
"\nExamples:",
|
"\nExamples:",
|
||||||
" $ mem0 delete abc-123-def-456 # single memory",
|
" $ mem0 delete abc-123-def-456 # single memory",
|
||||||
" $ mem0 delete --all -u alice --force # all memories for user",
|
" $ mem0 delete --all -u alice --force # all memories for user",
|
||||||
" $ mem0 delete --all --project --force # project-wide wipe",
|
" $ mem0 delete --all --project --force # project-wide wipe",
|
||||||
" $ mem0 delete --entity -u alice --force # entity + all its memories",
|
" $ mem0 delete --entity -u alice --force # entity + all its memories",
|
||||||
].join("\n"))
|
].join("\n"),
|
||||||
|
)
|
||||||
.action(async (memoryId, opts) => {
|
.action(async (memoryId, opts) => {
|
||||||
// ── Mutual-exclusion checks ──
|
// ── Mutual-exclusion checks ──
|
||||||
if (memoryId && opts.all) {
|
if (memoryId && opts.all) {
|
||||||
@@ -300,7 +358,9 @@ program
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
if (memoryId && opts.entity) {
|
if (memoryId && opts.entity) {
|
||||||
printError("Cannot combine <memoryId> with --entity. Use one or the other.");
|
printError(
|
||||||
|
"Cannot combine <memoryId> with --entity. Use one or the other.",
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
if (opts.all && opts.entity) {
|
if (opts.all && opts.entity) {
|
||||||
@@ -321,18 +381,36 @@ program
|
|||||||
if (memoryId) {
|
if (memoryId) {
|
||||||
const { cmdDelete } = await import("./commands/memory.js");
|
const { cmdDelete } = await import("./commands/memory.js");
|
||||||
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
await cmdDelete(backend, memoryId, { output: opts.output, dryRun: opts.dryRun, force: opts.force });
|
await cmdDelete(backend, memoryId, {
|
||||||
|
output: opts.output,
|
||||||
|
dryRun: opts.dryRun,
|
||||||
|
force: opts.force,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Dispatch: --all ──
|
// ── Dispatch: --all ──
|
||||||
if (opts.all) {
|
if (opts.all) {
|
||||||
const { cmdDeleteAll } = await import("./commands/memory.js");
|
const { cmdDeleteAll } = await import("./commands/memory.js");
|
||||||
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
const { backend, config } = getBackendAndConfig(
|
||||||
|
opts.apiKey,
|
||||||
|
opts.baseUrl,
|
||||||
|
);
|
||||||
const ids = opts.project
|
const ids = opts.project
|
||||||
? { userId: undefined, agentId: undefined, appId: undefined, runId: undefined }
|
? {
|
||||||
|
userId: undefined,
|
||||||
|
agentId: undefined,
|
||||||
|
appId: undefined,
|
||||||
|
runId: undefined,
|
||||||
|
}
|
||||||
: resolveIds(config, opts);
|
: resolveIds(config, opts);
|
||||||
await cmdDeleteAll(backend, { force: opts.force, dryRun: opts.dryRun, all: opts.project, ...ids, output: opts.output });
|
await cmdDeleteAll(backend, {
|
||||||
|
force: opts.force,
|
||||||
|
dryRun: opts.dryRun,
|
||||||
|
all: opts.project,
|
||||||
|
...ids,
|
||||||
|
output: opts.output,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,7 +434,10 @@ configCmd
|
|||||||
.command("show")
|
.command("show")
|
||||||
.description("Display current configuration (secrets redacted).")
|
.description("Display current configuration (secrets redacted).")
|
||||||
.option("-o, --output <format>", "Output: text, json.", "text")
|
.option("-o, --output <format>", "Output: text, json.", "text")
|
||||||
.addHelpText("after", "\nExamples:\n $ mem0 config show\n $ mem0 config show -o json")
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 config show\n $ mem0 config show -o json",
|
||||||
|
)
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
const { cmdConfigShow } = await import("./commands/config.js");
|
const { cmdConfigShow } = await import("./commands/config.js");
|
||||||
cmdConfigShow({ output: opts.output });
|
cmdConfigShow({ output: opts.output });
|
||||||
@@ -365,7 +446,10 @@ configCmd
|
|||||||
configCmd
|
configCmd
|
||||||
.command("get <key>")
|
.command("get <key>")
|
||||||
.description("Get a configuration value.")
|
.description("Get a configuration value.")
|
||||||
.addHelpText("after", "\nExamples:\n $ mem0 config get platform.api_key\n $ mem0 config get defaults.user_id")
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 config get platform.api_key\n $ mem0 config get defaults.user_id",
|
||||||
|
)
|
||||||
.action(async (key) => {
|
.action(async (key) => {
|
||||||
const { cmdConfigGet } = await import("./commands/config.js");
|
const { cmdConfigGet } = await import("./commands/config.js");
|
||||||
cmdConfigGet(key);
|
cmdConfigGet(key);
|
||||||
@@ -374,7 +458,10 @@ configCmd
|
|||||||
configCmd
|
configCmd
|
||||||
.command("set <key> <value>")
|
.command("set <key> <value>")
|
||||||
.description("Set a configuration value.")
|
.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")
|
.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) => {
|
.action(async (key, value) => {
|
||||||
const { cmdConfigSet } = await import("./commands/config.js");
|
const { cmdConfigSet } = await import("./commands/config.js");
|
||||||
cmdConfigSet(key, value);
|
cmdConfigSet(key, value);
|
||||||
@@ -394,7 +481,10 @@ entityCmd
|
|||||||
.option("-o, --output <format>", "Output: table, json.", "table")
|
.option("-o, --output <format>", "Output: table, json.", "table")
|
||||||
.option("--api-key <key>", "Override API key.")
|
.option("--api-key <key>", "Override API key.")
|
||||||
.option("--base-url <url>", "Override API base URL.")
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
.addHelpText("after", "\nExamples:\n $ mem0 entity list users\n $ mem0 entity list agents -o json")
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 entity list users\n $ mem0 entity list agents -o json",
|
||||||
|
)
|
||||||
.action(async (entityType, opts) => {
|
.action(async (entityType, opts) => {
|
||||||
const { cmdEntitiesList } = await import("./commands/entities.js");
|
const { cmdEntitiesList } = await import("./commands/entities.js");
|
||||||
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
@@ -413,7 +503,10 @@ entityCmd
|
|||||||
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
||||||
.option("--api-key <key>", "Override API key.")
|
.option("--api-key <key>", "Override API key.")
|
||||||
.option("--base-url <url>", "Override API base URL.")
|
.option("--base-url <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")
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 entity delete --user-id alice --force\n $ mem0 entity delete --user-id alice --dry-run",
|
||||||
|
)
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
const { cmdEntitiesDelete } = await import("./commands/entities.js");
|
const { cmdEntitiesDelete } = await import("./commands/entities.js");
|
||||||
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
@@ -439,7 +532,6 @@ program
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("import <filePath>")
|
.command("import <filePath>")
|
||||||
.description("Import memories from a JSON file.")
|
.description("Import memories from a JSON file.")
|
||||||
@@ -448,19 +540,28 @@ program
|
|||||||
.option("-o, --output <format>", "Output: text, json.", "text")
|
.option("-o, --output <format>", "Output: text, json.", "text")
|
||||||
.option("--api-key <key>", "Override API key.")
|
.option("--api-key <key>", "Override API key.")
|
||||||
.option("--base-url <url>", "Override API base URL.")
|
.option("--base-url <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")
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 import data.json --user-id alice\n $ mem0 import data.json -u alice -o json",
|
||||||
|
)
|
||||||
.action(async (filePath, opts) => {
|
.action(async (filePath, opts) => {
|
||||||
const { cmdImport } = await import("./commands/utils.js");
|
const { cmdImport } = await import("./commands/utils.js");
|
||||||
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
||||||
const ids = resolveIds(config, opts);
|
const ids = resolveIds(config, opts);
|
||||||
await cmdImport(backend, filePath, { userId: ids.userId, agentId: ids.agentId, output: opts.output });
|
await cmdImport(backend, filePath, {
|
||||||
|
userId: ids.userId,
|
||||||
|
agentId: ids.agentId,
|
||||||
|
output: opts.output,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Help (machine-readable) ──────────────────────────────────────────────
|
// ── Help (machine-readable) ──────────────────────────────────────────────
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("help")
|
.command("help")
|
||||||
.description("Show help. Use --json for machine-readable output (for LLM agents).")
|
.description(
|
||||||
|
"Show help. Use --json for machine-readable output (for LLM agents).",
|
||||||
|
)
|
||||||
.option("--json", "Output machine-readable JSON for LLM agents.", false)
|
.option("--json", "Output machine-readable JSON for LLM agents.", false)
|
||||||
.addHelpText("after", "\nExamples:\n $ mem0 help\n $ mem0 help --json")
|
.addHelpText("after", "\nExamples:\n $ mem0 help\n $ mem0 help --json")
|
||||||
.action((opts) => {
|
.action((opts) => {
|
||||||
@@ -472,19 +573,35 @@ program
|
|||||||
const spec = JSON.parse(fs.readFileSync(specPath, "utf-8"));
|
const spec = JSON.parse(fs.readFileSync(specPath, "utf-8"));
|
||||||
console.log(JSON.stringify(spec, null, 2));
|
console.log(JSON.stringify(spec, null, 2));
|
||||||
} else {
|
} else {
|
||||||
console.log(JSON.stringify({ name: "mem0", version: __CLI_VERSION__, description: "The Memory Layer for AI Agents" }, null, 2));
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
name: "mem0",
|
||||||
|
version: CLI_VERSION,
|
||||||
|
description: "The Memory Layer for AI Agents",
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const { brand: b } = colors;
|
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(
|
||||||
|
`${b("◆ Mem0 CLI")} v${CLI_VERSION} · Node.js SDK\n The Memory Layer for AI Agents\n`,
|
||||||
|
);
|
||||||
console.log("Usage: mem0 <command> [OPTIONS]\n");
|
console.log("Usage: mem0 <command> [OPTIONS]\n");
|
||||||
console.log("Commands:");
|
console.log("Commands:");
|
||||||
console.log(" add Add a memory from text, messages, file, or stdin");
|
console.log(
|
||||||
|
" add Add a memory from text, messages, file, or stdin",
|
||||||
|
);
|
||||||
console.log(" search Search memories by semantic query");
|
console.log(" search Search memories by semantic query");
|
||||||
console.log(" get Get a specific memory by ID");
|
console.log(" get Get a specific memory by ID");
|
||||||
console.log(" list List memories with optional filters");
|
console.log(" list List memories with optional filters");
|
||||||
console.log(" update Update a memory's text or metadata");
|
console.log(" update Update a memory's text or metadata");
|
||||||
console.log(" delete Delete a memory, all memories, or an entity");
|
console.log(
|
||||||
|
" delete Delete a memory, all memories, or an entity",
|
||||||
|
);
|
||||||
console.log(" import Import memories from a JSON file");
|
console.log(" import Import memories from a JSON file");
|
||||||
console.log(" config Manage configuration (show, get, set)");
|
console.log(" config Manage configuration (show, get, set)");
|
||||||
console.log(" entity Manage entities (list, delete)");
|
console.log(" entity Manage entities (list, delete)");
|
||||||
@@ -492,7 +609,9 @@ program
|
|||||||
console.log(" status Check connectivity and authentication");
|
console.log(" status Check connectivity and authentication");
|
||||||
console.log();
|
console.log();
|
||||||
console.log(" mem0 <command> --help Get help for a command");
|
console.log(" mem0 <command> --help Get help for a command");
|
||||||
console.log(" mem0 help --json Machine-readable help (for LLM agents)");
|
console.log(
|
||||||
|
" mem0 help --json Machine-readable help (for LLM agents)",
|
||||||
|
);
|
||||||
console.log();
|
console.log();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+28
-12
@@ -2,8 +2,8 @@
|
|||||||
* Output formatting for mem0 CLI — text, JSON, table, quiet modes.
|
* Output formatting for mem0 CLI — text, JSON, table, quiet modes.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Table from "cli-table3";
|
|
||||||
import boxen from "boxen";
|
import boxen from "boxen";
|
||||||
|
import Table from "cli-table3";
|
||||||
import { colors, sym } from "./branding.js";
|
import { colors, sym } from "./branding.js";
|
||||||
|
|
||||||
const { brand, accent, success, error: errorColor, dim } = colors;
|
const { brand, accent, success, error: errorColor, dim } = colors;
|
||||||
@@ -52,13 +52,20 @@ export function formatMemoriesText(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatMemoriesTable(memories: Record<string, unknown>[], opts: { showScore?: boolean } = {}): void {
|
export function formatMemoriesTable(
|
||||||
|
memories: Record<string, unknown>[],
|
||||||
|
opts: { showScore?: boolean } = {},
|
||||||
|
): void {
|
||||||
const head = opts.showScore
|
const head = opts.showScore
|
||||||
? [accent("ID"), accent("Score"), accent("Memory"), accent("Category"), accent("Created")]
|
? [
|
||||||
|
accent("ID"),
|
||||||
|
accent("Score"),
|
||||||
|
accent("Memory"),
|
||||||
|
accent("Category"),
|
||||||
|
accent("Created"),
|
||||||
|
]
|
||||||
: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")];
|
: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")];
|
||||||
const colWidths = opts.showScore
|
const colWidths = opts.showScore ? [38, 8, 40, 16, 14] : [38, 40, 16, 14];
|
||||||
? [38, 8, 40, 16, 14]
|
|
||||||
: [38, 40, 16, 14];
|
|
||||||
const table = new Table({
|
const table = new Table({
|
||||||
head,
|
head,
|
||||||
colWidths,
|
colWidths,
|
||||||
@@ -67,10 +74,10 @@ export function formatMemoriesTable(memories: Record<string, unknown>[], opts: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const mem of memories) {
|
for (const mem of memories) {
|
||||||
const memId = ((mem.id as string) ?? "");
|
const memId = (mem.id as string) ?? "";
|
||||||
let memoryText = (mem.memory ?? mem.text ?? "") as string;
|
let memoryText = (mem.memory ?? mem.text ?? "") as string;
|
||||||
if (memoryText.length > 60) {
|
if (memoryText.length > 60) {
|
||||||
memoryText = memoryText.slice(0, 57) + "...";
|
memoryText = `${memoryText.slice(0, 57)}...`;
|
||||||
}
|
}
|
||||||
const categories = mem.categories;
|
const categories = mem.categories;
|
||||||
const cat =
|
const cat =
|
||||||
@@ -123,7 +130,9 @@ export function formatSingleMemory(
|
|||||||
if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`);
|
if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`);
|
||||||
const categories = mem.categories;
|
const categories = mem.categories;
|
||||||
if (categories) {
|
if (categories) {
|
||||||
const catStr = Array.isArray(categories) ? categories.join(", ") : String(categories);
|
const catStr = Array.isArray(categories)
|
||||||
|
? categories.join(", ")
|
||||||
|
: String(categories);
|
||||||
lines.push(` ${dim("Categories:")} ${catStr}`);
|
lines.push(` ${dim("Categories:")} ${catStr}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +174,10 @@ export function formatAddResult(
|
|||||||
if (r.status === "PENDING") {
|
if (r.status === "PENDING") {
|
||||||
const eventId = ((r.event_id as string) ?? "").slice(0, 8);
|
const eventId = ((r.event_id as string) ?? "").slice(0, 8);
|
||||||
const icon = accent(sym("⧗", "..."));
|
const icon = accent(sym("⧗", "..."));
|
||||||
const parts = [` ${icon} ${dim("Queued".padEnd(10))}`, "Processing in background"];
|
const parts = [
|
||||||
|
` ${icon} ${dim("Queued".padEnd(10))}`,
|
||||||
|
"Processing in background",
|
||||||
|
];
|
||||||
if (eventId) parts.push(dim(`(event ${eventId})`));
|
if (eventId) parts.push(dim(`(event ${eventId})`));
|
||||||
console.log(parts.join(" "));
|
console.log(parts.join(" "));
|
||||||
continue;
|
continue;
|
||||||
@@ -173,7 +185,10 @@ export function formatAddResult(
|
|||||||
|
|
||||||
const event = (r.event ?? "ADD") as string;
|
const event = (r.event ?? "ADD") as string;
|
||||||
const memory = (r.memory ?? r.text ?? r.content ?? r.data ?? "") as string;
|
const memory = (r.memory ?? r.text ?? r.content ?? r.data ?? "") as string;
|
||||||
const memId = ((r.id as string) ?? (r.memory_id as string) ?? "").slice(0, 8);
|
const memId = ((r.id as string) ?? (r.memory_id as string) ?? "").slice(
|
||||||
|
0,
|
||||||
|
8,
|
||||||
|
);
|
||||||
|
|
||||||
let icon: string;
|
let icon: string;
|
||||||
let label: string;
|
let label: string;
|
||||||
@@ -237,7 +252,8 @@ export function printResultSummary(opts: {
|
|||||||
.map(([k, v]) => `${k.replace(/_/g, " ")}=${v}`);
|
.map(([k, v]) => `${k.replace(/_/g, " ")}=${v}`);
|
||||||
if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
|
if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
|
||||||
}
|
}
|
||||||
if (opts.durationSecs !== undefined) parts.push(`${opts.durationSecs.toFixed(2)}s`);
|
if (opts.durationSecs !== undefined)
|
||||||
|
parts.push(`${opts.durationSecs.toFixed(2)}s`);
|
||||||
|
|
||||||
console.log(` ${dim(parts.join(" · "))}`);
|
console.log(` ${dim(parts.join(" · "))}`);
|
||||||
console.log();
|
console.log();
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -80,19 +80,19 @@ describe("printWarning", () => {
|
|||||||
describe("printInfo", () => {
|
describe("printInfo", () => {
|
||||||
it("prints info message", () => {
|
it("prints info message", () => {
|
||||||
printInfo("Important note");
|
printInfo("Important note");
|
||||||
expect(output).toContain("Important note");
|
expect(errOutput).toContain("Important note");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("printScope", () => {
|
describe("printScope", () => {
|
||||||
it("prints scope when IDs present", () => {
|
it("prints scope when IDs present", () => {
|
||||||
printScope({ user_id: "alice", agent_id: "bot" });
|
printScope({ user_id: "alice", agent_id: "bot" });
|
||||||
expect(output).toContain("alice");
|
expect(errOutput).toContain("alice");
|
||||||
expect(output).toContain("bot");
|
expect(errOutput).toContain("bot");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prints nothing when no IDs", () => {
|
it("prints nothing when no IDs", () => {
|
||||||
printScope({});
|
printScope({});
|
||||||
expect(output).toBe("");
|
expect(errOutput).toBe("");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ describe("cmdSearch", () => {
|
|||||||
enableGraph: false,
|
enableGraph: false,
|
||||||
output: "text",
|
output: "text",
|
||||||
});
|
});
|
||||||
expect(output).toContain("No memories found");
|
expect(errOutput).toContain("No memories found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ describe("cmdList", () => {
|
|||||||
enableGraph: false,
|
enableGraph: false,
|
||||||
output: "text",
|
output: "text",
|
||||||
});
|
});
|
||||||
expect(output).toContain("No memories found");
|
expect(errOutput).toContain("No memories found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -253,8 +253,12 @@ def search(
|
|||||||
help="Specific fields to return (comma-separated).",
|
help="Specific fields to return (comma-separated).",
|
||||||
rich_help_panel="Search",
|
rich_help_panel="Search",
|
||||||
),
|
),
|
||||||
graph: bool = typer.Option(False, "--graph", help="Enable graph in search.", rich_help_panel="Search"),
|
graph: bool = typer.Option(
|
||||||
no_graph: bool = typer.Option(False, "--no-graph", help="Disable graph in search.", rich_help_panel="Search"),
|
False, "--graph", help="Enable graph in search.", rich_help_panel="Search"
|
||||||
|
),
|
||||||
|
no_graph: bool = typer.Option(
|
||||||
|
False, "--no-graph", help="Disable graph in search.", rich_help_panel="Search"
|
||||||
|
),
|
||||||
output: str = typer.Option(
|
output: str = typer.Option(
|
||||||
"text", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
"text", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
||||||
),
|
),
|
||||||
@@ -372,8 +376,12 @@ def list_cmd(
|
|||||||
before: str | None = typer.Option(
|
before: str | None = typer.Option(
|
||||||
None, "--before", help="Created before (YYYY-MM-DD).", rich_help_panel="Filters"
|
None, "--before", help="Created before (YYYY-MM-DD).", rich_help_panel="Filters"
|
||||||
),
|
),
|
||||||
graph: bool = typer.Option(False, "--graph", help="Enable graph in listing.", rich_help_panel="Filters"),
|
graph: bool = typer.Option(
|
||||||
no_graph: bool = typer.Option(False, "--no-graph", help="Disable graph in listing.", rich_help_panel="Filters"),
|
False, "--graph", help="Enable graph in listing.", rich_help_panel="Filters"
|
||||||
|
),
|
||||||
|
no_graph: bool = typer.Option(
|
||||||
|
False, "--no-graph", help="Disable graph in listing.", rich_help_panel="Filters"
|
||||||
|
),
|
||||||
output: str = typer.Option(
|
output: str = typer.Option(
|
||||||
"table", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
"table", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
||||||
),
|
),
|
||||||
@@ -463,11 +471,19 @@ def update(
|
|||||||
|
|
||||||
@app.command(rich_help_panel="Memory")
|
@app.command(rich_help_panel="Memory")
|
||||||
def delete(
|
def delete(
|
||||||
memory_id: str | None = typer.Argument(None, help="Memory ID to delete (omit when using --all or --entity)."),
|
memory_id: str | None = typer.Argument(
|
||||||
|
None, help="Memory ID to delete (omit when using --all or --entity)."
|
||||||
|
),
|
||||||
all_: bool = typer.Option(False, "--all", help="Delete all memories matching scope filters."),
|
all_: bool = typer.Option(False, "--all", help="Delete all memories matching scope filters."),
|
||||||
entity: bool = typer.Option(False, "--entity", help="Delete the entity itself and all its memories (cascade)."),
|
entity: bool = typer.Option(
|
||||||
project: bool = typer.Option(False, "--project", help="With --all: delete ALL memories project-wide."),
|
False, "--entity", help="Delete the entity itself and all its memories (cascade)."
|
||||||
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting."),
|
),
|
||||||
|
project: bool = typer.Option(
|
||||||
|
False, "--project", help="With --all: delete ALL memories project-wide."
|
||||||
|
),
|
||||||
|
dry_run: bool = typer.Option(
|
||||||
|
False, "--dry-run", help="Show what would be deleted without deleting."
|
||||||
|
),
|
||||||
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
|
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
|
||||||
user_id: str | None = typer.Option(
|
user_id: str | None = typer.Option(
|
||||||
None, "--user-id", "-u", help="Scope to user.", rich_help_panel="Scope"
|
None, "--user-id", "-u", help="Scope to user.", rich_help_panel="Scope"
|
||||||
@@ -641,14 +657,12 @@ def entity_delete(
|
|||||||
agent_id: str | None = typer.Option(
|
agent_id: str | None = typer.Option(
|
||||||
None, "--agent-id", help="Agent ID.", rich_help_panel="Scope"
|
None, "--agent-id", help="Agent ID.", rich_help_panel="Scope"
|
||||||
),
|
),
|
||||||
app_id: str | None = typer.Option(
|
app_id: str | None = typer.Option(None, "--app-id", help="App ID.", rich_help_panel="Scope"),
|
||||||
None, "--app-id", help="App ID.", rich_help_panel="Scope"
|
run_id: str | None = typer.Option(None, "--run-id", help="Run ID.", rich_help_panel="Scope"),
|
||||||
),
|
|
||||||
run_id: str | None = typer.Option(
|
|
||||||
None, "--run-id", help="Run ID.", rich_help_panel="Scope"
|
|
||||||
),
|
|
||||||
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
|
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
|
||||||
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting."),
|
dry_run: bool = typer.Option(
|
||||||
|
False, "--dry-run", help="Show what would be deleted without deleting."
|
||||||
|
),
|
||||||
output: str = typer.Option(
|
output: str = typer.Option(
|
||||||
"text", "--output", "-o", help="Output: text, json, quiet.", rich_help_panel="Output"
|
"text", "--output", "-o", help="Output: text, json, quiet.", rich_help_panel="Output"
|
||||||
),
|
),
|
||||||
@@ -694,9 +708,13 @@ app.add_typer(entity_app, name="entity", rich_help_panel="Management")
|
|||||||
@app.command(rich_help_panel="Management")
|
@app.command(rich_help_panel="Management")
|
||||||
def init(
|
def init(
|
||||||
api_key: str | None = typer.Option(None, "--api-key", help="API key (skip prompt)."),
|
api_key: str | None = typer.Option(None, "--api-key", help="API key (skip prompt)."),
|
||||||
user_id: str | None = typer.Option(None, "--user-id", "-u", help="Default user ID (skip prompt)."),
|
user_id: str | None = typer.Option(
|
||||||
|
None, "--user-id", "-u", help="Default user ID (skip prompt)."
|
||||||
|
),
|
||||||
email: str | None = typer.Option(None, "--email", help="Login via email verification code."),
|
email: str | None = typer.Option(None, "--email", help="Login via email verification code."),
|
||||||
code: str | None = typer.Option(None, "--code", help="Verification code (use with --email for non-interactive login)."),
|
code: str | None = typer.Option(
|
||||||
|
None, "--code", help="Verification code (use with --email for non-interactive login)."
|
||||||
|
),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Interactive setup wizard for mem0 CLI.
|
"""Interactive setup wizard for mem0 CLI.
|
||||||
|
|
||||||
@@ -747,7 +765,6 @@ def status(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("import", rich_help_panel="Management")
|
@app.command("import", rich_help_panel="Management")
|
||||||
def import_cmd(
|
def import_cmd(
|
||||||
file_path: str = typer.Argument(..., help="JSON file to import."),
|
file_path: str = typer.Argument(..., help="JSON file to import."),
|
||||||
@@ -1018,6 +1035,7 @@ def help(
|
|||||||
def version() -> None:
|
def version() -> None:
|
||||||
"""Show version and exit."""
|
"""Show version and exit."""
|
||||||
from mem0_cli.commands.utils import cmd_version
|
from mem0_cli.commands.utils import cmd_version
|
||||||
|
|
||||||
cmd_version()
|
cmd_version()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ class Backend(ABC):
|
|||||||
def entities(self, entity_type: str) -> list[dict]: ...
|
def entities(self, entity_type: str) -> list[dict]: ...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_backend(config: Mem0Config) -> Backend:
|
def get_backend(config: Mem0Config) -> Backend:
|
||||||
"""Return the Platform backend."""
|
"""Return the Platform backend."""
|
||||||
from mem0_cli.backend.platform import PlatformBackend
|
from mem0_cli.backend.platform import PlatformBackend
|
||||||
|
|||||||
@@ -312,7 +312,6 @@ class PlatformBackend(Backend):
|
|||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AuthError(Exception):
|
class AuthError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> Non
|
|||||||
"""List entities of a given type."""
|
"""List entities of a given type."""
|
||||||
valid_types = {"users", "agents", "apps", "runs"}
|
valid_types = {"users", "agents", "apps", "runs"}
|
||||||
if entity_type not in valid_types:
|
if entity_type not in valid_types:
|
||||||
print_error(err_console, f"Invalid entity type: {entity_type}. Use: {', '.join(valid_types)}")
|
print_error(
|
||||||
|
err_console, f"Invalid entity type: {entity_type}. Use: {', '.join(valid_types)}"
|
||||||
|
)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
_start = _time.perf_counter()
|
_start = _time.perf_counter()
|
||||||
@@ -76,7 +78,9 @@ def cmd_entities_delete(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Delete an entity and all its memories (cascade delete)."""
|
"""Delete an entity and all its memories (cascade delete)."""
|
||||||
if not any([user_id, agent_id, app_id, run_id]):
|
if not any([user_id, agent_id, app_id, run_id]):
|
||||||
print_error(err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id.")
|
print_error(
|
||||||
|
err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id."
|
||||||
|
)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
|
|||||||
@@ -265,7 +265,9 @@ def run_init(
|
|||||||
|
|
||||||
api_key_val = result.get("api_key")
|
api_key_val = result.get("api_key")
|
||||||
if not api_key_val:
|
if not api_key_val:
|
||||||
print_error(err_console, "Auth succeeded but no API key was returned. Contact support.")
|
print_error(
|
||||||
|
err_console, "Auth succeeded but no API key was returned. Contact support."
|
||||||
|
)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
config.platform.api_key = api_key_val
|
config.platform.api_key = api_key_val
|
||||||
config.platform.base_url = base_url
|
config.platform.base_url = base_url
|
||||||
|
|||||||
@@ -98,10 +98,14 @@ def cmd_add(
|
|||||||
# Validate --expires
|
# Validate --expires
|
||||||
if expires:
|
if expires:
|
||||||
import re
|
import re
|
||||||
if not re.match(r'^\d{4}-\d{2}-\d{2}$', expires):
|
|
||||||
print_error(err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).")
|
if not re.match(r"^\d{4}-\d{2}-\d{2}$", expires):
|
||||||
|
print_error(
|
||||||
|
err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31)."
|
||||||
|
)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
if date.fromisoformat(expires) <= date.today():
|
if date.fromisoformat(expires) <= date.today():
|
||||||
print_error(err_console, "--expires date must be in the future.")
|
print_error(err_console, "--expires date must be in the future.")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
@@ -290,6 +294,7 @@ def cmd_list(
|
|||||||
|
|
||||||
if output == "json":
|
if output == "json":
|
||||||
from mem0_cli.output import format_json_envelope
|
from mem0_cli.output import format_json_envelope
|
||||||
|
|
||||||
format_json_envelope(
|
format_json_envelope(
|
||||||
console,
|
console,
|
||||||
command="list",
|
command="list",
|
||||||
|
|||||||
@@ -110,7 +110,9 @@ def cmd_import(
|
|||||||
added = 0
|
added = 0
|
||||||
failed = 0
|
failed = 0
|
||||||
_start = _time.perf_counter()
|
_start = _time.perf_counter()
|
||||||
for item in track(data, description=f"[{DIM_COLOR}]Importing memories...[/]", console=err_console):
|
for item in track(
|
||||||
|
data, description=f"[{DIM_COLOR}]Importing memories...[/]", console=err_console
|
||||||
|
):
|
||||||
content = item.get("memory", item.get("text", item.get("content", "")))
|
content = item.get("memory", item.get("text", item.get("content", "")))
|
||||||
if not content:
|
if not content:
|
||||||
failed += 1
|
failed += 1
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ def format_memories_text(console: Console, memories: list[dict], title: str = "m
|
|||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
def format_memories_table(console: Console, memories: list[dict], *, show_score: bool = False) -> None:
|
def format_memories_table(
|
||||||
|
console: Console, memories: list[dict], *, show_score: bool = False
|
||||||
|
) -> None:
|
||||||
"""Render memories in a rich table."""
|
"""Render memories in a rich table."""
|
||||||
table = Table(
|
table = Table(
|
||||||
border_style=BRAND_COLOR,
|
border_style=BRAND_COLOR,
|
||||||
@@ -71,7 +73,11 @@ def format_memories_table(console: Console, memories: list[dict], *, show_score:
|
|||||||
memory_text = memory_text[:57] + "..."
|
memory_text = memory_text[:57] + "..."
|
||||||
categories = mem.get("categories", [])
|
categories = mem.get("categories", [])
|
||||||
if isinstance(categories, list) and categories:
|
if isinstance(categories, list) and categories:
|
||||||
cat = categories[0] if len(categories) == 1 else f"{categories[0]} (+{len(categories) - 1})"
|
cat = (
|
||||||
|
categories[0]
|
||||||
|
if len(categories) == 1
|
||||||
|
else f"{categories[0]} (+{len(categories) - 1})"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
cat = "—"
|
cat = "—"
|
||||||
created = _format_date(mem.get("created_at")) or "—"
|
created = _format_date(mem.get("created_at")) or "—"
|
||||||
|
|||||||
@@ -8,11 +8,19 @@ boundaries).
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[mKJHABCDfsu]")
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_ansi(text: str) -> str:
|
||||||
|
"""Remove ANSI escape codes so substring checks work regardless of color mode."""
|
||||||
|
return _ANSI_RE.sub("", text)
|
||||||
|
|
||||||
|
|
||||||
def _run(
|
def _run(
|
||||||
args: list[str],
|
args: list[str],
|
||||||
@@ -28,22 +36,35 @@ def _run(
|
|||||||
reads config from ``<home_dir>/.mem0/config.json`` instead
|
reads config from ``<home_dir>/.mem0/config.json`` instead
|
||||||
of the user's real config. This is critical for tests that
|
of the user's real config. This is critical for tests that
|
||||||
depend on a clean (no API key) or custom config state.
|
depend on a clean (no API key) or custom config state.
|
||||||
|
|
||||||
|
Returns a CompletedProcess whose stdout/stderr have ANSI escape codes
|
||||||
|
stripped. GitHub Actions sets FORCE_COLOR=1 which causes Rich/Typer to
|
||||||
|
fragment option names like --user-id into separately-styled ANSI segments,
|
||||||
|
making plain ``in`` checks fail. Stripping here is version-agnostic and
|
||||||
|
ensures all assertions see the same plain text regardless of terminal env.
|
||||||
"""
|
"""
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
# Strip all MEM0_ env vars so tests start clean
|
# Strip all MEM0_ env vars so tests start clean
|
||||||
for key in list(env.keys()):
|
for key in list(env.keys()):
|
||||||
if key.startswith("MEM0_"):
|
if key.startswith("MEM0_"):
|
||||||
del env[key]
|
del env[key]
|
||||||
|
env.pop("FORCE_COLOR", None)
|
||||||
if home_dir:
|
if home_dir:
|
||||||
env["HOME"] = home_dir
|
env["HOME"] = home_dir
|
||||||
if env_override:
|
if env_override:
|
||||||
env.update(env_override)
|
env.update(env_override)
|
||||||
return subprocess.run(
|
result = subprocess.run(
|
||||||
[sys.executable, "-m", "mem0_cli", *args],
|
[sys.executable, "-m", "mem0_cli", *args],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
|
return subprocess.CompletedProcess(
|
||||||
|
args=result.args,
|
||||||
|
returncode=result.returncode,
|
||||||
|
stdout=_strip_ansi(result.stdout),
|
||||||
|
stderr=_strip_ansi(result.stderr),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -67,7 +88,6 @@ class TestCLIIntegration:
|
|||||||
assert result.returncode == 0
|
assert result.returncode == 0
|
||||||
assert "0.1.0" in result.stdout
|
assert "0.1.0" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
def test_add_help(self):
|
def test_add_help(self):
|
||||||
result = _run(["add", "--help"])
|
result = _run(["add", "--help"])
|
||||||
assert result.returncode == 0
|
assert result.returncode == 0
|
||||||
@@ -161,7 +181,12 @@ class TestCLIIsolated:
|
|||||||
)
|
)
|
||||||
assert result.returncode != 0
|
assert result.returncode != 0
|
||||||
combined = result.stderr + result.stdout
|
combined = result.stderr + result.stdout
|
||||||
assert "memory ID" in combined.lower() or "--all" in combined or "--entity" in combined or "Error" in combined
|
assert (
|
||||||
|
"memory ID" in combined.lower()
|
||||||
|
or "--all" in combined
|
||||||
|
or "--entity" in combined
|
||||||
|
or "Error" in combined
|
||||||
|
)
|
||||||
|
|
||||||
def test_config_show_clean(self, clean_home):
|
def test_config_show_clean(self, clean_home):
|
||||||
"""config show with no config should still work."""
|
"""config show with no config should still work."""
|
||||||
|
|||||||
Reference in New Issue
Block a user