@mem0/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -0
- package/development.md +91 -0
- package/dist/chunk-EJ5AQPMT.js +120 -0
- package/dist/chunk-I7ABQZUR.js +111 -0
- package/dist/chunk-J7DYZDMM.js +187 -0
- package/dist/chunk-O3XZVUUX.js +252 -0
- package/dist/config-WKOCXNAS.js +86 -0
- package/dist/entities-XPRXH4X4.js +119 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +523 -0
- package/dist/init-N25QFHYP.js +161 -0
- package/dist/memory-JYJGE4VO.js +387 -0
- package/dist/utils-BAMFZ5H5.js +124 -0
- package/package.json +42 -0
- package/src/backend/base.ts +115 -0
- package/src/backend/index.ts +7 -0
- package/src/backend/platform.ts +303 -0
- package/src/branding.ts +145 -0
- package/src/commands/config.ts +90 -0
- package/src/commands/entities.ts +139 -0
- package/src/commands/init.ts +182 -0
- package/src/commands/memory.ts +487 -0
- package/src/commands/utils.ts +139 -0
- package/src/config.ts +159 -0
- package/src/help.ts +374 -0
- package/src/index.ts +501 -0
- package/src/output.ts +230 -0
- package/tests/branding.test.ts +98 -0
- package/tests/cli-integration.test.ts +156 -0
- package/tests/commands.test.ts +221 -0
- package/tests/config.test.ts +113 -0
- package/tests/output.test.ts +115 -0
- package/tests/setup.ts +75 -0
- package/tsconfig.json +18 -0
package/src/output.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatting for mem0 CLI — text, JSON, table, quiet modes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import Table from "cli-table3";
|
|
6
|
+
import boxen from "boxen";
|
|
7
|
+
import { colors, sym } from "./branding.js";
|
|
8
|
+
|
|
9
|
+
const { brand, accent, success, error: errorColor, dim } = colors;
|
|
10
|
+
|
|
11
|
+
function formatDate(dtStr?: string): string | undefined {
|
|
12
|
+
if (!dtStr) return undefined;
|
|
13
|
+
try {
|
|
14
|
+
const dt = new Date(dtStr.replace("Z", "+00:00"));
|
|
15
|
+
return dt.toISOString().slice(0, 10);
|
|
16
|
+
} catch {
|
|
17
|
+
return dtStr?.slice(0, 10);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatMemoriesText(
|
|
22
|
+
memories: Record<string, unknown>[],
|
|
23
|
+
title = "memories",
|
|
24
|
+
): void {
|
|
25
|
+
const count = memories.length;
|
|
26
|
+
console.log(`\n${brand(`Found ${count} ${title}:`)}\n`);
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < memories.length; i++) {
|
|
29
|
+
const mem = memories[i];
|
|
30
|
+
const memoryText = (mem.memory ?? mem.text ?? "") as string;
|
|
31
|
+
const memId = ((mem.id as string) ?? "").slice(0, 8);
|
|
32
|
+
const score = mem.score as number | undefined;
|
|
33
|
+
const created = formatDate(mem.created_at as string | undefined);
|
|
34
|
+
let category: string | undefined;
|
|
35
|
+
const cats = mem.categories;
|
|
36
|
+
if (Array.isArray(cats)) {
|
|
37
|
+
category = cats[0] as string | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(` ${i + 1}. ${memoryText}`);
|
|
41
|
+
|
|
42
|
+
const details: string[] = [];
|
|
43
|
+
if (score !== undefined) details.push(`Score: ${score.toFixed(2)}`);
|
|
44
|
+
if (memId) details.push(`ID: ${memId}`);
|
|
45
|
+
if (created) details.push(`Created: ${created}`);
|
|
46
|
+
if (category) details.push(`Category: ${category}`);
|
|
47
|
+
|
|
48
|
+
if (details.length > 0) {
|
|
49
|
+
console.log(` ${dim(details.join(" · "))}`);
|
|
50
|
+
}
|
|
51
|
+
console.log();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function formatMemoriesTable(memories: Record<string, unknown>[]): void {
|
|
56
|
+
const table = new Table({
|
|
57
|
+
head: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")],
|
|
58
|
+
colWidths: [12, 52, 16, 14],
|
|
59
|
+
wordWrap: true,
|
|
60
|
+
style: { head: [], border: [] },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
for (const mem of memories) {
|
|
64
|
+
const memId = ((mem.id as string) ?? "").slice(0, 8);
|
|
65
|
+
let memoryText = (mem.memory ?? mem.text ?? "") as string;
|
|
66
|
+
if (memoryText.length > 60) {
|
|
67
|
+
memoryText = memoryText.slice(0, 57) + "...";
|
|
68
|
+
}
|
|
69
|
+
const categories = mem.categories;
|
|
70
|
+
const cat =
|
|
71
|
+
Array.isArray(categories) && categories.length > 0
|
|
72
|
+
? (categories[0] as string)
|
|
73
|
+
: "—";
|
|
74
|
+
const created = formatDate(mem.created_at as string | undefined) ?? "—";
|
|
75
|
+
table.push([dim(memId), memoryText, cat, created]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log();
|
|
79
|
+
console.log(table.toString());
|
|
80
|
+
console.log();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function formatJson(data: unknown): void {
|
|
84
|
+
console.log(JSON.stringify(data, null, 2));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function formatSingleMemory(
|
|
88
|
+
mem: Record<string, unknown>,
|
|
89
|
+
output = "text",
|
|
90
|
+
): void {
|
|
91
|
+
if (output === "json") {
|
|
92
|
+
formatJson(mem);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const memoryText = (mem.memory ?? mem.text ?? "") as string;
|
|
97
|
+
const memId = (mem.id ?? "") as string;
|
|
98
|
+
|
|
99
|
+
const lines: string[] = [];
|
|
100
|
+
lines.push(` ${memoryText}`);
|
|
101
|
+
lines.push("");
|
|
102
|
+
|
|
103
|
+
if (memId) lines.push(` ${dim("ID:")} ${memId}`);
|
|
104
|
+
const created = formatDate(mem.created_at as string | undefined);
|
|
105
|
+
if (created) lines.push(` ${dim("Created:")} ${created}`);
|
|
106
|
+
const updated = formatDate(mem.updated_at as string | undefined);
|
|
107
|
+
if (updated) lines.push(` ${dim("Updated:")} ${updated}`);
|
|
108
|
+
const meta = mem.metadata;
|
|
109
|
+
if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`);
|
|
110
|
+
const categories = mem.categories;
|
|
111
|
+
if (categories) {
|
|
112
|
+
const catStr = Array.isArray(categories) ? categories.join(", ") : String(categories);
|
|
113
|
+
lines.push(` ${dim("Categories:")} ${catStr}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const content = lines.join("\n");
|
|
117
|
+
console.log();
|
|
118
|
+
console.log(
|
|
119
|
+
boxen(content, {
|
|
120
|
+
title: brand("Memory"),
|
|
121
|
+
titleAlignment: "left",
|
|
122
|
+
borderColor: "magenta",
|
|
123
|
+
padding: 1,
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
console.log();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function formatAddResult(
|
|
130
|
+
result: Record<string, unknown> | Record<string, unknown>[],
|
|
131
|
+
output = "text",
|
|
132
|
+
): void {
|
|
133
|
+
if (output === "json") {
|
|
134
|
+
formatJson(result);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (output === "quiet") return;
|
|
138
|
+
|
|
139
|
+
const results: Record<string, unknown>[] = Array.isArray(result)
|
|
140
|
+
? result
|
|
141
|
+
: ((result.results as Record<string, unknown>[]) ?? [result]);
|
|
142
|
+
|
|
143
|
+
if (!results.length) {
|
|
144
|
+
console.log(` ${dim("No memories extracted.")}`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log();
|
|
149
|
+
for (const r of results) {
|
|
150
|
+
// Detect async PENDING response
|
|
151
|
+
if (r.status === "PENDING") {
|
|
152
|
+
const eventId = ((r.event_id as string) ?? "").slice(0, 8);
|
|
153
|
+
const icon = accent(sym("⧗", "..."));
|
|
154
|
+
const parts = [` ${icon} ${dim("Queued".padEnd(10))}`, "Processing in background"];
|
|
155
|
+
if (eventId) parts.push(dim(`(event ${eventId})`));
|
|
156
|
+
console.log(parts.join(" "));
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const event = (r.event ?? "ADD") as string;
|
|
161
|
+
const memory = (r.memory ?? r.text ?? r.content ?? r.data ?? "") as string;
|
|
162
|
+
const memId = ((r.id as string) ?? (r.memory_id as string) ?? "").slice(0, 8);
|
|
163
|
+
|
|
164
|
+
let icon: string;
|
|
165
|
+
let label: string;
|
|
166
|
+
if (event === "ADD") {
|
|
167
|
+
icon = success("+");
|
|
168
|
+
label = "Added";
|
|
169
|
+
} else if (event === "UPDATE") {
|
|
170
|
+
icon = accent("~");
|
|
171
|
+
label = "Updated";
|
|
172
|
+
} else if (event === "DELETE") {
|
|
173
|
+
icon = errorColor("-");
|
|
174
|
+
label = "Deleted";
|
|
175
|
+
} else if (event === "NOOP") {
|
|
176
|
+
icon = dim("·");
|
|
177
|
+
label = "No change";
|
|
178
|
+
} else {
|
|
179
|
+
icon = dim("?");
|
|
180
|
+
label = event;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const parts = [` ${icon} ${dim(label.padEnd(10))}`];
|
|
184
|
+
if (memory) parts.push(memory);
|
|
185
|
+
if (memId) parts.push(dim(`(${memId})`));
|
|
186
|
+
console.log(parts.join(" "));
|
|
187
|
+
}
|
|
188
|
+
console.log();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function formatJsonEnvelope(opts: {
|
|
192
|
+
command: string;
|
|
193
|
+
data: unknown;
|
|
194
|
+
durationMs?: number;
|
|
195
|
+
scope?: Record<string, string | undefined>;
|
|
196
|
+
count?: number;
|
|
197
|
+
status?: string;
|
|
198
|
+
error?: string;
|
|
199
|
+
}): void {
|
|
200
|
+
const envelope: Record<string, unknown> = {
|
|
201
|
+
status: opts.status ?? "success",
|
|
202
|
+
command: opts.command,
|
|
203
|
+
};
|
|
204
|
+
if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs;
|
|
205
|
+
if (opts.scope !== undefined) envelope.scope = opts.scope;
|
|
206
|
+
if (opts.count !== undefined) envelope.count = opts.count;
|
|
207
|
+
if (opts.error) envelope.error = opts.error;
|
|
208
|
+
envelope.data = opts.data;
|
|
209
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function printResultSummary(opts: {
|
|
213
|
+
count: number;
|
|
214
|
+
durationSecs?: number;
|
|
215
|
+
page?: number;
|
|
216
|
+
scopeIds?: Record<string, string | undefined>;
|
|
217
|
+
}): void {
|
|
218
|
+
const parts = [`${opts.count} result${opts.count !== 1 ? "s" : ""}`];
|
|
219
|
+
if (opts.page !== undefined) parts.push(`page ${opts.page}`);
|
|
220
|
+
if (opts.scopeIds) {
|
|
221
|
+
const scopeParts = Object.entries(opts.scopeIds)
|
|
222
|
+
.filter(([, v]) => v)
|
|
223
|
+
.map(([k, v]) => `${k.replace(/_/g, " ")}=${v}`);
|
|
224
|
+
if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
|
|
225
|
+
}
|
|
226
|
+
if (opts.durationSecs !== undefined) parts.push(`${opts.durationSecs.toFixed(2)}s`);
|
|
227
|
+
|
|
228
|
+
console.log(` ${dim(parts.join(" · "))}`);
|
|
229
|
+
console.log();
|
|
230
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for branding utilities.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
BRAND_COLOR,
|
|
8
|
+
SUCCESS_COLOR,
|
|
9
|
+
ERROR_COLOR,
|
|
10
|
+
TAGLINE,
|
|
11
|
+
LOGO_MINI,
|
|
12
|
+
printSuccess,
|
|
13
|
+
printError,
|
|
14
|
+
printWarning,
|
|
15
|
+
printInfo,
|
|
16
|
+
printScope,
|
|
17
|
+
} from "../src/branding.js";
|
|
18
|
+
|
|
19
|
+
let output: string;
|
|
20
|
+
let errOutput: string;
|
|
21
|
+
const originalLog = console.log;
|
|
22
|
+
const originalError = console.error;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
output = "";
|
|
26
|
+
errOutput = "";
|
|
27
|
+
console.log = (...args: unknown[]) => {
|
|
28
|
+
output += args.map(String).join(" ") + "\n";
|
|
29
|
+
};
|
|
30
|
+
console.error = (...args: unknown[]) => {
|
|
31
|
+
errOutput += args.map(String).join(" ") + "\n";
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
console.log = originalLog;
|
|
37
|
+
console.error = originalError;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("branding constants", () => {
|
|
41
|
+
it("has correct brand color", () => {
|
|
42
|
+
expect(BRAND_COLOR).toBe("#8b5cf6");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("has correct tagline", () => {
|
|
46
|
+
expect(TAGLINE).toBe("The Memory Layer for AI Agents");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("has correct logo mini", () => {
|
|
50
|
+
expect(LOGO_MINI).toBe("◆ mem0");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("printSuccess", () => {
|
|
55
|
+
it("prints success message", () => {
|
|
56
|
+
printSuccess("Operation completed");
|
|
57
|
+
expect(output).toContain("Operation completed");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("printError", () => {
|
|
62
|
+
it("prints error message to stderr", () => {
|
|
63
|
+
printError("Something failed");
|
|
64
|
+
expect(errOutput).toContain("Something failed");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("prints hint when provided to stderr", () => {
|
|
68
|
+
printError("Failed", "Try again");
|
|
69
|
+
expect(errOutput).toContain("Try again");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("printWarning", () => {
|
|
74
|
+
it("prints warning message to stderr", () => {
|
|
75
|
+
printWarning("Be careful");
|
|
76
|
+
expect(errOutput).toContain("Be careful");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("printInfo", () => {
|
|
81
|
+
it("prints info message", () => {
|
|
82
|
+
printInfo("Important note");
|
|
83
|
+
expect(output).toContain("Important note");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("printScope", () => {
|
|
88
|
+
it("prints scope when IDs present", () => {
|
|
89
|
+
printScope({ user_id: "alice", agent_id: "bot" });
|
|
90
|
+
expect(output).toContain("alice");
|
|
91
|
+
expect(output).toContain("bot");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("prints nothing when no IDs", () => {
|
|
95
|
+
printScope({});
|
|
96
|
+
expect(output).toBe("");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests — invoke CLI as subprocess to test end-to-end.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
function run(
|
|
12
|
+
args: string[],
|
|
13
|
+
opts: { home?: string; env?: Record<string, string> } = {},
|
|
14
|
+
): { stdout: string; stderr: string; exitCode: number } {
|
|
15
|
+
const env = { ...process.env };
|
|
16
|
+
// Strip MEM0_ env vars
|
|
17
|
+
for (const key of Object.keys(env)) {
|
|
18
|
+
if (key.startsWith("MEM0_")) delete env[key];
|
|
19
|
+
}
|
|
20
|
+
if (opts.home) env.HOME = opts.home;
|
|
21
|
+
if (opts.env) Object.assign(env, opts.env);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const stdout = execSync(
|
|
25
|
+
`npx tsx src/index.ts ${args.join(" ")}`,
|
|
26
|
+
{ cwd: path.join(__dirname, ".."), env, encoding: "utf-8", timeout: 15000 },
|
|
27
|
+
);
|
|
28
|
+
return { stdout, stderr: "", exitCode: 0 };
|
|
29
|
+
} catch (e: any) {
|
|
30
|
+
return {
|
|
31
|
+
stdout: e.stdout ?? "",
|
|
32
|
+
stderr: e.stderr ?? "",
|
|
33
|
+
exitCode: e.status ?? 1,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("CLI Integration — help and version", () => {
|
|
39
|
+
it("shows help with --help", () => {
|
|
40
|
+
const result = run(["--help"]);
|
|
41
|
+
expect(result.exitCode).toBe(0);
|
|
42
|
+
expect(result.stdout).toContain("mem0");
|
|
43
|
+
expect(result.stdout).toContain("add");
|
|
44
|
+
expect(result.stdout).toContain("search");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("shows version with --version", () => {
|
|
48
|
+
const result = run(["--version"]);
|
|
49
|
+
expect(result.exitCode).toBe(0);
|
|
50
|
+
expect(result.stdout).toContain("0.1.0");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
it("help --json produces valid JSON", () => {
|
|
55
|
+
const result = run(["help", "--json"]);
|
|
56
|
+
expect(result.exitCode).toBe(0);
|
|
57
|
+
const parsed = JSON.parse(result.stdout);
|
|
58
|
+
// spec may have cli.name or top-level name
|
|
59
|
+
const name = parsed.name ?? parsed.cli?.name;
|
|
60
|
+
expect(name).toBe("mem0");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("shows add help", () => {
|
|
64
|
+
const result = run(["add", "--help"]);
|
|
65
|
+
expect(result.exitCode).toBe(0);
|
|
66
|
+
expect(result.stdout).toContain("user-id");
|
|
67
|
+
expect(result.stdout).toContain("messages");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("shows search help", () => {
|
|
71
|
+
const result = run(["search", "--help"]);
|
|
72
|
+
expect(result.exitCode).toBe(0);
|
|
73
|
+
expect(result.stdout).toContain("top-k");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("shows list help", () => {
|
|
77
|
+
const result = run(["list", "--help"]);
|
|
78
|
+
expect(result.exitCode).toBe(0);
|
|
79
|
+
expect(result.stdout).toContain("page-size");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("shows delete help with --all, --entity, --project", () => {
|
|
83
|
+
const result = run(["delete", "--help"]);
|
|
84
|
+
expect(result.exitCode).toBe(0);
|
|
85
|
+
expect(result.stdout).toContain("--all");
|
|
86
|
+
expect(result.stdout).toContain("--entity");
|
|
87
|
+
expect(result.stdout).toContain("--project");
|
|
88
|
+
expect(result.stdout).toContain("--force");
|
|
89
|
+
expect(result.stdout.toLowerCase()).toContain("memory");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("delete with no args errors", () => {
|
|
93
|
+
const result = run(["delete"]);
|
|
94
|
+
expect(result.exitCode).not.toBe(0);
|
|
95
|
+
const combined = result.stdout + result.stderr;
|
|
96
|
+
expect(combined).toContain("--all");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("shows entity list help", () => {
|
|
100
|
+
const result = run(["entity", "list", "--help"]);
|
|
101
|
+
expect(result.exitCode).toBe(0);
|
|
102
|
+
expect(result.stdout.toLowerCase()).toContain("entitytype");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("shows entity delete help", () => {
|
|
106
|
+
const result = run(["entity", "delete", "--help"]);
|
|
107
|
+
expect(result.exitCode).toBe(0);
|
|
108
|
+
expect(result.stdout).toContain("--user-id");
|
|
109
|
+
expect(result.stdout).toContain("--force");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("shows import help", () => {
|
|
113
|
+
const result = run(["import", "--help"]);
|
|
114
|
+
expect(result.exitCode).toBe(0);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("add help has --graph flag", () => {
|
|
118
|
+
const result = run(["add", "--help"]);
|
|
119
|
+
expect(result.exitCode).toBe(0);
|
|
120
|
+
expect(result.stdout).toContain("--graph");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("search help has --graph flag", () => {
|
|
124
|
+
const result = run(["search", "--help"]);
|
|
125
|
+
expect(result.exitCode).toBe(0);
|
|
126
|
+
expect(result.stdout).toContain("--graph");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("list help has --graph flag", () => {
|
|
130
|
+
const result = run(["list", "--help"]);
|
|
131
|
+
expect(result.exitCode).toBe(0);
|
|
132
|
+
expect(result.stdout).toContain("--graph");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("CLI Integration — isolated (clean home)", () => {
|
|
137
|
+
function cleanHome(): string {
|
|
138
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
it("add without API key errors", () => {
|
|
142
|
+
const home = cleanHome();
|
|
143
|
+
const result = run(["add", "test", "--user-id", "alice"], { home });
|
|
144
|
+
expect(result.exitCode).not.toBe(0);
|
|
145
|
+
const combined = result.stdout + result.stderr;
|
|
146
|
+
expect(combined.toLowerCase()).toMatch(/api.key|error/i);
|
|
147
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("config show works with clean home", () => {
|
|
151
|
+
const home = cleanHome();
|
|
152
|
+
const result = run(["config", "show"], { home });
|
|
153
|
+
expect(result.exitCode).toBe(0);
|
|
154
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CLI commands using mock backend.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
6
|
+
import { createMockBackend } from "./setup.js";
|
|
7
|
+
import type { Backend } from "../src/backend/base.js";
|
|
8
|
+
|
|
9
|
+
let mockBackend: Backend;
|
|
10
|
+
|
|
11
|
+
// Capture console.log and console.error output
|
|
12
|
+
let output: string;
|
|
13
|
+
let errOutput: string;
|
|
14
|
+
const originalLog = console.log;
|
|
15
|
+
const originalError = console.error;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
mockBackend = createMockBackend();
|
|
19
|
+
output = "";
|
|
20
|
+
errOutput = "";
|
|
21
|
+
console.log = (...args: unknown[]) => {
|
|
22
|
+
output += args.map(String).join(" ") + "\n";
|
|
23
|
+
};
|
|
24
|
+
console.error = (...args: unknown[]) => {
|
|
25
|
+
errOutput += args.map(String).join(" ") + "\n";
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Restore after each test
|
|
30
|
+
import { afterEach } from "vitest";
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
console.log = originalLog;
|
|
33
|
+
console.error = originalError;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("cmdAdd", () => {
|
|
37
|
+
it("adds text memory", async () => {
|
|
38
|
+
const { cmdAdd } = await import("../src/commands/memory.js");
|
|
39
|
+
await cmdAdd(mockBackend, "I prefer dark mode", {
|
|
40
|
+
userId: "alice",
|
|
41
|
+
immutable: false,
|
|
42
|
+
noInfer: false,
|
|
43
|
+
enableGraph: false,
|
|
44
|
+
output: "text",
|
|
45
|
+
});
|
|
46
|
+
expect(mockBackend.add).toHaveBeenCalledOnce();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("adds from messages JSON", async () => {
|
|
50
|
+
const { cmdAdd } = await import("../src/commands/memory.js");
|
|
51
|
+
await cmdAdd(mockBackend, undefined, {
|
|
52
|
+
userId: "alice",
|
|
53
|
+
messages: JSON.stringify([{ role: "user", content: "I love Python" }]),
|
|
54
|
+
immutable: false,
|
|
55
|
+
noInfer: false,
|
|
56
|
+
enableGraph: false,
|
|
57
|
+
output: "text",
|
|
58
|
+
});
|
|
59
|
+
expect(mockBackend.add).toHaveBeenCalledOnce();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("outputs json format", async () => {
|
|
63
|
+
const { cmdAdd } = await import("../src/commands/memory.js");
|
|
64
|
+
await cmdAdd(mockBackend, "test", {
|
|
65
|
+
userId: "alice",
|
|
66
|
+
immutable: false,
|
|
67
|
+
noInfer: false,
|
|
68
|
+
enableGraph: false,
|
|
69
|
+
output: "json",
|
|
70
|
+
});
|
|
71
|
+
expect(output).toContain("results");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("quiet mode produces no memory content", async () => {
|
|
75
|
+
const { cmdAdd } = await import("../src/commands/memory.js");
|
|
76
|
+
await cmdAdd(mockBackend, "test", {
|
|
77
|
+
userId: "alice",
|
|
78
|
+
immutable: false,
|
|
79
|
+
noInfer: false,
|
|
80
|
+
enableGraph: false,
|
|
81
|
+
output: "quiet",
|
|
82
|
+
});
|
|
83
|
+
expect(output).not.toContain("dark mode");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("cmdSearch", () => {
|
|
88
|
+
it("searches and shows results in text mode", async () => {
|
|
89
|
+
const { cmdSearch } = await import("../src/commands/memory.js");
|
|
90
|
+
await cmdSearch(mockBackend, "preferences", {
|
|
91
|
+
userId: "alice",
|
|
92
|
+
topK: 10,
|
|
93
|
+
threshold: 0.3,
|
|
94
|
+
rerank: false,
|
|
95
|
+
keyword: false,
|
|
96
|
+
enableGraph: false,
|
|
97
|
+
output: "text",
|
|
98
|
+
});
|
|
99
|
+
expect(output).toContain("Found 2");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("outputs json format", async () => {
|
|
103
|
+
const { cmdSearch } = await import("../src/commands/memory.js");
|
|
104
|
+
await cmdSearch(mockBackend, "preferences", {
|
|
105
|
+
userId: "alice",
|
|
106
|
+
topK: 10,
|
|
107
|
+
threshold: 0.3,
|
|
108
|
+
rerank: false,
|
|
109
|
+
keyword: false,
|
|
110
|
+
enableGraph: false,
|
|
111
|
+
output: "json",
|
|
112
|
+
});
|
|
113
|
+
expect(output).toContain("memory");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("shows no results message", async () => {
|
|
117
|
+
(mockBackend.search as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
|
118
|
+
const { cmdSearch } = await import("../src/commands/memory.js");
|
|
119
|
+
await cmdSearch(mockBackend, "nonexistent", {
|
|
120
|
+
userId: "alice",
|
|
121
|
+
topK: 10,
|
|
122
|
+
threshold: 0.3,
|
|
123
|
+
rerank: false,
|
|
124
|
+
keyword: false,
|
|
125
|
+
enableGraph: false,
|
|
126
|
+
output: "text",
|
|
127
|
+
});
|
|
128
|
+
expect(output).toContain("No memories found");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("cmdGet", () => {
|
|
133
|
+
it("gets memory in text mode", async () => {
|
|
134
|
+
const { cmdGet } = await import("../src/commands/memory.js");
|
|
135
|
+
await cmdGet(mockBackend, "abc-123-def-456", { output: "text" });
|
|
136
|
+
expect(output).toContain("dark mode");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("gets memory in json mode", async () => {
|
|
140
|
+
const { cmdGet } = await import("../src/commands/memory.js");
|
|
141
|
+
await cmdGet(mockBackend, "abc-123-def-456", { output: "json" });
|
|
142
|
+
expect(output).toContain("memory");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("cmdList", () => {
|
|
147
|
+
it("lists in table mode", async () => {
|
|
148
|
+
const { cmdList } = await import("../src/commands/memory.js");
|
|
149
|
+
await cmdList(mockBackend, {
|
|
150
|
+
userId: "alice",
|
|
151
|
+
page: 1,
|
|
152
|
+
pageSize: 100,
|
|
153
|
+
enableGraph: false,
|
|
154
|
+
output: "table",
|
|
155
|
+
});
|
|
156
|
+
expect(output).toContain("dark mode");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("shows empty message", async () => {
|
|
160
|
+
(mockBackend.listMemories as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
|
161
|
+
const { cmdList } = await import("../src/commands/memory.js");
|
|
162
|
+
await cmdList(mockBackend, {
|
|
163
|
+
userId: "alice",
|
|
164
|
+
page: 1,
|
|
165
|
+
pageSize: 100,
|
|
166
|
+
enableGraph: false,
|
|
167
|
+
output: "text",
|
|
168
|
+
});
|
|
169
|
+
expect(output).toContain("No memories found");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("cmdUpdate", () => {
|
|
174
|
+
it("updates memory", async () => {
|
|
175
|
+
const { cmdUpdate } = await import("../src/commands/memory.js");
|
|
176
|
+
await cmdUpdate(mockBackend, "abc-123", "New text", { output: "text" });
|
|
177
|
+
expect(output.toLowerCase()).toContain("updated");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("cmdDelete", () => {
|
|
182
|
+
it("deletes memory", async () => {
|
|
183
|
+
const { cmdDelete } = await import("../src/commands/memory.js");
|
|
184
|
+
await cmdDelete(mockBackend, "abc-123", { output: "text" });
|
|
185
|
+
expect(output.toLowerCase()).toContain("deleted");
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("cmdDeleteAll", () => {
|
|
190
|
+
it("deletes all with force", async () => {
|
|
191
|
+
const { cmdDeleteAll } = await import("../src/commands/memory.js");
|
|
192
|
+
await cmdDeleteAll(mockBackend, {
|
|
193
|
+
force: true,
|
|
194
|
+
userId: "alice",
|
|
195
|
+
output: "text",
|
|
196
|
+
});
|
|
197
|
+
expect(output.toLowerCase()).toContain("deleted");
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("cmdVersion", () => {
|
|
202
|
+
it("shows version", async () => {
|
|
203
|
+
const { cmdVersion } = await import("../src/commands/utils.js");
|
|
204
|
+
cmdVersion();
|
|
205
|
+
expect(output).toContain("0.1.0");
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("cmdEntitiesList", () => {
|
|
210
|
+
it("lists users in table mode", async () => {
|
|
211
|
+
const { cmdEntitiesList } = await import("../src/commands/entities.js");
|
|
212
|
+
await cmdEntitiesList(mockBackend, "users", { output: "table" });
|
|
213
|
+
expect(output).toContain("alice");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("lists in json mode", async () => {
|
|
217
|
+
const { cmdEntitiesList } = await import("../src/commands/entities.js");
|
|
218
|
+
await cmdEntitiesList(mockBackend, "users", { output: "json" });
|
|
219
|
+
expect(output).toContain("alice");
|
|
220
|
+
});
|
|
221
|
+
});
|