diff --git a/README.md b/README.md index ff2710f..8762949 100644 --- a/README.md +++ b/README.md @@ -47,18 +47,19 @@ export SUPERMEMORY_CC_API_KEY="sm_..." ### Shared Agents memory -Claude Code and `codex-supermemory` use one container for a repository: +Claude Code, Codex, and OpenCode use one container for a repository: - `repo___` stores automatic capture and every explicit save. - `sm_scope` metadata keeps personal and project memories filterable inside that container. The hash is derived from the normalized Git remote, so clones share memory while same-named repositories do not collide. Repositories without a remote fall back to -a local path identity. Both plugins also read the previous `user_project_*`, -`repo_`, `claudecode_project_*`, `codex_user_*`, and -`codex_project_*` containers, so existing memories remain searchable without a -migration. Set `SUPERMEMORY_ISOLATE_WORKTREES=true` to use the worktree path -instead of the remote identity. +a local path identity. The agent plugins also read the previous `user_project_*`, +`repo_`, `claudecode_project_*`, `codex_user_*`, +`codex_project_*`, `opencode_user_*`, and `opencode_project_*` containers, so +existing memories remain searchable without a migration. Set +`SUPERMEMORY_ISOLATE_WORKTREES=true` to use the worktree path instead of the +remote identity. Explicit `repoContainerTag`/`projectContainerTag` overrides remain the canonical write destination. Older personal/user overrides remain in the legacy read set. diff --git a/latest.json b/latest.json index d87a137..823e6f1 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "version": "0.0.11", + "version": "0.0.12", "updateCommand": "/plugin marketplace update supermemory-plugins\n/plugin install supermemory@supermemory-plugins\n\nOnly if you still have the old \"claude-supermemory\" plugin, also remove it:\n/plugin uninstall claude-supermemory@supermemory-plugins" } diff --git a/package-lock.json b/package-lock.json index 801902b..4af5ef4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-supermemory-dev", - "version": "0.0.11", + "version": "0.0.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-supermemory-dev", - "version": "0.0.11", + "version": "0.0.12", "dependencies": { "supermemory": "^4.0.0" }, diff --git a/package.json b/package.json index 1748d83..c0dc966 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-supermemory-dev", - "version": "0.0.11", + "version": "0.0.12", "description": "Claude code plugin by Supermemory AI", "private": true, "type": "commonjs", diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 938941d..1872e51 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "supermemory", "displayName": "Supermemory", - "version": "0.0.11", + "version": "0.0.12", "description": "Persistent memory across Claude Code sessions using Supermemory", "author": { "name": "Supermemory", diff --git a/plugin/package-lock.json b/plugin/package-lock.json index b806574..b6c61d2 100644 --- a/plugin/package-lock.json +++ b/plugin/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-supermemory", - "version": "0.0.9", + "version": "0.0.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-supermemory", - "version": "0.0.9", + "version": "0.0.12", "license": "MIT", "engines": { "node": ">=18.0.0" diff --git a/plugin/package.json b/plugin/package.json index 4e3a3ec..5333f83 100644 --- a/plugin/package.json +++ b/plugin/package.json @@ -1,6 +1,6 @@ { "name": "claude-supermemory", - "version": "0.0.11", + "version": "0.0.12", "description": "Persistent memory for Claude Code using Supermemory", "private": true, "engines": { diff --git a/plugin/scripts/context-hook.cjs b/plugin/scripts/context-hook.cjs index 34f3442..5dd3489 100755 --- a/plugin/scripts/context-hook.cjs +++ b/plugin/scripts/context-hook.cjs @@ -105,7 +105,7 @@ ${r.join(` ${Ln} `}function Ea(e,t,r){let n=r?`${r} memories for "${e}"`:`Memories for "${e}"`;if(!t||t.length===0)return`No ${r?`${r.toLowerCase()} `:""}memories found for "${e}"`;let o=t.map(s=>{let a=s.memory??"",i=s.updatedAt?tr(s.updatedAt):"",d=s.similarity!=null?`[${Math.round(s.similarity*100)}%]`:"";return`${i?`[${i}] `:""}${a} ${d}`.trim()});return`${n} ${o.join(` -`)}`}Vn.exports={formatContext:ka,combineContexts:Ka,formatRelativeTime:tr,formatSearchResults:Ea}});var wn=u((dc,gn)=>{function Ca(e){let t=e?.status;return t===400?"Bad request \u2014 your API key or request format may be invalid. Check your key at https://console.supermemory.ai":t===401?"Authentication failed \u2014 your API key may be expired or revoked. Re-authenticate with the supermemory login command or check https://console.supermemory.ai":t===403?"Permission denied \u2014 this feature may require a different Supermemory plan. Check https://supermemory.ai/pricing":t===429?"Rate limited \u2014 too many requests. Will retry next session.":typeof t=="number"&&t>=500?"Supermemory service is temporarily unavailable. Will retry next session.":e?.message||"Unknown error"}function Ia(e){let t=e?.status;return t===429||typeof t=="number"&&t>=500||t==null}function Ja(e){let t=e?.status;return t===404||t==null}gn.exports={getUserFriendlyError:Ca,isRetryableError:Ia,isBenignError:Ja}});var Zn=u((lc,Mn)=>{var Ba="0.0.11";Mn.exports={PLUGIN_VERSION:Ba}});var Sn=u((vc,Un)=>{var de=require("node:fs"),Dn=require("node:path"),Aa=require("node:os"),Qa=3e3,_a=4320*60*1e3,$a="https://raw.githubusercontent.com/supermemoryai/claude-supermemory/main/latest.json",rr=Dn.join(Aa.homedir(),".supermemory-claude"),nr=Dn.join(rr,"update-check.json");function Fn(e){let r=String(e).trim().replace(/^v/i,"").match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);return r?{parts:[Number(r[1]),Number(r[2]),Number(r[3])],prerelease:r[4]||null}:null}function ei(e,t){let r=Fn(e),n=Fn(t);if(!r||!n)return e!==t;for(let o=0;o<3;o++){if(r.parts[o]>n.parts[o])return!0;if(r.parts[o]=_a}async function oi(e){let t=new AbortController,r=setTimeout(()=>t.abort(),Qa);try{let n=await fetch($a,{signal:t.signal});if(!n.ok)return null;let o=await n.json(),s=typeof o.version=="string"?o.version:null;if(!s||!ei(s,e))return null;let a=ti();return ni(s,a)?(ri(s),{currentVersion:e,latestVersion:s,updateCommand:typeof o.updateCommand=="string"?o.updateCommand:"/plugin install supermemory"}):null}catch{return null}finally{clearTimeout(r)}}function si(e){return` +`)}`}Vn.exports={formatContext:ka,combineContexts:Ka,formatRelativeTime:tr,formatSearchResults:Ea}});var wn=u((dc,gn)=>{function Ca(e){let t=e?.status;return t===400?"Bad request \u2014 your API key or request format may be invalid. Check your key at https://console.supermemory.ai":t===401?"Authentication failed \u2014 your API key may be expired or revoked. Re-authenticate with the supermemory login command or check https://console.supermemory.ai":t===403?"Permission denied \u2014 this feature may require a different Supermemory plan. Check https://supermemory.ai/pricing":t===429?"Rate limited \u2014 too many requests. Will retry next session.":typeof t=="number"&&t>=500?"Supermemory service is temporarily unavailable. Will retry next session.":e?.message||"Unknown error"}function Ia(e){let t=e?.status;return t===429||typeof t=="number"&&t>=500||t==null}function Ja(e){let t=e?.status;return t===404||t==null}gn.exports={getUserFriendlyError:Ca,isRetryableError:Ia,isBenignError:Ja}});var Zn=u((lc,Mn)=>{var Ba="0.0.12";Mn.exports={PLUGIN_VERSION:Ba}});var Sn=u((vc,Un)=>{var de=require("node:fs"),Dn=require("node:path"),Aa=require("node:os"),Qa=3e3,_a=4320*60*1e3,$a="https://raw.githubusercontent.com/supermemoryai/claude-supermemory/main/latest.json",rr=Dn.join(Aa.homedir(),".supermemory-claude"),nr=Dn.join(rr,"update-check.json");function Fn(e){let r=String(e).trim().replace(/^v/i,"").match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);return r?{parts:[Number(r[1]),Number(r[2]),Number(r[3])],prerelease:r[4]||null}:null}function ei(e,t){let r=Fn(e),n=Fn(t);if(!r||!n)return e!==t;for(let o=0;o<3;o++){if(r.parts[o]>n.parts[o])return!0;if(r.parts[o]=_a}async function oi(e){let t=new AbortController,r=setTimeout(()=>t.abort(),Qa);try{let n=await fetch($a,{signal:t.signal});if(!n.ok)return null;let o=await n.json(),s=typeof o.version=="string"?o.version:null;if(!s||!ei(s,e))return null;let a=ti();return ni(s,a)?(ri(s),{currentVersion:e,latestVersion:s,updateCommand:typeof o.updateCommand=="string"?o.updateCommand:"/plugin install supermemory"}):null}catch{return null}finally{clearTimeout(r)}}function si(e){return` IMPORTANT: The user must see this update notice without asking for it. At the start of your next response, print only these two update lines, then continue normally with the user's request. Do not reveal or quote any other Supermemory context. diff --git a/src/lib/container-tag.js b/src/lib/container-tag.js index 8380187..1e1f1e1 100644 --- a/src/lib/container-tag.js +++ b/src/lib/container-tag.js @@ -120,6 +120,138 @@ function loadLegacyCodexConfig() { } } +function stripJsoncComments(content) { + let result = ''; + let index = 0; + let inString = false; + let singleLineComment = false; + let multiLineComment = false; + + while (index < content.length) { + const char = content[index]; + const next = content[index + 1]; + + if (!singleLineComment && !multiLineComment && char === '"') { + let backslashes = 0; + for ( + let cursor = index - 1; + cursor >= 0 && content[cursor] === '\\'; + cursor-- + ) { + backslashes++; + } + if (backslashes % 2 === 0) inString = !inString; + result += char; + index++; + continue; + } + + if (inString) { + result += char; + index++; + continue; + } + + if ( + !singleLineComment && + !multiLineComment && + char === '/' && + next === '/' + ) { + singleLineComment = true; + index += 2; + continue; + } + if ( + !singleLineComment && + !multiLineComment && + char === '/' && + next === '*' + ) { + multiLineComment = true; + index += 2; + continue; + } + if (singleLineComment) { + if (char === '\n') { + singleLineComment = false; + result += char; + } + index++; + continue; + } + if (multiLineComment) { + if (char === '*' && next === '/') { + multiLineComment = false; + index += 2; + continue; + } + if (char === '\n') result += char; + index++; + continue; + } + + result += char; + index++; + } + + return result.replace(/,\s*([}\]])/g, '$1'); +} + +function loadLegacyOpenCodeConfig() { + const configDir = path.join(os.homedir(), '.config', 'opencode'); + for (const filename of ['supermemory.jsonc', 'supermemory.json']) { + try { + const configPath = path.join(configDir, filename); + if (!fs.existsSync(configPath)) continue; + return JSON.parse( + stripJsoncComments(fs.readFileSync(configPath, 'utf-8')), + ); + } catch { + return null; + } + } + return null; +} + +function getLegacyOpenCodeUserTags(cwd) { + const config = loadLegacyOpenCodeConfig(); + let identity = null; + try { + identity = execSync('git config user.email', { + cwd: getProjectBasePath(cwd), + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + }).trim(); + } catch {} + identity = + identity || process.env.USER || process.env.USERNAME || 'anonymous'; + const suffix = sha256(identity); + return uniqueTags([ + config?.userContainerTag, + `${config?.containerTagPrefix || 'opencode'}_user_${suffix}`, + `opencode_user_${suffix}`, + ]); +} + +function getLegacyOpenCodeProjectTags(cwd) { + const config = loadLegacyOpenCodeConfig(); + const hashes = [ + ...new Set( + [cwd, path.resolve(cwd), getProjectBasePath(cwd)].map((value) => + sha256(value), + ), + ), + ]; + return uniqueTags([ + config?.projectContainerTag, + ...hashes.flatMap((suffix) => [ + `${config?.containerTagPrefix || 'opencode'}_project_${suffix}`, + `opencode_project_${suffix}`, + ]), + ]); +} + function getLegacyCodexUserTags(cwd) { const config = loadLegacyCodexConfig(); const defaultTag = getLegacyCodexUserTag(cwd); @@ -207,11 +339,13 @@ function getPersonalReadTags(cwd) { const legacyCodexConfig = loadLegacyCodexConfig(); return uniqueTags([ getContainerTag(cwd), + getGeneratedRepoContainerTag(cwd), projectConfig?.personalContainerTag, legacyCodexConfig?.userContainerTag, getGeneratedContainerTag(cwd), getLegacyContainerTag(cwd), ...getLegacyCodexUserTags(cwd), + ...getLegacyOpenCodeUserTags(cwd), ]); } @@ -221,6 +355,7 @@ function getProjectReadTags(cwd) { getGeneratedRepoContainerTag(cwd), getLegacyGeneratedRepoContainerTag(cwd), ...getLegacyCodexProjectTags(cwd), + ...getLegacyOpenCodeProjectTags(cwd), ]); } @@ -239,6 +374,8 @@ module.exports = { getLegacyContainerTag, getLegacyCodexUserTag, getLegacyCodexProjectTag, + getLegacyOpenCodeUserTags, + getLegacyOpenCodeProjectTags, getRepoContainerTag, getGeneratedRepoContainerTag, getLegacyGeneratedRepoContainerTag, diff --git a/src/lib/plugin-version.js b/src/lib/plugin-version.js index 3401d4e..448c91e 100644 --- a/src/lib/plugin-version.js +++ b/src/lib/plugin-version.js @@ -1,3 +1,3 @@ -const PLUGIN_VERSION = '0.0.11'; +const PLUGIN_VERSION = '0.0.12'; module.exports = { PLUGIN_VERSION }; diff --git a/src/lib/supermemory-client.js b/src/lib/supermemory-client.js index bf11ac2..b1b0969 100644 --- a/src/lib/supermemory-client.js +++ b/src/lib/supermemory-client.js @@ -35,7 +35,7 @@ function supportsScopedCanonicalTag(containerTag) { const AGENT_ENTITY_CONTEXT = `Shared coding-agent memory for one software repository. RULES: -- Preserve durable context that helps Claude Code or Codex continue the work +- Preserve durable context that helps Claude Code, Codex, or OpenCode 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/test/unit.mjs b/test/unit.mjs index 5098c56..6af9802 100644 --- a/test/unit.mjs +++ b/test/unit.mjs @@ -64,7 +64,7 @@ function readTags(cwd, home) { } describe('unified container tags', () => { - test('writes one stable canonical tag and reads Claude and Codex legacy tags', (t) => { + test('writes one stable canonical tag and reads Claude, Codex, and OpenCode legacy tags', (t) => { const { repo, git, home } = makeRepo(t); const tags = readTags(repo, home); const pathHash = hash16(git(['rev-parse', '--show-toplevel'])); @@ -79,11 +79,13 @@ describe('unified container tags', () => { `user_project_${pathHash}`, `claudecode_project_${pathHash}`, `codex_user_${userHash}`, + `opencode_user_${userHash}`, ]); assert.deepEqual(tags.projectReads, [ canonicalTag, 'repo_example_project', `codex_project_${pathHash}`, + `opencode_project_${pathHash}`, ]); });