Persistent memory for Claude Code. Automatically saves and recalls context from past coding sessions.
- Automatic Memory Save - Conversations are saved when Claude finishes responding
- Automatic Memory Retrieval - Relevant memories are retrieved when you submit a prompt
- Session Context - Recent work summary loaded on session start
- Memory Search - Manually search your memory history
- Memory Hub - Visual dashboard to explore and manage memories
curl -fsSL https://raw.githubusercontent.com/EverMind-AI/evermem-claude-code/main/install.sh | bashThis will:
- Prompt for your EverMem API key
- Save it to your shell profile
- Install the plugin via Claude Code's plugin system
Get your API key: console.evermind.ai
Visit console.evermind.ai to create an account and get your API key.
Add to your shell profile (~/.zshrc or ~/.bashrc):
export EVERMEM_API_KEY="your-api-key-here"Reload your shell:
source ~/.zshrc # or source ~/.bashrc# Add marketplace from GitHub (tracks updates automatically)
claude plugin marketplace add https://github.com/EverMind-AI/evermem-claude-code
# Install the plugin
claude plugin install evermem@evermem --scope userTo update the plugin later:
claude plugin marketplace update evermem
claude plugin update evermem@evermemRun /evermem:help to check if the plugin is configured correctly.
| Command | Description |
|---|---|
/evermem:help |
Show setup status and available commands |
/evermem:search <query> |
Search your memories for specific topics |
/evermem:ask <question> |
Ask about past work (combines memory + context) |
/evermem:hub |
Open the Memory Hub dashboard |
/evermem:debug |
View debug logs for troubleshooting |
/evermem:projects |
View your Claude Code projects table |
The plugin works automatically in the background:
On Session Start:
💡 EverMem: Last session (2h ago): "Implementing JWT authentication..." | 3 memories
Recent memories and last session summary are loaded to provide context.
On Prompt Submit:
You: "How should I handle authentication?"
↓
📝 Memory Retrieved (2):
• [0.85] (2 days ago) Discussion about JWT token implementation
• [0.72] (1 week ago) Auth middleware setup decisions
↓
Claude receives the relevant context and responds accordingly
On Response Complete:
💾 EverMem: Memory saved (4 messages)
The Memory Hub provides a visual interface to explore your memories:
- Activity heatmap (GitHub-style, 6 months)
- Memory statistics (Total, Projects, Active Days, Avg/Day, Avg/Project)
- Last 7 Days growth chart
- Project-based memory grouping with expandable cards
- Timeline view within each project (grouped by date)
- Load more pagination for large projects
To use the hub, run /evermem:hub and follow the instructions.
| Variable | Description | Required |
|---|---|---|
EVERMEM_API_KEY |
Your EverMem API key | Yes |
Create .claude/evermem.local.md in your project root for per-project configuration:
---
group_id: "my-project"
---
Project-specific notes here.# Check if the key is set
echo $EVERMEM_API_KEY
# If empty, add to your shell profile and reload
export EVERMEM_API_KEY="your-key-here"
source ~/.zshrc- Memories are only recalled after you've had previous conversations
- Short prompts (less than 3 words) are skipped
- Check that your API key is valid at console.evermind.ai
- 403 Forbidden: Invalid or expired API key
- 502 Bad Gateway: Server temporarily unavailable, try again
Enable debug logging to troubleshoot issues:
# Set environment variable
export EVERMEM_DEBUG=1
# View logs in real-time
tail -f /tmp/evermem-debug.log
# Clear logs
> /tmp/evermem-debug.logRun /evermem:debug to view recent debug logs directly.
- Console: console.evermind.ai
- API Documentation: docs.evermind.ai
- Issues: GitHub Issues
MIT
The following sections explain how EverMem works internally. This is useful for developers who want to understand the implementation or contribute to the project.
┌─────────────────────────────────────────────────────────────┐
│ Session Start │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ SessionStart Hook │
│ • Fetches recent memories from EverMem Cloud │
│ • Loads last session summary from local storage │
│ • Injects session context into Claude's prompt │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Your Prompt │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ UserPromptSubmit Hook │
│ • Searches EverMem Cloud for relevant memories │
│ • Displays memory summary to user │
│ • Injects context into Claude's prompt │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Claude Response │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Stop Hook │
│ • Extracts conversation from transcript │
│ • Sends to EverMem Cloud for storage │
│ • Server generates summary and stores memory │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Session End │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ SessionEnd Hook │
│ • Parses transcript to extract first user prompt │
│ • Saves session summary to local storage │
│ • No AI calls - pure local data extraction │
└─────────────────────────────────────────────────────────────┘
Reference: Claude Code Hooks Documentation
Claude Code provides a hooks system that allows plugins to execute custom scripts at specific lifecycle events. Hooks are event-driven - they don't run continuously but are triggered by Claude Code at specific moments.
┌─────────────────────────────────────────────────────────────────┐
│ Claude Code (Main Process) │
│ │
│ 1. Event occurs (e.g., user sends message, Claude responds) │
│ 2. Claude Code reads hooks.json │
│ 3. Finds matching hooks for the event │
│ 4. Spawns child process: node <script.js> │
│ 5. Sends JSON data via stdin pipe ─────────────┐ │
│ 6. Reads response from stdout │ │
└─────────────────────────────────────────────────│───────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Hook Script (Child Process) │
│ │
│ // Read JSON from stdin (sent by Claude Code) │
│ let input = ''; │
│ for await (const chunk of process.stdin) { │
│ input += chunk; │
│ } │
│ const hookInput = JSON.parse(input); │
│ │
│ // Process and return result via stdout │
│ console.log(JSON.stringify({ ... })); │
└─────────────────────────────────────────────────────────────────┘
| Event | Trigger | Use Case |
|---|---|---|
SessionStart |
Claude Code starts | Load context, setup environment |
UserPromptSubmit |
User sends a message | Validate prompt, inject context |
PreToolUse |
Before tool execution | Approve/deny/modify tool calls |
PostToolUse |
After tool execution | Validate results, run linters |
Stop |
Claude finishes responding | Save conversation, cleanup |
Notification |
System notification | Custom alerts |
{
"hooks": {
"EventName": [
{
"matcher": "*", // Pattern to match (for tool events)
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/my-hook.js",
"timeout": 30 // Timeout in seconds
}
]
}
]
}
}Environment Variables:
${CLAUDE_PLUGIN_ROOT}- Plugin directory path (for plugins)${CLAUDE_PROJECT_DIR}- Project root directory
{
"hooks": {
"SessionStart": [...], // Load session context + track groups locally
"UserPromptSubmit": [...], // Search & inject memories
"Stop": [...], // Save conversation to cloud
"SessionEnd": [...] // Save session summary locally
}
}The SessionStart hook runs when Claude Code starts a new session. It loads recent memories from the cloud and last session summary from local storage.
┌─────────────────────────────────────────────────────────────────┐
│ Claude Code Session Start │
│ │
│ 1. Claude Code spawns: session-context-wrapper.sh │
│ 2. Wrapper checks npm dependencies │
│ 3. Wrapper executes: node session-context.js │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ session-context.js │
│ │
│ 1. Read hook input from stdin (contains cwd) │
│ 2. Save group to local storage (groups.jsonl) │
│ 3. Fetch recent memories from EverMem API (limit: 100) │
│ 4. Take the 5 most recent memories │
│ 5. Get last session summary from sessions.jsonl │
│ 6. Output systemMessage + systemPrompt via stdout │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Claude Code Receives │
│ │
│ • systemMessage: "💡 EverMem: Last session (2h ago): \"...\" | 5 memories"│
│ • systemPrompt: <session-context>...</session-context> │
│ │
│ The systemPrompt is injected into Claude's context window │
└─────────────────────────────────────────────────────────────────┘
{
"session_id": "<session-uuid>",
"cwd": "/path/to/your/project",
"permission_mode": "default",
"hook_event_name": "SessionStart"
}{
"continue": true,
"systemMessage": "💡 EverMem: Last session (2h ago): \"Implementing JWT authentication...\" | 5 memories",
"systemPrompt": "<session-context>\nLast session (2h ago, 5 turns): Implementing JWT authentication for the API\n\nRecent memories (5):\n\n[1] (2/9/2026) JWT token implementation\n...\n</session-context>"
}| Field | Description |
|---|---|
continue |
Always true - never block session start |
systemMessage |
Displayed to user in terminal |
systemPrompt |
Injected into Claude's context (invisible to user) |
The hook combines two data sources:
- Cloud Memories - Recent memories from EverMem API (5 most recent)
- Local Session Summary - Last session from
data/sessions.jsonl(saved by SessionEnd hook)
No AI summarization is used - pure local data extraction for zero latency and no additional API costs.
| Error Type | User Message |
|---|---|
| Network error | "Cannot reach EverMem server. Check your internet connection." |
| Timeout | "EverMem server is slow or unreachable." |
| 401/Unauthorized | "Authentication failed. Check your EVERMEM_API_KEY." |
| 404 | "API endpoint not found. Check EVERMEM_BASE_URL." |
| Module not found | "Missing dependency. Run: npm install" |
All errors return continue: true to ensure session starts normally.
The hook requires Node.js 18+ for ES modules support. If an older version is detected:
{
"continue": true,
"systemMessage": "⚠️ EverMem: Node.js 16.x is too old. Please upgrade to Node.js 18+."
}The SessionEnd hook runs when a Claude Code session ends. It saves a session summary to local storage for use by the SessionStart hook.
┌─────────────────────────────────────────────────────────────────┐
│ Claude Code Session End │
│ │
│ Triggers: /exit, closing terminal, idle timeout │
│ Claude Code spawns: node session-summary.js │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ session-summary.js │
│ │
│ 1. Read hook input from stdin (contains transcript_path) │
│ 2. Check if session already summarized (skip if yes) │
│ 3. Parse transcript JSONL file │
│ 4. Extract: first user prompt, turn count, timestamps │
│ 5. Save to data/sessions.jsonl │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Local Storage │
│ │
│ data/sessions.jsonl: │
│ {"sessionId":"abc","groupId":"...","summary":"First user │
│ prompt truncated to 200 chars","turnCount":5,...} │
└─────────────────────────────────────────────────────────────────┘
{
"session_id": "<session-uuid>",
"transcript_path": "~/.claude/projects/<hash>/<session-uuid>.jsonl",
"cwd": "/path/to/your/project",
"reason": "user_exit",
"hook_event_name": "SessionEnd"
}{
"systemMessage": "📝 Session saved (5 turns): Implementing JWT authentication for the..."
}Each session is saved as a single line in data/sessions.jsonl:
{
"sessionId": "<session-uuid>",
"groupId": "claude-code:/path/to/project",
"summary": "First user prompt truncated to 200 characters",
"turnCount": 5,
"reason": "user_exit",
"startTime": "2026-02-09T10:00:00.000Z",
"endTime": "2026-02-09T10:30:00.000Z",
"timestamp": "2026-02-09T10:30:05.000Z"
}| Field | Description |
|---|---|
sessionId |
Unique session identifier (from Claude Code) |
groupId |
Project identifier (based on working directory) |
summary |
First user prompt (truncated to 200 chars) |
turnCount |
Number of conversation turns |
reason |
Why session ended (user_exit, idle_timeout, etc.) |
startTime |
First message timestamp |
endTime |
Last message timestamp |
timestamp |
When summary was saved |
Each session is only saved once. Before saving, the hook checks if the sessionId already exists in sessions.jsonl.
The SessionEnd hook uses a simple approach: the first user prompt becomes the session summary. This provides:
- Zero latency - No API calls needed
- Zero cost - No Haiku or other model usage
- Reliability - Works offline, no external dependencies
The first user prompt typically describes what the user wanted to accomplish, making it a natural summary of the session's purpose.
The SessionEnd and SessionStart hooks work together using a "save now, display later" pattern:
┌─────────────────────────────────────────────────────────────────┐
│ Session A (ending) │
│ │
│ SessionEnd Hook: │
│ • Extracts first user prompt, turn count, duration │
│ • Saves to sessions.jsonl │
│ • Output NOT displayed (session already closed) │
└─────────────────────────────────────────────────────────────────┘
│
│ sessions.jsonl (local storage)
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Session B (starting) │
│ │
│ SessionStart Hook: │
│ • Reads last session from sessions.jsonl │
│ • Displays: "Last (2h ago, 5 turns): Your question..." │
│ • Provides continuity across sessions │
└─────────────────────────────────────────────────────────────────┘
Why this design?
-
SessionEnd can't display messages - When a session ends (
/exit,Ctrl+D), the terminal is closing. AnysystemMessageoutput would be lost or not visible to the user. -
SessionStart is the right moment - The next time the user opens Claude Code, they see what they were working on. This creates a natural "welcome back" experience.
-
Local-first architecture - Session summaries are stored locally in
sessions.jsonl, not in the cloud. This ensures:- Instant access (no API latency)
- Works offline
- No additional API costs
- Privacy (session data stays on your machine)
-
Graceful degradation - If SessionEnd fails to run (e.g.,
Ctrl+Cforce quit), the next SessionStart still works with cloud memories. No single point of failure.
Data Flow Summary:
| Event | Action | Storage | Display |
|---|---|---|---|
| SessionEnd | Save summary | Local (sessions.jsonl) | None |
| SessionStart | Read summary | Local + Cloud | Yes |
The SessionStart hook automatically records project groups to data/groups.jsonl (JSONL format):
{"keyId":"9a823d2f8ea5","groupId":"claude-code:/path/to/project-a","name":"project-a","path":"/path/to/project-a","timestamp":"2026-02-09T06:00:00Z"}
{"keyId":"9a823d2f8ea5","groupId":"claude-code:/path/to/api-server","name":"api-server","path":"/path/to/api-server","timestamp":"2026-02-09T08:00:00Z"}Fields:
keyId: SHA-256 hash (first 12 chars) of the API key - associates groups with accountsgroupId: Unique identifier based on working directory, format:claude-code:{path}name: Project folder namepath: Full path to the projecttimestamp: When the group was first recorded
Deduplication: Each keyId + groupId combination is stored only once (no duplicates).
View tracked projects with /evermem:projects command.
Claude Code stores all conversations locally in JSONL (JSON Lines) format. The EverMem plugin reads this transcript and uploads the latest Q&A pair to the cloud.
When Claude finishes responding, the Stop hook receives input like this:
{
"session_id": "<session-uuid>",
"transcript_path": "~/.claude/projects/<project-hash>/<session-uuid>.jsonl",
"cwd": "/path/to/your/project",
"permission_mode": "default",
"hook_event_name": "Stop",
"stop_hook_active": false
}The transcript file (*.jsonl) contains one JSON object per line, recording every message and event in the session. Important: A single Claude response may span multiple lines with different content types.
Common Fields:
| Field | Description |
|---|---|
type |
Line type: user, assistant, progress, system, file-history-snapshot |
uuid |
Unique message ID |
parentUuid |
Parent message ID (for threading) |
timestamp |
ISO 8601 timestamp |
sessionId |
Session UUID |
message.role |
user or assistant |
message.content |
String or array of content blocks |
Content Block Types (in message.content array):
| Type | Description |
|---|---|
text |
Final text response to user |
thinking |
Claude's internal reasoning (extended thinking) |
tool_use |
Tool invocation (Read, Write, Bash, etc.) |
tool_result |
Result returned from tool execution |
Complete Conversation Example:
A single Q&A turn generates multiple JSONL lines:
// 1. User message
{"type":"user","message":{"role":"user","content":"debug.js 如何使用"},"uuid":"696034a3-...","timestamp":"2026-02-09T02:20:16.540Z"}
// 2. Assistant thinking (extended thinking mode)
{"type":"assistant","message":{"role":"assistant","content":[{"type":"thinking","thinking":"用户希望了解 debug.js 的使用方法...","signature":"EuAC..."}]},"uuid":"b375ff09-...","timestamp":"2026-02-09T02:20:26.866Z"}
// 3. Assistant tool use (e.g., Read file)
{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_01Qur8BnkKD9t53JSSorDLbm","name":"Read","input":{"file_path":"/path/to/README.md"}}]},"uuid":"f01ec15c-..."}
// 4. Progress event (hook execution)
{"type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read"},"uuid":"f4219b83-..."}
// 5. Tool result (returned as user message)
{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Qur8BnkKD9t53JSSorDLbm","type":"tool_result","content":"file contents here..."}]},"uuid":"f5c5f7c6-..."}
// 6. Assistant final text response
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"完成!README 已更新..."}]},"uuid":"cae1b79c-..."}
// 7. System events (stop hook, timing)
{"type":"system","subtype":"stop_hook_summary","hookCount":1,"hasOutput":true,"uuid":"25a25edf-..."}
{"type":"system","subtype":"turn_duration","durationMs":81371,"uuid":"55418b2c-..."}Simplified View:
User Input
↓
[thinking] → [tool_use] → [tool_result] → [tool_use] → ... → [text]
↓
System Events (hooks, timing)
Session Level: One JSONL file = One Session (filename is session ID)
Turn Level: A "Turn" = User sends message → Claude fully responds
Turn boundary marker (ONLY this one):
{"type":"system","subtype":"turn_duration","durationMs":30692}Note:
file-history-snapshotis NOT a turn boundary. It's a session-level marker that can appear anywhere in the file.
JSONL Structure:
Line 1: file-history-snapshot ← Session marker (NOT turn boundary)
Line 2-21: Turn 1
Line 22: turn_duration ← Turn 1 end ✓
Line 23: file-history-snapshot ← Can appear mid-session (NOT turn boundary)
Line 24-43: Turn 2
Line 44: turn_duration ← Turn 2 end ✓
...
Message Chain (parentUuid):
user (uuid: aaa, parent: None) ← Turn start
↓
assistant/thinking (parent: aaa)
↓
assistant/tool_use (parent: ...)
↓
user/tool_result (parent: ...) ← NOT user input, skip!
↓
assistant/text (parent: ...) ← Final response
↓
system/turn_duration (parent: ...) ← Turn end
The store-memories.js hook extracts the last complete Turn:
- Wait for completion - Retry reading file until
turn_durationmarker appears (indicates turn is complete) - Find turn boundaries - Start after last
turn_duration, end at currentturn_duration- ONLY
turn_durationis used as boundary (NOTfile-history-snapshot)
- ONLY
- Collect user text - Original input only (skip
tool_result) - Collect assistant text - All
textblocks (skipthinking,tool_use) - Merge content - Join scattered text blocks with
\n\nseparator - Upload to cloud - Send both user and assistant content to EverMem API
Race Condition Handling:
The Stop hook runs before turn_duration is written. To ensure complete content extraction:
// Retry until turn_duration appears (max 5 attempts, 100ms delay)
async function readTranscriptWithRetry(path) {
for (let attempt = 1; attempt <= 5; attempt++) {
const lines = readFile(path);
const lastLine = JSON.parse(lines[lines.length - 1]);
// turn_duration = turn complete
if (lastLine.type === 'system' && lastLine.subtype === 'turn_duration') {
return lines;
}
await sleep(100); // Wait and retry
}
}Why merge? A single Claude response spans multiple JSONL lines:
thinking→tool_use→tool_result→ ... →text(final response)
The hook merges all text blocks to capture the complete response.
Each message is sent to POST /api/v0/memories:
{
"message_id": "u_1770367656189",
"create_time": "2026-02-06T08:47:36.189Z",
"sender": "claude-code-user",
"role": "user",
"content": "How do I add authentication?",
"group_id": "claude-code:/path/to/project",
"group_name": "Claude Code Session"
}Response on success:
{
"message": "Message accepted and queued for processing",
"request_id": "<request-uuid>",
"status": "queued"
}The hook returns JSON via stdout to communicate with Claude Code:
{
"systemMessage": "💾 Memory saved (2) [user: 59, assistant: 127]"
}This message is displayed to the user after Claude finishes responding.
The /evermem:hub command opens a web dashboard for visualizing memories. Due to browser limitations (GET requests can't have body), a local proxy server bridges the dashboard and EverMem API.
┌─────────────────────────────────────────────────────────────────────────────┐
│ /evermem:hub Command │
│ 1. Start proxy server: node server/proxy.js & │
│ 2. Generate URL: http://localhost:3456/?key=${EVERMEM_API_KEY} │
│ 3. User opens URL in browser │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Browser (dashboard.html) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Stats │ │ Heatmap │ │ 7-Day │ │ Project │ │
│ │ Cards │ │ (6 months) │ │ Chart │ │ Cards │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ Data Flow: │
│ 1. GET /api/groups → Local groups.jsonl (filtered by keyId) │
│ 2. For each group: POST /api/v0/memories → Fetch memories │
│ 3. Render dashboard with aggregated data │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Proxy Server (localhost:3456) │
│ │
│ Routes: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ GET / → Serve dashboard.html │ │
│ │ GET /api/groups → Read groups.jsonl, filter by keyId │ │
│ │ POST /api/v0/memories → Convert to GET+body, forward to API │ │
│ │ GET /health → Health check │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Why Proxy? │
│ - Browser limitation: GET requests can't have body │
│ - EverMem API uses GET /api/v0/memories with JSON body │
│ - Proxy receives POST, converts to GET+body using curl │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ EverMem Cloud API │
│ https://api.evermind.ai │
│ │
│ GET /api/v0/memories (with body) │
│ Request: { user_id, group_id, memory_type, limit, offset } │
│ Response: { result: { memories[], total_count, has_more } } │
└─────────────────────────────────────────────────────────────────────────────┘
// Key function: Convert API key to keyId (for groups filtering)
function computeKeyId(apiKey) {
const hash = createHash('sha256').update(apiKey).digest('hex');
return hash.substring(0, 12); // First 12 chars of SHA-256
}
// Key function: Read groups.jsonl and filter by keyId
function getGroupsForKey(keyId) {
const content = readFileSync(GROUPS_FILE, 'utf8');
const lines = content.trim().split('\n');
const groupMap = new Map();
for (const line of lines) {
const entry = JSON.parse(line);
if (entry.keyId !== keyId) continue; // Filter by current API key
// Aggregate: count sessions, track first/last seen
// ...
}
return Array.from(groupMap.values());
}
// Key route: Forward POST as GET+body (browser workaround)
// Browser sends: POST /api/v0/memories { body }
// Proxy sends: GET /api/v0/memories { body } via curlData Loading Flow:
async function loadGroups() {
// 1. Fetch groups from local storage (via proxy)
const groupsData = await fetch('/api/groups', {
headers: { 'Authorization': `Bearer ${apiKey}` }
});
// 2. For each group, fetch memories with pagination
for (const group of groups) {
const data = await fetch('/api/v0/memories', {
method: 'POST',
body: JSON.stringify({
user_id: 'claude-code-user',
group_id: group.id,
memory_type: 'episodic_memory',
limit: 100,
offset: 0
})
});
// Store: memories[], totalCount, hasMore, offset
groupMemories[group.id] = { ... };
}
// 3. Render dashboard
renderDashboard(totalMemories);
}UI Components:
| Component | Description |
|---|---|
| Stats Grid | 5 cards: Total Memories, Projects, Active Days, Avg/Day, Avg/Project |
| Heatmap | GitHub-style 6-month activity grid with tooltips |
| Growth Chart | Last 7 days bar chart |
| Project Cards | Expandable cards showing memories per project |
| Timeline | Within each project, memories grouped by date |
| Load More | Pagination button when has_more: true |
Timeline within Project:
📁 evermem-claude-code (25 memories)
├── ● Sun, Feb 9 [Today] 3 memories
│ ├── 💭 Discussion about JWT... 10:30 AM
│ ├── 🔧 Fixed authentication... 09:15 AM
│ └── ✨ Created new API endpoint 08:00 AM
│
├── ● Sat, Feb 8 5 memories
│ ├── 📝 Updated README... 16:20 PM
│ └── ...
│
└── [Load more (17 remaining)]
Both inject-memories.js and store-memories.js use a shared debug utility:
import { debug, setDebugPrefix } from './utils/debug.js';
setDebugPrefix('inject'); // Log lines will show [inject] prefix
debug('hookInput:', data); // Only writes when EVERMEM_DEBUG=1Debug output by script:
| Script | Prefix | Debug Points |
|---|---|---|
inject-memories.js |
[inject] |
hookInput, search query, search results, filtered/selected memories, output |
store-memories.js |
[store] |
hookInput, read attempts, turn range, line types, extracted content, results |
Example debug log:
# Memory injection (UserPromptSubmit hook)
[2026-02-06T08:47:30.100Z] [inject] hookInput: { "prompt": "How do I add auth?", ... }
[2026-02-06T08:47:30.150Z] [inject] searching memories for prompt: How do I add auth?
[2026-02-06T08:47:30.500Z] [inject] search results: {"total": 5, "memories": [...]}
[2026-02-06T08:47:30.520Z] [inject] selected memories: [{"score": 0.85, "subject": "JWT implementation"}]
# Memory storage (Stop hook)
[2026-02-06T08:47:36.184Z] [store] hookInput: { "transcript_path": "...jsonl", ... }
# Retry logic - waiting for turn_duration
[2026-02-06T08:47:36.200Z] [store] read attempt 1: { "totalLines": 525, "isComplete": false, "lastLineType": "progress" }
[2026-02-06T08:47:36.201Z] [store] turn not complete, waiting 100ms before retry...
[2026-02-06T08:47:36.310Z] [store] read attempt 2: { "totalLines": 527, "isComplete": false, "lastLineType": "system/stop_hook_summary" }
[2026-02-06T08:47:36.311Z] [store] turn not complete, waiting 100ms before retry...
[2026-02-06T08:47:36.420Z] [store] read attempt 3: { "totalLines": 528, "isComplete": true, "lastLineType": "system/turn_duration" }
# Content extraction
[2026-02-06T08:47:36.425Z] [store] turn range: { "turnStartIndex": 500, "turnEndIndex": 528, "totalLines": 528 }
[2026-02-06T08:47:36.430Z] [store] assistantTexts count: 3
[2026-02-06T08:47:36.435Z] [store] extracted: { "userLength": 59, "assistantLength": 847, ... }
# API upload results
[2026-02-06T08:47:36.970Z] [store] results: [
{
"type": "USER",
"len": 59,
"status": 202,
"ok": true,
"response": {
"message": "Message accepted and queued for processing",
"status": "queued"
}
},
{
"type": "ASSISTANT",
"len": 127,
"status": 202,
"ok": true,
"response": { ... }
}
]
[2026-02-06T08:47:36.975Z] [store] skipped: []
Using debug.js in your own hooks:
import { debug, setDebugPrefix, isDebugEnabled } from './utils/debug.js';
// Set prefix to identify your script in logs
setDebugPrefix('my-hook');
// Log objects (auto JSON stringified) or strings
debug('processing:', { key: 'value' });
// Check if debug is enabled
if (isDebugEnabled()) {
// expensive debug operations
}evermem-plugin/
├── plugin.json # Plugin manifest
├── commands/
│ ├── help.md # /evermem:help command
│ ├── search.md # /evermem:search command
│ ├── hub.md # /evermem:hub command
│ ├── debug.md # /evermem:debug command
│ └── projects.md # /evermem:projects command
├── data/
│ ├── groups.jsonl # Local storage for tracked projects (JSONL format)
│ └── sessions.jsonl # Local storage for session summaries (JSONL format)
├── hooks/
│ ├── hooks.json # Hook configuration
│ └── scripts/
│ ├── inject-memories.js # Memory recall (UserPromptSubmit)
│ ├── store-memories.js # Memory save (Stop)
│ ├── session-context.js # Session context (SessionStart)
│ ├── session-summary.js # Session summary (SessionEnd)
│ └── utils/
│ ├── evermem-api.js # EverMem Cloud API client
│ ├── config.js # Configuration utilities
│ ├── debug.js # Shared debug logging utility
│ └── groups-store.js # Local groups persistence
├── assets/
│ └── dashboard.html # Memory Hub dashboard
├── server/
│ └── proxy.js # Local proxy server for dashboard
└── README.md
The plugin uses the EverMem Cloud API at https://api.evermind.ai:
POST /api/v0/memories- Store a new memoryGET /api/v0/memories/search- Search memories (hybrid retrieval, with JSON body)GET /api/v0/memories- Get memories (with query params)
# Clone the repository
git clone https://github.com/EverMind-AI/evermem-claude-code.git
cd evermem-claude-code
# Install dependencies
npm install
# Run Claude Code with local plugin
claude --plugin-dir .# Test memory recall
echo '{"prompt":"How do I handle authentication?"}' | node hooks/scripts/inject-memories.js
# Test memory save (requires transcript file)
echo '{"transcript_path":"/path/to/transcript.json"}' | node hooks/scripts/store-memories.js