Skip to content
Closed
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
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.

10 changes: 4 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "claude-supermemory-dev",
"version": "2.0.1",
"description": "Claude code plugin by Supermemory AI",
"name": "claude-supermemory-local",
"version": "3.0.0-local",
"description": "Claude Code persistent memory plugin — local file storage fork",
"private": true,
"type": "commonjs",
"scripts": {
Expand All @@ -18,7 +18,5 @@
"@biomejs/biome": "^2.3.13",
"esbuild": "^0.25.0"
},
"dependencies": {
"supermemory": "^4.0.0"
}
"dependencies": {}
}
85 changes: 4 additions & 81 deletions plugin/scripts/add-memory.cjs

Large diffs are not rendered by default.

127 changes: 23 additions & 104 deletions plugin/scripts/context-hook.cjs

Large diffs are not rendered by default.

85 changes: 4 additions & 81 deletions plugin/scripts/save-project-memory.cjs

Large diffs are not rendered by default.

119 changes: 21 additions & 98 deletions plugin/scripts/search-memory.cjs

Large diffs are not rendered by default.

101 changes: 12 additions & 89 deletions plugin/scripts/summary-hook.cjs

Large diffs are not rendered by default.

22 changes: 4 additions & 18 deletions src/add-memory.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
const {
SupermemoryClient,
LocalMemoryClient,
PERSONAL_ENTITY_CONTEXT,
} = require('./lib/supermemory-client');
} = require('./lib/local-memory-client');
const { getContainerTag, getProjectName } = require('./lib/container-tag');
const { loadSettings, getApiKey } = require('./lib/settings');
const { getUserFriendlyError } = require('./lib/error-helpers');

async function main() {
const content = process.argv.slice(2).join(' ');
Expand All @@ -16,23 +14,12 @@ async function main() {
return;
}

const settings = loadSettings();

let apiKey;
try {
apiKey = getApiKey(settings);
} catch {
console.log('Supermemory API key not configured.');
console.log('Set SUPERMEMORY_CC_API_KEY environment variable.');
return;
}

const cwd = process.cwd();
const containerTag = getContainerTag(cwd);
const projectName = getProjectName(cwd);

try {
const client = new SupermemoryClient(apiKey, containerTag);
const client = new LocalMemoryClient(containerTag);
const result = await client.addMemory(
content,
containerTag,
Expand All @@ -41,13 +28,12 @@ async function main() {
project: projectName,
timestamp: new Date().toISOString(),
},
{ entityContext: PERSONAL_ENTITY_CONTEXT },
);

console.log(`Memory saved to project: ${projectName}`);
console.log(`ID: ${result.id}`);
} catch (err) {
console.log(`Error saving memory: ${getUserFriendlyError(err)}`);
console.log(`Error saving memory: ${err.message}`);
}
}

Expand Down
79 changes: 11 additions & 68 deletions src/context-hook.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
const { SupermemoryClient } = require('./lib/supermemory-client');
const { LocalMemoryClient } = require('./lib/local-memory-client');
const {
getContainerTag,
getRepoContainerTag,
getProjectName,
} = require('./lib/container-tag');
const { loadSettings, getApiKey, debugLog } = require('./lib/settings');
const { loadSettings, debugLog } = require('./lib/settings');
const { readStdin, writeOutput } = require('./lib/stdin');
const { startAuthFlow, AUTH_BASE_URL } = require('./lib/auth');
const { formatContext, combineContexts } = require('./lib/format-context');
const { getUserFriendlyError, isBenignError } = require('./lib/error-helpers');

async function main() {
const settings = loadSettings();
Expand All @@ -20,62 +18,15 @@ async function main() {

debugLog(settings, 'SessionStart', { cwd, projectName });

let apiKey;
try {
apiKey = getApiKey(settings);
} catch {
try {
debugLog(settings, 'No API key found, starting browser auth flow');
apiKey = await startAuthFlow();
debugLog(settings, 'Auth flow completed successfully');
} catch (authErr) {
const isTimeout = authErr.message === 'AUTH_TIMEOUT';
writeOutput({
hookSpecificOutput: {
hookEventName: 'SessionStart',
additionalContext: `<supermemory-status>
${isTimeout ? 'Authentication timed out. Please complete login in the browser window.' : 'Authentication failed.'}
If the browser did not open, visit: ${AUTH_BASE_URL}
Or set SUPERMEMORY_CC_API_KEY environment variable manually.
</supermemory-status>`,
},
});
return;
}
}

const client = new SupermemoryClient(apiKey);
const client = new LocalMemoryClient();
const personalTag = getContainerTag(cwd);
const repoTag = getRepoContainerTag(cwd);

debugLog(settings, 'Fetching contexts', { personalTag, repoTag });

const apiErrors = [];

const handleProfileError = (label) => (err) => {
if (isBenignError(err)) {
debugLog(settings, `Benign error fetching ${label} context`, {
status: err.status,
message: err.message,
});
return null;
}
const friendly = getUserFriendlyError(err);
debugLog(settings, `Error fetching ${label} context`, {
status: err.status,
message: friendly,
});
apiErrors.push(friendly);
return null;
};

const [personalResult, repoResult] = await Promise.all([
client
.getProfile(personalTag, projectName)
.catch(handleProfileError('personal')),
client
.getProfile(repoTag, projectName)
.catch(handleProfileError('repo')),
client.getProfile(personalTag, projectName, settings.maxProfileItems).catch(() => null),
client.getProfile(repoTag, projectName, settings.maxProfileItems).catch(() => null),
]);

const personalContext = formatContext(
Expand All @@ -102,18 +53,11 @@ Or set SUPERMEMORY_CC_API_KEY environment variable manually.
},
]);

const errorNotice =
apiErrors.length > 0
? `<supermemory-status>\n${[...new Set(apiErrors)].join('\n')}\n</supermemory-status>\n`
: '';

if (!additionalContext) {
writeOutput({
hookSpecificOutput: {
hookEventName: 'SessionStart',
additionalContext: apiErrors.length > 0
? errorNotice
: `<supermemory-context>
additionalContext: `<supermemory-context>
No previous memories found for this project.
Memories will be saved as you work.
</supermemory-context>`,
Expand All @@ -131,18 +75,17 @@ Memories will be saved as you work.
writeOutput({
hookSpecificOutput: {
hookEventName: 'SessionStart',
additionalContext: errorNotice + additionalContext,
additionalContext,
},
});
} catch (err) {
const friendly = getUserFriendlyError(err);
debugLog(settings, 'Error', { error: friendly });
console.error(`Supermemory: ${friendly}`);
debugLog(settings, 'Error', { error: err.message });
console.error(`Supermemory-local: ${err.message}`);
writeOutput({
hookSpecificOutput: {
hookEventName: 'SessionStart',
additionalContext: `<supermemory-status>
Failed to load memories: ${friendly}
Failed to load memories: ${err.message}
Session will continue without memory context.
</supermemory-status>`,
},
Expand All @@ -151,6 +94,6 @@ Session will continue without memory context.
}

main().catch((err) => {
console.error(`Supermemory fatal: ${err.message}`);
console.error(`Supermemory-local fatal: ${err.message}`);
process.exit(1);
});
174 changes: 174 additions & 0 deletions src/lib/local-memory-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const crypto = require('node:crypto');

const MEMORIES_DIR = path.join(os.homedir(), '.supermemory-claude', 'memories');

const PERSONAL_ENTITY_CONTEXT = `Developer coding session transcript. Focus on USER message and intent.

RULES:
- Extract USER's action/intent, not every detail assistant provides
- Condense assistant responses into what user gained from it
- Skip granular facts from assistant output

EXTRACT:
- Research: "researched whisper.cpp for speech recognition"
- Actions: "built auth flow with JWT", "fixed memory leak in useEffect"
- Preferences: "prefers Tailwind over CSS modules"
- Decisions: "chose SQLite for local storage"
- Learnings: "learned about React Server Components"

SKIP:
- Every fact assistant mentions (condense to user's action)
- Generic assistant explanations user didn't confirm/use`;

const REPO_ENTITY_CONTEXT = `Project/codebase knowledge for team sharing.

EXTRACT:
- Architecture: "uses monorepo with turborepo", "API in /apps/api"
- Conventions: "components in PascalCase", "hooks prefixed with use"
- Patterns: "all API routes use withAuth wrapper", "errors thrown as ApiError"
- Setup: "requires .env with DATABASE_URL", "run pnpm db:migrate first"
- Decisions: "chose Drizzle over Prisma for performance", "using RSC for data fetching"`;

function ensureDir(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}

function containerDir(type, containerTag) {
return path.join(MEMORIES_DIR, type, containerTag);
}

function loadAllMemories(dir) {
if (!fs.existsSync(dir)) return [];
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
const memories = [];
for (const file of files) {
try {
const raw = fs.readFileSync(path.join(dir, file), 'utf-8');
memories.push(JSON.parse(raw));
} catch {
// skip corrupt files
}
}
return memories;
}

function scoreMemory(memory, queryTokens) {
const content = (memory.content || '').toLowerCase();
let score = 0;
for (const token of queryTokens) {
if (content.includes(token)) {
score += 1;
}
}
// Boost recent memories
const age = Date.now() - new Date(memory.createdAt || 0).getTime();
const recencyBonus = Math.max(0, 1 - age / (30 * 24 * 60 * 60 * 1000)); // decay over 30 days
return score + recencyBonus * 0.5;
}

class LocalMemoryClient {
constructor(containerTag) {
this.containerTag = containerTag || 'default';
}

async addMemory(content, containerTag, metadata = {}) {
const tag = containerTag || this.containerTag;
const type = metadata.type === 'project-knowledge' ? 'repo' : 'personal';
const dir = containerDir(type, tag);
ensureDir(dir);

const id = crypto.randomUUID();
const entry = {
id,
content,
metadata: { sm_source: 'claude-code-local', ...metadata },
createdAt: new Date().toISOString(),
};

fs.writeFileSync(path.join(dir, `${id}.json`), JSON.stringify(entry, null, 2));
return { id, status: 'saved', containerTag: tag };
}

async search(query, containerTag, options = {}) {
const tag = containerTag || this.containerTag;
const limit = options.limit || 10;

// Search both personal and repo for this tag
const personalDir = containerDir('personal', tag);
const repoDir = containerDir('repo', tag);
const allMemories = [...loadAllMemories(personalDir), ...loadAllMemories(repoDir)];

if (allMemories.length === 0) {
return { results: [], total: 0 };
}

const queryTokens = query
.toLowerCase()
.split(/\s+/)
.filter((t) => t.length > 2);

const scored = allMemories
.map((m) => ({
memory: m.content || '',
metadata: m.metadata,
updatedAt: m.createdAt,
similarity: queryTokens.length > 0 ? scoreMemory(m, queryTokens) / queryTokens.length : 0,
}))
.filter((m) => m.similarity > 0)
.sort((a, b) => b.similarity - a.similarity)
.slice(0, limit);

return { results: scored, total: scored.length };
}

async getProfile(containerTag, projectName, maxItems = 5) {
const tag = containerTag || this.containerTag;
const personalDir = containerDir('personal', tag);
const repoDir = containerDir('repo', tag);

const personalMemories = loadAllMemories(personalDir);
const repoMemories = loadAllMemories(repoDir);

// Sort by date descending
const sortByDate = (a, b) =>
new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime();

personalMemories.sort(sortByDate);
repoMemories.sort(sortByDate);

// Static = repo knowledge, Dynamic = recent personal
const staticFacts = repoMemories.slice(0, maxItems).map((m) => m.content);
const dynamicFacts = personalMemories.slice(0, maxItems).map((m) => m.content);

// Recent search results (most recent memories from both)
const allRecent = [...personalMemories, ...repoMemories]
.sort(sortByDate)
.slice(0, maxItems);

const searchResults = {
results: allRecent.map((m) => ({
id: m.id,
memory: m.content,
similarity: 1,
updatedAt: m.createdAt,
})),
total: allRecent.length,
};

return {
profile: { static: staticFacts, dynamic: dynamicFacts },
searchResults,
};
}
}

module.exports = {
LocalMemoryClient,
PERSONAL_ENTITY_CONTEXT,
REPO_ENTITY_CONTEXT,
};
Loading