Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ The `supermemory` tool is available to the agent:

**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.

## Memory Scoping

| Scope | Tag | Persists |
Expand Down
11 changes: 9 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import type { Plugin, PluginInput } from "@opencode-ai/plugin";
import type { Part } from "@opencode-ai/sdk";
import { tool } from "@opencode-ai/plugin";

import { supermemoryClient } from "./services/client.js";
import {
PROJECT_ENTITY_CONTEXT,
USER_ENTITY_CONTEXT,
supermemoryClient,
} from "./services/client.js";
import { formatContextForPrompt } from "./services/context.js";
import { getTags } from "./services/tags.js";
import { stripPrivateContent, isFullyPrivate } from "./services/privacy.js";
Expand Down Expand Up @@ -312,11 +316,14 @@ 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 result = await supermemoryClient.addMemory(
sanitizedContent,
containerTag,
{ type: args.type }
{ type: args.type },
{ entityContext }
);

if (!result.success) {
Expand Down
76 changes: 65 additions & 11 deletions src/services/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,32 @@ import type {

const TIMEOUT_MS = 30000;
const MAX_CONVERSATION_CHARS = 100_000;
const OPENCODE_SOURCE = "opencode";

export const USER_ENTITY_CONTEXT = `Developer coding session transcript for a persistent user profile.

EXTRACT:
- User preferences: preferred languages, frameworks, libraries, editors, workflows, and communication style
- Stable habits: testing style, code review expectations, formatting preferences, privacy preferences
- Repeated personal decisions: tools the user consistently chooses or avoids
- Long-lived learnings: concepts the user learned or wants remembered across projects

SKIP:
- One-off assistant suggestions the user did not accept
- Low-level implementation details that only matter inside the current repository`;

export const PROJECT_ENTITY_CONTEXT = `Project/codebase knowledge from OpenCode coding sessions.

EXTRACT:
- Architecture: repo structure, services, modules, data flow, and integration boundaries
- Conventions: naming, component patterns, API patterns, testing practices, and style rules
- Decisions: chosen approaches, tradeoffs, migrations, and rejected alternatives
- Setup: commands, environment requirements, deployment notes, and debugging workflows
- Implementation lessons: bugs fixed, root causes, and reusable project-specific context

SKIP:
- Verbatim assistant explanations unless they became an accepted project decision
- Transient command output with no lasting project value`;

function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
return Promise.race([
Expand Down Expand Up @@ -57,7 +83,7 @@ export class SupermemoryClient {
this.client = new Supermemory({
apiKey: SUPERMEMORY_API_KEY,
baseURL: getApiBaseUrl(),
defaultHeaders: { "x-sm-source": "opencode" },
defaultHeaders: { "x-sm-source": OPENCODE_SOURCE },
});
this.client.settings.update({
shouldLLMFilter: true,
Expand Down Expand Up @@ -111,25 +137,45 @@ export class SupermemoryClient {
async addMemory(
content: string,
containerTag: string,
metadata?: { type?: MemoryType; tool?: string; [key: string]: unknown }
metadata?: { type?: MemoryType; tool?: string; [key: string]: unknown },
options?: { customId?: string; entityContext?: string }
) {
log("addMemory: start", { containerTag, contentLength: content.length });
log("addMemory: start", {
containerTag,
contentLength: content.length,
customId: options?.customId,
hasEntityContext: !!options?.entityContext,
});
try {
// Always stamp `sm_source` so mono's `document.source` column attributes
// these writes to the OpenCode plugin. Caller-provided metadata wins on
// conflicts.
const mergedMetadata = {
sm_source: "opencode",
sm_source: OPENCODE_SOURCE,
sm_capture_mode: metadata?.sm_capture_mode ?? "tool",
...(metadata ?? {}),
} as unknown as Record<string, string | number | boolean | string[]>;

const payload: {
content: string;
containerTag: string;
metadata: Record<string, string | number | boolean | string[]>;
customId?: string;
entityContext?: string;
} = {
content,
containerTag,
metadata: mergedMetadata,
};
if (options?.customId) {
payload.customId = options.customId;
}
if (options?.entityContext) {
payload.entityContext = options.entityContext;
}

const result = await withTimeout(
this.getClient().memories.add({
content,
containerTag,
metadata: mergedMetadata,
}),
this.getClient().memories.add(payload),
TIMEOUT_MS
);
log("addMemory: success", { id: result.id });
Expand Down Expand Up @@ -183,7 +229,11 @@ export class SupermemoryClient {
conversationId: string,
messages: ConversationMessage[],
containerTags: string[],
metadata?: Record<string, string | number | boolean>
metadata?: Record<string, string | number | boolean>,
options?: {
defaultEntityContext?: string;
entityContextByContainerTag?: Record<string, string>;
}
) {
log("ingestConversation: start", {
conversationId,
Expand Down Expand Up @@ -219,7 +269,11 @@ export class SupermemoryClient {
let firstError: string | null = null;

for (const tag of uniqueTags) {
const result = await this.addMemory(content, tag, ingestMetadata);
const entityContext =
options?.entityContextByContainerTag?.[tag] ?? options?.defaultEntityContext;
const result = await this.addMemory(content, tag, ingestMetadata, {
...(entityContext ? { entityContext } : {}),
});
if (result.success) {
savedIds.push(result.id);
} else if (!firstError) {
Expand Down
5 changes: 3 additions & 2 deletions src/services/compaction.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { supermemoryClient } from "./client.js";
import { PROJECT_ENTITY_CONTEXT, supermemoryClient } from "./client.js";
import { log } from "./logger.js";
import { CONFIG } from "../config.js";

Expand Down Expand Up @@ -301,7 +301,8 @@ export function createCompactionHook(
const result = await supermemoryClient.addMemory(
`[Session Summary]\n${summaryContent}`,
tags.project,
{ type: "conversation" }
{ type: "conversation" },
{ entityContext: PROJECT_ENTITY_CONTEXT }
);

if (result.success) {
Expand Down
Loading