Skip to content
Open
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
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.5",
"version": "2.0.6",
"description": "OpenCode plugin that gives coding agents persistent memory using Supermemory",
"type": "module",
"main": "dist/index.js",
Expand Down
3 changes: 3 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { homedir } from "node:os";
import * as readline from "node:readline";
import { stripJsoncComments } from "./services/jsonc.js";
import { startAuthFlow, clearCredentials, loadCredentials } from "./services/auth.js";
import { writeInstallDefaults, CONFIG_FILE } from "./config.js";

const OPENCODE_CONFIG_DIR = join(homedir(), ".config", "opencode");
const OPENCODE_COMMAND_DIR = join(OPENCODE_CONFIG_DIR, "command");
Expand Down Expand Up @@ -376,6 +377,8 @@ interface InstallOptions {
async function install(options: InstallOptions): Promise<number> {
console.log("\n🧠 opencode-supermemory installer\n");

writeInstallDefaults(existsSync(CONFIG_FILE));

const rl = options.tui ? createReadline() : null;

// Step 1: Register plugin in config
Expand Down
37 changes: 30 additions & 7 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { existsSync, readFileSync } from "node:fs";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { stripJsoncComments } from "./services/jsonc.js";
Expand All @@ -23,6 +23,8 @@ interface SupermemoryConfig {
filterPrompt?: string;
keywordPatterns?: string[];
compactionThreshold?: number;
autoRecallEveryPrompt?: boolean;
captureEveryNTurns?: number;
}

const DEFAULT_KEYWORD_PATTERNS = [
Expand Down Expand Up @@ -54,6 +56,8 @@ const DEFAULTS: Required<Omit<SupermemoryConfig, "apiKey" | "userContainerTag" |
filterPrompt: "You are a stateful coding agent. Remember all the information, including but not limited to user's coding preferences, tech stack, behaviours, workflows, and any other relevant details.",
keywordPatterns: [],
compactionThreshold: 0.80,
autoRecallEveryPrompt: false,
captureEveryNTurns: 0,
};

function isValidRegex(pattern: string): boolean {
Expand All @@ -73,31 +77,31 @@ function validateCompactionThreshold(value: number | undefined): number {
return value;
}

function loadConfig(): SupermemoryConfig {
function loadRawConfig(): { config: SupermemoryConfig; existed: boolean } {
for (const path of CONFIG_FILES) {
if (existsSync(path)) {
try {
const content = readFileSync(path, "utf-8");
const json = stripJsoncComments(content);
return JSON.parse(json) as SupermemoryConfig;
return { config: JSON.parse(json) as SupermemoryConfig, existed: true };
} catch {
// Invalid config, use defaults
return { config: {}, existed: true };
}
}
}
return {};
return { config: {}, existed: false };
}

const fileConfig = loadConfig();
const { config: fileConfig, existed: configExisted } = loadRawConfig();

function getApiKey(): string | undefined {
// Priority: env var > config file > OAuth credentials
if (process.env.SUPERMEMORY_API_KEY) return process.env.SUPERMEMORY_API_KEY;
if (fileConfig.apiKey) return fileConfig.apiKey;
return loadCredentials()?.apiKey;
}

export const SUPERMEMORY_API_KEY = getApiKey();
export const CONFIG_FILE = CONFIG_FILES[1];

export const CONFIG = {
similarityThreshold: fileConfig.similarityThreshold ?? DEFAULTS.similarityThreshold,
Expand All @@ -114,8 +118,27 @@ export const CONFIG = {
...(fileConfig.keywordPatterns ?? []).filter(isValidRegex),
],
compactionThreshold: validateCompactionThreshold(fileConfig.compactionThreshold),
autoRecallEveryPrompt:
fileConfig.autoRecallEveryPrompt ??
(configExisted ? true : DEFAULTS.autoRecallEveryPrompt),
captureEveryNTurns:
fileConfig.captureEveryNTurns ??
(configExisted ? 3 : DEFAULTS.captureEveryNTurns),
};

export function isConfigured(): boolean {
return !!SUPERMEMORY_API_KEY;
}

export function writeInstallDefaults(isExistingInstall: boolean): void {
const current = loadRawConfig().config;
const next: SupermemoryConfig = { ...current };
if (isExistingInstall) {
if (next.autoRecallEveryPrompt === undefined) next.autoRecallEveryPrompt = true;
if (next.captureEveryNTurns === undefined) next.captureEveryNTurns = 3;
} else {
next.autoRecallEveryPrompt = false;
next.captureEveryNTurns = 0;
}
writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2));
}
60 changes: 34 additions & 26 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,33 +127,41 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
if (isFirstMessage) {
injectedSessions.add(input.sessionID);

const [profileResult, userMemoriesResult, projectMemoriesListResult] = await Promise.all([
supermemoryClient.getProfile(tags.user, userMessage),
supermemoryClient.searchMemories(userMessage, tags.user),
supermemoryClient.listMemories(tags.project, CONFIG.maxProjectMemories),
]);

const profile = profileResult.success ? profileResult : null;
const userMemories = userMemoriesResult.success ? userMemoriesResult : { results: [] };
const projectMemoriesList = projectMemoriesListResult.success ? projectMemoriesListResult : { memories: [] };

const projectMemories = {
results: (projectMemoriesList.memories || []).map((m: any) => ({
id: m.id,
memory: m.summary || m.content || m.title || "",
similarity: 1,
title: m.title,
metadata: m.metadata,
})),
total: projectMemoriesList.memories?.length || 0,
timing: 0,
};
let memoryContext = "";

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),
]);

const profile = profileResult.success ? profileResult : null;
const userMemories = userMemoriesResult.success ? userMemoriesResult : { results: [] };
const projectMemoriesList = projectMemoriesListResult.success ? projectMemoriesListResult : { memories: [] };

const projectMemories = {
results: (projectMemoriesList.memories || []).map((m: any) => ({
id: m.id,
memory: m.summary || m.content || m.title || "",
similarity: 1,
title: m.title,
metadata: m.metadata,
})),
total: projectMemoriesList.memories?.length || 0,
timing: 0,
};

const memoryContext = formatContextForPrompt(
profile,
userMemories,
projectMemories
);
memoryContext = formatContextForPrompt(
profile,
userMemories,
projectMemories
);
} else {
const profileResult = await supermemoryClient.getProfile(tags.user);
const profile = profileResult.success ? profileResult : null;
memoryContext = formatContextForPrompt(profile, { results: [] }, { results: [] });
}

if (memoryContext) {
const contextPart: Part = {
Expand Down
18 changes: 16 additions & 2 deletions src/services/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ export class SupermemoryClient {
if (!isConfigured()) {
throw new Error("SUPERMEMORY_API_KEY not set");
}
this.client = new Supermemory({ apiKey: SUPERMEMORY_API_KEY });
// `x-sm-source` is read by mono's API to attribute searches and
// writes to the OpenCode plugin in PostHog / `document.source`.
this.client = new Supermemory({
apiKey: SUPERMEMORY_API_KEY,
defaultHeaders: { "x-sm-source": "opencode" },
});
this.client.settings.update({
shouldLLMFilter: true,
filterPrompt: CONFIG.filterPrompt
Expand Down Expand Up @@ -109,11 +114,20 @@ export class SupermemoryClient {
) {
log("addMemory: start", { containerTag, contentLength: content.length });
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_capture_mode: metadata?.sm_capture_mode ?? "tool",
...(metadata ?? {}),
} as Record<string, string | number | boolean | string[]>;

const result = await withTimeout(
this.getClient().memories.add({
content,
containerTag,
metadata: metadata as Record<string, string | number | boolean | string[]>,
metadata: mergedMetadata,
}),
TIMEOUT_MS
);
Expand Down
Loading