diff --git a/package.json b/package.json index 9ced747..21b9adc 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/cli.ts b/src/cli.ts index cbd3a3b..9114fae 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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"); @@ -376,6 +377,8 @@ interface InstallOptions { async function install(options: InstallOptions): Promise { console.log("\n🧠 opencode-supermemory installer\n"); + writeInstallDefaults(existsSync(CONFIG_FILE)); + const rl = options.tui ? createReadline() : null; // Step 1: Register plugin in config diff --git a/src/config.ts b/src/config.ts index 0aca56b..de238f9 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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"; @@ -23,6 +23,8 @@ interface SupermemoryConfig { filterPrompt?: string; keywordPatterns?: string[]; compactionThreshold?: number; + autoRecallEveryPrompt?: boolean; + captureEveryNTurns?: number; } const DEFAULT_KEYWORD_PATTERNS = [ @@ -54,6 +56,8 @@ const DEFAULTS: Required 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, @@ -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)); +} diff --git a/src/index.ts b/src/index.ts index fecd624..28cc359 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 = { diff --git a/src/services/client.ts b/src/services/client.ts index 6fb3c3c..67c2cb2 100644 --- a/src/services/client.ts +++ b/src/services/client.ts @@ -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 @@ -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; + const result = await withTimeout( this.getClient().memories.add({ content, containerTag, - metadata: metadata as Record, + metadata: mergedMetadata, }), TIMEOUT_MS );