Compare commits

..
27 changed files with 4485 additions and 204 deletions
+13
View File
@@ -729,6 +729,19 @@ mode: "wide"
<Tab title="TypeScript">
<Update label="2026-03-14" description="v2.4.0">
**Bug Fixes:**
- **OSS Storage:** Fixed `SQLITE_CANTOPEN` errors when running as a LaunchAgent, systemd service, or in containers where `process.cwd()` is read-only (e.g. `/`). Default `vector_store.db` location changed from `process.cwd()/vector_store.db` to `~/.mem0/vector_store.db`.
- **OSS Storage:** Fixed `historyDbPath` config being silently ignored — config merging always overwrote it with defaults. Top-level `historyDbPath` is now correctly propagated into `historyStore.config` with proper precedence.
- **OSS Storage:** Added `ensureSQLiteDirectory()` — parent directories for SQLite database files are now auto-created before opening, preventing `SQLITE_CANTOPEN` when using nested paths.
**Improvements:**
- **Migration:** Added deprecation warning when an existing `vector_store.db` is found at the old `process.cwd()` location, guiding users to move it or set `vectorStore.config.dbPath` explicitly.
- **Config:** Limited default SQLite config spreading to only SQLite history providers, preventing config leaking into Supabase or other providers.
</Update>
<Update label="2026-03-09" description="v2.3.0">
**Breaking Changes:**
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mem0ai",
"version": "2.3.0",
"version": "2.4.0",
"description": "The Memory Layer For Your AI Apps",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
-1
View File
@@ -93,7 +93,6 @@ export class ConfigManager {
return {
baseURL: userConf?.baseURL || defaultConf.baseURL,
url: userConf?.url,
apiKey:
userConf?.apiKey !== undefined
? userConf.apiKey
+1 -1
View File
@@ -12,7 +12,7 @@ export class OllamaEmbedder implements Embedder {
constructor(config: EmbeddingConfig) {
this.ollama = new Ollama({
host: config.url || config.baseURL || "http://localhost:11434",
host: config.url || "http://localhost:11434",
});
this.model = config.model || "nomic-embed-text:latest";
this.embeddingDims = config.embeddingDims || 768;
+1 -1
View File
@@ -11,7 +11,7 @@ export class OllamaLLM implements LLM {
constructor(config: LLMConfig) {
this.ollama = new Ollama({
host: config.url || config.baseURL || "http://localhost:11434",
host: config.config?.url || "http://localhost:11434",
});
this.model = config.model || "llama3.1:8b";
this.ensureModelExists().catch((err) => {
+5 -1
View File
@@ -278,5 +278,9 @@ export function parseMessages(messages: string[]): string {
}
export function removeCodeBlocks(text: string): string {
return text.replace(/```[^`]*```/g, "");
// Extract content inside code fences instead of deleting it.
// The old regex /```[^`]*```/g replaced the entire block (including
// its content) with an empty string, so when an LLM returned JSON
// wrapped in ```json ... ``` the actual payload was discarded.
return text.replace(/```(?:\w+)?\n?([\s\S]*?)```/g, "$1").trim();
}
@@ -210,11 +210,7 @@ describe("backward compat: MemoryVectorStore", () => {
try {
const store = new MemoryVectorStore({ dimension: 3, dbPath });
await store.insert(
[normalize([1, 0, 0])],
["id1"],
[{ text: "hello" }],
);
await store.insert([normalize([1, 0, 0])], ["id1"], [{ text: "hello" }]);
expect(fs.existsSync(dbPath)).toBe(true);
@@ -200,9 +200,9 @@ describe("MemoryVectorStore – path handling", () => {
expect(
fs.existsSync(path.join(fakeHome, ".mem0", "vector_store.db")),
).toBe(true);
expect(
fs.existsSync(path.join(readOnlyCwd, "vector_store.db")),
).toBe(false);
expect(fs.existsSync(path.join(readOnlyCwd, "vector_store.db"))).toBe(
false,
);
} finally {
fs.chmodSync(readOnlyCwd, 0o755);
fs.rmSync(fakeHome, { recursive: true, force: true });
-2
View File
@@ -43,7 +43,6 @@ export interface HistoryStoreConfig {
export interface LLMConfig {
provider?: string;
baseURL?: string;
url?: string;
config?: Record<string, any>;
apiKey?: string;
model?: string | any;
@@ -144,7 +143,6 @@ export const MemoryConfigSchema = z.object({
model: z.union([z.string(), z.any()]).optional(),
modelProperties: z.record(z.string(), z.any()).optional(),
baseURL: z.string().optional(),
url: z.string().optional(),
}),
}),
historyDbPath: z.string().optional(),
+1 -4
View File
@@ -273,10 +273,7 @@ export class Qdrant implements VectorStore {
}
}
private async ensureCollection(
name: string,
size: number,
): Promise<void> {
private async ensureCollection(name: string, size: number): Promise<void> {
try {
await this.client.createCollection(name, {
vectors: {
@@ -85,80 +85,4 @@ describe("ConfigManager", () => {
expect(config.vectorStore.config.dimension).toBe(768);
});
});
describe("mergeConfig - LLM url passthrough for Ollama", () => {
const baseEmbedder = {
provider: "openai",
config: { apiKey: "test-key" },
};
const baseVectorStore = {
provider: "memory",
config: { collectionName: "test" },
};
it("should preserve url in LLM config when provided", () => {
const config = ConfigManager.mergeConfig({
embedder: baseEmbedder,
vectorStore: baseVectorStore,
llm: {
provider: "ollama",
config: { model: "llama3.2:3b", url: "http://10.0.0.100:11434" },
},
});
expect(config.llm.config.url).toBe("http://10.0.0.100:11434");
});
it("should prefer baseURL over url when both are provided", () => {
const config = ConfigManager.mergeConfig({
embedder: baseEmbedder,
vectorStore: baseVectorStore,
llm: {
provider: "ollama",
config: {
model: "llama3.2:3b",
baseURL: "http://custom:11434",
url: "http://fallback:11434",
},
},
});
expect(config.llm.config.baseURL).toBe("http://custom:11434");
expect(config.llm.config.url).toBe("http://fallback:11434");
});
it("should use default baseURL when no url or baseURL provided", () => {
const config = ConfigManager.mergeConfig({
embedder: baseEmbedder,
vectorStore: baseVectorStore,
llm: {
provider: "ollama",
config: { model: "llama3.2:3b" },
},
});
expect(config.llm.config.url).toBeUndefined();
expect(config.llm.config.baseURL).toBe("https://api.openai.com/v1");
});
it("should preserve url in embedder config (existing behavior)", () => {
const config = ConfigManager.mergeConfig({
embedder: {
provider: "ollama",
config: {
model: "nomic-embed-text",
url: "http://10.0.0.100:11434",
},
},
vectorStore: baseVectorStore,
llm: {
provider: "ollama",
config: { model: "llama3.2:3b", url: "http://10.0.0.100:11434" },
},
});
expect(config.embedder.config.url).toBe("http://10.0.0.100:11434");
expect(config.llm.config.url).toBe("http://10.0.0.100:11434");
});
});
});
@@ -13,7 +13,10 @@
*/
import { MemoryGraph } from "../src/memory/graph_memory";
import { EXTRACT_RELATIONS_PROMPT, getDeleteMessages } from "../src/graphs/utils";
import {
EXTRACT_RELATIONS_PROMPT,
getDeleteMessages,
} from "../src/graphs/utils";
// ---------------------------------------------------------------------------
// Mocks – we replace heavy dependencies so tests run without Neo4j / OpenAI
@@ -100,7 +103,10 @@ describe("_retrieveNodesFromData", () => {
});
const mg = graph();
const result = await mg._retrieveNodesFromData("Alice likes pizza", FILTERS);
const result = await mg._retrieveNodesFromData(
"Alice likes pizza",
FILTERS,
);
expect(result).toEqual({ alice: "person", pizza: "food" });
});
@@ -166,7 +172,9 @@ describe("_retrieveNodesFromData", () => {
toolCalls: [
{
name: "some_other_tool",
arguments: JSON.stringify({ entities: [{ entity: "X", entity_type: "Y" }] }),
arguments: JSON.stringify({
entities: [{ entity: "X", entity_type: "Y" }],
}),
},
],
});
@@ -273,9 +281,7 @@ describe("_establishNodesRelationsFromData", () => {
it("throws on malformed JSON in tool call arguments (no try/catch in source)", async () => {
mockGenerateResponse.mockResolvedValueOnce({
toolCalls: [
{ name: "establish_relationships", arguments: "<<BROKEN>>" },
],
toolCalls: [{ name: "establish_relationships", arguments: "<<BROKEN>>" }],
});
const mg = graph();
@@ -347,7 +353,11 @@ describe("_getDeleteEntitiesFromSearchOutput", () => {
});
const mg = graph();
const result = await mg._getDeleteEntitiesFromSearchOutput(SEARCH_OUTPUT, "Alice hates pizza", FILTERS);
const result = await mg._getDeleteEntitiesFromSearchOutput(
SEARCH_OUTPUT,
"Alice hates pizza",
FILTERS,
);
expect(result).toEqual([
{ source: "alice", relationship: "likes", destination: "pizza" },
@@ -358,7 +368,11 @@ describe("_getDeleteEntitiesFromSearchOutput", () => {
mockGenerateResponse.mockResolvedValueOnce("string response");
const mg = graph();
const result = await mg._getDeleteEntitiesFromSearchOutput(SEARCH_OUTPUT, "x", FILTERS);
const result = await mg._getDeleteEntitiesFromSearchOutput(
SEARCH_OUTPUT,
"x",
FILTERS,
);
expect(result).toEqual([]);
});
@@ -366,7 +380,11 @@ describe("_getDeleteEntitiesFromSearchOutput", () => {
mockGenerateResponse.mockResolvedValueOnce({ toolCalls: [] });
const mg = graph();
const result = await mg._getDeleteEntitiesFromSearchOutput(SEARCH_OUTPUT, "x", FILTERS);
const result = await mg._getDeleteEntitiesFromSearchOutput(
SEARCH_OUTPUT,
"x",
FILTERS,
);
expect(result).toEqual([]);
});
@@ -381,7 +399,11 @@ describe("_getDeleteEntitiesFromSearchOutput", () => {
});
const mg = graph();
const result = await mg._getDeleteEntitiesFromSearchOutput(SEARCH_OUTPUT, "x", FILTERS);
const result = await mg._getDeleteEntitiesFromSearchOutput(
SEARCH_OUTPUT,
"x",
FILTERS,
);
expect(result).toEqual([]);
});
@@ -390,17 +412,29 @@ describe("_getDeleteEntitiesFromSearchOutput", () => {
toolCalls: [
{
name: "delete_graph_memory",
arguments: JSON.stringify({ source: "A", relationship: "r1", destination: "B" }),
arguments: JSON.stringify({
source: "A",
relationship: "r1",
destination: "B",
}),
},
{
name: "delete_graph_memory",
arguments: JSON.stringify({ source: "C", relationship: "r2", destination: "D" }),
arguments: JSON.stringify({
source: "C",
relationship: "r2",
destination: "D",
}),
},
],
});
const mg = graph();
const result = await mg._getDeleteEntitiesFromSearchOutput(SEARCH_OUTPUT, "x", FILTERS);
const result = await mg._getDeleteEntitiesFromSearchOutput(
SEARCH_OUTPUT,
"x",
FILTERS,
);
expect(result).toHaveLength(2);
expect(result[0].source).toBe("a");
expect(result[1].source).toBe("c");
@@ -425,7 +459,11 @@ describe("_getDeleteEntitiesFromSearchOutput", () => {
mockGenerateResponse.mockResolvedValueOnce({ toolCalls: [] });
const mg = graph();
const result = await mg._getDeleteEntitiesFromSearchOutput([], "data", FILTERS);
const result = await mg._getDeleteEntitiesFromSearchOutput(
[],
"data",
FILTERS,
);
expect(result).toEqual([]);
});
});
@@ -481,7 +519,11 @@ describe("_removeSpacesFromEntities (via _establishNodesRelationsFromData)", ()
name: "establish_relationships",
arguments: JSON.stringify({
entities: [
{ source: "New York", relationship: "Capital Of", destination: "United States" },
{
source: "New York",
relationship: "Capital Of",
destination: "United States",
},
],
}),
},
@@ -489,10 +531,18 @@ describe("_removeSpacesFromEntities (via _establishNodesRelationsFromData)", ()
});
const mg = graph();
const result = await mg._establishNodesRelationsFromData("test", FILTERS, {});
const result = await mg._establishNodesRelationsFromData(
"test",
FILTERS,
{},
);
expect(result).toEqual([
{ source: "new_york", relationship: "capital_of", destination: "united_states" },
{
source: "new_york",
relationship: "capital_of",
destination: "united_states",
},
]);
});
});
+13 -4
View File
@@ -31,7 +31,8 @@ describe("Graph prompts — JSON keyword requirement", () => {
it("EXTRACT_RELATIONS_PROMPT produces a message containing 'json' once the suffix is appended", () => {
// graph_memory.ts appends "\nPlease provide your response in JSON format."
const withSuffix =
EXTRACT_RELATIONS_PROMPT + "\nPlease provide your response in JSON format.";
EXTRACT_RELATIONS_PROMPT +
"\nPlease provide your response in JSON format.";
expect(withSuffix.toLowerCase()).toContain("json");
});
@@ -86,13 +87,21 @@ describe("getDeleteMessages", () => {
});
it("handles special characters in userId (e.g. angle brackets, quotes)", () => {
const [system] = getDeleteMessages("mem", "data", '<script>alert("xss")</script>');
const [system] = getDeleteMessages(
"mem",
"data",
'<script>alert("xss")</script>',
);
expect(system).toContain('<script>alert("xss")</script>');
expect(system).not.toContain("USER_ID");
});
it("handles unicode input", () => {
const [system, user] = getDeleteMessages("日本語メモリ", "新しい情報", "ユーザー1");
const [system, user] = getDeleteMessages(
"日本語メモリ",
"新しい情報",
"ユーザー1",
);
expect(system).toContain("ユーザー1");
expect(user).toContain("日本語メモリ");
expect(user).toContain("新しい情報");
@@ -130,7 +139,7 @@ describe("formatEntities", () => {
it("preserves special characters in entity fields", () => {
const result = formatEntities([
{ source: "O'Brien", relationship: "said \"hello\"", destination: "café" },
{ source: "O'Brien", relationship: 'said "hello"', destination: "café" },
]);
expect(result).toContain("O'Brien");
expect(result).toContain('said "hello"');
+23 -13
View File
@@ -151,10 +151,14 @@ describeIfQdrant("Issue #4212/#4173: Qdrant dimension mismatch", () => {
const id1 = uuidv4();
const id2 = uuidv4();
await store.insert([vec1, vec2], [id1, id2], [
{ data: "hello", userId: "u1" },
{ data: "world", userId: "u1" },
]);
await store.insert(
[vec1, vec2],
[id1, id2],
[
{ data: "hello", userId: "u1" },
{ data: "world", userId: "u1" },
],
);
// Search with 768-dim query — this USED TO fail with Bad Request
const results = await store.search(vec1, 2, { userId: "u1" });
@@ -203,9 +207,11 @@ describeIfQdrant("Issue #4212/#4173: Qdrant dimension mismatch", () => {
return {
EmbedderFactory: { create: jest.fn().mockReturnValue(fakeEmbedder) },
VectorStoreFactory: {
create: jest.fn().mockImplementation((_provider: string, config: any) => {
return new QdrantStore(config);
}),
create: jest
.fn()
.mockImplementation((_provider: string, config: any) => {
return new QdrantStore(config);
}),
},
LLMFactory: {
create: jest.fn().mockReturnValue({
@@ -280,9 +286,11 @@ describeIfQdrant("Issue #4212/#4173: Qdrant dimension mismatch", () => {
return {
EmbedderFactory: { create: jest.fn().mockReturnValue(fakeEmbedder) },
VectorStoreFactory: {
create: jest.fn().mockImplementation((_provider: string, config: any) => {
return new QdrantStore(config);
}),
create: jest
.fn()
.mockImplementation((_provider: string, config: any) => {
return new QdrantStore(config);
}),
},
LLMFactory: {
create: jest.fn().mockReturnValue({
@@ -350,9 +358,11 @@ describeIfQdrant("Issue #4212/#4173: Qdrant dimension mismatch", () => {
return {
EmbedderFactory: { create: jest.fn().mockReturnValue(fakeEmbedder) },
VectorStoreFactory: {
create: jest.fn().mockImplementation((_provider: string, config: any) => {
return new QdrantStore(config);
}),
create: jest
.fn()
.mockImplementation((_provider: string, config: any) => {
return new QdrantStore(config);
}),
},
LLMFactory: {
create: jest.fn().mockReturnValue({
@@ -0,0 +1,30 @@
import { removeCodeBlocks } from "../src/prompts";
describe("removeCodeBlocks", () => {
it("extracts JSON from ```json code fence", () => {
const input = '```json\n{"facts": ["hello"]}\n```';
expect(removeCodeBlocks(input)).toBe('{"facts": ["hello"]}');
});
it("extracts content from bare ``` code fence", () => {
const input = '```\n{"key": "value"}\n```';
expect(removeCodeBlocks(input)).toBe('{"key": "value"}');
});
it("returns plain text unchanged", () => {
const input = '{"facts": ["hello"]}';
expect(removeCodeBlocks(input)).toBe('{"facts": ["hello"]}');
});
it("handles multiple code blocks", () => {
const input = '```json\n{"a":1}\n```\nsome text\n```json\n{"b":2}\n```';
expect(removeCodeBlocks(input)).toBe('{"a":1}\n\nsome text\n{"b":2}');
});
it("handles Claude-style response with surrounding text", () => {
const input =
'Here is the JSON:\n```json\n{"facts": ["user likes TypeScript"]}\n```';
expect(removeCodeBlocks(input)).toContain('"facts"');
expect(removeCodeBlocks(input)).not.toContain("```");
});
});
@@ -76,10 +76,14 @@ describe("MemoryVectorStore – full backward compat", () => {
vec2[1] = 1.0;
// Insert
await store.insert([vec1, vec2], ["id-1", "id-2"], [
{ data: "alpha", userId: "u1" },
{ data: "beta", userId: "u1" },
]);
await store.insert(
[vec1, vec2],
["id-1", "id-2"],
[
{ data: "alpha", userId: "u1" },
{ data: "beta", userId: "u1" },
],
);
// Get
const item = await store.get("id-1");
@@ -204,66 +208,87 @@ describe("MemoryVectorStore – full backward compat", () => {
describe("Qdrant – backward compat with mocked client", () => {
function createMockQdrantClient() {
const collections = new Map<string, number>();
const points = new Map<string, { id: string; vector: number[]; payload: any }>();
const points = new Map<
string,
{ id: string; vector: number[]; payload: any }
>();
return {
_collections: collections,
_points: points,
createCollection: jest.fn().mockImplementation(async (name: string, opts: any) => {
if (collections.has(name)) {
const err: any = new Error("Collection already exists");
err.status = 409;
throw err;
}
collections.set(name, opts.vectors.size);
}),
createCollection: jest
.fn()
.mockImplementation(async (name: string, opts: any) => {
if (collections.has(name)) {
const err: any = new Error("Collection already exists");
err.status = 409;
throw err;
}
collections.set(name, opts.vectors.size);
}),
getCollection: jest.fn().mockImplementation(async (name: string) => {
if (!collections.has(name)) {
const err: any = new Error("Not found");
err.status = 404;
throw err;
}
return { config: { params: { vectors: { size: collections.get(name) } } } };
return {
config: { params: { vectors: { size: collections.get(name) } } },
};
}),
getCollections: jest.fn().mockResolvedValue({
collections: [],
}),
upsert: jest.fn().mockImplementation(async (collName: string, opts: any) => {
for (const pt of opts.points) {
points.set(`${collName}:${pt.id}`, { id: pt.id, vector: pt.vector, payload: pt.payload });
}
}),
retrieve: jest.fn().mockImplementation(async (collName: string, opts: any) => {
const results = [];
for (const id of opts.ids) {
const pt = points.get(`${collName}:${id}`);
if (pt) results.push({ id: pt.id, payload: pt.payload });
}
return results;
}),
search: jest.fn().mockImplementation(async (collName: string, opts: any) => {
const results: any[] = [];
points.forEach((pt, key) => {
if (key.startsWith(`${collName}:`)) {
results.push({ id: pt.id, payload: pt.payload, score: 0.9 });
upsert: jest
.fn()
.mockImplementation(async (collName: string, opts: any) => {
for (const pt of opts.points) {
points.set(`${collName}:${pt.id}`, {
id: pt.id,
vector: pt.vector,
payload: pt.payload,
});
}
});
return results.slice(0, opts.limit);
}),
scroll: jest.fn().mockImplementation(async (collName: string, opts: any) => {
const results: any[] = [];
points.forEach((pt, key) => {
if (key.startsWith(`${collName}:`)) {
results.push({ id: pt.id, payload: pt.payload });
}),
retrieve: jest
.fn()
.mockImplementation(async (collName: string, opts: any) => {
const results = [];
for (const id of opts.ids) {
const pt = points.get(`${collName}:${id}`);
if (pt) results.push({ id: pt.id, payload: pt.payload });
}
});
return { points: results.slice(0, opts.limit) };
}),
delete: jest.fn().mockImplementation(async (collName: string, opts: any) => {
for (const id of opts.points) {
points.delete(`${collName}:${id}`);
}
}),
return results;
}),
search: jest
.fn()
.mockImplementation(async (collName: string, opts: any) => {
const results: any[] = [];
points.forEach((pt, key) => {
if (key.startsWith(`${collName}:`)) {
results.push({ id: pt.id, payload: pt.payload, score: 0.9 });
}
});
return results.slice(0, opts.limit);
}),
scroll: jest
.fn()
.mockImplementation(async (collName: string, opts: any) => {
const results: any[] = [];
points.forEach((pt, key) => {
if (key.startsWith(`${collName}:`)) {
results.push({ id: pt.id, payload: pt.payload });
}
});
return { points: results.slice(0, opts.limit) };
}),
delete: jest
.fn()
.mockImplementation(async (collName: string, opts: any) => {
for (const id of opts.points) {
points.delete(`${collName}:${id}`);
}
}),
deleteCollection: jest.fn().mockImplementation(async (name: string) => {
collections.delete(name);
}),
@@ -324,7 +349,10 @@ describe("Qdrant – backward compat with mocked client", () => {
// Insert
await store.insert(
[[1, 2, 3], [4, 5, 6]],
[
[1, 2, 3],
[4, 5, 6],
],
["id-1", "id-2"],
[{ data: "alpha" }, { data: "beta" }],
);
@@ -391,9 +419,9 @@ describe("Redis – backward compat with mocked client", () => {
connect: jest.fn().mockResolvedValue(undefined),
on: jest.fn(),
isOpen: false,
moduleList: jest.fn().mockResolvedValue([
["name", "search", "ver", 20000],
]),
moduleList: jest
.fn()
.mockResolvedValue([["name", "search", "ver", 20000]]),
ft: {
dropIndex: jest.fn().mockResolvedValue(undefined),
create: jest.fn().mockResolvedValue(undefined),
@@ -599,14 +627,17 @@ describe("AzureAISearch – backward compat with mocked client", () => {
createOrUpdateIndex: jest.fn().mockResolvedValue({}),
deleteIndex: jest.fn().mockResolvedValue({}),
})),
AzureKeyCredential: jest.fn().mockImplementation((key: string) => ({ key })),
AzureKeyCredential: jest
.fn()
.mockImplementation((key: string) => ({ key })),
}));
jest.doMock("@azure/identity", () => ({
DefaultAzureCredential: jest.fn(),
}));
AzureAISearch = require("../src/vector_stores/azure_ai_search").AzureAISearch;
AzureAISearch =
require("../src/vector_stores/azure_ai_search").AzureAISearch;
});
afterEach(() => {
@@ -661,7 +692,9 @@ describe("Vectorize – backward compat with mocked client", () => {
jest.doMock("cloudflare", () => {
const mockIndexes = {
list: jest.fn().mockReturnValue({
[Symbol.asyncIterator]: () => ({ next: async () => ({ done: true }) }),
[Symbol.asyncIterator]: () => ({
next: async () => ({ done: true }),
}),
}),
create: jest.fn().mockResolvedValue({}),
delete: jest.fn().mockResolvedValue({}),
@@ -772,9 +805,14 @@ describe("LangchainVectorStore – backward compat", () => {
const { LangchainVectorStore } = require("../src/vector_stores/langchain");
const mockLcStore = {
addVectors: jest.fn().mockResolvedValue(undefined),
similaritySearchVectorWithScore: jest.fn().mockResolvedValue([
[{ metadata: { _mem0_id: "id-1", data: "test" }, pageContent: "" }, 0.95],
]),
similaritySearchVectorWithScore: jest
.fn()
.mockResolvedValue([
[
{ metadata: { _mem0_id: "id-1", data: "test" }, pageContent: "" },
0.95,
],
]),
};
const store = new LangchainVectorStore({
client: mockLcStore,
@@ -821,9 +859,9 @@ describe("LangchainVectorStore – backward compat", () => {
dimension: 4,
});
await expect(
store.insert([[1, 2, 3]], ["id-1"], [{}]),
).rejects.toThrow("Vector dimension mismatch");
await expect(store.insert([[1, 2, 3]], ["id-1"], [{}])).rejects.toThrow(
"Vector dimension mismatch",
);
});
});
@@ -976,11 +1014,27 @@ describe("Memory class – backward compat with all providers", () => {
]);
mockVStore.get.mockResolvedValue({
id: "id-1",
payload: { memory: "test", hash: "h", created_at: new Date().toISOString(), updated_at: new Date().toISOString() },
payload: {
memory: "test",
hash: "h",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
});
mockVStore.list.mockResolvedValue([[
{ id: "id-1", payload: { memory: "test", hash: "h", created_at: new Date().toISOString(), updated_at: new Date().toISOString() } },
], 1]);
mockVStore.list.mockResolvedValue([
[
{
id: "id-1",
payload: {
memory: "test",
hash: "h",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
},
],
1,
]);
mockVectorStoreFactory.create.mockReturnValue(mockVStore);
const mem = new MemoryClass({
@@ -1055,7 +1109,9 @@ describe("Memory class – backward compat with all providers", () => {
};
mockEmbedderFactory.create.mockReturnValue(failingEmbedder);
const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {});
const consoleSpy = jest
.spyOn(console, "error")
.mockImplementation(() => {});
const mem = new MemoryClass({
embedder: { provider: "ollama", config: { model: "test" } },
@@ -1077,4 +1133,3 @@ describe("Memory class – backward compat with all providers", () => {
consoleSpy.mockRestore();
});
});
+4 -4
View File
@@ -30,10 +30,10 @@ class MemoryGraph:
def __init__(self, config):
self.config = config
self.graph = Neo4jGraph(
self.config.graph_store.config.url,
self.config.graph_store.config.username,
self.config.graph_store.config.password,
self.config.graph_store.config.database,
url=self.config.graph_store.config.url,
username=self.config.graph_store.config.username,
password=self.config.graph_store.config.password,
database=self.config.graph_store.config.database,
refresh_schema=False,
driver_config={"notifications_min_severity": "OFF"},
)
+2
View File
@@ -0,0 +1,2 @@
package-manager-strict-version=false
approve-builds=esbuild
+1 -1
View File
@@ -138,7 +138,7 @@ class PlatformProvider implements Mem0Provider {
private async _init(): Promise<void> {
const { default: MemoryClient } = await import("mem0ai");
const opts: Record<string, string> = { apiKey: this.apiKey };
const opts: { apiKey: string; org_id?: string; project_id?: string } = { apiKey: this.apiKey };
if (this.orgId) opts.org_id = this.orgId;
if (this.projectId) opts.project_id = this.projectId;
this.client = new MemoryClient(opts);
+30
View File
@@ -0,0 +1,30 @@
declare module "openclaw/plugin-sdk" {
export interface OpenClawPluginApi {
pluginConfig: Record<string, unknown>;
logger: {
info(msg: string): void;
warn(msg: string): void;
error(msg: string): void;
debug(msg: string): void;
};
resolvePath(p: string): string;
registerTool(
definition: Record<string, unknown>,
metadata?: Record<string, unknown>,
): void;
on(
event: string,
handler: (event: any, ctx: any) => any,
): void;
registerCli(
handler: (context: { program: any }) => void,
options?: Record<string, unknown>,
): void;
registerService(service: {
id: string;
start: () => void;
stop: () => void;
}): void;
[key: string]: unknown;
}
}
+18 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/openclaw-mem0",
"version": "0.3.1",
"version": "0.3.3",
"type": "module",
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
"license": "Apache-2.0",
@@ -11,7 +11,20 @@
"mem0",
"long-term-memory"
],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist",
"openclaw.plugin.json"
],
"scripts": {
"build": "tsup",
"test": "vitest run"
},
"dependencies": {
@@ -20,10 +33,13 @@
},
"openclaw": {
"extensions": [
"./index.ts"
"./dist/index.js"
]
},
"devDependencies": {
"@types/node": "^22.15.0",
"tsup": "^8.5.0",
"typescript": "^5.8.3",
"vitest": "^4.0.18"
}
}
+4105
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
approveBuilds: esbuild
onlyBuiltDependencies:
- better-sqlite3
- esbuild
- protobufjs
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": ".",
"strict": false,
"noImplicitAny": false,
"types": ["node"],
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"verbatimModuleSyntax": true
},
"include": ["index.ts", "openclaw-plugin-sdk.d.ts"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["index.ts"],
format: ["esm"],
dts: true,
sourcemap: true,
clean: true,
});
+1 -1
View File
@@ -20,7 +20,7 @@ dependencies = [
"posthog>=3.5.0",
"pytz>=2024.1",
"sqlalchemy>=2.0.31",
"protobuf>=5.29.0,<6.0.0",
"protobuf>=5.29.6,<7.0.0",
]
[project.optional-dependencies]
+6
View File
@@ -13,6 +13,12 @@ When installed, Claude can:
## Installation
### CLI (Claude Code, OpenCode, OpenClaw, or any tool that supports skills)
```bash
npx skills add https://github.com/mem0ai/mem0 --skill mem0
```
### Claude.ai
1. Download this `skills/mem0` folder as a ZIP