Compare commits

...

5 Commits

33 changed files with 982 additions and 473 deletions
+2 -5
View File
@@ -30,9 +30,6 @@ jobs:
cache: 'pnpm'
cache-dependency-path: cli/node/pnpm-lock.yaml
- name: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -43,7 +40,7 @@ jobs:
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npm publish --provenance --access public --tag "$PREID"
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npm publish --provenance --access public
npx npm@latest publish --provenance --access public
fi
+2 -5
View File
@@ -30,9 +30,6 @@ jobs:
cache: 'pnpm'
cache-dependency-path: openclaw/pnpm-lock.yaml
- name: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -43,7 +40,7 @@ jobs:
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npm publish --provenance --access public --tag "$PREID"
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npm publish --provenance --access public
npx npm@latest publish --provenance --access public
fi
+2 -5
View File
@@ -30,9 +30,6 @@ jobs:
cache: 'pnpm'
cache-dependency-path: mem0-ts/pnpm-lock.yaml
- name: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -43,7 +40,7 @@ jobs:
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npm publish --provenance --access public --tag "$PREID"
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npm publish --provenance --access public
npx npm@latest publish --provenance --access public
fi
+2 -5
View File
@@ -30,9 +30,6 @@ jobs:
cache: 'pnpm'
cache-dependency-path: vercel-ai-sdk/pnpm-lock.yaml
- name: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -43,7 +40,7 @@ jobs:
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npm publish --provenance --access public --tag "$PREID"
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npm publish --provenance --access public
npx npm@latest publish --provenance --access public
fi
+20
View File
@@ -8,6 +8,19 @@ mode: "wide"
<Tabs>
<Tab title="Python">
<Update label="2026-04-04" description="v1.0.11">
**New Features & Updates:**
- **SDK:** Added `multilingual` parameter to project update ([#4314](https://github.com/mem0ai/mem0/pull/4314))
**Bug Fixes:**
- **LLMs:** Fixed Groq model configuration ([#4700](https://github.com/mem0ai/mem0/pull/4700))
- **Core:** Prevented thread and memory leaks from PostHog telemetry ([#4535](https://github.com/mem0ai/mem0/pull/4535))
- **Vector Stores:** Used `DatetimeRange` for datetime string values in Qdrant range filters ([#4659](https://github.com/mem0ai/mem0/pull/4659))
- **Configs:** Added missing `ConfigDict` to vector store configs (Elasticsearch, MongoDB, Neptune, OpenSearch, PGVector, Supabase, Valkey) ([#4656](https://github.com/mem0ai/mem0/pull/4656))
</Update>
<Update label="2026-04-01" description="v1.0.10">
**New Features & Updates:**
@@ -832,6 +845,13 @@ mode: "wide"
<Tab title="TypeScript">
<Update label="2026-04-04" description="v2.4.6">
**New Features & Updates:**
- **Client:** Added `multilingual` parameter to project update types ([#4314](https://github.com/mem0ai/mem0/pull/4314))
</Update>
<Update label="2026-04-01" description="v2.4.5">
**Bug Fixes:**
+12 -4
View File
@@ -147,9 +147,11 @@
"tab": "OpenClaw",
"groups": [
{
"group": "OpenClaw",
"group": "Agent Harness",
"icon": "robot",
"pages": [
"integrations/openclaw"
"integrations/openclaw",
"integrations/hermes"
]
}
]
@@ -411,8 +413,6 @@
"integrations/autogen",
"integrations/agno",
"integrations/camel-ai",
"integrations/openclaw",
"integrations/hermes",
"integrations/openai-agents-sdk",
"integrations/google-ai-adk",
"integrations/mastra",
@@ -460,6 +460,14 @@
"integrations/cursor",
"integrations/codex"
]
},
{
"group": "Agent Harness",
"icon": "robot",
"pages": [
"integrations/openclaw",
"integrations/hermes"
]
}
]
},
+16 -52
View File
@@ -382,67 +382,31 @@ Here are the available integrations for Mem0:
Build AI agents with persistent memory using Mastra's framework and tools.
</Card>
<Card
title="Claude Code"
icon={
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.92888 16.2181L9.86008 14.0122L9.92585 13.8199L9.86008 13.7137H9.66782L9.01009 13.6732L6.76369 13.6125L4.81581 13.5315L2.92863 13.4303L2.45304 13.3292L2.00781 12.7423L2.05335 12.4488L2.45304 12.1807L3.02476 12.2313L4.28962 12.3173L6.18692 12.4488L7.56309 12.5298L9.60204 12.7423H9.92585L9.97138 12.6107L9.86008 12.5298L9.77407 12.4488L7.811 11.1182L5.68603 9.71165L4.57295 8.90214L3.97088 8.49233L3.66731 8.10781L3.53577 7.26794L4.08219 6.66587L4.81581 6.71646L5.00301 6.76706L5.74674 7.33877L7.33541 8.56822L9.40979 10.0962L9.71335 10.3491L9.83478 10.2631L9.84996 10.2024L9.71335 9.97475L8.5851 7.93579L7.38095 5.86141L6.84465 5.00131L6.70298 4.48524C6.65239 4.27275 6.61697 4.09567 6.61697 3.87811L7.23928 3.03318L7.58332 2.92188L8.41307 3.03318L8.76218 3.33675L9.27824 4.5156L10.113 6.37242L11.4083 8.89708L11.7877 9.64588L11.9901 10.339L12.066 10.5515H12.1975V10.4301L12.3038 9.00839L12.5011 7.26288L12.6934 5.01649L12.7591 4.38406L13.0728 3.62514L13.6951 3.21532L14.1808 3.44806L14.5805 4.01978L14.5249 4.38912L14.2871 5.93225L13.8216 8.35066L13.5181 9.96969H13.6951L13.8975 9.76731L14.7171 8.67953L16.0933 6.95931L16.7005 6.27629L17.4088 5.52243L17.8641 5.16321H18.7242L19.3567 6.10427L19.0733 7.07568L18.1879 8.19888L17.4543 9.15006L16.4019 10.5667L15.7442 11.7L15.8049 11.7911L15.9618 11.7759L18.3397 11.27L19.6248 11.0372L21.1578 10.7741L21.851 11.0979L21.9269 11.4268L21.6537 12.0997L20.0144 12.5045L18.0918 12.889L15.2282 13.567L15.1927 13.5923L15.2332 13.6428L16.5234 13.7643L17.0749 13.7946H18.4257L20.9403 13.9818L21.598 14.4169L21.9926 14.9482L21.9269 15.3529L20.915 15.869L19.5489 15.5452L16.3615 14.7863L15.2686 14.5131H15.1168V14.6041L16.0275 15.4946L17.6972 17.0023L19.7867 18.9451L19.893 19.4258L19.6248 19.8053L19.3415 19.7648L17.5049 18.3835L16.7966 17.7612L15.1927 16.4104H15.0865V16.552L15.4558 17.0934L17.4088 20.0279L17.51 20.9285L17.3683 21.2219L16.8624 21.399L16.3058 21.2978L15.1624 19.6939L13.9835 17.8877L13.0324 16.2687L12.916 16.3345L12.3544 22.3805L12.0913 22.6891L11.4842 22.9219L10.9782 22.5374L10.7101 21.915L10.9782 20.6856L11.302 19.0818L11.5651 17.8068L11.8029 16.2232L11.9446 15.697L11.9345 15.6616L11.8181 15.6767L10.6241 17.316L8.80771 19.7698L7.37083 21.3079L7.02679 21.4445L6.42977 21.1359L6.48542 20.5844L6.81935 20.0936L8.80771 17.5639L10.0068 15.9955L10.7809 15.0898L10.7758 14.9583H10.7303L5.44824 18.3886L4.50718 18.51L4.10242 18.1306L4.15302 17.5083L4.34528 17.3059L5.93394 16.213L5.92888 16.2181Z" fill="currentColor"/>
</svg>
}
href="/integrations/claude-code"
title="OpenAI Agents SDK"
icon="robot"
href="/integrations/openai-agents-sdk"
>
Add persistent memory to Claude Code and Cowork with MCP server, lifecycle hooks, and SDK skill.
Integrate Mem0 with the OpenAI Agents SDK for persistent memory across multi-agent workflows.
</Card>
<Card
title="Cursor"
icon={
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.4216 6.73383L12.4151 2.11135C12.158 1.96288 11.8408 1.96288 11.5837 2.11135L3.57759 6.73383C3.36146 6.85862 3.22803 7.08941 3.22803 7.33937V16.6606C3.22803 16.9106 3.36146 17.1414 3.57759 17.2662L11.5841 21.8886C11.8412 22.0371 12.1584 22.0371 12.4155 21.8886L20.4219 17.2662C20.6381 17.1414 20.7715 16.9106 20.7715 16.6606V7.33937C20.7715 7.08941 20.6377 6.85862 20.4216 6.73383ZM19.9187 7.71298L12.1896 21.1001C12.1373 21.1903 11.9994 21.1534 11.9994 21.0489V12.2832C11.9994 12.1081 11.9058 11.9461 11.7539 11.8581L4.16282 7.47543C4.07261 7.42319 4.10945 7.28524 4.21394 7.28524H19.6721C19.8916 7.28524 20.0284 7.52279 19.9187 7.71298Z" fill="currentColor"/>
</svg>
}
href="/integrations/cursor"
title="Google ADK"
icon="google"
href="/integrations/google-ai-adk"
>
Add persistent memory to Cursor with MCP server, lifecycle hooks, and context-aware coding.
Integrate Mem0 with Google Agent Development Kit for persistent memory across multi-agent workflows.
</Card>
<Card
title="Codex"
icon={
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.5565 10.6338C21.0009 9.27575 20.8528 7.76958 20.1367 6.53501C19.0503 4.63378 16.8528 3.67081 14.7046 4.11526C13.7663 3.05353 12.3836 2.46094 10.9515 2.46094C8.75399 2.46094 6.82807 3.86835 6.13671 5.94242C4.7293 6.23872 3.51943 7.10291 2.80338 8.36217C1.71696 10.2634 1.96387 12.6338 3.42066 14.2634C2.97622 15.6461 3.14906 17.1276 3.8651 18.3622C4.95152 20.2634 7.14906 21.2511 9.2972 20.7819C10.2602 21.8437 11.6182 22.4609 13.0503 22.4609C15.2478 22.4609 17.1737 21.0535 17.8651 18.9795C19.2725 18.6832 20.4824 17.819 21.1984 16.5597C22.2849 14.6585 22.0379 12.2634 20.5565 10.6338ZM13.0503 21.1523C12.1614 21.1523 11.3219 20.856 10.6552 20.2881C10.6799 20.2634 10.754 20.2387 10.7787 20.214L14.754 17.9177C14.9515 17.7943 15.075 17.5967 15.075 17.3498V11.7449L16.754 12.7079C16.7787 12.7079 16.7787 12.7325 16.7787 12.7572V17.3992C16.8034 19.4733 15.1244 21.1523 13.0503 21.1523ZM5.00091 17.7202C4.55646 16.9548 4.40831 16.0659 4.55646 15.2017C4.58115 15.2264 4.63054 15.2511 4.67992 15.2758L8.65523 17.572C8.85276 17.6955 9.09967 17.6955 9.2972 17.572L14.1614 14.7572V16.7079C14.1614 16.7325 14.1614 16.7572 14.1367 16.7572L10.112 19.0782C8.33424 20.1153 6.03794 19.498 5.00091 17.7202ZM3.96387 9.02884C4.40831 8.26341 5.09967 7.69551 5.91449 7.37452V12.1153C5.91449 12.3375 6.03794 12.5597 6.23548 12.6832L11.0997 15.498L9.42066 16.4609C9.39597 16.4609 9.37128 16.4856 9.37128 16.4609L5.34659 14.1399C3.51943 13.1029 2.92683 10.8066 3.96387 9.02884ZM17.791 12.2387L12.9268 9.4239L14.6058 8.46094C14.6305 8.46094 14.6552 8.43625 14.6552 8.46094L18.6799 10.7819C20.4824 11.819 21.075 14.1153 20.0379 15.893C19.5935 16.6585 18.9021 17.2264 18.0873 17.5227V12.8066C18.112 12.5844 17.9886 12.3622 17.791 12.2387ZM19.4454 9.7202C19.4207 9.69551 19.3713 9.67081 19.3219 9.64612L15.3466 7.34983C15.1491 7.22637 14.9021 7.22637 14.7046 7.34983L9.84041 10.1646V8.21402C9.84041 8.18933 9.84041 8.16464 9.86511 8.16464L13.8898 5.84365C15.6923 4.80662 17.9639 5.4239 19.0009 7.22637C19.4454 7.96711 19.5935 8.856 19.4454 9.7202ZM8.92683 13.177L7.24782 12.214C7.22313 12.214 7.22313 12.1893 7.22313 12.1646V7.52267C7.22313 5.44859 8.90214 3.76958 10.9762 3.76958C11.8651 3.76958 12.7046 4.06588 13.3713 4.63378C13.3466 4.65847 13.2972 4.68316 13.2478 4.70785L9.27251 7.00415C9.07498 7.1276 8.95152 7.32514 8.95152 7.57205V13.177H8.92683ZM9.84041 11.2017L12.0133 9.94242L14.1861 11.2017V13.6955L12.0133 14.9548L9.84041 13.6955V11.2017Z" fill="currentColor"/>
</svg>
}
href="/integrations/codex"
title="Flowise"
icon="diagram-project"
href="/integrations/flowise"
>
Add persistent memory to OpenAI Codex with MCP server and skill-based memory protocol.
Add persistent Mem0 memory to Flowise chatflows for context-aware conversations in the low-code builder.
</Card>
<Card
title="OpenClaw"
icon={
<svg width="24" height="24" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="m153.5 173.5q24.62 1.46 46 13.5 12.11 8.1 17.5 21.5 0.74 2.45 0.5 5 0.09 0.81 1 1 1.48-4.9 1-10 5.04 10.48 1.5 22-9.81 27.86-35.5 42.5-26.17 14.97-56 19.5-2.77-0.4-2 1 2.86 1.27 6 1 25.64 1.53 48.5-10 0.34 10.08 2 20 1.08 5.76 5 10 1 1.5 0 3-31.11 20.84-68.5 17.5-23.7-5.7-32.5-28.5-4.39-9.18-3.5-19 15.41 6.23 32 4.5-20.68-6.39-39-18-34.81-27.22-12.5-65.5 11.84-14.83 29-23 4.21 7.66 11.5 12.5 3 1 6 0-26.04-34.62-29-78-0.13-8.46 2-16.5 1 6.5 2 13 3.43 39.53 24.5 73 2.03 2.28 4.5 4 0.5-1.25 1-2.5-1.27-6.54-5-12 0.5-0.75 1-1.5 9.72-3.43 20-4 0.55 10.34 8 17.5 1.94 0.74 4 0.5-17.8-64.6 16.5-122 0.98-1.79 1.5 0-28.21 56.64-13.5 118 1.08 1.43 2.5 0.5 2.21-4.98 2-10.5z" fill="currentColor"/>
<path fill-rule="evenodd" d="m454.5 97.5q-1.33 11.18-8.5 20-21.81 26.28-55.5 32-1.11-0.2-2 0.5 2.31 2.82 5.5 4.5 1 2 0 4-9.56 11.3-19.5 20 19.71-8.72 31-27 2.68-0.43 5 1-14.24 30.97-48 36.5-9.93 1.71-20 1.5-6.8-0.48-13 1 5.81 6.92 14 11-10.78 16.03-27 26.5 27.16-7.4 38-33.5 4.34 1.35 9 1-9.08 23.84-33 33.5-18.45 6.41-38 7 22.59 8.92 45-1 12.05-5.52 24-11 9.01-1.79 17 2.5 5.28-4.38 11-8 12.8-6.07 27-5 0 0.5 0 1-19.34 2.69-34 15.5 0.5 0.25 1 0.5 17.79-8.09 36-15 2.71-0.79 5-2 2.5-1 5-2 5.53-4.04 11-8 11.7-4.18 24-6.5 7.78-1.36 15 1.5-2.97 18.45-13.5 34-34.92 49.37-94.5 62.5-59.27 12.45-108-23-15.53-12.52-21.5-31.5-2.47-14.26 4-27-3.15 24.41 14 42-4.92-10.28-7-22-1.97-17.63 7-33 47.28-69.5 125.5-100 15.86-3.42 32-5.5 18.63-1.47 37 1.5z" fill="currentColor"/>
<path fill-rule="evenodd" d="m231.5 238.5q1.31-0.2 2 1-3.13 28.62 15 51-16.25 6.75-27-7.5-1-1-2 0 14.73 29.34 46 18.5 1.79 0.52 0 1.5-37.63 16.82-50.5-22.5-5.1-26.48 16.5-42z" fill="currentColor"/>
<path fill-rule="evenodd" d="m203.5 266.5q1.31-0.2 2 1-2.48 22.08 12 39-6.99 1.35-14 0.5 4.59 4.08 10 7-8.71 0.28-14.5-6.5-16.98-22.76 4.5-41z" fill="currentColor"/>
<path fill-rule="evenodd" d="m58.5 284.5q9.6-2.17 14.5 6 5.15 14.18-1 28-11.05-13.14-27.5-17.5 5.15-9.9 14-16.5z" fill="currentColor"/>
<path fill-rule="evenodd" d="m56.5 313.5q3.43 5.43 8 10-4.88 0.44-8 4-1.11-0.2-2 0.5 28.91 1.65 38 28.5 0.45 3.16-1 6-11.02-7.01-23-12.5-4.75-3.75-9.5-7.5 1.47 7.42 7 13 8.34 27.18 32 43 0.99 2.41-1.5 3.5-40.25 5.58-66.5-25.5-15.67-22.01-8-48 10.46-23.87 34.5-15z" fill="currentColor"/>
<path fill-rule="evenodd" d="m198.5 319.5q1.44 0.68 2.5 2 2.41 8.23 6 16 1.2 2.64-0.5 5-30.65 21.41-68 18.5-25.16-6.17-32.5-30.5 6.96 4.99 15.5 6.5 8.99 0.75 18 0.5 16.25 2.38 32-2.5 15.9-3.94 27-15.5z" fill="currentColor"/>
<path fill-rule="evenodd" d="m239.5 342.5q7.02-0.25 14 0.5 4.46 1.06 8 3.5-5.2 2.35-10 5.5-3.88 4.65-9 7.5-9.89-3.09-9.5-13 2.36-3.63 6.5-4z" fill="currentColor"/>
<path fill-rule="evenodd" d="m214.5 349.5q5.96 7.2 13.5 13 1 1 0 2-28.58 23.34-65.5 20.5-18.15-4.24-27.5-19.5 1.13 0.94 2.5 1.5 14.7 1.42 29-1.5 26.57-0.52 48-16z" fill="currentColor"/>
<path fill-rule="evenodd" d="m302.5 373.5q0.21 2.44-2 3.5-28.69 7.6-50.5-12.5-0.06-6.71 6.5-9 4.45-0.75 9-1 22.26 2.27 37 19z" fill="currentColor"/>
<path fill-rule="evenodd" d="m232.5 365.5q17.6 6.19 10.5 23-10.6 10.42-25.5 11.5-25.94 3.21-49-9 36.75-1.65 64-25.5z" fill="currentColor"/>
<path fill-rule="evenodd" d="m113.5 367.5q7.7-0.01 9.5 7-9.69 7.19-18.5 15.5-7.23 5.76-5.5-3.5 3.12-12.84 14.5-19z" fill="currentColor"/>
<path fill-rule="evenodd" d="m126.5 380.5q7.88-0.4 12 6.5-8.5 7.25-17 14.5-5.62-12.55 5-21z" fill="currentColor"/>
<path fill-rule="evenodd" d="m283.5 385.5q3.22 2.95 7 5.5 2.8 4.03 6 7.5 0.42 2.77-2 4-15.5-9.75-31-19.5-1.79-0.98 0-1.5 9.96 2.49 20 4z" fill="currentColor"/>
</svg>
}
href="/integrations/openclaw"
title="AWS Bedrock"
icon="cloud"
href="/integrations/aws-bedrock"
>
Add long-term memory to OpenClaw agents with auto-recall and auto-capture support.
</Card>
<Card
title="Hermes Agent"
icon="bolt"
href="/integrations/hermes"
>
Add long-term memory to Hermes agents with automatic background sync and zero-latency prefetch.
Use Mem0 with AWS Bedrock and OpenSearch Service for cloud-native persistent semantic memory storage.
</Card>
</CardGroup>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mem0ai",
"version": "2.4.5",
"version": "2.4.6",
"description": "The Memory Layer For Your AI Apps",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
+7 -1
View File
@@ -9,6 +9,8 @@ All notable changes to the `@mem0/openclaw-mem0` plugin will be documented in th
- **`memory_add` tool**: Replaces `memory_store` — name now matches `mem0` CLI and platform API
- **`memory_delete` tool**: Unified delete — single ID, search-then-delete, bulk, entity cascade. Replaces `memory_forget` and `memory_delete_all`
- **CLI subcommands**: `openclaw mem0 init`, `openclaw mem0 status` (renamed from `stats`), `openclaw mem0 config show`, `openclaw mem0 config set`
- **`import` CLI command**: Bulk-import memories from a JSON file with `--user-id` and `--agent-id` overrides
- **`event list` / `event status` CLI commands**: Monitor background processing events
- **`fs-safe.ts` module**: Isolated filesystem wrappers (sync read/write/exists/mkdir/unlink) in a separate entry point — keeps file I/O out of the main bundle
- **`backend/` module**: `PlatformBackend` with direct HTTP API access for CLI commands
- **`cli/config-file.ts`**: Persistent plugin auth storage in `~/.openclaw/openclaw.json`
@@ -16,7 +18,7 @@ All notable changes to the `@mem0/openclaw-mem0` plugin will be documented in th
- **Test suite**: 329 tests across 10 test files covering tools, CLI, config, dream gate, providers, and skill-loader
### Changed
- **Modular architecture**: Extracted tools into `tools/` directory (7 files) and CLI into `cli/commands.ts` — `index.ts` down from ~1700 to ~890 lines
- **Modular architecture**: Extracted tools into `tools/` directory (6 files) and CLI into `cli/commands.ts` — `index.ts` down from ~1700 to ~890 lines
- **Code splitting**: tsup builds with `splitting: true` and two entry points (`index.ts`, `fs-safe.ts`), separating filesystem I/O from the main bundle
- **Skills updated**: All SKILL.md files reference new tool names (`memory_add`, `memory_delete`) matching the plugin manifest
- **WRITE_TOOLS updated**: Dream gate tracks `memory_delete` and `memory_add` instead of `memory_forget` and `memory_store`
@@ -25,11 +27,15 @@ All notable changes to the `@mem0/openclaw-mem0` plugin will be documented in th
- **Auto-capture fire-and-forget**: `provider.add()` now runs in the background via `.then()/.catch()` — the `agent_end` hook returns immediately, zero event loop blocking
- **Auto-capture minimum content gate**: Skips extraction when total user content is <50 chars after filtering — trivial conversations ("ok", "thanks") no longer trigger LLM calls
- **CLI search**: Lowered threshold to 0.3 so explicit searches are more permissive than auto-recall
- **Init defaults**: Choice defaults to `1` (email login) on Enter, User ID defaults to OS username — no more empty values
- **Init no longer stores `baseUrl`**: Uses `https://api.mem0.ai` directly instead of persisting it to config
- **Help output reordered**: `openclaw mem0 help` now shows high-value commands first (search, add) instead of alphabetical
### Removed
- `memory_store` tool — replaced by `memory_add`
- `memory_forget` tool — replaced by `memory_delete`
- `memory_delete_all` tool — merged into `memory_delete`
- `memory_history` tool and `history` CLI command — deprecated
- **`custom_instructions` / `custom_categories` in `buildAddOptions`**: No longer injected into every auto-capture API call. Config fields (`customInstructions`, `customCategories`) remain as user-configurable options.
## [1.0.3] - 2026-04-03
+41 -63
View File
@@ -12,7 +12,7 @@ openclaw plugins install @mem0/openclaw-mem0
### Platform (Mem0 Cloud)
Get an API key from [app.mem0.ai](https://app.mem0.ai):
Get an API key from [app.mem0.ai](https://app.mem0.ai/dashboard/api-keys):
```bash
openclaw mem0 init --api-key <your-key> --user-id <your-user-id>
@@ -32,7 +32,9 @@ Or configure manually in `openclaw.json`:
### Open-Source (Self-hosted)
No Mem0 key needed. Requires `OPENAI_API_KEY` for default embeddings and LLM.
No Mem0 key needed. Requires `OPENAI_API_KEY` for default embeddings and LLM. Vectors are stored locally in SQLite at `~/.mem0/vector_store.db` — no external database required.
Defaults: `text-embedding-3-small` for embeddings, `gpt-5.4` for fact extraction.
```json5
"openclaw-mem0": {
@@ -53,7 +55,7 @@ Customize the embedder, vector store, or LLM via the `oss` block:
"oss": {
"embedder": { "provider": "openai", "config": { "model": "text-embedding-3-small" } },
"vectorStore": { "provider": "qdrant", "config": { "host": "localhost", "port": 6333 } },
"llm": { "provider": "openai", "config": { "model": "gpt-4o" } }
"llm": { "provider": "openai", "config": { "model": "gpt-5.4" } }
}
}
```
@@ -74,79 +76,57 @@ Both run silently. No prompting, no manual calls required.
### Memory Scopes
| Scope | Description |
|-------|-------------|
| **Session (short-term)** | Memories scoped to the current conversation via `run_id`. Automatically recalled alongside long-term memories. |
| **User (long-term)** | Persistent memories that span all sessions. Stored via `memory_add` with `longTerm: true` (the default). |
During auto-recall, both scopes are searched and presented separately — long-term first, then session — so the agent has full context.
- **Session (short-term)** — Scoped to the current conversation via `run_id`. Recalled alongside long-term memories.
- **User (long-term)** — Persistent across all sessions. Default for `memory_add`.
### Multi-Agent Isolation
In multi-agent setups, each agent gets its own memory namespace automatically. Session keys matching `agent:<name>:<uuid>` route memories to `userId:agent:<name>`. Single-agent deployments are unaffected.
All memory tools accept an optional `agentId` parameter for cross-agent queries:
```
memory_search({ query: "user's tech stack", agentId: "researcher" })
```
Each agent gets its own memory namespace automatically via session key routing (`agent:<name>:<uuid>` maps to `userId:agent:<name>`). Single-agent setups are unaffected.
## Agent Tools
Seven tools are available to the agent during conversations:
Eight tools are registered for agent use:
| Tool | Description |
|------|-------------|
| **`memory_search`** | Search memories by natural language query. Supports `scope` (`session`, `long-term`, `all`) and `agentId` filtering. |
| **`memory_add`** | Save a fact to memory. Supports `category`, `importance`, `longTerm`, and `agentId`. |
| **`memory_get`** | Retrieve a specific memory by ID. |
| **`memory_list`** | List stored memories with optional `userId`, `agentId`, and `limit` filters. |
| **`memory_update`** | Update an existing memory's text in place. Preserves edit history. |
| **`memory_delete`** | Delete by ID, search query, or bulk (`all: true`). Requires `confirm: true` for bulk. |
| **`memory_history`** | View the edit history of a specific memory. |
| ---- | ----------- |
| `memory_search` | Search by natural language query. Supports `scope` (`session`, `long-term`, `all`), `categories`, `filters`, and `agentId`. |
| `memory_add` | Store facts. Accepts `text` or `facts` array, `category`, `importance`, `longTerm`, `metadata`. |
| `memory_get` | Retrieve a single memory by ID. |
| `memory_list` | List all memories. Filter by `userId`, `agentId`, `scope`. |
| `memory_update` | Update a memory's text in place. Preserves history. |
| `memory_delete` | Delete by `memoryId`, `query` (search-and-delete), or `all: true` (requires `confirm: true`). |
| `memory_event_list` | List recent background processing events. Platform mode only. |
| `memory_event_status` | Get status of a specific event by ID. Platform mode only. |
## CLI
All commands follow the pattern `openclaw mem0 <command>`.
### Memory Operations
All commands: `openclaw mem0 <command>`.
```bash
# Add a memory
# Memory operations
openclaw mem0 add "User prefers TypeScript over JavaScript"
# Search memories
openclaw mem0 search "what languages does the user know"
openclaw mem0 search "preferences" --scope long-term
openclaw mem0 search "context" --scope session
# Get, list, update, delete
openclaw mem0 get <memory_id>
openclaw mem0 list --user-id alice --top-k 20
openclaw mem0 update <memory_id> "Updated preference text"
openclaw mem0 delete <memory_id>
openclaw mem0 delete --all --user-id alice --confirm
openclaw mem0 import memories.json
# View edit history
openclaw mem0 history <memory_id>
```
### Management
```bash
# Authenticate and configure
# Management
openclaw mem0 init
openclaw mem0 init --api-key <key> --user-id alice
# Check connectivity
openclaw mem0 status
# Manage configuration
openclaw mem0 config show
openclaw mem0 config get api_key
openclaw mem0 config set user_id alice
# Memory consolidation (review, merge, prune)
# Events (platform only)
openclaw mem0 event list
openclaw mem0 event status <event_id>
# Memory consolidation
openclaw mem0 dream
openclaw mem0 dream --dry-run
```
@@ -156,9 +136,9 @@ openclaw mem0 dream --dry-run
### General
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| --- | ---- | ------- | ----------- |
| `mode` | `"platform"` \| `"open-source"` | `"platform"` | Backend mode |
| `userId` | `string` | `"default"` | Unique identifier for the user. You define this — it's not found in any dashboard. All memories are scoped to this value. |
| `userId` | `string` | OS username | User identifier. All memories scoped to this value. |
| `autoRecall` | `boolean` | `true` | Inject relevant memories before each turn |
| `autoCapture` | `boolean` | `true` | Extract and store facts after each turn |
| `topK` | `number` | `5` | Max memories returned per recall |
@@ -167,32 +147,30 @@ openclaw mem0 dream --dry-run
### Platform Mode
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| --- | ---- | ------- | ----------- |
| `apiKey` | `string` | — | **Required.** Mem0 API key (supports `${MEM0_API_KEY}`) |
| `orgId` | `string` | — | Organization ID |
| `projectId` | `string` | — | Project ID |
| `enableGraph` | `boolean` | `false` | Enable entity graph for relationship tracking |
| `customInstructions` | `string` | *(built-in)* | Custom extraction rules for what to store and how to format |
| `customCategories` | `object` | *(12 defaults)* | Category name to description map for memory tagging |
| `enableGraph` | `boolean` | `false` | Entity graph for relationship tracking |
| `customInstructions` | `string` | *(built-in)* | Custom extraction rules |
| `customCategories` | `object` | *(12 defaults)* | Category name to description map |
### Open-Source Mode
All fields below are optional. Defaults use OpenAI embeddings, in-memory vector store, and OpenAI LLM.
All fields optional. Defaults: `text-embedding-3-small` embeddings, local SQLite vector store (`~/.mem0/vector_store.db`), `gpt-5.4` LLM.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `customPrompt` | `string` | *(built-in)* | Extraction prompt for memory processing |
| --- | ---- | ------- | ----------- |
| `customPrompt` | `string` | *(built-in)* | Extraction prompt |
| `oss.embedder.provider` | `string` | `"openai"` | Embedding provider |
| `oss.embedder.config` | `object` | — | Provider config (`apiKey`, `model`, `baseURL`) |
| `oss.vectorStore.provider` | `string` | `"memory"` | Vector store provider |
| `oss.vectorStore.config` | `object` | — | Provider config (`host`, `port`, `collectionName`) |
| `oss.vectorStore.provider` | `string` | `"memory"` | Vector store provider (see list above) |
| `oss.vectorStore.config` | `object` | — | Provider config (`host`, `port`, `collectionName`, `dbPath`) |
| `oss.llm.provider` | `string` | `"openai"` | LLM provider |
| `oss.llm.config` | `object` | — | Provider config (`apiKey`, `model`, `baseURL`) |
| `oss.historyDbPath` | `string` | — | SQLite path for memory edit history |
| `oss.disableHistory` | `boolean` | `false` | Skip history DB initialization |
Supported providers: `openai`, `anthropic`, `ollama`, `lmstudio`, `qdrant`, `chroma`, and more. See the [Mem0 OSS docs](https://docs.mem0.ai/open-source/node-quickstart) for the full list.
| `oss.historyDbPath` | `string` | — | SQLite path for edit history |
| `oss.disableHistory` | `boolean` | `false` | Skip history DB |
## License
Apache 2.0
[Apache 2.0](LICENSE)
+254 -77
View File
@@ -10,7 +10,7 @@
* - list : List memories with optional filters (--user-id, --agent-id, --top-k)
* - update : Update a memory's text
* - delete : Delete a memory or all memories (--all, --confirm)
* - history : View edit history of a memory
* - import : Import memories from a JSON file
*
* Management:
* - init : Authenticate with Mem0 Platform (email or API key)
@@ -18,6 +18,8 @@
* - config show : Display current plugin configuration
* - config get : Get a single config value
* - config set : Update a plugin config field
* - event list : List recent background events
* - event status: Get status of a specific event
* - dream : Run memory consolidation
*
* Naming conventions match the Python CLI (`mem0 init`, `mem0 search`, etc.)
@@ -35,12 +37,12 @@ import type {
SearchOptions,
} from "../types.ts";
import { loadDreamPrompt } from "../skill-loader.ts";
import { readText } from "../fs-safe.ts";
import type { PluginAuthConfig } from "./config-file.ts";
import {
readPluginAuth,
writePluginAuth,
writePluginConfigField,
getBaseUrl,
OPENCLAW_CONFIG_FILE,
} from "./config-file.ts";
@@ -49,13 +51,14 @@ import {
// ============================================================================
/** Prompt user for input on stderr (keeps stdout clean for piping). */
function promptInput(question: string): Promise<string> {
function promptInput(question: string, prefill?: string): Promise<string> {
const rl = createInterface({ input: process.stdin, output: process.stderr });
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer.trim());
});
if (prefill) rl.write(prefill);
});
}
@@ -196,14 +199,13 @@ async function verifyEmailCode(
*/
function saveLoginConfig(
apiKey: string,
baseUrl: string,
userIdFlag?: string,
userEmail?: string,
): void {
const existingAuth = readPluginAuth();
const userId = resolveUserId(userIdFlag, existingAuth.userId);
writePluginAuth({ apiKey, baseUrl, userId, mode: "platform", ...(userEmail && { userEmail }) });
writePluginAuth({ apiKey, userId, mode: "platform", ...(userEmail && { userEmail }) });
console.log(` Configuration saved to ${OPENCLAW_CONFIG_FILE}`);
console.log(` Mode: platform`);
@@ -245,7 +247,8 @@ export function registerCliCommands(
({ program }) => {
const mem0 = program
.command("mem0")
.description("Mem0 memory plugin commands");
.description("Mem0 memory plugin commands")
.configureHelp({ sortSubcommands: false, subcommandTerm: (cmd) => cmd.name() });
// Telemetry: fire event for each CLI subcommand
if (captureCliEvent) {
@@ -278,7 +281,7 @@ export function registerCliCommands(
userId?: string;
}) => {
try {
const baseUrl = getBaseUrl();
const baseUrl = "https://api.mem0.ai";
const existingAuth = readPluginAuth();
const hasExistingConfig = !!(existingAuth.apiKey || existingAuth.mode);
@@ -290,7 +293,7 @@ export function registerCliCommands(
}
const check = await validateApiKey(baseUrl, opts.apiKey);
saveLoginConfig(opts.apiKey, baseUrl, opts.userId, check.userEmail);
saveLoginConfig(opts.apiKey, opts.userId, check.userEmail);
if (hasExistingConfig) {
console.log(
" Existing configuration detected — updated API key (other settings preserved).",
@@ -322,7 +325,7 @@ export function registerCliCommands(
const apiKey = await verifyEmailCode(baseUrl, email, opts.code);
if (!apiKey) return;
saveLoginConfig(apiKey, baseUrl, opts.userId, email);
saveLoginConfig(apiKey, opts.userId, email);
if (hasExistingConfig) {
console.log(
" Existing configuration detected — updated API key (other settings preserved).",
@@ -390,7 +393,7 @@ export function registerCliCommands(
// Validate existing key before asking
if (existingAuth.apiKey) {
const check = await validateApiKey(
existingAuth.baseUrl || baseUrl,
baseUrl,
existingAuth.apiKey,
);
if (check.ok) {
@@ -429,7 +432,7 @@ export function registerCliCommands(
console.log(" 2. Enter API key manually");
console.log(" 3. Open-source mode (self-hosted)\n");
const choice = await promptInput(" Choice (1/2/3): ");
const choice = (await promptInput(" Choice: ", "1")) || "1";
if (choice === "1") {
// --- Email interactive flow ---
@@ -461,13 +464,13 @@ export function registerCliCommands(
if (!userIdValue) {
const defaultUid = resolveUserId(undefined, existingAuth.userId);
const uidInput = await promptInput(
` User ID (${defaultUid}): `,
` User ID: `, defaultUid,
);
userIdValue = uidInput || undefined;
userIdValue = uidInput || defaultUid;
}
console.log("");
saveLoginConfig(apiKey, baseUrl, userIdValue, email);
saveLoginConfig(apiKey, userIdValue, email);
console.log(" Authenticated!");
console.log(
" Restart the gateway: openclaw gateway restart\n",
@@ -485,14 +488,14 @@ export function registerCliCommands(
if (!userIdValue2) {
const defaultUid = resolveUserId(undefined, existingAuth.userId);
const uidInput = await promptInput(
` User ID (${defaultUid}): `,
` User ID: `, defaultUid,
);
userIdValue2 = uidInput || undefined;
userIdValue2 = uidInput || defaultUid;
}
console.log("");
const check = await validateApiKey(baseUrl, key);
saveLoginConfig(key, baseUrl, userIdValue2, check.userEmail);
saveLoginConfig(key, userIdValue2, check.userEmail);
if (check.ok) {
console.log(
@@ -545,7 +548,7 @@ export function registerCliCommands(
"\n Skipped. You can add it later via:",
);
console.log(
" openclaw mem0 config set oss.embedder.config.apiKey <key>",
" openclaw mem0 config set embedder_key <key>",
);
console.log(
" Or set OPENAI_API_KEY in your environment.\n",
@@ -557,9 +560,9 @@ export function registerCliCommands(
if (!userIdValue3) {
const defaultUid = resolveUserId(undefined, existingAuth.userId);
const uidInput = await promptInput(
` User ID (${defaultUid}): `,
` User ID: `, defaultUid,
);
userIdValue3 = uidInput || undefined;
userIdValue3 = uidInput || defaultUid;
}
console.log("");
@@ -901,27 +904,6 @@ export function registerCliCommands(
},
);
// ====================================================================
// history (matches: mem0 history <memory_id>)
// ====================================================================
mem0
.command("history")
.description("View edit history of a memory")
.argument("<memory_id>", "Memory ID to view history for")
.action(async (memoryId: string) => {
try {
const entries = await provider.history(memoryId);
if (!entries.length) {
console.log("No history found for this memory.");
return;
}
console.log(JSON.stringify(entries, null, 2));
} catch (err) {
console.error(`History failed: ${String(err)}`);
}
});
// ====================================================================
// status (matches: mem0 status)
// ====================================================================
@@ -967,6 +949,7 @@ export function registerCliCommands(
const CONFIG_KEYS: Record<string, string> = {
// Short aliases (matches Python CLI)
api_key: "apiKey",
email: "userEmail",
base_url: "baseUrl",
user_id: "userId",
org_id: "orgId",
@@ -976,30 +959,34 @@ export function registerCliCommands(
auto_capture: "autoCapture",
top_k: "topK",
mode: "mode",
// Dot notation (matches Python CLI: platform.api_key, defaults.user_id)
"platform.api_key": "apiKey",
"platform.base_url": "baseUrl",
"defaults.user_id": "userId",
"defaults.org_id": "orgId",
"defaults.project_id": "projectId",
"defaults.enable_graph": "enableGraph",
"defaults.auto_recall": "autoRecall",
"defaults.auto_capture": "autoCapture",
"defaults.top_k": "topK",
embedder_provider: "oss.embedder.provider",
embedder_model: "oss.embedder.config.model",
embedder_key: "oss.embedder.config.apiKey",
llm_provider: "oss.llm.provider",
llm_model: "oss.llm.config.model",
llm_key: "oss.llm.config.apiKey",
vector_provider: "oss.vectorStore.provider",
vector_host: "oss.vectorStore.config.host",
vector_port: "oss.vectorStore.config.port",
collection_name: "oss.vectorStore.config.collectionName",
vector_db_path: "oss.vectorStore.config.dbPath",
history_db_path: "oss.historyDbPath",
disable_history: "oss.disableHistory",
};
// Keys that contain secrets — redact in show/get output
const SECRET_KEYS = new Set(["apiKey"]);
const SECRET_KEYS = new Set(["apiKey", "oss.embedder.config.apiKey", "oss.llm.config.apiKey"]);
// Boolean config fields — coerce "true"/"1"/"yes" on set
const BOOLEAN_KEYS = new Set([
"enableGraph",
"autoRecall",
"autoCapture",
"oss.disableHistory",
]);
// Integer config fields — coerce to number on set
const INTEGER_KEYS = new Set(["topK"]);
const INTEGER_KEYS = new Set(["topK", "oss.vectorStore.config.port"]);
/** Resolve a user-facing key to the internal camelCase field name. */
function resolveConfigKey(key: string): string | null {
@@ -1008,6 +995,15 @@ export function registerCliCommands(
/** Read a config value by internal field name. */
function getConfigValue(field: string): unknown {
if (field.startsWith("oss.")) {
const parts = field.split(".");
let current: unknown = cfg.oss;
for (let i = 1; i < parts.length && current != null; i++) {
current = (current as Record<string, unknown>)[parts[i]];
}
return current;
}
const auth = readPluginAuth();
const values: Record<string, unknown> = {
apiKey: auth.apiKey ?? cfg.apiKey,
@@ -1016,6 +1012,7 @@ export function registerCliCommands(
orgId: auth.orgId ?? cfg.orgId,
projectId: auth.projectId ?? cfg.projectId,
mode: auth.mode ?? cfg.mode,
userEmail: auth.userEmail,
enableGraph: cfg.enableGraph,
autoRecall: cfg.autoRecall,
autoCapture: cfg.autoCapture,
@@ -1045,20 +1042,37 @@ export function registerCliCommands(
.command("show")
.description("Show current configuration")
.action(() => {
// Display order matching Python CLI: platform first, then defaults
const entries: Array<[string, string, string]> = [
["platform.api_key", "apiKey", ""],
["platform.base_url", "baseUrl", ""],
["defaults.user_id", "userId", ""],
["defaults.org_id", "orgId", ""],
["defaults.project_id", "projectId", ""],
["defaults.enable_graph", "enableGraph", ""],
["defaults.auto_recall", "autoRecall", ""],
["defaults.auto_capture", "autoCapture", ""],
["defaults.top_k", "topK", ""],
["mode", "mode", ""],
// Display order: general first, then mode-specific
const entries: Array<[string, string]> = [
["mode", "mode"],
["user_id", "userId"],
["auto_recall", "autoRecall"],
["auto_capture", "autoCapture"],
["top_k", "topK"],
];
if (cfg.mode === "platform") {
entries.push(
["api_key", "apiKey"],
["email", "userEmail"],
["org_id", "orgId"],
["project_id", "projectId"],
["enable_graph", "enableGraph"],
);
} else {
entries.push(
["embedder_provider", "oss.embedder.provider"],
["embedder_model", "oss.embedder.config.model"],
["embedder_key", "oss.embedder.config.apiKey"],
["llm_provider", "oss.llm.provider"],
["llm_model", "oss.llm.config.model"],
["llm_key", "oss.llm.config.apiKey"],
["vector_provider", "oss.vectorStore.provider"],
["history_db_path", "oss.historyDbPath"],
["disable_history", "oss.disableHistory"],
);
}
// Calculate column widths
const maxKeyLen = Math.max(
...entries.map(([k]) => k.length),
@@ -1086,17 +1100,21 @@ export function registerCliCommands(
console.log(" openclaw mem0 config set <key> <value>");
console.log("");
console.log(" Examples:");
console.log(" openclaw mem0 config set mode open-source");
console.log(" openclaw mem0 config set mode platform");
console.log(" openclaw mem0 config set auto_recall false");
console.log(" openclaw mem0 config set top_k 10");
if (cfg.mode === "platform") {
console.log(" openclaw mem0 config set mode open-source");
console.log(" openclaw mem0 config set auto_recall false");
} else {
console.log(" openclaw mem0 config set vector_provider qdrant");
console.log(" openclaw mem0 config set llm_model gpt-4o");
console.log(" openclaw mem0 config set embedder_provider openai");
}
console.log("");
});
configCmd
.command("get")
.description("Get a config value")
.argument("<key>", "Config key (e.g. user_id, platform.api_key)")
.argument("<key>", "Config key (e.g. user_id, api_key, llm_model)")
.action((key: string) => {
const field = resolveConfigKey(key);
if (!field) {
@@ -1112,7 +1130,7 @@ export function registerCliCommands(
configCmd
.command("set")
.description("Set a config value")
.argument("<key>", "Config key (e.g. user_id, platform.api_key)")
.argument("<key>", "Config key (e.g. user_id, api_key, llm_model)")
.argument("<value>", "New value")
.action((key: string, rawValue: string) => {
const field = resolveConfigKey(key);
@@ -1139,12 +1157,171 @@ export function registerCliCommands(
value = parsed;
}
writePluginAuth({ [field]: value } as PluginAuthConfig);
// Nested OSS fields use dot-path writer; flat fields use auth writer
if (field.startsWith("oss.")) {
writePluginConfigField(field.split("."), value);
} else {
writePluginAuth({ [field]: value } as PluginAuthConfig);
}
console.log(
`${key} = ${displayValue(field, value)}`,
);
});
// ====================================================================
// import (matches: mem0 import <file>)
// ====================================================================
mem0
.command("import")
.description("Import memories from a JSON file")
.argument("<file>", "Path to JSON file containing memories")
.option("--user-id <userId>", "Override user ID for all imported memories")
.option("--agent-id <agentId>", "Override agent ID for all imported memories")
.action(
async (
file: string,
opts: { userId?: string; agentId?: string },
) => {
try {
let data: unknown;
try {
data = JSON.parse(readText(file));
} catch (err) {
console.error(`Failed to read file: ${String(err)}`);
return;
}
const items = Array.isArray(data) ? data : [data];
let added = 0;
let failed = 0;
for (const item of items) {
const content =
item?.memory ?? item?.text ?? item?.content ?? "";
if (!content) {
failed++;
continue;
}
try {
await backend.add(content, undefined, {
userId: opts.userId ?? item?.user_id ?? cfg.userId,
agentId: opts.agentId ?? item?.agent_id,
metadata: item?.metadata,
});
added++;
} catch {
failed++;
}
}
console.log(`Imported ${added} memories.`);
if (failed) {
console.error(`${failed} memories failed to import.`);
}
} catch (err) {
console.error(`Import failed: ${String(err)}`);
}
},
);
// ====================================================================
// event (matches: mem0 event list, mem0 event status <id>)
// ====================================================================
const eventCmd = mem0
.command("event")
.description("Manage background processing events");
eventCmd
.command("list")
.description("List recent background events")
.action(async () => {
try {
const results = await backend.listEvents();
if (!results.length) {
console.log("No events found.");
return;
}
// Table header
const header = [
"Event ID".padEnd(36),
"Type".padEnd(14),
"Status".padEnd(12),
"Latency".padStart(10),
"Created".padEnd(20),
].join(" ");
console.log(header);
console.log("-".repeat(header.length));
for (const ev of results) {
const evId = String(ev.id ?? "");
const evType = String(ev.event_type ?? "—").padEnd(14);
const status = String(ev.status ?? "—").padEnd(12);
const latency = typeof ev.latency === "number"
? `${Math.round(ev.latency as number)}ms`
: "—";
const created = String(ev.created_at ?? "—")
.slice(0, 19)
.replace("T", " ");
console.log(
`${evId.padEnd(36)} ${evType} ${status} ${latency.padStart(10)} ${created}`,
);
}
console.log(`\n${results.length} event${results.length !== 1 ? "s" : ""}`);
} catch (err) {
console.error(`Failed to list events: ${String(err)}`);
}
});
eventCmd
.command("status")
.description("Get status of a specific background event")
.argument("<event_id>", "Event ID to check")
.action(async (eventId: string) => {
try {
const ev = await backend.getEvent(eventId);
const status = String(ev.status ?? "—");
const evType = String(ev.event_type ?? "—");
const latency = typeof ev.latency === "number"
? `${Math.round(ev.latency as number)}ms`
: "—";
const created = String(ev.created_at ?? "—")
.slice(0, 19)
.replace("T", " ");
const updated = String(ev.updated_at ?? "—")
.slice(0, 19)
.replace("T", " ");
console.log(`Event ID: ${eventId}`);
console.log(`Type: ${evType}`);
console.log(`Status: ${status}`);
console.log(`Latency: ${latency}`);
console.log(`Created: ${created}`);
console.log(`Updated: ${updated}`);
const results = ev.results as Record<string, unknown>[] | undefined;
if (results && Array.isArray(results) && results.length) {
console.log(`\nResults (${results.length}):`);
for (const r of results) {
const memId = String(r.id ?? "").slice(0, 8);
const data = r.data as Record<string, unknown> | undefined;
const memory = data?.memory ?? "";
const evName = String(r.event ?? "");
const user = String(r.user_id ?? "");
let detail = `${evName} ${memory}`;
if (user) detail += ` (user_id=${user})`;
console.log(` · ${detail} (${memId})`);
}
}
} catch (err) {
console.error(`Failed to get event: ${String(err)}`);
}
});
// ====================================================================
// help (matches: mem0 help, mem0 help --json)
// ====================================================================
@@ -1156,20 +1333,21 @@ export function registerCliCommands(
.action((opts: { json?: boolean }) => {
const commands = {
memory: {
add: "Add a memory from text, messages, or stdin",
search: "Query your memory store — semantic, keyword, or hybrid retrieval",
add: "Add a memory from text, messages, or stdin",
get: "Get a specific memory by ID",
list: "List memories with optional filters",
update: "Update a memory's text or metadata",
delete: "Delete a memory, all memories, or an entity",
history: "View edit history of a memory",
import: "Import memories from a JSON file",
},
management: {
init: "Interactive setup wizard for mem0 CLI",
status: "Check connectivity and authentication",
help: "Show help. Use --json for machine-readable output (for LLM agents)",
config: "Manage mem0 configuration (show, get, set)",
event: "Manage background processing events (list, status)",
dream: "Run memory consolidation (review, merge, prune)",
help: "Show help. Use --json for machine-readable output (for LLM agents)",
},
};
@@ -1283,9 +1461,8 @@ export function registerCliCommands(
});
},
{
commands: ["mem0"],
descriptors: [
{ name: "mem0", description: "Mem0 memory plugin commands" },
{ name: "mem0", description: "Mem0 memory plugin commands", hasSubcommands: true },
],
},
);
+1
View File
@@ -151,6 +151,7 @@ const ALLOWED_KEYS = [
"apiKey",
"baseUrl",
"userId",
"userEmail",
"orgId",
"projectId",
"autoCapture",
+7
View File
@@ -34,3 +34,10 @@ export function mkdirp(dirPath: string, mode?: number): void {
export function unlink(filePath: string): void {
fs.unlinkSync(filePath);
}
export function bootstrapTelemetryFlag(): void {
const val = process.env.MEM0_TELEMETRY;
if (val !== undefined) {
(globalThis as any).__mem0_telemetry_override = val;
}
}
+25 -8
View File
@@ -5,8 +5,8 @@
* and the open-source self-hosted SDK. Uses the official `mem0ai` package.
*
* Features:
* - 7 core tools: memory_search, memory_add, memory_get, memory_list,
* memory_update, memory_delete, memory_history
* - 6 core tools: memory_search, memory_add, memory_get, memory_list,
* memory_update, memory_delete
* - Short-term (session-scoped) and long-term (user-scoped) memory
* - Auto-recall: injects relevant memories (both scopes) before each agent turn
* - Auto-capture: stores key facts scoped to the current session after each agent turn
@@ -16,6 +16,7 @@
* - Dual mode: platform or open-source (self-hosted)
*/
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type {
@@ -56,6 +57,9 @@ import { readPluginAuth } from "./cli/config-file.ts";
import { registerAllTools } from "./tools/index.ts";
import type { ToolDeps } from "./tools/index.ts";
import { captureEvent } from "./telemetry.ts";
import { bootstrapTelemetryFlag } from "./fs-safe.ts";
bootstrapTelemetryFlag();
// ============================================================================
// Re-exports (for tests and external consumers)
@@ -87,12 +91,10 @@ export { createProvider } from "./providers.ts";
// Plugin Definition
// ============================================================================
const memoryPlugin = {
const memoryPlugin = definePluginEntry({
id: "openclaw-mem0",
name: "Memory (Mem0)",
description: "Mem0 memory backend — Mem0 platform or self-hosted open-source",
kind: "memory" as const,
configSchema: mem0ConfigSchema,
register(api: OpenClawPluginApi) {
// Read auth from openclaw.json plugin config (picks up post-startup login).
@@ -231,6 +233,7 @@ const memoryPlugin = {
api,
provider,
cfg,
backend,
resolveUserId: _resolveUserId,
effectiveUserId: _effectiveUserId,
agentUserId: _agentUserId,
@@ -298,7 +301,7 @@ const memoryPlugin = {
},
});
},
};
});
// ============================================================================
// Lifecycle Hook Registration
@@ -579,11 +582,11 @@ function registerHooks(
// Track last seen session ID to detect actual new sessions (not every turn)
let lastRecallSessionId: string | undefined;
// Auto-recall: inject relevant memories before agent starts
// Auto-recall: inject relevant memories before prompt is built
if (cfg.autoRecall) {
const RECALL_TIMEOUT_MS = 8_000;
api.on("before_agent_start", async (event: any, ctx: any) => {
api.on("before_prompt_build", async (event: any, ctx: any) => {
if (!event.prompt || event.prompt.length < 5) return;
// Skip non-interactive triggers (cron, heartbeat, automation)
@@ -772,6 +775,20 @@ function registerHooks(
// Update shared state for tools (best-effort — tools don't have ctx)
if (sessionId) session.setCurrentSessionId(sessionId);
const MEMORY_MUTATE_TOOLS = new Set(["memory_add", "memory_update", "memory_delete"]);
const agentUsedMemoryTool = event.messages.some((msg: any) => {
if (msg?.role !== "assistant" || !Array.isArray(msg?.content)) return false;
return msg.content.some(
(block: any) => block?.type === "tool_use" && MEMORY_MUTATE_TOOLS.has(block.name),
);
});
if (agentUsedMemoryTool) {
api.logger.info(
"openclaw-mem0: skipping auto-capture — agent already used memory tools this turn",
);
return;
}
// --- Build capture payload synchronously (cheap), then fire-and-forget ---
// Patterns indicating an assistant message contains a summary of
+30 -3
View File
@@ -9,19 +9,46 @@ declare module "openclaw/plugin-sdk" {
};
resolvePath(p: string): string;
registerTool(
definition: Record<string, unknown>,
metadata?: Record<string, unknown>,
definition: {
name: string;
description: string;
parameters: unknown;
execute: (
toolCallId: string,
params: Record<string, unknown>,
) => Promise<{ content: Array<{ type: string; text: string }>; [key: string]: unknown }>;
[key: string]: unknown;
},
metadata?: { optional?: boolean; [key: string]: unknown },
): void;
on(event: string, handler: (event: any, ctx: any) => any): void;
registerCli(
handler: (context: { program: any }) => void,
options?: Record<string, unknown>,
): void;
registerCommand?(definition: Record<string, unknown>): void;
registerService(service: {
id: string;
start: () => void;
start: (...args: any[]) => void;
stop: () => void;
}): void;
[key: string]: unknown;
}
}
declare module "openclaw/plugin-sdk/plugin-entry" {
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
export interface PluginEntry {
id: string;
name: string;
description?: string;
register(api: OpenClawPluginApi): void;
}
export function definePluginEntry<T extends PluginEntry>(entry: T): T;
}
declare module "openclaw/plugin-sdk/core" {
export * from "openclaw/plugin-sdk";
}
+3 -9
View File
@@ -7,7 +7,7 @@
"contracts": {
"tools": [
"memory_search", "memory_add", "memory_get", "memory_list",
"memory_update", "memory_delete", "memory_history"
"memory_update", "memory_delete", "memory_event_list", "memory_event_status"
]
},
"providerAuthEnvVars": {
@@ -26,12 +26,6 @@
"placeholder": "m0-...",
"help": "Platform mode only. Use a SecretRef ({\"source\":\"env\",\"provider\":\"default\",\"id\":\"MEM0_API_KEY\"}) or ${MEM0_API_KEY} instead of storing the key directly."
},
"baseUrl": {
"label": "API Base URL",
"placeholder": "https://api.mem0.ai",
"advanced": true,
"help": "Override the Mem0 API base URL. Only needed for custom deployments."
},
"userId": {
"label": "Default User ID",
"placeholder": "default",
@@ -109,10 +103,10 @@
"apiKey": {
"type": "string"
},
"baseUrl": {
"userId": {
"type": "string"
},
"userId": {
"userEmail": {
"type": "string"
},
"orgId": {
+60 -20
View File
@@ -81,6 +81,7 @@ class PlatformProvider implements Mem0Provider {
constructor(
private readonly apiKey: string,
private readonly baseUrl?: string,
private readonly orgId?: string,
private readonly projectId?: string,
) {}
@@ -97,11 +98,17 @@ class PlatformProvider implements Mem0Provider {
private async _init(): Promise<void> {
const { default: MemoryClient } = await import("mem0ai");
const opts: { apiKey: string; org_id?: string; project_id?: string } = {
const opts: {
apiKey: string;
host?: string;
organizationId?: string;
projectId?: string;
} = {
apiKey: this.apiKey,
};
if (this.orgId) opts.org_id = this.orgId;
if (this.projectId) opts.project_id = this.projectId;
if (this.baseUrl) opts.host = this.baseUrl;
if (this.orgId) opts.organizationId = this.orgId;
if (this.projectId) opts.projectId = this.projectId;
this.client = new MemoryClient(opts);
}
@@ -133,20 +140,11 @@ class PlatformProvider implements Mem0Provider {
async search(query: string, options: SearchOptions): Promise<MemoryItem[]> {
await this.ensureClient();
// Base filters: always scope by user_id, optionally by run_id
const baseFilters: Record<string, unknown> = { user_id: options.user_id };
if (options.run_id) baseFilters.run_id = options.run_id;
// Merge agent-provided filters (created_at ranges, metadata, etc.)
// with base filters. Agent filters extend, never override user scoping.
const mergedFilters = options.filters
? { AND: [baseFilters, options.filters] }
: baseFilters;
const opts: Record<string, unknown> = {
api_version: "v2",
filters: mergedFilters,
user_id: options.user_id,
};
if (options.run_id) opts.run_id = options.run_id;
if (options.top_k != null) opts.top_k = options.top_k;
if (options.threshold != null) opts.threshold = options.threshold;
if (options.keyword_search != null)
@@ -155,6 +153,14 @@ class PlatformProvider implements Mem0Provider {
if (options.filter_memories != null)
opts.filter_memories = options.filter_memories;
if (options.categories != null) opts.categories = options.categories;
const baseFilters: Record<string, unknown> = { user_id: options.user_id };
if (options.run_id) baseFilters.run_id = options.run_id;
if (options.filters) {
opts.filters = { AND: [baseFilters, options.filters] };
} else {
opts.filters = baseFilters;
}
const results = await this.client.search(query, opts);
return normalizeSearchResults(results);
@@ -168,10 +174,16 @@ class PlatformProvider implements Mem0Provider {
async getAll(options: ListOptions): Promise<MemoryItem[]> {
await this.ensureClient();
const opts: Record<string, unknown> = { user_id: options.user_id };
if (options.run_id) opts.run_id = options.run_id;
const opts: Record<string, unknown> = {
api_version: "v2",
user_id: options.user_id,
filters: { user_id: options.user_id },
};
if (options.run_id) {
opts.run_id = options.run_id;
(opts.filters as Record<string, unknown>).run_id = options.run_id;
}
if (options.page_size != null) opts.page_size = options.page_size;
if (options.source) opts.source = options.source;
const results = await this.client.getAll(opts);
if (Array.isArray(results)) return results.map(normalizeMemoryItem);
@@ -242,10 +254,38 @@ class OSSProvider implements Mem0Provider {
const config: Record<string, unknown> = { version: "v1.1" };
if (this.ossConfig?.embedder) config.embedder = this.ossConfig.embedder;
const defaultEmbedder = { provider: "openai", config: { model: "text-embedding-3-small" } };
const defaultLlm = { provider: "openai", config: { model: "gpt-5.4" } };
// Helper: strip empty-string values so they don't clobber defaults
const stripEmpty = (obj: Record<string, unknown>) => {
const out = { ...obj };
for (const k of Object.keys(out)) { if (out[k] === "") delete out[k]; }
return out;
};
if (this.ossConfig?.embedder) {
const ec = stripEmpty(this.ossConfig.embedder.config ?? {});
config.embedder = {
provider: this.ossConfig.embedder.provider || defaultEmbedder.provider,
config: { ...defaultEmbedder.config, ...ec },
};
} else {
config.embedder = defaultEmbedder;
}
if (this.ossConfig?.llm) {
const lc = stripEmpty(this.ossConfig.llm.config ?? {});
config.llm = {
provider: this.ossConfig.llm.provider || defaultLlm.provider,
config: { ...defaultLlm.config, ...lc },
};
} else {
config.llm = defaultLlm;
}
if (this.ossConfig?.vectorStore)
config.vectorStore = this.ossConfig.vectorStore;
if (this.ossConfig?.llm) config.llm = this.ossConfig.llm;
if (this.ossConfig?.historyDbPath) {
const dbPath = this.resolvePath
@@ -419,7 +459,7 @@ export function createProvider(
);
}
return new PlatformProvider(cfg.apiKey!, cfg.orgId, cfg.projectId);
return new PlatformProvider(cfg.apiKey!, cfg.baseUrl, cfg.orgId, cfg.projectId);
}
// ============================================================================
-3
View File
@@ -34,9 +34,6 @@ Identify which memories need action. Use the tools to investigate.
**Search for recent additions:**
Call `memory_search` with a `created_at` filter to find memories added since the last consolidation. These are the most likely to need merging or cleanup.
**Check edit history on suspicious entries:**
If a memory looks like it was updated multiple times or has contradictory content, call `memory_history` on it to understand how it evolved.
**Classify each target into one of these actions:**
- DELETE: contains credentials, expired by TTL, pure noise, raw tool output, standalone timestamps
- MERGE: two or more memories express the same fact in different words, or a series tracks incremental changes to the same entity
+14 -2
View File
@@ -22,9 +22,21 @@ const FLUSH_THRESHOLD = 10;
let eventQueue: Record<string, unknown>[] = [];
let flushTimer: ReturnType<typeof setInterval> | undefined;
let _telemetryEnabled: boolean | undefined;
function isTelemetryEnabled(): boolean {
const val = (process.env.MEM0_TELEMETRY ?? "true").toLowerCase();
return val !== "false" && val !== "0" && val !== "no";
if (_telemetryEnabled !== undefined) return _telemetryEnabled;
try {
const val = (globalThis as any).__mem0_telemetry_override;
if (val !== undefined) {
const s = String(val).toLowerCase();
_telemetryEnabled = s !== "false" && s !== "0" && s !== "no";
} else {
_telemetryEnabled = true;
}
} catch {
_telemetryEnabled = true;
}
return _telemetryEnabled;
}
/**
+7
View File
@@ -0,0 +1,7 @@
/**
* Test shim for openclaw/plugin-sdk/plugin-entry.
* At runtime this is resolved from the OpenClaw gateway.
*/
export function definePluginEntry<T>(entry: T): T {
return entry;
}
+19
View File
@@ -0,0 +1,19 @@
/**
* Test shim for openclaw/plugin-sdk.
* At runtime this is resolved from the OpenClaw gateway.
*/
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: (...args: any[]) => void; stop: () => void }): void;
[key: string]: unknown;
}
+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");
});
});
+6 -2
View File
@@ -1,5 +1,6 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type { Mem0Config, Mem0Provider, AddOptions, SearchOptions } from "../types.ts";
import type { Backend } from "../backend/base.ts";
import { createMemorySearchTool } from "./memory-search.ts";
import { createMemoryAddTool } from "./memory-add.ts";
@@ -7,12 +8,14 @@ import { createMemoryGetTool } from "./memory-get.ts";
import { createMemoryListTool } from "./memory-list.ts";
import { createMemoryUpdateTool } from "./memory-update.ts";
import { createMemoryDeleteTool } from "./memory-delete.ts";
import { createMemoryHistoryTool } from "./memory-history.ts";
import { createMemoryEventListTool } from "./memory-event-list.ts";
import { createMemoryEventStatusTool } from "./memory-event-status.ts";
export interface ToolDeps {
api: OpenClawPluginApi;
provider: Mem0Provider;
cfg: Mem0Config;
backend?: Backend;
resolveUserId: (opts: { agentId?: string; userId?: string }) => string;
effectiveUserId: (sessionKey?: string) => string;
agentUserId: (id: string) => string;
@@ -32,5 +35,6 @@ export function registerAllTools(deps: ToolDeps): void {
api.registerTool(createMemoryListTool(deps));
api.registerTool(createMemoryUpdateTool(deps));
api.registerTool(createMemoryDeleteTool(deps));
api.registerTool(createMemoryHistoryTool(deps));
api.registerTool(createMemoryEventListTool(deps));
api.registerTool(createMemoryEventStatusTool(deps));
}
+60
View File
@@ -0,0 +1,60 @@
import { Type } from "@sinclair/typebox";
import type { ToolDeps } from "./index.ts";
export function createMemoryEventListTool(deps: ToolDeps) {
return {
name: "memory_event_list",
label: "Memory Event List",
description:
"List recent background processing events from the Mem0 Platform. Use to check whether memory operations (add, update, delete) were processed successfully.",
parameters: Type.Object({}),
async execute(_toolCallId: string, _params: Record<string, unknown>) {
const start = Date.now();
try {
if (!deps.backend) {
deps.captureToolEvent("memory_event_list", { success: false, latency_ms: 0, error: "not_platform" });
return {
content: [{ type: "text", text: "Event tracking is only available in platform mode." }],
details: { error: "not_platform" },
};
}
const results = await deps.backend.listEvents();
if (!results.length) {
deps.captureToolEvent("memory_event_list", { success: true, latency_ms: Date.now() - start, count: 0 });
return {
content: [{ type: "text", text: "No events found." }],
details: { count: 0 },
};
}
const rows = results.map((ev) => {
const evId = String(ev.id ?? "");
const evType = String(ev.event_type ?? "—");
const status = String(ev.status ?? "—");
const latency =
typeof ev.latency === "number" ? `${Math.round(ev.latency as number)}ms` : "—";
const created = String(ev.created_at ?? "—").slice(0, 19).replace("T", " ");
return { id: evId, type: evType, status, latency, created };
});
const text = rows
.map((r) => `- ${r.id} | ${r.type} | ${r.status} | ${r.latency} | ${r.created}`)
.join("\n");
deps.captureToolEvent("memory_event_list", { success: true, latency_ms: Date.now() - start, count: results.length });
return {
content: [{ type: "text", text: `${results.length} event(s):\n${text}` }],
details: { count: results.length, events: rows },
};
} catch (err) {
deps.captureToolEvent("memory_event_list", { success: false, latency_ms: Date.now() - start, error: String(err) });
return {
content: [{ type: "text", text: `Failed to list events: ${String(err)}` }],
details: { error: String(err) },
};
}
},
};
}
+63
View File
@@ -0,0 +1,63 @@
import { Type } from "@sinclair/typebox";
import type { ToolDeps } from "./index.ts";
export function createMemoryEventStatusTool(deps: ToolDeps) {
return {
name: "memory_event_status",
label: "Memory Event Status",
description:
"Get detailed status of a specific background processing event. Use to verify whether a memory add/update/delete was processed, view latency, and inspect results.",
parameters: Type.Object({
event_id: Type.String({ description: "The event ID to check" }),
}),
async execute(_toolCallId: string, params: Record<string, unknown>) {
const { event_id: eventId } = params as { event_id: string };
const start = Date.now();
try {
if (!deps.backend) {
deps.captureToolEvent("memory_event_status", { success: false, latency_ms: 0, error: "not_platform" });
return {
content: [{ type: "text", text: "Event tracking is only available in platform mode." }],
details: { error: "not_platform" },
};
}
const ev = await deps.backend.getEvent(eventId);
const status = String(ev.status ?? "—");
const evType = String(ev.event_type ?? "—");
const latency =
typeof ev.latency === "number" ? `${Math.round(ev.latency as number)}ms` : "—";
const created = String(ev.created_at ?? "—").slice(0, 19).replace("T", " ");
const updated = String(ev.updated_at ?? "—").slice(0, 19).replace("T", " ");
let text = `Event: ${eventId}\nType: ${evType}\nStatus: ${status}\nLatency: ${latency}\nCreated: ${created}\nUpdated: ${updated}`;
const results = ev.results as Record<string, unknown>[] | undefined;
if (results && Array.isArray(results) && results.length) {
const resultLines = results.map((r) => {
const memId = String(r.id ?? "").slice(0, 8);
const data = r.data as Record<string, unknown> | undefined;
const memory = data?.memory ?? "";
const evName = String(r.event ?? "");
return `- [${evName}] ${memory} (${memId})`;
});
text += `\n\nResults (${results.length}):\n${resultLines.join("\n")}`;
}
deps.captureToolEvent("memory_event_status", { success: true, latency_ms: Date.now() - start });
return {
content: [{ type: "text", text }],
details: { event: ev },
};
} catch (err) {
deps.captureToolEvent("memory_event_status", { success: false, latency_ms: Date.now() - start, error: String(err) });
return {
content: [{ type: "text", text: `Failed to get event: ${String(err)}` }],
details: { error: String(err) },
};
}
},
};
}
-37
View File
@@ -1,37 +0,0 @@
import { Type } from "@sinclair/typebox";
import type { ToolDeps } from "./index.ts";
export function createMemoryHistoryTool(deps: ToolDeps) {
const { provider } = deps;
return {
name: "memory_history",
label: "Memory History",
description: "View the edit history of a specific memory.",
parameters: Type.Object({
memoryId: Type.String({ description: "The memory ID to view history for" }),
}),
async execute(_toolCallId: string, params: Record<string, unknown>) {
const { memoryId } = params as { memoryId: string };
const start = Date.now();
try {
const history = await provider.history(memoryId);
deps.captureToolEvent("memory_history", { success: true, latency_ms: Date.now() - start, result_count: history?.length ?? 0 });
if (!history || history.length === 0) {
return { content: [{ type: "text", text: `No history found for memory ${memoryId}.` }], details: { count: 0 } };
}
const text = history.map((h, i) =>
`${i + 1}. [${h.event}] ${h.created_at}\n Old: ${h.old_memory || "(none)"}\n New: ${h.new_memory || "(none)"}`
).join("\n\n");
return {
content: [{ type: "text", text: `History for memory ${memoryId} (${history.length} entries):\n\n${text}` }],
details: { count: history.length, history },
};
} catch (err) {
deps.captureToolEvent("memory_history", { success: false, latency_ms: Date.now() - start, error: String(err) });
return { content: [{ type: "text", text: `Memory history failed: ${String(err)}` }], details: { error: String(err) } };
}
},
};
}
+1 -1
View File
@@ -19,6 +19,6 @@
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": ["index.ts", "types.ts", "providers.ts", "config.ts", "filtering.ts", "isolation.ts", "openclaw-plugin-sdk.d.ts", "backend/**/*.ts", "tools/**/*.ts", "cli/**/*.ts", "skill-loader.ts", "recall.ts", "dream-gate.ts"],
"include": ["index.ts", "types.ts", "providers.ts", "config.ts", "filtering.ts", "isolation.ts", "openclaw-plugin-sdk.d.ts", "backend/**/*.ts", "tools/**/*.ts", "cli/**/*.ts", "skill-loader.ts", "recall.ts", "dream-gate.ts", "telemetry.ts", "fs-safe.ts"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
+1 -1
View File
@@ -7,5 +7,5 @@ export default defineConfig({
dts: true,
sourcemap: true,
clean: true,
external: [/^node:/, "fs", "os", "path", "url", "readline", "module"],
external: [/^node:/, /^openclaw\//, "fs", "os", "path", "url", "readline", "module"],
});
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
alias: {
// OpenClaw SDK modules are resolved from the gateway at runtime.
// During unit tests we provide lightweight stubs.
"openclaw/plugin-sdk/plugin-entry": new URL(
"./test-shims/plugin-entry.ts",
import.meta.url,
).pathname,
"openclaw/plugin-sdk": new URL(
"./test-shims/plugin-sdk.ts",
import.meta.url,
).pathname,
},
},
});
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mem0ai"
version = "1.0.10"
version = "1.0.11"
description = "Long-term memory for AI Agents"
authors = [
{ name = "Mem0", email = "support@mem0.ai" }