feat: add import and event commands, refactor CLI, remove baseUrl config, update docs (#4704)

This commit is contained in:
Kartik
2026-04-06 16:54:27 +05:30
committed by GitHub
parent 4642a1d6e3
commit b13748c446
24 changed files with 924 additions and 395 deletions
+197 -52
View File
@@ -58,6 +58,8 @@ interface MockCommand {
_args: Array<{ name: string; desc: string }>;
command(name: string): MockCommand;
description(desc: string): MockCommand;
configureHelp(opts: any): MockCommand;
hook(event: string, fn: (...args: any[]) => any): MockCommand;
option(flags: string, desc: string, defaultVal?: string): MockCommand;
argument(name: string, desc: string): MockCommand;
action(fn: (...args: any[]) => any): MockCommand;
@@ -80,6 +82,12 @@ function createMockCommand(name: string): MockCommand {
cmd._description = desc;
return cmd;
},
configureHelp(_opts: any) {
return cmd;
},
hook(_event: string, _fn: (...args: any[]) => any) {
return cmd;
},
option(flags: string, desc: string, defaultVal?: string) {
cmd._options.push({ flags, desc, defaultVal });
return cmd;
@@ -145,6 +153,18 @@ function createMockBackend() {
connected: true,
url: "https://api.mem0.ai",
}),
add: vi.fn().mockResolvedValue({ id: "new-1" }),
listEvents: vi.fn().mockResolvedValue([
{ id: "evt-1111-2222-3333-4444", event_type: "ADD", status: "SUCCEEDED", latency: 1500, created_at: "2026-04-01T12:00:00Z" },
]),
getEvent: vi.fn().mockResolvedValue({
id: "evt-1111-2222-3333-4444",
event_type: "ADD",
status: "SUCCEEDED",
latency: 1500,
created_at: "2026-04-01T12:00:00Z",
updated_at: "2026-04-01T12:00:01Z",
}),
};
}
@@ -284,7 +304,6 @@ describe("registerCliCommands", () => {
expect(names).toContain("list");
expect(names).toContain("update");
expect(names).toContain("delete");
expect(names).toContain("history");
expect(names).toContain("status");
expect(names).toContain("config");
expect(names).toContain("dream");
@@ -321,7 +340,6 @@ describe("registerCliCommands", () => {
expect(writePluginAuth).toHaveBeenCalledWith(
expect.objectContaining({
apiKey: "m0-my-key-1234",
baseUrl: "https://api.mem0.ai",
mode: "platform",
}),
);
@@ -857,48 +875,6 @@ describe("registerCliCommands", () => {
});
});
// ========================================================================
// history subcommand
// ========================================================================
describe("history subcommand", () => {
it("calls provider.history and prints entries as JSON", async () => {
const { mem0, provider } = setup();
const historyCmd = findCommand(mem0, "history")!;
await historyCmd._action!("m1");
expect(provider.history).toHaveBeenCalledWith("m1");
expect(consoleSpy.log).toHaveBeenCalledWith(
expect.stringContaining("UPDATE"),
);
});
it("prints message when no history found", async () => {
const { mem0, provider } = setup();
provider.history.mockResolvedValueOnce([]);
const historyCmd = findCommand(mem0, "history")!;
await historyCmd._action!("m1");
expect(consoleSpy.log).toHaveBeenCalledWith(
"No history found for this memory.",
);
});
it("handles history errors gracefully", async () => {
const { mem0, provider } = setup();
provider.history.mockRejectedValueOnce(new Error("history boom"));
const historyCmd = findCommand(mem0, "history")!;
await historyCmd._action!("m1");
expect(consoleSpy.error).toHaveBeenCalledWith(
expect.stringContaining("History failed"),
);
});
});
// ========================================================================
// status subcommand
// ========================================================================
@@ -964,9 +940,9 @@ describe("registerCliCommands", () => {
const allOutput = consoleSpy.log.mock.calls.map((c) => c[0]).join("\n");
expect(allOutput).toContain("Key");
expect(allOutput).toContain("Value");
expect(allOutput).toContain("platform.api_key");
expect(allOutput).toContain("platform.base_url");
expect(allOutput).toContain("defaults.user_id");
expect(allOutput).toContain("api_key");
expect(allOutput).toContain("email");
expect(allOutput).toContain("user_id");
expect(allOutput).toContain("Config file:");
});
});
@@ -1026,14 +1002,14 @@ describe("registerCliCommands", () => {
expect(logged).not.toContain("supersecret");
});
it("supports dot-notation keys like platform.api_key", () => {
it("supports short alias keys like email", () => {
const { mem0 } = setup();
const configCmd = findCommand(mem0, "config")!;
const getCmd = findCommand(configCmd, "get")!;
getCmd._action!("platform.base_url");
getCmd._action!("email");
expect(consoleSpy.log).toHaveBeenCalledWith("https://api.mem0.ai");
expect(consoleSpy.log).toHaveBeenCalledWith("(not set)");
});
});
@@ -1143,12 +1119,12 @@ describe("registerCliCommands", () => {
expect(writePluginAuth).not.toHaveBeenCalled();
});
it("supports dot-notation keys for set", () => {
it("supports short alias keys for set", () => {
const { mem0 } = setup();
const configCmd = findCommand(mem0, "config")!;
const setCmd = findCommand(configCmd, "set")!;
setCmd._action!("defaults.user_id", "bob");
setCmd._action!("user_id", "bob");
expect(writePluginAuth).toHaveBeenCalledWith(
expect.objectContaining({ userId: "bob" }),
@@ -1268,4 +1244,173 @@ describe("registerCliCommands", () => {
);
});
});
// ========================================================================
// import subcommand
// ========================================================================
describe("import subcommand", () => {
it("imports memories from a JSON array file", async () => {
const { mem0, backend } = setup();
const { readText } = await import("../fs-safe.ts");
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
JSON.stringify([
{ memory: "fact one" },
{ memory: "fact two" },
]),
);
const importCmd = findCommand(mem0, "import")!;
await importCmd._action!("memories.json", {});
expect(backend.add).toHaveBeenCalledTimes(2);
expect(consoleSpy.log).toHaveBeenCalledWith("Imported 2 memories.");
});
it("imports a single JSON object", async () => {
const { mem0, backend } = setup();
const { readText } = await import("../fs-safe.ts");
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
JSON.stringify({ memory: "single fact" }),
);
const importCmd = findCommand(mem0, "import")!;
await importCmd._action!("single.json", {});
expect(backend.add).toHaveBeenCalledTimes(1);
expect(consoleSpy.log).toHaveBeenCalledWith("Imported 1 memories.");
});
it("skips items with no extractable content", async () => {
const { mem0, backend } = setup();
const { readText } = await import("../fs-safe.ts");
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
JSON.stringify([{ memory: "valid" }, { nofield: true }]),
);
const importCmd = findCommand(mem0, "import")!;
await importCmd._action!("mixed.json", {});
expect(backend.add).toHaveBeenCalledTimes(1);
expect(consoleSpy.error).toHaveBeenCalledWith("1 memories failed to import.");
});
it("uses --user-id and --agent-id overrides", async () => {
const { mem0, backend } = setup();
const { readText } = await import("../fs-safe.ts");
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
JSON.stringify([{ memory: "fact" }]),
);
const importCmd = findCommand(mem0, "import")!;
await importCmd._action!("f.json", { userId: "override-user", agentId: "agent-1" });
expect(backend.add).toHaveBeenCalledWith("fact", undefined, expect.objectContaining({
userId: "override-user",
agentId: "agent-1",
}));
});
it("handles file read errors gracefully", async () => {
const { mem0 } = setup();
const { readText } = await import("../fs-safe.ts");
(readText as ReturnType<typeof vi.fn>).mockImplementationOnce(() => {
throw new Error("ENOENT");
});
const importCmd = findCommand(mem0, "import")!;
await importCmd._action!("missing.json", {});
expect(consoleSpy.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to read file"),
);
});
it("handles backend.add failures gracefully", async () => {
const { mem0, backend } = setup();
const { readText } = await import("../fs-safe.ts");
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
JSON.stringify([{ memory: "will fail" }]),
);
backend.add.mockRejectedValueOnce(new Error("API error"));
const importCmd = findCommand(mem0, "import")!;
await importCmd._action!("fail.json", {});
expect(consoleSpy.log).toHaveBeenCalledWith("Imported 0 memories.");
expect(consoleSpy.error).toHaveBeenCalledWith("1 memories failed to import.");
});
});
// ========================================================================
// event subcommand
// ========================================================================
describe("event subcommand", () => {
it("lists events in table format", async () => {
const { mem0, backend } = setup();
const eventCmd = findCommand(mem0, "event")!;
const listCmd = findCommand(eventCmd, "list")!;
await listCmd._action!();
expect(backend.listEvents).toHaveBeenCalled();
expect(consoleSpy.log).toHaveBeenCalledWith(
expect.stringContaining("evt-1111-2222-3333-4444"),
);
expect(consoleSpy.log).toHaveBeenCalledWith(
expect.stringContaining("1 event"),
);
});
it("prints message when no events found", async () => {
const { mem0, backend } = setup();
backend.listEvents.mockResolvedValueOnce([]);
const eventCmd = findCommand(mem0, "event")!;
const listCmd = findCommand(eventCmd, "list")!;
await listCmd._action!();
expect(consoleSpy.log).toHaveBeenCalledWith("No events found.");
});
it("handles event list errors gracefully", async () => {
const { mem0, backend } = setup();
backend.listEvents.mockRejectedValueOnce(new Error("event boom"));
const eventCmd = findCommand(mem0, "event")!;
const listCmd = findCommand(eventCmd, "list")!;
await listCmd._action!();
expect(consoleSpy.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to list events"),
);
});
it("shows event status details", async () => {
const { mem0, backend } = setup();
const eventCmd = findCommand(mem0, "event")!;
const statusCmd = findCommand(eventCmd, "status")!;
await statusCmd._action!("evt-1111-2222-3333-4444");
expect(backend.getEvent).toHaveBeenCalledWith("evt-1111-2222-3333-4444");
expect(consoleSpy.log).toHaveBeenCalledWith("Event ID: evt-1111-2222-3333-4444");
expect(consoleSpy.log).toHaveBeenCalledWith("Type: ADD");
expect(consoleSpy.log).toHaveBeenCalledWith("Status: SUCCEEDED");
});
it("handles event status errors gracefully", async () => {
const { mem0, backend } = setup();
backend.getEvent.mockRejectedValueOnce(new Error("not found"));
const eventCmd = findCommand(mem0, "event")!;
const statusCmd = findCommand(eventCmd, "status")!;
await statusCmd._action!("bad-id");
expect(consoleSpy.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to get event"),
);
});
});
});
+37
View File
@@ -0,0 +1,37 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { bootstrapTelemetryFlag } from "../fs-safe.ts";
describe("bootstrapTelemetryFlag", () => {
const originalEnv = process.env.MEM0_TELEMETRY;
beforeEach(() => {
delete (globalThis as any).__mem0_telemetry_override;
delete process.env.MEM0_TELEMETRY;
});
afterEach(() => {
delete (globalThis as any).__mem0_telemetry_override;
if (originalEnv !== undefined) {
process.env.MEM0_TELEMETRY = originalEnv;
} else {
delete process.env.MEM0_TELEMETRY;
}
});
it("sets globalThis override when MEM0_TELEMETRY is set", () => {
process.env.MEM0_TELEMETRY = "false";
bootstrapTelemetryFlag();
expect((globalThis as any).__mem0_telemetry_override).toBe("false");
});
it("does not set globalThis override when MEM0_TELEMETRY is unset", () => {
bootstrapTelemetryFlag();
expect((globalThis as any).__mem0_telemetry_override).toBeUndefined();
});
it("passes through truthy values", () => {
process.env.MEM0_TELEMETRY = "true";
bootstrapTelemetryFlag();
expect((globalThis as any).__mem0_telemetry_override).toBe("true");
});
});
+65
View File
@@ -0,0 +1,65 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Mock config-file before importing telemetry
vi.mock("../cli/config-file.ts", () => ({
readPluginAuth: vi.fn().mockReturnValue({}),
}));
import { captureEvent, PLUGIN_VERSION } from "../telemetry.ts";
import { readPluginAuth } from "../cli/config-file.ts";
describe("telemetry", () => {
let fetchSpy: ReturnType<typeof vi.fn>;
beforeEach(() => {
// Reset telemetry enabled state
(globalThis as any).__mem0_telemetry_override = undefined;
fetchSpy = vi.fn().mockResolvedValue({ ok: true });
vi.stubGlobal("fetch", fetchSpy);
});
afterEach(() => {
vi.restoreAllMocks();
delete (globalThis as any).__mem0_telemetry_override;
});
it("exports PLUGIN_VERSION", () => {
expect(PLUGIN_VERSION).toBe("1.0.4");
});
it("captureEvent does not throw", () => {
expect(() => captureEvent("test_event")).not.toThrow();
});
it("captureEvent accepts properties and context", () => {
expect(() =>
captureEvent("test_event", { key: "val" }, { apiKey: "m0-key", mode: "platform" }),
).not.toThrow();
});
it("captureEvent is silent when telemetry disabled", () => {
(globalThis as any).__mem0_telemetry_override = "false";
// Force re-evaluation by resetting cached value
// Since _telemetryEnabled is module-level, we test indirectly
expect(() => captureEvent("test_event")).not.toThrow();
});
it("uses userEmail as distinct ID when available", () => {
(readPluginAuth as ReturnType<typeof vi.fn>).mockReturnValueOnce({
userEmail: "test@example.com",
});
expect(() => captureEvent("test_event")).not.toThrow();
});
it("falls back to anonymous-openclaw when no apiKey", () => {
(readPluginAuth as ReturnType<typeof vi.fn>).mockReturnValueOnce({});
expect(() => captureEvent("test_event", {}, {})).not.toThrow();
});
it("handles readPluginAuth errors gracefully", () => {
(readPluginAuth as ReturnType<typeof vi.fn>).mockImplementationOnce(() => {
throw new Error("config read failed");
});
expect(() => captureEvent("test_event")).not.toThrow();
});
});
+8 -116
View File
@@ -15,7 +15,8 @@ import { createMemoryGetTool } from "../tools/memory-get.ts";
import { createMemoryDeleteTool } from "../tools/memory-delete.ts";
import { createMemoryListTool } from "../tools/memory-list.ts";
import { createMemoryUpdateTool } from "../tools/memory-update.ts";
import { createMemoryHistoryTool } from "../tools/memory-history.ts";
import { createMemoryEventListTool } from "../tools/memory-event-list.ts";
import { createMemoryEventStatusTool } from "../tools/memory-event-status.ts";
// ---------------------------------------------------------------------------
// Mock helper
@@ -80,10 +81,10 @@ function createMockToolDeps(overrides = {}): ToolDeps {
// ---------------------------------------------------------------------------
describe("registerAllTools", () => {
it("calls api.registerTool exactly 7 times", () => {
it("calls api.registerTool exactly 8 times", () => {
const ctx = createMockToolDeps();
registerAllTools(ctx);
expect(ctx.api.registerTool).toHaveBeenCalledTimes(7);
expect(ctx.api.registerTool).toHaveBeenCalledTimes(8);
});
it("registers tools with the correct names", () => {
@@ -103,7 +104,8 @@ describe("registerAllTools", () => {
"memory_list",
"memory_update",
"memory_delete",
"memory_history",
"memory_event_list",
"memory_event_status",
]);
});
@@ -129,6 +131,8 @@ describe("tool factory shape", () => {
{ fn: createMemoryGetTool, expectedName: "memory_get" },
{ fn: createMemoryDeleteTool, expectedName: "memory_delete" },
{ fn: createMemoryListTool, expectedName: "memory_list" },
{ fn: createMemoryEventListTool, expectedName: "memory_event_list" },
{ fn: createMemoryEventStatusTool, expectedName: "memory_event_status" },
];
for (const { fn, expectedName } of factories) {
@@ -883,115 +887,3 @@ describe("memory_update execute", () => {
});
});
// ---------------------------------------------------------------------------
// memory_history execute
// ---------------------------------------------------------------------------
describe("memory_history execute", () => {
it("returns formatted history", async () => {
const historyEntries = [
{
event: "ADD",
created_at: "2026-01-01T00:00:00Z",
old_memory: null,
new_memory: "Initial memory text",
},
{
event: "UPDATE",
created_at: "2026-01-02T00:00:00Z",
old_memory: "Initial memory text",
new_memory: "Updated memory text",
},
];
const ctx = createMockToolDeps({
provider: {
search: vi.fn(),
add: vi.fn(),
getAll: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
get: vi.fn(),
history: vi.fn().mockResolvedValue(historyEntries),
},
});
const tool = createMemoryHistoryTool(ctx);
const result = await tool.execute("call-1", { memoryId: "mem-abc" });
expect(ctx.provider!.history).toHaveBeenCalledWith("mem-abc");
expect(result.content[0].text).toContain("History for memory mem-abc");
expect(result.content[0].text).toContain("2 entries");
expect(result.content[0].text).toContain("[ADD]");
expect(result.content[0].text).toContain("[UPDATE]");
expect(result.content[0].text).toContain("Initial memory text");
expect(result.content[0].text).toContain("Updated memory text");
expect(result.content[0].text).toContain("Old: (none)");
expect(result.details.count).toBe(2);
expect(result.details.history).toEqual(historyEntries);
});
it("handles empty history", async () => {
const ctx = createMockToolDeps({
provider: {
search: vi.fn(),
add: vi.fn(),
getAll: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
get: vi.fn(),
history: vi.fn().mockResolvedValue([]),
},
});
const tool = createMemoryHistoryTool(ctx);
const result = await tool.execute("call-2", { memoryId: "mem-empty" });
expect(result.content[0].text).toBe(
"No history found for memory mem-empty.",
);
expect(result.details.count).toBe(0);
});
it("handles null history", async () => {
const ctx = createMockToolDeps({
provider: {
search: vi.fn(),
add: vi.fn(),
getAll: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
get: vi.fn(),
history: vi.fn().mockResolvedValue(null),
},
});
const tool = createMemoryHistoryTool(ctx);
const result = await tool.execute("call-3", { memoryId: "mem-null" });
expect(result.content[0].text).toBe(
"No history found for memory mem-null.",
);
expect(result.details.count).toBe(0);
});
it("handles errors gracefully", async () => {
const ctx = createMockToolDeps({
provider: {
search: vi.fn(),
add: vi.fn(),
getAll: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
get: vi.fn(),
history: vi.fn().mockRejectedValue(new Error("history unavailable")),
},
});
const tool = createMemoryHistoryTool(ctx);
const result = await tool.execute("call-4", { memoryId: "mem-err" });
expect(result.content[0].text).toContain("Memory history failed");
expect(result.content[0].text).toContain("history unavailable");
expect(result.details.error).toContain("history unavailable");
});
});