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
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
22 changes: 18 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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, "");
Expand All @@ -36,6 +38,10 @@ function detectMemoryKeyword(text: string): boolean {
return MEMORY_KEYWORD_PATTERN.test(textWithoutCode);
}

function combineContextParts(parts: Array<string | null | undefined>): 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);
Expand Down Expand Up @@ -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([
Expand Down Expand Up @@ -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,
};

Expand All @@ -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,
});
}
}
Expand Down
73 changes: 73 additions & 0 deletions src/services/version-check.ts
Original file line number Diff line number Diff line change
@@ -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<UpdateInfo | null> {
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");
}
Loading