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:
Saket Aryan
2026-03-31 22:30:31 +05:30
committed by GitHub
co-authored by Claude Sonnet 4.6
parent 9bebcf45f7
commit 32c1ccba5b
30 changed files with 3042 additions and 2457 deletions
+67 -64
View File
@@ -3,88 +3,91 @@
*/
import Table from "cli-table3";
import { printError, printSuccess, colors } from "../branding.js";
import { colors, printError, printSuccess } from "../branding.js";
import {
getNestedValue,
loadConfig,
redactKey,
saveConfig,
setNestedValue,
getNestedValue,
loadConfig,
redactKey,
saveConfig,
setNestedValue,
} from "../config.js";
import { formatJsonEnvelope } from "../output.js";
const { brand, accent, dim } = colors;
export function cmdConfigShow(opts: { output?: string } = {}): void {
const config = loadConfig();
const config = loadConfig();
if (opts.output === "json") {
formatJsonEnvelope({
command: "config show",
data: {
defaults: {
user_id: config.defaults.userId || null,
agent_id: config.defaults.agentId || null,
app_id: config.defaults.appId || null,
run_id: config.defaults.runId || null,
enable_graph: config.defaults.enableGraph,
},
platform: {
api_key: redactKey(config.platform.apiKey),
base_url: config.platform.baseUrl,
},
},
});
return;
}
if (opts.output === "json") {
formatJsonEnvelope({
command: "config show",
data: {
defaults: {
user_id: config.defaults.userId || null,
agent_id: config.defaults.agentId || null,
app_id: config.defaults.appId || null,
run_id: config.defaults.runId || null,
enable_graph: config.defaults.enableGraph,
},
platform: {
api_key: redactKey(config.platform.apiKey),
base_url: config.platform.baseUrl,
},
},
});
return;
}
console.log();
console.log(` ${brand("◆ mem0 Configuration")}\n`);
console.log();
console.log(` ${brand("◆ mem0 Configuration")}\n`);
const table = new Table({
head: [accent("Key"), accent("Value")],
style: { head: [], border: [] },
});
const table = new Table({
head: [accent("Key"), accent("Value")],
style: { head: [], border: [] },
});
// Defaults
table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]);
table.push(["defaults.agent_id", config.defaults.agentId || dim("(not set)")]);
table.push(["defaults.app_id", config.defaults.appId || dim("(not set)")]);
table.push(["defaults.run_id", config.defaults.runId || dim("(not set)")]);
table.push(["defaults.enable_graph", String(config.defaults.enableGraph)]);
table.push(["", ""]);
// Defaults
table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]);
table.push([
"defaults.agent_id",
config.defaults.agentId || dim("(not set)"),
]);
table.push(["defaults.app_id", config.defaults.appId || dim("(not set)")]);
table.push(["defaults.run_id", config.defaults.runId || dim("(not set)")]);
table.push(["defaults.enable_graph", String(config.defaults.enableGraph)]);
table.push(["", ""]);
// Platform
table.push(["platform.api_key", redactKey(config.platform.apiKey)]);
table.push(["platform.base_url", config.platform.baseUrl]);
// Platform
table.push(["platform.api_key", redactKey(config.platform.apiKey)]);
table.push(["platform.base_url", config.platform.baseUrl]);
console.log(table.toString());
console.log();
console.log(table.toString());
console.log();
}
export function cmdConfigGet(key: string): void {
const config = loadConfig();
const value = getNestedValue(config, key);
const config = loadConfig();
const value = getNestedValue(config, key);
if (value === undefined) {
printError(`Unknown config key: ${key}`);
} else {
// Redact secrets
if (key.includes("api_key") || key.split(".").pop() === "key") {
console.log(redactKey(String(value)));
} else {
console.log(String(value));
}
}
if (value === undefined) {
printError(`Unknown config key: ${key}`);
} else {
// Redact secrets
if (key.includes("api_key") || key.split(".").pop() === "key") {
console.log(redactKey(String(value)));
} else {
console.log(String(value));
}
}
}
export function cmdConfigSet(key: string, value: string): void {
const config = loadConfig();
if (setNestedValue(config, key, value)) {
saveConfig(config);
const display = key.includes("key") ? redactKey(value) : value;
printSuccess(`${key} = ${display}`);
} else {
printError(`Unknown config key: ${key}`);
}
const config = loadConfig();
if (setNestedValue(config, key, value)) {
saveConfig(config);
const display = key.includes("key") ? redactKey(value) : value;
printSuccess(`${key} = ${display}`);
} else {
printError(`Unknown config key: ${key}`);
}
}
+125 -108
View File
@@ -4,8 +4,14 @@
import readline from "node:readline";
import Table from "cli-table3";
import { printError, printInfo, printSuccess, timedStatus, colors } from "../branding.js";
import type { Backend } from "../backend/base.js";
import {
colors,
printError,
printInfo,
printSuccess,
timedStatus,
} from "../branding.js";
import { formatJson } from "../output.js";
const { brand, accent, dim } = colors;
@@ -13,127 +19,138 @@ const { brand, accent, dim } = colors;
const VALID_TYPES = new Set(["users", "agents", "apps", "runs"]);
export async function cmdEntitiesList(
backend: Backend,
entityType: string,
opts: { output: string },
backend: Backend,
entityType: string,
opts: { output: string },
): Promise<void> {
if (!VALID_TYPES.has(entityType)) {
printError(`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`);
process.exit(1);
}
if (!VALID_TYPES.has(entityType)) {
printError(
`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`,
);
process.exit(1);
}
const start = performance.now();
let results: Record<string, unknown>[];
try {
results = await timedStatus(`Fetching ${entityType}...`, async () => {
return backend.entities(entityType);
});
} catch (e) {
printError(
e instanceof Error ? e.message : String(e),
"This feature may require the mem0 Platform.",
);
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
const start = performance.now();
let results: Record<string, unknown>[];
try {
results = await timedStatus(`Fetching ${entityType}...`, async () => {
return backend.entities(entityType);
});
} catch (e) {
printError(
e instanceof Error ? e.message : String(e),
"This feature may require the mem0 Platform.",
);
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJson(results);
return;
}
if (opts.output === "json") {
formatJson(results);
return;
}
if (!results.length) {
printInfo(`No ${entityType} found.`);
return;
}
if (!results.length) {
printInfo(`No ${entityType} found.`);
return;
}
const table = new Table({
head: [accent("Name / ID"), accent("Created")],
style: { head: [], border: [] },
});
const table = new Table({
head: [accent("Name / ID"), accent("Created")],
style: { head: [], border: [] },
});
for (const entity of results) {
const name = String(entity.name ?? entity.id ?? "—");
const created = String(entity.created_at ?? "—").slice(0, 10);
table.push([name, created]);
}
for (const entity of results) {
const name = String(entity.name ?? entity.id ?? "—");
const created = String(entity.created_at ?? "—").slice(0, 10);
table.push([name, created]);
}
console.log();
console.log(table.toString());
console.log(` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`);
console.log();
console.log();
console.log(table.toString());
console.log(
` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`,
);
console.log();
}
export async function cmdEntitiesDelete(
backend: Backend,
opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
dryRun?: boolean;
force: boolean;
output: string;
},
backend: Backend,
opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
dryRun?: boolean;
force: boolean;
output: string;
},
): Promise<void> {
if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) {
printError("Provide at least one of --user-id, --agent-id, --app-id, --run-id.");
process.exit(1);
}
if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) {
printError(
"Provide at least one of --user-id, --agent-id, --app-id, --run-id.",
);
process.exit(1);
}
if (opts.dryRun) {
const scopeParts: string[] = [];
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
printInfo(`Would delete entity ${scopeParts.join(", ")} and all its memories.`);
printInfo("No changes made.");
return;
}
if (opts.dryRun) {
const scopeParts: string[] = [];
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
printInfo(
`Would delete entity ${scopeParts.join(", ")} and all its memories.`,
);
printInfo("No changes made.");
return;
}
if (!opts.force) {
const scopeParts: string[] = [];
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
const scope = scopeParts.join(", ");
if (!opts.force) {
const scopeParts: string[] = [];
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
const scope = scopeParts.join(", ");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise<string>((resolve) => {
rl.question(
`\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `,
resolve,
);
});
rl.close();
if (answer.toLowerCase() !== "y") {
printInfo("Cancelled.");
process.exit(0);
}
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const answer = await new Promise<string>((resolve) => {
rl.question(
`\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `,
resolve,
);
});
rl.close();
if (answer.toLowerCase() !== "y") {
printInfo("Cancelled.");
process.exit(0);
}
}
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Deleting entity...", async () => {
return backend.deleteEntities({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
});
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Deleting entity...", async () => {
return backend.deleteEntities({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
});
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJson(result);
} else if (opts.output !== "quiet") {
printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`);
}
if (opts.output === "json") {
formatJson(result);
} else if (opts.output !== "quiet") {
printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`);
}
}
+311 -285
View File
@@ -3,355 +3,381 @@
*/
import readline from "node:readline";
import {
printBanner,
printError,
printInfo,
printSuccess,
colors,
} from "../branding.js";
import { type Mem0Config, createDefaultConfig, loadConfig, saveConfig, DEFAULT_BASE_URL } from "../config.js";
import { PlatformBackend } from "../backend/platform.js";
import {
colors,
printBanner,
printError,
printInfo,
printSuccess,
} from "../branding.js";
import {
DEFAULT_BASE_URL,
type Mem0Config,
createDefaultConfig,
loadConfig,
saveConfig,
} from "../config.js";
const { brand, dim } = colors;
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
function validateEmail(email: string): void {
if (!EMAIL_RE.test(email)) {
printError(`Invalid email address: ${JSON.stringify(email)}`);
process.exit(1);
}
if (!EMAIL_RE.test(email)) {
printError(`Invalid email address: ${JSON.stringify(email)}`);
process.exit(1);
}
}
async function emailLogin(
email: string,
code: string | undefined,
baseUrl: string,
email: string,
code: string | undefined,
baseUrl: string,
): Promise<Record<string, unknown>> {
const url = baseUrl.replace(/\/+$/, "");
const url = baseUrl.replace(/\/+$/, "");
let codeValue = code;
if (!code) {
const resp = await fetch(`${url}/api/v1/auth/email_code/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(30_000),
});
if (resp.status === 429) {
printError("Too many attempts. Try again in a few minutes.");
process.exit(1);
}
if (!resp.ok) {
let detail: string;
try {
const body = (await resp.json()) as Record<string, unknown>;
detail = (body["error"] ?? body["detail"] ?? resp.statusText) as string;
} catch {
detail = resp.statusText;
}
printError(`Failed to send code: ${detail}`);
process.exit(1);
}
if (!codeValue) {
const resp = await fetch(`${url}/api/v1/auth/email_code/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(30_000),
});
if (resp.status === 429) {
printError("Too many attempts. Try again in a few minutes.");
process.exit(1);
}
if (!resp.ok) {
let detail: string;
try {
const body = (await resp.json()) as Record<string, unknown>;
detail = (body.error ?? body.detail ?? resp.statusText) as string;
} catch {
detail = resp.statusText;
}
printError(`Failed to send code: ${detail}`);
process.exit(1);
}
printSuccess("Verification code sent! Check your email.");
printSuccess("Verification code sent! Check your email.");
if (!process.stdin.isTTY) {
printError(
"No --code provided and terminal is non-interactive.",
"Run: mem0 init --email <email> --code <code>",
);
process.exit(1);
}
if (!process.stdin.isTTY) {
printError(
"No --code provided and terminal is non-interactive.",
"Run: mem0 init --email <email> --code <code>",
);
process.exit(1);
}
console.log();
const entered = await promptLine(` ${brand("Verification Code")}`);
if (!entered) {
printError("Code is required.");
process.exit(1);
}
code = entered;
}
console.log();
const entered = await promptLine(` ${brand("Verification Code")}`);
if (!entered) {
printError("Code is required.");
process.exit(1);
}
codeValue = entered;
}
const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, code: code.trim() }),
signal: AbortSignal.timeout(30_000),
});
if (verifyResp.status === 429) {
printError("Too many attempts. Try again in a few minutes.");
process.exit(1);
}
if (!verifyResp.ok) {
let detail: string;
try {
const body = (await verifyResp.json()) as Record<string, unknown>;
detail = (body["error"] ?? body["detail"] ?? verifyResp.statusText) as string;
} catch {
detail = verifyResp.statusText;
}
printError(`Verification failed: ${detail}`);
process.exit(1);
}
const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, code: codeValue.trim() }),
signal: AbortSignal.timeout(30_000),
});
if (verifyResp.status === 429) {
printError("Too many attempts. Try again in a few minutes.");
process.exit(1);
}
if (!verifyResp.ok) {
let detail: string;
try {
const body = (await verifyResp.json()) as Record<string, unknown>;
detail = (body.error ?? body.detail ?? verifyResp.statusText) as string;
} catch {
detail = verifyResp.statusText;
}
printError(`Verification failed: ${detail}`);
process.exit(1);
}
return verifyResp.json() as Promise<Record<string, unknown>>;
return verifyResp.json() as Promise<Record<string, unknown>>;
}
function promptSecret(label: string): Promise<string> {
return new Promise((resolve, reject) => {
process.stdout.write(label);
return new Promise((resolve, reject) => {
process.stdout.write(label);
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
process.stdin.resume();
process.stdin.setEncoding("utf-8");
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
process.stdin.resume();
process.stdin.setEncoding("utf-8");
const chars: string[] = [];
const chars: string[] = [];
const onData = (key: string) => {
for (const ch of key) {
if (ch === "\r" || ch === "\n") {
cleanup();
process.stdout.write("\n");
resolve(chars.join(""));
return;
}
if (ch === "\x03") {
cleanup();
reject(new Error("Interrupted"));
return;
}
if (ch === "\x7f" || ch === "\x08") {
// backspace
if (chars.length > 0) {
chars.pop();
process.stdout.write("\b \b");
}
} else if (ch === "\x15") {
// Ctrl+U — clear line
process.stdout.write("\b \b".repeat(chars.length));
chars.length = 0;
} else if (ch >= " ") {
chars.push(ch);
process.stdout.write("*");
}
}
};
const onData = (key: string) => {
for (const ch of key) {
if (ch === "\r" || ch === "\n") {
cleanup();
process.stdout.write("\n");
resolve(chars.join(""));
return;
}
if (ch === "\x03") {
cleanup();
reject(new Error("Interrupted"));
return;
}
if (ch === "\x7f" || ch === "\x08") {
// backspace
if (chars.length > 0) {
chars.pop();
process.stdout.write("\b \b");
}
} else if (ch === "\x15") {
// Ctrl+U — clear line
process.stdout.write("\b \b".repeat(chars.length));
chars.length = 0;
} else if (ch >= " ") {
chars.push(ch);
process.stdout.write("*");
}
}
};
const cleanup = () => {
process.stdin.removeListener("data", onData);
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
process.stdin.pause();
};
const cleanup = () => {
process.stdin.removeListener("data", onData);
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
process.stdin.pause();
};
process.stdin.on("data", onData);
});
process.stdin.on("data", onData);
});
}
function promptLine(label: string, defaultValue?: string): Promise<string> {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `;
return new Promise((resolve) => {
rl.question(prompt, (answer) => {
rl.close();
resolve(answer.trim() || defaultValue || "");
});
});
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `;
return new Promise((resolve) => {
rl.question(prompt, (answer) => {
rl.close();
resolve(answer.trim() || defaultValue || "");
});
});
}
async function setupPlatform(config: Mem0Config): Promise<void> {
console.log();
console.log(` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys")}`);
console.log();
console.log();
console.log(
` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys")}`,
);
console.log();
process.stdout.write(` ${brand("API Key")}: `);
const apiKey = await promptSecret("");
if (!apiKey) {
printError("API key is required.");
process.exit(1);
}
config.platform.apiKey = apiKey;
process.stdout.write(` ${brand("API Key")}: `);
const apiKey = await promptSecret("");
if (!apiKey) {
printError("API key is required.");
process.exit(1);
}
config.platform.apiKey = apiKey;
}
async function setupDefaults(config: Mem0Config): Promise<void> {
console.log();
printInfo("Set default entity IDs (press Enter to skip).\n");
console.log();
printInfo("Set default entity IDs (press Enter to skip).\n");
const userId = await promptLine(` ${brand("Default User ID")} ${dim("(recommended)")}`, "mem0-cli");
if (userId) config.defaults.userId = userId;
const userId = await promptLine(
` ${brand("Default User ID")} ${dim("(recommended)")}`,
"mem0-cli",
);
if (userId) config.defaults.userId = userId;
}
async function validatePlatform(config: Mem0Config): Promise<void> {
console.log();
printInfo("Validating connection...");
try {
const backend = new PlatformBackend(config.platform);
const status = await backend.status({
userId: config.defaults.userId || undefined,
agentId: config.defaults.agentId || undefined,
});
if (status.connected) {
printSuccess("Connected to mem0 Platform!");
} else {
printError(
`Could not connect: ${status.error ?? "Unknown error"}`,
"Check your API key and try again.",
);
}
} catch (e) {
printError(`Connection test failed: ${e instanceof Error ? e.message : e}`);
}
console.log();
printInfo("Validating connection...");
try {
const backend = new PlatformBackend(config.platform);
const status = await backend.status({
userId: config.defaults.userId || undefined,
agentId: config.defaults.agentId || undefined,
});
if (status.connected) {
printSuccess("Connected to mem0 Platform!");
} else {
printError(
`Could not connect: ${status.error ?? "Unknown error"}`,
"Check your API key and try again.",
);
}
} catch (e) {
printError(`Connection test failed: ${e instanceof Error ? e.message : e}`);
}
}
export async function runInit(
opts: { apiKey?: string; userId?: string; email?: string; code?: string } = {},
opts: {
apiKey?: string;
userId?: string;
email?: string;
code?: string;
} = {},
): Promise<void> {
const config = createDefaultConfig();
const savedConfig = loadConfig();
const baseUrl =
process.env["MEM0_BASE_URL"] || savedConfig.platform.baseUrl || DEFAULT_BASE_URL;
const config = createDefaultConfig();
const savedConfig = loadConfig();
const baseUrl =
process.env.MEM0_BASE_URL ||
savedConfig.platform.baseUrl ||
DEFAULT_BASE_URL;
// Guards
if (opts.code && !opts.email) {
printError("--code requires --email.");
process.exit(1);
}
if (opts.email && opts.apiKey) {
printError("Cannot use both --api-key and --email.");
process.exit(1);
}
// Guards
if (opts.code && !opts.email) {
printError("--code requires --email.");
process.exit(1);
}
if (opts.email && opts.apiKey) {
printError("Cannot use both --api-key and --email.");
process.exit(1);
}
// ── Email login flow ──────────────────────────────────────────────────────
if (opts.email) {
const email = opts.email.trim().toLowerCase();
validateEmail(email);
// ── Email login flow ──────────────────────────────────────────────────────
if (opts.email) {
const email = opts.email.trim().toLowerCase();
validateEmail(email);
printBanner();
console.log();
printInfo(`Logging in as ${email}...\n`);
printBanner();
console.log();
printInfo(`Logging in as ${email}...\n`);
const result = await emailLogin(email, opts.code, baseUrl);
const result = await emailLogin(email, opts.code, baseUrl);
const apiKeyVal = result["api_key"] as string | undefined;
if (!apiKeyVal) {
printError("Auth succeeded but no API key was returned. Contact support.");
process.exit(1);
}
const apiKeyVal = result.api_key as string | undefined;
if (!apiKeyVal) {
printError(
"Auth succeeded but no API key was returned. Contact support.",
);
process.exit(1);
}
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli";
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli";
saveConfig(config);
console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
console.log();
return;
}
saveConfig(config);
console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
console.log();
return;
}
// ── API key flow ──────────────────────────────────────────────────────────
// ── API key flow ──────────────────────────────────────────────────────────
// Non-interactive: both flags provided
if (opts.apiKey && opts.userId) {
config.platform.apiKey = opts.apiKey;
config.defaults.userId = opts.userId;
await validatePlatform(config);
saveConfig(config);
printSuccess("Configuration saved to ~/.mem0/config.json");
return;
}
// Non-interactive: both flags provided
if (opts.apiKey && opts.userId) {
config.platform.apiKey = opts.apiKey;
config.defaults.userId = opts.userId;
await validatePlatform(config);
saveConfig(config);
printSuccess("Configuration saved to ~/.mem0/config.json");
return;
}
// Non-TTY without full flags: error with usage hint
if (!process.stdin.isTTY && (!opts.apiKey || !opts.userId)) {
printError(
"Non-interactive terminal detected and missing required flags.",
"Usage: mem0 init --api-key <key> --user-id <id>",
);
process.exit(1);
}
// Non-TTY without full flags: error with usage hint
if (!process.stdin.isTTY && (!opts.apiKey || !opts.userId)) {
printError(
"Non-interactive terminal detected and missing required flags.",
"Usage: mem0 init --api-key <key> --user-id <id>",
);
process.exit(1);
}
printBanner();
console.log();
printInfo("Welcome! Let's set up your mem0 CLI.\n");
printBanner();
console.log();
printInfo("Welcome! Let's set up your mem0 CLI.\n");
// Use provided API key or prompt
if (opts.apiKey) {
config.platform.apiKey = opts.apiKey;
} else {
console.log(` ${brand("How would you like to authenticate?")}`);
console.log(` ${dim("1.")} Login with email ${dim("(recommended)")}`);
console.log(` ${dim("2.")} Enter API key manually`);
console.log();
// Use provided API key or prompt
if (opts.apiKey) {
config.platform.apiKey = opts.apiKey;
} else {
console.log(` ${brand("How would you like to authenticate?")}`);
console.log(` ${dim("1.")} Login with email ${dim("(recommended)")}`);
console.log(` ${dim("2.")} Enter API key manually`);
console.log();
const choice = await promptLine(` ${brand("Choose")} [1/2]`, "1");
const choice = await promptLine(` ${brand("Choose")} [1/2]`, "1");
if (choice === "1") {
console.log();
const emailAddr = await promptLine(` ${brand("Email")}`);
if (!emailAddr) {
printError("Email is required.");
process.exit(1);
}
if (choice === "1") {
console.log();
const emailAddr = await promptLine(` ${brand("Email")}`);
if (!emailAddr) {
printError("Email is required.");
process.exit(1);
}
const email = emailAddr.trim().toLowerCase();
validateEmail(email);
printInfo(`Logging in as ${email}...\n`);
const email = emailAddr.trim().toLowerCase();
validateEmail(email);
printInfo(`Logging in as ${email}...\n`);
const result = await emailLogin(email, undefined, baseUrl);
const result = await emailLogin(email, undefined, baseUrl);
const apiKeyVal = result["api_key"] as string | undefined;
if (!apiKeyVal) {
printError("Auth succeeded but no API key was returned. Contact support.");
process.exit(1);
}
const apiKeyVal = result.api_key as string | undefined;
if (!apiKeyVal) {
printError(
"Auth succeeded but no API key was returned. Contact support.",
);
process.exit(1);
}
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli";
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli";
saveConfig(config);
console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
console.log();
return;
}
saveConfig(config);
console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
console.log();
return;
}
// choice === "2": fall through to API key prompt
await setupPlatform(config);
}
// choice === "2": fall through to API key prompt
await setupPlatform(config);
}
// Use provided user ID or prompt
if (opts.userId) {
config.defaults.userId = opts.userId;
} else {
await setupDefaults(config);
}
// Use provided user ID or prompt
if (opts.userId) {
config.defaults.userId = opts.userId;
} else {
await setupDefaults(config);
}
await validatePlatform(config);
await validatePlatform(config);
saveConfig(config);
console.log();
printSuccess("Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
if (config.defaults.userId) {
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
} else {
console.log(` ${dim(' mem0 add "I prefer dark mode" --user-id alice')}`);
console.log(` ${dim(' mem0 search "preferences" --user-id alice')}`);
}
console.log();
saveConfig(config);
console.log();
printSuccess("Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
if (config.defaults.userId) {
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
} else {
console.log(` ${dim(' mem0 add "I prefer dark mode" --user-id alice')}`);
console.log(` ${dim(' mem0 search "preferences" --user-id alice')}`);
}
console.log();
}
File diff suppressed because it is too large Load Diff
+115 -105
View File
@@ -3,133 +3,143 @@
*/
import fs from "node:fs";
import { printError, printSuccess, timedStatus, colors } from "../branding.js";
import type { Backend } from "../backend/base.js";
import { formatJsonEnvelope } from "../output.js";
import boxen from "boxen";
import type { Backend } from "../backend/base.js";
import { colors, printError, printSuccess, timedStatus } from "../branding.js";
import { formatJsonEnvelope } from "../output.js";
import { CLI_VERSION } from "../version.js";
const { brand, dim, success, error: errorColor } = colors;
export async function cmdStatus(
backend: Backend,
opts: { userId?: string; agentId?: string; output?: string } = {},
backend: Backend,
opts: { userId?: string; agentId?: string; output?: string } = {},
): Promise<void> {
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Checking connection...", async () => {
return backend.status({ userId: opts.userId, agentId: opts.agentId });
});
} catch (e) {
result = { connected: false, error: e instanceof Error ? e.message : String(e) };
}
const elapsed = (performance.now() - start) / 1000;
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Checking connection...", async () => {
return backend.status({ userId: opts.userId, agentId: opts.agentId });
});
} catch (e) {
result = {
connected: false,
error: e instanceof Error ? e.message : String(e),
};
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJsonEnvelope({
command: "status",
data: {
connected: result.connected,
backend: result.backend ?? null,
base_url: result.base_url ?? null,
latency_ms: Math.round(elapsed * 1000),
},
durationMs: Math.round(elapsed * 1000),
});
return;
}
if (opts.output === "json") {
formatJsonEnvelope({
command: "status",
data: {
connected: result.connected,
backend: result.backend ?? null,
base_url: result.base_url ?? null,
latency_ms: Math.round(elapsed * 1000),
},
durationMs: Math.round(elapsed * 1000),
});
return;
}
const lines: string[] = [];
if (result.connected) {
lines.push(` ${success("\u25cf")} Connected`);
} else {
lines.push(` ${errorColor("\u25cf")} Disconnected`);
}
const lines: string[] = [];
if (result.connected) {
lines.push(` ${success("\u25cf")} Connected`);
} else {
lines.push(` ${errorColor("\u25cf")} Disconnected`);
}
lines.push(` ${dim("Backend:")} ${result.backend ?? "?"}`);
if (result.base_url) {
lines.push(` ${dim("API URL:")} ${result.base_url}`);
}
if (result.error) {
lines.push(` ${errorColor("Error:")} ${result.error}`);
}
lines.push(` ${dim("Latency:")} ${elapsed.toFixed(2)}s`);
lines.push(` ${dim("Backend:")} ${result.backend ?? "?"}`);
if (result.base_url) {
lines.push(` ${dim("API URL:")} ${result.base_url}`);
}
if (result.error) {
lines.push(` ${errorColor("Error:")} ${result.error}`);
}
lines.push(` ${dim("Latency:")} ${elapsed.toFixed(2)}s`);
const content = lines.join("\n");
console.log();
console.log(
boxen(content, {
title: brand("Connection Status"),
titleAlignment: "left",
borderColor: "magenta",
padding: 1,
}),
);
console.log();
const content = lines.join("\n");
console.log();
console.log(
boxen(content, {
title: brand("Connection Status"),
titleAlignment: "left",
borderColor: "magenta",
padding: 1,
}),
);
console.log();
}
export function cmdVersion(): void {
console.log(` ${brand("◆ Mem0")} CLI v${__CLI_VERSION__}`);
console.log(` ${brand("◆ Mem0")} CLI v${CLI_VERSION}`);
}
export async function cmdImport(
backend: Backend,
filePath: string,
opts: { userId?: string; agentId?: string; output?: string },
backend: Backend,
filePath: string,
opts: { userId?: string; agentId?: string; output?: string },
): Promise<void> {
let data: Record<string, unknown>[];
try {
const raw = fs.readFileSync(filePath, "utf-8");
const parsed = JSON.parse(raw);
data = Array.isArray(parsed) ? parsed : [parsed];
} catch (e) {
printError(`Failed to read file: ${e instanceof Error ? e.message : e}`);
process.exit(1);
}
let data: Record<string, unknown>[];
try {
const raw = fs.readFileSync(filePath, "utf-8");
const parsed = JSON.parse(raw);
data = Array.isArray(parsed) ? parsed : [parsed];
} catch (e) {
printError(`Failed to read file: ${e instanceof Error ? e.message : e}`);
process.exit(1);
}
let added = 0;
let failed = 0;
const start = performance.now();
let added = 0;
let failed = 0;
const start = performance.now();
for (let i = 0; i < data.length; i++) {
const item = data[i];
const content = (item.memory ?? item.text ?? item.content ?? "") as string;
if (!content) {
failed++;
continue;
}
for (let i = 0; i < data.length; i++) {
const item = data[i];
const content = (item.memory ?? item.text ?? item.content ?? "") as string;
if (!content) {
failed++;
continue;
}
try {
await backend.add(content, undefined, {
userId: opts.userId ?? (item.user_id as string | undefined),
agentId: opts.agentId ?? (item.agent_id as string | undefined),
metadata: item.metadata as Record<string, unknown> | undefined,
});
added++;
} catch {
failed++;
}
try {
await backend.add(content, undefined, {
userId: opts.userId ?? (item.user_id as string | undefined),
agentId: opts.agentId ?? (item.agent_id as string | undefined),
metadata: item.metadata as Record<string, unknown> | undefined,
});
added++;
} catch {
failed++;
}
// Simple progress indicator
if ((i + 1) % 10 === 0 || i === data.length - 1) {
process.stdout.write(`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`);
}
}
// Simple progress indicator
if ((i + 1) % 10 === 0 || i === data.length - 1) {
process.stdout.write(
`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`,
);
}
}
const elapsed = (performance.now() - start) / 1000;
console.log(); // Clear progress line
const elapsed = (performance.now() - start) / 1000;
console.log(); // Clear progress line
if (opts.output === "json") {
formatJsonEnvelope({
command: "import",
data: { added, failed, duration_s: parseFloat(elapsed.toFixed(2)) },
durationMs: Math.round(elapsed * 1000),
});
return;
}
if (opts.output === "json") {
formatJsonEnvelope({
command: "import",
data: {
added,
failed,
duration_s: Number.parseFloat(elapsed.toFixed(2)),
},
durationMs: Math.round(elapsed * 1000),
});
return;
}
printSuccess(`Imported ${added} memories (${elapsed.toFixed(2)}s)`);
if (failed > 0) {
printError(`${failed} memories failed to import.`);
}
printSuccess(`Imported ${added} memories (${elapsed.toFixed(2)}s)`);
if (failed > 0) {
printError(`${failed} memories failed to import.`);
}
}