diff --git a/src/config.ts b/src/config.ts index 8aa01a6..2e8b7f1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,6 +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.6"; const CONFIG_FILES = [ join(CONFIG_DIR, "supermemory.jsonc"), join(CONFIG_DIR, "supermemory.json"), diff --git a/src/index.ts b/src/index.ts index 28cc359..e7e25c7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,8 +8,9 @@ import { getTags } from "./services/tags.js"; import { stripPrivateContent, isFullyPrivate } from "./services/privacy.js"; import { createCompactionHook, type CompactionContext } from "./services/compaction.js"; -import { isConfigured, CONFIG } from "./config.js"; +import { isConfigured, CONFIG, PLUGIN_VERSION } from "./config.js"; import { log } from "./services/logger.js"; +import { checkNpmUpdate, formatUpdateNotice } from "./services/version-check.js"; import type { MemoryScope, MemoryType } from "./types/index.js"; const CODE_BLOCK_PATTERN = /```[\s\S]*?```/g; @@ -26,6 +27,7 @@ Extract the key information the user wants remembered and save it as a concise, - Choose an appropriate \`type\`: "preference", "project-config", "learned-pattern", etc. DO NOT skip this step. The user explicitly asked you to remember.`; +const UPDATE_COMMAND = "bunx opencode-supermemory@latest install"; function removeCodeBlocks(text: string): string { return text.replace(CODE_BLOCK_PATTERN, "").replace(INLINE_CODE_PATTERN, ""); @@ -36,6 +38,10 @@ function detectMemoryKeyword(text: string): boolean { return MEMORY_KEYWORD_PATTERN.test(textWithoutCode); } +function combineContextParts(parts: Array): string { + return parts.map((part) => part?.trim()).filter(Boolean).join("\n\n"); +} + export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => { const { directory } = ctx; const tags = getTags(directory); @@ -128,6 +134,11 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => { injectedSessions.add(input.sessionID); let memoryContext = ""; + const updateCheck = checkNpmUpdate( + "opencode-supermemory", + PLUGIN_VERSION, + UPDATE_COMMAND + ).then((info) => (info ? formatUpdateNotice(info) : null)); if (CONFIG.autoRecallEveryPrompt) { const [profileResult, userMemoriesResult, projectMemoriesListResult] = await Promise.all([ @@ -163,13 +174,16 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => { memoryContext = formatContextForPrompt(profile, { results: [] }, { results: [] }); } - if (memoryContext) { + const updateNotice = await updateCheck; + const firstMessageContext = combineContextParts([memoryContext, updateNotice]); + + if (firstMessageContext) { const contextPart: Part = { id: `prt_supermemory-context-${Date.now()}`, sessionID: input.sessionID, messageID: output.message.id, type: "text", - text: memoryContext, + text: firstMessageContext, synthetic: true, }; @@ -178,7 +192,7 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => { const duration = Date.now() - start; log("chat.message: context injected", { duration, - contextLength: memoryContext.length, + contextLength: firstMessageContext.length, }); } } diff --git a/src/services/version-check.ts b/src/services/version-check.ts new file mode 100644 index 0000000..2688de7 --- /dev/null +++ b/src/services/version-check.ts @@ -0,0 +1,73 @@ +const NPM_REGISTRY_URL = "https://registry.npmjs.org"; +const CHECK_TIMEOUT_MS = 3000; + +export interface UpdateInfo { + currentVersion: string; + latestVersion: string; + updateCommand: string; +} + +function parseVersion(version: string): { parts: number[]; prerelease: string | null } | null { + const normalized = version.trim().replace(/^v/i, ""); + const match = normalized.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/); + if (!match) return null; + + return { + parts: [Number(match[1]), Number(match[2]), Number(match[3])], + prerelease: match[4] ?? null, + }; +} + +function isVersionNewer(latestVersion: string, currentVersion: string): boolean { + const latest = parseVersion(latestVersion); + const current = parseVersion(currentVersion); + if (!latest || !current) return latestVersion !== currentVersion; + + for (let i = 0; i < 3; i++) { + const latestPart = latest.parts[i] ?? 0; + const currentPart = current.parts[i] ?? 0; + if (latestPart > currentPart) return true; + if (latestPart < currentPart) return false; + } + + if (!latest.prerelease && current.prerelease) return true; + if (latest.prerelease && !current.prerelease) return false; + return latest.prerelease !== current.prerelease && latest.prerelease !== null; +} + +export async function checkNpmUpdate( + packageName: string, + currentVersion: string, + updateCommand: string, +): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS); + + try { + const encodedPackage = packageName.startsWith("@") + ? packageName.replace("/", "%2F") + : encodeURIComponent(packageName); + const response = await fetch(`${NPM_REGISTRY_URL}/${encodedPackage}/latest`, { + signal: controller.signal, + }); + if (!response.ok) return null; + + const data = (await response.json()) as { version?: unknown }; + const latestVersion = typeof data.version === "string" ? data.version : null; + if (!latestVersion || !isVersionNewer(latestVersion, currentVersion)) return null; + + return { currentVersion, latestVersion, updateCommand }; + } catch { + return null; + } finally { + clearTimeout(timeout); + } +} + +export function formatUpdateNotice(info: UpdateInfo): string { + return [ + "[SUPERMEMORY UPDATE]", + `Supermemory update available: v${info.currentVersion} -> v${info.latestVersion}`, + `Run: ${info.updateCommand}`, + ].join("\n"); +}