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