Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0fb194c76 | ||
|
|
215d8b5a71 | ||
|
|
82525dbf0f | ||
|
|
32c1ccba5b | ||
|
|
9bebcf45f7 | ||
|
|
93bd4e248c | ||
|
|
af19495f66 | ||
|
|
dbac83218f | ||
|
|
3618aeff22 |
@@ -0,0 +1,100 @@
|
|||||||
|
name: CLI Node CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'cli/node/**'
|
||||||
|
- '.github/workflows/cli-node-ci.yml'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'cli/node/**'
|
||||||
|
- '.github/workflows/cli-node-ci.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: cli/node/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: cli/node
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
working-directory: cli/node
|
||||||
|
run: pnpm run lint
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
working-directory: cli/node
|
||||||
|
run: pnpm run typecheck
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [20, 22]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- name: Setup Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: cli/node/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: cli/node
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
working-directory: cli/node
|
||||||
|
run: pnpm run test
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: cli/node/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: cli/node
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
working-directory: cli/node
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Verify dist output
|
||||||
|
run: |
|
||||||
|
test -f cli/node/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
name: CLI Python CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'cli/python/**'
|
||||||
|
- '.github/workflows/cli-python-ci.yml'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'cli/python/**'
|
||||||
|
- '.github/workflows/cli-python-ci.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Install dev dependencies
|
||||||
|
working-directory: cli/python
|
||||||
|
run: pip install -e ".[dev]"
|
||||||
|
|
||||||
|
- name: Lint with ruff
|
||||||
|
working-directory: cli/python
|
||||||
|
run: ruff check .
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
working-directory: cli/python
|
||||||
|
run: ruff format --check .
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.10", "3.11", "3.12"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Install dev dependencies
|
||||||
|
working-directory: cli/python
|
||||||
|
run: pip install -e ".[dev]"
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
working-directory: cli/python
|
||||||
|
run: pytest
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Install Hatch
|
||||||
|
run: pip install hatch
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
working-directory: cli/python
|
||||||
|
run: hatch build --clean
|
||||||
|
|
||||||
|
- name: Verify dist output
|
||||||
|
run: |
|
||||||
|
ls cli/python/dist/*.whl || (echo "Wheel file missing" && exit 1)
|
||||||
|
ls cli/python/dist/*.tar.gz || (echo "Source dist missing" && exit 1)
|
||||||
@@ -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) · [Twitter](https://x.com/mem0ai)
|
- Community: [Discord](https://mem0.dev/DiG) · [X (formerly Twitter)](https://x.com/mem0ai)
|
||||||
- Contact: founders@mem0.ai
|
- Contact: founders@mem0.ai
|
||||||
|
|
||||||
## Citation
|
## Citation
|
||||||
|
|||||||
@@ -153,8 +153,19 @@ 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.
|
||||||
@@ -164,6 +175,8 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+8
-2
@@ -17,9 +17,15 @@ Both packages install a `mem0` binary with identical behavior.
|
|||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Authenticate and save config
|
# Interactive setup wizard
|
||||||
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
|
||||||
|
|
||||||
@@ -40,7 +46,7 @@ mem0 delete <memory-id>
|
|||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `mem0 init` | Interactive setup wizard — configures API key and default user ID |
|
| `mem0 init` | Setup wizard — login via email or configure API key manually |
|
||||||
| `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 |
|
||||||
|
|||||||
+8
-3
@@ -503,15 +503,20 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "init",
|
"name": "init",
|
||||||
"description": "Interactive setup wizard for mem0 CLI.",
|
"description": "Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
|
||||||
"usage": "mem0 init",
|
"usage": "mem0 init [OPTIONS]",
|
||||||
"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",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"mem0": "./dist/index.js"
|
"mem0": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup src/index.ts --format esm --dts --clean",
|
"build": "tsup",
|
||||||
"dev": "tsx src/index.ts",
|
"dev": "tsx src/index.ts",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
|
|||||||
@@ -67,7 +67,10 @@ export interface Backend {
|
|||||||
opts?: AddOptions,
|
opts?: AddOptions,
|
||||||
): Promise<Record<string, unknown>>;
|
): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
search(query: string, opts?: SearchOptions): Promise<Record<string, unknown>[]>;
|
search(
|
||||||
|
query: string,
|
||||||
|
opts?: SearchOptions,
|
||||||
|
): Promise<Record<string, unknown>[]>;
|
||||||
|
|
||||||
get(memoryId: string): Promise<Record<string, unknown>>;
|
get(memoryId: string): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
@@ -79,17 +82,24 @@ export interface Backend {
|
|||||||
metadata?: Record<string, unknown>,
|
metadata?: Record<string, unknown>,
|
||||||
): Promise<Record<string, unknown>>;
|
): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
delete(memoryId?: string, opts?: DeleteOptions): Promise<Record<string, unknown>>;
|
delete(
|
||||||
|
memoryId?: string,
|
||||||
|
opts?: DeleteOptions,
|
||||||
|
): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>;
|
deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
status(opts?: { userId?: string; agentId?: string }): Promise<Record<string, unknown>>;
|
status(opts?: { userId?: string; agentId?: string }): Promise<
|
||||||
|
Record<string, unknown>
|
||||||
|
>;
|
||||||
|
|
||||||
entities(entityType: string): Promise<Record<string, unknown>[]>;
|
entities(entityType: string): Promise<Record<string, unknown>[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthError extends Error {
|
export class AuthError extends Error {
|
||||||
constructor(message = "Authentication failed. Your API key may be invalid or expired.") {
|
constructor(
|
||||||
|
message = "Authentication failed. Your API key may be invalid or expired.",
|
||||||
|
) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = "AuthError";
|
this.name = "AuthError";
|
||||||
}
|
}
|
||||||
@@ -112,4 +122,3 @@ export class APIError extends Error {
|
|||||||
export function getBackend(config: Mem0Config): Backend {
|
export function getBackend(config: Mem0Config): Backend {
|
||||||
return new PlatformBackend(config.platform);
|
return new PlatformBackend(config.platform);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export { getBackend } from "./base.js";
|
export { getBackend } from "./base.js";
|
||||||
export type { Backend, AddOptions, SearchOptions, ListOptions, DeleteOptions, EntityIds } from "./base.js";
|
export type {
|
||||||
|
Backend,
|
||||||
|
AddOptions,
|
||||||
|
SearchOptions,
|
||||||
|
ListOptions,
|
||||||
|
DeleteOptions,
|
||||||
|
EntityIds,
|
||||||
|
} from "./base.js";
|
||||||
export { AuthError, NotFoundError, APIError } from "./base.js";
|
export { AuthError, NotFoundError, APIError } from "./base.js";
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
import type { PlatformConfig } from "../config.js";
|
import type { PlatformConfig } from "../config.js";
|
||||||
import {
|
import {
|
||||||
type AddOptions,
|
|
||||||
APIError,
|
APIError,
|
||||||
|
type AddOptions,
|
||||||
AuthError,
|
AuthError,
|
||||||
type Backend,
|
type Backend,
|
||||||
type DeleteOptions,
|
type DeleteOptions,
|
||||||
@@ -58,15 +58,24 @@ 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();
|
const body = (await resp.json()) as Record<string, unknown>;
|
||||||
detail = (body as Record<string, string>).detail ?? resp.statusText;
|
detail =
|
||||||
|
((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) {
|
||||||
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
|
let detail: string = resp.statusText;
|
||||||
|
try {
|
||||||
|
const body = (await resp.json()) as Record<string, unknown>;
|
||||||
|
detail = (body.detail ?? body.message ?? resp.statusText) as string;
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
throw new Error(`HTTP ${resp.status}: ${detail}`);
|
||||||
}
|
}
|
||||||
if (resp.status === 204) {
|
if (resp.status === 204) {
|
||||||
return {};
|
return {};
|
||||||
@@ -98,10 +107,9 @@ export class PlatformBackend implements Backend {
|
|||||||
if (opts.categories) payload.categories = opts.categories;
|
if (opts.categories) payload.categories = opts.categories;
|
||||||
if (opts.enableGraph) payload.enable_graph = true;
|
if (opts.enableGraph) payload.enable_graph = true;
|
||||||
|
|
||||||
return (await this._request("POST", "/v1/memories/", { json: payload })) as Record<
|
return (await this._request("POST", "/v1/memories/", {
|
||||||
string,
|
json: payload,
|
||||||
unknown
|
})) as Record<string, unknown>;
|
||||||
>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _buildFilters(opts: {
|
private _buildFilters(opts: {
|
||||||
@@ -112,7 +120,10 @@ export class PlatformBackend implements Backend {
|
|||||||
extraFilters?: Record<string, unknown>;
|
extraFilters?: Record<string, unknown>;
|
||||||
}): Record<string, unknown> | undefined {
|
}): Record<string, unknown> | undefined {
|
||||||
// If caller passed a pre-built filter structure, use it directly
|
// If caller passed a pre-built filter structure, use it directly
|
||||||
if (opts.extraFilters && ("AND" in opts.extraFilters || "OR" in opts.extraFilters)) {
|
if (
|
||||||
|
opts.extraFilters &&
|
||||||
|
("AND" in opts.extraFilters || "OR" in opts.extraFilters)
|
||||||
|
) {
|
||||||
return opts.extraFilters;
|
return opts.extraFilters;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +144,10 @@ export class PlatformBackend implements Backend {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query: string, opts: SearchOptions = {}): Promise<Record<string, unknown>[]> {
|
async search(
|
||||||
|
query: string,
|
||||||
|
opts: SearchOptions = {},
|
||||||
|
): Promise<Record<string, unknown>[]> {
|
||||||
const payload: Record<string, unknown> = {
|
const payload: Record<string, unknown> = {
|
||||||
query,
|
query,
|
||||||
top_k: opts.topK ?? 10,
|
top_k: opts.topK ?? 10,
|
||||||
@@ -162,10 +176,15 @@ export class PlatformBackend implements Backend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get(memoryId: string): Promise<Record<string, unknown>> {
|
async get(memoryId: string): Promise<Record<string, unknown>> {
|
||||||
return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record<string, unknown>;
|
return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async listMemories(opts: ListOptions = {}): Promise<Record<string, unknown>[]> {
|
async listMemories(
|
||||||
|
opts: ListOptions = {},
|
||||||
|
): Promise<Record<string, unknown>[]> {
|
||||||
const payload: Record<string, unknown> = {};
|
const payload: Record<string, unknown> = {};
|
||||||
const params: Record<string, string> = {
|
const params: Record<string, string> = {
|
||||||
page: String(opts.page ?? 1),
|
page: String(opts.page ?? 1),
|
||||||
@@ -177,10 +196,16 @@ export class PlatformBackend implements Backend {
|
|||||||
extra.categories = { contains: opts.category };
|
extra.categories = { contains: opts.category };
|
||||||
}
|
}
|
||||||
if (opts.after) {
|
if (opts.after) {
|
||||||
extra.created_at = { ...(extra.created_at as Record<string, unknown> | undefined), gte: opts.after };
|
extra.created_at = {
|
||||||
|
...(extra.created_at as Record<string, unknown> | undefined),
|
||||||
|
gte: opts.after,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (opts.before) {
|
if (opts.before) {
|
||||||
extra.created_at = { ...(extra.created_at as Record<string, unknown> | undefined), lte: opts.before };
|
extra.created_at = {
|
||||||
|
...(extra.created_at as Record<string, unknown> | undefined),
|
||||||
|
lte: opts.before,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiFilters = this._buildFilters({
|
const apiFilters = this._buildFilters({
|
||||||
@@ -193,7 +218,10 @@ export class PlatformBackend implements Backend {
|
|||||||
if (apiFilters) payload.filters = apiFilters;
|
if (apiFilters) payload.filters = apiFilters;
|
||||||
if (opts.enableGraph) payload.enable_graph = true;
|
if (opts.enableGraph) payload.enable_graph = true;
|
||||||
|
|
||||||
const result = (await this._request("POST", "/v2/memories/", { json: payload, params })) as unknown;
|
const result = (await this._request("POST", "/v2/memories/", {
|
||||||
|
json: payload,
|
||||||
|
params,
|
||||||
|
})) as unknown;
|
||||||
if (Array.isArray(result)) return result;
|
if (Array.isArray(result)) return result;
|
||||||
const obj = result as Record<string, unknown>;
|
const obj = result as Record<string, unknown>;
|
||||||
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
|
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
|
||||||
@@ -222,16 +250,15 @@ export class PlatformBackend implements Backend {
|
|||||||
if (opts.agentId) params.agent_id = opts.agentId;
|
if (opts.agentId) params.agent_id = opts.agentId;
|
||||||
if (opts.appId) params.app_id = opts.appId;
|
if (opts.appId) params.app_id = opts.appId;
|
||||||
if (opts.runId) params.run_id = opts.runId;
|
if (opts.runId) params.run_id = opts.runId;
|
||||||
return (await this._request("DELETE", "/v1/memories/", { params })) as Record<
|
return (await this._request("DELETE", "/v1/memories/", {
|
||||||
string,
|
params,
|
||||||
unknown
|
})) as Record<string, unknown>;
|
||||||
>;
|
|
||||||
}
|
}
|
||||||
if (memoryId) {
|
if (memoryId) {
|
||||||
return (await this._request("DELETE", `/v1/memories/${memoryId}/`)) as Record<
|
return (await this._request(
|
||||||
string,
|
"DELETE",
|
||||||
unknown
|
`/v1/memories/${memoryId}/`,
|
||||||
>;
|
)) as Record<string, unknown>;
|
||||||
}
|
}
|
||||||
throw new Error("Either memoryId or --all is required");
|
throw new Error("Either memoryId or --all is required");
|
||||||
}
|
}
|
||||||
@@ -245,10 +272,9 @@ export class PlatformBackend implements Backend {
|
|||||||
if (Object.keys(params).length === 0) {
|
if (Object.keys(params).length === 0) {
|
||||||
throw new Error("At least one entity ID is required for deleteEntities.");
|
throw new Error("At least one entity ID is required for deleteEntities.");
|
||||||
}
|
}
|
||||||
return (await this._request("DELETE", "/v1/entities/", { params })) as Record<
|
return (await this._request("DELETE", "/v1/entities/", {
|
||||||
string,
|
params,
|
||||||
unknown
|
})) as Record<string, unknown>;
|
||||||
>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async status(
|
async status(
|
||||||
@@ -257,13 +283,19 @@ export class PlatformBackend implements Backend {
|
|||||||
try {
|
try {
|
||||||
if (opts.userId || opts.agentId) {
|
if (opts.userId || opts.agentId) {
|
||||||
const payload: Record<string, unknown> = {};
|
const payload: Record<string, unknown> = {};
|
||||||
const statusParams: Record<string, string> = { page: "1", page_size: "1" };
|
const statusParams: Record<string, string> = {
|
||||||
|
page: "1",
|
||||||
|
page_size: "1",
|
||||||
|
};
|
||||||
const apiFilters = this._buildFilters({
|
const apiFilters = this._buildFilters({
|
||||||
userId: opts.userId,
|
userId: opts.userId,
|
||||||
agentId: opts.agentId,
|
agentId: opts.agentId,
|
||||||
});
|
});
|
||||||
if (apiFilters) payload.filters = apiFilters;
|
if (apiFilters) payload.filters = apiFilters;
|
||||||
await this._request("POST", "/v2/memories/", { json: payload, params: statusParams });
|
await this._request("POST", "/v2/memories/", {
|
||||||
|
json: payload,
|
||||||
|
params: statusParams,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
await this._request("GET", "/v1/entities/");
|
await this._request("GET", "/v1/entities/");
|
||||||
}
|
}
|
||||||
@@ -283,7 +315,10 @@ export class PlatformBackend implements Backend {
|
|||||||
if (Array.isArray(result)) {
|
if (Array.isArray(result)) {
|
||||||
items = result;
|
items = result;
|
||||||
} else {
|
} else {
|
||||||
items = ((result as Record<string, unknown>).results ?? []) as Record<string, unknown>[];
|
items = ((result as Record<string, unknown>).results ?? []) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeMap: Record<string, string> = {
|
const typeMap: Record<string, string> = {
|
||||||
|
|||||||
+15
-11
@@ -4,10 +4,7 @@
|
|||||||
|
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import ora, { type Ora } from "ora";
|
import ora, { type Ora } from "ora";
|
||||||
import { createRequire } from "node:module";
|
import { CLI_VERSION } from "./version.js";
|
||||||
|
|
||||||
const _require = createRequire(import.meta.url);
|
|
||||||
const PKG_VERSION: string = _require("../package.json").version;
|
|
||||||
|
|
||||||
export const LOGO = `
|
export const LOGO = `
|
||||||
███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
|
███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
|
||||||
@@ -48,7 +45,7 @@ export function printBanner(): void {
|
|||||||
const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2))
|
const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2))
|
||||||
const logoLines = LOGO.trimEnd().split("\n");
|
const logoLines = LOGO.trimEnd().split("\n");
|
||||||
const tagline = ` ${TAGLINE}`;
|
const tagline = ` ${TAGLINE}`;
|
||||||
const subtitle = `Node.js SDK · v${PKG_VERSION}`;
|
const subtitle = `Node.js SDK · v${CLI_VERSION}`;
|
||||||
const contentLines = ["", ...logoLines, "", tagline, ""];
|
const contentLines = ["", ...logoLines, "", tagline, ""];
|
||||||
|
|
||||||
// Compute inner width from longest content line + padding both sides
|
// Compute inner width from longest content line + padding both sides
|
||||||
@@ -58,7 +55,9 @@ export function printBanner(): void {
|
|||||||
|
|
||||||
const topBorder = brand(`╭${"─".repeat(totalWidth - 2)}╮`);
|
const topBorder = brand(`╭${"─".repeat(totalWidth - 2)}╮`);
|
||||||
const subtitleFill = totalWidth - 2 - subtitle.length - 3; // 3 = "─ " before subtitle + "─" after
|
const subtitleFill = totalWidth - 2 - subtitle.length - 3; // 3 = "─ " before subtitle + "─" after
|
||||||
const bottomBorder = brand(`╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`);
|
const bottomBorder = brand(
|
||||||
|
`╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`,
|
||||||
|
);
|
||||||
|
|
||||||
const body = contentLines.map((line) => {
|
const body = contentLines.map((line) => {
|
||||||
const rightPad = innerWidth - pad - line.length;
|
const rightPad = innerWidth - pad - line.length;
|
||||||
@@ -67,7 +66,8 @@ export function printBanner(): void {
|
|||||||
// Re-color tagline line with accent instead of brand.bold
|
// Re-color tagline line with accent instead of brand.bold
|
||||||
const taglineIdx = body.length - 2; // second-to-last (before trailing empty line)
|
const taglineIdx = body.length - 2; // second-to-last (before trailing empty line)
|
||||||
const taglineRightPad = innerWidth - pad - tagline.length;
|
const taglineRightPad = innerWidth - pad - tagline.length;
|
||||||
body[taglineIdx] = `${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`;
|
body[taglineIdx] =
|
||||||
|
`${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`;
|
||||||
|
|
||||||
console.log(topBorder);
|
console.log(topBorder);
|
||||||
for (const line of body) console.log(line);
|
for (const line of body) console.log(line);
|
||||||
@@ -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.log(`${brand(sym("◆", "*"))} ${message}`);
|
console.error(`${brand(sym("◆", "*"))} ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function printScope(ids: Record<string, string | undefined>): void {
|
export function printScope(ids: Record<string, string | undefined>): void {
|
||||||
@@ -102,7 +102,7 @@ export function printScope(ids: Record<string, string | undefined>): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (parts.length > 0) {
|
if (parts.length > 0) {
|
||||||
console.log(` ${dim(`Scope: ${parts.join(", ")}`)}`);
|
console.error(` ${dim(`Scope: ${parts.join(", ")}`)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +120,11 @@ export async function timedStatus<T>(
|
|||||||
fn: (ctx: TimedStatusContext) => Promise<T>,
|
fn: (ctx: TimedStatusContext) => Promise<T>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
|
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
|
||||||
const spinner = ora({ text: dim(message), color: "magenta", stream: process.stderr }).start();
|
const spinner = ora({
|
||||||
|
text: dim(message),
|
||||||
|
color: "magenta",
|
||||||
|
stream: process.stderr,
|
||||||
|
}).start();
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Table from "cli-table3";
|
import Table from "cli-table3";
|
||||||
import { printError, printSuccess, colors } from "../branding.js";
|
import { colors, printError, printSuccess } from "../branding.js";
|
||||||
import {
|
import {
|
||||||
getNestedValue,
|
getNestedValue,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
@@ -48,7 +48,10 @@ export function cmdConfigShow(opts: { output?: string } = {}): void {
|
|||||||
|
|
||||||
// Defaults
|
// Defaults
|
||||||
table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]);
|
table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]);
|
||||||
table.push(["defaults.agent_id", config.defaults.agentId || dim("(not set)")]);
|
table.push([
|
||||||
|
"defaults.agent_id",
|
||||||
|
config.defaults.agentId || dim("(not set)"),
|
||||||
|
]);
|
||||||
table.push(["defaults.app_id", config.defaults.appId || dim("(not set)")]);
|
table.push(["defaults.app_id", config.defaults.appId || dim("(not set)")]);
|
||||||
table.push(["defaults.run_id", config.defaults.runId || dim("(not set)")]);
|
table.push(["defaults.run_id", config.defaults.runId || dim("(not set)")]);
|
||||||
table.push(["defaults.enable_graph", String(config.defaults.enableGraph)]);
|
table.push(["defaults.enable_graph", String(config.defaults.enableGraph)]);
|
||||||
|
|||||||
@@ -4,8 +4,14 @@
|
|||||||
|
|
||||||
import readline from "node:readline";
|
import readline from "node:readline";
|
||||||
import Table from "cli-table3";
|
import Table from "cli-table3";
|
||||||
import { printError, printInfo, printSuccess, timedStatus, colors } from "../branding.js";
|
|
||||||
import type { Backend } from "../backend/base.js";
|
import type { Backend } from "../backend/base.js";
|
||||||
|
import {
|
||||||
|
colors,
|
||||||
|
printError,
|
||||||
|
printInfo,
|
||||||
|
printSuccess,
|
||||||
|
timedStatus,
|
||||||
|
} from "../branding.js";
|
||||||
import { formatJson } from "../output.js";
|
import { formatJson } from "../output.js";
|
||||||
|
|
||||||
const { brand, accent, dim } = colors;
|
const { brand, accent, dim } = colors;
|
||||||
@@ -18,7 +24,9 @@ export async function cmdEntitiesList(
|
|||||||
opts: { output: string },
|
opts: { output: string },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!VALID_TYPES.has(entityType)) {
|
if (!VALID_TYPES.has(entityType)) {
|
||||||
printError(`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`);
|
printError(
|
||||||
|
`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`,
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +68,9 @@ export async function cmdEntitiesList(
|
|||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
console.log(table.toString());
|
console.log(table.toString());
|
||||||
console.log(` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`);
|
console.log(
|
||||||
|
` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`,
|
||||||
|
);
|
||||||
console.log();
|
console.log();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +87,9 @@ export async function cmdEntitiesDelete(
|
|||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) {
|
if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) {
|
||||||
printError("Provide at least one of --user-id, --agent-id, --app-id, --run-id.");
|
printError(
|
||||||
|
"Provide at least one of --user-id, --agent-id, --app-id, --run-id.",
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +99,9 @@ export async function cmdEntitiesDelete(
|
|||||||
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
|
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
|
||||||
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
|
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
|
||||||
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
||||||
printInfo(`Would delete entity ${scopeParts.join(", ")} and all its memories.`);
|
printInfo(
|
||||||
|
`Would delete entity ${scopeParts.join(", ")} and all its memories.`,
|
||||||
|
);
|
||||||
printInfo("No changes made.");
|
printInfo("No changes made.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -100,7 +114,10 @@ export async function cmdEntitiesDelete(
|
|||||||
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
||||||
const scope = scopeParts.join(", ");
|
const scope = scopeParts.join(", ");
|
||||||
|
|
||||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
const answer = await new Promise<string>((resolve) => {
|
const answer = await new Promise<string>((resolve) => {
|
||||||
rl.question(
|
rl.question(
|
||||||
`\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `,
|
`\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `,
|
||||||
|
|||||||
@@ -3,18 +3,108 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import readline from "node:readline";
|
import readline from "node:readline";
|
||||||
|
import { PlatformBackend } from "../backend/platform.js";
|
||||||
import {
|
import {
|
||||||
|
colors,
|
||||||
printBanner,
|
printBanner,
|
||||||
printError,
|
printError,
|
||||||
printInfo,
|
printInfo,
|
||||||
printSuccess,
|
printSuccess,
|
||||||
colors,
|
|
||||||
} from "../branding.js";
|
} from "../branding.js";
|
||||||
import { type Mem0Config, createDefaultConfig, saveConfig } from "../config.js";
|
import {
|
||||||
import { PlatformBackend } from "../backend/platform.js";
|
DEFAULT_BASE_URL,
|
||||||
|
type Mem0Config,
|
||||||
|
createDefaultConfig,
|
||||||
|
loadConfig,
|
||||||
|
saveConfig,
|
||||||
|
} from "../config.js";
|
||||||
|
|
||||||
const { brand, dim } = colors;
|
const { brand, dim } = colors;
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -70,7 +160,10 @@ function promptSecret(label: string): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function promptLine(label: string, defaultValue?: string): Promise<string> {
|
function promptLine(label: string, defaultValue?: string): Promise<string> {
|
||||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `;
|
const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `;
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
rl.question(prompt, (answer) => {
|
rl.question(prompt, (answer) => {
|
||||||
@@ -82,7 +175,9 @@ function promptLine(label: string, defaultValue?: string): Promise<string> {
|
|||||||
|
|
||||||
async function setupPlatform(config: Mem0Config): Promise<void> {
|
async function setupPlatform(config: Mem0Config): Promise<void> {
|
||||||
console.log();
|
console.log();
|
||||||
console.log(` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys")}`);
|
console.log(
|
||||||
|
` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys")}`,
|
||||||
|
);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
process.stdout.write(` ${brand("API Key")}: `);
|
process.stdout.write(` ${brand("API Key")}: `);
|
||||||
@@ -98,7 +193,10 @@ async function setupDefaults(config: Mem0Config): Promise<void> {
|
|||||||
console.log();
|
console.log();
|
||||||
printInfo("Set default entity IDs (press Enter to skip).\n");
|
printInfo("Set default entity IDs (press Enter to skip).\n");
|
||||||
|
|
||||||
const userId = await promptLine(` ${brand("Default User ID")} ${dim("(recommended)")}`, "mem0-cli");
|
const userId = await promptLine(
|
||||||
|
` ${brand("Default User ID")} ${dim("(recommended)")}`,
|
||||||
|
"mem0-cli",
|
||||||
|
);
|
||||||
if (userId) config.defaults.userId = userId;
|
if (userId) config.defaults.userId = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,8 +222,66 @@ async function validatePlatform(config: Mem0Config): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runInit(opts: { apiKey?: string; userId?: string } = {}): Promise<void> {
|
export async function runInit(
|
||||||
|
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) {
|
||||||
@@ -154,6 +310,51 @@ export async function runInit(opts: { apiKey?: string; userId?: string } = {}):
|
|||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+138
-25
@@ -3,11 +3,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
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,
|
||||||
@@ -51,7 +58,9 @@ export async function cmdAdd(
|
|||||||
try {
|
try {
|
||||||
msgs = JSON.parse(opts.messages);
|
msgs = JSON.parse(opts.messages);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
printError(`Invalid JSON in --messages: ${e instanceof Error ? e.message : e}`);
|
printError(
|
||||||
|
`Invalid JSON in --messages: ${e instanceof Error ? e.message : e}`,
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,10 +69,30 @@ export async function cmdAdd(
|
|||||||
content = fs.readFileSync(0, "utf-8").trim();
|
content = fs.readFileSync(0, "utf-8").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content && !msgs) {
|
if (content !== undefined && content.trim() === "") {
|
||||||
printError("No content provided. Pass text, --messages, --file, or pipe via stdin.");
|
printError("Content cannot be empty.");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
if (!content && !msgs) {
|
||||||
|
printError(
|
||||||
|
"No content provided. Pass text, --messages, --file, or pipe via stdin.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate --expires
|
||||||
|
if (opts.expires) {
|
||||||
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(opts.expires)) {
|
||||||
|
printError(
|
||||||
|
"Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (new Date(opts.expires) <= new Date()) {
|
||||||
|
printError("--expires date must be in the future.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let meta: Record<string, unknown> | undefined;
|
let meta: Record<string, unknown> | undefined;
|
||||||
if (opts.metadata) {
|
if (opts.metadata) {
|
||||||
@@ -113,10 +142,19 @@ export async function cmdAdd(
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
printScope({ user_id: opts.userId, agent_id: opts.agentId, app_id: opts.appId, run_id: opts.runId });
|
printScope({
|
||||||
const results = Array.isArray(result) ? result : ((result.results as unknown[]) ?? [result]);
|
user_id: opts.userId,
|
||||||
|
agent_id: opts.agentId,
|
||||||
|
app_id: opts.appId,
|
||||||
|
run_id: opts.runId,
|
||||||
|
});
|
||||||
|
const results = Array.isArray(result)
|
||||||
|
? result
|
||||||
|
: ((result.results as unknown[]) ?? [result]);
|
||||||
const count = results.length;
|
const count = results.length;
|
||||||
printSuccess(`Memory processed — ${count} memor${count === 1 ? "y" : "ies"} extracted`);
|
printSuccess(
|
||||||
|
`Memory processed — ${count} memor${count === 1 ? "y" : "ies"} extracted`,
|
||||||
|
);
|
||||||
formatAddResult(result, opts.output);
|
formatAddResult(result, opts.output);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,12 +191,24 @@ export async function cmdSearch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldList = opts.fields ? opts.fields.split(",").map((f) => f.trim()) : undefined;
|
const fieldList = opts.fields
|
||||||
|
? opts.fields.split(",").map((f) => f.trim())
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (opts.topK < 1) {
|
||||||
|
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,
|
||||||
@@ -179,12 +229,18 @@ 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);
|
formatMemoriesTable(results, { showScore: true });
|
||||||
printResultSummary({ count: results.length, durationSecs: elapsed, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } });
|
printResultSummary({
|
||||||
|
count: results.length,
|
||||||
|
durationSecs: elapsed,
|
||||||
|
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log();
|
console.log();
|
||||||
printInfo("No memories found matching your query.");
|
printInfo("No memories found matching your query.");
|
||||||
@@ -193,7 +249,11 @@ export async function cmdSearch(
|
|||||||
} else {
|
} else {
|
||||||
if (results.length > 0) {
|
if (results.length > 0) {
|
||||||
formatMemoriesText(results);
|
formatMemoriesText(results);
|
||||||
printResultSummary({ count: results.length, durationSecs: elapsed, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } });
|
printResultSummary({
|
||||||
|
count: results.length,
|
||||||
|
durationSecs: elapsed,
|
||||||
|
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log();
|
console.log();
|
||||||
printInfo("No memories found matching your query.");
|
printInfo("No memories found matching your query.");
|
||||||
@@ -236,6 +296,15 @@ 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 {
|
||||||
@@ -259,12 +328,24 @@ 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") {
|
||||||
formatJson(results);
|
formatJsonEnvelope({
|
||||||
|
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({ count: results.length, durationSecs: elapsed, page: opts.page, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } });
|
printResultSummary({
|
||||||
|
count: results.length,
|
||||||
|
durationSecs: elapsed,
|
||||||
|
page: opts.page,
|
||||||
|
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log();
|
console.log();
|
||||||
printInfo("No memories found.");
|
printInfo("No memories found.");
|
||||||
@@ -273,7 +354,12 @@ export async function cmdList(
|
|||||||
} else {
|
} else {
|
||||||
if (results.length > 0) {
|
if (results.length > 0) {
|
||||||
formatMemoriesText(results, "memories");
|
formatMemoriesText(results, "memories");
|
||||||
printResultSummary({ count: results.length, durationSecs: elapsed, page: opts.page, scopeIds: { user_id: opts.userId, agent_id: opts.agentId } });
|
printResultSummary({
|
||||||
|
count: results.length,
|
||||||
|
durationSecs: elapsed,
|
||||||
|
page: opts.page,
|
||||||
|
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log();
|
console.log();
|
||||||
printInfo("No memories found.");
|
printInfo("No memories found.");
|
||||||
@@ -313,7 +399,9 @@ export async function cmdUpdate(
|
|||||||
if (opts.output === "json") {
|
if (opts.output === "json") {
|
||||||
formatJson(result);
|
formatJson(result);
|
||||||
} else if (opts.output !== "quiet") {
|
} else if (opts.output !== "quiet") {
|
||||||
printSuccess(`Memory ${memoryId.slice(0, 8)} updated (${elapsed.toFixed(2)}s)`);
|
printSuccess(
|
||||||
|
`Memory ${memoryId.slice(0, 8)} updated (${elapsed.toFixed(2)}s)`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,7 +439,9 @@ export async function cmdDelete(
|
|||||||
if (opts.output === "json") {
|
if (opts.output === "json") {
|
||||||
formatJson(result);
|
formatJson(result);
|
||||||
} else if (opts.output !== "quiet") {
|
} else if (opts.output !== "quiet") {
|
||||||
printSuccess(`Memory ${memoryId.slice(0, 8)} deleted (${elapsed.toFixed(2)}s)`);
|
printSuccess(
|
||||||
|
`Memory ${memoryId.slice(0, 8)} deleted (${elapsed.toFixed(2)}s)`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,15 +462,24 @@ export async function cmdDeleteAll(
|
|||||||
// Project-wide wipe using wildcard entity IDs
|
// Project-wide wipe using wildcard entity IDs
|
||||||
if (opts.dryRun) {
|
if (opts.dryRun) {
|
||||||
printInfo("Would delete ALL memories project-wide.");
|
printInfo("Would delete ALL memories project-wide.");
|
||||||
|
printInfo(
|
||||||
|
"Run without --dry-run to see the actual count of deleted memories.",
|
||||||
|
);
|
||||||
printInfo("No changes made.");
|
printInfo("No changes made.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!opts.force) {
|
if (!opts.force) {
|
||||||
const readline = await import("node:readline");
|
const readline = await import("node:readline");
|
||||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
const answer = await new Promise<string>((resolve) => {
|
const answer = await new Promise<string>((resolve) => {
|
||||||
rl.question(`\n \u26a0 Delete ALL memories across the ENTIRE project? This cannot be undone. [y/N] `, resolve);
|
rl.question(
|
||||||
|
"\n \u26a0 Delete ALL memories across the ENTIRE project? This cannot be undone. [y/N] ",
|
||||||
|
resolve,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
rl.close();
|
rl.close();
|
||||||
if (answer.toLowerCase() !== "y") {
|
if (answer.toLowerCase() !== "y") {
|
||||||
@@ -392,7 +491,9 @@ export async function cmdDeleteAll(
|
|||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
let result: Record<string, unknown>;
|
let result: Record<string, unknown>;
|
||||||
try {
|
try {
|
||||||
result = await timedStatus("Deleting all memories project-wide...", async () => {
|
result = await timedStatus(
|
||||||
|
"Deleting all memories project-wide...",
|
||||||
|
async () => {
|
||||||
return backend.delete(undefined, {
|
return backend.delete(undefined, {
|
||||||
all: true,
|
all: true,
|
||||||
userId: "*",
|
userId: "*",
|
||||||
@@ -400,7 +501,8 @@ export async function cmdDeleteAll(
|
|||||||
appId: "*",
|
appId: "*",
|
||||||
runId: "*",
|
runId: "*",
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
printError(e instanceof Error ? e.message : String(e));
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -411,7 +513,9 @@ export async function cmdDeleteAll(
|
|||||||
formatJson(result);
|
formatJson(result);
|
||||||
} else if (opts.output !== "quiet") {
|
} else if (opts.output !== "quiet") {
|
||||||
if (result.message) {
|
if (result.message) {
|
||||||
printInfo("Deletion started. Memories will be removed in the background.");
|
printInfo(
|
||||||
|
"Deletion started. Memories will be removed in the background.",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
printSuccess(`All project memories deleted (${elapsed.toFixed(2)}s)`);
|
printSuccess(`All project memories deleted (${elapsed.toFixed(2)}s)`);
|
||||||
}
|
}
|
||||||
@@ -443,12 +547,19 @@ export async function cmdDeleteAll(
|
|||||||
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
|
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
|
||||||
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
|
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
|
||||||
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
||||||
const scope = scopeParts.length > 0 ? scopeParts.join(", ") : "ALL entities";
|
const scope =
|
||||||
|
scopeParts.length > 0 ? scopeParts.join(", ") : "ALL entities";
|
||||||
|
|
||||||
const readline = await import("node:readline");
|
const readline = await import("node:readline");
|
||||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
const answer = await new Promise<string>((resolve) => {
|
const answer = await new Promise<string>((resolve) => {
|
||||||
rl.question(`\n \u26a0 Delete ALL memories for ${scope}? This cannot be undone. [y/N] `, resolve);
|
rl.question(
|
||||||
|
`\n \u26a0 Delete ALL memories for ${scope}? This cannot be undone. [y/N] `,
|
||||||
|
resolve,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
rl.close();
|
rl.close();
|
||||||
if (answer.toLowerCase() !== "y") {
|
if (answer.toLowerCase() !== "y") {
|
||||||
@@ -479,7 +590,9 @@ export async function cmdDeleteAll(
|
|||||||
formatJson(result);
|
formatJson(result);
|
||||||
} else if (opts.output !== "quiet") {
|
} else if (opts.output !== "quiet") {
|
||||||
if (result.message) {
|
if (result.message) {
|
||||||
printInfo("Deletion started. Memories will be removed in the background.");
|
printInfo(
|
||||||
|
"Deletion started. Memories will be removed in the background.",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
printSuccess(`All matching memories deleted (${elapsed.toFixed(2)}s)`);
|
printSuccess(`All matching memories deleted (${elapsed.toFixed(2)}s)`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { createRequire } from "node:module";
|
|
||||||
import { printError, printSuccess, timedStatus, colors } from "../branding.js";
|
|
||||||
import type { Backend } from "../backend/base.js";
|
|
||||||
import { formatJsonEnvelope } from "../output.js";
|
|
||||||
import boxen from "boxen";
|
import boxen from "boxen";
|
||||||
|
import type { Backend } from "../backend/base.js";
|
||||||
|
import { colors, printError, printSuccess, timedStatus } from "../branding.js";
|
||||||
|
import { formatJsonEnvelope } from "../output.js";
|
||||||
|
import { CLI_VERSION } from "../version.js";
|
||||||
|
|
||||||
const { brand, dim, success, error: errorColor } = colors;
|
const { brand, dim, success, error: errorColor } = colors;
|
||||||
|
|
||||||
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 } = {},
|
||||||
@@ -25,7 +22,10 @@ export async function cmdStatus(
|
|||||||
return backend.status({ userId: opts.userId, agentId: opts.agentId });
|
return backend.status({ userId: opts.userId, agentId: opts.agentId });
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result = { connected: false, error: e instanceof Error ? e.message : String(e) };
|
result = {
|
||||||
|
connected: false,
|
||||||
|
error: e instanceof Error ? e.message : String(e),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const elapsed = (performance.now() - start) / 1000;
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ export async function cmdStatus(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function cmdVersion(): void {
|
export function cmdVersion(): void {
|
||||||
console.log(` ${brand("◆ Mem0")} CLI v${VERSION}`);
|
console.log(` ${brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cmdImport(
|
export async function cmdImport(
|
||||||
@@ -116,7 +116,9 @@ export async function cmdImport(
|
|||||||
|
|
||||||
// Simple progress indicator
|
// Simple progress indicator
|
||||||
if ((i + 1) % 10 === 0 || i === data.length - 1) {
|
if ((i + 1) % 10 === 0 || i === data.length - 1) {
|
||||||
process.stdout.write(`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`);
|
process.stdout.write(
|
||||||
|
`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +128,11 @@ export async function cmdImport(
|
|||||||
if (opts.output === "json") {
|
if (opts.output === "json") {
|
||||||
formatJsonEnvelope({
|
formatJsonEnvelope({
|
||||||
command: "import",
|
command: "import",
|
||||||
data: { added, failed, duration_s: parseFloat(elapsed.toFixed(2)) },
|
data: {
|
||||||
|
added,
|
||||||
|
failed,
|
||||||
|
duration_s: Number.parseFloat(elapsed.toFixed(2)),
|
||||||
|
},
|
||||||
durationMs: Math.round(elapsed * 1000),
|
durationMs: Math.round(elapsed * 1000),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|||||||
+24
-8
@@ -80,10 +80,14 @@ export function loadConfig(): Mem0Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Environment variable overrides
|
// Environment variable overrides
|
||||||
if (process.env.MEM0_API_KEY) config.platform.apiKey = process.env.MEM0_API_KEY;
|
if (process.env.MEM0_API_KEY)
|
||||||
if (process.env.MEM0_BASE_URL) config.platform.baseUrl = process.env.MEM0_BASE_URL;
|
config.platform.apiKey = process.env.MEM0_API_KEY;
|
||||||
if (process.env.MEM0_USER_ID) config.defaults.userId = process.env.MEM0_USER_ID;
|
if (process.env.MEM0_BASE_URL)
|
||||||
if (process.env.MEM0_AGENT_ID) config.defaults.agentId = process.env.MEM0_AGENT_ID;
|
config.platform.baseUrl = process.env.MEM0_BASE_URL;
|
||||||
|
if (process.env.MEM0_USER_ID)
|
||||||
|
config.defaults.userId = process.env.MEM0_USER_ID;
|
||||||
|
if (process.env.MEM0_AGENT_ID)
|
||||||
|
config.defaults.agentId = process.env.MEM0_AGENT_ID;
|
||||||
if (process.env.MEM0_APP_ID) config.defaults.appId = process.env.MEM0_APP_ID;
|
if (process.env.MEM0_APP_ID) config.defaults.appId = process.env.MEM0_APP_ID;
|
||||||
if (process.env.MEM0_RUN_ID) config.defaults.runId = process.env.MEM0_RUN_ID;
|
if (process.env.MEM0_RUN_ID) config.defaults.runId = process.env.MEM0_RUN_ID;
|
||||||
if (process.env.MEM0_ENABLE_GRAPH) {
|
if (process.env.MEM0_ENABLE_GRAPH) {
|
||||||
@@ -119,8 +123,8 @@ export function saveConfig(config: Mem0Config): void {
|
|||||||
|
|
||||||
export function redactKey(key: string): string {
|
export function redactKey(key: string): string {
|
||||||
if (!key) return "(not set)";
|
if (!key) return "(not set)";
|
||||||
if (key.length <= 8) return key.slice(0, 2) + "***";
|
if (key.length <= 8) return `${key.slice(0, 2)}***`;
|
||||||
return key.slice(0, 4) + "..." + key.slice(-4);
|
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Key map from dotted config path to the config object fields. */
|
/** Key map from dotted config path to the config object fields. */
|
||||||
@@ -132,6 +136,14 @@ 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 {
|
||||||
@@ -141,7 +153,11 @@ export function getNestedValue(config: Mem0Config, dottedKey: string): unknown {
|
|||||||
return (config[section] as unknown as Record<string, unknown>)[field];
|
return (config[section] as unknown as Record<string, unknown>)[field];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setNestedValue(config: Mem0Config, dottedKey: string, value: string): boolean {
|
export function setNestedValue(
|
||||||
|
config: Mem0Config,
|
||||||
|
dottedKey: string,
|
||||||
|
value: string,
|
||||||
|
): boolean {
|
||||||
const mapping = KEY_MAP[dottedKey];
|
const mapping = KEY_MAP[dottedKey];
|
||||||
if (!mapping) return false;
|
if (!mapping) return false;
|
||||||
const [section, field] = mapping;
|
const [section, field] = mapping;
|
||||||
@@ -151,7 +167,7 @@ export function setNestedValue(config: Mem0Config, dottedKey: string, value: str
|
|||||||
if (typeof current === "boolean") {
|
if (typeof current === "boolean") {
|
||||||
obj[field] = ["true", "1", "yes"].includes(value.toLowerCase());
|
obj[field] = ["true", "1", "yes"].includes(value.toLowerCase());
|
||||||
} else if (typeof current === "number") {
|
} else if (typeof current === "number") {
|
||||||
obj[field] = parseInt(value, 10);
|
obj[field] = Number.parseInt(value, 10);
|
||||||
} else {
|
} else {
|
||||||
obj[field] = value;
|
obj[field] = value;
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
/** Injected by tsup at build time from package.json version field. Undefined in dev/test. */
|
||||||
|
declare const __CLI_VERSION__: string | undefined;
|
||||||
+21
-17
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import type { Command, Help, Option, Argument } from "commander";
|
import type { Argument, Command, Help, Option } from "commander";
|
||||||
// Colors imported from chalk directly to match Typer/Rich defaults
|
// Colors imported from chalk directly to match Typer/Rich defaults
|
||||||
|
|
||||||
// ── Colors (matching Typer/Rich defaults) ────────────────────────────────
|
// ── Colors (matching Typer/Rich defaults) ────────────────────────────────
|
||||||
@@ -19,7 +19,7 @@ const dimBorder = chalk.dim; // panel borders
|
|||||||
|
|
||||||
// ── Strip ANSI ───────────────────────────────────────────────────────────
|
// ── Strip ANSI ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// eslint-disable-next-line no-control-regex
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequence is intentional
|
||||||
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
||||||
|
|
||||||
function stripAnsi(str: string): number {
|
function stripAnsi(str: string): number {
|
||||||
@@ -141,11 +141,7 @@ const PANEL_ORDER: string[] = [
|
|||||||
* ╰────────────────────────────────╯
|
* ╰────────────────────────────────╯
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
function renderPanel(
|
function renderPanel(title: string, rows: string[], width: number): string {
|
||||||
title: string,
|
|
||||||
rows: string[],
|
|
||||||
width: number,
|
|
||||||
): string {
|
|
||||||
if (rows.length === 0) return "";
|
if (rows.length === 0) return "";
|
||||||
|
|
||||||
// Inner width is total width minus the two border chars
|
// Inner width is total width minus the two border chars
|
||||||
@@ -161,13 +157,14 @@ function renderPanel(
|
|||||||
dimBorder("╮");
|
dimBorder("╮");
|
||||||
|
|
||||||
// Bottom border: ╰─...─╯
|
// Bottom border: ╰─...─╯
|
||||||
const bottomLine = dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯");
|
const bottomLine =
|
||||||
|
dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯");
|
||||||
|
|
||||||
// Content rows
|
// Content rows
|
||||||
const contentLines = rows.map((row) => {
|
const contentLines = rows.map((row) => {
|
||||||
const visLen = stripAnsi(row);
|
const visLen = stripAnsi(row);
|
||||||
const pad = Math.max(0, inner - 1 - visLen);
|
const pad = Math.max(0, inner - 1 - visLen);
|
||||||
return dimBorder("│") + " " + row + " ".repeat(pad) + dimBorder("│");
|
return `${dimBorder("│")} ${row}${" ".repeat(pad)}${dimBorder("│")}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
return [topLine, ...contentLines, bottomLine].join("\n");
|
return [topLine, ...contentLines, bottomLine].join("\n");
|
||||||
@@ -185,7 +182,7 @@ function formatOptionTerm(opt: Option): string {
|
|||||||
if (opt.flags) {
|
if (opt.flags) {
|
||||||
const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/);
|
const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/);
|
||||||
if (match) {
|
if (match) {
|
||||||
term += " " + match[0];
|
term += ` ${match[0]}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return term;
|
return term;
|
||||||
@@ -220,7 +217,9 @@ export function richFormatHelp(cmd: Command, helper: Help): string {
|
|||||||
lines.push("");
|
lines.push("");
|
||||||
if (isRoot) {
|
if (isRoot) {
|
||||||
// Root: "Usage: mem0 <command> [options]" — <command> yellow, [options] bold
|
// Root: "Usage: mem0 <command> [options]" — <command> yellow, [options] bold
|
||||||
lines.push(` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`);
|
lines.push(
|
||||||
|
` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Subcommands: split into command path (bold) and args (yellow)
|
// Subcommands: split into command path (bold) and args (yellow)
|
||||||
const usageParts = usage.split(" ");
|
const usageParts = usage.split(" ");
|
||||||
@@ -235,7 +234,9 @@ export function richFormatHelp(cmd: Command, helper: Help): string {
|
|||||||
argParts.push(part);
|
argParts.push(part);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines.push(` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`);
|
lines.push(
|
||||||
|
` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
@@ -260,7 +261,9 @@ export function richFormatHelp(cmd: Command, helper: Help): string {
|
|||||||
if (!isRoot) {
|
if (!isRoot) {
|
||||||
const visibleArgs = helper.visibleArguments(cmd);
|
const visibleArgs = helper.visibleArguments(cmd);
|
||||||
if (visibleArgs.length > 0) {
|
if (visibleArgs.length > 0) {
|
||||||
const maxLen = Math.max(...visibleArgs.map((a: Argument) => a.name().length));
|
const maxLen = Math.max(
|
||||||
|
...visibleArgs.map((a: Argument) => a.name().length),
|
||||||
|
);
|
||||||
const argRows = visibleArgs.map((a: Argument) => {
|
const argRows = visibleArgs.map((a: Argument) => {
|
||||||
const name = cyanBold(a.name().padEnd(maxLen));
|
const name = cyanBold(a.name().padEnd(maxLen));
|
||||||
const description = helper.argumentDescription(a);
|
const description = helper.argumentDescription(a);
|
||||||
@@ -274,7 +277,8 @@ export function richFormatHelp(cmd: Command, helper: Help): string {
|
|||||||
// ── Collect options (grouped into panels for subcommands) ──
|
// ── Collect options (grouped into panels for subcommands) ──
|
||||||
const visibleOpts = helper.visibleOptions(cmd);
|
const visibleOpts = helper.visibleOptions(cmd);
|
||||||
const cmdName = cmd.name();
|
const cmdName = cmd.name();
|
||||||
const panelMap = (!isRoot && OPTION_PANELS[cmdName]) ? OPTION_PANELS[cmdName] : {};
|
const panelMap =
|
||||||
|
!isRoot && OPTION_PANELS[cmdName] ? OPTION_PANELS[cmdName] : {};
|
||||||
|
|
||||||
const grouped: Record<string, Option[]> = { Options: [] };
|
const grouped: Record<string, Option[]> = { Options: [] };
|
||||||
for (const panelName of PANEL_ORDER) {
|
for (const panelName of PANEL_ORDER) {
|
||||||
@@ -287,7 +291,7 @@ export function richFormatHelp(cmd: Command, helper: Help): string {
|
|||||||
if (panel && PANEL_ORDER.includes(panel)) {
|
if (panel && PANEL_ORDER.includes(panel)) {
|
||||||
grouped[panel].push(opt);
|
grouped[panel].push(opt);
|
||||||
} else {
|
} else {
|
||||||
grouped["Options"].push(opt);
|
grouped.Options.push(opt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,8 +300,8 @@ export function richFormatHelp(cmd: Command, helper: Help): string {
|
|||||||
|
|
||||||
if (isRoot) {
|
if (isRoot) {
|
||||||
// ROOT: Options first, then command groups (matches Python/Typer ordering)
|
// ROOT: Options first, then command groups (matches Python/Typer ordering)
|
||||||
if (grouped["Options"].length > 0) {
|
if (grouped.Options.length > 0) {
|
||||||
const optRows = formatOptionRows(grouped["Options"]);
|
const optRows = formatOptionRows(grouped.Options);
|
||||||
const panel = renderPanel("Options", optRows, width);
|
const panel = renderPanel("Options", optRows, width);
|
||||||
if (panel) lines.push(panel);
|
if (panel) lines.push(panel);
|
||||||
}
|
}
|
||||||
|
|||||||
+174
-44
@@ -7,16 +7,13 @@
|
|||||||
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();
|
||||||
|
|
||||||
@@ -62,7 +59,12 @@ function resolveIds(
|
|||||||
runId?: string;
|
runId?: string;
|
||||||
},
|
},
|
||||||
): { userId?: string; agentId?: string; appId?: string; runId?: string } {
|
): { userId?: string; agentId?: string; appId?: string; runId?: string } {
|
||||||
const hasExplicit = !!(opts.userId || opts.agentId || opts.appId || opts.runId);
|
const hasExplicit = !!(
|
||||||
|
opts.userId ||
|
||||||
|
opts.agentId ||
|
||||||
|
opts.appId ||
|
||||||
|
opts.runId
|
||||||
|
);
|
||||||
if (hasExplicit) {
|
if (hasExplicit) {
|
||||||
return {
|
return {
|
||||||
userId: opts.userId || undefined,
|
userId: opts.userId || undefined,
|
||||||
@@ -95,10 +97,12 @@ function resolveGraph(
|
|||||||
|
|
||||||
program
|
program
|
||||||
.name("mem0")
|
.name("mem0")
|
||||||
.description(`◆ Mem0 CLI v${VERSION} · Node.js SDK\n\nThe Memory Layer for AI Agents`)
|
.description(
|
||||||
|
`◆ Mem0 CLI v${CLI_VERSION} · Node.js SDK\n\nThe Memory Layer for AI Agents`,
|
||||||
|
)
|
||||||
.option("--version", "Show version and exit.")
|
.option("--version", "Show version and exit.")
|
||||||
.on("option:version", () => {
|
.on("option:version", () => {
|
||||||
console.log(` ${colors.brand("◆ Mem0")} CLI v${VERSION}`);
|
console.log(` ${colors.brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
})
|
})
|
||||||
.usage("<command> [options]")
|
.usage("<command> [options]")
|
||||||
@@ -110,13 +114,28 @@ program
|
|||||||
|
|
||||||
program
|
program
|
||||||
.command("init")
|
.command("init")
|
||||||
.description("Interactive setup wizard for mem0 CLI.")
|
.description(
|
||||||
|
"Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
|
||||||
|
)
|
||||||
.option("--api-key <key>", "API key (skip prompt).")
|
.option("--api-key <key>", "API key (skip prompt).")
|
||||||
.option("-u, --user-id <id>", "Default user ID (skip prompt).")
|
.option("-u, --user-id <id>", "Default user ID (skip prompt).")
|
||||||
.addHelpText("after", "\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice")
|
.option("--email <email>", "Login via email verification code.")
|
||||||
|
.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({ apiKey: opts.apiKey, userId: opts.userId });
|
await runInit({
|
||||||
|
apiKey: opts.apiKey,
|
||||||
|
userId: opts.userId,
|
||||||
|
email: opts.email,
|
||||||
|
code: opts.code,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Memory: add ───────────────────────────────────────────────────────────
|
// ── Memory: add ───────────────────────────────────────────────────────────
|
||||||
@@ -140,7 +159,10 @@ program
|
|||||||
.option("-o, --output <format>", "Output format: text, json, quiet.", "text")
|
.option("-o, --output <format>", "Output format: text, json, quiet.", "text")
|
||||||
.option("--api-key <key>", "Override API key.")
|
.option("--api-key <key>", "Override API key.")
|
||||||
.option("--base-url <url>", "Override API base URL.")
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
.addHelpText("after", '\nExamples:\n $ mem0 add "I prefer dark mode" --user-id alice\n $ echo "text" | mem0 add -u alice\n $ mem0 add --file msgs.json -u alice -o json')
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
'\nExamples:\n $ mem0 add "I prefer dark mode" --user-id alice\n $ echo "text" | mem0 add -u alice\n $ mem0 add --file msgs.json -u alice -o json',
|
||||||
|
)
|
||||||
.action(async (text, opts) => {
|
.action(async (text, opts) => {
|
||||||
const { cmdAdd } = await import("./commands/memory.js");
|
const { cmdAdd } = await import("./commands/memory.js");
|
||||||
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
||||||
@@ -158,8 +180,18 @@ program
|
|||||||
.option("--agent-id <id>", "Filter by agent.")
|
.option("--agent-id <id>", "Filter by agent.")
|
||||||
.option("--app-id <id>", "Filter by app.")
|
.option("--app-id <id>", "Filter by app.")
|
||||||
.option("--run-id <id>", "Filter by run.")
|
.option("--run-id <id>", "Filter by run.")
|
||||||
.option("-k, --top-k <n>", "Number of results.", (v) => parseInt(v), 10)
|
.option(
|
||||||
.option("--threshold <n>", "Minimum similarity score.", (v) => parseFloat(v), 0.3)
|
"-k, --top-k <n>",
|
||||||
|
"Number of results.",
|
||||||
|
(v) => Number.parseInt(v),
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
.option(
|
||||||
|
"--threshold <n>",
|
||||||
|
"Minimum similarity score.",
|
||||||
|
(v) => Number.parseFloat(v),
|
||||||
|
0.3,
|
||||||
|
)
|
||||||
.option("--rerank", "Enable reranking (Platform only).", false)
|
.option("--rerank", "Enable reranking (Platform only).", false)
|
||||||
.option("--keyword", "Use keyword search.", false)
|
.option("--keyword", "Use keyword search.", false)
|
||||||
.option("--filter <json>", "Advanced filter expression (JSON).")
|
.option("--filter <json>", "Advanced filter expression (JSON).")
|
||||||
@@ -169,7 +201,10 @@ program
|
|||||||
.option("-o, --output <format>", "Output: text, json, table.", "text")
|
.option("-o, --output <format>", "Output: text, json, table.", "text")
|
||||||
.option("--api-key <key>", "Override API key.")
|
.option("--api-key <key>", "Override API key.")
|
||||||
.option("--base-url <url>", "Override API base URL.")
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
.addHelpText("after", '\nExamples:\n $ mem0 search "preferences" --user-id alice\n $ mem0 search "tools" -u alice -o json -k 5\n $ echo "preferences" | mem0 search -u alice')
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
'\nExamples:\n $ mem0 search "preferences" --user-id alice\n $ mem0 search "tools" -u alice -o json -k 5\n $ echo "preferences" | mem0 search -u alice',
|
||||||
|
)
|
||||||
.action(async (query, opts) => {
|
.action(async (query, opts) => {
|
||||||
let resolvedQuery = query;
|
let resolvedQuery = query;
|
||||||
if (!resolvedQuery && !process.stdin.isTTY) {
|
if (!resolvedQuery && !process.stdin.isTTY) {
|
||||||
@@ -204,7 +239,10 @@ program
|
|||||||
.option("-o, --output <format>", "Output: text, json.", "text")
|
.option("-o, --output <format>", "Output: text, json.", "text")
|
||||||
.option("--api-key <key>", "Override API key.")
|
.option("--api-key <key>", "Override API key.")
|
||||||
.option("--base-url <url>", "Override API base URL.")
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
.addHelpText("after", "\nExamples:\n $ mem0 get abc-123-def-456\n $ mem0 get abc-123-def-456 -o json")
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 get abc-123-def-456\n $ mem0 get abc-123-def-456 -o json",
|
||||||
|
)
|
||||||
.action(async (memoryId, opts) => {
|
.action(async (memoryId, opts) => {
|
||||||
const { cmdGet } = await import("./commands/memory.js");
|
const { cmdGet } = await import("./commands/memory.js");
|
||||||
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
@@ -220,8 +258,13 @@ program
|
|||||||
.option("--agent-id <id>", "Filter by agent.")
|
.option("--agent-id <id>", "Filter by agent.")
|
||||||
.option("--app-id <id>", "Filter by app.")
|
.option("--app-id <id>", "Filter by app.")
|
||||||
.option("--run-id <id>", "Filter by run.")
|
.option("--run-id <id>", "Filter by run.")
|
||||||
.option("--page <n>", "Page number.", (v) => parseInt(v), 1)
|
.option("--page <n>", "Page number.", (v) => Number.parseInt(v), 1)
|
||||||
.option("--page-size <n>", "Results per page.", (v) => parseInt(v), 100)
|
.option(
|
||||||
|
"--page-size <n>",
|
||||||
|
"Results per page.",
|
||||||
|
(v) => Number.parseInt(v),
|
||||||
|
100,
|
||||||
|
)
|
||||||
.option("--category <name>", "Filter by category.")
|
.option("--category <name>", "Filter by category.")
|
||||||
.option("--after <date>", "Created after (YYYY-MM-DD).")
|
.option("--after <date>", "Created after (YYYY-MM-DD).")
|
||||||
.option("--before <date>", "Created before (YYYY-MM-DD).")
|
.option("--before <date>", "Created before (YYYY-MM-DD).")
|
||||||
@@ -230,7 +273,10 @@ program
|
|||||||
.option("-o, --output <format>", "Output: text, json, table.", "table")
|
.option("-o, --output <format>", "Output: text, json, table.", "table")
|
||||||
.option("--api-key <key>", "Override API key.")
|
.option("--api-key <key>", "Override API key.")
|
||||||
.option("--base-url <url>", "Override API base URL.")
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
.addHelpText("after", "\nExamples:\n $ mem0 list -u alice\n $ mem0 list --category prefs --after 2024-01-01 -o json")
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 list -u alice\n $ mem0 list --category prefs --after 2024-01-01 -o json",
|
||||||
|
)
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
const { cmdList } = await import("./commands/memory.js");
|
const { cmdList } = await import("./commands/memory.js");
|
||||||
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
||||||
@@ -257,7 +303,10 @@ program
|
|||||||
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
||||||
.option("--api-key <key>", "Override API key.")
|
.option("--api-key <key>", "Override API key.")
|
||||||
.option("--base-url <url>", "Override API base URL.")
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
.addHelpText("after", `\nExamples:\n $ mem0 update abc-123 "new text"\n $ mem0 update abc-123 --metadata '{"key":"val"}'\n $ echo "new text" | mem0 update abc-123`)
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
`\nExamples:\n $ mem0 update abc-123 "new text"\n $ mem0 update abc-123 --metadata '{"key":"val"}'\n $ echo "new text" | mem0 update abc-123`,
|
||||||
|
)
|
||||||
.action(async (memoryId, text, opts) => {
|
.action(async (memoryId, text, opts) => {
|
||||||
let resolvedText = text;
|
let resolvedText = text;
|
||||||
if (!resolvedText && !opts.metadata && !process.stdin.isTTY) {
|
if (!resolvedText && !opts.metadata && !process.stdin.isTTY) {
|
||||||
@@ -265,7 +314,10 @@ program
|
|||||||
}
|
}
|
||||||
const { cmdUpdate } = await import("./commands/memory.js");
|
const { cmdUpdate } = await import("./commands/memory.js");
|
||||||
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
await cmdUpdate(backend, memoryId, resolvedText, { metadata: opts.metadata, output: opts.output });
|
await cmdUpdate(backend, memoryId, resolvedText, {
|
||||||
|
metadata: opts.metadata,
|
||||||
|
output: opts.output,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Memory: delete (consolidated) ─────────────────────────────────────────
|
// ── Memory: delete (consolidated) ─────────────────────────────────────────
|
||||||
@@ -274,7 +326,11 @@ program
|
|||||||
.command("delete [memoryId]")
|
.command("delete [memoryId]")
|
||||||
.description("Delete a memory, all memories matching a scope, or an entity.")
|
.description("Delete a memory, all memories matching a scope, or an entity.")
|
||||||
.option("--all", "Delete all memories matching scope filters.", false)
|
.option("--all", "Delete all memories matching scope filters.", false)
|
||||||
.option("--entity", "Delete the entity itself and all its memories (cascade).", false)
|
.option(
|
||||||
|
"--entity",
|
||||||
|
"Delete the entity itself and all its memories (cascade).",
|
||||||
|
false,
|
||||||
|
)
|
||||||
.option("--project", "With --all: delete ALL memories project-wide.", false)
|
.option("--project", "With --all: delete ALL memories project-wide.", false)
|
||||||
.option("--dry-run", "Show what would be deleted without deleting.", false)
|
.option("--dry-run", "Show what would be deleted without deleting.", false)
|
||||||
.option("--force", "Skip confirmation.", false)
|
.option("--force", "Skip confirmation.", false)
|
||||||
@@ -285,13 +341,16 @@ program
|
|||||||
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
||||||
.option("--api-key <key>", "Override API key.")
|
.option("--api-key <key>", "Override API key.")
|
||||||
.option("--base-url <url>", "Override API base URL.")
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
.addHelpText("after", [
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
[
|
||||||
"\nExamples:",
|
"\nExamples:",
|
||||||
" $ mem0 delete abc-123-def-456 # single memory",
|
" $ mem0 delete abc-123-def-456 # single memory",
|
||||||
" $ mem0 delete --all -u alice --force # all memories for user",
|
" $ mem0 delete --all -u alice --force # all memories for user",
|
||||||
" $ mem0 delete --all --project --force # project-wide wipe",
|
" $ mem0 delete --all --project --force # project-wide wipe",
|
||||||
" $ mem0 delete --entity -u alice --force # entity + all its memories",
|
" $ mem0 delete --entity -u alice --force # entity + all its memories",
|
||||||
].join("\n"))
|
].join("\n"),
|
||||||
|
)
|
||||||
.action(async (memoryId, opts) => {
|
.action(async (memoryId, opts) => {
|
||||||
// ── Mutual-exclusion checks ──
|
// ── Mutual-exclusion checks ──
|
||||||
if (memoryId && opts.all) {
|
if (memoryId && opts.all) {
|
||||||
@@ -299,7 +358,9 @@ program
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
if (memoryId && opts.entity) {
|
if (memoryId && opts.entity) {
|
||||||
printError("Cannot combine <memoryId> with --entity. Use one or the other.");
|
printError(
|
||||||
|
"Cannot combine <memoryId> with --entity. Use one or the other.",
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
if (opts.all && opts.entity) {
|
if (opts.all && opts.entity) {
|
||||||
@@ -320,18 +381,36 @@ program
|
|||||||
if (memoryId) {
|
if (memoryId) {
|
||||||
const { cmdDelete } = await import("./commands/memory.js");
|
const { cmdDelete } = await import("./commands/memory.js");
|
||||||
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
await cmdDelete(backend, memoryId, { output: opts.output, dryRun: opts.dryRun, force: opts.force });
|
await cmdDelete(backend, memoryId, {
|
||||||
|
output: opts.output,
|
||||||
|
dryRun: opts.dryRun,
|
||||||
|
force: opts.force,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Dispatch: --all ──
|
// ── Dispatch: --all ──
|
||||||
if (opts.all) {
|
if (opts.all) {
|
||||||
const { cmdDeleteAll } = await import("./commands/memory.js");
|
const { cmdDeleteAll } = await import("./commands/memory.js");
|
||||||
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
const { backend, config } = getBackendAndConfig(
|
||||||
|
opts.apiKey,
|
||||||
|
opts.baseUrl,
|
||||||
|
);
|
||||||
const ids = opts.project
|
const ids = opts.project
|
||||||
? { userId: undefined, agentId: undefined, appId: undefined, runId: undefined }
|
? {
|
||||||
|
userId: undefined,
|
||||||
|
agentId: undefined,
|
||||||
|
appId: undefined,
|
||||||
|
runId: undefined,
|
||||||
|
}
|
||||||
: resolveIds(config, opts);
|
: resolveIds(config, opts);
|
||||||
await cmdDeleteAll(backend, { force: opts.force, dryRun: opts.dryRun, all: opts.project, ...ids, output: opts.output });
|
await cmdDeleteAll(backend, {
|
||||||
|
force: opts.force,
|
||||||
|
dryRun: opts.dryRun,
|
||||||
|
all: opts.project,
|
||||||
|
...ids,
|
||||||
|
output: opts.output,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,7 +434,10 @@ configCmd
|
|||||||
.command("show")
|
.command("show")
|
||||||
.description("Display current configuration (secrets redacted).")
|
.description("Display current configuration (secrets redacted).")
|
||||||
.option("-o, --output <format>", "Output: text, json.", "text")
|
.option("-o, --output <format>", "Output: text, json.", "text")
|
||||||
.addHelpText("after", "\nExamples:\n $ mem0 config show\n $ mem0 config show -o json")
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 config show\n $ mem0 config show -o json",
|
||||||
|
)
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
const { cmdConfigShow } = await import("./commands/config.js");
|
const { cmdConfigShow } = await import("./commands/config.js");
|
||||||
cmdConfigShow({ output: opts.output });
|
cmdConfigShow({ output: opts.output });
|
||||||
@@ -364,7 +446,10 @@ configCmd
|
|||||||
configCmd
|
configCmd
|
||||||
.command("get <key>")
|
.command("get <key>")
|
||||||
.description("Get a configuration value.")
|
.description("Get a configuration value.")
|
||||||
.addHelpText("after", "\nExamples:\n $ mem0 config get platform.api_key\n $ mem0 config get defaults.user_id")
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 config get platform.api_key\n $ mem0 config get defaults.user_id",
|
||||||
|
)
|
||||||
.action(async (key) => {
|
.action(async (key) => {
|
||||||
const { cmdConfigGet } = await import("./commands/config.js");
|
const { cmdConfigGet } = await import("./commands/config.js");
|
||||||
cmdConfigGet(key);
|
cmdConfigGet(key);
|
||||||
@@ -373,7 +458,10 @@ configCmd
|
|||||||
configCmd
|
configCmd
|
||||||
.command("set <key> <value>")
|
.command("set <key> <value>")
|
||||||
.description("Set a configuration value.")
|
.description("Set a configuration value.")
|
||||||
.addHelpText("after", "\nExamples:\n $ mem0 config set defaults.user_id alice\n $ mem0 config set platform.base_url https://api.mem0.ai")
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 config set defaults.user_id alice\n $ mem0 config set platform.base_url https://api.mem0.ai",
|
||||||
|
)
|
||||||
.action(async (key, value) => {
|
.action(async (key, value) => {
|
||||||
const { cmdConfigSet } = await import("./commands/config.js");
|
const { cmdConfigSet } = await import("./commands/config.js");
|
||||||
cmdConfigSet(key, value);
|
cmdConfigSet(key, value);
|
||||||
@@ -393,7 +481,10 @@ entityCmd
|
|||||||
.option("-o, --output <format>", "Output: table, json.", "table")
|
.option("-o, --output <format>", "Output: table, json.", "table")
|
||||||
.option("--api-key <key>", "Override API key.")
|
.option("--api-key <key>", "Override API key.")
|
||||||
.option("--base-url <url>", "Override API base URL.")
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
.addHelpText("after", "\nExamples:\n $ mem0 entity list users\n $ mem0 entity list agents -o json")
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 entity list users\n $ mem0 entity list agents -o json",
|
||||||
|
)
|
||||||
.action(async (entityType, opts) => {
|
.action(async (entityType, opts) => {
|
||||||
const { cmdEntitiesList } = await import("./commands/entities.js");
|
const { cmdEntitiesList } = await import("./commands/entities.js");
|
||||||
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
@@ -412,7 +503,10 @@ entityCmd
|
|||||||
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
||||||
.option("--api-key <key>", "Override API key.")
|
.option("--api-key <key>", "Override API key.")
|
||||||
.option("--base-url <url>", "Override API base URL.")
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
.addHelpText("after", "\nExamples:\n $ mem0 entity delete --user-id alice --force\n $ mem0 entity delete --user-id alice --dry-run")
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 entity delete --user-id alice --force\n $ mem0 entity delete --user-id alice --dry-run",
|
||||||
|
)
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
const { cmdEntitiesDelete } = await import("./commands/entities.js");
|
const { cmdEntitiesDelete } = await import("./commands/entities.js");
|
||||||
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
@@ -438,7 +532,6 @@ program
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("import <filePath>")
|
.command("import <filePath>")
|
||||||
.description("Import memories from a JSON file.")
|
.description("Import memories from a JSON file.")
|
||||||
@@ -447,19 +540,28 @@ program
|
|||||||
.option("-o, --output <format>", "Output: text, json.", "text")
|
.option("-o, --output <format>", "Output: text, json.", "text")
|
||||||
.option("--api-key <key>", "Override API key.")
|
.option("--api-key <key>", "Override API key.")
|
||||||
.option("--base-url <url>", "Override API base URL.")
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
.addHelpText("after", "\nExamples:\n $ mem0 import data.json --user-id alice\n $ mem0 import data.json -u alice -o json")
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 import data.json --user-id alice\n $ mem0 import data.json -u alice -o json",
|
||||||
|
)
|
||||||
.action(async (filePath, opts) => {
|
.action(async (filePath, opts) => {
|
||||||
const { cmdImport } = await import("./commands/utils.js");
|
const { cmdImport } = await import("./commands/utils.js");
|
||||||
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
||||||
const ids = resolveIds(config, opts);
|
const ids = resolveIds(config, opts);
|
||||||
await cmdImport(backend, filePath, { userId: ids.userId, agentId: ids.agentId, output: opts.output });
|
await cmdImport(backend, filePath, {
|
||||||
|
userId: ids.userId,
|
||||||
|
agentId: ids.agentId,
|
||||||
|
output: opts.output,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Help (machine-readable) ──────────────────────────────────────────────
|
// ── Help (machine-readable) ──────────────────────────────────────────────
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("help")
|
.command("help")
|
||||||
.description("Show help. Use --json for machine-readable output (for LLM agents).")
|
.description(
|
||||||
|
"Show help. Use --json for machine-readable output (for LLM agents).",
|
||||||
|
)
|
||||||
.option("--json", "Output machine-readable JSON for LLM agents.", false)
|
.option("--json", "Output machine-readable JSON for LLM agents.", false)
|
||||||
.addHelpText("after", "\nExamples:\n $ mem0 help\n $ mem0 help --json")
|
.addHelpText("after", "\nExamples:\n $ mem0 help\n $ mem0 help --json")
|
||||||
.action((opts) => {
|
.action((opts) => {
|
||||||
@@ -471,19 +573,35 @@ program
|
|||||||
const spec = JSON.parse(fs.readFileSync(specPath, "utf-8"));
|
const spec = JSON.parse(fs.readFileSync(specPath, "utf-8"));
|
||||||
console.log(JSON.stringify(spec, null, 2));
|
console.log(JSON.stringify(spec, null, 2));
|
||||||
} else {
|
} else {
|
||||||
console.log(JSON.stringify({ name: "mem0", version: VERSION, description: "The Memory Layer for AI Agents" }, null, 2));
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
name: "mem0",
|
||||||
|
version: CLI_VERSION,
|
||||||
|
description: "The Memory Layer for AI Agents",
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const { brand: b } = colors;
|
const { brand: b } = colors;
|
||||||
console.log(`${b("◆ Mem0 CLI")} v${VERSION} · Node.js SDK\n The Memory Layer for AI Agents\n`);
|
console.log(
|
||||||
|
`${b("◆ Mem0 CLI")} v${CLI_VERSION} · Node.js SDK\n The Memory Layer for AI Agents\n`,
|
||||||
|
);
|
||||||
console.log("Usage: mem0 <command> [OPTIONS]\n");
|
console.log("Usage: mem0 <command> [OPTIONS]\n");
|
||||||
console.log("Commands:");
|
console.log("Commands:");
|
||||||
console.log(" add Add a memory from text, messages, file, or stdin");
|
console.log(
|
||||||
|
" add Add a memory from text, messages, file, or stdin",
|
||||||
|
);
|
||||||
console.log(" search Search memories by semantic query");
|
console.log(" search Search memories by semantic query");
|
||||||
console.log(" get Get a specific memory by ID");
|
console.log(" get Get a specific memory by ID");
|
||||||
console.log(" list List memories with optional filters");
|
console.log(" list List memories with optional filters");
|
||||||
console.log(" update Update a memory's text or metadata");
|
console.log(" update Update a memory's text or metadata");
|
||||||
console.log(" delete Delete a memory, all memories, or an entity");
|
console.log(
|
||||||
|
" delete Delete a memory, all memories, or an entity",
|
||||||
|
);
|
||||||
console.log(" import Import memories from a JSON file");
|
console.log(" import Import memories from a JSON file");
|
||||||
console.log(" config Manage configuration (show, get, set)");
|
console.log(" config Manage configuration (show, get, set)");
|
||||||
console.log(" entity Manage entities (list, delete)");
|
console.log(" entity Manage entities (list, delete)");
|
||||||
@@ -491,11 +609,23 @@ program
|
|||||||
console.log(" status Check connectivity and authentication");
|
console.log(" status Check connectivity and authentication");
|
||||||
console.log();
|
console.log();
|
||||||
console.log(" mem0 <command> --help Get help for a command");
|
console.log(" mem0 <command> --help Get help for a command");
|
||||||
console.log(" mem0 help --json Machine-readable help (for LLM agents)");
|
console.log(
|
||||||
|
" mem0 help --json Machine-readable help (for LLM agents)",
|
||||||
|
);
|
||||||
console.log();
|
console.log();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Version ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("version")
|
||||||
|
.description("Show version.")
|
||||||
|
.action(async () => {
|
||||||
|
const { cmdVersion } = await import("./commands/utils.js");
|
||||||
|
cmdVersion();
|
||||||
|
});
|
||||||
|
|
||||||
// ── Entrypoint ────────────────────────────────────────────────────────────
|
// ── Entrypoint ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
program.parse();
|
program.parse();
|
||||||
|
|||||||
+41
-11
@@ -2,8 +2,8 @@
|
|||||||
* Output formatting for mem0 CLI — text, JSON, table, quiet modes.
|
* Output formatting for mem0 CLI — text, JSON, table, quiet modes.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Table from "cli-table3";
|
|
||||||
import boxen from "boxen";
|
import boxen from "boxen";
|
||||||
|
import Table from "cli-table3";
|
||||||
import { colors, sym } from "./branding.js";
|
import { colors, sym } from "./branding.js";
|
||||||
|
|
||||||
const { brand, accent, success, error: errorColor, dim } = colors;
|
const { brand, accent, success, error: errorColor, dim } = colors;
|
||||||
@@ -52,28 +52,49 @@ export function formatMemoriesText(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatMemoriesTable(memories: Record<string, unknown>[]): void {
|
export function formatMemoriesTable(
|
||||||
|
memories: Record<string, unknown>[],
|
||||||
|
opts: { showScore?: boolean } = {},
|
||||||
|
): void {
|
||||||
|
const head = opts.showScore
|
||||||
|
? [
|
||||||
|
accent("ID"),
|
||||||
|
accent("Score"),
|
||||||
|
accent("Memory"),
|
||||||
|
accent("Category"),
|
||||||
|
accent("Created"),
|
||||||
|
]
|
||||||
|
: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")];
|
||||||
|
const colWidths = opts.showScore ? [38, 8, 40, 16, 14] : [38, 40, 16, 14];
|
||||||
const table = new Table({
|
const table = new Table({
|
||||||
head: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")],
|
head,
|
||||||
colWidths: [12, 52, 16, 14],
|
colWidths,
|
||||||
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) ?? "").slice(0, 8);
|
const memId = (mem.id as string) ?? "";
|
||||||
let memoryText = (mem.memory ?? mem.text ?? "") as string;
|
let memoryText = (mem.memory ?? mem.text ?? "") as string;
|
||||||
if (memoryText.length > 60) {
|
if (memoryText.length > 60) {
|
||||||
memoryText = memoryText.slice(0, 57) + "...";
|
memoryText = `${memoryText.slice(0, 57)}...`;
|
||||||
}
|
}
|
||||||
const categories = mem.categories;
|
const categories = mem.categories;
|
||||||
const cat =
|
const cat =
|
||||||
Array.isArray(categories) && categories.length > 0
|
Array.isArray(categories) && categories.length > 0
|
||||||
? (categories[0] as string)
|
? categories.length > 1
|
||||||
|
? `${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());
|
||||||
@@ -109,7 +130,9 @@ export function formatSingleMemory(
|
|||||||
if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`);
|
if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`);
|
||||||
const categories = mem.categories;
|
const categories = mem.categories;
|
||||||
if (categories) {
|
if (categories) {
|
||||||
const catStr = Array.isArray(categories) ? categories.join(", ") : String(categories);
|
const catStr = Array.isArray(categories)
|
||||||
|
? categories.join(", ")
|
||||||
|
: String(categories);
|
||||||
lines.push(` ${dim("Categories:")} ${catStr}`);
|
lines.push(` ${dim("Categories:")} ${catStr}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +174,10 @@ export function formatAddResult(
|
|||||||
if (r.status === "PENDING") {
|
if (r.status === "PENDING") {
|
||||||
const eventId = ((r.event_id as string) ?? "").slice(0, 8);
|
const eventId = ((r.event_id as string) ?? "").slice(0, 8);
|
||||||
const icon = accent(sym("⧗", "..."));
|
const icon = accent(sym("⧗", "..."));
|
||||||
const parts = [` ${icon} ${dim("Queued".padEnd(10))}`, "Processing in background"];
|
const parts = [
|
||||||
|
` ${icon} ${dim("Queued".padEnd(10))}`,
|
||||||
|
"Processing in background",
|
||||||
|
];
|
||||||
if (eventId) parts.push(dim(`(event ${eventId})`));
|
if (eventId) parts.push(dim(`(event ${eventId})`));
|
||||||
console.log(parts.join(" "));
|
console.log(parts.join(" "));
|
||||||
continue;
|
continue;
|
||||||
@@ -159,7 +185,10 @@ export function formatAddResult(
|
|||||||
|
|
||||||
const event = (r.event ?? "ADD") as string;
|
const event = (r.event ?? "ADD") as string;
|
||||||
const memory = (r.memory ?? r.text ?? r.content ?? r.data ?? "") as string;
|
const memory = (r.memory ?? r.text ?? r.content ?? r.data ?? "") as string;
|
||||||
const memId = ((r.id as string) ?? (r.memory_id as string) ?? "").slice(0, 8);
|
const memId = ((r.id as string) ?? (r.memory_id as string) ?? "").slice(
|
||||||
|
0,
|
||||||
|
8,
|
||||||
|
);
|
||||||
|
|
||||||
let icon: string;
|
let icon: string;
|
||||||
let label: string;
|
let label: string;
|
||||||
@@ -223,7 +252,8 @@ export function printResultSummary(opts: {
|
|||||||
.map(([k, v]) => `${k.replace(/_/g, " ")}=${v}`);
|
.map(([k, v]) => `${k.replace(/_/g, " ")}=${v}`);
|
||||||
if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
|
if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
|
||||||
}
|
}
|
||||||
if (opts.durationSecs !== undefined) parts.push(`${opts.durationSecs.toFixed(2)}s`);
|
if (opts.durationSecs !== undefined)
|
||||||
|
parts.push(`${opts.durationSecs.toFixed(2)}s`);
|
||||||
|
|
||||||
console.log(` ${dim(parts.join(" · "))}`);
|
console.log(` ${dim(parts.join(" · "))}`);
|
||||||
console.log();
|
console.log();
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { createRequire } from "node:module";
|
||||||
|
|
||||||
|
// __CLI_VERSION__ is replaced at build time by tsup (see tsup.config.ts).
|
||||||
|
// When running via tsx in dev/test mode, fall back to reading package.json.
|
||||||
|
// typeof is safe to use on undeclared identifiers — it returns 'undefined' without throwing.
|
||||||
|
export const CLI_VERSION: string =
|
||||||
|
typeof __CLI_VERSION__ !== "undefined"
|
||||||
|
? (__CLI_VERSION__ as string)
|
||||||
|
: (createRequire(import.meta.url)("../package.json") as { version: string })
|
||||||
|
.version;
|
||||||
@@ -80,19 +80,19 @@ describe("printWarning", () => {
|
|||||||
describe("printInfo", () => {
|
describe("printInfo", () => {
|
||||||
it("prints info message", () => {
|
it("prints info message", () => {
|
||||||
printInfo("Important note");
|
printInfo("Important note");
|
||||||
expect(output).toContain("Important note");
|
expect(errOutput).toContain("Important note");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("printScope", () => {
|
describe("printScope", () => {
|
||||||
it("prints scope when IDs present", () => {
|
it("prints scope when IDs present", () => {
|
||||||
printScope({ user_id: "alice", agent_id: "bot" });
|
printScope({ user_id: "alice", agent_id: "bot" });
|
||||||
expect(output).toContain("alice");
|
expect(errOutput).toContain("alice");
|
||||||
expect(output).toContain("bot");
|
expect(errOutput).toContain("bot");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prints nothing when no IDs", () => {
|
it("prints nothing when no IDs", () => {
|
||||||
printScope({});
|
printScope({});
|
||||||
expect(output).toBe("");
|
expect(errOutput).toBe("");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ describe("cmdSearch", () => {
|
|||||||
enableGraph: false,
|
enableGraph: false,
|
||||||
output: "text",
|
output: "text",
|
||||||
});
|
});
|
||||||
expect(output).toContain("No memories found");
|
expect(errOutput).toContain("No memories found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ describe("cmdList", () => {
|
|||||||
enableGraph: false,
|
enableGraph: false,
|
||||||
output: "text",
|
output: "text",
|
||||||
});
|
});
|
||||||
expect(output).toContain("No memories found");
|
expect(errOutput).toContain("No memories found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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),
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { createRequire } from "node:module";
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
const _require = createRequire(import.meta.url);
|
||||||
|
const pkg = _require("./package.json") as { version: string };
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
define: {
|
||||||
|
__CLI_VERSION__: JSON.stringify(pkg.version),
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -253,8 +253,12 @@ def search(
|
|||||||
help="Specific fields to return (comma-separated).",
|
help="Specific fields to return (comma-separated).",
|
||||||
rich_help_panel="Search",
|
rich_help_panel="Search",
|
||||||
),
|
),
|
||||||
graph: bool = typer.Option(False, "--graph", help="Enable graph in search.", rich_help_panel="Search"),
|
graph: bool = typer.Option(
|
||||||
no_graph: bool = typer.Option(False, "--no-graph", help="Disable graph in search.", rich_help_panel="Search"),
|
False, "--graph", help="Enable graph in search.", rich_help_panel="Search"
|
||||||
|
),
|
||||||
|
no_graph: bool = typer.Option(
|
||||||
|
False, "--no-graph", help="Disable graph in search.", rich_help_panel="Search"
|
||||||
|
),
|
||||||
output: str = typer.Option(
|
output: str = typer.Option(
|
||||||
"text", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
"text", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
||||||
),
|
),
|
||||||
@@ -281,8 +285,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 query is None:
|
if not query or not query.strip():
|
||||||
print_error(err_console, "No query provided. Pass a query argument or pipe via stdin.")
|
print_error(err_console, "Search query cannot be empty.")
|
||||||
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)
|
||||||
@@ -372,8 +376,12 @@ def list_cmd(
|
|||||||
before: str | None = typer.Option(
|
before: str | None = typer.Option(
|
||||||
None, "--before", help="Created before (YYYY-MM-DD).", rich_help_panel="Filters"
|
None, "--before", help="Created before (YYYY-MM-DD).", rich_help_panel="Filters"
|
||||||
),
|
),
|
||||||
graph: bool = typer.Option(False, "--graph", help="Enable graph in listing.", rich_help_panel="Filters"),
|
graph: bool = typer.Option(
|
||||||
no_graph: bool = typer.Option(False, "--no-graph", help="Disable graph in listing.", rich_help_panel="Filters"),
|
False, "--graph", help="Enable graph in listing.", rich_help_panel="Filters"
|
||||||
|
),
|
||||||
|
no_graph: bool = typer.Option(
|
||||||
|
False, "--no-graph", help="Disable graph in listing.", rich_help_panel="Filters"
|
||||||
|
),
|
||||||
output: str = typer.Option(
|
output: str = typer.Option(
|
||||||
"table", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
"table", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
||||||
),
|
),
|
||||||
@@ -463,11 +471,19 @@ def update(
|
|||||||
|
|
||||||
@app.command(rich_help_panel="Memory")
|
@app.command(rich_help_panel="Memory")
|
||||||
def delete(
|
def delete(
|
||||||
memory_id: str | None = typer.Argument(None, help="Memory ID to delete (omit when using --all or --entity)."),
|
memory_id: str | None = typer.Argument(
|
||||||
|
None, help="Memory ID to delete (omit when using --all or --entity)."
|
||||||
|
),
|
||||||
all_: bool = typer.Option(False, "--all", help="Delete all memories matching scope filters."),
|
all_: bool = typer.Option(False, "--all", help="Delete all memories matching scope filters."),
|
||||||
entity: bool = typer.Option(False, "--entity", help="Delete the entity itself and all its memories (cascade)."),
|
entity: bool = typer.Option(
|
||||||
project: bool = typer.Option(False, "--project", help="With --all: delete ALL memories project-wide."),
|
False, "--entity", help="Delete the entity itself and all its memories (cascade)."
|
||||||
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting."),
|
),
|
||||||
|
project: bool = typer.Option(
|
||||||
|
False, "--project", help="With --all: delete ALL memories project-wide."
|
||||||
|
),
|
||||||
|
dry_run: bool = typer.Option(
|
||||||
|
False, "--dry-run", help="Show what would be deleted without deleting."
|
||||||
|
),
|
||||||
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
|
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
|
||||||
user_id: str | None = typer.Option(
|
user_id: str | None = typer.Option(
|
||||||
None, "--user-id", "-u", help="Scope to user.", rich_help_panel="Scope"
|
None, "--user-id", "-u", help="Scope to user.", rich_help_panel="Scope"
|
||||||
@@ -641,14 +657,12 @@ def entity_delete(
|
|||||||
agent_id: str | None = typer.Option(
|
agent_id: str | None = typer.Option(
|
||||||
None, "--agent-id", help="Agent ID.", rich_help_panel="Scope"
|
None, "--agent-id", help="Agent ID.", rich_help_panel="Scope"
|
||||||
),
|
),
|
||||||
app_id: str | None = typer.Option(
|
app_id: str | None = typer.Option(None, "--app-id", help="App ID.", rich_help_panel="Scope"),
|
||||||
None, "--app-id", help="App ID.", rich_help_panel="Scope"
|
run_id: str | None = typer.Option(None, "--run-id", help="Run ID.", rich_help_panel="Scope"),
|
||||||
),
|
|
||||||
run_id: str | None = typer.Option(
|
|
||||||
None, "--run-id", help="Run ID.", rich_help_panel="Scope"
|
|
||||||
),
|
|
||||||
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
|
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
|
||||||
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting."),
|
dry_run: bool = typer.Option(
|
||||||
|
False, "--dry-run", help="Show what would be deleted without deleting."
|
||||||
|
),
|
||||||
output: str = typer.Option(
|
output: str = typer.Option(
|
||||||
"text", "--output", "-o", help="Output: text, json, quiet.", rich_help_panel="Output"
|
"text", "--output", "-o", help="Output: text, json, quiet.", rich_help_panel="Output"
|
||||||
),
|
),
|
||||||
@@ -694,17 +708,25 @@ app.add_typer(entity_app, name="entity", rich_help_panel="Management")
|
|||||||
@app.command(rich_help_panel="Management")
|
@app.command(rich_help_panel="Management")
|
||||||
def init(
|
def init(
|
||||||
api_key: str | None = typer.Option(None, "--api-key", help="API key (skip prompt)."),
|
api_key: str | None = typer.Option(None, "--api-key", help="API key (skip prompt)."),
|
||||||
user_id: str | None = typer.Option(None, "--user-id", "-u", help="Default user ID (skip prompt)."),
|
user_id: str | None = typer.Option(
|
||||||
|
None, "--user-id", "-u", help="Default user ID (skip prompt)."
|
||||||
|
),
|
||||||
|
email: str | None = typer.Option(None, "--email", help="Login via email verification code."),
|
||||||
|
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)
|
run_init(api_key=api_key, user_id=user_id, email=email, code=code)
|
||||||
|
|
||||||
|
|
||||||
# (entity_app registered at module level, below sub-group definitions)
|
# (entity_app registered at module level, below sub-group definitions)
|
||||||
@@ -743,7 +765,6 @@ def status(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("import", rich_help_panel="Management")
|
@app.command("import", rich_help_panel="Management")
|
||||||
def import_cmd(
|
def import_cmd(
|
||||||
file_path: str = typer.Argument(..., help="JSON file to import."),
|
file_path: str = typer.Argument(..., help="JSON file to import."),
|
||||||
@@ -1010,6 +1031,14 @@ 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")
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ class Backend(ABC):
|
|||||||
def entities(self, entity_type: str) -> list[dict]: ...
|
def entities(self, entity_type: str) -> list[dict]: ...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_backend(config: Mem0Config) -> Backend:
|
def get_backend(config: Mem0Config) -> Backend:
|
||||||
"""Return the Platform backend."""
|
"""Return the Platform backend."""
|
||||||
from mem0_cli.backend.platform import PlatformBackend
|
from mem0_cli.backend.platform import PlatformBackend
|
||||||
|
|||||||
@@ -312,7 +312,6 @@ class PlatformBackend(Backend):
|
|||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AuthError(Exception):
|
class AuthError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> Non
|
|||||||
"""List entities of a given type."""
|
"""List entities of a given type."""
|
||||||
valid_types = {"users", "agents", "apps", "runs"}
|
valid_types = {"users", "agents", "apps", "runs"}
|
||||||
if entity_type not in valid_types:
|
if entity_type not in valid_types:
|
||||||
print_error(err_console, f"Invalid entity type: {entity_type}. Use: {', '.join(valid_types)}")
|
print_error(
|
||||||
|
err_console, f"Invalid entity type: {entity_type}. Use: {', '.join(valid_types)}"
|
||||||
|
)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
_start = _time.perf_counter()
|
_start = _time.perf_counter()
|
||||||
@@ -76,7 +78,9 @@ def cmd_entities_delete(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Delete an entity and all its memories (cascade delete)."""
|
"""Delete an entity and all its memories (cascade delete)."""
|
||||||
if not any([user_id, agent_id, app_id, run_id]):
|
if not any([user_id, agent_id, app_id, run_id]):
|
||||||
print_error(err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id.")
|
print_error(
|
||||||
|
err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id."
|
||||||
|
)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
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
|
||||||
@@ -16,7 +19,7 @@ from mem0_cli.branding import (
|
|||||||
print_info,
|
print_info,
|
||||||
print_success,
|
print_success,
|
||||||
)
|
)
|
||||||
from mem0_cli.config import Mem0Config, save_config
|
from mem0_cli.config import DEFAULT_BASE_URL, Mem0Config, save_config
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
err_console = Console(stderr=True)
|
err_console = Console(stderr=True)
|
||||||
@@ -84,7 +87,89 @@ def _prompt_secret(label: str) -> str:
|
|||||||
return "".join(chars)
|
return "".join(chars)
|
||||||
|
|
||||||
|
|
||||||
def run_init(*, api_key: str | None = None, user_id: str | None = None) -> None:
|
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
@@ -93,6 +178,48 @@ def run_init(*, api_key: str | None = None, user_id: str | None = None) -> None:
|
|||||||
"""
|
"""
|
||||||
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
|
||||||
@@ -115,7 +242,49 @@ def run_init(*, api_key: str | None = None, user_id: str | None = None) -> None:
|
|||||||
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")
|
||||||
|
|
||||||
# Use provided flags or prompt
|
# If no flags at all, ask user how they want to authenticate
|
||||||
|
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:
|
||||||
|
|||||||
@@ -95,6 +95,21 @@ 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(
|
||||||
@@ -113,7 +128,6 @@ 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":
|
||||||
@@ -164,6 +178,13 @@ 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:
|
||||||
@@ -186,11 +207,14 @@ 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)
|
format_memories_table(console, results, show_score=True)
|
||||||
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
|
||||||
)
|
)
|
||||||
@@ -238,6 +262,13 @@ 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:
|
||||||
@@ -258,8 +289,19 @@ 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":
|
||||||
format_json(console, results)
|
from mem0_cli.output import format_json_envelope
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -376,6 +418,7 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,9 @@ def cmd_import(
|
|||||||
added = 0
|
added = 0
|
||||||
failed = 0
|
failed = 0
|
||||||
_start = _time.perf_counter()
|
_start = _time.perf_counter()
|
||||||
for item in track(data, description=f"[{DIM_COLOR}]Importing memories...[/]", console=err_console):
|
for item in track(
|
||||||
|
data, description=f"[{DIM_COLOR}]Importing memories...[/]", console=err_console
|
||||||
|
):
|
||||||
content = item.get("memory", item.get("text", item.get("content", "")))
|
content = item.get("memory", item.get("text", item.get("content", "")))
|
||||||
if not content:
|
if not content:
|
||||||
failed += 1
|
failed += 1
|
||||||
|
|||||||
@@ -45,6 +45,17 @@ 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)
|
||||||
@@ -140,7 +151,8 @@ 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'."""
|
"""Get a config value by dotted path, e.g. 'platform.api_key' or short form '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:
|
||||||
@@ -153,6 +165,7 @@ 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]:
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ def format_memories_text(console: Console, memories: list[dict], title: str = "m
|
|||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
def format_memories_table(console: Console, memories: list[dict]) -> None:
|
def format_memories_table(
|
||||||
|
console: Console, memories: list[dict], *, show_score: bool = False
|
||||||
|
) -> None:
|
||||||
"""Render memories in a rich table."""
|
"""Render memories in a rich table."""
|
||||||
table = Table(
|
table = Table(
|
||||||
border_style=BRAND_COLOR,
|
border_style=BRAND_COLOR,
|
||||||
@@ -57,19 +59,33 @@ def format_memories_table(console: Console, memories: list[dict]) -> None:
|
|||||||
row_styles=["", "dim"],
|
row_styles=["", "dim"],
|
||||||
padding=(0, 1),
|
padding=(0, 1),
|
||||||
)
|
)
|
||||||
table.add_column("ID", style="dim", max_width=10)
|
table.add_column("ID", style="dim", max_width=38, no_wrap=True)
|
||||||
|
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", "")[:8]
|
mem_id = mem.get("id", "")
|
||||||
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", [])
|
||||||
cat = categories[0] if isinstance(categories, list) and categories else "—"
|
if isinstance(categories, list) and categories:
|
||||||
|
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()
|
||||||
|
|||||||
@@ -8,11 +8,19 @@ boundaries).
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[mKJHABCDfsu]")
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_ansi(text: str) -> str:
|
||||||
|
"""Remove ANSI escape codes so substring checks work regardless of color mode."""
|
||||||
|
return _ANSI_RE.sub("", text)
|
||||||
|
|
||||||
|
|
||||||
def _run(
|
def _run(
|
||||||
args: list[str],
|
args: list[str],
|
||||||
@@ -28,22 +36,35 @@ def _run(
|
|||||||
reads config from ``<home_dir>/.mem0/config.json`` instead
|
reads config from ``<home_dir>/.mem0/config.json`` instead
|
||||||
of the user's real config. This is critical for tests that
|
of the user's real config. This is critical for tests that
|
||||||
depend on a clean (no API key) or custom config state.
|
depend on a clean (no API key) or custom config state.
|
||||||
|
|
||||||
|
Returns a CompletedProcess whose stdout/stderr have ANSI escape codes
|
||||||
|
stripped. GitHub Actions sets FORCE_COLOR=1 which causes Rich/Typer to
|
||||||
|
fragment option names like --user-id into separately-styled ANSI segments,
|
||||||
|
making plain ``in`` checks fail. Stripping here is version-agnostic and
|
||||||
|
ensures all assertions see the same plain text regardless of terminal env.
|
||||||
"""
|
"""
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
# Strip all MEM0_ env vars so tests start clean
|
# Strip all MEM0_ env vars so tests start clean
|
||||||
for key in list(env.keys()):
|
for key in list(env.keys()):
|
||||||
if key.startswith("MEM0_"):
|
if key.startswith("MEM0_"):
|
||||||
del env[key]
|
del env[key]
|
||||||
|
env.pop("FORCE_COLOR", None)
|
||||||
if home_dir:
|
if home_dir:
|
||||||
env["HOME"] = home_dir
|
env["HOME"] = home_dir
|
||||||
if env_override:
|
if env_override:
|
||||||
env.update(env_override)
|
env.update(env_override)
|
||||||
return subprocess.run(
|
result = subprocess.run(
|
||||||
[sys.executable, "-m", "mem0_cli", *args],
|
[sys.executable, "-m", "mem0_cli", *args],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
|
return subprocess.CompletedProcess(
|
||||||
|
args=result.args,
|
||||||
|
returncode=result.returncode,
|
||||||
|
stdout=_strip_ansi(result.stdout),
|
||||||
|
stderr=_strip_ansi(result.stderr),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -67,7 +88,6 @@ class TestCLIIntegration:
|
|||||||
assert result.returncode == 0
|
assert result.returncode == 0
|
||||||
assert "0.1.0" in result.stdout
|
assert "0.1.0" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
def test_add_help(self):
|
def test_add_help(self):
|
||||||
result = _run(["add", "--help"])
|
result = _run(["add", "--help"])
|
||||||
assert result.returncode == 0
|
assert result.returncode == 0
|
||||||
@@ -161,7 +181,12 @@ class TestCLIIsolated:
|
|||||||
)
|
)
|
||||||
assert result.returncode != 0
|
assert result.returncode != 0
|
||||||
combined = result.stderr + result.stdout
|
combined = result.stderr + result.stdout
|
||||||
assert "memory ID" in combined.lower() or "--all" in combined or "--entity" in combined or "Error" in combined
|
assert (
|
||||||
|
"memory ID" in combined.lower()
|
||||||
|
or "--all" in combined
|
||||||
|
or "--entity" in combined
|
||||||
|
or "Error" in combined
|
||||||
|
)
|
||||||
|
|
||||||
def test_config_show_clean(self, clean_home):
|
def test_config_show_clean(self, clean_home):
|
||||||
"""config show with no config should still work."""
|
"""config show with no config should still work."""
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ mode: "custom"
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="/cli"
|
href="/platform/cli"
|
||||||
className="group flex h-full flex-col overflow-hidden rounded-2xl border border-gray-200 dark:border-zinc-800/40 bg-white dark:bg-zinc-900/40 transition hover:border-primary/60 hover:bg-gray-50 dark:hover:bg-zinc-900"
|
className="group flex h-full flex-col overflow-hidden rounded-2xl border border-gray-200 dark:border-zinc-800/40 bg-white dark:bg-zinc-900/40 transition hover:border-primary/60 hover:bg-gray-50 dark:hover:bg-zinc-900"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ 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
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ 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
|
||||||
|
|||||||
@@ -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()
|
collection_names = database.list_collection_names(authorizedCollections=True)
|
||||||
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()
|
collections = self.db.list_collection_names(authorizedCollections=True)
|
||||||
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:
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# 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)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"plugins": {
|
||||||
|
"openclaw-mem0": {
|
||||||
|
"mode": "platform",
|
||||||
|
"apiKey": "${MEM0_API_KEY}",
|
||||||
|
"userId": "__MEM0_USER_ID__",
|
||||||
|
"autoCapture": true,
|
||||||
|
"autoRecall": true,
|
||||||
|
"searchThreshold": 0.5,
|
||||||
|
"topK": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
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:
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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
|
||||||
Executable
+268
@@ -0,0 +1,268 @@
|
|||||||
|
#!/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 ""
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# 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)
|
||||||
Executable
+280
@@ -0,0 +1,280 @@
|
|||||||
|
#!/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
|
||||||
@@ -113,3 +113,47 @@ 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?"
|
||||||
|
|||||||
@@ -88,6 +88,50 @@ 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, \
|
||||||
|
|||||||
Reference in New Issue
Block a user