fix(pi-agent-plugin): make command results visible and relevance-filtered (#5504)

This commit is contained in:
Kartik
2026-06-12 19:13:31 +05:30
committed by GitHub
parent b5ec46be5b
commit f681889b14
9 changed files with 383 additions and 236 deletions
+16
View File
@@ -1,5 +1,21 @@
# Changelog
## 0.1.2 (2026-06-12)
### Fixed
- **Visible command feedback** — `/mem0-remember`, `/mem0-forget`, `/mem0-pin`, and `/mem0-scope` now render their results as persistent message blocks (`pi.sendMessage({ display: true })`) instead of `ctx.ui.notify(..., "info")`. The Pi TUI draws `"info"` notifications as dim, collapsible status text that overwrites the previous line, so command results were easily missed (felt like "no feedback"). Warnings and errors still use `ctx.ui.notify`, which renders prominently.
- **Relevance-filtered search** — `/mem0-search`, `/mem0-forget`, and `/mem0-pin` pass a similarity `threshold` (`searchThreshold`, default `0.3`; configurable in `mem0-config.json`, shown in `/mem0-status`), `top_k`, and `rerank` to the mem0 search API, matching the Claude Code and OpenClaw integrations. mem0 ranks results by similarity with no relevance floor, so without a threshold an unrelated query returns the closest (weak) memories — and `/mem0-forget` would offer them for deletion. The server-side threshold makes a query with no sufficiently similar memory report no match, and reranking orders the genuine matches by deeper relevance. Raise `searchThreshold` to be stricter; lower it if relevant results are missed.
### Improved
- **Richer feedback across every command** — results now show what actually happened: an action heading plus the relevant scope, query, match count, and affected memories.
- `/mem0-remember` echoes the stored text and the scope it landed in, instead of a generic success line.
- `/mem0-search` adds an `N matches for "<query>"` header.
- `/mem0-forget` and `/mem0-pin` name the query when nothing matches, list the affected memory, and label the disambiguation dialog with the match count.
- `/mem0-scope` explains where new memories will be saved.
- **Cleaner `/mem0-dream`** — the consolidation protocol is now sent to the agent hidden (`display: false`, still included in LLM context) behind a concise "Dreaming…" status line, instead of dumping the raw protocol into the transcript.
## 0.1.1 (2026-06-10)
Maintenance release — no functional changes.
+3
View File
@@ -43,6 +43,7 @@ Or create a config file at `~/.pi/agent/mem0-config.json`:
"userId": "your-username",
"autoCapture": true,
"defaultScope": "project",
"searchThreshold": 0.2,
"dream": {
"enabled": true,
"auto": true,
@@ -55,6 +56,8 @@ Or create a config file at `~/.pi/agent/mem0-config.json`:
Environment variables (`MEM0_API_KEY`, `MEM0_USER_ID`) override the config file.
`searchThreshold` (default `0.3`) is the minimum similarity score (0–1) a memory must reach to count as a match for `/mem0-search`, `/mem0-forget`, and `/mem0-pin`. It is passed to the mem0 search API (along with reranking for higher-precision ordering), so a query with no sufficiently similar memory reports no match instead of returning the closest unrelated memories. Raise it to be stricter; lower it if relevant results are missed.
## Commands
| Command | Description |
+9 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/pi-agent-plugin",
"version": "0.1.1",
"version": "0.1.2",
"type": "module",
"description": "Mem0 memory extension for Pi Agent persistent, scoped, semantic memory across sessions and projects",
"license": "Apache-2.0",
@@ -39,8 +39,12 @@
"LICENSE"
],
"pi": {
"extensions": ["./src/entry.ts"],
"skills": ["./skills"]
"extensions": [
"./src/entry.ts"
],
"skills": [
"./skills"
]
},
"scripts": {
"build": "tsup",
@@ -49,8 +53,8 @@
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"@earendil-works/pi-coding-agent": "*",
"@earendil-works/pi-ai": "*",
"@earendil-works/pi-coding-agent": "*",
"typebox": "*"
},
"devDependencies": {
@@ -63,7 +67,7 @@
"vitest": "^4.1.7"
},
"dependencies": {
"mem0ai": "^3.0.6"
"mem0ai": "^3.0.7"
},
"pnpm": {
"overrides": {
+7 -149
View File
@@ -12,8 +12,8 @@ importers:
.:
dependencies:
mem0ai:
specifier: ^3.0.6
version: 3.0.6(@anthropic-ai/sdk@0.91.1(zod@3.25.76))(@azure/identity@4.13.1)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260610.1)(@google/genai@1.52.0)(@langchain/core@1.1.48(openai@6.26.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@mistralai/mistralai@2.2.1)(@qdrant/js-client-rest@1.18.0(typescript@6.0.3))(@supabase/supabase-js@2.108.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.10.0)(cloudflare@4.5.0)(compromise@14.15.1)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.11.3)(redis@4.7.1)(ws@8.21.0)
specifier: ^3.0.7
version: 3.0.7(@anthropic-ai/sdk@0.91.1(zod@3.25.76))(@azure/identity@4.13.1)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260610.1)(@google/genai@1.52.0)(@langchain/core@1.1.48(openai@6.26.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@mistralai/mistralai@2.2.1)(@qdrant/js-client-rest@1.18.0(typescript@6.0.3))(@supabase/supabase-js@2.108.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.10.0)(cloudflare@4.5.0)(compromise@14.15.1)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.21.0)(redis@5.12.1)(ws@8.21.0)
devDependencies:
'@earendil-works/pi-ai':
specifier: ^0.79.0
@@ -469,35 +469,30 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@mariozechner/clipboard-linux-arm64-musl@0.3.9':
resolution: {integrity: sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@mariozechner/clipboard-linux-riscv64-gnu@0.3.9':
resolution: {integrity: sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@mariozechner/clipboard-linux-x64-gnu@0.3.9':
resolution: {integrity: sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@mariozechner/clipboard-linux-x64-musl@0.3.9':
resolution: {integrity: sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@mariozechner/clipboard-win32-arm64-msvc@0.3.9':
resolution: {integrity: sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==}
@@ -573,21 +568,12 @@ packages:
resolution: {integrity: sha512-oQG/FejNpItrxRHoyctYvT3rwGZOnK4jr3JdppO/c78ktDvkWiPXPHNsrDf33K9sZdRb6PR7gi4noIapu5q4HA==}
engines: {node: '>=18.0.0', pnpm: '>=8'}
'@redis/bloom@1.2.0':
resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==}
peerDependencies:
'@redis/client': ^1.0.0
'@redis/bloom@5.12.1':
resolution: {integrity: sha512-PUUfv+ms7jgPSBVoo/DN4AkPHj4D5TZSd6SbJX7egzBplkYUcKmHRE8RKia7UtZ8bSQbLguLvxVO+asKtQfZWA==}
engines: {node: '>= 18.19.0'}
peerDependencies:
'@redis/client': ^5.12.1
'@redis/client@1.6.1':
resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==}
engines: {node: '>=14'}
'@redis/client@5.12.1':
resolution: {integrity: sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==}
engines: {node: '>= 18.19.0'}
@@ -600,38 +586,18 @@ packages:
'@opentelemetry/api':
optional: true
'@redis/graph@1.1.1':
resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==}
peerDependencies:
'@redis/client': ^1.0.0
'@redis/json@1.0.7':
resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==}
peerDependencies:
'@redis/client': ^1.0.0
'@redis/json@5.12.1':
resolution: {integrity: sha512-eOze75esLve4vfqDel7aMX08CNaiLLQS2fV8mpRN9NxPe1rVR4vQyYiW/OgtGUysF6QOr9ANhfxABKNOJfXdKg==}
engines: {node: '>= 18.19.0'}
peerDependencies:
'@redis/client': ^5.12.1
'@redis/search@1.2.0':
resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==}
peerDependencies:
'@redis/client': ^1.0.0
'@redis/search@5.12.1':
resolution: {integrity: sha512-ItlxbxC9cKI6IU1TLWoczwJCRb6TdmkEpWv05UrPawqaAnWGRu3rcIqsc5vN483T2fSociuyV1UkWIL5I4//2w==}
engines: {node: '>= 18.19.0'}
peerDependencies:
'@redis/client': ^5.12.1
'@redis/time-series@1.1.0':
resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==}
peerDependencies:
'@redis/client': ^1.0.0
'@redis/time-series@5.12.1':
resolution: {integrity: sha512-c6JL6E3EcZJuNqKFz+KM+l9l5mpcQiKvTwgA3blt5glWJ8hjDk0yeHN3beE/MpqYIQ8UEX44ItQzgkE/gCBELQ==}
engines: {node: '>= 18.19.0'}
@@ -673,42 +639,36 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.3':
resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.0.3':
resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.0.3':
resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.0.3':
resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.3':
resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.3':
resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==}
@@ -770,79 +730,66 @@ packages:
resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.61.1':
resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.61.1':
resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.61.1':
resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.61.1':
resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.61.1':
resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.61.1':
resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.61.1':
resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.61.1':
resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.61.1':
resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.61.1':
resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.61.1':
resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.61.1':
resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.61.1':
resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==}
@@ -1133,10 +1080,6 @@ packages:
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
buffer-writer@2.0.0:
resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==}
engines: {node: '>=4'}
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
@@ -1427,10 +1370,6 @@ packages:
resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==}
engines: {node: '>=18'}
generic-pool@3.9.0:
resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==}
engines: {node: '>= 4'}
get-east-asian-width@1.6.0:
resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==}
engines: {node: '>=18'}
@@ -1661,28 +1600,24 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
@@ -1754,8 +1689,8 @@ packages:
md5@2.3.0:
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
mem0ai@3.0.6:
resolution: {integrity: sha512-X1KbCVE5351qpJ6X2KXFhQJiW+dqoHj1otA66DSdXtIEO1uXuIdyeTKeURdAH+wjXtIcFa08ylL2meA1pBk9Iw==}
mem0ai@3.0.7:
resolution: {integrity: sha512-CUHzX7DyeKTHcI3aDsSqY9LXTD7GcFxf988796TuOa4yJgGuF2Xd2NROcBhVFRo3r9y8fVmbo3c5TF9jv1KlKw==}
engines: {node: '>=18'}
peerDependencies:
'@anthropic-ai/sdk': ^0.40.1
@@ -1966,9 +1901,6 @@ packages:
resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==}
engines: {node: '>=8'}
packet-reader@1.0.0:
resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==}
partial-json@0.1.7:
resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==}
@@ -2017,15 +1949,6 @@ packages:
resolution: {integrity: sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg==}
engines: {node: '>=10'}
pg@8.11.3:
resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==}
engines: {node: '>= 8.0.0'}
peerDependencies:
pg-native: '>=3.0.1'
peerDependenciesMeta:
pg-native:
optional: true
pg@8.21.0:
resolution: {integrity: sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==}
engines: {node: '>= 16.0.0'}
@@ -2156,9 +2079,6 @@ packages:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
redis@4.7.1:
resolution: {integrity: sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==}
redis@5.12.1:
resolution: {integrity: sha512-LDsoVvb/CpoV9EN3FXvgvSHNJWuCIzl9MiO3ppOevuGLpSGJhwfQjpEwfFJcQvNSddHADDdZaWx0HnmMxRXG7g==}
engines: {node: '>= 18.19.0'}
@@ -2553,9 +2473,6 @@ packages:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yaml@2.9.0:
resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
engines: {node: '>= 14.6'}
@@ -3242,48 +3159,22 @@ snapshots:
'@qdrant/openapi-typescript-fetch@1.2.6': {}
'@redis/bloom@1.2.0(@redis/client@1.6.1)':
dependencies:
'@redis/client': 1.6.1
'@redis/bloom@5.12.1(@redis/client@5.12.1)':
dependencies:
'@redis/client': 5.12.1
'@redis/client@1.6.1':
dependencies:
cluster-key-slot: 1.1.2
generic-pool: 3.9.0
yallist: 4.0.0
'@redis/client@5.12.1':
dependencies:
cluster-key-slot: 1.1.2
'@redis/graph@1.1.1(@redis/client@1.6.1)':
dependencies:
'@redis/client': 1.6.1
'@redis/json@1.0.7(@redis/client@1.6.1)':
dependencies:
'@redis/client': 1.6.1
'@redis/json@5.12.1(@redis/client@5.12.1)':
dependencies:
'@redis/client': 5.12.1
'@redis/search@1.2.0(@redis/client@1.6.1)':
dependencies:
'@redis/client': 1.6.1
'@redis/search@5.12.1(@redis/client@5.12.1)':
dependencies:
'@redis/client': 5.12.1
'@redis/time-series@1.1.0(@redis/client@1.6.1)':
dependencies:
'@redis/client': 1.6.1
'@redis/time-series@5.12.1(@redis/client@5.12.1)':
dependencies:
'@redis/client': 5.12.1
@@ -3706,8 +3597,6 @@ snapshots:
buffer-equal-constant-time@1.0.1: {}
buffer-writer@2.0.0: {}
buffer@5.7.1:
dependencies:
base64-js: 1.5.1
@@ -3995,8 +3884,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
generic-pool@3.9.0: {}
get-east-asian-width@1.6.0: {}
get-intrinsic@1.3.0:
@@ -4303,7 +4190,7 @@ snapshots:
crypt: 0.0.2
is-buffer: 1.1.6
mem0ai@3.0.6(@anthropic-ai/sdk@0.91.1(zod@3.25.76))(@azure/identity@4.13.1)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260610.1)(@google/genai@1.52.0)(@langchain/core@1.1.48(openai@6.26.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@mistralai/mistralai@2.2.1)(@qdrant/js-client-rest@1.18.0(typescript@6.0.3))(@supabase/supabase-js@2.108.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.10.0)(cloudflare@4.5.0)(compromise@14.15.1)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.11.3)(redis@4.7.1)(ws@8.21.0):
mem0ai@3.0.7(@anthropic-ai/sdk@0.91.1(zod@3.25.76))(@azure/identity@4.13.1)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260610.1)(@google/genai@1.52.0)(@langchain/core@1.1.48(openai@6.26.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@mistralai/mistralai@2.2.1)(@qdrant/js-client-rest@1.18.0(typescript@6.0.3))(@supabase/supabase-js@2.108.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.10.0)(cloudflare@4.5.0)(compromise@14.15.1)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.21.0)(redis@5.12.1)(ws@8.21.0):
dependencies:
'@anthropic-ai/sdk': 0.91.1(zod@3.25.76)
'@azure/identity': 4.13.1
@@ -4324,8 +4211,8 @@ snapshots:
natural: 8.1.1
ollama: 0.5.18
openai: 4.104.0(ws@8.21.0)(zod@3.25.76)
pg: 8.11.3
redis: 4.7.1
pg: 8.21.0
redis: 5.12.1
uuid: 14.0.0
zod: 3.25.76
transitivePeerDependencies:
@@ -4515,8 +4402,6 @@ snapshots:
dependencies:
p-finally: 1.0.0
packet-reader@1.0.0: {}
partial-json@0.1.7: {}
path-expression-matcher@1.5.0: {}
@@ -4539,10 +4424,6 @@ snapshots:
pg-numeric@1.0.2: {}
pg-pool@3.14.0(pg@8.11.3):
dependencies:
pg: 8.11.3
pg-pool@3.14.0(pg@8.21.0):
dependencies:
pg: 8.21.0
@@ -4567,18 +4448,6 @@ snapshots:
postgres-interval: 3.0.0
postgres-range: 1.1.4
pg@8.11.3:
dependencies:
buffer-writer: 2.0.0
packet-reader: 1.0.0
pg-connection-string: 2.13.0
pg-pool: 3.14.0(pg@8.11.3)
pg-protocol: 1.14.0
pg-types: 2.2.0
pgpass: 1.0.5
optionalDependencies:
pg-cloudflare: 1.4.0
pg@8.21.0:
dependencies:
pg-connection-string: 2.13.0
@@ -4711,15 +4580,6 @@ snapshots:
readdirp@4.1.2: {}
redis@4.7.1:
dependencies:
'@redis/bloom': 1.2.0(@redis/client@1.6.1)
'@redis/client': 1.6.1
'@redis/graph': 1.1.1(@redis/client@1.6.1)
'@redis/json': 1.0.7(@redis/client@1.6.1)
'@redis/search': 1.2.0(@redis/client@1.6.1)
'@redis/time-series': 1.1.0(@redis/client@1.6.1)
redis@5.12.1:
dependencies:
'@redis/bloom': 5.12.1(@redis/client@5.12.1)
@@ -5061,8 +4921,6 @@ snapshots:
xtend@4.0.2: {}
yallist@4.0.0: {}
yaml@2.9.0: {}
zod-to-json-schema@3.25.2(zod@4.4.3):
+226 -13
View File
@@ -55,6 +55,7 @@ const defaultConfig: Mem0Config = {
autoCapture: false,
defaultScope: "project",
contextInjection: false,
searchThreshold: 0.3,
dream: { enabled: false, auto: false, minHours: 24, minSessions: 5, minMemories: 20 },
};
@@ -67,6 +68,7 @@ describe("registerCommands", () => {
beforeEach(() => {
pi = makePi();
mem0 = makeMem0();
defaultConfig.defaultScope = "project";
registerCommands(pi as any, mem0, defaultConfig, () => scopeCtx);
});
@@ -90,11 +92,17 @@ describe("registerCommands", () => {
expect(mem0.search).not.toHaveBeenCalled();
});
it("notifies when no memories match", async () => {
it("sends a visible message naming the query when no memories match", async () => {
const ctx = makeCtx();
mem0.search.mockResolvedValue({ results: [] });
await pi._invoke("mem0-forget", "old preference", ctx);
expect(ctx.ui.notify).toHaveBeenCalledWith("No matching memories found.", "info");
expect(pi.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({
customType: "mem0-forget",
content: expect.stringContaining('No matches for "old preference"'),
display: true,
}),
);
});
it("asks for confirmation before deleting a single match", async () => {
@@ -111,6 +119,22 @@ describe("registerCommands", () => {
expect(mem0.delete).toHaveBeenCalledWith("abc-123");
});
it("sends a visible confirmation showing what was forgotten", async () => {
const ctx = makeCtx(true);
mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "test mem" }] });
mem0.delete.mockResolvedValue({ message: "Deleted" });
await pi._invoke("mem0-forget", "test", ctx);
expect(pi.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({
customType: "mem0-forget",
content: expect.stringContaining("Forgotten"),
display: true,
}),
);
});
it("does not delete when user cancels confirmation", async () => {
const ctx = makeCtx(false);
mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "test mem" }] });
@@ -119,7 +143,9 @@ describe("registerCommands", () => {
expect(ctx.ui.confirm).toHaveBeenCalled();
expect(mem0.delete).not.toHaveBeenCalled();
expect(ctx.ui.notify).toHaveBeenCalledWith("Cancelled.", "info");
expect(pi.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({ content: expect.stringContaining("Cancelled"), display: true }),
);
});
it("uses select UI for multiple matches and deletes chosen memory", async () => {
@@ -136,13 +162,20 @@ describe("registerCommands", () => {
await pi._invoke("mem0-forget", "test", ctx);
expect(ctx.ui.select).toHaveBeenCalledWith(
"Which memory should I delete?",
expect.stringContaining("which should I delete"),
expect.arrayContaining([
expect.stringContaining("mem one"),
expect.stringContaining("mem two"),
]),
);
expect(mem0.delete).toHaveBeenCalledWith("id-2");
expect(pi.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({
customType: "mem0-forget",
content: expect.stringContaining("Forgotten"),
display: true,
}),
);
});
it("does not delete when user cancels select", async () => {
@@ -158,7 +191,9 @@ describe("registerCommands", () => {
await pi._invoke("mem0-forget", "test", ctx);
expect(mem0.delete).not.toHaveBeenCalled();
expect(ctx.ui.notify).toHaveBeenCalledWith("Cancelled.", "info");
expect(pi.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({ content: expect.stringContaining("Cancelled"), display: true }),
);
});
});
@@ -179,6 +214,22 @@ describe("registerCommands", () => {
expect(mem0.delete).not.toHaveBeenCalled();
});
it("sends a visible confirmation after pinning", async () => {
const ctx = makeCtx(true);
mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "important fact" }] });
mem0.update.mockResolvedValue([]);
await pi._invoke("mem0-pin", "important", ctx);
expect(pi.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({
customType: "mem0-pin",
content: expect.stringContaining("Pinned"),
display: true,
}),
);
});
it("does not pin when user cancels", async () => {
const ctx = makeCtx(false);
mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "fact" }] });
@@ -188,7 +239,7 @@ describe("registerCommands", () => {
expect(mem0.update).not.toHaveBeenCalled();
});
it("skips already-pinned memories", async () => {
it("skips already-pinned memories with a visible message", async () => {
const ctx = makeCtx();
mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "[PINNED] fact" }] });
@@ -196,7 +247,9 @@ describe("registerCommands", () => {
expect(ctx.ui.confirm).not.toHaveBeenCalled();
expect(mem0.add).not.toHaveBeenCalled();
expect(ctx.ui.notify).toHaveBeenCalledWith("Already pinned.", "info");
expect(pi.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({ content: expect.stringContaining("Already pinned"), display: true }),
);
});
it("uses select UI for multiple matches and pins chosen memory", async () => {
@@ -213,7 +266,7 @@ describe("registerCommands", () => {
await pi._invoke("mem0-pin", "fact", ctx);
expect(ctx.ui.select).toHaveBeenCalledWith(
"Which memory should I pin?",
expect.stringContaining("which should I pin"),
expect.arrayContaining([
expect.stringContaining("fact one"),
expect.stringContaining("fact two"),
@@ -235,18 +288,23 @@ describe("registerCommands", () => {
await pi._invoke("mem0-pin", "fact", ctx);
expect(mem0.update).not.toHaveBeenCalled();
expect(ctx.ui.notify).toHaveBeenCalledWith("Cancelled.", "info");
expect(pi.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({ content: expect.stringContaining("Cancelled"), display: true }),
);
});
});
describe("/mem0-search", () => {
it("always performs semantic search", async () => {
it("performs server-side semantic search with a relevance threshold", async () => {
const ctx = makeCtx();
mem0.search.mockResolvedValue({ results: [{ id: "id-1", memory: "result" }] });
await pi._invoke("mem0-search", "my preferences", ctx);
expect(mem0.search).toHaveBeenCalledWith("my preferences", expect.any(Object));
expect(mem0.search).toHaveBeenCalledWith(
"my preferences",
expect.objectContaining({ threshold: 0.3, topK: 10, rerank: true }),
);
expect(pi.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({ customType: "mem0-search" }),
);
@@ -263,16 +321,49 @@ describe("registerCommands", () => {
expect(mem0.get).not.toHaveBeenCalled();
});
it("shows empty results message", async () => {
it("shows a no-matches message naming the query", async () => {
const ctx = makeCtx();
mem0.search.mockResolvedValue({ results: [] });
await pi._invoke("mem0-search", "nonexistent", ctx);
expect(pi.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({ content: "No memories found." }),
expect.objectContaining({ content: expect.stringContaining("No matches") }),
);
});
it("shows a result count header when there are matches", async () => {
const ctx = makeCtx();
mem0.search.mockResolvedValue({
results: [
{ id: "id-1", memory: "one" },
{ id: "id-2", memory: "two" },
],
});
await pi._invoke("mem0-search", "stuff", ctx);
expect(pi.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({ content: expect.stringContaining("2 matches") }),
);
});
it("shows all results the API returns (relevance gating is server-side)", async () => {
const ctx = makeCtx();
mem0.search.mockResolvedValue({
results: [
{ id: "id-1", memory: "first match", score: 0.62 },
{ id: "id-2", memory: "second match", score: 0.31 },
],
});
await pi._invoke("mem0-search", "stuff", ctx);
const call = pi.sendMessage.mock.calls.find(([m]: any[]) => m.customType === "mem0-search");
expect(call?.[0].content).toContain("first match");
expect(call?.[0].content).toContain("second match");
expect(call?.[0].content).toContain("2 matches");
});
});
describe("/mem0-remember", () => {
@@ -288,10 +379,132 @@ describe("registerCommands", () => {
);
});
it("shows the stored text in a visible confirmation (infer:false status response)", async () => {
const ctx = makeCtx();
mem0.add.mockResolvedValue({ message: "Memories stored successfully" });
await pi._invoke("mem0-remember", "I prefer dark mode", ctx);
expect(pi.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({
customType: "mem0-remember",
content: expect.stringContaining("I prefer dark mode"),
display: true,
}),
);
});
it("lists memory objects returned by the API when present", async () => {
const ctx = makeCtx();
mem0.add.mockResolvedValue([{ id: "m1", memory: "Uses dark mode", event: "ADD" }]);
await pi._invoke("mem0-remember", "I prefer dark mode", ctx);
expect(pi.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({
customType: "mem0-remember",
content: expect.stringContaining("Uses dark mode"),
display: true,
}),
);
});
it("shows warning when no text provided", async () => {
const ctx = makeCtx();
await pi._invoke("mem0-remember", " ", ctx);
expect(ctx.ui.notify).toHaveBeenCalledWith("Usage: /mem0-remember <text>", "warning");
});
});
describe("/mem0-scope", () => {
it("sends a visible message showing the current scope when no arg is given", async () => {
const ctx = makeCtx();
await pi._invoke("mem0-scope", "", ctx);
expect(pi.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({
customType: "mem0-scope",
content: expect.stringContaining("Current scope:"),
display: true,
}),
);
});
it("sends a visible confirmation after changing scope", async () => {
const ctx = makeCtx();
await pi._invoke("mem0-scope", "global", ctx);
expect(pi.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({
customType: "mem0-scope",
content: expect.stringContaining("Scope changed to global"),
display: true,
}),
);
});
it("warns on an invalid scope", async () => {
const ctx = makeCtx();
await pi._invoke("mem0-scope", "bogus", ctx);
expect(ctx.ui.notify).toHaveBeenCalledWith(
expect.stringContaining('Invalid scope "bogus"'),
"warning",
);
});
});
describe("/mem0-tour", () => {
it("shows an empty-state message when there are no memories", async () => {
const ctx = makeCtx();
mem0.getAll.mockResolvedValue({ results: [] });
await pi._invoke("mem0-tour", "", ctx);
expect(pi.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({
customType: "mem0-tour",
content: expect.stringContaining("No memories"),
display: true,
}),
);
});
it("groups memories by category with a count header", async () => {
const ctx = makeCtx();
mem0.getAll.mockResolvedValue({
results: [
{ id: "id-1", memory: "likes tea", categories: ["preferences"] },
{ id: "id-2", memory: "uses vim", categories: ["technical"] },
],
});
await pi._invoke("mem0-tour", "", ctx);
expect(pi.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({
customType: "mem0-tour",
content: expect.stringContaining("Memory tour"),
display: true,
}),
);
});
});
describe("/mem0-dream", () => {
it("feeds the protocol to the agent and shows a clean status line", async () => {
const ctx = makeCtx();
await pi._invoke("mem0-dream", "", ctx);
expect(pi.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({ customType: "mem0-dream", display: false }),
expect.objectContaining({ triggerTurn: true }),
);
expect(pi.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({
customType: "mem0-dream",
content: expect.stringContaining("Dreaming"),
display: true,
}),
);
});
});
});
+117 -69
View File
@@ -9,6 +9,8 @@ import { acquireDreamLock } from "./dream/index.ts";
import { CONFIG_DIR } from "./config/index.ts";
import { captureCommandEvent } from "./telemetry.ts";
const SEARCH_TOP_K = 10;
export function registerCommands(
pi: ExtensionAPI,
mem0: MemoryClient,
@@ -16,7 +18,24 @@ export function registerCommands(
getScopeCtx: () => ScopeContext,
telemetryCtx?: { apiKey?: string },
): void {
// ── /mem0-remember ──────────────────────────────────────────────────
const sendFeedback = (customType: string, content: string): void => {
pi.sendMessage({ customType, content, display: true });
};
const pluralize = (n: number, one: string, many: string): string =>
`${n} ${n === 1 ? one : many}`;
const searchMemories = async (query: string, scope: Scope) => {
const filters = resolveSearchFilters(scope, getScopeCtx());
const result = await mem0.search(query, {
filters,
threshold: config.searchThreshold,
topK: SEARCH_TOP_K,
rerank: true,
});
return result.results ?? [];
};
pi.registerCommand("mem0-remember", {
description: "Store a memory verbatim (no inference)",
handler: async (args, ctx) => {
@@ -26,19 +45,24 @@ export function registerCommands(
return;
}
const scopeCtx = getScopeCtx();
const addParams = resolveAddParams(config.defaultScope, scopeCtx);
const addParams = resolveAddParams(config.defaultScope, getScopeCtx());
const result = await mem0.add(
[{ role: "user", content: text }],
{ ...addParams, customCategories: DEFAULT_CUSTOM_CATEGORIES, infer: false },
);
const msg = (result as { message?: string }).message ?? "Memory stored.";
captureCommandEvent("mem0-remember", {}, telemetryCtx);
ctx.ui.notify(msg, "info");
const storedItems = (Array.isArray(result) ? result : [])
.map((m) => (m as { memory?: string }).memory)
.filter((m): m is string => Boolean(m));
const items = storedItems.length > 0 ? storedItems : [text];
sendFeedback(
"mem0-remember",
[`**Stored to ${config.defaultScope} memory**`, ...items.map((m) => `- ${m}`)].join("\n"),
);
},
});
// ── /mem0-forget ────────────────────────────────────────────────────
pi.registerCommand("mem0-forget", {
description: "Delete memories matching a natural language query",
handler: async (args, ctx) => {
@@ -48,49 +72,51 @@ export function registerCommands(
return;
}
const scopeCtx = getScopeCtx();
const filters = resolveSearchFilters(config.defaultScope, scopeCtx);
const result = await mem0.search(query, { filters });
const memories = result.results ?? [];
const memories = await searchMemories(query, config.defaultScope);
if (memories.length === 0) {
captureCommandEvent("mem0-forget", { result_count: 0 }, telemetryCtx);
ctx.ui.notify("No matching memories found.", "info");
sendFeedback("mem0-forget", `**No matches for "${query}"** — nothing to forget.`);
return;
}
const forgotten = (mem: Parameters<typeof formatMemoryCompact>[0]) => {
captureCommandEvent("mem0-forget", { deleted_count: 1 }, telemetryCtx);
sendFeedback(
"mem0-forget",
[`**Forgotten from ${config.defaultScope} memory**`, `- ${formatMemoryCompact(mem)}`].join("\n"),
);
};
if (memories.length === 1) {
const target = memories[0];
const confirmed = await ctx.ui.confirm(
"Delete this memory?",
formatMemoryCompact(target),
);
const confirmed = await ctx.ui.confirm("Delete this memory?", formatMemoryCompact(target));
if (!confirmed) {
ctx.ui.notify("Cancelled.", "info");
sendFeedback("mem0-forget", "**Cancelled** — no memories deleted.");
return;
}
await mem0.delete(target.id);
captureCommandEvent("mem0-forget", { deleted_count: 1 }, telemetryCtx);
ctx.ui.notify(`Deleted: ${formatMemoryCompact(target)}`, "info");
forgotten(target);
return;
}
const labels = memories.map((m) => formatMemoryCompact(m));
const selected = await ctx.ui.select("Which memory should I delete?", labels);
const selected = await ctx.ui.select(
`Found ${pluralize(memories.length, "match", "matches")} for "${query}" — which should I delete?`,
labels,
);
if (!selected) {
ctx.ui.notify("Cancelled.", "info");
sendFeedback("mem0-forget", "**Cancelled** — no memories deleted.");
return;
}
const idx = labels.indexOf(selected);
if (idx < 0) return;
const target = memories[idx];
await mem0.delete(target.id);
captureCommandEvent("mem0-forget", { deleted_count: 1 }, telemetryCtx);
ctx.ui.notify(`Deleted: ${formatMemoryCompact(target)}`, "info");
forgotten(target);
},
});
// ── /mem0-search ────────────────────────────────────────────────────
pi.registerCommand("mem0-search", {
description: "Semantic search across memories",
handler: async (args, ctx) => {
@@ -100,21 +126,25 @@ export function registerCommands(
return;
}
const scopeCtx = getScopeCtx();
const filters = resolveSearchFilters(config.defaultScope, scopeCtx);
const result = await mem0.search(query, { filters });
const memories = result.results ?? [];
const memories = await searchMemories(query, config.defaultScope);
captureCommandEvent("mem0-search", { result_count: memories.length }, telemetryCtx);
pi.sendMessage({
customType: "mem0-search",
content: formatMemoryList(memories),
display: true,
});
if (memories.length === 0) {
sendFeedback("mem0-search", `**No matches for "${query}"** · ${config.defaultScope} scope`);
return;
}
sendFeedback(
"mem0-search",
[
`**${pluralize(memories.length, "match", "matches")} for "${query}"** · ${config.defaultScope} scope`,
"",
formatMemoryList(memories),
].join("\n"),
);
},
});
// ── /mem0-tour ──────────────────────────────────────────────────────
pi.registerCommand("mem0-tour", {
description: "Browse all memories grouped by category",
handler: async (args, ctx) => {
@@ -125,19 +155,21 @@ export function registerCommands(
return;
}
const scope: Scope = (raw as Scope) || config.defaultScope;
const scopeCtx = getScopeCtx();
const filters = resolveSearchFilters(scope, scopeCtx);
const filters = resolveSearchFilters(scope, getScopeCtx());
const result = await mem0.getAll({ filters });
const memories = result.results ?? [];
if (memories.length === 0) {
captureCommandEvent("mem0-tour", { memory_count: 0, scope }, telemetryCtx);
pi.sendMessage({ customType: "mem0-tour", content: "No memories found.", display: true });
sendFeedback("mem0-tour", `**No memories in ${scope} scope yet** — store one with \`/mem0-remember\`.`);
return;
}
const groups = groupByCategory(memories);
const lines: string[] = [`**Memory Tour** (${memories.length} total, scope: ${scope})`, ""];
const lines: string[] = [
`**Memory tour** · ${pluralize(memories.length, "memory", "memories")} · ${scope} scope`,
"",
];
for (const [category, items] of groups) {
lines.push(`### ${category} (${items.length})`);
@@ -148,11 +180,10 @@ export function registerCommands(
}
captureCommandEvent("mem0-tour", { memory_count: memories.length, scope }, telemetryCtx);
pi.sendMessage({ customType: "mem0-tour", content: lines.join("\n"), display: true });
sendFeedback("mem0-tour", lines.join("\n"));
},
});
// ── /mem0-dream ─────────────────────────────────────────────────────
pi.registerCommand("mem0-dream", {
description: "Consolidate memories — merge duplicates, prune stale entries, resolve contradictions",
handler: async (_args, ctx) => {
@@ -162,12 +193,14 @@ export function registerCommands(
}
captureCommandEvent("mem0-dream", {}, telemetryCtx);
pi.sendMessage({ customType: "mem0-dream", content: DREAM_PROTOCOL, display: true }, { triggerTurn: true });
ctx.ui.notify("Dream consolidation started.", "info");
pi.sendMessage({ customType: "mem0-dream", content: DREAM_PROTOCOL, display: false }, { triggerTurn: true });
sendFeedback(
"mem0-dream",
"**Dreaming** — reviewing your memories to merge duplicates, resolve contradictions, and prune stale entries. I'll report what changed.",
);
},
});
// ── /mem0-pin ───────────────────────────────────────────────────────
pi.registerCommand("mem0-pin", {
description: "Pin a memory to protect it from dream pruning",
handler: async (args, ctx) => {
@@ -177,42 +210,49 @@ export function registerCommands(
return;
}
const scopeCtx = getScopeCtx();
const filters = resolveSearchFilters(config.defaultScope, scopeCtx);
const result = await mem0.search(query, { filters });
const memories = result.results ?? [];
const memories = await searchMemories(query, config.defaultScope);
if (memories.length === 0) {
captureCommandEvent("mem0-pin", { result_count: 0 }, telemetryCtx);
ctx.ui.notify("No matching memories found to pin.", "info");
sendFeedback("mem0-pin", `**No matches for "${query}"** — nothing to pin.`);
return;
}
const pinned = (mem: Parameters<typeof formatMemoryCompact>[0]) => {
captureCommandEvent("mem0-pin", { pinned: true }, telemetryCtx);
sendFeedback(
"mem0-pin",
["**Pinned** — protected from dream pruning", `- ${formatMemoryCompact(mem)}`].join("\n"),
);
};
const alreadyPinned = (mem: Parameters<typeof formatMemoryCompact>[0]) => {
sendFeedback("mem0-pin", ["**Already pinned**", `- ${formatMemoryCompact(mem)}`].join("\n"));
};
if (memories.length === 1) {
const target = memories[0];
const text = target.memory ?? "";
if (text.startsWith("[PINNED]")) {
ctx.ui.notify("Already pinned.", "info");
alreadyPinned(target);
return;
}
const confirmed = await ctx.ui.confirm(
"Pin this memory?",
formatMemoryCompact(target),
);
const confirmed = await ctx.ui.confirm("Pin this memory?", formatMemoryCompact(target));
if (!confirmed) {
ctx.ui.notify("Cancelled.", "info");
sendFeedback("mem0-pin", "**Cancelled** — nothing was pinned.");
return;
}
await mem0.update(target.id, { text: `[PINNED] ${text}` });
captureCommandEvent("mem0-pin", { pinned: true }, telemetryCtx);
ctx.ui.notify(`Pinned: ${formatMemoryCompact(target)}`, "info");
pinned(target);
return;
}
const labels = memories.map((m) => formatMemoryCompact(m));
const selected = await ctx.ui.select("Which memory should I pin?", labels);
const selected = await ctx.ui.select(
`Found ${pluralize(memories.length, "match", "matches")} for "${query}" — which should I pin?`,
labels,
);
if (!selected) {
ctx.ui.notify("Cancelled.", "info");
sendFeedback("mem0-pin", "**Cancelled** — nothing was pinned.");
return;
}
const idx = labels.indexOf(selected);
@@ -220,16 +260,14 @@ export function registerCommands(
const target = memories[idx];
const selectedText = target.memory ?? "";
if (selectedText.startsWith("[PINNED]")) {
ctx.ui.notify("Already pinned.", "info");
alreadyPinned(target);
return;
}
await mem0.update(target.id, { text: `[PINNED] ${selectedText}` });
captureCommandEvent("mem0-pin", { pinned: true }, telemetryCtx);
ctx.ui.notify(`Pinned: ${formatMemoryCompact(target)}`, "info");
pinned(target);
},
});
// ── /mem0-scope ─────────────────────────────────────────────────────
pi.registerCommand("mem0-scope", {
description: "Change default memory scope for this session (project, session, global)",
handler: async (args, ctx) => {
@@ -237,7 +275,13 @@ export function registerCommands(
const valid: Scope[] = ["project", "session", "global"];
if (!scope) {
ctx.ui.notify(`Current scope: ${config.defaultScope}. Usage: /mem0-scope <${valid.join("|")}>`, "info");
sendFeedback(
"mem0-scope",
[
`**Current scope: ${config.defaultScope}**`,
`New memories save to the **${config.defaultScope}** pool. Switch with \`/mem0-scope <${valid.join(" | ")}>\`.`,
].join("\n"),
);
return;
}
@@ -248,11 +292,16 @@ export function registerCommands(
config.defaultScope = scope as Scope;
captureCommandEvent("mem0-scope", { scope }, telemetryCtx);
ctx.ui.notify(`Default scope changed to "${scope}" for this session.`, "info");
sendFeedback(
"mem0-scope",
[
`**Scope changed to ${scope}**`,
`New memories now save to the **${scope}** pool for this session.`,
].join("\n"),
);
},
});
// ── /mem0-status ────────────────────────────────────────────────────
pi.registerCommand("mem0-status", {
description: "Show connection health, identity, project, and memory count",
handler: async (_args, _ctx) => {
@@ -270,22 +319,21 @@ export function registerCommands(
}
const lines = [
"**Mem0 Status**",
"**Mem0 status**",
"",
`- Connection: ${connected ? "connected" : "disconnected"}`,
`- User: ${scopeCtx.userId}`,
`- Project: ${scopeCtx.appId}`,
`- Session: ${scopeCtx.runId}`,
`- Default scope: ${config.defaultScope}`,
`- Search relevance threshold: ${config.searchThreshold}`,
`- Project memories: ${count}`,
`- Auto-capture: ${config.autoCapture ? "on" : "off"}`,
`- Dream: ${config.dream.enabled ? "enabled" : "disabled"}`,
];
captureCommandEvent("mem0-status", { connected, memory_count: count }, telemetryCtx);
pi.sendMessage({ customType: "mem0-status", content: lines.join("\n"), display: true });
sendFeedback("mem0-status", lines.join("\n"));
},
});
}
@@ -21,6 +21,7 @@ const DEFAULT_CONFIG: Mem0Config = {
autoCapture: true,
defaultScope: "project",
contextInjection: false,
searchThreshold: 0.3,
dream: DEFAULT_DREAM,
};
@@ -14,6 +14,7 @@ export interface Mem0Config {
autoCapture: boolean;
defaultScope: Scope;
contextInjection: boolean;
searchThreshold: number;
dream: DreamConfig;
}