Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 28 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Run `/supermemory-init` to have the agent explore and memorize the codebase.

On first message, the agent receives (invisible to user):

- User profile (cross-project preferences)
- Personal profile for the current project
- Project memories (all project knowledge)
- Relevant user memories (semantic search)

Expand Down Expand Up @@ -193,19 +193,28 @@ The `supermemory` tool is available to the agent:
| `list` | `scope?`, `limit?` | List memories |
| `forget` | `memoryId`, `scope?` | Delete memory |

**Scopes:** `user` (cross-project), `project` (default)
**Scopes:** `user` (personal memories for the current project), `project` (default)

**Types:** `project-config`, `architecture`, `error-solution`, `preference`, `learned-pattern`, `conversation`

OpenCode sends entity context when saving memories so Supermemory can extract
different facts for user profile memories versus project/codebase knowledge.
OpenCode sends the same shared coding-agent entity context as Claude Code and
Codex. Personal and project memories are distinguished with `sm_scope`
metadata inside the shared repository container.

## Memory Scoping

| Scope | Tag | Persists |
| ------- | -------------------------------------- | ------------ |
| User | `opencode_user_{sha256(git email)}` | All projects |
| Project | `opencode_project_{sha256(directory)}` | This project |
| Scope | Tag | Metadata |
| ------- | ------------------------------------------- | ----------------------- |
| User | `repo_{project-name}__{repository-hash}` | `sm_scope: "personal"` |
| Project | `repo_{project-name}__{repository-hash}` | `sm_scope: "project"` |

The repository hash comes from the normalized Git `origin` remote, so Claude
Code, Codex, and OpenCode use the same container for the same repository.
Repositories with the same name but different remotes remain isolated. Without
an origin remote, OpenCode falls back to the repository's real filesystem path.
OpenCode also reads previous `user_project_*`, `repo_<project-name>`,
`claudecode_project_*`, `codex_user_*`, `codex_project_*`, `opencode_user_*`,
and `opencode_project_*` containers, so upgrading does not require a migration.

## Configuration

Expand Down Expand Up @@ -234,10 +243,10 @@ Create `~/.config/opencode/supermemory.jsonc`:
// Include user profile in context
"injectProfile": true,

// Prefix for container tags (used when userContainerTag/projectContainerTag not set)
// Legacy prefix retained when reading containers made by older versions
"containerTagPrefix": "opencode",

// Optional: Set exact user container tag (overrides auto-generated tag)
// Optional legacy personal container to keep reading
"userContainerTag": "my-custom-user-tag",

// Optional: Set exact project container tag (overrides auto-generated tag)
Expand All @@ -255,26 +264,28 @@ All fields optional. Env var `SUPERMEMORY_API_KEY` takes precedence over config

### Container Tag Selection

By default, container tags are auto-generated using `containerTagPrefix` plus a hash:
By default, new writes use:

- User tag: `{prefix}_user_{hash(git_email)}`
- Project tag: `{prefix}_project_{hash(directory)}`
- Repository tag: `repo_{project-name}__{hash(normalized-origin-remote)}`
- No origin remote: `repo_{project-name}__{hash(real-repository-path)}`

You can override this by specifying exact container tags:
Older `{prefix}_user_*` and `{prefix}_project_*` containers remain readable.
`userContainerTag` is treated as a legacy personal read. You can still override
the unified write container with `projectContainerTag`:

```jsonc
{
// Use a specific container tag for user memories
// Continue reading a personal container made by an older version
"userContainerTag": "my-team-workspace",

// Use a specific container tag for project memories
// Override the unified container used for new writes
"projectContainerTag": "my-awesome-project",
}
```

This is useful when you want to:

- Share memories across team members (same `userContainerTag`)
- Preserve a legacy personal memory container
- Sync memories between different machines for the same project
- Organize memories using your own naming scheme
- Integrate with existing Supermemory container tags from other tools
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "opencode-supermemory",
"version": "2.0.8",
"version": "2.0.9",
"description": "OpenCode plugin that gives coding agents persistent memory using Supermemory",
"type": "module",
"main": "dist/index.js",
Expand Down
15 changes: 10 additions & 5 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ How the codebase works and why:
- Known issues and their solutions

**User-scoped** (\`scope: "user"\`):
- Personal coding preferences across all projects
- Personal coding preferences relevant to this project
- Communication style preferences
- General workflow habits

Expand Down Expand Up @@ -598,11 +598,12 @@ async function status(): Promise<number> {
lines.push(`Connected: ${isConfigured() ? "checking..." : "no"}`);
lines.push(`API key: ${maskKey(SUPERMEMORY_API_KEY)} (${getKeySource()})`);
lines.push(`API URL: ${apiUrl}`);
lines.push("Memory scope: current project + user profile");
lines.push("Memory scope: unified project container with personal/project metadata");
lines.push(`Recall mode: ${CONFIG.autoRecallEveryPrompt ? "auto-recall on every prompt" : "session/event based"}`);
lines.push(`Capture cadence: ${CONFIG.captureEveryNTurns > 0 ? `every ${CONFIG.captureEveryNTurns} turn${CONFIG.captureEveryNTurns === 1 ? "" : "s"} + session end` : "session end only"}`);
lines.push(`Project tag: ${tags.project}`);
lines.push(`User tag: ${tags.user}`);
lines.push(`Project container: ${tags.canonical}`);
lines.push(`Personal reads: ${tags.personalReads.join(", ")}`);
lines.push(`Project reads: ${tags.projectReads.join(", ")}`);

if (!isConfigured()) {
lines.push("");
Expand All @@ -613,7 +614,11 @@ async function status(): Promise<number> {

const client = new SupermemoryClient();
const [profileResult, accountInfo] = await Promise.all([
client.getProfile(tags.user),
client.getProfileScoped(
tags.canonical,
tags.personalReads,
"personal",
),
getAccountInfo(apiUrl),
]);

Expand Down
2 changes: 1 addition & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { stripJsoncComments } from "./services/jsonc.js";
import { loadCredentials } from "./services/auth.js";

const CONFIG_DIR = join(homedir(), ".config", "opencode");
export const PLUGIN_VERSION = "2.0.8";
export const PLUGIN_VERSION = "2.0.9";
const CONFIG_FILES = [
join(CONFIG_DIR, "supermemory.jsonc"),
join(CONFIG_DIR, "supermemory.json"),
Expand Down
134 changes: 73 additions & 61 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import type { Plugin, PluginInput } from "@opencode-ai/plugin";
import type { Part } from "@opencode-ai/sdk";
import { tool } from "@opencode-ai/plugin";

import {
PROJECT_ENTITY_CONTEXT,
USER_ENTITY_CONTEXT,
} from "./services/entity-context.js";
import { AGENT_ENTITY_CONTEXT } from "./services/entity-context.js";
import { supermemoryClient } from "./services/client.js";
import { formatContextForPrompt } from "./services/context.js";
import { getTags } from "./services/tags.js";
Expand All @@ -27,7 +24,7 @@ The user wants you to remember something. You MUST use the \`supermemory\` tool

Extract the key information the user wants remembered and save it as a concise, searchable memory.
- Use \`scope: "project"\` for project-specific preferences (e.g., "run lint with tests")
- Use \`scope: "user"\` for cross-project preferences (e.g., "prefers concise responses")
- Use \`scope: "user"\` for personal preferences in this project (e.g., "prefers concise responses")
- Choose an appropriate \`type\`: "preference", "project-config", "learned-pattern", etc.

DO NOT skip this step. The user explicitly asked you to remember.`;
Expand Down Expand Up @@ -146,9 +143,24 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {

if (CONFIG.autoRecallEveryPrompt) {
const [profileResult, userMemoriesResult, projectMemoriesListResult] = await Promise.all([
supermemoryClient.getProfile(tags.user, userMessage),
supermemoryClient.searchMemories(userMessage, tags.user),
supermemoryClient.listMemories(tags.project, CONFIG.maxProjectMemories),
supermemoryClient.getProfileScoped(
tags.canonical,
tags.personalReads,
"personal",
userMessage,
),
supermemoryClient.searchMemoriesScoped(
userMessage,
tags.canonical,
tags.personalReads,
"personal",
),
supermemoryClient.listMemoriesScoped(
tags.canonical,
tags.projectReads,
"project",
CONFIG.maxProjectMemories,
),
]);

const profile = profileResult.success ? profileResult : null;
Expand All @@ -173,7 +185,11 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
projectMemories
);
} else {
const profileResult = await supermemoryClient.getProfile(tags.user);
const profileResult = await supermemoryClient.getProfileScoped(
tags.canonical,
tags.personalReads,
"personal",
);
const profile = profileResult.success ? profileResult : null;
memoryContext = formatContextForPrompt(profile, { results: [] }, { results: [] });
}
Expand Down Expand Up @@ -283,7 +299,7 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
},
],
scopes: {
user: "Cross-project preferences and knowledge",
user: "Personal preferences and knowledge for this project",
project: "Project-specific knowledge (default)",
},
types: [
Expand Down Expand Up @@ -314,16 +330,20 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
}

const scope = args.scope || "project";
const containerTag =
scope === "user" ? tags.user : tags.project;
const entityContext =
scope === "user" ? USER_ENTITY_CONTEXT : PROJECT_ENTITY_CONTEXT;
const internalScope =
scope === "user" ? "personal" : "project";

const result = await supermemoryClient.addMemory(
sanitizedContent,
containerTag,
{ type: args.type },
{ entityContext }
tags.canonical,
{
type: args.type,
project: tags.projectName,
sm_project_id: tags.projectId,
sm_scope: internalScope,
sm_capture_mode: "tool",
},
{ entityContext: AGENT_ENTITY_CONTEXT }
);

if (!result.success) {
Expand Down Expand Up @@ -353,9 +373,11 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
const scope = args.scope;

if (scope === "user") {
const result = await supermemoryClient.searchMemories(
const result = await supermemoryClient.searchMemoriesScoped(
args.query,
tags.user
tags.canonical,
tags.personalReads,
"personal",
);
if (!result.success) {
return JSON.stringify({
Expand All @@ -367,9 +389,11 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
}

if (scope === "project") {
const result = await supermemoryClient.searchMemories(
const result = await supermemoryClient.searchMemoriesScoped(
args.query,
tags.project
tags.canonical,
tags.projectReads,
"project",
);
if (!result.success) {
return JSON.stringify({
Expand All @@ -380,46 +404,30 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
return formatSearchResults(args.query, scope, result, args.limit);
}

const [userResult, projectResult] = await Promise.all([
supermemoryClient.searchMemories(args.query, tags.user),
supermemoryClient.searchMemories(args.query, tags.project),
]);

if (!userResult.success || !projectResult.success) {
const result = await supermemoryClient.searchMemoriesMany(
args.query,
tags.allReads,
);
if (!result.success) {
return JSON.stringify({
success: false,
error: userResult.error || projectResult.error || "Failed to search memories",
error: result.error || "Failed to search memories",
});
}

const combined = [
...(userResult.results || []).map((r) => ({
...r,
scope: "user" as const,
})),
...(projectResult.results || []).map((r) => ({
...r,
scope: "project" as const,
})),
].sort((a, b) => (b.similarity ?? 0) - (a.similarity ?? 0));

return JSON.stringify({
success: true,
query: args.query,
count: combined.length,
results: combined.slice(0, args.limit || 10).map((r) => ({
id: r.id,
content: r.memory || r.chunk,
similarity: Math.round((r.similarity ?? 0) * 100),
scope: r.scope,
})),
});
return formatSearchResults(
args.query,
undefined,
result,
args.limit,
);
}

case "profile": {
const result = await supermemoryClient.getProfile(
tags.user,
args.query
const result = await supermemoryClient.getProfileScoped(
tags.canonical,
tags.personalReads,
"personal",
args.query,
);

if (!result.success) {
Expand All @@ -441,12 +449,16 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
case "list": {
const scope = args.scope || "project";
const limit = args.limit || 20;
const containerTag =
scope === "user" ? tags.user : tags.project;

const result = await supermemoryClient.listMemories(
containerTag,
limit
const internalScope =
scope === "user" ? "personal" : "project";
const readTags =
scope === "user" ? tags.personalReads : tags.projectReads;

const result = await supermemoryClient.listMemoriesScoped(
tags.canonical,
readTags,
internalScope,
limit,
);

if (!result.success) {
Expand Down Expand Up @@ -524,7 +536,7 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
function formatSearchResults(
query: string,
scope: string | undefined,
results: { results?: Array<{ id: string; memory?: string; chunk?: string; similarity?: number }> },
results: { results?: Array<{ id?: string; memory?: string; chunk?: string; similarity?: number }> },
limit?: number
): string {
const memoryResults = results.results || [];
Expand Down
2 changes: 1 addition & 1 deletion src/services/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export function startAuthFlow(timeoutMs = AUTH_TIMEOUT): Promise<AuthResult> {
hostname: `opencode - ${hostname()}`,
os: `${platform()}-${arch()}`,
cwd: process.cwd(),
cli_version: "2.0.8",
cli_version: "2.0.9",
});
const authUrl = `${AUTH_BASE_URL}?${params.toString()}`;

Expand Down
Loading
Loading