diff --git a/src/config.ts b/src/config.ts index 0aca56b..d467991 100644 --- a/src/config.ts +++ b/src/config.ts @@ -26,6 +26,7 @@ interface SupermemoryConfig { } const DEFAULT_KEYWORD_PATTERNS = [ + // English "remember", "memorize", "save\\s+this", @@ -42,6 +43,18 @@ const DEFAULT_KEYWORD_PATTERNS = [ "remember\\s+that", "never\\s+forget", "always\\s+remember", + // Japanese + "覚えて", + "記憶して", + "メモして", + "保存して", + "忘れないで", + "忘れるな", + "記録して", + "覚えておいて", + "メモっておいて", + "メモっといて", + "ノートして", ]; const DEFAULTS: Required> = { diff --git a/src/index.ts b/src/index.ts index 7a5a441..a514a69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { formatContextForPrompt } from "./services/context.js"; import { getTags } from "./services/tags.js"; import { stripPrivateContent, isFullyPrivate } from "./services/privacy.js"; import { createCompactionHook, type CompactionContext } from "./services/compaction.js"; +import { generatePartId } from "./services/ids.js"; import { isConfigured, CONFIG } from "./config.js"; import { log } from "./services/logger.js"; @@ -80,6 +81,51 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => { }) : null; + // Track messages per session for auto-save on session end + const sessionMessages = new Map(); + + const collectMessage = (sessionID: string, text: string) => { + if (!text.trim()) return; + const messages = sessionMessages.get(sessionID) || []; + // Keep last N messages to avoid unbounded growth + const MAX_TRACKED = 50; + if (messages.length >= MAX_TRACKED) messages.shift(); + messages.push(text); + sessionMessages.set(sessionID, messages); + }; + + const saveSessionSummary = async (sessionID: string) => { + const messages = sessionMessages.get(sessionID); + if (!messages || messages.length < 3) { + log("session-save: too few messages, skipping", { sessionID, count: messages?.length || 0 }); + sessionMessages.delete(sessionID); + return; + } + + // Build a condensed transcript (limit size) + const MAX_CHARS = 50_000; + let transcript = messages.join("\n---\n"); + if (transcript.length > MAX_CHARS) { + transcript = transcript.slice(0, MAX_CHARS) + "\n...[truncated]"; + } + + const content = `[Session Conversation - ${new Date().toISOString()}]\n${transcript}`; + + try { + const result = await supermemoryClient.ingestConversation( + sessionID, + [{ role: "user", content }], + [tags.project, tags.user], + { type: "conversation", source: "session-end" } + ); + log("session-save: saved", { sessionID, success: result.success }); + } catch (err) { + log("session-save: error", { sessionID, error: String(err) }); + } + + sessionMessages.delete(sessionID); + }; + return { "chat.message": async (input, output) => { if (!isConfigured()) return; @@ -109,10 +155,13 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => { textPartsCount: textParts.length, }); + // Track message for session-end auto-save + collectMessage(input.sessionID, userMessage); + if (detectMemoryKeyword(userMessage)) { log("chat.message: memory keyword detected"); const nudgePart: Part = { - id: `supermemory-nudge-${Date.now()}`, + id: generatePartId(), sessionID: input.sessionID, messageID: output.message.id, type: "text", @@ -157,7 +206,7 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => { if (memoryContext) { const contextPart: Part = { - id: `supermemory-context-${Date.now()}`, + id: generatePartId(), sessionID: input.sessionID, messageID: output.message.id, type: "text", @@ -485,6 +534,29 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => { }, event: async (input: { event: { type: string; properties?: unknown } }) => { + // Auto-save session conversations on session end + if (input.event.type === "session.deleted" && isConfigured()) { + const props = input.event.properties as Record | undefined; + const sessionInfo = props?.info as { id?: string } | undefined; + if (sessionInfo?.id) { + await saveSessionSummary(sessionInfo.id); + } + } + + // Track assistant messages for richer session summaries + if (input.event.type === "message.updated" && isConfigured()) { + const props = input.event.properties as Record | undefined; + const info = props?.info as { role?: string; sessionID?: string; finish?: boolean } | undefined; + if (info?.role === "assistant" && info?.finish && info?.sessionID) { + // Try to get assistant text from parts + const parts = props?.parts as Array<{ type: string; text?: string }> | undefined; + if (parts) { + const text = parts.filter(p => p.type === "text" && p.text).map(p => p.text!).join("\n"); + if (text) collectMessage(info.sessionID, `[assistant] ${text}`); + } + } + } + if (compactionHook) { await compactionHook.event(input); } diff --git a/src/services/compaction.ts b/src/services/compaction.ts index 4701beb..7bc2f09 100644 --- a/src/services/compaction.ts +++ b/src/services/compaction.ts @@ -4,6 +4,7 @@ import { homedir } from "node:os"; import { supermemoryClient } from "./client.js"; import { log } from "./logger.js"; import { CONFIG } from "../config.js"; +import { generatePartId, generateMessageId } from "./ids.js"; const MESSAGE_STORAGE = join(homedir(), ".opencode", "messages"); const PART_STORAGE = join(homedir(), ".opencode", "parts"); @@ -152,18 +153,6 @@ function findNearestMessageWithFields(messageDir: string): StoredMessage | null return null; } -function generateMessageId(): string { - const timestamp = Date.now().toString(16); - const random = Math.random().toString(36).substring(2, 14); - return `msg_${timestamp}${random}`; -} - -function generatePartId(): string { - const timestamp = Date.now().toString(16); - const random = Math.random().toString(36).substring(2, 10); - return `prt_${timestamp}${random}`; -} - function injectHookMessage( sessionID: string, hookContent: string, diff --git a/src/services/ids.ts b/src/services/ids.ts new file mode 100644 index 0000000..7e7786e --- /dev/null +++ b/src/services/ids.ts @@ -0,0 +1,16 @@ +/** + * Shared ID generation utilities. + * OpenCode v1.2.25+ requires all part IDs to start with "prt". + */ + +export function generatePartId(): string { + const timestamp = Date.now().toString(16); + const random = Math.random().toString(36).substring(2, 10); + return `prt_${timestamp}${random}`; +} + +export function generateMessageId(): string { + const timestamp = Date.now().toString(16); + const random = Math.random().toString(36).substring(2, 14); + return `msg_${timestamp}${random}`; +}