feat(mem0-plugin): auto coding categories, global search, OpenCode parity (#5300)

This commit is contained in:
Kartik
2026-05-30 00:18:46 +05:30
committed by GitHub
parent 9328c36a46
commit 1019f0e17c
19 changed files with 748 additions and 138 deletions
+138 -60
View File
@@ -6,6 +6,9 @@ import { userInfo } from "os";
import { basename, resolve, dirname } from "path";
import { randomBytes } from "crypto";
import { existsSync, readdirSync, cpSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { homedir } from "os";
import { join } from "path";
import { createHash } from "crypto";
async function getUserId(): Promise<string> {
if (process.env.MEM0_USER_ID) return process.env.MEM0_USER_ID;
@@ -63,6 +66,68 @@ function redact(text: string): string {
return out;
}
function loadGlobalSearch(): boolean {
try {
const settingsPath = join(homedir(), ".mem0", "settings.json");
if (!existsSync(settingsPath)) return false;
const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
return settings.global_search === true;
} catch {}
return false;
}
const CODING_CATEGORIES = [
"architecture_decisions", "api_design", "data_models", "algorithms",
"dependencies", "environment_setup", "testing_strategy", "debugging_notes",
"performance", "security", "deployment", "code_conventions",
"error_handling", "refactoring_history", "integrations", "onboarding",
"project_meta",
];
function categoriesFingerprint(): string {
const sorted = [...CODING_CATEGORIES].sort();
return createHash("sha256").update(sorted.join("\n")).digest("hex").slice(0, 16);
}
function apiKeyFingerprint(apiKey: string): string {
return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
}
async function autoSetupCategories(mem0: MemoryClient, apiKey: string): Promise<void> {
const stateDir = join(homedir(), ".mem0");
const stateFile = join(stateDir, "categories_setup.json");
const keyFp = apiKeyFingerprint(apiKey);
const catFp = categoriesFingerprint();
let state: Record<string, string> = {};
try {
if (existsSync(stateFile)) {
state = JSON.parse(readFileSync(stateFile, "utf8"));
}
} catch {}
if (state[keyFp] === catFp) return;
try {
const project = await mem0.getProject({ fields: ["customCategories"] });
const existing: string[] = (project as any)?.custom_categories ?? (project as any)?.customCategories ?? [];
const sortedExisting = [...existing].sort();
const sortedTarget = [...CODING_CATEGORIES].sort();
if (JSON.stringify(sortedExisting) === JSON.stringify(sortedTarget)) {
state[keyFp] = catFp;
mkdirSync(stateDir, { recursive: true });
writeFileSync(stateFile, JSON.stringify(state, null, 2) + "\n");
return;
}
await mem0.updateProject({ customCategories: CODING_CATEGORIES as any });
state[keyFp] = catFp;
mkdirSync(stateDir, { recursive: true });
writeFileSync(stateFile, JSON.stringify(state, null, 2) + "\n");
} catch {}
}
const NUDGE_RE =
/\b(remember\s+(this|that)|memorize|save\s+this|note\s+(this|that)|don'?t\s+forget|always\s+remember|never\s+forget|keep\s+(this|that)\s+in\s+(mind|memory)|store\s+(this|that))\b/i;
@@ -173,6 +238,7 @@ const Mem0Plugin: Plugin = async (ctx) => {
const branch = await getBranch($);
const stats = { adds: 0, searches: 0, messages: 0 };
const sessionId = generateSessionId();
const globalSearch = loadGlobalSearch();
let initialized = false;
let memoryCount = 0;
@@ -180,6 +246,9 @@ const Mem0Plugin: Plugin = async (ctx) => {
const systemContext: string[] = [];
// Auto-configure coding categories in background (idempotent, never blocks)
Promise.resolve().then(() => autoSetupCategories(mem0, apiKey)).catch(() => {});
return {
"chat.message": async (input: any, output: any) => {
const userText = extractUserText(input, output);
@@ -192,11 +261,13 @@ const Mem0Plugin: Plugin = async (ctx) => {
if (!initialized) {
initialized = true;
const searchFilters = globalSearch
? { OR: [{ user_id: "*" }] }
: { AND: [{ user_id: userId }, { app_id: appId }] };
try {
const all = await mem0.getAll({
filters: {
AND: [{ user_id: userId }, { app_id: appId }],
},
filters: searchFilters,
page: 1,
pageSize: 1,
});
@@ -205,9 +276,15 @@ const Mem0Plugin: Plugin = async (ctx) => {
(all as any)?.results?.length ??
0;
systemContext.push(
`Always include user_id="${userId}" and app_id="${appId}" in every search_memories filter and add_memory call.`,
);
if (globalSearch) {
systemContext.push(
`Global search is ON — searches return all memories across all users and projects. Writes still use user_id="${userId}", app_id="${appId}".`,
);
} else {
systemContext.push(
`Always include user_id="${userId}" and app_id="${appId}" in every search_memories filter and add_memory call.`,
);
}
if (memoryCount === 0) {
systemContext.push(
@@ -223,9 +300,7 @@ const Mem0Plugin: Plugin = async (ctx) => {
const res = await mem0.search(
"recent session state decisions and learnings",
{
filters: {
AND: [{ user_id: userId }, { app_id: appId }],
},
filters: searchFilters,
topK: 5,
},
);
@@ -265,25 +340,21 @@ const Mem0Plugin: Plugin = async (ctx) => {
const hasResume = RESUME_RE.test(safeText);
if (hasResume) {
try {
const [stateRes, decisionsRes] = await Promise.all([
mem0.search("session state current task", {
filters: {
const resumeFilters = globalSearch
? { OR: [{ user_id: "*" }] }
: {
AND: [
{ user_id: userId },
{ app_id: appId },
{ metadata: { type: "session_state" } },
],
},
};
const [stateRes, decisionsRes] = await Promise.all([
mem0.search("session state current task", {
filters: resumeFilters,
topK: 3,
}),
mem0.search("recent decisions and learnings", {
filters: {
AND: [
{ user_id: userId },
{ app_id: appId },
{ metadata: { type: "decision" } },
],
},
filters: resumeFilters,
topK: 3,
}),
]);
@@ -309,8 +380,11 @@ const Mem0Plugin: Plugin = async (ctx) => {
if (!hasResume && memoryCount > 0) {
try {
const msgFilters = globalSearch
? { OR: [{ user_id: "*" }] }
: { AND: [{ user_id: userId }, { app_id: appId }] };
const res = await mem0.search(safeText, {
filters: { AND: [{ user_id: userId }, { app_id: appId }] },
filters: msgFilters,
topK: 5,
});
stats.searches++;
@@ -401,32 +475,36 @@ const Mem0Plugin: Plugin = async (ctx) => {
}
if (isMem0SearchOrGet(toolName)) {
const existingFilters = output.args.filters;
if (existingFilters === undefined || existingFilters === null) {
output.args.filters = {
AND: [{ user_id: userId }, { app_id: appId }],
};
} else if (typeof existingFilters === "object") {
const andClauses: any[] = existingFilters.AND;
if (Array.isArray(andClauses)) {
const hasUid = andClauses.some(
(c: any) => c && typeof c === "object" && "user_id" in c,
);
const hasAid = andClauses.some(
(c: any) => c && typeof c === "object" && "app_id" in c,
);
if (!hasUid) andClauses.push({ user_id: userId });
if (!hasAid) andClauses.push({ app_id: appId });
} else if (andClauses === undefined) {
const hasUid = "user_id" in existingFilters;
const hasAid = "app_id" in existingFilters;
if (!hasUid || !hasAid) {
const existing = Object.entries(existingFilters).map(
([k, v]) => ({ [k]: v }),
if (globalSearch) {
output.args.filters = { OR: [{ user_id: "*" }] };
} else {
const existingFilters = output.args.filters;
if (existingFilters === undefined || existingFilters === null) {
output.args.filters = {
AND: [{ user_id: userId }, { app_id: appId }],
};
} else if (typeof existingFilters === "object") {
const andClauses: any[] = existingFilters.AND;
if (Array.isArray(andClauses)) {
const hasUid = andClauses.some(
(c: any) => c && typeof c === "object" && "user_id" in c,
);
if (!hasUid) existing.push({ user_id: userId });
if (!hasAid) existing.push({ app_id: appId });
output.args.filters = { AND: existing };
const hasAid = andClauses.some(
(c: any) => c && typeof c === "object" && "app_id" in c,
);
if (!hasUid) andClauses.push({ user_id: userId });
if (!hasAid) andClauses.push({ app_id: appId });
} else if (andClauses === undefined) {
const hasUid = "user_id" in existingFilters;
const hasAid = "app_id" in existingFilters;
if (!hasUid || !hasAid) {
const existing = Object.entries(existingFilters).map(
([k, v]) => ({ [k]: v }),
);
if (!hasUid) existing.push({ user_id: userId });
if (!hasAid) existing.push({ app_id: appId });
output.args.filters = { AND: existing };
}
}
}
}
@@ -481,25 +559,21 @@ const Mem0Plugin: Plugin = async (ctx) => {
const errorQuery = errorLine.slice(0, 80);
if (errorQuery.length < 10) return;
const [antiPatternRes, bugFixRes] = await Promise.all([
mem0.search(`error: ${errorQuery}`, {
filters: {
const errorFilters = globalSearch
? { OR: [{ user_id: "*" }] }
: {
AND: [
{ user_id: userId },
{ app_id: appId },
{ metadata: { type: "anti_pattern" } },
],
},
};
const [antiPatternRes, bugFixRes] = await Promise.all([
mem0.search(`error: ${errorQuery}`, {
filters: errorFilters,
topK: 3,
}),
mem0.search(`error: ${errorQuery}`, {
filters: {
AND: [
{ user_id: userId },
{ app_id: appId },
{ metadata: { type: "bug_fix" } },
],
},
filters: errorFilters,
topK: 3,
}),
]);
@@ -554,8 +628,11 @@ const Mem0Plugin: Plugin = async (ctx) => {
} catch {}
});
const compactFilters = globalSearch
? { OR: [{ user_id: "*" }] }
: { AND: [{ user_id: userId }, { app_id: appId }] };
const res = await mem0.search("session state decisions learnings", {
filters: { AND: [{ user_id: userId }, { app_id: appId }] },
filters: compactFilters,
topK: 10,
});
const memories = extractMemories(res);
@@ -577,6 +654,7 @@ const Mem0Plugin: Plugin = async (ctx) => {
output.env.MEM0_APP_ID = appId;
output.env.MEM0_SESSION_ID = sessionId;
output.env.MEM0_BRANCH = branch;
output.env.MEM0_GLOBAL_SEARCH = globalSearch ? "true" : "false";
}
},
};
@@ -128,58 +128,38 @@ If `add_memory` calls fail, print:
- Project file import failed. Check API key and MCP connection, then retry with: /mem0:onboard
```
## Step 5: Set up coding categories
## Step 5: Verify coding categories
Ask: "Install coding categories optimized for development workflows? [Y/n]"
Coding categories are now configured automatically in the background when the plugin starts. This step only verifies they are set up.
If the user says no or skips, print `- Coding categories skipped.` and proceed to Step 6.
Check if the categories are already configured by searching for a project_profile memory:
If yes, store a project profile memory that records the project and its active coding categories. Call `add_memory` once with:
Call `search_memories` with `query="coding categories project profile"`, `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`, `top_k=1`.
- `data`: a plain-text description of the project profile and the list of active coding categories (see below)
If a project_profile memory is found, print:
```
- Coding categories already configured (17 categories, auto-installed).
```
If no project_profile memory is found, store one as a fallback. Call `add_memory` once with:
- `data`: a plain-text description of the project profile and the list of active coding categories:
```
Project profile for <project_id>.
Active coding categories: architecture_decisions, api_design, data_models,
algorithms, dependencies, environment_setup, testing_strategy, debugging_notes,
performance, security, deployment, code_conventions, error_handling,
refactoring_history, integrations, onboarding, project_meta.
```
- `user_id`: the active user id
- `app_id`: the active project id
- `metadata`: `{"type": "project_profile", "source": "onboard"}`
The standard set of 17 coding categories to include in the `data` field:
```
architecture_decisions - High-level design choices and system structure
api_design - Interface contracts, REST/GraphQL/RPC conventions
data_models - Schemas, entity definitions, relationships
algorithms - Non-trivial logic, performance-sensitive routines
dependencies - Libraries, versions, upgrade notes
environment_setup - Dev environment, tooling, build system
testing_strategy - Test patterns, coverage targets, mocking approach
debugging_notes - Known issues, workarounds, gotchas
performance - Bottlenecks, profiling results, optimizations
security - Auth patterns, secret handling, threat notes
deployment - CI/CD pipelines, infra config, release process
code_conventions - Naming, formatting, style rules beyond the linter
error_handling - Error taxonomy, recovery patterns, logging approach
refactoring_history - Past rewrites, why changes were made
integrations - Third-party services, webhooks, external APIs
onboarding - New-contributor notes, repo orientation
project_meta - Goals, non-goals, stakeholder context
```
Example `data` value:
```
Project profile for <project_id>.
Active coding categories: architecture_decisions, api_design, data_models,
algorithms, dependencies, environment_setup, testing_strategy, debugging_notes,
performance, security, deployment, code_conventions, error_handling,
refactoring_history, integrations, onboarding, project_meta.
```
After the `add_memory` call succeeds, print:
```
- Coding categories installed (17 categories).
```
If `add_memory` fails, print the error and suggest re-running `/mem0:onboard`.
## Step 6: Summary
Print a summary:
@@ -1,18 +1,75 @@
---
name: switch-project
description: Overrides the auto-detected project scope to read and write memories under a different project ID. Use when working across multiple projects, accessing memories from another repo, or when auto-detection resolves to the wrong project.
description: Overrides the auto-detected project scope to read and write memories under a different project ID, or enables global search to access all memories across all users and projects. Use when working across multiple projects, accessing memories from another repo, enabling team-wide memory access, or when auto-detection resolves to the wrong project.
---
# Mem0 Switch Project
Override the automatic project_id detection for the current directory.
Override the automatic project_id detection for the current directory, or enable global search mode.
## Usage
The user provides a project name as an argument: `/mem0:switch-project <project-name>`
- `/mem0:switch-project <project-name>` — switch to a specific project scope
- `/mem0:switch-project --global` — enable global search (all memories, all users, all projects)
- `/mem0:switch-project --no-global` — disable global search and return to per-project scoping
## Execution
### If `--global` flag is provided:
1. Set `global_search: true` in `~/.mem0/settings.json` using the Bash tool:
```bash
python3 -c "
import json, os
settings_file = os.path.expanduser('~/.mem0/settings.json')
settings = {}
if os.path.isfile(settings_file):
with open(settings_file) as f:
settings = json.load(f)
settings['global_search'] = True
with open(settings_file, 'w') as f:
json.dump(settings, f, indent=2)
print('Global search enabled')
"
```
2. Print:
```
Global search enabled.
Searches now return all memories across all users and projects.
Writes still use the current user_id and app_id.
Restart the session for the change to take effect.
```
### If `--no-global` flag is provided:
1. Set `global_search: false` in `~/.mem0/settings.json` using the Bash tool:
```bash
python3 -c "
import json, os
settings_file = os.path.expanduser('~/.mem0/settings.json')
settings = {}
if os.path.isfile(settings_file):
with open(settings_file) as f:
settings = json.load(f)
settings['global_search'] = False
with open(settings_file, 'w') as f:
json.dump(settings, f, indent=2)
print('Global search disabled')
"
```
2. Print:
```
Global search disabled.
Searches now return only memories scoped to the current project.
Restart the session for the change to take effect.
```
### If a project name is provided (no flags):
1. If no project name was given, ask: "What project_id should this directory use?"
2. Write the mapping to `~/.mem0/project_map.json` using the Bash tool:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/opencode-plugin",
"version": "0.1.1",
"version": "0.1.2",
"type": "module",
"description": "Mem0 persistent memory plugin for OpenCode — add, search, and manage memories across sessions",
"main": "dist/index.js",