Compare commits

..
Author SHA1 Message Date
mintlify[bot]andGitHub 3ce9a5d517 Fix broken /cli link in introduction.mdx
Generated-By: mintlify-agent
2026-03-31 00:07:47 +00:00
51 changed files with 2264 additions and 4263 deletions
-100
View File
@@ -1,100 +0,0 @@
name: CLI Node CI
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'cli/node/**'
- '.github/workflows/cli-node-ci.yml'
pull_request:
paths:
- 'cli/node/**'
- '.github/workflows/cli-node-ci.yml'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: cli/node/pnpm-lock.yaml
- name: Install dependencies
working-directory: cli/node
run: pnpm install --frozen-lockfile
- name: Lint
working-directory: cli/node
run: pnpm run lint
- name: Type check
working-directory: cli/node
run: pnpm run typecheck
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
cache-dependency-path: cli/node/pnpm-lock.yaml
- name: Install dependencies
working-directory: cli/node
run: pnpm install --frozen-lockfile
- name: Run tests
working-directory: cli/node
run: pnpm run test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: cli/node/pnpm-lock.yaml
- name: Install dependencies
working-directory: cli/node
run: pnpm install --frozen-lockfile
- name: Build
working-directory: cli/node
run: pnpm run build
- name: Verify dist output
run: |
test -f cli/node/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
-79
View File
@@ -1,79 +0,0 @@
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)
+1 -1
View File
@@ -162,7 +162,7 @@ For detailed integration steps, see the [Quickstart](https://docs.mem0.ai/quicks
## 📚 Documentation & Support ## 📚 Documentation & Support
- Full docs: https://docs.mem0.ai - Full docs: https://docs.mem0.ai
- Community: [Discord](https://mem0.dev/DiG) · [X (formerly Twitter)](https://x.com/mem0ai) - Community: [Discord](https://mem0.dev/DiG) · [Twitter](https://x.com/mem0ai)
- Contact: founders@mem0.ai - Contact: founders@mem0.ai
## Citation ## Citation
-13
View File
@@ -153,19 +153,8 @@ Interactive setup wizard for mem0 CLI.
|-----------------|--------|----------|---------|------| |-----------------|--------|----------|---------|------|
| `--api-key` | string | No | - | API key (skip prompt). | | `--api-key` | string | No | - | API key (skip prompt). |
| `-u, --user-id` | string | No | - | Default user ID (skip prompt). | | `-u, --user-id` | string | No | - | Default user ID (skip prompt). |
| `--email` | string | No | - | Login via email verification code. |
| `--code` | string | No | - | Verification code (use with --email for non-interactive login). |
**Behavior:** **Behavior:**
*Email login flow (when `--email` is provided):*
- Sends a 6-digit verification code to the email via `POST /api/v1/auth/email_code/`.
- If `--code` is also provided, verifies immediately (fully non-interactive).
- If `--code` is not provided, prompts for the code interactively.
- On success: receives API key, org_id, project_id. Saves to config. Creates account if email is new.
- Cannot be combined with `--api-key`.
*API key flow (existing behavior):*
- If both `--api-key` and `--user-id` are provided, runs non-interactively (no prompts). - If both `--api-key` and `--user-id` are provided, runs non-interactively (no prompts).
- If running in a non-TTY without both flags, prints an error with usage hint and exits. - If running in a non-TTY without both flags, prints an error with usage hint and exits.
- Interactive mode: prints banner, prompts for API key (masked with `*`), prompts for default user ID (default: `mem0-cli`), validates connection, saves config. - Interactive mode: prints banner, prompts for API key (masked with `*`), prompts for default user ID (default: `mem0-cli`), validates connection, saves config.
@@ -175,8 +164,6 @@ Interactive setup wizard for mem0 CLI.
```bash ```bash
mem0 init mem0 init
mem0 init --api-key m0-xxx --user-id alice mem0 init --api-key m0-xxx --user-id alice
mem0 init --email alice@company.com
mem0 init --email alice@company.com --code 482901
``` ```
--- ---
+2 -8
View File
@@ -17,15 +17,9 @@ Both packages install a `mem0` binary with identical behavior.
## Quick start ## Quick start
```bash ```bash
# Interactive setup wizard # Authenticate and save config
mem0 init mem0 init
# Or login via email (get a new API key)
mem0 init --email alice@company.com
# Or authenticate with an existing API key
mem0 init --api-key m0-xxx
# Add a memory # Add a memory
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
@@ -46,7 +40,7 @@ mem0 delete <memory-id>
| Command | Description | | Command | Description |
|---------|-------------| |---------|-------------|
| `mem0 init` | Setup wizard — login via email or configure API key manually | | `mem0 init` | Interactive setup wizard — configures API key and default user ID |
| `mem0 add` | Add a memory from text, JSON messages, a file, or stdin | | `mem0 add` | Add a memory from text, JSON messages, a file, or stdin |
| `mem0 search` | Search memories using natural language | | `mem0 search` | Search memories using natural language |
| `mem0 list` | List memories with optional filters and pagination | | `mem0 list` | List memories with optional filters and pagination |
+3 -8
View File
@@ -503,20 +503,15 @@
}, },
{ {
"name": "init", "name": "init",
"description": "Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).", "description": "Interactive setup wizard for mem0 CLI.",
"usage": "mem0 init [OPTIONS]", "usage": "mem0 init",
"needsBackend": false, "needsBackend": false,
"needsConfig": false, "needsConfig": false,
"resolveIds": false, "resolveIds": false,
"resolveGraph": false, "resolveGraph": false,
"confirmDangerous": false, "confirmDangerous": false,
"arguments": [], "arguments": [],
"options": [ "options": []
{ "name": "api-key", "flags": ["--api-key"], "type": "string", "default": null, "help": "API key (skip prompt)." },
{ "name": "user-id", "flags": ["-u", "--user-id"], "type": "string", "default": null, "help": "Default user ID (skip prompt)." },
{ "name": "email", "flags": ["--email"], "type": "string", "default": null, "help": "Login via email verification code." },
{ "name": "code", "flags": ["--code"], "type": "string", "default": null, "help": "Verification code (use with --email for non-interactive login)." }
]
}, },
{ {
"name": "status", "name": "status",
+1 -1
View File
@@ -7,7 +7,7 @@
"mem0": "./dist/index.js" "mem0": "./dist/index.js"
}, },
"scripts": { "scripts": {
"build": "tsup", "build": "tsup src/index.ts --format esm --dts --clean",
"dev": "tsx src/index.ts", "dev": "tsx src/index.ts",
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest", "test:watch": "vitest",
+5 -14
View File
@@ -67,10 +67,7 @@ export interface Backend {
opts?: AddOptions, opts?: AddOptions,
): Promise<Record<string, unknown>>; ): Promise<Record<string, unknown>>;
search( search(query: string, opts?: SearchOptions): Promise<Record<string, unknown>[]>;
query: string,
opts?: SearchOptions,
): Promise<Record<string, unknown>[]>;
get(memoryId: string): Promise<Record<string, unknown>>; get(memoryId: string): Promise<Record<string, unknown>>;
@@ -82,24 +79,17 @@ export interface Backend {
metadata?: Record<string, unknown>, metadata?: Record<string, unknown>,
): Promise<Record<string, unknown>>; ): Promise<Record<string, unknown>>;
delete( delete(memoryId?: string, opts?: DeleteOptions): Promise<Record<string, unknown>>;
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< status(opts?: { userId?: string; agentId?: string }): Promise<Record<string, unknown>>;
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( constructor(message = "Authentication failed. Your API key may be invalid or expired.") {
message = "Authentication failed. Your API key may be invalid or expired.",
) {
super(message); super(message);
this.name = "AuthError"; this.name = "AuthError";
} }
@@ -122,3 +112,4 @@ 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);
} }
+1 -8
View File
@@ -3,12 +3,5 @@
*/ */
export { getBackend } from "./base.js"; export { getBackend } from "./base.js";
export type { export type { Backend, AddOptions, SearchOptions, ListOptions, DeleteOptions, EntityIds } from "./base.js";
Backend,
AddOptions,
SearchOptions,
ListOptions,
DeleteOptions,
EntityIds,
} from "./base.js";
export { AuthError, NotFoundError, APIError } from "./base.js"; export { AuthError, NotFoundError, APIError } from "./base.js";
+30 -65
View File
@@ -4,8 +4,8 @@
import type { PlatformConfig } from "../config.js"; import type { PlatformConfig } from "../config.js";
import { import {
APIError,
type AddOptions, type AddOptions,
APIError,
AuthError, AuthError,
type Backend, type Backend,
type DeleteOptions, type DeleteOptions,
@@ -58,24 +58,15 @@ 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();
detail = detail = (body as Record<string, string>).detail ?? resp.statusText;
((body.detail ?? body.message ?? JSON.stringify(body)) as string) ??
resp.statusText;
} catch { } catch {
detail = resp.statusText; detail = resp.statusText;
} }
throw new APIError(path, detail); throw new APIError(path, detail);
} }
if (!resp.ok) { if (!resp.ok) {
let detail: string = resp.statusText; throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
try {
const body = (await resp.json()) as Record<string, unknown>;
detail = (body.detail ?? body.message ?? resp.statusText) as string;
} catch {
/* ignore */
}
throw new Error(`HTTP ${resp.status}: ${detail}`);
} }
if (resp.status === 204) { if (resp.status === 204) {
return {}; return {};
@@ -107,9 +98,10 @@ 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/", { return (await this._request("POST", "/v1/memories/", { json: payload })) as Record<
json: payload, string,
})) as Record<string, unknown>; unknown
>;
} }
private _buildFilters(opts: { private _buildFilters(opts: {
@@ -120,10 +112,7 @@ 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 ( if (opts.extraFilters && ("AND" in opts.extraFilters || "OR" in opts.extraFilters)) {
opts.extraFilters &&
("AND" in opts.extraFilters || "OR" in opts.extraFilters)
) {
return opts.extraFilters; return opts.extraFilters;
} }
@@ -144,10 +133,7 @@ export class PlatformBackend implements Backend {
return undefined; return undefined;
} }
async search( async search(query: string, opts: SearchOptions = {}): Promise<Record<string, unknown>[]> {
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,
@@ -176,15 +162,10 @@ 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< return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record<string, unknown>;
string,
unknown
>;
} }
async listMemories( async listMemories(opts: ListOptions = {}): Promise<Record<string, unknown>[]> {
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),
@@ -196,16 +177,10 @@ 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 = { ...(extra.created_at as Record<string, unknown> | undefined), gte: opts.after };
...(extra.created_at as Record<string, unknown> | undefined),
gte: opts.after,
};
} }
if (opts.before) { if (opts.before) {
extra.created_at = { extra.created_at = { ...(extra.created_at as Record<string, unknown> | undefined), lte: opts.before };
...(extra.created_at as Record<string, unknown> | undefined),
lte: opts.before,
};
} }
const apiFilters = this._buildFilters({ const apiFilters = this._buildFilters({
@@ -218,10 +193,7 @@ 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/", { const result = (await this._request("POST", "/v2/memories/", { json: payload, params })) as unknown;
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>[];
@@ -250,15 +222,16 @@ 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/", { return (await this._request("DELETE", "/v1/memories/", { params })) as Record<
params, string,
})) as Record<string, unknown>; unknown
>;
} }
if (memoryId) { if (memoryId) {
return (await this._request( return (await this._request("DELETE", `/v1/memories/${memoryId}/`)) as Record<
"DELETE", string,
`/v1/memories/${memoryId}/`, unknown
)) as Record<string, unknown>; >;
} }
throw new Error("Either memoryId or --all is required"); throw new Error("Either memoryId or --all is required");
} }
@@ -272,9 +245,10 @@ 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/", { return (await this._request("DELETE", "/v1/entities/", { params })) as Record<
params, string,
})) as Record<string, unknown>; unknown
>;
} }
async status( async status(
@@ -283,19 +257,13 @@ 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> = { const statusParams: Record<string, string> = { page: "1", page_size: "1" };
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/", { await this._request("POST", "/v2/memories/", { json: payload, params: statusParams });
json: payload,
params: statusParams,
});
} else { } else {
await this._request("GET", "/v1/entities/"); await this._request("GET", "/v1/entities/");
} }
@@ -315,10 +283,7 @@ 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< items = ((result as Record<string, unknown>).results ?? []) as Record<string, unknown>[];
string,
unknown
>[];
} }
const typeMap: Record<string, string> = { const typeMap: Record<string, string> = {
+11 -15
View File
@@ -4,7 +4,10 @@
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"; import { createRequire } from "node:module";
const _require = createRequire(import.meta.url);
const PKG_VERSION: string = _require("../package.json").version;
export const LOGO = ` export const LOGO = `
███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗ ███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
@@ -45,7 +48,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${PKG_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
@@ -55,9 +58,7 @@ 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( const bottomBorder = brand(`╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`);
`╰${"─".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;
@@ -66,8 +67,7 @@ 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] = body[taglineIdx] = `${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`;
`${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);
@@ -79,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)}`);
} }
@@ -90,7 +90,7 @@ export function printWarning(message: string): void {
} }
export function printInfo(message: string): void { export function printInfo(message: string): void {
console.error(`${brand(sym("◆", "*"))} ${message}`); console.log(`${brand(sym("◆", "*"))} ${message}`);
} }
export function printScope(ids: Record<string, string | undefined>): void { export function printScope(ids: Record<string, string | undefined>): void {
@@ -102,7 +102,7 @@ export function printScope(ids: Record<string, string | undefined>): void {
} }
} }
if (parts.length > 0) { if (parts.length > 0) {
console.error(` ${dim(`Scope: ${parts.join(", ")}`)}`); console.log(` ${dim(`Scope: ${parts.join(", ")}`)}`);
} }
} }
@@ -120,11 +120,7 @@ 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({ const spinner = ora({ text: dim(message), color: "magenta", stream: process.stderr }).start();
text: dim(message),
color: "magenta",
stream: process.stderr,
}).start();
const start = performance.now(); const start = performance.now();
try { try {
+2 -5
View File
@@ -3,7 +3,7 @@
*/ */
import Table from "cli-table3"; import Table from "cli-table3";
import { colors, printError, printSuccess } from "../branding.js"; import { printError, printSuccess, colors } from "../branding.js";
import { import {
getNestedValue, getNestedValue,
loadConfig, loadConfig,
@@ -48,10 +48,7 @@ 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([ table.push(["defaults.agent_id", config.defaults.agentId || dim("(not set)")]);
"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)]);
+6 -23
View File
@@ -4,14 +4,8 @@
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;
@@ -24,9 +18,7 @@ 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( printError(`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`);
`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`,
);
process.exit(1); process.exit(1);
} }
@@ -68,9 +60,7 @@ export async function cmdEntitiesList(
console.log(); console.log();
console.log(table.toString()); console.log(table.toString());
console.log( console.log(` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`);
` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`,
);
console.log(); console.log();
} }
@@ -87,9 +77,7 @@ 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( printError("Provide at least one of --user-id, --agent-id, --app-id, --run-id.");
"Provide at least one of --user-id, --agent-id, --app-id, --run-id.",
);
process.exit(1); process.exit(1);
} }
@@ -99,9 +87,7 @@ 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( printInfo(`Would delete entity ${scopeParts.join(", ")} and all its memories.`);
`Would delete entity ${scopeParts.join(", ")} and all its memories.`,
);
printInfo("No changes made."); printInfo("No changes made.");
return; return;
} }
@@ -114,10 +100,7 @@ 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({ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
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] `,
+7 -208
View File
@@ -3,108 +3,18 @@
*/ */
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 { import { type Mem0Config, createDefaultConfig, saveConfig } from "../config.js";
DEFAULT_BASE_URL, import { PlatformBackend } from "../backend/platform.js";
type Mem0Config,
createDefaultConfig,
loadConfig,
saveConfig,
} from "../config.js";
const { brand, dim } = colors; const { brand, dim } = colors;
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
function validateEmail(email: string): void {
if (!EMAIL_RE.test(email)) {
printError(`Invalid email address: ${JSON.stringify(email)}`);
process.exit(1);
}
}
async function emailLogin(
email: string,
code: string | undefined,
baseUrl: string,
): Promise<Record<string, unknown>> {
const url = baseUrl.replace(/\/+$/, "");
let codeValue = code;
if (!codeValue) {
const resp = await fetch(`${url}/api/v1/auth/email_code/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(30_000),
});
if (resp.status === 429) {
printError("Too many attempts. Try again in a few minutes.");
process.exit(1);
}
if (!resp.ok) {
let detail: string;
try {
const body = (await resp.json()) as Record<string, unknown>;
detail = (body.error ?? body.detail ?? resp.statusText) as string;
} catch {
detail = resp.statusText;
}
printError(`Failed to send code: ${detail}`);
process.exit(1);
}
printSuccess("Verification code sent! Check your email.");
if (!process.stdin.isTTY) {
printError(
"No --code provided and terminal is non-interactive.",
"Run: mem0 init --email <email> --code <code>",
);
process.exit(1);
}
console.log();
const entered = await promptLine(` ${brand("Verification Code")}`);
if (!entered) {
printError("Code is required.");
process.exit(1);
}
codeValue = entered;
}
const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, code: codeValue.trim() }),
signal: AbortSignal.timeout(30_000),
});
if (verifyResp.status === 429) {
printError("Too many attempts. Try again in a few minutes.");
process.exit(1);
}
if (!verifyResp.ok) {
let detail: string;
try {
const body = (await verifyResp.json()) as Record<string, unknown>;
detail = (body.error ?? body.detail ?? verifyResp.statusText) as string;
} catch {
detail = verifyResp.statusText;
}
printError(`Verification failed: ${detail}`);
process.exit(1);
}
return verifyResp.json() as Promise<Record<string, unknown>>;
}
function promptSecret(label: string): Promise<string> { function promptSecret(label: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
process.stdout.write(label); process.stdout.write(label);
@@ -160,10 +70,7 @@ 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({ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
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) => {
@@ -175,9 +82,7 @@ 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( console.log(` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys")}`);
` ${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")}: `);
@@ -193,10 +98,7 @@ 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( const userId = await promptLine(` ${brand("Default User ID")} ${dim("(recommended)")}`, "mem0-cli");
` ${brand("Default User ID")} ${dim("(recommended)")}`,
"mem0-cli",
);
if (userId) config.defaults.userId = userId; if (userId) config.defaults.userId = userId;
} }
@@ -222,66 +124,8 @@ async function validatePlatform(config: Mem0Config): Promise<void> {
} }
} }
export async function runInit( export async function runInit(opts: { apiKey?: string; userId?: string } = {}): Promise<void> {
opts: {
apiKey?: string;
userId?: string;
email?: string;
code?: string;
} = {},
): Promise<void> {
const config = createDefaultConfig(); const config = createDefaultConfig();
const savedConfig = loadConfig();
const baseUrl =
process.env.MEM0_BASE_URL ||
savedConfig.platform.baseUrl ||
DEFAULT_BASE_URL;
// Guards
if (opts.code && !opts.email) {
printError("--code requires --email.");
process.exit(1);
}
if (opts.email && opts.apiKey) {
printError("Cannot use both --api-key and --email.");
process.exit(1);
}
// ── Email login flow ──────────────────────────────────────────────────────
if (opts.email) {
const email = opts.email.trim().toLowerCase();
validateEmail(email);
printBanner();
console.log();
printInfo(`Logging in as ${email}...\n`);
const result = await emailLogin(email, opts.code, baseUrl);
const apiKeyVal = result.api_key as string | undefined;
if (!apiKeyVal) {
printError(
"Auth succeeded but no API key was returned. Contact support.",
);
process.exit(1);
}
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli";
saveConfig(config);
console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
console.log();
return;
}
// ── API key flow ──────────────────────────────────────────────────────────
// Non-interactive: both flags provided // Non-interactive: both flags provided
if (opts.apiKey && opts.userId) { if (opts.apiKey && opts.userId) {
@@ -310,51 +154,6 @@ export async function runInit(
if (opts.apiKey) { if (opts.apiKey) {
config.platform.apiKey = opts.apiKey; config.platform.apiKey = opts.apiKey;
} else { } else {
console.log(` ${brand("How would you like to authenticate?")}`);
console.log(` ${dim("1.")} Login with email ${dim("(recommended)")}`);
console.log(` ${dim("2.")} Enter API key manually`);
console.log();
const choice = await promptLine(` ${brand("Choose")} [1/2]`, "1");
if (choice === "1") {
console.log();
const emailAddr = await promptLine(` ${brand("Email")}`);
if (!emailAddr) {
printError("Email is required.");
process.exit(1);
}
const email = emailAddr.trim().toLowerCase();
validateEmail(email);
printInfo(`Logging in as ${email}...\n`);
const result = await emailLogin(email, undefined, baseUrl);
const apiKeyVal = result.api_key as string | undefined;
if (!apiKeyVal) {
printError(
"Auth succeeded but no API key was returned. Contact support.",
);
process.exit(1);
}
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli";
saveConfig(config);
console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
console.log();
return;
}
// choice === "2": fall through to API key prompt
await setupPlatform(config); await setupPlatform(config);
} }
+24 -137
View File
@@ -3,18 +3,11 @@
*/ */
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,
formatJsonEnvelope,
formatMemoriesTable, formatMemoriesTable,
formatMemoriesText, formatMemoriesText,
formatSingleMemory, formatSingleMemory,
@@ -58,9 +51,7 @@ export async function cmdAdd(
try { try {
msgs = JSON.parse(opts.messages); msgs = JSON.parse(opts.messages);
} catch (e) { } catch (e) {
printError( printError(`Invalid JSON in --messages: ${e instanceof Error ? e.message : e}`);
`Invalid JSON in --messages: ${e instanceof Error ? e.message : e}`,
);
process.exit(1); process.exit(1);
} }
} }
@@ -69,31 +60,11 @@ export async function cmdAdd(
content = fs.readFileSync(0, "utf-8").trim(); content = fs.readFileSync(0, "utf-8").trim();
} }
if (content !== undefined && content.trim() === "") {
printError("Content cannot be empty.");
process.exit(1);
}
if (!content && !msgs) { if (!content && !msgs) {
printError( printError("No content provided. Pass text, --messages, --file, or pipe via stdin.");
"No content provided. Pass text, --messages, --file, or pipe via stdin.",
);
process.exit(1); process.exit(1);
} }
// Validate --expires
if (opts.expires) {
if (!/^\d{4}-\d{2}-\d{2}$/.test(opts.expires)) {
printError(
"Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).",
);
process.exit(1);
}
if (new Date(opts.expires) <= new Date()) {
printError("--expires date must be in the future.");
process.exit(1);
}
}
let meta: Record<string, unknown> | undefined; let meta: Record<string, unknown> | undefined;
if (opts.metadata) { if (opts.metadata) {
try { try {
@@ -142,19 +113,10 @@ export async function cmdAdd(
} }
console.log(); console.log();
printScope({ printScope({ user_id: opts.userId, agent_id: opts.agentId, app_id: opts.appId, run_id: opts.runId });
user_id: opts.userId, const results = Array.isArray(result) ? result : ((result.results as unknown[]) ?? [result]);
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( printSuccess(`Memory processed — ${count} memor${count === 1 ? "y" : "ies"} extracted`);
`Memory processed — ${count} memor${count === 1 ? "y" : "ies"} extracted`,
);
formatAddResult(result, opts.output); formatAddResult(result, opts.output);
} }
@@ -191,24 +153,12 @@ export async function cmdSearch(
} }
} }
const fieldList = opts.fields const fieldList = opts.fields ? opts.fields.split(",").map((f) => f.trim()) : undefined;
? opts.fields.split(",").map((f) => f.trim())
: undefined;
if (opts.topK < 1) {
printError("--top-k must be >= 1.");
process.exit(1);
}
if (opts.threshold < 0 || opts.threshold > 1) {
printError("--threshold must be between 0.0 and 1.0.");
process.exit(1);
}
const start = performance.now(); const start = performance.now();
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,
@@ -229,18 +179,12 @@ export async function cmdSearch(
} }
const elapsed = (performance.now() - start) / 1000; const elapsed = (performance.now() - start) / 1000;
if (opts.output === "quiet") return;
if (opts.output === "json") { if (opts.output === "json") {
formatJson(results); formatJson(results);
} else if (opts.output === "table") { } else if (opts.output === "table") {
if (results.length > 0) { if (results.length > 0) {
formatMemoriesTable(results, { showScore: true }); formatMemoriesTable(results);
printResultSummary({ printResultSummary({ count: results.length, durationSecs: elapsed, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } });
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.");
@@ -249,11 +193,7 @@ export async function cmdSearch(
} else { } else {
if (results.length > 0) { if (results.length > 0) {
formatMemoriesText(results); formatMemoriesText(results);
printResultSummary({ printResultSummary({ count: results.length, durationSecs: elapsed, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } });
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.");
@@ -296,15 +236,6 @@ export async function cmdList(
output: string; output: string;
}, },
): Promise<void> { ): Promise<void> {
if (opts.pageSize < 1) {
printError("--page-size must be >= 1.");
process.exit(1);
}
if (opts.page < 1) {
printError("--page must be >= 1.");
process.exit(1);
}
const start = performance.now(); const start = performance.now();
let results: Record<string, unknown>[]; let results: Record<string, unknown>[];
try { try {
@@ -328,24 +259,12 @@ export async function cmdList(
} }
const elapsed = (performance.now() - start) / 1000; const elapsed = (performance.now() - start) / 1000;
if (opts.output === "quiet") return;
if (opts.output === "json") { if (opts.output === "json") {
formatJsonEnvelope({ formatJson(results);
command: "list",
data: results,
count: results.length,
scope: { user_id: opts.userId, agent_id: opts.agentId },
});
} else if (opts.output === "table") { } else if (opts.output === "table") {
if (results.length > 0) { if (results.length > 0) {
formatMemoriesTable(results); formatMemoriesTable(results);
printResultSummary({ printResultSummary({ count: results.length, durationSecs: elapsed, page: opts.page, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } });
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.");
@@ -354,12 +273,7 @@ export async function cmdList(
} else { } else {
if (results.length > 0) { if (results.length > 0) {
formatMemoriesText(results, "memories"); formatMemoriesText(results, "memories");
printResultSummary({ printResultSummary({ count: results.length, durationSecs: elapsed, page: opts.page, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } });
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.");
@@ -399,9 +313,7 @@ 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( printSuccess(`Memory ${memoryId.slice(0, 8)} updated (${elapsed.toFixed(2)}s)`);
`Memory ${memoryId.slice(0, 8)} updated (${elapsed.toFixed(2)}s)`,
);
} }
} }
@@ -439,9 +351,7 @@ 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( printSuccess(`Memory ${memoryId.slice(0, 8)} deleted (${elapsed.toFixed(2)}s)`);
`Memory ${memoryId.slice(0, 8)} deleted (${elapsed.toFixed(2)}s)`,
);
} }
} }
@@ -462,24 +372,15 @@ 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("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({ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
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 ALL memories across the ENTIRE project? This cannot be undone. [y/N] `, resolve);
"\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") {
@@ -491,9 +392,7 @@ 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( result = await timedStatus("Deleting all memories project-wide...", async () => {
"Deleting all memories project-wide...",
async () => {
return backend.delete(undefined, { return backend.delete(undefined, {
all: true, all: true,
userId: "*", userId: "*",
@@ -501,8 +400,7 @@ 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);
@@ -513,9 +411,7 @@ 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( printInfo("Deletion started. Memories will be removed in the background.");
"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)`);
} }
@@ -547,19 +443,12 @@ 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 = const scope = scopeParts.length > 0 ? scopeParts.join(", ") : "ALL entities";
scopeParts.length > 0 ? scopeParts.join(", ") : "ALL entities";
const readline = await import("node:readline"); const readline = await import("node:readline");
const rl = readline.createInterface({ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
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 ALL memories for ${scope}? This cannot be undone. [y/N] `, resolve);
`\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") {
@@ -590,9 +479,7 @@ 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( printInfo("Deletion started. Memories will be removed in the background.");
"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)`);
} }
+10 -16
View File
@@ -3,14 +3,17 @@
*/ */
import fs from "node:fs"; import fs from "node:fs";
import boxen from "boxen"; import { createRequire } from "node:module";
import { printError, printSuccess, timedStatus, colors } from "../branding.js";
import type { Backend } from "../backend/base.js"; import type { Backend } from "../backend/base.js";
import { colors, printError, printSuccess, timedStatus } from "../branding.js";
import { formatJsonEnvelope } from "../output.js"; import { formatJsonEnvelope } from "../output.js";
import { CLI_VERSION } from "../version.js"; import boxen from "boxen";
const { brand, dim, success, error: errorColor } = colors; const { brand, dim, success, error: errorColor } = colors;
const _require = createRequire(import.meta.url);
const VERSION: string = _require("../../package.json").version;
export async function cmdStatus( export async function cmdStatus(
backend: Backend, backend: Backend,
opts: { userId?: string; agentId?: string; output?: string } = {}, opts: { userId?: string; agentId?: string; output?: string } = {},
@@ -22,10 +25,7 @@ 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 = { result = { connected: false, error: e instanceof Error ? e.message : String(e) };
connected: false,
error: e instanceof Error ? e.message : String(e),
};
} }
const elapsed = (performance.now() - start) / 1000; const elapsed = (performance.now() - start) / 1000;
@@ -73,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${VERSION}`);
} }
export async function cmdImport( export async function cmdImport(
@@ -116,9 +116,7 @@ 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( process.stdout.write(`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`);
`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`,
);
} }
} }
@@ -128,11 +126,7 @@ export async function cmdImport(
if (opts.output === "json") { if (opts.output === "json") {
formatJsonEnvelope({ formatJsonEnvelope({
command: "import", command: "import",
data: { data: { added, failed, duration_s: parseFloat(elapsed.toFixed(2)) },
added,
failed,
duration_s: Number.parseFloat(elapsed.toFixed(2)),
},
durationMs: Math.round(elapsed * 1000), durationMs: Math.round(elapsed * 1000),
}); });
return; return;
+8 -24
View File
@@ -80,14 +80,10 @@ export function loadConfig(): Mem0Config {
} }
// Environment variable overrides // Environment variable overrides
if (process.env.MEM0_API_KEY) if (process.env.MEM0_API_KEY) config.platform.apiKey = process.env.MEM0_API_KEY;
config.platform.apiKey = process.env.MEM0_API_KEY; if (process.env.MEM0_BASE_URL) config.platform.baseUrl = process.env.MEM0_BASE_URL;
if (process.env.MEM0_BASE_URL) if (process.env.MEM0_USER_ID) config.defaults.userId = process.env.MEM0_USER_ID;
config.platform.baseUrl = process.env.MEM0_BASE_URL; if (process.env.MEM0_AGENT_ID) config.defaults.agentId = process.env.MEM0_AGENT_ID;
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) {
@@ -123,8 +119,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. */
@@ -136,14 +132,6 @@ const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
"defaults.app_id": ["defaults", "appId"], "defaults.app_id": ["defaults", "appId"],
"defaults.run_id": ["defaults", "runId"], "defaults.run_id": ["defaults", "runId"],
"defaults.enable_graph": ["defaults", "enableGraph"], "defaults.enable_graph": ["defaults", "enableGraph"],
// Short-form aliases
api_key: ["platform", "apiKey"],
base_url: ["platform", "baseUrl"],
user_id: ["defaults", "userId"],
agent_id: ["defaults", "agentId"],
app_id: ["defaults", "appId"],
run_id: ["defaults", "runId"],
enable_graph: ["defaults", "enableGraph"],
}; };
export function getNestedValue(config: Mem0Config, dottedKey: string): unknown { export function getNestedValue(config: Mem0Config, dottedKey: string): unknown {
@@ -153,11 +141,7 @@ 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( export function setNestedValue(config: Mem0Config, dottedKey: string, value: string): boolean {
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;
@@ -167,7 +151,7 @@ export function setNestedValue(
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] = Number.parseInt(value, 10); obj[field] = parseInt(value, 10);
} else { } else {
obj[field] = value; obj[field] = value;
} }
-2
View File
@@ -1,2 +0,0 @@
/** Injected by tsup at build time from package.json version field. Undefined in dev/test. */
declare const __CLI_VERSION__: string | undefined;
+17 -21
View File
@@ -4,7 +4,7 @@
*/ */
import chalk from "chalk"; import chalk from "chalk";
import type { Argument, Command, Help, Option } from "commander"; import type { Command, Help, Option, Argument } 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 ───────────────────────────────────────────────────────────
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequence is intentional // eslint-disable-next-line no-control-regex
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,7 +141,11 @@ const PANEL_ORDER: string[] = [
* ╰────────────────────────────────╯ * ╰────────────────────────────────╯
* ``` * ```
*/ */
function renderPanel(title: string, rows: string[], width: number): string { function renderPanel(
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
@@ -157,14 +161,13 @@ function renderPanel(title: string, rows: string[], width: number): string {
dimBorder("╮"); dimBorder("╮");
// Bottom border: ╰─...─╯ // Bottom border: ╰─...─╯
const bottomLine = const bottomLine = dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯");
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");
@@ -182,7 +185,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;
@@ -217,9 +220,7 @@ 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( lines.push(` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`);
` ${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(" ");
@@ -234,9 +235,7 @@ export function richFormatHelp(cmd: Command, helper: Help): string {
argParts.push(part); argParts.push(part);
} }
} }
lines.push( lines.push(` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`);
` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`,
);
} }
lines.push(""); lines.push("");
@@ -261,9 +260,7 @@ 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( const maxLen = Math.max(...visibleArgs.map((a: Argument) => a.name().length));
...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);
@@ -277,8 +274,7 @@ 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 = const panelMap = (!isRoot && OPTION_PANELS[cmdName]) ? OPTION_PANELS[cmdName] : {};
!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) {
@@ -291,7 +287,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);
} }
} }
@@ -300,8 +296,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);
} }
+44 -174
View File
@@ -7,13 +7,16 @@
import fs from "node:fs"; 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 { createRequire } from "node:module";
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 _require = createRequire(import.meta.url);
const VERSION: string = _require("../package.json").version;
const program = new Command(); const program = new Command();
@@ -59,12 +62,7 @@ function resolveIds(
runId?: string; runId?: string;
}, },
): { userId?: string; agentId?: string; appId?: string; runId?: string } { ): { userId?: string; agentId?: string; appId?: string; runId?: string } {
const hasExplicit = !!( const hasExplicit = !!(opts.userId || opts.agentId || opts.appId || opts.runId);
opts.userId ||
opts.agentId ||
opts.appId ||
opts.runId
);
if (hasExplicit) { if (hasExplicit) {
return { return {
userId: opts.userId || undefined, userId: opts.userId || undefined,
@@ -97,12 +95,10 @@ function resolveGraph(
program program
.name("mem0") .name("mem0")
.description( .description(`◆ Mem0 CLI v${VERSION} · Node.js SDK\n\nThe Memory Layer for AI Agents`)
`◆ 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${VERSION}`);
process.exit(0); process.exit(0);
}) })
.usage("<command> [options]") .usage("<command> [options]")
@@ -114,28 +110,13 @@ program
program program
.command("init") .command("init")
.description( .description("Interactive setup wizard for mem0 CLI.")
"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.") .addHelpText("after", "\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice")
.option(
"--code <code>",
"Verification code (use with --email for non-interactive login).",
)
.addHelpText(
"after",
"\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice\n $ mem0 init --email you@example.com\n $ mem0 init --email you@example.com --code 123456",
)
.action(async (opts) => { .action(async (opts) => {
const { runInit } = await import("./commands/init.js"); const { runInit } = await import("./commands/init.js");
await runInit({ await runInit({ apiKey: opts.apiKey, userId: opts.userId });
apiKey: opts.apiKey,
userId: opts.userId,
email: opts.email,
code: opts.code,
});
}); });
// ── Memory: add ─────────────────────────────────────────────────────────── // ── Memory: add ───────────────────────────────────────────────────────────
@@ -159,10 +140,7 @@ 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( .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')
"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);
@@ -180,18 +158,8 @@ 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( .option("-k, --top-k <n>", "Number of results.", (v) => parseInt(v), 10)
"-k, --top-k <n>", .option("--threshold <n>", "Minimum similarity score.", (v) => parseFloat(v), 0.3)
"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).")
@@ -201,10 +169,7 @@ 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( .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')
"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) {
@@ -239,10 +204,7 @@ 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( .addHelpText("after", "\nExamples:\n $ mem0 get abc-123-def-456\n $ mem0 get abc-123-def-456 -o json")
"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);
@@ -258,13 +220,8 @@ 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) => Number.parseInt(v), 1) .option("--page <n>", "Page number.", (v) => parseInt(v), 1)
.option( .option("--page-size <n>", "Results per page.", (v) => parseInt(v), 100)
"--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).")
@@ -273,10 +230,7 @@ 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( .addHelpText("after", "\nExamples:\n $ mem0 list -u alice\n $ mem0 list --category prefs --after 2024-01-01 -o json")
"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);
@@ -303,10 +257,7 @@ 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( .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`)
"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) {
@@ -314,10 +265,7 @@ 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, { await cmdUpdate(backend, memoryId, resolvedText, { metadata: opts.metadata, output: opts.output });
metadata: opts.metadata,
output: opts.output,
});
}); });
// ── Memory: delete (consolidated) ───────────────────────────────────────── // ── Memory: delete (consolidated) ─────────────────────────────────────────
@@ -326,11 +274,7 @@ 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( .option("--entity", "Delete the entity itself and all its memories (cascade).", false)
"--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)
@@ -341,16 +285,13 @@ 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( .addHelpText("after", [
"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) {
@@ -358,9 +299,7 @@ program
process.exit(1); process.exit(1);
} }
if (memoryId && opts.entity) { if (memoryId && opts.entity) {
printError( printError("Cannot combine <memoryId> with --entity. Use one or the other.");
"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) {
@@ -381,36 +320,18 @@ 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, { await cmdDelete(backend, memoryId, { output: opts.output, dryRun: opts.dryRun, force: opts.force });
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( const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
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, { await cmdDeleteAll(backend, { force: opts.force, dryRun: opts.dryRun, all: opts.project, ...ids, output: opts.output });
force: opts.force,
dryRun: opts.dryRun,
all: opts.project,
...ids,
output: opts.output,
});
return; return;
} }
@@ -434,10 +355,7 @@ 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( .addHelpText("after", "\nExamples:\n $ mem0 config show\n $ mem0 config show -o json")
"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 });
@@ -446,10 +364,7 @@ configCmd
configCmd configCmd
.command("get <key>") .command("get <key>")
.description("Get a configuration value.") .description("Get a configuration value.")
.addHelpText( .addHelpText("after", "\nExamples:\n $ mem0 config get platform.api_key\n $ mem0 config get defaults.user_id")
"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);
@@ -458,10 +373,7 @@ configCmd
configCmd configCmd
.command("set <key> <value>") .command("set <key> <value>")
.description("Set a configuration value.") .description("Set a configuration value.")
.addHelpText( .addHelpText("after", "\nExamples:\n $ mem0 config set defaults.user_id alice\n $ mem0 config set platform.base_url https://api.mem0.ai")
"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);
@@ -481,10 +393,7 @@ 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( .addHelpText("after", "\nExamples:\n $ mem0 entity list users\n $ mem0 entity list agents -o json")
"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);
@@ -503,10 +412,7 @@ 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( .addHelpText("after", "\nExamples:\n $ mem0 entity delete --user-id alice --force\n $ mem0 entity delete --user-id alice --dry-run")
"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);
@@ -532,6 +438,7 @@ program
}); });
}); });
program program
.command("import <filePath>") .command("import <filePath>")
.description("Import memories from a JSON file.") .description("Import memories from a JSON file.")
@@ -540,28 +447,19 @@ 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( .addHelpText("after", "\nExamples:\n $ mem0 import data.json --user-id alice\n $ mem0 import data.json -u alice -o json")
"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, { await cmdImport(backend, filePath, { userId: ids.userId, agentId: ids.agentId, output: opts.output });
userId: ids.userId,
agentId: ids.agentId,
output: opts.output,
});
}); });
// ── Help (machine-readable) ────────────────────────────────────────────── // ── Help (machine-readable) ──────────────────────────────────────────────
program program
.command("help") .command("help")
.description( .description("Show help. Use --json for machine-readable output (for LLM agents).")
"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) => {
@@ -573,35 +471,19 @@ 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( console.log(JSON.stringify({ name: "mem0", version: VERSION, description: "The Memory Layer for AI Agents" }, null, 2));
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( console.log(`${b("◆ Mem0 CLI")} v${VERSION} · Node.js SDK\n The Memory Layer for AI Agents\n`);
`${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( console.log(" add Add a memory from text, messages, file, or stdin");
" 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( console.log(" delete Delete a memory, all memories, or an entity");
" 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)");
@@ -609,23 +491,11 @@ 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( console.log(" mem0 help --json Machine-readable help (for LLM agents)");
" mem0 help --json Machine-readable help (for LLM agents)",
);
console.log(); console.log();
} }
}); });
// ── Version ───────────────────────────────────────────────────────────────
program
.command("version")
.description("Show version.")
.action(async () => {
const { cmdVersion } = await import("./commands/utils.js");
cmdVersion();
});
// ── Entrypoint ──────────────────────────────────────────────────────────── // ── Entrypoint ────────────────────────────────────────────────────────────
program.parse(); program.parse();
+11 -41
View File
@@ -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 boxen from "boxen";
import Table from "cli-table3"; import Table from "cli-table3";
import boxen from "boxen";
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,49 +52,28 @@ export function formatMemoriesText(
} }
} }
export function formatMemoriesTable( export function formatMemoriesTable(memories: Record<string, unknown>[]): void {
memories: Record<string, unknown>[],
opts: { showScore?: boolean } = {},
): void {
const head = opts.showScore
? [
accent("ID"),
accent("Score"),
accent("Memory"),
accent("Category"),
accent("Created"),
]
: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")];
const colWidths = opts.showScore ? [38, 8, 40, 16, 14] : [38, 40, 16, 14];
const table = new Table({ const table = new Table({
head, head: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")],
colWidths, colWidths: [12, 52, 16, 14],
wordWrap: true, wordWrap: true,
style: { head: [], border: [] }, style: { head: [], border: [] },
}); });
for (const mem of memories) { for (const mem of memories) {
const memId = (mem.id as string) ?? ""; const memId = ((mem.id as string) ?? "").slice(0, 8);
let memoryText = (mem.memory ?? mem.text ?? "") as string; let memoryText = (mem.memory ?? mem.text ?? "") as string;
if (memoryText.length > 60) { if (memoryText.length > 60) {
memoryText = `${memoryText.slice(0, 57)}...`; memoryText = memoryText.slice(0, 57) + "...";
} }
const categories = mem.categories; const categories = mem.categories;
const cat = const cat =
Array.isArray(categories) && categories.length > 0 Array.isArray(categories) && categories.length > 0
? categories.length > 1 ? (categories[0] as string)
? `${categories[0]} (+${categories.length - 1})`
: (categories[0] as string)
: "—"; : "—";
const created = formatDate(mem.created_at as string | undefined) ?? "—"; const created = formatDate(mem.created_at as string | undefined) ?? "—";
if (opts.showScore) {
const score = mem.score as number | undefined;
const scoreStr = score !== undefined ? score.toFixed(2) : "—";
table.push([dim(memId), scoreStr, memoryText, cat, created]);
} else {
table.push([dim(memId), memoryText, cat, created]); table.push([dim(memId), memoryText, cat, created]);
} }
}
console.log(); console.log();
console.log(table.toString()); console.log(table.toString());
@@ -130,9 +109,7 @@ 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) const catStr = Array.isArray(categories) ? categories.join(", ") : String(categories);
? categories.join(", ")
: String(categories);
lines.push(` ${dim("Categories:")} ${catStr}`); lines.push(` ${dim("Categories:")} ${catStr}`);
} }
@@ -174,10 +151,7 @@ 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 = [ const parts = [` ${icon} ${dim("Queued".padEnd(10))}`, "Processing in background"];
` ${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;
@@ -185,10 +159,7 @@ 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( const memId = ((r.id as string) ?? (r.memory_id as string) ?? "").slice(0, 8);
0,
8,
);
let icon: string; let icon: string;
let label: string; let label: string;
@@ -252,8 +223,7 @@ 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) if (opts.durationSecs !== undefined) parts.push(`${opts.durationSecs.toFixed(2)}s`);
parts.push(`${opts.durationSecs.toFixed(2)}s`);
console.log(` ${dim(parts.join(" · "))}`); console.log(` ${dim(parts.join(" · "))}`);
console.log(); console.log();
-10
View File
@@ -1,10 +0,0 @@
import { createRequire } from "node:module";
// __CLI_VERSION__ is replaced at build time by tsup (see tsup.config.ts).
// When running via tsx in dev/test mode, fall back to reading package.json.
// typeof is safe to use on undeclared identifiers — it returns 'undefined' without throwing.
export const CLI_VERSION: string =
typeof __CLI_VERSION__ !== "undefined"
? (__CLI_VERSION__ as string)
: (createRequire(import.meta.url)("../package.json") as { version: string })
.version;
+4 -4
View File
@@ -80,19 +80,19 @@ describe("printWarning", () => {
describe("printInfo", () => { describe("printInfo", () => {
it("prints info message", () => { it("prints info message", () => {
printInfo("Important note"); printInfo("Important note");
expect(errOutput).toContain("Important note"); expect(output).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(errOutput).toContain("alice"); expect(output).toContain("alice");
expect(errOutput).toContain("bot"); expect(output).toContain("bot");
}); });
it("prints nothing when no IDs", () => { it("prints nothing when no IDs", () => {
printScope({}); printScope({});
expect(errOutput).toBe(""); expect(output).toBe("");
}); });
}); });
+2 -2
View File
@@ -125,7 +125,7 @@ describe("cmdSearch", () => {
enableGraph: false, enableGraph: false,
output: "text", output: "text",
}); });
expect(errOutput).toContain("No memories found"); expect(output).toContain("No memories found");
}); });
}); });
@@ -166,7 +166,7 @@ describe("cmdList", () => {
enableGraph: false, enableGraph: false,
output: "text", output: "text",
}); });
expect(errOutput).toContain("No memories found"); expect(output).toContain("No memories found");
}); });
}); });
-15
View File
@@ -1,15 +0,0 @@
import { defineConfig } from 'tsup';
import { createRequire } from 'node:module';
const _require = createRequire(import.meta.url);
const pkg = _require('./package.json');
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
clean: true,
define: {
__CLI_VERSION__: JSON.stringify(pkg.version),
},
});
-11
View File
@@ -1,11 +0,0 @@
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),
},
});
+20 -49
View File
@@ -253,12 +253,8 @@ 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( graph: bool = typer.Option(False, "--graph", help="Enable 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"),
),
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"
), ),
@@ -285,8 +281,8 @@ def search(
# STEP 7: stdin fallback for query # STEP 7: stdin fallback for query
if query is None: if query is None:
query = _read_stdin() query = _read_stdin()
if not query or not query.strip(): if query is None:
print_error(err_console, "Search query cannot be empty.") print_error(err_console, "No query provided. Pass a query argument or pipe via stdin.")
raise typer.Exit(1) raise typer.Exit(1)
backend, config = _get_backend_and_config(api_key, base_url) backend, config = _get_backend_and_config(api_key, base_url)
@@ -376,12 +372,8 @@ 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( graph: bool = typer.Option(False, "--graph", help="Enable 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"),
),
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"
), ),
@@ -471,19 +463,11 @@ def update(
@app.command(rich_help_panel="Memory") @app.command(rich_help_panel="Memory")
def delete( def delete(
memory_id: str | None = typer.Argument( memory_id: str | None = typer.Argument(None, help="Memory ID to delete (omit when using --all or --entity)."),
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( entity: bool = typer.Option(False, "--entity", help="Delete the entity itself and all its memories (cascade)."),
False, "--entity", help="Delete the entity itself and all its memories (cascade)." project: bool = typer.Option(False, "--project", help="With --all: delete ALL memories project-wide."),
), dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting."),
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"
@@ -657,12 +641,14 @@ 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(None, "--app-id", help="App ID.", rich_help_panel="Scope"), app_id: str | None = typer.Option(
run_id: str | None = typer.Option(None, "--run-id", help="Run ID.", rich_help_panel="Scope"), None, "--app-id", help="App ID.", rich_help_panel="Scope"
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
dry_run: bool = typer.Option(
False, "--dry-run", help="Show what would be deleted without deleting."
), ),
run_id: str | None = typer.Option(
None, "--run-id", help="Run ID.", rich_help_panel="Scope"
),
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting."),
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"
), ),
@@ -708,25 +694,17 @@ 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( user_id: str | None = typer.Option(None, "--user-id", "-u", help="Default user ID (skip prompt)."),
None, "--user-id", "-u", help="Default user ID (skip prompt)."
),
email: str | None = typer.Option(None, "--email", help="Login via email verification code."),
code: str | None = typer.Option(
None, "--code", help="Verification code (use with --email for non-interactive login)."
),
) -> None: ) -> None:
"""Interactive setup wizard for mem0 CLI. """Interactive setup wizard for mem0 CLI.
Examples: Examples:
mem0 init mem0 init
mem0 init --api-key m0-xxx --user-id alice mem0 init --api-key m0-xxx --user-id alice
mem0 init --email alice@company.com
mem0 init --email alice@company.com --code 482901
""" """
from mem0_cli.commands.init_cmd import run_init from mem0_cli.commands.init_cmd import run_init
run_init(api_key=api_key, user_id=user_id, email=email, code=code) run_init(api_key=api_key, user_id=user_id)
# (entity_app registered at module level, below sub-group definitions) # (entity_app registered at module level, below sub-group definitions)
@@ -765,6 +743,7 @@ 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."),
@@ -1031,14 +1010,6 @@ def help(
console.print() console.print()
@app.command(rich_help_panel="Utility")
def version() -> None:
"""Show version and exit."""
from mem0_cli.commands.utils import cmd_version
cmd_version()
# Register config subgroup here so it appears after help in Management panel # Register config subgroup here so it appears after help in Management panel
app.add_typer(config_app, name="config", rich_help_panel="Management") app.add_typer(config_app, name="config", rich_help_panel="Management")
+1
View File
@@ -105,6 +105,7 @@ 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,6 +312,7 @@ class PlatformBackend(Backend):
return items return items
class AuthError(Exception): class AuthError(Exception):
pass pass
+2 -6
View File
@@ -28,9 +28,7 @@ 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( print_error(err_console, f"Invalid entity type: {entity_type}. Use: {', '.join(valid_types)}")
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()
@@ -78,9 +76,7 @@ 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( print_error(err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id.")
err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id."
)
raise typer.Exit(1) raise typer.Exit(1)
if dry_run: if dry_run:
+3 -172
View File
@@ -2,11 +2,8 @@
from __future__ import annotations from __future__ import annotations
import os
import re
import sys import sys
import httpx
import typer import typer
from rich.console import Console from rich.console import Console
from rich.prompt import Prompt from rich.prompt import Prompt
@@ -19,7 +16,7 @@ from mem0_cli.branding import (
print_info, print_info,
print_success, print_success,
) )
from mem0_cli.config import DEFAULT_BASE_URL, Mem0Config, save_config from mem0_cli.config import Mem0Config, save_config
console = Console() console = Console()
err_console = Console(stderr=True) err_console = Console(stderr=True)
@@ -87,89 +84,7 @@ def _prompt_secret(label: str) -> str:
return "".join(chars) return "".join(chars)
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") def run_init(*, api_key: str | None = None, user_id: str | None = None) -> None:
def _validate_email(email: str) -> None:
"""Exit with an error if *email* doesn't look like a valid address."""
if not _EMAIL_RE.match(email):
print_error(err_console, f"Invalid email address: {email!r}")
raise typer.Exit(1)
def _email_login(
email: str,
code: str | None,
base_url: str,
) -> dict:
"""Run the email verification code login flow.
Returns the parsed JSON response from the verify endpoint.
The caller expects at minimum an ``api_key`` field.
"""
url = base_url.rstrip("/")
with httpx.Client(timeout=30.0) as client:
# If code is already provided, skip sending — user already has a code
if not code:
# Step 1: Request verification code
resp = client.post(
f"{url}/api/v1/auth/email_code/",
json={"email": email},
)
if resp.status_code == 429:
print_error(err_console, "Too many attempts. Try again in a few minutes.")
raise typer.Exit(1)
if resp.status_code != 200:
try:
detail = resp.json().get("error", resp.text)
except Exception:
detail = resp.text
print_error(err_console, f"Failed to send code: {detail}")
raise typer.Exit(1)
print_success(console, "Verification code sent! Check your email.")
# Step 2: Get code from user
if not sys.stdin.isatty():
print_error(
err_console,
"No --code provided and terminal is non-interactive.",
hint="Run: mem0 init --email <email> --code <code>",
)
raise typer.Exit(1)
console.print()
code = Prompt.ask(f" [{BRAND_COLOR}]Verification Code[/]")
if not code:
print_error(err_console, "Code is required.")
raise typer.Exit(1)
# Step 3: Verify code
resp = client.post(
f"{url}/api/v1/auth/email_code/verify/",
json={"email": email, "code": code.strip()},
)
if resp.status_code == 429:
print_error(err_console, "Too many attempts. Try again in a few minutes.")
raise typer.Exit(1)
if resp.status_code != 200:
try:
detail = resp.json().get("error", resp.text)
except Exception:
detail = resp.text
print_error(err_console, f"Verification failed: {detail}")
raise typer.Exit(1)
return resp.json()
def run_init(
*,
api_key: str | None = None,
user_id: str | None = None,
email: str | None = None,
code: str | None = None,
) -> None:
"""Interactive setup wizard for mem0 CLI. """Interactive setup wizard for mem0 CLI.
When both *api_key* and *user_id* are supplied, all prompts are skipped When both *api_key* and *user_id* are supplied, all prompts are skipped
@@ -178,48 +93,6 @@ def run_init(
""" """
config = Mem0Config() config = Mem0Config()
base_url = os.environ.get("MEM0_BASE_URL", config.platform.base_url or DEFAULT_BASE_URL)
if code and not email:
print_error(err_console, "--code requires --email.")
raise typer.Exit(1)
# ── Email login flow ──────────────────────────────────────────────
if email:
if api_key:
print_error(err_console, "Cannot use both --api-key and --email.")
raise typer.Exit(1)
email = email.strip().lower()
_validate_email(email)
print_banner(console)
console.print()
print_info(console, f"Logging in as {email}...\n")
result = _email_login(email, code, base_url)
api_key_val = result.get("api_key")
if not api_key_val:
print_error(err_console, "Auth succeeded but no API key was returned. Contact support.")
raise typer.Exit(1)
config.platform.api_key = api_key_val
config.platform.base_url = base_url
config.defaults.user_id = user_id or "mem0-cli"
save_config(config)
console.print()
print_success(console, "Authenticated! Configuration saved to ~/.mem0/config.json")
console.print()
console.print(f" [{DIM_COLOR}]Get started:[/]")
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode"[/]')
console.print(f' [{DIM_COLOR}] mem0 search "preferences"[/]')
console.print()
return
# ── API key flow (existing) ───────────────────────────────────────
# Fully non-interactive when both flags provided # Fully non-interactive when both flags provided
if api_key and user_id: if api_key and user_id:
config.platform.api_key = api_key config.platform.api_key = api_key
@@ -242,49 +115,7 @@ def run_init(
console.print() console.print()
print_info(console, "Welcome! Let's set up your mem0 CLI.\n") print_info(console, "Welcome! Let's set up your mem0 CLI.\n")
# If no flags at all, ask user how they want to authenticate # Use provided flags or prompt
if not api_key:
console.print(f" [{BRAND_COLOR}]How would you like to authenticate?[/]")
console.print(f" [{DIM_COLOR}]1.[/] Login with email [{DIM_COLOR}](recommended)[/]")
console.print(f" [{DIM_COLOR}]2.[/] Enter API key manually")
console.print()
choice = Prompt.ask(f" [{BRAND_COLOR}]Choose[/]", choices=["1", "2"], default="1")
if choice == "1":
console.print()
email_addr = Prompt.ask(f" [{BRAND_COLOR}]Email[/]")
if not email_addr:
print_error(err_console, "Email is required.")
raise typer.Exit(1)
email_addr = email_addr.strip().lower()
_validate_email(email_addr)
print_info(console, f"Logging in as {email_addr}...\n")
result = _email_login(email_addr, None, base_url)
api_key_val = result.get("api_key")
if not api_key_val:
print_error(
err_console, "Auth succeeded but no API key was returned. Contact support."
)
raise typer.Exit(1)
config.platform.api_key = api_key_val
config.platform.base_url = base_url
config.defaults.user_id = user_id or "mem0-cli"
save_config(config)
console.print()
print_success(console, "Authenticated! Configuration saved to ~/.mem0/config.json")
console.print()
console.print(f" [{DIM_COLOR}]Get started:[/]")
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode"[/]')
console.print(f' [{DIM_COLOR}] mem0 search "preferences"[/]')
console.print()
return
# API key flow
if api_key: if api_key:
config.platform.api_key = api_key config.platform.api_key = api_key
else: else:
+3 -46
View File
@@ -95,21 +95,6 @@ def cmd_add(
except json.JSONDecodeError: except json.JSONDecodeError:
cats = [c.strip() for c in categories.split(",")] cats = [c.strip() for c in categories.split(",")]
# Validate --expires
if expires:
import re
if not re.match(r"^\d{4}-\d{2}-\d{2}$", expires):
print_error(
err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31)."
)
raise typer.Exit(1)
from datetime import date
if date.fromisoformat(expires) <= date.today():
print_error(err_console, "--expires date must be in the future.")
raise typer.Exit(1)
with timed_status(err_console, "Adding memory...") as ts: with timed_status(err_console, "Adding memory...") as ts:
try: try:
result = backend.add( result = backend.add(
@@ -128,6 +113,7 @@ def cmd_add(
) )
except Exception as e: except Exception as e:
ts.error_msg = str(e) ts.error_msg = str(e)
print_error(err_console, str(e))
raise typer.Exit(1) from None raise typer.Exit(1) from None
if output == "quiet": if output == "quiet":
@@ -178,13 +164,6 @@ def cmd_search(
if fields: if fields:
field_list = [f.strip() for f in fields.split(",")] field_list = [f.strip() for f in fields.split(",")]
if top_k < 1:
print_error(err_console, "--top-k must be >= 1.")
raise typer.Exit(1)
if not (0.0 <= threshold <= 1.0):
print_error(err_console, "--threshold must be between 0.0 and 1.0.")
raise typer.Exit(1)
_start = _time.perf_counter() _start = _time.perf_counter()
with timed_status(err_console, "Searching memories...") as _ts: with timed_status(err_console, "Searching memories...") as _ts:
try: try:
@@ -207,14 +186,11 @@ def cmd_search(
raise typer.Exit(1) from None raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start _elapsed = _time.perf_counter() - _start
if output == "quiet":
return
if output == "json": if output == "json":
format_json(console, results) format_json(console, results)
elif output == "table": elif output == "table":
if results: if results:
format_memories_table(console, results, show_score=True) format_memories_table(console, results)
print_result_summary( print_result_summary(
console, len(results), duration_secs=_elapsed, user_id=user_id, agent_id=agent_id console, len(results), duration_secs=_elapsed, user_id=user_id, agent_id=agent_id
) )
@@ -262,13 +238,6 @@ def cmd_list(
output: str = "table", output: str = "table",
) -> None: ) -> None:
"""List memories.""" """List memories."""
if page_size < 1:
print_error(err_console, "--page-size must be >= 1.")
raise typer.Exit(1)
if page < 1:
print_error(err_console, "--page must be >= 1.")
raise typer.Exit(1)
_start = _time.perf_counter() _start = _time.perf_counter()
with timed_status(err_console, "Listing memories...") as _ts: with timed_status(err_console, "Listing memories...") as _ts:
try: try:
@@ -289,19 +258,8 @@ def cmd_list(
raise typer.Exit(1) from None raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start _elapsed = _time.perf_counter() - _start
if output == "quiet":
return
if output == "json": if output == "json":
from mem0_cli.output import format_json_envelope format_json(console, results)
format_json_envelope(
console,
command="list",
data=results,
count=len(results),
scope={k: v for k, v in {"user_id": user_id, "agent_id": agent_id}.items() if v},
)
elif output == "table": elif output == "table":
if results: if results:
format_memories_table(console, results) format_memories_table(console, results)
@@ -418,7 +376,6 @@ def cmd_delete_all(
# Project-wide wipe using wildcard entity IDs # Project-wide wipe using wildcard entity IDs
if dry_run: if dry_run:
print_info(console, "Would delete ALL memories project-wide.") print_info(console, "Would delete ALL memories project-wide.")
print_info(console, "Run without --dry-run to see the actual count.")
print_info(console, "No changes made (dry run).") print_info(console, "No changes made (dry run).")
return return
+1 -3
View File
@@ -110,9 +110,7 @@ def cmd_import(
added = 0 added = 0
failed = 0 failed = 0
_start = _time.perf_counter() _start = _time.perf_counter()
for item in track( for item in track(data, description=f"[{DIM_COLOR}]Importing memories...[/]", console=err_console):
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
+1 -14
View File
@@ -45,17 +45,6 @@ class Mem0Config:
platform: PlatformConfig = field(default_factory=PlatformConfig) platform: PlatformConfig = field(default_factory=PlatformConfig)
SHORT_KEY_ALIASES: dict[str, str] = {
"api_key": "platform.api_key",
"base_url": "platform.base_url",
"user_id": "defaults.user_id",
"agent_id": "defaults.agent_id",
"app_id": "defaults.app_id",
"run_id": "defaults.run_id",
"enable_graph": "defaults.enable_graph",
}
def ensure_config_dir() -> Path: def ensure_config_dir() -> Path:
"""Create ~/.mem0 directory with secure permissions if it doesn't exist.""" """Create ~/.mem0 directory with secure permissions if it doesn't exist."""
CONFIG_DIR.mkdir(parents=True, exist_ok=True) CONFIG_DIR.mkdir(parents=True, exist_ok=True)
@@ -151,8 +140,7 @@ def redact_key(key: str) -> str:
def get_nested_value(config: Mem0Config, dotted_key: str) -> Any: def get_nested_value(config: Mem0Config, dotted_key: str) -> Any:
"""Get a config value by dotted path, e.g. 'platform.api_key' or short form 'api_key'.""" """Get a config value by dotted path, e.g. 'platform.api_key'."""
dotted_key = SHORT_KEY_ALIASES.get(dotted_key, dotted_key)
parts = dotted_key.split(".") parts = dotted_key.split(".")
obj: Any = config obj: Any = config
for part in parts: for part in parts:
@@ -165,7 +153,6 @@ def get_nested_value(config: Mem0Config, dotted_key: str) -> Any:
def set_nested_value(config: Mem0Config, dotted_key: str, value: str) -> bool: def set_nested_value(config: Mem0Config, dotted_key: str, value: str) -> bool:
"""Set a config value by dotted path. Returns True on success.""" """Set a config value by dotted path. Returns True on success."""
dotted_key = SHORT_KEY_ALIASES.get(dotted_key, dotted_key)
parts = dotted_key.split(".") parts = dotted_key.split(".")
obj: Any = config obj: Any = config
for part in parts[:-1]: for part in parts[:-1]:
+4 -20
View File
@@ -49,9 +49,7 @@ def format_memories_text(console: Console, memories: list[dict], title: str = "m
console.print() console.print()
def format_memories_table( def format_memories_table(console: Console, memories: list[dict]) -> None:
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,
@@ -59,33 +57,19 @@ def format_memories_table(
row_styles=["", "dim"], row_styles=["", "dim"],
padding=(0, 1), padding=(0, 1),
) )
table.add_column("ID", style="dim", max_width=38, no_wrap=True) table.add_column("ID", style="dim", max_width=10)
if show_score:
table.add_column("Score", max_width=7, justify="right")
table.add_column("Memory", max_width=50, no_wrap=False) table.add_column("Memory", max_width=50, no_wrap=False)
table.add_column("Category", max_width=14) table.add_column("Category", max_width=14)
table.add_column("Created", max_width=12) table.add_column("Created", max_width=12)
for mem in memories: for mem in memories:
mem_id = mem.get("id", "") mem_id = mem.get("id", "")[:8]
memory_text = mem.get("memory", mem.get("text", "")) memory_text = mem.get("memory", mem.get("text", ""))
if len(memory_text) > 60: if len(memory_text) > 60:
memory_text = memory_text[:57] + "..." memory_text = memory_text[:57] + "..."
categories = mem.get("categories", []) categories = mem.get("categories", [])
if isinstance(categories, list) and categories: cat = categories[0] if isinstance(categories, list) and categories else "—"
cat = (
categories[0]
if len(categories) == 1
else f"{categories[0]} (+{len(categories) - 1})"
)
else:
cat = "—"
created = _format_date(mem.get("created_at")) or "—" created = _format_date(mem.get("created_at")) or "—"
if show_score:
score = mem.get("score")
score_str = f"{score:.2f}" if score is not None else "—"
table.add_row(mem_id, score_str, memory_text, cat, created)
else:
table.add_row(mem_id, memory_text, cat, created) table.add_row(mem_id, memory_text, cat, created)
console.print() console.print()
+3 -28
View File
@@ -8,19 +8,11 @@ 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],
@@ -36,35 +28,22 @@ 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)
result = subprocess.run( return 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
@@ -88,6 +67,7 @@ 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
@@ -181,12 +161,7 @@ class TestCLIIsolated:
) )
assert result.returncode != 0 assert result.returncode != 0
combined = result.stderr + result.stdout combined = result.stderr + result.stdout
assert ( assert "memory ID" in combined.lower() or "--all" in combined or "--entity" in combined or "Error" in combined
"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."""
-2
View File
@@ -99,8 +99,6 @@ class DeepSeekLLM(LLMBase):
} }
) )
if response_format:
params["response_format"] = response_format
if tools: if tools:
params["tools"] = tools params["tools"] = tools
params["tool_choice"] = tool_choice params["tool_choice"] = tool_choice
-2
View File
@@ -99,8 +99,6 @@ class VllmLLM(LLMBase):
} }
) )
if response_format:
params["response_format"] = response_format
if tools: if tools:
params["tools"] = tools params["tools"] = tools
params["tool_choice"] = tool_choice params["tool_choice"] = tool_choice
+2 -2
View File
@@ -51,7 +51,7 @@ class MongoDB(VectorStoreBase):
"""Create new collection with vector search index.""" """Create new collection with vector search index."""
try: try:
database = self.client[self.db_name] database = self.client[self.db_name]
collection_names = database.list_collection_names(authorizedCollections=True) collection_names = database.list_collection_names()
if self.collection_name not in collection_names: if self.collection_name not in collection_names:
logger.info(f"Collection '{self.collection_name}' does not exist. Creating it now.") logger.info(f"Collection '{self.collection_name}' does not exist. Creating it now.")
collection = database[self.collection_name] collection = database[self.collection_name]
@@ -240,7 +240,7 @@ class MongoDB(VectorStoreBase):
List[str]: List of collection names. List[str]: List of collection names.
""" """
try: try:
collections = self.db.list_collection_names(authorizedCollections=True) collections = self.db.list_collection_names()
logger.info(f"Listing collections in database '{self.db_name}': {collections}") logger.info(f"Listing collections in database '{self.db_name}': {collections}")
return collections return collections
except PyMongoError as e: except PyMongoError as e:
-26
View File
@@ -1,26 +0,0 @@
# OpenClaw + Mem0 Docker Setup
# Copy this file to .env and fill in your values:
# cp .env.example .env
# ── Required (one of these depending on mode) ───────────────────────────────
# Platform mode: Mem0 Cloud API key from https://app.mem0.ai
MEM0_API_KEY=
# OSS mode: OpenAI key for embeddings, LLM, and the OpenClaw agent
# Platform mode: only needed if you want a specific LLM for the agent itself
OPENAI_API_KEY=
# ── Optional ─────────────────────────────────────────────────────────────────
# User ID for memory scoping (defaults to your OS username)
MEM0_USER_ID=
# Web UI port (default: 18789)
OPENCLAW_PORT=18789
# Config directory on host (default: ~/.openclaw)
# OPENCLAW_CONFIG_DIR=~/.openclaw
# Timezone (default: UTC)
TZ=UTC
-102
View File
@@ -1,102 +0,0 @@
# OpenClaw + Mem0 One-Click Docker Setup
Get a working OpenClaw agent with Mem0 persistent memory in under 2 minutes.
## Prerequisites
- [Docker Desktop](https://www.docker.com/products/docker-desktop/) or Docker Engine with Compose v2
- One of:
- **Platform mode**: A Mem0 API key from [app.mem0.ai](https://app.mem0.ai)
- **OSS mode**: An OpenAI API key
## Quick Start
```bash
git clone https://github.com/mem0ai/mem0 && cd mem0/openclaw/docker
./setup.sh
```
The setup script will:
1. Ask you to choose Platform or Open-Source mode
2. Prompt for your API key(s)
3. Start the containers automatically
Then open **http://localhost:18789** to start chatting.
## What Gets Deployed
### Platform Mode
```
┌─────────────────────────┐
│ OpenClaw (port 18789) │
│ + Mem0 Plugin │──→ Mem0 Cloud (app.mem0.ai)
└─────────────────────────┘
```
### OSS Mode
```
┌─────────────────────────┐ ┌─────────────────┐
│ OpenClaw (port 18789) │ │ Qdrant │
│ + Mem0 Plugin │────→│ (port 6333) │
└─────────────────────────┘ │ Vector Storage │
└─────────────────┘
```
## Modes Explained
| Feature | Platform (Mem0 Cloud) | Open-Source (Self-Hosted) |
|---------|----------------------|--------------------------|
| Setup | API key only | API key + Qdrant container |
| Vector storage | Managed by Mem0 | Local Qdrant |
| LLM for extraction | Managed by Mem0 | Your OpenAI key |
| Graph memory | Supported | Not yet |
| Dashboard | app.mem0.ai | CLI commands |
## Configuration
After setup, your config lives at `~/.openclaw/openclaw.json`. You can edit it directly.
Key settings in the `openclaw-mem0` plugin section:
| Setting | Default | Description |
|---------|---------|-------------|
| `autoCapture` | `true` | Auto-store conversation facts |
| `autoRecall` | `true` | Auto-retrieve relevant memories |
| `searchThreshold` | `0.5` | Minimum similarity score (0-1) |
| `topK` | `5` | Max memories to retrieve |
To change the port or other Docker settings, edit `.env` in this directory.
## Troubleshooting
**Port 18789 already in use**
```bash
lsof -i :18789
# Kill the process or change OPENCLAW_PORT in .env
```
**API key errors**
- Platform: Verify your key at [app.mem0.ai](https://app.mem0.ai)
- OSS: Ensure `OPENAI_API_KEY` is valid and has credits
**Containers won't start**
```bash
docker compose logs
# or for OSS mode:
docker compose -f docker-compose.yml -f docker-compose.oss.yml logs
```
**Docker not found**
Install [Docker Desktop](https://www.docker.com/products/docker-desktop/) or follow the [engine install guide](https://docs.docker.com/engine/install/).
**Plugin not loading**
```bash
# Reinstall the plugin manually
docker compose run --rm openclaw openclaw plugins install @mem0/openclaw-mem0
```
## Links
- [Mem0 Documentation](https://docs.mem0.ai)
- [Mem0 OpenClaw Plugin](https://github.com/mem0ai/mem0/tree/main/openclaw)
- [OpenClaw Documentation](https://openclaw.com/docs)
-21
View File
@@ -1,21 +0,0 @@
{
"plugins": {
"openclaw-mem0": {
"mode": "open-source",
"userId": "__MEM0_USER_ID__",
"autoCapture": true,
"autoRecall": true,
"searchThreshold": 0.5,
"topK": 5,
"oss": {
"vectorStore": {
"provider": "qdrant",
"config": {
"host": "qdrant",
"port": 6333
}
}
}
}
}
}
@@ -1,13 +0,0 @@
{
"plugins": {
"openclaw-mem0": {
"mode": "platform",
"apiKey": "${MEM0_API_KEY}",
"userId": "__MEM0_USER_ID__",
"autoCapture": true,
"autoRecall": true,
"searchThreshold": 0.5,
"topK": 5
}
}
}
-23
View File
@@ -1,23 +0,0 @@
services:
qdrant:
image: qdrant/qdrant:latest
volumes:
- qdrant_data:/qdrant/storage
# No host port exposure — only accessible via Docker network (openclaw → qdrant:6333)
# Uncomment the next line for debugging: ports: ["127.0.0.1:6333:6333"]
expose:
- "6333"
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "bash -c ':> /dev/tcp/localhost/6333' || exit 1"]
interval: 10s
timeout: 3s
retries: 5
openclaw:
depends_on:
qdrant:
condition: service_healthy
volumes:
qdrant_data:
-22
View File
@@ -1,22 +0,0 @@
services:
openclaw:
image: ghcr.io/openclaw/openclaw:latest
environment:
HOME: /home/node
TERM: xterm-256color
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
MEM0_API_KEY: ${MEM0_API_KEY:-}
TZ: ${TZ:-UTC}
volumes:
- ${OPENCLAW_CONFIG_DIR:-~/.openclaw}:/home/node/.openclaw
ports:
# Bound to localhost only for security — change to "18789:18789" for LAN access
- "127.0.0.1:${OPENCLAW_PORT:-18789}:18789"
restart: unless-stopped
command: ["node", "dist/index.js", "gateway", "--bind", "lan", "--port", "18789"]
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:18789/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 15s
timeout: 5s
retries: 5
start_period: 30s
-268
View File
@@ -1,268 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# ============================================================================
# OpenClaw + Mem0 One-Click Setup
# ============================================================================
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
OPENCLAW_CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw}"
OPENCLAW_PORT="${OPENCLAW_PORT:-18789}"
# Colors (disabled if not a terminal)
if [ -t 1 ]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
NC='\033[0m'
else
RED='' GREEN='' YELLOW='' BLUE='' BOLD='' NC=''
fi
info() { echo -e "${BLUE}ℹ${NC} $*"; }
ok() { echo -e "${GREEN}✔${NC} $*"; }
warn() { echo -e "${YELLOW}⚠${NC} $*"; }
error() { echo -e "${RED}✖${NC} $*" >&2; }
header() { echo -e "\n${BOLD}$*${NC}"; }
# ============================================================================
# Step 1: Check prerequisites
# ============================================================================
header "Checking prerequisites..."
if ! command -v docker &>/dev/null; then
error "Docker is not installed."
echo " Install Docker Desktop: https://www.docker.com/products/docker-desktop/"
echo " Or Docker Engine: https://docs.docker.com/engine/install/"
exit 1
fi
if ! docker compose version &>/dev/null; then
error "Docker Compose v2 is not available."
echo " Ensure you have Docker Compose v2 (comes with Docker Desktop)."
echo " Docs: https://docs.docker.com/compose/install/"
exit 1
fi
if ! docker info &>/dev/null 2>&1; then
error "Docker daemon is not running. Please start Docker and try again."
exit 1
fi
ok "Docker and Docker Compose are available."
# Check for port conflict
if command -v lsof &>/dev/null && lsof -i :"$OPENCLAW_PORT" &>/dev/null; then
warn "Port $OPENCLAW_PORT is already in use."
echo " Check what's using it: lsof -i :$OPENCLAW_PORT"
echo " Or set a different port: OPENCLAW_PORT=18790 ./setup.sh"
exit 1
fi
# ============================================================================
# Step 2: Check for existing config
# ============================================================================
if [ -f "$OPENCLAW_CONFIG_DIR/openclaw.json" ]; then
warn "Existing config found at $OPENCLAW_CONFIG_DIR/openclaw.json"
read -rp " Overwrite with new config? [y/N] " overwrite
if [[ ! "$overwrite" =~ ^[Yy]$ ]]; then
info "Keeping existing config. Skipping config generation."
SKIP_CONFIG=true
else
SKIP_CONFIG=false
fi
else
SKIP_CONFIG=false
fi
# ============================================================================
# Step 3: Choose mode
# ============================================================================
header "Choose your setup mode:"
echo ""
echo " 1) Platform — Uses Mem0 Cloud (managed storage, dashboard at app.mem0.ai)"
echo " 2) OSS — Self-hosted with local Qdrant (fully offline-capable)"
echo ""
while true; do
read -rp "Enter 1 or 2: " mode_choice
case "$mode_choice" in
1) MODE="platform"; break ;;
2) MODE="oss"; break ;;
*) echo " Please enter 1 or 2." ;;
esac
done
ok "Mode: $MODE"
# ============================================================================
# Step 4: Collect API keys
# ============================================================================
header "API Key Configuration"
MEM0_API_KEY=""
OPENAI_API_KEY=""
if [ "$MODE" = "platform" ]; then
echo " Platform mode requires a Mem0 API key from https://app.mem0.ai"
echo ""
while true; do
read -rp " Mem0 API Key: " MEM0_API_KEY
if [ -n "$MEM0_API_KEY" ]; then break; fi
warn "API key is required for platform mode."
done
echo ""
read -rp " OpenAI API Key (optional, for agent LLM — press Enter to skip): " OPENAI_API_KEY
else
echo " OSS mode requires an OpenAI API key for embeddings, LLM, and the agent."
echo ""
while true; do
read -rp " OpenAI API Key: " OPENAI_API_KEY
if [ -n "$OPENAI_API_KEY" ]; then break; fi
warn "API key is required for OSS mode."
done
fi
ok "API keys configured."
# ============================================================================
# Step 5: User ID
# ============================================================================
DEFAULT_USER_ID="$(whoami)"
read -rp " User ID for memory scoping [${DEFAULT_USER_ID}]: " MEM0_USER_ID
MEM0_USER_ID="${MEM0_USER_ID:-$DEFAULT_USER_ID}"
ok "User ID: $MEM0_USER_ID"
# ============================================================================
# Step 6: Generate .env
# ============================================================================
header "Generating configuration..."
cat > "$SCRIPT_DIR/.env" <<EOF
# Generated by setup.sh on $(date -u +"%Y-%m-%dT%H:%M:%SZ")
MEM0_API_KEY=${MEM0_API_KEY}
OPENAI_API_KEY=${OPENAI_API_KEY}
MEM0_USER_ID=${MEM0_USER_ID}
OPENCLAW_PORT=${OPENCLAW_PORT}
OPENCLAW_CONFIG_DIR=${OPENCLAW_CONFIG_DIR}
TZ=${TZ:-UTC}
EOF
ok "Created .env"
# ============================================================================
# Step 7: Generate OpenClaw config
# ============================================================================
if [ "$SKIP_CONFIG" = false ]; then
mkdir -p "$OPENCLAW_CONFIG_DIR"
if [ "$MODE" = "platform" ]; then
TEMPLATE="$SCRIPT_DIR/config/openclaw.platform.json"
else
TEMPLATE="$SCRIPT_DIR/config/openclaw.oss.json"
fi
# Substitute the user ID placeholder (apiKey uses env var resolution at runtime)
sed "s/__MEM0_USER_ID__/${MEM0_USER_ID}/g" "$TEMPLATE" > "$OPENCLAW_CONFIG_DIR/openclaw.json"
ok "Created $OPENCLAW_CONFIG_DIR/openclaw.json"
fi
# ============================================================================
# Step 8: Install Mem0 plugin
# ============================================================================
header "Installing Mem0 plugin..."
COMPOSE_CMD="docker compose"
if [ "$MODE" = "oss" ]; then
COMPOSE_CMD="docker compose -f docker-compose.yml -f docker-compose.oss.yml"
fi
cd "$SCRIPT_DIR"
if $COMPOSE_CMD run --rm openclaw openclaw plugins install @mem0/openclaw-mem0 2>/dev/null; then
ok "Mem0 plugin installed."
else
warn "Plugin install via Docker failed. This may be normal on first run."
echo " You can install manually later:"
echo " $COMPOSE_CMD run --rm openclaw openclaw plugins install @mem0/openclaw-mem0"
fi
# ============================================================================
# Step 9: Copy onboarding skill
# ============================================================================
SKILL_DIR="$OPENCLAW_CONFIG_DIR/workspace/skills/mem0-onboarding"
mkdir -p "$SKILL_DIR"
cp "$SCRIPT_DIR/skills/mem0-onboarding/SKILL.md" "$SKILL_DIR/SKILL.md"
ok "Onboarding skill installed."
# ============================================================================
# Step 10: Start containers
# ============================================================================
header "Starting OpenClaw..."
$COMPOSE_CMD up -d
# ============================================================================
# Step 11: Health check
# ============================================================================
info "Waiting for OpenClaw to be ready..."
HEALTH_URL="http://127.0.0.1:${OPENCLAW_PORT}/healthz"
TIMEOUT=90
ELAPSED=0
INTERVAL=3
while [ $ELAPSED -lt $TIMEOUT ]; do
if curl -sf "$HEALTH_URL" &>/dev/null; then
echo ""
ok "OpenClaw is ready!"
break
fi
printf "."
sleep $INTERVAL
ELAPSED=$((ELAPSED + INTERVAL))
done
if [ $ELAPSED -ge $TIMEOUT ]; then
echo ""
warn "Health check timed out after ${TIMEOUT}s."
echo " OpenClaw may still be starting. Check logs:"
echo " cd $SCRIPT_DIR && $COMPOSE_CMD logs -f"
exit 1
fi
# ============================================================================
# Done!
# ============================================================================
echo ""
header "Setup complete!"
echo ""
echo " Open the web UI: ${BOLD}http://localhost:${OPENCLAW_PORT}${NC}"
echo " Mode: ${MODE}"
echo " User ID: ${MEM0_USER_ID}"
echo " Config: ${OPENCLAW_CONFIG_DIR}/openclaw.json"
echo ""
echo " ${BLUE}Tip:${NC} Your first chat will trigger the Mem0 onboarding skill."
echo " Try telling the agent something about yourself to test memory!"
echo ""
echo " Useful commands:"
echo " View logs: cd $SCRIPT_DIR && $COMPOSE_CMD logs -f"
echo " Stop: cd $SCRIPT_DIR && $COMPOSE_CMD down"
echo " Restart: cd $SCRIPT_DIR && $COMPOSE_CMD restart"
echo ""
@@ -1,37 +0,0 @@
# Mem0 Memory Onboarding
Welcome! This OpenClaw agent has **persistent memory** powered by Mem0. It remembers things across sessions so you never have to repeat yourself.
## How It Works
- **Auto-Capture**: After each conversation turn, important facts are automatically extracted and stored.
- **Auto-Recall**: Before each turn, relevant memories are retrieved to give context.
## Quick Demo — Try These 3 Steps
### Step 1: Tell me something about yourself
Share a preference, a project you're working on, or a fact about yourself. For example:
> "I prefer TypeScript over JavaScript and I'm building a SaaS app called Acme."
### Step 2: Confirm storage
After you share, I'll confirm that the information was captured. You can also check with:
> "What do you remember about me?"
### Step 3: Verify persistence
Start a **new session** (close this chat and open a new one), then ask:
> "What do you know about me?"
Your memories carry over — that's the magic of persistent memory.
## Power User Commands
If your OpenClaw setup includes the Mem0 CLI commands:
- `openclaw mem0 search "query"` — Search your memories
- `openclaw mem0 stats` — View memory statistics
- `openclaw mem0 list` — List recent memories
## Learn More
- [Mem0 Documentation](https://docs.mem0.ai)
- [OpenClaw Plugin README](https://github.com/mem0ai/mem0/tree/main/openclaw)
-280
View File
@@ -1,280 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# ============================================================================
# Test script for OpenClaw + Mem0 Docker setup
# Validates everything works without needing real API keys
# ============================================================================
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PASS=0
FAIL=0
TESTS=()
pass() { PASS=$((PASS + 1)); TESTS+=("✔ $1"); echo " ✔ $1"; }
fail() { FAIL=$((FAIL + 1)); TESTS+=("✖ $1: $2"); echo " ✖ $1: $2"; }
echo ""
echo "═══════════════════════════════════════════════════"
echo " OpenClaw + Mem0 Docker Setup — Test Suite"
echo "═══════════════════════════════════════════════════"
# ── Prerequisites ────────────────────────────────────────────────────────────
echo ""
echo "▸ Prerequisites"
if command -v docker &>/dev/null; then
pass "Docker installed"
else
fail "Docker installed" "docker not found"
fi
if docker compose version &>/dev/null; then
pass "Docker Compose v2 available"
else
fail "Docker Compose v2 available" "docker compose not found"
fi
if docker info &>/dev/null 2>&1; then
pass "Docker daemon running"
else
fail "Docker daemon running" "daemon not responding"
fi
# ── File structure ───────────────────────────────────────────────────────────
echo ""
echo "▸ File structure"
EXPECTED_FILES=(
"setup.sh"
"docker-compose.yml"
"docker-compose.oss.yml"
".env.example"
"config/openclaw.platform.json"
"config/openclaw.oss.json"
"skills/mem0-onboarding/SKILL.md"
"README.md"
)
for f in "${EXPECTED_FILES[@]}"; do
if [ -f "$SCRIPT_DIR/$f" ]; then
pass "File exists: $f"
else
fail "File exists: $f" "missing"
fi
done
if [ -x "$SCRIPT_DIR/setup.sh" ]; then
pass "setup.sh is executable"
else
fail "setup.sh is executable" "not executable"
fi
# ── Bash syntax ──────────────────────────────────────────────────────────────
echo ""
echo "▸ Script syntax"
if bash -n "$SCRIPT_DIR/setup.sh" 2>&1; then
pass "setup.sh syntax valid"
else
fail "setup.sh syntax valid" "syntax errors"
fi
# ── JSON validity ────────────────────────────────────────────────────────────
echo ""
echo "▸ JSON configs"
for json_file in config/openclaw.platform.json config/openclaw.oss.json; do
if python3 -c "import json; json.load(open('$SCRIPT_DIR/$json_file'))" 2>/dev/null; then
pass "$json_file is valid JSON"
else
fail "$json_file is valid JSON" "parse error"
fi
done
# Check platform config has expected fields
if python3 -c "
import json, sys
c = json.load(open('$SCRIPT_DIR/config/openclaw.platform.json'))
p = c['plugins']['openclaw-mem0']
assert p['mode'] == 'platform', 'wrong mode'
assert p['apiKey'] == '\${MEM0_API_KEY}', 'apiKey not using env var'
assert p['userId'] == '__MEM0_USER_ID__', 'userId placeholder missing'
assert p['autoCapture'] is True
assert p['autoRecall'] is True
" 2>/dev/null; then
pass "Platform config has correct structure"
else
fail "Platform config has correct structure" "missing or wrong fields"
fi
# Check OSS config has Qdrant vector store
if python3 -c "
import json
c = json.load(open('$SCRIPT_DIR/config/openclaw.oss.json'))
p = c['plugins']['openclaw-mem0']
assert p['mode'] == 'open-source', 'wrong mode'
vs = p['oss']['vectorStore']
assert vs['provider'] == 'qdrant'
assert vs['config']['host'] == 'qdrant'
assert vs['config']['port'] == 6333
" 2>/dev/null; then
pass "OSS config has Qdrant vector store"
else
fail "OSS config has Qdrant vector store" "missing or wrong fields"
fi
# ── Docker Compose validation ────────────────────────────────────────────────
echo ""
echo "▸ Docker Compose validation"
cd "$SCRIPT_DIR"
# Create a temporary .env so compose can resolve variables
TEMP_ENV=false
if [ ! -f .env ]; then
cat > .env <<'EOF'
MEM0_API_KEY=test-key-for-validation
OPENAI_API_KEY=test-key-for-validation
MEM0_USER_ID=testuser
OPENCLAW_PORT=18789
OPENCLAW_CONFIG_DIR=/tmp/openclaw-test
TZ=UTC
EOF
TEMP_ENV=true
fi
if docker compose config --quiet 2>/dev/null; then
pass "docker-compose.yml is valid"
else
fail "docker-compose.yml is valid" "$(docker compose config 2>&1 | tail -1)"
fi
if docker compose -f docker-compose.yml -f docker-compose.oss.yml config --quiet 2>/dev/null; then
pass "docker-compose.oss.yml override is valid"
else
fail "docker-compose.oss.yml override is valid" "$(docker compose -f docker-compose.yml -f docker-compose.oss.yml config 2>&1 | tail -1)"
fi
# Verify services are correctly defined
PLATFORM_SERVICES=$(docker compose config --services 2>/dev/null)
if echo "$PLATFORM_SERVICES" | grep -q "openclaw"; then
pass "Platform mode defines 'openclaw' service"
else
fail "Platform mode defines 'openclaw' service" "service not found"
fi
OSS_SERVICES=$(docker compose -f docker-compose.yml -f docker-compose.oss.yml config --services 2>/dev/null)
if echo "$OSS_SERVICES" | grep -q "qdrant"; then
pass "OSS mode adds 'qdrant' service"
else
fail "OSS mode adds 'qdrant' service" "service not found"
fi
# Verify port binding is localhost-only
PORT_CONFIG=$(docker compose config 2>/dev/null | grep -A5 "published" | head -10)
if docker compose config 2>/dev/null | grep -q "127.0.0.1"; then
pass "Port is bound to 127.0.0.1 (localhost only)"
else
fail "Port is bound to 127.0.0.1 (localhost only)" "binding not found"
fi
# Verify healthcheck is defined
if docker compose config 2>/dev/null | grep -q "healthz"; then
pass "Healthcheck is configured"
else
fail "Healthcheck is configured" "healthcheck not found"
fi
# ── Docker image pull test ───────────────────────────────────────────────────
echo ""
echo "▸ Docker image availability"
# Just check if the image reference is parseable (don't actually pull — too slow for a test)
IMAGE=$(docker compose config 2>/dev/null | grep "image:" | head -1 | awk '{print $2}')
if [ -n "$IMAGE" ]; then
pass "OpenClaw image reference: $IMAGE"
else
fail "OpenClaw image reference" "could not extract image name"
fi
QDRANT_IMAGE=$(docker compose -f docker-compose.yml -f docker-compose.oss.yml config 2>/dev/null | grep "image:" | grep qdrant | awk '{print $2}')
if [ -n "$QDRANT_IMAGE" ]; then
pass "Qdrant image reference: $QDRANT_IMAGE"
else
fail "Qdrant image reference" "could not extract image name"
fi
# ── setup.sh simulation ─────────────────────────────────────────────────────
echo ""
echo "▸ setup.sh config generation simulation"
# Test the sed substitution that setup.sh does
TEST_CONFIG_DIR=$(mktemp -d)
sed "s/__MEM0_USER_ID__/testuser/g" "$SCRIPT_DIR/config/openclaw.platform.json" > "$TEST_CONFIG_DIR/openclaw.json"
if python3 -c "
import json
c = json.load(open('$TEST_CONFIG_DIR/openclaw.json'))
p = c['plugins']['openclaw-mem0']
assert p['userId'] == 'testuser', f'expected testuser, got {p[\"userId\"]}'
assert p['apiKey'] == '\${MEM0_API_KEY}', 'apiKey should remain as env var template'
" 2>/dev/null; then
pass "sed substitution works correctly (userId baked, apiKey templated)"
else
fail "sed substitution" "userId not replaced or apiKey was wrongly replaced"
fi
rm -rf "$TEST_CONFIG_DIR"
# OSS config substitution
TEST_CONFIG_DIR=$(mktemp -d)
sed "s/__MEM0_USER_ID__/testuser/g" "$SCRIPT_DIR/config/openclaw.oss.json" > "$TEST_CONFIG_DIR/openclaw.json"
if python3 -c "
import json
c = json.load(open('$TEST_CONFIG_DIR/openclaw.json'))
p = c['plugins']['openclaw-mem0']
assert p['userId'] == 'testuser'
assert p['oss']['vectorStore']['config']['host'] == 'qdrant'
" 2>/dev/null; then
pass "OSS config sed substitution works correctly"
else
fail "OSS config sed substitution" "fields incorrect after substitution"
fi
rm -rf "$TEST_CONFIG_DIR"
# ── Cleanup ──────────────────────────────────────────────────────────────────
if [ "$TEMP_ENV" = true ]; then
rm -f "$SCRIPT_DIR/.env"
fi
# ── Summary ──────────────────────────────────────────────────────────────────
echo ""
echo "═══════════════════════════════════════════════════"
TOTAL=$((PASS + FAIL))
if [ $FAIL -eq 0 ]; then
echo " All $TOTAL tests passed ✔"
else
echo " $PASS/$TOTAL passed, $FAIL failed"
echo ""
for t in "${TESTS[@]}"; do
if [[ "$t" == "✖"* ]]; then
echo " $t"
fi
done
fi
echo "═══════════════════════════════════════════════════"
echo ""
exit $FAIL
-44
View File
@@ -113,47 +113,3 @@ def test_generate_response_with_tools(mock_deepseek_client):
assert len(response["tool_calls"]) == 1 assert len(response["tool_calls"]) == 1
assert response["tool_calls"][0]["name"] == "add_memory" assert response["tool_calls"][0]["name"] == "add_memory"
assert response["tool_calls"][0]["arguments"] == {"data": "Today is a sunny day."} assert response["tool_calls"][0]["arguments"] == {"data": "Today is a sunny day."}
def test_generate_response_with_response_format(mock_deepseek_client):
config = BaseLlmConfig(model="deepseek-chat", temperature=0.7, max_tokens=100, top_p=1.0)
llm = DeepSeekLLM(config)
messages = [
{"role": "system", "content": "You are a memory extraction assistant."},
{"role": "user", "content": "I like hiking on weekends."},
]
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content='{"facts": ["User likes hiking on weekends"]}'))]
mock_deepseek_client.chat.completions.create.return_value = mock_response
response = llm.generate_response(messages, response_format={"type": "json_object"})
mock_deepseek_client.chat.completions.create.assert_called_once_with(
model="deepseek-chat",
messages=messages,
temperature=0.7,
max_tokens=100,
top_p=1.0,
response_format={"type": "json_object"},
)
assert response == '{"facts": ["User likes hiking on weekends"]}'
def test_generate_response_without_response_format(mock_deepseek_client):
config = BaseLlmConfig(model="deepseek-chat", temperature=0.7, max_tokens=100, top_p=1.0)
llm = DeepSeekLLM(config)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Tell me a joke."},
]
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content="Why did the chicken cross the road?"))]
mock_deepseek_client.chat.completions.create.return_value = mock_response
response = llm.generate_response(messages)
call_kwargs = mock_deepseek_client.chat.completions.create.call_args[1]
assert "response_format" not in call_kwargs
assert response == "Why did the chicken cross the road?"
-44
View File
@@ -88,50 +88,6 @@ def test_generate_response_with_tools(mock_vllm_client):
def test_generate_response_with_response_format(mock_vllm_client):
config = BaseLlmConfig(model="Qwen/Qwen2.5-32B-Instruct", temperature=0.7, max_tokens=100, top_p=1.0)
llm = VllmLLM(config)
messages = [
{"role": "system", "content": "You are a memory extraction assistant."},
{"role": "user", "content": "I like hiking on weekends."},
]
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content='{"facts": ["User likes hiking on weekends"]}'))]
mock_vllm_client.chat.completions.create.return_value = mock_response
response = llm.generate_response(messages, response_format={"type": "json_object"})
mock_vllm_client.chat.completions.create.assert_called_once_with(
model="Qwen/Qwen2.5-32B-Instruct",
messages=messages,
temperature=0.7,
max_tokens=100,
top_p=1.0,
response_format={"type": "json_object"},
)
assert response == '{"facts": ["User likes hiking on weekends"]}'
def test_generate_response_without_response_format(mock_vllm_client):
config = BaseLlmConfig(model="Qwen/Qwen2.5-32B-Instruct", temperature=0.7, max_tokens=100, top_p=1.0)
llm = VllmLLM(config)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Tell me a joke."},
]
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content="Why did the chicken cross the road?"))]
mock_vllm_client.chat.completions.create.return_value = mock_response
response = llm.generate_response(messages)
call_kwargs = mock_vllm_client.chat.completions.create.call_args[1]
assert "response_format" not in call_kwargs
assert response == "Why did the chicken cross the road?"
def create_mocked_memory(): def create_mocked_memory():
"""Create a fully mocked Memory instance for testing.""" """Create a fully mocked Memory instance for testing."""
with patch('mem0.utils.factory.LlmFactory.create') as mock_llm_factory, \ with patch('mem0.utils.factory.LlmFactory.create') as mock_llm_factory, \