diff --git a/package.json b/package.json index c8f2aba..a366f5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opencode-supermemory", - "version": "2.0.9", + "version": "2.0.10", "description": "OpenCode plugin that gives coding agents persistent memory using Supermemory", "type": "module", "main": "dist/index.js", @@ -11,7 +11,8 @@ "scripts": { "build": "bun build ./src/index.ts --outdir ./dist --target node && bun build ./src/cli.ts --outfile ./dist/cli.js --target node && tsc --emitDeclarationOnly", "dev": "tsc --watch", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "bun test" }, "keywords": [ "opencode", diff --git a/src/config.ts b/src/config.ts index 3943b3b..c7c1495 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,7 +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.9"; +export const PLUGIN_VERSION = "2.0.10"; const CONFIG_FILES = [ join(CONFIG_DIR, "supermemory.jsonc"), join(CONFIG_DIR, "supermemory.json"), diff --git a/src/services/auth.ts b/src/services/auth.ts index 90b0a4d..5e368c0 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -162,7 +162,7 @@ export function startAuthFlow(timeoutMs = AUTH_TIMEOUT): Promise { hostname: `opencode - ${hostname()}`, os: `${platform()}-${arch()}`, cwd: process.cwd(), - cli_version: "2.0.9", + cli_version: "2.0.10", }); const authUrl = `${AUTH_BASE_URL}?${params.toString()}`; diff --git a/src/services/entity-context.ts b/src/services/entity-context.ts index 4f80572..4cd6f02 100644 --- a/src/services/entity-context.ts +++ b/src/services/entity-context.ts @@ -1,7 +1,7 @@ export const AGENT_ENTITY_CONTEXT = `Shared coding-agent memory for one software repository. RULES: -- Preserve durable context that helps Claude Code, Codex, or OpenCode continue the work +- Preserve durable context that helps Claude Code, Codex, OpenCode, or Cursor continue the work - Condense assistant responses into decisions, outcomes, and reusable knowledge - Keep user preferences and project facts concise and independently understandable diff --git a/src/services/tags.test.ts b/src/services/tags.test.ts new file mode 100644 index 0000000..6cf8bd7 --- /dev/null +++ b/src/services/tags.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, test } from "bun:test"; +import { normalizeGitRemote, sanitizeRepoName } from "./tags.js"; + +describe("repository identity", () => { + test("normalizes equivalent HTTPS and SSH remotes", () => { + expect( + normalizeGitRemote("https://github.com/SupermemoryAI/mono.git"), + ).toBe("github.com/supermemoryai/mono"); + expect(normalizeGitRemote("git@github.com:SupermemoryAI/mono.git")).toBe( + "github.com/supermemoryai/mono", + ); + }); + + test("sanitizes repository display names", () => { + expect(sanitizeRepoName("Cursor Supermemory.js")).toBe( + "cursor_supermemory_js", + ); + }); +}); diff --git a/src/services/tags.ts b/src/services/tags.ts index 2f67313..be26c57 100644 --- a/src/services/tags.ts +++ b/src/services/tags.ts @@ -1,7 +1,7 @@ import { createHash } from "node:crypto"; import { execSync } from "node:child_process"; import { existsSync, readFileSync, realpathSync } from "node:fs"; -import { hostname, homedir } from "node:os"; +import { hostname, homedir, userInfo } from "node:os"; import { basename, dirname, join, resolve, sep } from "node:path"; import { CONFIG } from "../config.js"; @@ -183,6 +183,63 @@ function loadCodexConfig(): { } } +function loadLegacyCursorConfig(directory: string): { + repoContainerTag?: string; + userContainerTag?: string; + projectContainerTag?: string; +} { + let globalConfig: { + repoContainerTag?: string; + userContainerTag?: string; + projectContainerTag?: string; + } | null = null; + try { + const configPath = join( + homedir(), + ".config", + "cursor", + "supermemory.json", + ); + if (existsSync(configPath)) { + globalConfig = JSON.parse(readFileSync(configPath, "utf-8")) as { + repoContainerTag?: string; + userContainerTag?: string; + projectContainerTag?: string; + }; + } + } catch {} + + let projectConfig: { + repoContainerTag?: string; + userContainerTag?: string; + projectContainerTag?: string; + } | null = null; + let current = resolve(directory); + while (true) { + try { + const configPath = join( + current, + ".cursor", + ".supermemory", + "config.json", + ); + if (existsSync(configPath)) { + projectConfig = JSON.parse(readFileSync(configPath, "utf-8")) as { + repoContainerTag?: string; + userContainerTag?: string; + projectContainerTag?: string; + }; + break; + } + } catch {} + const parent = dirname(current); + if (parent === current) break; + current = parent; + } + + return { ...(globalConfig ?? {}), ...(projectConfig ?? {}) }; +} + export function sanitizeRepoName(name: string): string { const sanitized = name .toLowerCase() @@ -223,6 +280,8 @@ export function getLegacyGeneratedProjectTag(directory: string): string { export function getProjectTag(directory: string): string { return ( loadClaudeProjectConfig(directory)?.repoContainerTag || + process.env.SUPERMEMORY_REPO_TAG || + loadLegacyCursorConfig(directory).repoContainerTag || CONFIG.projectContainerTag || loadCodexConfig()?.projectContainerTag || getGeneratedProjectTag(directory) @@ -302,6 +361,26 @@ export function getLegacyOpenCodeProjectTags(directory: string): string[] { ]); } +export function getLegacyCursorUserTags(directory: string): string[] { + const config = loadLegacyCursorConfig(directory); + const identity = + config.userContainerTag || + process.env.SUPERMEMORY_USER_TAG || + process.env.CURSOR_USER_EMAIL || + getGitEmail(directory) || + `${hostname()}_${userInfo().username}`; + return [`cursor_user_${sha256(identity)}`]; +} + +export function getLegacyCursorProjectTags(directory: string): string[] { + const config = loadLegacyCursorConfig(directory); + const identity = + config.projectContainerTag || + process.env.SUPERMEMORY_PROJECT_TAG || + getProjectBasePath(directory); + return [`cursor_project_${sha256(identity)}`]; +} + function uniqueTags(tags: Array): string[] { return [ ...new Set( @@ -320,6 +399,7 @@ export function getPersonalReadTags(directory: string): string[] { ...getLegacyClaudePersonalTags(directory), ...getLegacyCodexUserTags(directory), ...getLegacyOpenCodeUserTags(directory), + ...getLegacyCursorUserTags(directory), ]); } @@ -330,6 +410,7 @@ export function getProjectReadTags(directory: string): string[] { getLegacyGeneratedProjectTag(directory), ...getLegacyCodexProjectTags(directory), ...getLegacyOpenCodeProjectTags(directory), + ...getLegacyCursorProjectTags(directory), ]); }