1109 lines
47 KiB
Markdown
1109 lines
47 KiB
Markdown
# EverMem Plugin for Claude Code
|
||
|
||
Persistent memory for Claude Code. Automatically saves and recalls context from past coding sessions.
|
||
|
||
> Compatibility note: this folder documents a legacy EverMem Cloud
|
||
> plugin. It still uses the old cloud `/api/v1/memories/*` routes and
|
||
> should not be treated as the canonical local EverOS 1.0.0 OSS API.
|
||
> New integrations should follow
|
||
> [EverOS 1.0.0 migration notes](../../docs/migration-to-1.0.0.md) and
|
||
> [the API reference](../../docs/api.md).
|
||
|
||

|
||
|
||
## Features
|
||
|
||
- **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
|
||
|
||
## Quick Install
|
||
|
||
```bash
|
||
curl -fsSL https://raw.githubusercontent.com/EverMind-AI/evermem-claude-code/main/install.sh | bash
|
||
```
|
||
|
||
This will:
|
||
1. Prompt for your EverMem API key
|
||
2. Save it to your shell profile
|
||
3. Install the plugin via Claude Code's plugin system
|
||
|
||
**Get your API key:** [console.evermind.ai](https://console.evermind.ai/)
|
||
|
||
## Manual Installation
|
||
|
||
### 1. Get Your API Key
|
||
|
||
Visit [console.evermind.ai](https://console.evermind.ai/) to create an account and get your API key.
|
||
|
||
### 2. Configure Environment Variable
|
||
|
||
Add to your shell profile (`~/.zshrc` or `~/.bashrc`):
|
||
|
||
```bash
|
||
export EVERMEM_API_KEY="your-api-key-here"
|
||
```
|
||
|
||
Reload your shell:
|
||
|
||
```bash
|
||
source ~/.zshrc # or source ~/.bashrc
|
||
```
|
||
|
||
### 3. Install the Plugin
|
||
|
||
```bash
|
||
# 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 user
|
||
```
|
||
|
||
To update the plugin later:
|
||
|
||
```bash
|
||
claude plugin marketplace update evermem
|
||
claude plugin update evermem@evermem
|
||
```
|
||
|
||
### 4. Verify Installation
|
||
|
||
Run `/evermem:help` to check if the plugin is configured correctly.
|
||
|
||
## Usage
|
||
|
||
### Commands
|
||
|
||
| 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 |
|
||
|
||
### Automatic Behavior
|
||
|
||
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)
|
||
```
|
||
|
||
### Memory Hub
|
||
|
||
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.
|
||
|
||
## Configuration
|
||
|
||
### Environment Variables
|
||
|
||
| Variable | Description | Required |
|
||
|----------|-------------|----------|
|
||
| `EVERMEM_API_KEY` | Your EverMem API key | Yes |
|
||
|
||
### Project-Specific Settings
|
||
|
||
Create `.claude/evermem.local.md` in your project root for per-project configuration:
|
||
|
||
```markdown
|
||
---
|
||
group_id: "my-project"
|
||
---
|
||
|
||
Project-specific notes here.
|
||
```
|
||
|
||
## Troubleshooting
|
||
|
||
### API Key Not Configured
|
||
|
||
```bash
|
||
# 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
|
||
```
|
||
|
||
### No Memories Found
|
||
|
||
1. Memories are only recalled after you've had previous conversations
|
||
2. Short prompts (less than 3 words) are skipped
|
||
3. Check that your API key is valid at [console.evermind.ai](https://console.evermind.ai/)
|
||
|
||
### API Errors
|
||
|
||
- **403 Forbidden**: Invalid or expired API key
|
||
- **502 Bad Gateway**: Server temporarily unavailable, try again
|
||
|
||
### Debug Mode
|
||
|
||
Enable debug logging to troubleshoot issues:
|
||
|
||
```bash
|
||
# Set environment variable
|
||
export EVERMEM_DEBUG=1
|
||
|
||
# View logs in real-time
|
||
tail -f /tmp/evermem-debug.log
|
||
|
||
# Clear logs
|
||
> /tmp/evermem-debug.log
|
||
```
|
||
|
||
Run `/evermem:debug` to view recent debug logs directly.
|
||
|
||
## Links
|
||
|
||
- **Console**: [console.evermind.ai](https://console.evermind.ai/)
|
||
- **API Documentation**: [docs.evermind.ai](https://docs.evermind.ai)
|
||
- **Issues**: [GitHub Issues](https://github.com/EverMind-AI/evermem-claude-code/issues)
|
||
|
||
## License
|
||
|
||
MIT
|
||
|
||
---
|
||
|
||
# Technical Details
|
||
|
||
The following sections explain how EverMem works internally. This is useful for developers who want to understand the implementation or contribute to the project.
|
||
|
||
## How It Works
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 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 │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## Claude Code Hooks Mechanism
|
||
|
||
> Reference: [Claude Code Hooks Documentation](https://docs.anthropic.com/en/docs/claude-code/hooks)
|
||
|
||
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.
|
||
|
||
### How Hooks Work
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 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({ ... })); │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Hook Events
|
||
|
||
| 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 |
|
||
|
||
### Plugin hooks.json Configuration
|
||
|
||
```json
|
||
{
|
||
"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
|
||
|
||
### EverMem Plugin Hooks
|
||
|
||
```json
|
||
{
|
||
"hooks": {
|
||
"SessionStart": [...], // Load session context + track groups locally
|
||
"UserPromptSubmit": [...], // Search & inject memories
|
||
"Stop": [...], // Save conversation to cloud
|
||
"SessionEnd": [...] // Save session summary locally
|
||
}
|
||
}
|
||
```
|
||
|
||
## SessionStart Hook
|
||
|
||
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.
|
||
|
||
### Architecture
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 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 │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Hook Input (stdin)
|
||
|
||
```json
|
||
{
|
||
"session_id": "<session-uuid>",
|
||
"cwd": "/path/to/your/project",
|
||
"permission_mode": "default",
|
||
"hook_event_name": "SessionStart"
|
||
}
|
||
```
|
||
|
||
### Hook Output (stdout)
|
||
|
||
```json
|
||
{
|
||
"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>"
|
||
}
|
||
```
|
||
|
||
### Output Fields
|
||
|
||
| Field | Description |
|
||
|-------|-------------|
|
||
| `continue` | Always `true` - never block session start |
|
||
| `systemMessage` | Displayed to user in terminal |
|
||
| `systemPrompt` | Injected into Claude's context (invisible to user) |
|
||
|
||
### Data Sources
|
||
|
||
The hook combines two data sources:
|
||
|
||
1. **Cloud Memories** - Recent memories from EverMem API (5 most recent)
|
||
2. **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 Handling
|
||
|
||
| 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.
|
||
|
||
### Node.js Version Check
|
||
|
||
The hook requires Node.js 18+ for ES modules support. If an older version is detected:
|
||
|
||
```json
|
||
{
|
||
"continue": true,
|
||
"systemMessage": "⚠️ EverMem: Node.js 16.x is too old. Please upgrade to Node.js 18+."
|
||
}
|
||
```
|
||
|
||
## SessionEnd Hook
|
||
|
||
The SessionEnd hook runs when a Claude Code session ends. It saves a session summary to local storage for use by the SessionStart hook.
|
||
|
||
### Architecture
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 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,...} │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Hook Input (stdin)
|
||
|
||
```json
|
||
{
|
||
"session_id": "<session-uuid>",
|
||
"transcript_path": "~/.claude/projects/<hash>/<session-uuid>.jsonl",
|
||
"cwd": "/path/to/your/project",
|
||
"reason": "user_exit",
|
||
"hook_event_name": "SessionEnd"
|
||
}
|
||
```
|
||
|
||
### Hook Output (stdout)
|
||
|
||
```json
|
||
{
|
||
"systemMessage": "📝 Session saved (5 turns): Implementing JWT authentication for the..."
|
||
}
|
||
```
|
||
|
||
### Session Summary Format
|
||
|
||
Each session is saved as a single line in `data/sessions.jsonl`:
|
||
|
||
```json
|
||
{
|
||
"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"
|
||
}
|
||
```
|
||
|
||
### Fields
|
||
|
||
| 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 |
|
||
|
||
### Deduplication
|
||
|
||
Each session is only saved once. Before saving, the hook checks if the sessionId already exists in `sessions.jsonl`.
|
||
|
||
### No AI Summarization
|
||
|
||
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.
|
||
|
||
### Design Philosophy: Deferred Display Pattern
|
||
|
||
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?**
|
||
|
||
1. **SessionEnd can't display messages** - When a session ends (`/exit`, `Ctrl+D`), the terminal is closing. Any `systemMessage` output would be lost or not visible to the user.
|
||
|
||
2. **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.
|
||
|
||
3. **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)
|
||
|
||
4. **Graceful degradation** - If SessionEnd fails to run (e.g., `Ctrl+C` force 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 |
|
||
|
||
## Local Groups Tracking
|
||
|
||
The SessionStart hook automatically records project groups to `data/groups.jsonl` (JSONL format):
|
||
|
||
```jsonl
|
||
{"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 accounts
|
||
- `groupId`: Unique identifier based on working directory, format: `claude-code:{path}`
|
||
- `name`: Project folder name
|
||
- `path`: Full path to the project
|
||
- `timestamp`: When the group was first recorded
|
||
|
||
**Deduplication:** Each `keyId + groupId` combination is stored only once (no duplicates).
|
||
|
||
View tracked projects with `/evermem:projects` command.
|
||
|
||
## Stop Hook: Conversation Flow
|
||
|
||
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.
|
||
|
||
### Hook Input
|
||
|
||
When Claude finishes responding, the Stop hook receives input like this:
|
||
|
||
```json
|
||
{
|
||
"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
|
||
}
|
||
```
|
||
|
||
### Transcript File Format
|
||
|
||
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:
|
||
|
||
```jsonl
|
||
// 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)
|
||
```
|
||
|
||
### Turn Boundary & Segmentation
|
||
|
||
**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):**
|
||
```json
|
||
{"type":"system","subtype":"turn_duration","durationMs":30692}
|
||
```
|
||
|
||
> **Note:** `file-history-snapshot` is 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
|
||
```
|
||
|
||
### Memory Extraction
|
||
|
||
The `store-memories.js` hook extracts the **last complete Turn**:
|
||
|
||
1. **Wait for completion** - Retry reading file until `turn_duration` marker appears (indicates turn is complete)
|
||
2. **Find turn boundaries** - Start after last `turn_duration`, end at current `turn_duration`
|
||
- **ONLY** `turn_duration` is used as boundary (NOT `file-history-snapshot`)
|
||
3. **Collect user text** - Original input only (skip `tool_result`)
|
||
4. **Collect assistant text** - All `text` blocks (skip `thinking`, `tool_use`)
|
||
5. **Merge content** - Join scattered text blocks with `\n\n` separator
|
||
6. **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:
|
||
|
||
```javascript
|
||
// 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.
|
||
|
||
### API Upload
|
||
|
||
Each message is sent to `POST /api/v1/memories/group` (or `POST /api/v1/memories` for personal memories):
|
||
|
||
```json
|
||
{
|
||
"group_id": "claude-code:/path/to/project",
|
||
"messages": [
|
||
{
|
||
"sender_id": "claude-code-user",
|
||
"role": "user",
|
||
"timestamp": 1770367656189,
|
||
"content": "How do I add authentication?"
|
||
}
|
||
],
|
||
"async_mode": true
|
||
}
|
||
```
|
||
|
||
Response on success:
|
||
```json
|
||
{
|
||
"message": "Message accepted and queued for processing",
|
||
"request_id": "<request-uuid>",
|
||
"status": "queued"
|
||
}
|
||
```
|
||
|
||
### Hook Output (stdout)
|
||
|
||
The hook returns JSON via stdout to communicate with Claude Code:
|
||
|
||
```json
|
||
{
|
||
"systemMessage": "💾 Memory saved (2) [user: 59, assistant: 127]"
|
||
}
|
||
```
|
||
|
||
This message is displayed to the user after Claude finishes responding.
|
||
|
||
## Memory Hub Implementation
|
||
|
||
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.
|
||
|
||
### Architecture
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
│ /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/v1/memories/get → 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/v1/memories/search → Forward to EverMind API │ │
|
||
│ │ POST /api/v1/memories/get → Forward to EverMind API │ │
|
||
│ │ GET /health → Health check │ │
|
||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Why Proxy? │
|
||
│ - The proxy forwards browser calls to the EverMind API and serves the │
|
||
│ dashboard. │
|
||
└─────────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
│ EverMem Cloud API │
|
||
│ https://api.evermind.ai │
|
||
│ │
|
||
│ POST /api/v1/memories/search { query, filters, method, memory_types, top_k } │
|
||
│ POST /api/v1/memories/get { memory_type, filters, page, page_size, ... } │
|
||
│ Response: { data: { episodes[], total_count, count } } │
|
||
└─────────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Proxy Server (`server/proxy.js`)
|
||
|
||
```javascript
|
||
// 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 requests to EverMind API
|
||
// Browser sends: POST /api/v1/memories/search { query, filters, ... }
|
||
// Browser sends: POST /api/v1/memories/get { memory_type, filters, ... }
|
||
// Proxy forwards to upstream API with Authorization header
|
||
```
|
||
|
||
### Dashboard (`dashboard/dashboard.html`)
|
||
|
||
**Data Loading Flow:**
|
||
|
||
```javascript
|
||
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/v1/memories/get', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
memory_type: 'episodic_memory',
|
||
filters: { group_id: group.id },
|
||
page: 1,
|
||
page_size: 100,
|
||
rank_by: 'timestamp',
|
||
rank_order: 'desc'
|
||
})
|
||
});
|
||
|
||
// 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)]
|
||
```
|
||
|
||
## Debug Logging
|
||
|
||
Both `inject-memories.js` and `store-memories.js` use a shared debug utility:
|
||
|
||
```javascript
|
||
import { debug, setDebugPrefix } from './utils/debug.js';
|
||
|
||
setDebugPrefix('inject'); // Log lines will show [inject] prefix
|
||
debug('hookInput:', data); // Only writes when EVERMEM_DEBUG=1
|
||
```
|
||
|
||
**Debug 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:**
|
||
|
||
```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:**
|
||
|
||
```javascript
|
||
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
|
||
}
|
||
```
|
||
|
||
## Project Structure
|
||
|
||
```
|
||
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
|
||
├── dashboard/
|
||
│ └── dashboard.html # Memory Hub dashboard
|
||
├── server/
|
||
│ └── proxy.js # Local proxy server for dashboard
|
||
└── README.md
|
||
```
|
||
|
||
## API Reference
|
||
|
||
The plugin uses the EverMem Cloud API at `https://api.evermind.ai`:
|
||
|
||
- `POST /api/v1/memories/group` - Store a new memory (group); `POST /api/v1/memories` for personal memories
|
||
- `POST /api/v1/memories/search` - Search memories (hybrid retrieval, with JSON body)
|
||
- `POST /api/v1/memories/get` - Get memories (list with JSON body)
|
||
|
||
## Development
|
||
|
||
### Local Development
|
||
|
||
```bash
|
||
# 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 .
|
||
```
|
||
|
||
### Testing Hooks
|
||
|
||
```bash
|
||
# 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
|
||
```
|