Skip to content
Merged
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
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<project-name>__<remote-hash>` 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_<project-name>`, `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_<project-name>`, `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.
Expand Down
2 changes: 1 addition & 1 deletion latest.json
Original file line number Diff line number Diff line change
@@ -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"
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions plugin/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion plugin/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion plugin/scripts/context-hook.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ ${r.join(`
${Ln}
</supermemory-context>`}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]<n.parts[o])return!1}return!r.prerelease&&n.prerelease?!0:r.prerelease&&!n.prerelease?!1:r.prerelease!==n.prerelease&&r.prerelease!==null}function ti(){try{if(!de.existsSync(nr))return null;let e=JSON.parse(de.readFileSync(nr,"utf-8"));if(typeof e.lastNotifiedAt!="string"||typeof e.lastNotifiedVersion!="string")return null;let t=new Date(e.lastNotifiedAt).getTime();return Number.isNaN(t)?null:{lastNotifiedAt:t,lastNotifiedVersion:e.lastNotifiedVersion}}catch{return null}}function ri(e){try{de.existsSync(rr)||de.mkdirSync(rr,{recursive:!0}),de.writeFileSync(nr,JSON.stringify({lastNotifiedAt:new Date().toISOString(),lastNotifiedVersion:e},null,2))}catch{}}function ni(e,t){return!t||t.lastNotifiedVersion!==e?!0:Date.now()-t.lastNotifiedAt>=_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`<supermemory-update>
`)}`}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]<n.parts[o])return!1}return!r.prerelease&&n.prerelease?!0:r.prerelease&&!n.prerelease?!1:r.prerelease!==n.prerelease&&r.prerelease!==null}function ti(){try{if(!de.existsSync(nr))return null;let e=JSON.parse(de.readFileSync(nr,"utf-8"));if(typeof e.lastNotifiedAt!="string"||typeof e.lastNotifiedVersion!="string")return null;let t=new Date(e.lastNotifiedAt).getTime();return Number.isNaN(t)?null:{lastNotifiedAt:t,lastNotifiedVersion:e.lastNotifiedVersion}}catch{return null}}function ri(e){try{de.existsSync(rr)||de.mkdirSync(rr,{recursive:!0}),de.writeFileSync(nr,JSON.stringify({lastNotifiedAt:new Date().toISOString(),lastNotifiedVersion:e},null,2))}catch{}}function ni(e,t){return!t||t.lastNotifiedVersion!==e?!0:Date.now()-t.lastNotifiedAt>=_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`<supermemory-update>
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.
Expand Down
137 changes: 137 additions & 0 deletions src/lib/container-tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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),
]);
}

Expand All @@ -221,6 +355,7 @@ function getProjectReadTags(cwd) {
getGeneratedRepoContainerTag(cwd),
getLegacyGeneratedRepoContainerTag(cwd),
...getLegacyCodexProjectTags(cwd),
...getLegacyOpenCodeProjectTags(cwd),
]);
}

Expand All @@ -239,6 +374,8 @@ module.exports = {
getLegacyContainerTag,
getLegacyCodexUserTag,
getLegacyCodexProjectTag,
getLegacyOpenCodeUserTags,
getLegacyOpenCodeProjectTags,
getRepoContainerTag,
getGeneratedRepoContainerTag,
getLegacyGeneratedRepoContainerTag,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/plugin-version.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
const PLUGIN_VERSION = '0.0.11';
const PLUGIN_VERSION = '0.0.12';

module.exports = { PLUGIN_VERSION };
2 changes: 1 addition & 1 deletion src/lib/supermemory-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 3 additions & 1 deletion test/unit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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']));
Expand All @@ -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}`,
]);
});

Expand Down
Loading