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
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.15",
"version": "0.0.16",
"updateCommand": "claude plugin update 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.15",
"version": "0.0.16",
"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.15",
"version": "0.0.16",
"description": "Persistent memory across Claude Code sessions using Supermemory",
"author": {
"name": "Supermemory",
Expand Down
24 changes: 24 additions & 0 deletions plugin/commands/statusline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
name: statusline
description: Install the Supermemory activity status line for Claude Code
allowed-tools: Bash(node:*) PowerShell(node:*)
---

# Install the Supermemory status line

Run this command exactly:

```bash
node '${CLAUDE_PLUGIN_ROOT}/scripts/install-statusline.cjs' '${CLAUDE_PLUGIN_DATA}'
```

Then report the command output to the user.

The installer is intentionally conservative:

- It uses Claude Code's persistent plugin data directory, so plugin upgrades do not break the configured path.
- It uses Node on macOS, Linux, and Windows; no POSIX shell syntax is required.
- It preserves all unrelated Claude settings.
- If another status line is already configured, it leaves that configuration untouched and explains what to do next.

Once enabled, the status line displays factual per-session activity such as memory items loaded, search results, session saving, and session captured.
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.15",
"version": "0.0.16",
"description": "Persistent memory for Claude Code using Supermemory",
"private": true,
"engines": {
Expand Down
61 changes: 32 additions & 29 deletions plugin/scripts/context-hook.cjs

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions plugin/scripts/install-statusline.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env node
var v=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var C=v((x,D)=>{var f=require("node:crypto"),o=require("node:fs"),g=require("node:os"),s=require("node:path"),u="statusline.cjs";function A(t){o.mkdirSync(t,{recursive:!0,mode:448});try{o.chmodSync(t,448)}catch{}}function p(t,e){let n=s.dirname(e);A(n);let r=s.join(n,`.${s.basename(e)}.${process.pid}.${f.randomUUID()}.tmp`);try{o.copyFileSync(t,r,o.constants.COPYFILE_EXCL);try{o.chmodSync(r,384)}catch{}o.renameSync(r,e)}finally{try{o.unlinkSync(r)}catch{}}}function j(t,e,n=384){let r=s.dirname(t);o.mkdirSync(r,{recursive:!0,mode:448});let i=s.join(r,`.settings.${process.pid}.${f.randomUUID()}.tmp`);try{o.writeFileSync(i,`${JSON.stringify(e,null,2)}
`,{encoding:"utf8",flag:"wx",mode:n}),o.renameSync(i,t)}finally{try{o.unlinkSync(i)}catch{}}}function m(t){let e=/^(?:[A-Za-z]:[\\/]|\\\\)/.test(t),r=(s.isAbsolute(t)||e?t:s.resolve(t)).replaceAll("\\","/");if([...r].some(a=>{let c=a.charCodeAt(0);return a==="'"||c<32||c===127}))throw new Error("The statusline path cannot contain control characters or single quotes");return`node '${r}'`}function h(t){if(!o.existsSync(t))return{};let e=o.readFileSync(t,"utf8"),n=JSON.parse(e);if(!n||typeof n!="object"||Array.isArray(n))throw new Error("Claude settings must contain a JSON object");return n}function I(t){let e=s.resolve(t.pluginDataDir),n=s.resolve(t.sourcePath),r=s.resolve(t.configDir||process.env.CLAUDE_CONFIG_DIR||s.join(g.homedir(),".claude")),i=s.join(r,"settings.json"),a=s.join(e,u),c=m(a),d=h(i),l=d.statusLine;if(l!=null&&l?.command!==c)return{status:"existing-statusline",settingsPath:i,runtimePath:a};p(n,a);let y=384;try{y=o.statSync(i).mode&511}catch{}return d.statusLine={...l&&typeof l=="object"?l:{},type:"command",command:c,refreshInterval:1},j(i,d,y),{status:"installed",command:c,settingsPath:i,runtimePath:a}}function L(t={}){let e=t.pluginRoot||process.env.CLAUDE_PLUGIN_ROOT,n=t.pluginDataDir||process.env.CLAUDE_PLUGIN_DATA;if(!e||!n)return!1;let r=s.join(e,"scripts",u),i=s.join(n,u);if(!o.existsSync(r)||!S(n,t.configDir))return!1;try{return o.readFileSync(r).equals(o.readFileSync(i))?!1:(p(r,i),!0)}catch{return!1}}function S(t,e){if(!t)return!1;let n=s.resolve(t,u),r=s.join(s.resolve(e||process.env.CLAUDE_CONFIG_DIR||s.join(g.homedir(),".claude")),"settings.json");try{let i=h(r);return o.existsSync(n)&&i.statusLine?.type==="command"&&i.statusLine.command===m(n)}catch{return!1}}D.exports={RUNTIME_FILENAME:u,formatStatuslineCommand:m,installStatusline:I,isStatuslineInstalled:S,refreshInstalledStatusline:L}});var U=require("node:path"),{installStatusline:_}=C();function E(){let t=process.argv[2]||process.env.CLAUDE_PLUGIN_DATA;if(!t)throw new Error("Claude did not provide a persistent plugin data directory");let e=_({pluginDataDir:t,sourcePath:U.join(__dirname,"statusline.cjs")});if(e.status==="existing-statusline"){console.log(`An existing Claude Code status line is already configured in ${e.settingsPath}. It was left unchanged.`),console.log("Remove the existing status line setting, then run /supermemory:statusline again.");return}console.log(`Supermemory status line installed in ${e.settingsPath}.`),console.log("It will appear on the next Claude Code status refresh.")}try{E()}catch(t){console.error(`Could not install the Supermemory status line: ${t.message}`),process.exitCode=1}
44 changes: 22 additions & 22 deletions plugin/scripts/search-memory.cjs

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions plugin/scripts/statusline.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env node
var R=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var L=R((nt,I)=>{var A=require("node:crypto"),i=require("node:fs"),J=require("node:os"),c=require("node:path"),N=1,H="statusline-state",V=new Set(["context","capture","search"]),M=10080*60*1e3;function g(t){return t||process.env.CLAUDE_PLUGIN_DATA||c.join(J.homedir(),".supermemory-claude","statusline")}function G(t){return A.createHash("sha256").update(String(t)).digest("hex")}function h(t){let e=Number(t);return Number.isFinite(e)?Math.max(0,Math.floor(e)):0}function $(t){return c.join(g(t),H)}function p(t,e){return typeof t!="string"||!t.trim()?null:c.join($(e),G(t.trim()))}function k(t){i.mkdirSync(t,{recursive:!0,mode:448});try{i.chmodSync(t,448)}catch{}}function x(t,e){let r=c.dirname(t);k(r);let n=c.join(r,`.${c.basename(t)}.${process.pid}.${A.randomUUID()}.tmp`);try{i.writeFileSync(n,JSON.stringify(e),{encoding:"utf8",flag:"wx",mode:384}),i.renameSync(n,t);try{i.chmodSync(t,384)}catch{}}finally{try{i.unlinkSync(n)}catch{}}}function z(t,e){return t==="context"?{status:["loading","ready","error"].includes(e.status)?e.status:"ready",memoryItemsLoaded:h(e.memoryItemsLoaded)}:t==="capture"?{status:["saving","saved","error"].includes(e.status)?e.status:"error"}:{results:h(e.results)}}function B(t,e,r={},n={}){if(!V.has(e))return!1;let s=p(t,n.dataDir);if(!s)return!1;try{let o={version:N,event:e,updatedAt:n.now??Date.now(),...z(e,r)};return x(c.join(s,`${e}.json`),o),!0}catch{return!1}}function S(t,e){try{let r=JSON.parse(i.readFileSync(c.join(t,`${e}.json`),"utf8"));return r?.version!==N||r?.event!==e||!Number.isFinite(r?.updatedAt)?null:r}catch{return null}}function W(t,e={}){let r=p(t,e.dataDir);return r?{context:S(r,"context"),capture:S(r,"capture"),search:S(r,"search")}:{}}function X(t,e){let r=h(e),n=Math.min(t?.profile?.static?.length||0,r),s=Math.min(t?.profile?.dynamic?.length||0,r);return n+s}function Y(t={}){let e=$(t.dataDir),r=(t.now??Date.now())-M;try{for(let n of i.readdirSync(e,{withFileTypes:!0})){if(!n.isDirectory()||!/^[a-f0-9]{64}$/.test(n.name))continue;let s=c.join(e,n.name),o=0;try{o=i.statSync(s).mtimeMs;for(let a of i.readdirSync(s))o=Math.max(o,i.statSync(c.join(s,a)).mtimeMs)}catch{continue}o<r&&i.rmSync(s,{recursive:!0,force:!0})}}catch{}}I.exports={SESSION_RETENTION_MS:M,atomicWriteJson:x,countLoadedProfileItems:X,getSessionDir:p,pruneState:Y,readState:W,resolveStatuslineDataDir:g,writeState:B}});var{readState:K}=L(),w=30*1e3,U=120*1e3,T=60*1e3,v=1440*60*1e3,O=500,Q="\x1B[38;2;59;53;243m",Z="\x1B[97m",tt="\x1B[1m",b="\x1B[0m";function m(t,e,r,n=0){return t&&t.updatedAt>=n&&r-t.updatedAt>=0&&r-t.updatedAt<e}function j(t,e=Date.now()){let r=t.context,n=Number.isFinite(r?.updatedAt)?r.updatedAt:0,s=t.capture,o=t.search;if(s?.status==="saving"&&m(s,w,e,n))return"saving session";if(s?.status==="saved"&&m(s,U,e,n))return"session captured";if(s?.status==="error"&&m(s,T,e,n))return"session sync failed";if(m(o,T,e,n)){let f=o.results||0;return`${f} search ${f===1?"result":"results"}`}if(!m(r,v,e)||r.status!=="ready")return null;let a=r.memoryItemsLoaded||0;return a===0?"ready":`${a} memory ${a===1?"item":"items"} loaded`}function C(t,e={}){let r=j(t,e.now);return r?e.color===!1?`supermemory \xB7 ${r}`:`${Q}${tt}\u26A1 supermemory${b} ${Z}\xB7 ${r}${b}`:""}function F(t=process.stdin,e={}){let r=e.timeoutMs??O;return new Promise((n,s)=>{let o="",a=!1,f,P=()=>{clearTimeout(f),t.off("data",E),t.off("end",_),t.off("error",D);try{t.pause(),t.unref?.()}catch{}},d=(u,l)=>{a||(a=!0,P(),u(l))},y=u=>{let l=o.trim();if(!l){u&&d(n,{});return}try{d(n,JSON.parse(l))}catch(q){u&&d(s,new Error(`Failed to parse statusline JSON: ${q.message}`))}};function E(u){o+=u,y(!1)}function _(){y(!0)}function D(u){d(s,u)}if(t.setEncoding("utf8"),t.on("data",E),t.on("end",_),t.on("error",D),t.isTTY){d(n,{});return}a||(f=setTimeout(()=>y(!0),r))})}async function et(){try{let e=(await F())?.session_id;if(!e)return;let r=K(e,{dataDir:process.env.CLAUDE_PLUGIN_DATA||__dirname}),n=C(r);n&&process.stdout.write(n)}catch{}}require.main===module&&et();module.exports={CAPTURE_TTL_MS:U,CONTEXT_TTL_MS:v,SAVING_TTL_MS:w,SEARCH_TTL_MS:T,STATUSLINE_INPUT_TIMEOUT_MS:O,getStatusLabel:j,readStatuslineInput:F,renderStatusline:C};
28 changes: 14 additions & 14 deletions plugin/scripts/summary-hook.cjs

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions plugin/skills/supermemory-search/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: supermemory-search
description: Search your coding memory. Use when user asks about past work, previous sessions, how something was implemented, what they worked on before, or wants to recall information from earlier sessions.
allowed-tools: Bash(node:*)
allowed-tools: Bash(node:*) PowerShell(node:*)
---

# Supermemory Search
Expand All @@ -13,7 +13,7 @@ Search Supermemory for past coding sessions, decisions, and saved information.
Run the search script with the user's query and optional scope flag:

```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/search-memory.cjs" [--user|--repo|--both] "USER_QUERY_HERE"
node '${CLAUDE_PLUGIN_ROOT}/scripts/search-memory.cjs' --statusline-data-dir '${CLAUDE_PLUGIN_DATA}' [--user|--repo|--both] "USER_QUERY_HERE"
```

### Scope Flags
Expand All @@ -27,18 +27,18 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/search-memory.cjs" [--user|--repo|--both] "U
- User asks "what did I work on yesterday":

```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/search-memory.cjs" "work yesterday recent activity"
node '${CLAUDE_PLUGIN_ROOT}/scripts/search-memory.cjs' --statusline-data-dir '${CLAUDE_PLUGIN_DATA}' "work yesterday recent activity"
```

- User asks "how did we implement auth" (project-specific):

```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/search-memory.cjs" --repo "authentication implementation"
node '${CLAUDE_PLUGIN_ROOT}/scripts/search-memory.cjs' --statusline-data-dir '${CLAUDE_PLUGIN_DATA}' --repo "authentication implementation"
```

- User asks "what are my coding preferences":
```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/search-memory.cjs" --user "coding preferences style"
node '${CLAUDE_PLUGIN_ROOT}/scripts/search-memory.cjs' --statusline-data-dir '${CLAUDE_PLUGIN_DATA}' --user "coding preferences style"
```

## Present Results
Expand Down
2 changes: 2 additions & 0 deletions scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const hooks = [
'add-memory',
'save-project-memory',
'status',
'install-statusline',
'statusline',
];

async function build() {
Expand Down
95 changes: 93 additions & 2 deletions src/context-hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,50 @@ const { formatContext, combineContexts } = require('./lib/format-context');
const { getUserFriendlyError, isBenignError } = require('./lib/error-helpers');
const { PLUGIN_VERSION } = require('./lib/plugin-version');
const { checkForUpdate, formatUpdateNotice } = require('./lib/version-check');
const {
countLoadedProfileItems,
pruneState,
resolveStatuslineDataDir,
writeState,
} = require('./lib/statusline-state');
const {
isStatuslineInstalled,
refreshInstalledStatusline,
} = require('./lib/statusline-install');
const fs = require('node:fs');
const path = require('node:path');

const STATUSLINE_TIP_FILE = 'statusline-tip-shown';

function getStatuslineOnboardingNotice(dataDir) {
if (isStatuslineInstalled(dataDir)) return null;
const tipFile = path.join(dataDir, STATUSLINE_TIP_FILE);

try {
if (fs.existsSync(tipFile)) return null;
} catch {
// continue
}

try {
fs.mkdirSync(dataDir, { recursive: true, mode: 0o700 });
fs.writeFileSync(tipFile, new Date().toISOString(), {
encoding: 'utf8',
mode: 0o600,
});
} catch {
// best-effort
}

return 'Supermemory status line is available. Run /supermemory:statusline to enable it.';
}

function combineOutputParts(parts) {
return parts
.map((part) => part?.trim())
.filter(Boolean)
.join('\n\n');
}

function writeSessionStartOutput(additionalContext, systemMessage = null) {
writeOutput({
Expand All @@ -26,10 +70,22 @@ function writeSessionStartOutput(additionalContext, systemMessage = null) {

async function main() {
const settings = loadSettings();
let sessionId;
let statuslineDataDir;

try {
const input = await readStdin();
sessionId = input.session_id;
const cwd = input.cwd || process.cwd();
statuslineDataDir = resolveStatuslineDataDir();
refreshInstalledStatusline();
pruneState({ dataDir: statuslineDataDir });
writeState(
sessionId,
'context',
{ status: 'loading', memoryItemsLoaded: 0 },
{ dataDir: statuslineDataDir },
);
const projectName = getProjectName(cwd);
const updateCheck = checkForUpdate(PLUGIN_VERSION).then((info) =>
info ? formatUpdateNotice(info) : null,
Expand All @@ -48,6 +104,12 @@ async function main() {
debugLog(settings, 'Auth flow completed successfully');
} catch (authErr) {
const isTimeout = authErr.message === 'AUTH_TIMEOUT';
writeState(
sessionId,
'context',
{ status: 'error', memoryItemsLoaded: 0 },
{ dataDir: statuslineDataDir },
);
writeSessionStartOutput(
`<supermemory-status>
${isTimeout ? 'Authentication timed out. Please complete login in the browser window.' : 'Authentication failed.'}
Expand Down Expand Up @@ -99,6 +161,20 @@ Or set SUPERMEMORY_CC_API_KEY environment variable manually.
false,
);

const memoryItemsLoaded = countLoadedProfileItems(
projectResult,
settings.maxProfileItems,
);
writeState(
sessionId,
'context',
{
status: apiErrors.length > 0 ? 'error' : 'ready',
memoryItemsLoaded,
},
{ dataDir: statuslineDataDir },
);

const additionalContext = combineContexts([
{
label: '### Project Memories (Shared across agents)',
Expand All @@ -120,7 +196,10 @@ Or set SUPERMEMORY_CC_API_KEY environment variable manually.
No previous memories found for this project.
Memories will be saved as you work.
</supermemory-context>`,
updateNotice,
combineOutputParts([
updateNotice,
getStatuslineOnboardingNotice(statuslineDataDir),
]),
);
return;
}
Expand All @@ -131,11 +210,23 @@ Memories will be saved as you work.
});

const updateNotice = await updateCheck;
writeSessionStartOutput(errorNotice + additionalContext, updateNotice);
writeSessionStartOutput(
errorNotice + additionalContext,
combineOutputParts([
updateNotice,
getStatuslineOnboardingNotice(statuslineDataDir),
]),
);
} catch (err) {
const friendly = getUserFriendlyError(err);
debugLog(settings, 'Error', { error: friendly });
console.error(`Supermemory: ${friendly}`);
writeState(
sessionId,
'context',
{ status: 'error', memoryItemsLoaded: 0 },
{ dataDir: statuslineDataDir },
);
writeSessionStartOutput(`<supermemory-status>
Failed to load memories: ${friendly}
Session will continue without memory context.
Expand Down
38 changes: 38 additions & 0 deletions src/install-statusline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const path = require('node:path');
const { installStatusline } = require('./lib/statusline-install');

function main() {
const pluginDataDir = process.argv[2] || process.env.CLAUDE_PLUGIN_DATA;
if (!pluginDataDir) {
throw new Error(
'Claude did not provide a persistent plugin data directory',
);
}

const result = installStatusline({
pluginDataDir,
sourcePath: path.join(__dirname, 'statusline.cjs'),
});

if (result.status === 'existing-statusline') {
console.log(
`An existing Claude Code status line is already configured in ${result.settingsPath}. It was left unchanged.`,
);
console.log(
'Remove the existing status line setting, then run /supermemory:statusline again.',
);
return;
}

console.log(`Supermemory status line installed in ${result.settingsPath}.`);
console.log('It will appear on the next Claude Code status refresh.');
}

try {
main();
} catch (error) {
console.error(
`Could not install the Supermemory status line: ${error.message}`,
);
process.exitCode = 1;
}
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.15';
const PLUGIN_VERSION = '0.0.16';

module.exports = { PLUGIN_VERSION };
25 changes: 25 additions & 0 deletions src/lib/search-args.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
function parseSearchArgs(args) {
let containerType = 'both';
let statuslineDataDir;
const queryParts = [];

for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === '--user') {
containerType = 'user';
} else if (arg === '--repo') {
containerType = 'repo';
} else if (arg === '--both') {
containerType = 'both';
} else if (arg === '--statusline-data-dir') {
statuslineDataDir = args[index + 1];
index += 1;
} else {
queryParts.push(arg);
}
}

return { containerType, query: queryParts.join(' '), statuslineDataDir };
}

module.exports = { parseSearchArgs };
Loading
Loading