Compare commits
159 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7b9f68d80 | |||
| c48290a156 | |||
| 226a52f8b8 | |||
| 93b5f80168 | |||
| f6cf895847 | |||
| dba29de2a7 | |||
| c16f453017 | |||
| 8b4c962e62 | |||
| 12149470a2 | |||
| fd4cd0444c | |||
| 0adbf38c39 | |||
| 15362d1aed | |||
| c04e5c571c | |||
| 66a69f3044 | |||
| e50c6142bb | |||
| 8d5bfec90e | |||
| ab2c44069b | |||
| babb375955 | |||
| 334fbdd913 | |||
| 4a269183a3 | |||
| 37cd8b8328 | |||
| 33d2422faa | |||
| dad3a104b4 | |||
| bcad4c484d | |||
| 4284b31f42 | |||
| f556546994 | |||
| 6441d9103c | |||
| 2952b7fb8d | |||
| 63f930a433 | |||
| 3cb003f4e4 | |||
| 8b5e5efeb5 | |||
| a57aba82a4 | |||
| 7b7a53ee19 | |||
| e57bfc4187 | |||
| 3cfdd5fd65 | |||
| 1a226b08f0 | |||
| 15b5176fe6 | |||
| 418dc963b2 | |||
| 2314362028 | |||
| f373b682dd | |||
| 1c25c3a7e7 | |||
| 3ca5e33b4a | |||
| 3042de1093 | |||
| f7f62ef3f3 | |||
| 726f167ebf | |||
| a62887a6e0 | |||
| 635cc87462 | |||
| c27f07023c | |||
| e073c0b75f | |||
| 562194612e | |||
| 02bce8a6e6 | |||
| e9bcb7e9db | |||
| 86214b93a9 | |||
| ef572ec032 | |||
| 3cb99ab7f4 | |||
| 20136121d8 | |||
| df0f3fd96c | |||
| fe861a85bd | |||
| 32f45a1100 | |||
| 1ea692e0b0 | |||
| 861cd20adf | |||
| d275715974 | |||
| fd4684dcb3 | |||
| c54e50ec0c | |||
| c42444e06c | |||
| cb2f2a0432 | |||
| 692bb07dfe | |||
| 58e9dcc0fc | |||
| f60ee3b4f5 | |||
| 7ed166323c | |||
| 651989c423 | |||
| 9dd8b180ac | |||
| 9b1d801a46 | |||
| 6fb9570291 | |||
| ae90b26995 | |||
| 3b0cd0b3f6 | |||
| f849a69506 | |||
| daf368e343 | |||
| cbd43240c7 | |||
| 47c1398ce7 | |||
| b44359c136 | |||
| be0ab12789 | |||
| 7ff611feb5 | |||
| cf9d1d4a0b | |||
| 9149acf4d1 | |||
| 002f7a94b8 | |||
| 6d68fd44ca | |||
| bc41e367c0 | |||
| c41c4b21ea | |||
| 5dd663730b | |||
| 56167c47a2 | |||
| 115270c35e | |||
| c27682c799 | |||
| 1fbba55aa3 | |||
| 05f3889deb | |||
| 874815770a | |||
| d452913487 | |||
| 635f456ecf | |||
| 81101ef1a6 | |||
| 938eb9dc0e | |||
| a11199a527 | |||
| 015b38c763 | |||
| be936d8413 | |||
| d4a71c994d | |||
| 372854948c | |||
| d6462919cb | |||
| ec79e085b2 | |||
| cedb635176 | |||
| 6f62a569df | |||
| 7c5e5b1941 | |||
| 8e460a8c2a | |||
| 18d5e0d3bb | |||
| 3e617a8b1e | |||
| 307c87b9f6 | |||
| 2d080b0264 | |||
| 834cf4095e | |||
| 723f1f5374 | |||
| 5f05f991bc | |||
| b44853fb2c | |||
| 29aa945ae0 | |||
| f2551ac366 | |||
| 2ba840aaac | |||
| eddb321489 | |||
| 6e9be84a01 | |||
| 18aa4f2538 | |||
| 4489249ecc | |||
| 2608fb180e | |||
| edeed2ee2c | |||
| 01b477da26 | |||
| 58a9554bb3 | |||
| 29f1cb3b4c | |||
| 7307563cfe | |||
| 047298a183 | |||
| 78fd1368db | |||
| d07a40616d | |||
| e81ea69143 | |||
| 917ab9740c | |||
| b43b7f02ae | |||
| d989ba62f9 | |||
| 87f26d7b0d | |||
| 7fac3e3bb6 | |||
| 2663121d9f | |||
| b5bfc029c3 | |||
| 5886fe7d8f | |||
| 5f15695c3f | |||
| c49533c250 | |||
| 4f49cb1bc9 | |||
| 874726b193 | |||
| 5244a12422 | |||
| 5b30764fa8 | |||
| 85ed7c3d2f | |||
| 4d5b307a74 | |||
| 68566b556c | |||
| b0032c1745 | |||
| 35b7aab174 | |||
| 2601215c91 | |||
| 4ebf0cad6b | |||
| 98d959112c | |||
| d01c2afaa6 |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "thedotmack",
|
||||
"owner": {
|
||||
"name": "Alex Newman"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Plugins by Alex Newman (thedotmack)",
|
||||
"homepage": "https://github.com/thedotmack/claude-mem"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "4.2.4",
|
||||
"source": "./plugin",
|
||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"env": {}
|
||||
}
|
||||
@@ -5,3 +5,6 @@ node_modules/
|
||||
.env.local
|
||||
*.tmp
|
||||
*.temp
|
||||
.claude/settings.local.json
|
||||
plugin/data/
|
||||
plugin/data.backup/
|
||||
+174
-1
@@ -5,6 +5,180 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
## [4.2.3] - 2025-10-23
|
||||
|
||||
### Security
|
||||
- **FTS5 injection vulnerability fix**: Added proper escaping to prevent SQL injection attacks in search functions
|
||||
- Implemented double-quote escaping for FTS5 full-text search queries
|
||||
- Added comprehensive test suite with 332 new tests covering injection scenarios
|
||||
- Affects: `search_observations`, `search_sessions`, `search_user_prompts` MCP tools
|
||||
|
||||
### Fixed
|
||||
- **ESM/CJS compatibility**: Fixed getDirname function to work in both ESM (hooks) and CJS (worker) contexts
|
||||
- Detects context using `typeof __dirname !== 'undefined'`
|
||||
- Falls back to `fileURLToPath(import.meta.url)` for ESM modules
|
||||
- Resolves path resolution issues across different module systems
|
||||
- **Windows PowerShell compatibility**: Fixed SessionStart hook error on Windows systems
|
||||
- Replaced bash-specific test command `[` with standard cross-platform npm install
|
||||
- Simplified hook command to use idempotent npm install (fast when dependencies exist)
|
||||
- Dependencies install from root package.json in marketplace folder
|
||||
|
||||
### Changed
|
||||
- **SessionStart hook command**: Now uses `cd ... && npm install --prefer-offline --no-audit --no-fund --loglevel=error && node context-hook.js`
|
||||
- Removed bash-specific conditional check
|
||||
- npm install is fast (~500ms) and idempotent when dependencies already exist
|
||||
- Works cross-platform on Windows, macOS, and Linux
|
||||
|
||||
|
||||
## [4.2.1] - 2025-10-22
|
||||
|
||||
### Added
|
||||
- **Summary skip logic**: Summaries now skip when work is already covered, banter/trivial requests, or no meaningful observations
|
||||
- New "WHEN NOT TO SUMMARIZE" section in buildSummaryPrompt guides SDK to avoid duplicate/trivial summaries
|
||||
- Parser detects `<skip_summary reason="..."/>` format and logs reason
|
||||
- Prevents duplicate summaries like the three "restore 6 types" summaries observed in session d9137878
|
||||
|
||||
### Fixed
|
||||
- **Observation type validation**: Parser now validates all 6 observation types (bugfix, feature, refactor, change, discovery, decision) instead of only 3
|
||||
|
||||
### Changed
|
||||
- **Chronological summary guidance**: Summaries now explicitly instructed to capture "what happened in THIS prompt" rather than re-summarizing previous work
|
||||
|
||||
|
||||
## [4.1.1] - 2025-10-21
|
||||
|
||||
### Removed
|
||||
- **advanced_search tool**: Removed redundant MCP tool that provided no functionality beyond calling search_observations + search_sessions
|
||||
|
||||
### Fixed
|
||||
- **MCP search limit bug**: Fixed findByConcept, findByType, and findByFile methods to properly respect limit/offset parameters
|
||||
- **Type contamination in concepts**: Added parser validation to prevent observation types from being added to concepts array
|
||||
- **Token limit warnings**: Added guidance in tool descriptions to start with 3-5 results to avoid MCP token limits
|
||||
|
||||
### Changed
|
||||
- **Simplified MCP API**: Reduced from 7 to 6 search tools by removing the redundant advanced_search
|
||||
- **Improved search prompts**: Enhanced type and concept constraint language in SDK prompts to prevent AI contamination
|
||||
|
||||
|
||||
## [4.1.0] - 2025-10-21
|
||||
|
||||
### Changed
|
||||
- **Graceful session cleanup**: Cleanup hook now marks sessions as completed instead of sending DELETE requests to worker
|
||||
- **Natural worker shutdown**: Workers now finish pending operations (like summary generation) before terminating
|
||||
- **Restored MCP search server**: Re-enabled full-text search capabilities from backup
|
||||
|
||||
### Fixed
|
||||
- Session summaries no longer interrupted by aggressive cleanup during session end
|
||||
- Workers can now complete final operations before shutdown
|
||||
|
||||
|
||||
## [4.0.2] - 2025-10-19
|
||||
|
||||
### Changed
|
||||
- **PM2 bundled as dependency**: Moved pm2 from devDependencies to dependencies for out-of-the-box functionality
|
||||
- **Worker scripts use local PM2**: All npm worker scripts now use `npx pm2` to ensure local binary is used
|
||||
- **Worker startup uses local PM2**: Worker auto-start now uses `node_modules/.bin/pm2` instead of global pm2
|
||||
|
||||
### Fixed
|
||||
- **Fail loudly on missing dependencies**: Worker startup now throws explicit errors when bundled pm2 is missing instead of silently falling back
|
||||
- **Better error messages**: Clear actionable error messages guide users to run `npm install` when dependencies are missing
|
||||
- **Removed silent fallback**: Eliminated silent degradation that masked "works on my machine" installation failures
|
||||
|
||||
### Documentation
|
||||
- Updated README system requirements to reflect pm2 is bundled with plugin (no global install required)
|
||||
|
||||
|
||||
## [4.0.0] - 2025-10-18
|
||||
|
||||
### BREAKING CHANGES
|
||||
- **Data directory moved to plugin location**: Database and worker files now stored in `${CLAUDE_PLUGIN_ROOT}/data/` instead of `~/.claude-mem/`
|
||||
- **Fresh start required**: No automatic migration from v3.x databases. Users must start fresh with v4.0.0
|
||||
- **Worker auto-starts**: Worker service now starts automatically on SessionStart hook, no manual PM2 commands needed
|
||||
|
||||
### Added
|
||||
- **MCP Search Server**: 6 specialized search tools with FTS5 full-text search capabilities
|
||||
- `search_observations` - Full-text search across observation titles, narratives, facts, and concepts
|
||||
- `search_sessions` - Full-text search across session summaries, requests, and learnings
|
||||
- `find_by_concept` - Find observations tagged with specific concepts
|
||||
- `find_by_file` - Find observations and sessions that reference specific file paths
|
||||
- `find_by_type` - Find observations by type (decision, bugfix, feature, refactor, discovery, change)
|
||||
- `advanced_search` - Combined search with filters across observations and sessions
|
||||
- **Citation support**: All search results include `claude-mem://` URI citations for referencing specific observations and sessions
|
||||
- **Automatic worker startup**: Worker service now starts automatically in SessionStart hook
|
||||
- **Plugin data directory**: Full integration with Claude Code plugin system using `CLAUDE_PLUGIN_ROOT`
|
||||
|
||||
### Changed
|
||||
- **Worker service architecture**: HTTP REST API with PM2 management for long-running background service
|
||||
- **Data directory priority**: `CLAUDE_PLUGIN_ROOT/data` > `CLAUDE_MEM_DATA_DIR` > `~/.claude-mem` (fallback for dev)
|
||||
- **Port file location**: Worker port file now stored in plugin data directory
|
||||
- **Session continuity**: Automatic context injection from last 3 sessions on startup
|
||||
- **Package structure**: Reorganized to properly distribute plugin/, dist/, and src/ directories
|
||||
|
||||
### Fixed
|
||||
- Context hook now uses proper `hookSpecificOutput` JSON format for SessionStart
|
||||
- Added missing process.exit(0) calls in all hook entry points
|
||||
- Worker service now ensures data directory exists before writing port file
|
||||
- Improved error handling and graceful degradation across all components
|
||||
|
||||
|
||||
## [3.7.1] - 2025-09-17
|
||||
|
||||
### Added
|
||||
- SQLite storage backend with session, memory, overview, and diagnostics management
|
||||
- Mintlify documentation site with searchable interface and comprehensive guides
|
||||
- Context7 MCP integration for documentation retrieval
|
||||
|
||||
### Changed
|
||||
- Session-start overviews to display chronologically from oldest to newest
|
||||
|
||||
### Fixed
|
||||
- Migration index parsing bug that prevented JSONL records from importing to SQLite
|
||||
|
||||
|
||||
## [3.6.10] - 2025-09-16
|
||||
|
||||
### Added
|
||||
- Claude Code statusline integration for real-time memory status
|
||||
- MCP memory tools server providing compress, stats, search, and overview commands
|
||||
- Concept documentation explaining memory compression and context loading
|
||||
|
||||
### Fixed
|
||||
- Corrected integration architecture to use hooks instead of MCP SDK
|
||||
|
||||
|
||||
## [3.6.9] - 2025-09-14
|
||||
|
||||
### Added
|
||||
- Display current date and time at the top of session-start hook output for better temporal context
|
||||
|
||||
### Changed
|
||||
- Enhanced session-start hook formatting with emoji icons and separator lines for improved readability
|
||||
|
||||
|
||||
## [3.6.8] - 2025-09-14
|
||||
|
||||
### Fixed
|
||||
- Fixed publish command failing when no version-related memories exist for changelog generation
|
||||
|
||||
|
||||
## [3.6.6] - 2025-09-14
|
||||
|
||||
### Fixed
|
||||
- Resolved compaction errors when processing large conversation histories by reducing chunk size limits to stay within Claude's context window
|
||||
|
||||
|
||||
## [3.6.5] - 2025-09-14
|
||||
|
||||
### Changed
|
||||
- Session groups now display in chronological order (most recent first)
|
||||
|
||||
### Fixed
|
||||
- Improved CLI path detection for cross-platform compatibility
|
||||
|
||||
|
||||
## [3.6.4] - 2025-09-13
|
||||
|
||||
### Changed
|
||||
@@ -61,4 +235,3 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
### Changed
|
||||
- Standardized GitHub release naming to lowercase 'claude-mem vX.X.X' format for consistent branding
|
||||
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
# Claude-Mem: Persistent Memory for Claude Code
|
||||
|
||||
## Overview
|
||||
|
||||
Claude-mem is a persistent memory compression system that preserves context across Claude Code sessions. It automatically captures tool usage observations, processes them through the Claude Agent SDK, and makes summaries available to future sessions.
|
||||
|
||||
**Current Version**: 4.2.4
|
||||
**License**: AGPL-3.0
|
||||
**Author**: Alex Newman (@thedotmack)
|
||||
|
||||
## What It Does
|
||||
|
||||
Claude-mem operates as a Claude Code plugin that:
|
||||
- Captures every tool execution during your coding sessions
|
||||
- Processes observations using AI-powered compression
|
||||
- Generates session summaries when sessions end
|
||||
- Injects relevant context into future sessions
|
||||
- Provides full-text search across your entire project history
|
||||
|
||||
This creates a continuous memory system where Claude can learn from past sessions and maintain context across your entire project lifecycle.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Hook-Based Lifecycle System
|
||||
|
||||
Claude-mem integrates with Claude Code through 5 lifecycle hooks:
|
||||
|
||||
1. **SessionStart Hook** (`context-hook`)
|
||||
- Ensures dependencies are installed (runs fast idempotent npm install)
|
||||
- Injects context from previous sessions
|
||||
- Auto-starts PM2 worker service
|
||||
- Retrieves last 10 session summaries with three-tier verbosity (v4.2.0)
|
||||
- Fixed in v4.1.0 to use proper JSON hookSpecificOutput format
|
||||
|
||||
2. **UserPromptSubmit Hook** (`new-hook`)
|
||||
- Creates new session records
|
||||
- Initializes session tracking
|
||||
- Saves raw user prompts for full-text search (as of v4.2.0)
|
||||
|
||||
3. **PostToolUse Hook** (`save-hook`)
|
||||
- Captures tool execution observations
|
||||
- Sends observations to worker service for processing
|
||||
|
||||
4. **Summary Hook**
|
||||
- Generates AI-powered session summaries
|
||||
- Processes accumulated observations
|
||||
|
||||
5. **SessionEnd Hook** (`cleanup-hook`)
|
||||
- Marks sessions as completed (graceful cleanup as of v4.1.0)
|
||||
- Skips cleanup on `/clear` commands to preserve ongoing sessions
|
||||
- Previously sent DELETE requests; now allows workers to finish naturally
|
||||
|
||||
### Worker Service Architecture
|
||||
|
||||
- **Technology**: HTTP REST API built with Express.js, managed by PM2
|
||||
- **Port**: Fixed port 37777 (configurable via CLAUDE_MEM_WORKER_PORT)
|
||||
- **Location**: `src/services/worker-service.ts`
|
||||
- **Configurable Model**: Uses `CLAUDE_MEM_MODEL` environment variable (default: claude-sonnet-4-5)
|
||||
|
||||
**REST API Endpoints** (6 total):
|
||||
- Session management endpoints
|
||||
- Observation processing endpoints
|
||||
- Worker port tracking
|
||||
|
||||
The worker service runs as a PM2-managed background process that handles AI processing separately from the hook execution, preventing hook timeout issues.
|
||||
|
||||
### Database Layer
|
||||
|
||||
**Technology**: SQLite 3 with better-sqlite3 native module
|
||||
**Location**: `~/.claude-mem/claude-mem.db`
|
||||
|
||||
**Note**: SessionStore and SessionSearch use better-sqlite3 as the primary database implementation. Database.ts (which uses bun:sqlite) is legacy code.
|
||||
|
||||
**Core Tables**:
|
||||
- `sdk_sessions` - Session tracking with prompt counters
|
||||
- `session_summaries` - AI-generated session summaries (multiple per session)
|
||||
- `observations` - Captured tool usage with structured fields
|
||||
- `user_prompts` - Raw user prompts with FTS5 search (as of v4.2.0)
|
||||
|
||||
**Schema Features**:
|
||||
- FTS5 (Full-Text Search) virtual tables for fast searching
|
||||
- Automatic sync triggers between main tables and FTS5 tables
|
||||
- Support for multi-prompt sessions (prompt_counter, prompt_number)
|
||||
- Hierarchical observations (title, subtitle, facts, narrative, concepts, files_read, files_modified)
|
||||
- Observation types: decision, bugfix, feature, refactor, discovery, change
|
||||
|
||||
**Database Classes**:
|
||||
- `SessionStore` - CRUD operations for sessions, observations, summaries, user prompts
|
||||
- `SessionSearch` - FTS5 full-text search with 8 search methods
|
||||
|
||||
### MCP Search Server
|
||||
|
||||
**Location**: `src/servers/search-server.ts`
|
||||
**Configuration**: `plugin/.mcp.json`
|
||||
|
||||
Exposes 8 specialized search tools to Claude:
|
||||
|
||||
1. **search_observations** - Full-text search across observations
|
||||
2. **search_sessions** - Full-text search across session summaries
|
||||
3. **search_user_prompts** - Full-text search across raw user prompts (as of v4.2.0)
|
||||
4. **find_by_concept** - Find observations tagged with specific concepts
|
||||
5. **find_by_file** - Find observations referencing specific file paths
|
||||
6. **find_by_type** - Find observations by type (decision/bugfix/feature/etc.)
|
||||
7. **get_recent_context** - Get recent session context including summaries and observations for a project
|
||||
8. **advanced_search** - Combine multiple filters with full-text search
|
||||
|
||||
**Search Pipeline**:
|
||||
```
|
||||
Claude Request → MCP Server → SessionSearch Service → FTS5 Database → Results → Claude
|
||||
```
|
||||
|
||||
**Citations**: All search results use the `claude-mem://` URI scheme for referencing specific observations and sessions.
|
||||
|
||||
## Installation
|
||||
|
||||
### Requirements
|
||||
- Node.js 18+
|
||||
- Claude Code plugin system
|
||||
|
||||
### Installation Method
|
||||
|
||||
**Local Marketplace Installation** (recommended as of v4.0.4+):
|
||||
|
||||
```bash
|
||||
# 1. Clone the repository
|
||||
git clone https://github.com/thedotmack/claude-mem.git
|
||||
cd claude-mem
|
||||
|
||||
# 2. Add to Claude Code marketplace
|
||||
/plugin marketplace add .claude-plugin/marketplace.json
|
||||
|
||||
# 3. Install the plugin
|
||||
/plugin install claude-mem
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Model Selection
|
||||
|
||||
Configure which AI model processes your observations:
|
||||
|
||||
**Using the interactive script**:
|
||||
```bash
|
||||
./claude-mem-settings.sh
|
||||
```
|
||||
|
||||
**Available models**:
|
||||
- `claude-haiku-4-5` - Fast, cost-efficient
|
||||
- `claude-sonnet-4-5` - Balanced (default)
|
||||
- `claude-opus-4` - Most capable
|
||||
- `claude-3-7-sonnet` - Alternative version
|
||||
|
||||
The script manages `CLAUDE_MEM_MODEL` in `~/.claude/settings.json`.
|
||||
TODO: also have script create and manage `CLAUDE_MEM_MODEL` in `~/.claude/plugins/marketplaces/thedotmack/.env` so our worker script has access to the value (we may not even need it in our settings but only in our plugin folder since hooks shouldn't be calling queries, not sure).
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Memory Pipeline
|
||||
```
|
||||
Tool Execution → Hook Capture → Worker Processing → AI Compression → Database Storage → Future Context Injection
|
||||
```
|
||||
|
||||
### Search Pipeline
|
||||
```
|
||||
Search Query → MCP Server → SessionSearch → FTS5 Query → Results with Citations
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Directory Structure
|
||||
```
|
||||
claude-mem/
|
||||
├── src/
|
||||
│ ├── bin/hooks/ # Hook entry points
|
||||
│ ├── hooks/ # Hook implementations
|
||||
│ ├── services/ # Worker service
|
||||
│ ├── services/sqlite/ # Database layer
|
||||
│ ├── servers/ # MCP search server
|
||||
│ ├── sdk/ # Claude Agent SDK integration
|
||||
│ ├── shared/ # Shared utilities
|
||||
│ └── utils/ # General utilities
|
||||
├── plugin/ # Built plugin files
|
||||
│ ├── scripts/ # Built hook executables
|
||||
│ └── .mcp.json # MCP server configuration
|
||||
└── .claude-plugin/ # Plugin metadata
|
||||
└── marketplace.json # Marketplace definition
|
||||
```
|
||||
|
||||
### Technology Stack
|
||||
- **Language**: TypeScript
|
||||
- **Database**: SQLite 3 with better-sqlite3
|
||||
- **HTTP**: Express.js
|
||||
- **Process Management**: PM2
|
||||
- **AI SDK**: @anthropic-ai/claude-agent-sdk (v0.1.23)
|
||||
- **MCP SDK**: @modelcontextprotocol/sdk (v1.20.1)
|
||||
- **Schema Validation**: zod-to-json-schema (v3.24.6)
|
||||
|
||||
### Build Process
|
||||
```bash
|
||||
npm run build && git commit -a -m "Build and update" && git push && cd ~/.claude/plugins/marketplaces/thedotmack/ && git pull && pm2 flush claude-mem-worker && pm2 restart claude-mem-worker && pm2 logs claude-mem-worker --nostream
|
||||
```
|
||||
|
||||
1) Compiles TypeScript and outputs hook executables to `plugin/scripts/`
|
||||
2) Does all the things needed to update and test since plugin-based installs are out of the .claude/ folder
|
||||
|
||||
**Build Outputs**:
|
||||
- Hook executables: `*-hook.js` (ESM format)
|
||||
- Worker service: `worker-service.cjs` (CJS format)
|
||||
- Search server: `search-server.js` (ESM format)
|
||||
|
||||
## Version History
|
||||
|
||||
### v4.2.4 (Current)
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**Improvements**:
|
||||
- Enhanced summary prompt clarity and reliability
|
||||
- Removed optional skip_summary functionality (summaries now always generated)
|
||||
- Clarified that summaries are mid-session checkpoints, not session endings
|
||||
- Improved request field instructions to better form descriptive titles
|
||||
- Changed wording from "discovered" to "learned" for consistency
|
||||
|
||||
**Technical Details**:
|
||||
- Updated `src/sdk/prompts.ts` to remove `WHEN NOT TO SUMMARIZE` section
|
||||
- Added footer text clarifying summaries track progress within ongoing sessions
|
||||
- Changed request field prompt from "Use their original sentiment" to "Form a title that reflects the actual request"
|
||||
- Affects both observation and summary prompt generation
|
||||
|
||||
### v4.2.3
|
||||
**Breaking Changes**: None (patch version)
|
||||
|
||||
**Security**:
|
||||
- Fixed FTS5 injection vulnerability in search functions
|
||||
- Implemented proper double-quote escaping for FTS5 queries
|
||||
- Added comprehensive test suite with 332 injection attack tests
|
||||
- Affects: `search_observations`, `search_sessions`, `search_user_prompts` MCP tools
|
||||
|
||||
**Fixes**:
|
||||
- Fixed ESM/CJS compatibility for getDirname function in src/shared/paths.ts
|
||||
- Detects context using `typeof __dirname !== 'undefined'`
|
||||
- Falls back to `fileURLToPath(import.meta.url)` for ESM modules
|
||||
- Resolves path resolution issues across hook (ESM) and worker (CJS) contexts
|
||||
- Fixed Windows PowerShell compatibility issue with SessionStart hook
|
||||
- Replaced bash-specific test command `[` with cross-platform npm install command
|
||||
- Hook now runs `npm install` with quiet flags (fast and idempotent when dependencies exist)
|
||||
|
||||
**Technical Details**:
|
||||
- SessionSearch.ts now escapes double quotes in FTS5 queries: `query.replace(/"/g, '""')`
|
||||
- Updated `plugin/hooks/hooks.json` SessionStart command to use standard shell syntax
|
||||
- Changed from: `[ ! -d ... ] && cd ... && npm install && node ... || node ...`
|
||||
- Changed to: `cd ... && npm install --prefer-offline --no-audit --no-fund --loglevel=error && node ...`
|
||||
- Dependencies are installed in marketplace folder (parent of CLAUDE_PLUGIN_ROOT) where root package.json exists
|
||||
- getDirname function now properly handles both CommonJS (__dirname) and ES modules (import.meta.url)
|
||||
|
||||
### v4.2.0
|
||||
**Breaking Changes**: None (minor version)
|
||||
|
||||
**Features**:
|
||||
- User prompt storage with FTS5 full-text search
|
||||
- New `user_prompts` table stores raw user input for every prompt
|
||||
- New `search_user_prompts` MCP tool enables searching actual user requests
|
||||
- Automatic FTS5 indexing of all user prompts for fast retrieval
|
||||
|
||||
**Benefits**:
|
||||
- Full context reconstruction from user intent → Claude actions → outcomes
|
||||
- Pattern detection for repeated requests (identify when Claude isn't listening)
|
||||
- Improved debugging by tracing from original user words to final implementation
|
||||
- Historical search: "How many times did user ask for X feature?"
|
||||
|
||||
**Implementation**:
|
||||
- Migration 10: Creates user_prompts table with FTS5 virtual table and sync triggers
|
||||
- UserPromptSubmit hook now saves prompts using claude_session_id (available immediately)
|
||||
- Citations use `claude-mem://user-prompt/{id}` URI scheme
|
||||
|
||||
### v4.1.0
|
||||
**Breaking Changes**: None (minor version)
|
||||
|
||||
**Features**:
|
||||
- Graceful session cleanup (marks complete instead of DELETE)
|
||||
- Restored MCP search server from backup
|
||||
- Updated dependencies (claude-agent-sdk 0.1.23, MCP SDK 1.20.1)
|
||||
|
||||
**Fixes**:
|
||||
- `/clear` command now skips cleanup to prevent session interruption
|
||||
- Session workers can finish pending operations naturally
|
||||
|
||||
### v4.0.0
|
||||
**Breaking Changes**:
|
||||
- Data directory relocated to `${CLAUDE_PLUGIN_ROOT}/data/`
|
||||
- Fresh start required (no migration from v3.x)
|
||||
- Worker auto-starts in SessionStart hook
|
||||
|
||||
**Features**:
|
||||
- MCP Search Server with 8 specialized search tools
|
||||
- FTS5 full-text search across observations, sessions, and user prompts
|
||||
- Citation support with `claude-mem://` URIs
|
||||
- HTTP REST API architecture with PM2 management
|
||||
- Plugin data directory integration
|
||||
|
||||
**Changes**:
|
||||
- Improved session continuity
|
||||
- Enhanced error handling
|
||||
- Better process cleanup
|
||||
|
||||
### Earlier Versions (v3.x)
|
||||
- v3.9.17: MCP integration, hookSpecificOutput JSON format
|
||||
- v3.7.1: SQLite storage backend
|
||||
- Earlier: Mintlify documentation, statusline support
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### Graceful Cleanup (v4.1.0)
|
||||
Changed from aggressive session deletion (HTTP DELETE to workers) to graceful completion (marking sessions complete and allowing workers to finish). This prevents interruption of important operations like summary generation.
|
||||
|
||||
### FTS5 for Search Performance
|
||||
Implements SQLite FTS5 (Full-Text Search) virtual tables with automatic synchronization triggers, enabling fast full-text search across thousands of observations without performance degradation.
|
||||
|
||||
### Multi-Prompt Session Support
|
||||
Tracks `prompt_counter` and `prompt_number` across sessions and observations, enabling context preservation across conversation restarts within the same coding session.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Worker Service Issues
|
||||
- Check PM2 status: `pm2 list`
|
||||
- View logs: `npm run worker:logs`
|
||||
- Restart worker: `npm run worker:restart`
|
||||
|
||||
### Database Issues
|
||||
- Database location: `~/.claude-mem/claude-mem.db`
|
||||
- Check schema: `sqlite3 <db-path> ".schema"`
|
||||
- FTS5 tables are automatically synchronized via triggers
|
||||
|
||||
### Hook Issues
|
||||
- Hooks output to Claude Code's hook execution log
|
||||
- Check `plugin/scripts/` for built executables
|
||||
|
||||
### Model Configuration Issues
|
||||
- Use `./claude-mem-settings.sh` to manage model settings
|
||||
- Settings stored in `~/.claude/settings.json`
|
||||
- Default fallback: `claude-sonnet-4-5`
|
||||
|
||||
## Citations & References
|
||||
|
||||
This project uses the `claude-mem://` URI scheme for citations:
|
||||
- `claude-mem://observation/{id}` - References specific observations
|
||||
- `claude-mem://session/{id}` - References specific sessions
|
||||
|
||||
All MCP search results include citations, enabling Claude to reference specific historical context.
|
||||
|
||||
## License
|
||||
|
||||
AGPL-3.0
|
||||
|
||||
## Repository
|
||||
|
||||
https://github.com/thedotmack/claude-mem
|
||||
@@ -1,86 +1,289 @@
|
||||
# 🧠 Claude Memory System (claude-mem)
|
||||
<h1 align="center">
|
||||
<br>
|
||||
<a href="https://github.com/thedotmack/claude-mem">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="docs/claude-mem-logo-for-dark-mode.webp">
|
||||
<source media="(prefers-color-scheme: light)" srcset="docs/claude-mem-logo-for-light-mode.webp">
|
||||
<img src="docs/claude-mem-logo-for-light-mode.webp" alt="Claude-Mem" width="400">
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
## Remember that one thing? Neither do we… but `claude-mem` does! 😵💫
|
||||
<h4 align="center">Persistent memory compression system built for <a href="https://claude.com/claude-code" target="_blank">Claude Code</a>.</h4>
|
||||
|
||||
Stop repeating yourself. `claude-mem` remembers what you and Claude Code figure out, so every new chat starts smarter than the last.
|
||||
<p align="center">
|
||||
<a href="LICENSE">
|
||||
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
|
||||
</a>
|
||||
<a href="package.json">
|
||||
<img src="https://img.shields.io/badge/version-4.2.3-green.svg" alt="Version">
|
||||
</a>
|
||||
<a href="package.json">
|
||||
<img src="https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg" alt="Node">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## ⚡️ 10‑Second Setup
|
||||
<p align="center">
|
||||
<a href="#quick-start">Quick Start</a> •
|
||||
<a href="#how-it-works">How It Works</a> •
|
||||
<a href="#mcp-search-tools">Search Tools</a> •
|
||||
<a href="#documentation">Documentation</a> •
|
||||
<a href="#configuration">Configuration</a> •
|
||||
<a href="#troubleshooting">Troubleshooting</a> •
|
||||
<a href="#license">License</a>
|
||||
</p>
|
||||
|
||||
```bash
|
||||
npm install -g claude-mem && claude-mem install
|
||||
```
|
||||
|
||||
That’s it. Restart Claude Code and you’re good. No config. No tedious setup or dependencies.
|
||||
|
||||
## ✨ What You Get
|
||||
|
||||
- Remembers key insights from your chats with Claude Code
|
||||
- Starts new sessions with the right context
|
||||
- Works quietly in the background
|
||||
- One-command install and status check
|
||||
|
||||
## 🗑️ Smart Trash™ (Your Panic Button)
|
||||
|
||||
Delete something by accident? It’s not gone.
|
||||
- Everything goes to `~/.claude-mem/trash/`
|
||||
- Restore with a single command: `claude-mem restore`
|
||||
- Timestamped so you can see when things moved
|
||||
|
||||
## 🎯 Why It’s Useful
|
||||
|
||||
- No more re-explaining your project over and over
|
||||
- Pick up exactly where you left off
|
||||
- Find past solutions fast when you face a familiar bug
|
||||
- Your knowledge compounds the more you use it
|
||||
|
||||
## 🧭 Minimal Commands You’ll Ever Need
|
||||
|
||||
```bash
|
||||
claude-mem install # Set up/repair integration
|
||||
claude-mem status # Check everything’s working
|
||||
claude-mem load-context # Peek at what it remembers
|
||||
claude-mem logs # If you’re curious
|
||||
claude-mem uninstall # Remove hooks
|
||||
|
||||
# Extras
|
||||
claude-mem trash-view # See what’s in Smart Trash™
|
||||
claude-mem restore # Restore deleted items
|
||||
```
|
||||
|
||||
## 📁 Where Stuff Lives (super simple)
|
||||
|
||||
```
|
||||
~/.claude-mem/
|
||||
├── index/ # memory index
|
||||
├── archives/ # transcripts
|
||||
├── hooks/ # integration bits
|
||||
├── trash/ # Smart Trash™
|
||||
└── logs/ # diagnostics
|
||||
```
|
||||
|
||||
## ✅ Requirements
|
||||
|
||||
- Node.js 18+
|
||||
- Claude Code
|
||||
|
||||
## 🆘 If Something’s Weird
|
||||
|
||||
```bash
|
||||
claude-mem status # quick health check
|
||||
claude-mem install --force # fixes most issues
|
||||
```
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under AGPL-3.0. See `LICENSE`.
|
||||
<p align="center">
|
||||
Claude-Mem seamlessly preserves context across sessions by automatically capturing tool usage observations, generating semantic summaries, and making them available to future sessions. This enables Claude to maintain continuity of knowledge about projects even after sessions end or reconnect.
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Ready to remember more and repeat less?
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
npm install -g claude-mem
|
||||
claude-mem install
|
||||
Start a new Claude Code session in the terminal and enter the following commands:
|
||||
|
||||
```
|
||||
> /plugin marketplace add thedotmack/claude-mem
|
||||
|
||||
> /plugin install claude-mem
|
||||
```
|
||||
|
||||
Your future self will thank you. 🧠✨
|
||||
Restart Claude Code. Context from previous sessions will automatically appear in new sessions.
|
||||
|
||||
**Key Features:**
|
||||
- 🧠 **Persistent Memory** - Context survives across sessions
|
||||
- 🔍 **7 Search Tools** - Query your project history via MCP
|
||||
- 🤖 **Automatic Operation** - No manual intervention required
|
||||
- 📊 **FTS5 Search** - Fast full-text search across observations
|
||||
- 🔗 **Citations** - Reference past decisions with `claude-mem://` URIs
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
📚 **[View Full Documentation](docs/)** - Browse markdown docs on GitHub
|
||||
|
||||
💻 **Local Preview**: Run Mintlify docs locally:
|
||||
```bash
|
||||
cd docs
|
||||
npx mintlify dev
|
||||
```
|
||||
|
||||
### Getting Started
|
||||
- **[Installation Guide](docs/installation.mdx)** - Quick start & advanced installation
|
||||
- **[Usage Guide](docs/usage/getting-started.mdx)** - How Claude-Mem works automatically
|
||||
- **[MCP Search Tools](docs/usage/search-tools.mdx)** - Query your project history
|
||||
|
||||
### Architecture
|
||||
- **[Overview](docs/architecture/overview.mdx)** - System components & data flow
|
||||
- **[Hooks](docs/architecture/hooks.mdx)** - 5 lifecycle hooks explained
|
||||
- **[Worker Service](docs/architecture/worker-service.mdx)** - HTTP API & PM2 management
|
||||
- **[Database](docs/architecture/database.mdx)** - SQLite schema & FTS5 search
|
||||
- **[MCP Search](docs/architecture/mcp-search.mdx)** - 7 search tools & examples
|
||||
|
||||
### Configuration & Development
|
||||
- **[Configuration](docs/configuration.mdx)** - Environment variables & settings
|
||||
- **[Development](docs/development.mdx)** - Building, testing, contributing
|
||||
- **[Troubleshooting](docs/troubleshooting.mdx)** - Common issues & solutions
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Session Start → Inject context from last 10 sessions │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ User Prompts → Create session, save user prompts │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Tool Executions → Capture observations (Read, Write, etc.) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Worker Processes → Extract learnings via Claude Agent SDK │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Session Ends → Generate summary, ready for next session │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Core Components:**
|
||||
1. **5 Lifecycle Hooks** - SessionStart, UserPromptSubmit, PostToolUse, Stop, SessionEnd
|
||||
2. **Worker Service** - HTTP API on port 37777 managed by PM2
|
||||
3. **SQLite Database** - Stores sessions, observations, summaries with FTS5 search
|
||||
4. **7 MCP Search Tools** - Query historical context with citations
|
||||
|
||||
See [Architecture Overview](docs/architecture/overview.mdx) for details.
|
||||
|
||||
---
|
||||
|
||||
## MCP Search Tools
|
||||
|
||||
Claude-Mem provides 7 specialized search tools:
|
||||
|
||||
1. **search_observations** - Full-text search across observations
|
||||
2. **search_sessions** - Full-text search across session summaries
|
||||
3. **search_user_prompts** - Search raw user requests
|
||||
4. **find_by_concept** - Find by concept tags
|
||||
5. **find_by_file** - Find by file references
|
||||
6. **find_by_type** - Find by type (decision, bugfix, feature, etc.)
|
||||
7. **get_recent_context** - Get recent session context
|
||||
|
||||
**Example Queries:**
|
||||
```
|
||||
search_observations with query="authentication" and type="decision"
|
||||
find_by_file with filePath="worker-service.ts"
|
||||
search_user_prompts with query="add dark mode"
|
||||
get_recent_context with limit=5
|
||||
```
|
||||
|
||||
See [MCP Search Tools Guide](docs/usage/search-tools.mdx) for detailed examples.
|
||||
|
||||
---
|
||||
|
||||
## What's New in v4.2.3
|
||||
|
||||
**Security:**
|
||||
- Fixed FTS5 injection vulnerability in search functions
|
||||
- Added comprehensive test suite with 332 injection attack tests
|
||||
|
||||
**Fixes:**
|
||||
- Fixed ESM/CJS compatibility for getDirname function
|
||||
- Fixed Windows PowerShell compatibility in SessionStart hook
|
||||
- Cross-platform dependency installation now works on Windows, macOS, and Linux
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md) for complete version history.
|
||||
|
||||
---
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Node.js**: 18.0.0 or higher
|
||||
- **Claude Code**: Latest version with plugin support
|
||||
- **PM2**: Process manager (bundled - no global install required)
|
||||
- **SQLite 3**: For persistent storage (bundled)
|
||||
|
||||
---
|
||||
|
||||
## Key Benefits
|
||||
|
||||
### Automatic Memory
|
||||
- Context automatically injected when Claude starts
|
||||
- No manual commands or configuration needed
|
||||
- Works transparently in the background
|
||||
|
||||
### Full History Search
|
||||
- Search across all sessions and observations
|
||||
- FTS5 full-text search for fast queries
|
||||
- Citations link back to specific observations
|
||||
|
||||
### Structured Observations
|
||||
- AI-powered extraction of learnings
|
||||
- Categorized by type (decision, bugfix, feature, etc.)
|
||||
- Tagged with concepts and file references
|
||||
|
||||
### Multi-Prompt Sessions
|
||||
- Sessions span multiple user prompts
|
||||
- Context preserved across `/clear` commands
|
||||
- Track entire conversation threads
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
**Model Selection:**
|
||||
```bash
|
||||
./claude-mem-settings.sh
|
||||
```
|
||||
|
||||
**Environment Variables:**
|
||||
- `CLAUDE_MEM_MODEL` - AI model for processing (default: claude-sonnet-4-5)
|
||||
- `CLAUDE_MEM_WORKER_PORT` - Worker port (default: 37777)
|
||||
- `CLAUDE_MEM_DATA_DIR` - Data directory override (dev only)
|
||||
|
||||
See [Configuration Guide](docs/configuration.mdx) for details.
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Clone and build
|
||||
git clone https://github.com/thedotmack/claude-mem.git
|
||||
cd claude-mem
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
|
||||
# Start worker
|
||||
npm run worker:start
|
||||
|
||||
# View logs
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
See [Development Guide](docs/development.mdx) for detailed instructions.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Common Issues:**
|
||||
- Worker not starting → `npm run worker:restart`
|
||||
- No context appearing → `npm run test:context`
|
||||
- Database issues → `sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"`
|
||||
- Search not working → Check FTS5 tables exist
|
||||
|
||||
See [Troubleshooting Guide](docs/troubleshooting.mdx) for complete solutions.
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please:
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes with tests
|
||||
4. Update documentation
|
||||
5. Submit a Pull Request
|
||||
|
||||
See [Development Guide](docs/development.mdx) for contribution workflow.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the **GNU Affero General Public License v3.0** (AGPL-3.0).
|
||||
|
||||
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
|
||||
|
||||
See the [LICENSE](LICENSE) file for full details.
|
||||
|
||||
**What This Means:**
|
||||
- You can use, modify, and distribute this software freely
|
||||
- If you modify and deploy on a network server, you must make your source code available
|
||||
- Derivative works must also be licensed under AGPL-3.0
|
||||
- There is NO WARRANTY for this software
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
- **Documentation**: [docs/](docs/)
|
||||
- **Issues**: [GitHub Issues](https://github.com/thedotmack/claude-mem/issues)
|
||||
- **Repository**: [github.com/thedotmack/claude-mem](https://github.com/thedotmack/claude-mem)
|
||||
- **Author**: Alex Newman ([@thedotmack](https://github.com/thedotmack))
|
||||
|
||||
---
|
||||
|
||||
**Built with Claude Agent SDK** | **Powered by Claude Code** | **Made with TypeScript**
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "claude-mem",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.0",
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
"boxen": "^8.0.1",
|
||||
"chalk": "^5.6.0",
|
||||
"commander": "^14.0.0",
|
||||
"glob": "^11.0.3",
|
||||
"gradient-string": "^3.0.0",
|
||||
"handlebars": "^4.7.8",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.1.15", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^3.24.1" } }, "sha512-x0UR/YW87lRel3wYVimWjAkUOEGapg/nXx2GYNLbUD1ORsetRHeXZGFdJMuhRkk1Jt9sbn5m/RpT42b5R0pgYg=="],
|
||||
|
||||
"@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="],
|
||||
|
||||
"@clack/prompts": ["@clack/prompts@0.11.0", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
|
||||
|
||||
"@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
|
||||
|
||||
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
|
||||
|
||||
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||
|
||||
"@types/tinycolor2": ["@types/tinycolor2@1.4.6", "", {}, "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw=="],
|
||||
|
||||
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"better-sqlite3": ["better-sqlite3@11.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ=="],
|
||||
|
||||
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
|
||||
|
||||
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
|
||||
|
||||
"boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="],
|
||||
|
||||
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||
|
||||
"camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="],
|
||||
|
||||
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
|
||||
|
||||
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"commander": ["commander@14.0.1", "", {}, "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||
|
||||
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||
|
||||
"expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
|
||||
|
||||
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
|
||||
|
||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||
|
||||
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
|
||||
|
||||
"get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
|
||||
|
||||
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
|
||||
|
||||
"glob": ["glob@11.0.3", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA=="],
|
||||
|
||||
"gradient-string": ["gradient-string@3.0.0", "", { "dependencies": { "chalk": "^5.3.0", "tinygradient": "^1.1.5" } }, "sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg=="],
|
||||
|
||||
"handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="],
|
||||
|
||||
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
|
||||
|
||||
"minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
|
||||
|
||||
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
|
||||
|
||||
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
|
||||
|
||||
"node-abi": ["node-abi@3.78.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-scurry": ["path-scurry@2.0.0", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
|
||||
|
||||
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
||||
|
||||
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
|
||||
|
||||
"simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
|
||||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
||||
|
||||
"tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
|
||||
|
||||
"tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
|
||||
|
||||
"tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="],
|
||||
|
||||
"tinygradient": ["tinygradient@1.1.5", "", { "dependencies": { "@types/tinycolor2": "^1.4.0", "tinycolor2": "^1.0.0" } }, "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw=="],
|
||||
|
||||
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
||||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
|
||||
|
||||
"wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||
|
||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
|
||||
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
argument-hint: help | save [message] | remember [context] | (no args for help)
|
||||
description: Manage claude-mem operations and memory context
|
||||
allowed-tools: Bash(claude-mem:*), Bash(echo:*), Bash(cat:*)
|
||||
---
|
||||
|
||||
## Claude-Mem Command Handler
|
||||
|
||||
### Check for help command first
|
||||
!`[ -z "$ARGUMENTS" ] || [ "$ARGUMENTS" = "help" ] && printf '%s\n' '## 🧠 Claude-Mem Help' '' '**Available Commands:**' '' '• /claude-mem save [message] - Quick save of conversation overview' '• /claude-mem remember [query] - Search saved memories' '• /claude-mem help - Show this help' '' '**Quick Shortcuts:**' '• /save - Direct save' '• /remember - Direct search' '' '**About /save:**' 'Quick way to save an overview to claude-mem without processing the' 'entire transcript. Use this when you dont need a detailed archive,' 'just a summary of key points and decisions.' '' '**Optional Features (configure during install):**' '• Compress on /clear: Archives full transcript when clearing (off by default)' '• Session start: Loads recent memories when starting Claude Code' '' 'For more details: claude-mem --help' && exit 0`
|
||||
|
||||
### Process other commands
|
||||
Handle claude-mem operation: $ARGUMENTS
|
||||
|
||||
If $ARGUMENTS starts with "save":
|
||||
- Write an overview of the current conversation context
|
||||
- Add it to claude-mem using the chroma MCP tools
|
||||
- Save the overview using: `claude-mem save "your overview message"`
|
||||
|
||||
If $ARGUMENTS starts with "remember":
|
||||
- Search claude-mem for relevant memories using the query
|
||||
- Display the most relevant memories from previous sessions
|
||||
- Use chroma_query_documents to find and present context
|
||||
@@ -1 +0,0 @@
|
||||
Search claude-mem for #$ARGUMENTS and look up relevant context to help clarify what we are working on.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
allowed-tools: Bash
|
||||
description: Write an overview and save with claude-mem
|
||||
---
|
||||
**Write an overview** of the current conversation context and:
|
||||
1. **Add it to claude-mem** using the chroma MCP tools
|
||||
2. **IMPORTANT! Save the overview to index** using the claude-mem CLI tool: `claude-mem save "your overview message"`
|
||||
@@ -0,0 +1,987 @@
|
||||
# Claude-Mem Architecture v3 to v4 Plan (✅ Completed)
|
||||
|
||||
This file exists as a reference to explain the path forward from v3 to v4.
|
||||
|
||||
## Core Purpose
|
||||
|
||||
Create a lightweight, hook-driven memory system that captures important context during Claude Code sessions and makes it available in future sessions.
|
||||
|
||||
**Principles:**
|
||||
- Hooks should be fast and non-blocking
|
||||
- SDK agent synthesizes observations, not just stores raw data
|
||||
- Storage should be simple and queryable
|
||||
- Users should never notice the memory system working
|
||||
|
||||
---
|
||||
|
||||
## Understanding the Foundation
|
||||
|
||||
### What Claude Code Hooks Actually Do
|
||||
|
||||
**SessionStart Hook:**
|
||||
- Runs when Claude Code starts or resumes
|
||||
- Can inject context via stdout (plain text) OR JSON `additionalContext`
|
||||
- This is how we show "What's new" to Claude
|
||||
|
||||
**UserPromptSubmit Hook:**
|
||||
- Runs BEFORE Claude processes the user's message
|
||||
- Can inject context via stdout OR JSON `additionalContext`
|
||||
- This is where we initialize per-session tracking
|
||||
|
||||
**PostToolUse Hook:**
|
||||
- Runs AFTER each tool completes successfully
|
||||
- Gets both tool input and output
|
||||
- Runs in PARALLEL with other matching hooks
|
||||
- This is where we observe what Claude is doing
|
||||
|
||||
**Stop Hook:**
|
||||
- Runs when main agent finishes (NOT on user interrupt)
|
||||
- This is where we finalize the session
|
||||
- Summary should be structured responses that answer the following:
|
||||
- What did user request?
|
||||
- What did you investigate?
|
||||
- What did you learn?
|
||||
- What did you do?
|
||||
- What's next?
|
||||
- Files read
|
||||
- Files edited
|
||||
- Notes
|
||||
|
||||
### How SDK Streaming Actually Works
|
||||
|
||||
**Streaming Input Mode (what we need):**
|
||||
- Persistent session with AsyncGenerator
|
||||
- Can queue multiple messages
|
||||
- Supports interruption via `interrupt()` method
|
||||
- Natural multi-turn conversations
|
||||
- The SDK maintains conversation state
|
||||
|
||||
**Critical insight:** We use "Streaming Input Mode" which creates ONE long-running SDK session per Claude Code session, not multiple short sessions.
|
||||
|
||||
**Session ID Management:**
|
||||
- Session IDs change with each turn of the conversation
|
||||
- Must capture session ID from the initial system message
|
||||
- SDK worker needs to track session ID updates continuously, not just capture once
|
||||
- The first message in the response stream is a system init message with the session_id
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Visual Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ CLAUDE CODE SESSION │
|
||||
│ (Main session - user interacting with Claude Code) │
|
||||
│ │
|
||||
│ User → Claude → Tools (Read, Edit, Write, Bash, etc.) │
|
||||
│ │ │
|
||||
│ │ PostToolUse Hook │
|
||||
│ ↓ │
|
||||
│ claude-mem save │
|
||||
│ (queues observation) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ SQLite observation_queue
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ SDK WORKER PROCESS │
|
||||
│ (Background process - detached from main session) │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ Message Generator (AsyncIterable) │ │
|
||||
│ │ - Yields initial prompt │ │
|
||||
│ │ - Polls observation_queue │ │
|
||||
│ │ - Yields observation prompts │ │
|
||||
│ └─────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ SDK query() → Claude API │ │
|
||||
│ │ Model: claude-sonnet-4-5 │ │
|
||||
│ │ No tools needed (text-only synthesis) │ │
|
||||
│ └─────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ Response Handler │ │
|
||||
│ │ - Parses XML <observation> blocks │ │
|
||||
│ │ - Parses XML <summary> blocks │ │
|
||||
│ │ - Writes to SQLite tables │ │
|
||||
│ └─────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ SQLite: observations, session_summaries
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ NEXT CLAUDE CODE SESSION │
|
||||
│ │
|
||||
│ SessionStart Hook → claude-mem context │
|
||||
│ (Reads from SQLite and injects context) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### What is the SDK agent's job?
|
||||
|
||||
The SDK agent is a **synthesis engine**, not a data collector.
|
||||
|
||||
It should:
|
||||
- Receive tool observations as they happen
|
||||
- Extract meaningful patterns and insights
|
||||
- Store atomic, searchable observations in SQLite
|
||||
- Synthesize a human-readable summary at the end
|
||||
|
||||
It should NOT:
|
||||
- Store raw tool outputs
|
||||
- Try to capture everything
|
||||
- Make decisions about what Claude Code should do
|
||||
- Block or slow down the main session
|
||||
|
||||
### Session Management Strategy
|
||||
|
||||
**Built-in SDK Session Resumption:**
|
||||
|
||||
The Agent SDK provides native session resumption capabilities. Instead of manually tracking and rebuilding session state, we can leverage the SDK's built-in features:
|
||||
|
||||
```typescript
|
||||
// Resume a previous SDK session
|
||||
const resumedResponse = query({
|
||||
prompt: "Continue where we left off",
|
||||
options: {
|
||||
resume: sdkSessionId // Use the session ID captured from init message
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**When to use session resumption:**
|
||||
- User interrupts Claude Code and resumes later
|
||||
- SDK worker crashes and needs to restart
|
||||
- Long-running observations that span multiple Claude Code sessions
|
||||
|
||||
**Session state tracking:**
|
||||
- Store SDK session ID in database when captured from init message
|
||||
- Mark sessions as 'active', 'completed', 'interrupted', or 'failed'
|
||||
- Use session status to determine whether to resume or start fresh
|
||||
|
||||
### How hooks run in parallel
|
||||
|
||||
PostToolUse hooks run in parallel. Handle this by:
|
||||
- Make SDK agent calls async and fire-and-forget
|
||||
- Use the observation_queue SQLite table to serialize observations
|
||||
- SDK worker polls this queue and processes observations sequentially
|
||||
|
||||
### What if the user interrupts Claude Code?
|
||||
|
||||
Stop hook doesn't run on interrupts. So:
|
||||
- Observations stay in queue
|
||||
- Next session continues where left off
|
||||
- Mark session as 'interrupted' after 24h of inactivity
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
```sql
|
||||
-- Tracks SDK streaming sessions
|
||||
CREATE TABLE sdk_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT UNIQUE NOT NULL,
|
||||
sdk_session_id TEXT UNIQUE NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
user_prompt TEXT,
|
||||
started_at TEXT NOT NULL,
|
||||
started_at_epoch INTEGER NOT NULL,
|
||||
completed_at TEXT,
|
||||
completed_at_epoch INTEGER,
|
||||
status TEXT CHECK(status IN ('active', 'completed', 'failed'))
|
||||
);
|
||||
|
||||
-- Tracks pending observations (message queue)
|
||||
CREATE TABLE observation_queue (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
tool_name TEXT NOT NULL,
|
||||
tool_input TEXT NOT NULL, -- JSON
|
||||
tool_output TEXT NOT NULL, -- JSON
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
processed_at_epoch INTEGER,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id)
|
||||
);
|
||||
|
||||
-- Stores extracted observations (what SDK decides is important)
|
||||
CREATE TABLE observations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
type TEXT NOT NULL, -- 'decision' | 'bugfix' | 'feature' | 'refactor' | 'discovery'
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_observations_project ON observations(project);
|
||||
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
|
||||
|
||||
-- Stores session summaries
|
||||
CREATE TABLE session_summaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT UNIQUE NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX idx_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hook Implementation
|
||||
|
||||
**IMPORTANT DISTINCTION:**
|
||||
|
||||
There are TWO separate hook systems at play here:
|
||||
|
||||
1. **Claude Code Hooks** - External command hooks configured in `~/.config/claude-code/settings.json`
|
||||
- These hooks observe the MAIN Claude Code session
|
||||
- They run as external commands (like `claude-mem save`)
|
||||
- This is what we use to capture observations from the user's session
|
||||
|
||||
2. **SDK Hooks** - Programmatic hooks configured in TypeScript code via `HookMatcher`
|
||||
- These hooks would observe the MEMORY SDK agent's own tool usage
|
||||
- They run as TypeScript callbacks within the SDK worker process
|
||||
- We're NOT using these (yet) - they're a future enhancement
|
||||
|
||||
**Our architecture:** Use Claude Code hooks (external commands) to observe the main session, and run a separate SDK worker process that doesn't need its own hooks.
|
||||
|
||||
### 1. SessionStart Hook
|
||||
|
||||
**Purpose:** Show user what happened in recent sessions
|
||||
|
||||
**Claude Code Hook Config (in settings.json):**
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [{
|
||||
"matcher": "startup",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "claude-mem context"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Command: `claude-mem context`**
|
||||
|
||||
Flow:
|
||||
1. Read stdin JSON (session_id, cwd, source, etc.)
|
||||
2. If source !== "startup", exit immediately
|
||||
3. Extract project from cwd basename
|
||||
4. Query SQLite for recent summaries:
|
||||
```sql
|
||||
SELECT summary, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 10
|
||||
```
|
||||
5. Format results as human-readable text
|
||||
6. Output to stdout (Claude Code automatically injects this)
|
||||
7. Exit with code 0
|
||||
|
||||
### 2. UserPromptSubmit Hook
|
||||
|
||||
**Purpose:** Initialize SDK memory session in background
|
||||
|
||||
**Hook config:**
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "claude-mem new"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Command: `claude-mem new`**
|
||||
|
||||
Flow:
|
||||
1. Read stdin JSON (session_id, prompt, cwd, etc.)
|
||||
2. Extract project from cwd
|
||||
3. Create SDK session record in database
|
||||
4. Start SDK session with initialization prompt in background process
|
||||
5. Save SDK session ID to database
|
||||
6. Output: `{"continue": true, "suppressOutput": true}`
|
||||
7. Exit immediately (SDK runs in background daemon/process)
|
||||
|
||||
**The Background SDK Process:**
|
||||
|
||||
The SDK session should run as a detached background process:
|
||||
```typescript
|
||||
// In claude-mem new
|
||||
const child = spawn('claude-mem', ['sdk-worker', session_id], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
});
|
||||
child.unref();
|
||||
```
|
||||
|
||||
The SDK worker:
|
||||
```typescript
|
||||
// claude-mem sdk-worker <session_id>
|
||||
import { query } from '@anthropic-ai/agent-sdk';
|
||||
import type { Query, UserMessage } from '@anthropic-ai/agent-sdk';
|
||||
|
||||
async function runSDKWorker(sessionId: string) {
|
||||
const session = await loadSessionFromDB(sessionId);
|
||||
|
||||
// Track the SDK session ID from the init message
|
||||
let sdkSessionId: string | undefined;
|
||||
const abortController = new AbortController();
|
||||
|
||||
// Message generator yields UserMessage objects (role + content)
|
||||
// This matches the SDK's expected format for streaming input mode
|
||||
async function* messageGenerator(): AsyncIterable<UserMessage> {
|
||||
// Initial prompt
|
||||
yield {
|
||||
role: "user",
|
||||
content: buildInitPrompt(session)
|
||||
};
|
||||
|
||||
// Then listen for queued observations
|
||||
while (session.status === 'active' && !abortController.signal.aborted) {
|
||||
const observations = await pollObservationQueue(session.sdk_session_id);
|
||||
|
||||
for (const obs of observations) {
|
||||
yield {
|
||||
role: "user",
|
||||
content: buildObservationPrompt(obs)
|
||||
};
|
||||
markObservationProcessed(obs.id);
|
||||
}
|
||||
|
||||
await sleep(1000); // Poll every second
|
||||
}
|
||||
}
|
||||
|
||||
// Run SDK session with proper streaming interface
|
||||
// The query function signature: query({ prompt, options }): Query
|
||||
const response: Query = query({
|
||||
prompt: messageGenerator(), // AsyncIterable<UserMessage>
|
||||
options: {
|
||||
model: 'claude-sonnet-4-5', // Use documented model name
|
||||
disallowedTools: ['Glob', 'Grep', 'ListMcpResourcesTool', 'WebSearch'], // More efficient than filtering in hooks
|
||||
maxTurns: 1000,
|
||||
cwd: session.cwd,
|
||||
abortController: abortController
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// Consume responses from the Query AsyncGenerator
|
||||
for await (const msg of response) {
|
||||
// Capture session ID from system init message
|
||||
if (msg.type === 'system' && msg.subtype === 'init') {
|
||||
sdkSessionId = msg.session_id;
|
||||
console.log(`SDK session started with ID: ${sdkSessionId}`);
|
||||
// Store SDK session ID in database for potential resumption
|
||||
await updateSDKSessionId(session.id, sdkSessionId);
|
||||
}
|
||||
|
||||
if (msg.type === 'text') {
|
||||
// Use an XML parser library (e.g., fast-xml-parser or similar) to parse observations and summaries
|
||||
// Parse <observation> blocks and call storeObservation(session_id, project, type, text)
|
||||
// Parse <summary> blocks, extract all 8 fields, format and call storeSummary(session_id, project, text)
|
||||
|
||||
parseAndStoreObservations(msg.content, session);
|
||||
parseAndStoreSummary(msg.content, session);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (abortController.signal.aborted) {
|
||||
console.log('SDK worker interrupted gracefully');
|
||||
} else {
|
||||
console.error('SDK worker error:', error);
|
||||
await markSessionFailed(session.id, error);
|
||||
}
|
||||
} finally {
|
||||
await cleanupSession(session.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Query and UserMessage types are imported from '@anthropic-ai/agent-sdk'
|
||||
// Query extends AsyncGenerator and provides interrupt() and setPermissionMode() methods
|
||||
// UserMessage has the shape: { role: "user", content: string }
|
||||
```
|
||||
|
||||
### 3. PostToolUse Hook
|
||||
|
||||
**Purpose:** Queue tool observations for SDK processing
|
||||
|
||||
**Hook config:**
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "claude-mem save"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Command: `claude-mem save`**
|
||||
|
||||
Flow:
|
||||
1. Read stdin JSON (tool_name, tool_input, tool_output, etc.)
|
||||
2. Find active SDK session for this project
|
||||
3. Insert observation into queue:
|
||||
```sql
|
||||
INSERT INTO observation_queue
|
||||
(sdk_session_id, tool_name, tool_input, tool_output, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
```
|
||||
4. Output: `{"continue": true, "suppressOutput": true}`
|
||||
5. Exit immediately
|
||||
|
||||
**Observation Filtering:**
|
||||
|
||||
Tool filtering is now handled at the SDK level (see SDK Worker section) using `disallowedTools` option. The hook no longer needs to filter tools manually - all tool observations that reach the hook are already relevant.
|
||||
|
||||
However, you may still want to skip certain tools for performance reasons:
|
||||
```typescript
|
||||
// Optional: Skip very frequent or low-value tools
|
||||
const SKIP_TOOLS = new Set(['TodoWrite', 'ListMcpResourcesTool']);
|
||||
|
||||
if (SKIP_TOOLS.has(tool_name)) {
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
process.exit(0);
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** The SDK worker uses `disallowedTools: ['Glob', 'Grep', 'ListMcpResourcesTool', 'WebSearch']` which is more efficient than hook-level filtering because it prevents the SDK agent from using these tools entirely.
|
||||
|
||||
### 4. Stop Hook
|
||||
|
||||
**Purpose:** Signal SDK to finalize and generate summary
|
||||
|
||||
**Hook config:**
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"Stop": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "claude-mem summary"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Command: `claude-mem summary`**
|
||||
|
||||
Flow:
|
||||
1. Read stdin JSON (session_id, cwd, etc.)
|
||||
2. Find active SDK session
|
||||
3. Insert special "FINALIZE" message into observation queue:
|
||||
```sql
|
||||
INSERT INTO observation_queue
|
||||
(sdk_session_id, tool_name, tool_input, tool_output, created_at_epoch)
|
||||
VALUES (?, 'FINALIZE', '{}', '{}', ?)
|
||||
```
|
||||
4. Output: `{"continue": true, "suppressOutput": true}`
|
||||
5. Exit immediately
|
||||
|
||||
**SDK Worker Handling:**
|
||||
|
||||
When SDK worker sees FINALIZE message:
|
||||
```typescript
|
||||
if (obs.tool_name === 'FINALIZE') {
|
||||
yield {
|
||||
type: "user",
|
||||
message: {
|
||||
role: "user",
|
||||
content: buildFinalizePrompt(session)
|
||||
}
|
||||
};
|
||||
|
||||
// Wait for SDK to finish processing
|
||||
await waitForSDKCompletion();
|
||||
|
||||
// Update session status
|
||||
await markSessionCompleted(session.id);
|
||||
|
||||
// Exit worker
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SDK Agent Prompts
|
||||
|
||||
### Initialization Prompt
|
||||
|
||||
```typescript
|
||||
function buildInitPrompt(project: string, sessionId: string, userPrompt: string): string {
|
||||
return `You are a memory assistant for the "${project}" project.
|
||||
|
||||
SESSION CONTEXT
|
||||
---------------
|
||||
Session ID: ${sessionId}
|
||||
User's Goal: ${userPrompt}
|
||||
Date: ${new Date().toISOString().split('T')[0]}
|
||||
|
||||
YOUR ROLE
|
||||
---------
|
||||
You will observe tool executions during this Claude Code session. Your job is to:
|
||||
|
||||
1. Extract meaningful insights (not just raw data)
|
||||
2. Store atomic observations in SQLite
|
||||
3. Focus on: key decisions, patterns discovered, problems solved, technical insights
|
||||
|
||||
WHAT TO CAPTURE
|
||||
----------------
|
||||
✓ Architecture decisions (e.g., "chose PostgreSQL over MongoDB for ACID guarantees")
|
||||
✓ Bug fixes (e.g., "fixed race condition in auth middleware by adding mutex")
|
||||
✓ New features (e.g., "implemented JWT refresh token flow")
|
||||
✓ Refactorings (e.g., "extracted validation logic into separate service")
|
||||
✓ Discoveries (e.g., "found that API rate limit is 100 req/min")
|
||||
|
||||
✗ NOT routine operations (reading files, listing directories)
|
||||
✗ NOT work-in-progress (only completed work)
|
||||
✗ NOT obvious facts (e.g., "TypeScript file has types")
|
||||
|
||||
HOW TO STORE OBSERVATIONS
|
||||
--------------------------
|
||||
When you identify something worth remembering, output your observation in this EXACT XML format:
|
||||
|
||||
\`\`\`xml
|
||||
<observation>
|
||||
<type>feature</type>
|
||||
<text>Implemented JWT token refresh flow with 7-day expiry</text>
|
||||
</observation>
|
||||
\`\`\`
|
||||
|
||||
Valid types: decision, bugfix, feature, refactor, discovery
|
||||
|
||||
Structure requirements:
|
||||
- <observation> is the root element
|
||||
- <type> must be one of the 5 valid types (single word)
|
||||
- <text> contains your concise observation (one sentence preferred)
|
||||
- No additional fields or nesting
|
||||
|
||||
The SDK worker will parse all <observation> blocks from your response using regex and store them in SQLite.
|
||||
|
||||
You can include your reasoning before or after the observation block, or just output the observation by itself.
|
||||
|
||||
EXAMPLE
|
||||
-------
|
||||
Bad: "Read src/auth.ts file"
|
||||
Good: "Implemented JWT token refresh flow with 7-day expiry"
|
||||
|
||||
Wait for tool observations. Acknowledge this message briefly.`;
|
||||
}
|
||||
```
|
||||
|
||||
### Observation Prompt
|
||||
|
||||
```typescript
|
||||
function buildObservationPrompt(obs: Observation): string {
|
||||
return `TOOL OBSERVATION
|
||||
================
|
||||
Tool: ${obs.tool_name}
|
||||
Time: ${new Date(obs.created_at_epoch).toISOString()}
|
||||
|
||||
Input:
|
||||
${JSON.stringify(JSON.parse(obs.tool_input), null, 2)}
|
||||
|
||||
Output:
|
||||
${JSON.stringify(JSON.parse(obs.tool_output), null, 2)}
|
||||
|
||||
ANALYSIS TASK
|
||||
-------------
|
||||
1. Does this observation contain something worth remembering?
|
||||
2. If YES: Output the observation in this EXACT XML format:
|
||||
|
||||
\`\`\`xml
|
||||
<observation>
|
||||
<type>feature</type>
|
||||
<text>Your concise observation here</text>
|
||||
</observation>
|
||||
\`\`\`
|
||||
|
||||
Requirements:
|
||||
- Use one of these types: decision, bugfix, feature, refactor, discovery
|
||||
- Keep text concise (one sentence preferred)
|
||||
- No markdown formatting inside <text>
|
||||
- No additional XML fields
|
||||
|
||||
3. If NO: Just acknowledge and wait for next observation
|
||||
|
||||
Remember: Quality over quantity. Only store meaningful insights.`;
|
||||
}
|
||||
```
|
||||
|
||||
### Finalization Prompt
|
||||
|
||||
```typescript
|
||||
function buildFinalizePrompt(session: SDKSession): string {
|
||||
return `SESSION ENDING
|
||||
==============
|
||||
The Claude Code session is finishing.
|
||||
|
||||
FINAL TASK
|
||||
----------
|
||||
1. Review the observations you've stored this session
|
||||
2. Generate a structured summary that answers these questions:
|
||||
- What did user request?
|
||||
- What did you investigate?
|
||||
- What did you learn?
|
||||
- What did you do?
|
||||
- What's next?
|
||||
- Files read
|
||||
- Files edited
|
||||
- Notes
|
||||
|
||||
3. Generate the structured summary and output it in this EXACT XML format:
|
||||
|
||||
\`\`\`xml
|
||||
<summary>
|
||||
<request>Implement JWT authentication system</request>
|
||||
<investigated>Existing auth middleware, session management, token storage patterns</investigated>
|
||||
<learned>Current system uses session cookies; no JWT support; race condition in middleware</learned>
|
||||
<completed>Implemented JWT token + refresh flow with 7-day expiry; fixed race condition with mutex; added token validation middleware</completed>
|
||||
<next_steps>Add token revocation API endpoint; write integration tests</next_steps>
|
||||
<files_read>
|
||||
<file>src/auth.ts</file>
|
||||
<file>src/middleware/session.ts</file>
|
||||
<file>src/types/user.ts</file>
|
||||
</files_read>
|
||||
<files_edited>
|
||||
<file>src/auth.ts</file>
|
||||
<file>src/middleware/auth.ts</file>
|
||||
<file>src/routes/auth.ts</file>
|
||||
</files_edited>
|
||||
<notes>Token secret stored in .env; refresh tokens use rotation strategy</notes>
|
||||
</summary>
|
||||
\`\`\`
|
||||
|
||||
Structure requirements:
|
||||
- <summary> is the root element
|
||||
- All 8 child elements are REQUIRED: request, investigated, learned, completed, next_steps, files_read, files_edited, notes
|
||||
- <files_read> and <files_edited> must contain <file> child elements (one per file)
|
||||
- If no files were read/edited, use empty tags: <files_read></files_read>
|
||||
- Text fields can be multiple sentences but avoid markdown formatting
|
||||
- Use underscores in element names: next_steps, files_read, files_edited
|
||||
|
||||
The SDK worker will parse the <summary> block and extract all fields to store in SQLite.
|
||||
|
||||
Generate the summary now in the required XML format.`;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hook Commands Architecture
|
||||
|
||||
All four hook commands (`claude-mem context`, `claude-mem new`, `claude-mem save`, `claude-mem summary`) are implemented as standalone TypeScript functions that:
|
||||
|
||||
1. **Use bun:sqlite directly** - No spawning child processes or CLI subcommands
|
||||
2. **Are self-contained** - Each hook has all the logic it needs
|
||||
3. **Share a common database layer** - Import from shared `db.ts` module
|
||||
4. **Never call other claude-mem commands** - All functionality via direct library calls
|
||||
|
||||
```typescript
|
||||
// Example structure
|
||||
import { Database } from 'bun:sqlite';
|
||||
|
||||
export function contextHook(stdin: HookInput) {
|
||||
const db = new Database('~/.claude-mem/db.sqlite');
|
||||
// Query and return context directly
|
||||
const summaries = db.query('SELECT ...').all();
|
||||
console.log(formatContext(summaries));
|
||||
db.close();
|
||||
}
|
||||
|
||||
export function saveHook(stdin: HookInput) {
|
||||
const db = new Database('~/.claude-mem/db.sqlite');
|
||||
// Insert observation directly
|
||||
db.run('INSERT INTO observation_queue ...', params);
|
||||
db.close();
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
}
|
||||
```
|
||||
|
||||
**Key principle:** Hooks are fast, synchronous database operations. The SDK worker process is where async/complex logic happens.
|
||||
|
||||
---
|
||||
|
||||
## Background Process Management
|
||||
|
||||
The `claude-mem save` hook just queues observations - processing happens in the background SDK worker process that polls the queue continuously.
|
||||
|
||||
The SDK worker is spawned by `claude-mem new` as a detached process and runs for the duration of the Claude Code session.
|
||||
|
||||
Benefits:
|
||||
- Works on all platforms (no systemd/launchd needed)
|
||||
- Self-contained (spawned and managed by claude-mem itself)
|
||||
- Simple state management (all state in SQLite)
|
||||
|
||||
---
|
||||
|
||||
## Advanced SDK Features
|
||||
|
||||
### Permission Integration (Future Enhancement)
|
||||
|
||||
The SDK provides a permission system that could be integrated with memory for context-aware decisions:
|
||||
|
||||
```typescript
|
||||
canUseTool: async (toolName, input) => {
|
||||
// Check memory for previous decisions about this tool/context
|
||||
const previousDecisions = await queryMemoryForTool(toolName, input);
|
||||
|
||||
if (previousDecisions.shouldAllow) {
|
||||
return {
|
||||
behavior: "allow",
|
||||
updatedInput: input
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
behavior: "ask_user",
|
||||
message: `This tool was previously flagged. Allow anyway?`
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This could enable:
|
||||
- Learning from previous tool use patterns
|
||||
- Automatically allowing/denying based on historical context
|
||||
- Providing smart defaults based on project-specific patterns
|
||||
|
||||
**Implementation priority:** Low (add after core functionality is stable)
|
||||
|
||||
### SDK Hook Configuration (Alternative to Claude Code Hooks)
|
||||
|
||||
Instead of using external command hooks via Claude Code settings.json, the SDK supports native hook configuration:
|
||||
|
||||
```typescript
|
||||
import { HookMatcher } from '@anthropic-ai/agent-sdk';
|
||||
|
||||
const response = query({
|
||||
prompt: messageGenerator(),
|
||||
options: {
|
||||
hooks: {
|
||||
'PreToolUse': [
|
||||
HookMatcher(matcher='Bash', hooks=[validateBashCommand]),
|
||||
HookMatcher(hooks=[logToolUse]) // Applies to all tools
|
||||
],
|
||||
'PostToolUse': [
|
||||
HookMatcher(hooks=[captureObservation])
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
type HookCallback = (
|
||||
input: HookInput,
|
||||
toolUseID: string | undefined,
|
||||
options: { signal: AbortSignal }
|
||||
) => Promise<HookJSONOutput>;
|
||||
```
|
||||
|
||||
**When to use SDK hooks vs Claude Code hooks:**
|
||||
- **Claude Code hooks**: For integrating with the main Claude Code session (our current approach)
|
||||
- **SDK hooks**: For controlling the memory agent's own tool usage (future enhancement)
|
||||
|
||||
**Implementation priority:** Medium (could simplify architecture, but adds complexity to migration)
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
**SDK worker failures:**
|
||||
- Each observation processing is atomic
|
||||
- Failed observations stay in queue
|
||||
- Next worker run retries
|
||||
- After 3 failures, mark observation as skipped
|
||||
- Use AbortController for graceful cancellation
|
||||
|
||||
**Abort signal handling:**
|
||||
```typescript
|
||||
try {
|
||||
for await (const msg of response) {
|
||||
if (abortController.signal.aborted) {
|
||||
throw new Error('Aborted');
|
||||
}
|
||||
// Process message
|
||||
}
|
||||
} catch (error) {
|
||||
if (abortController.signal.aborted) {
|
||||
// Clean shutdown
|
||||
await response.interrupt();
|
||||
} else {
|
||||
// Actual error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Database corruption:**
|
||||
- SQLite with WAL mode (write-ahead logging)
|
||||
- Regular backups to ~/.claude-mem/backups/
|
||||
- Automatic recovery from backups
|
||||
|
||||
**SDK API failures:**
|
||||
- Retry with exponential backoff
|
||||
- Don't block main Claude Code session
|
||||
- Log errors for debugging
|
||||
- Mark session as 'failed' after max retries
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. **Database setup** - Create tables and migration scripts
|
||||
2. **Hook commands** - Implement the 4 hook commands (context, new, save, summary)
|
||||
3. **SDK worker** - Implement the background worker process with response parsing
|
||||
4. **SDK prompts** - Wire up the prompts and message generator
|
||||
5. **Test end-to-end** - Run a real Claude Code session and verify it works
|
||||
|
||||
Start simple. Get one hook working before moving to the next. Don't try to build everything at once.
|
||||
|
||||
**Note:** MCP is only used for retrieval (when Claude Code needs to access stored memories), not for storage. The SDK agent stores data by outputting specially formatted text that the SDK worker parses and writes to SQLite.
|
||||
|
||||
### SDK Import Verification
|
||||
|
||||
Before implementing, verify the SDK exports match your usage:
|
||||
|
||||
```typescript
|
||||
// Required imports from @anthropic-ai/agent-sdk
|
||||
import { query } from '@anthropic-ai/agent-sdk';
|
||||
import type { Query, UserMessage, Options } from '@anthropic-ai/agent-sdk';
|
||||
|
||||
// Verify the query function signature:
|
||||
// function query(options: { prompt: string | AsyncIterable<UserMessage>; options?: Options }): Query
|
||||
|
||||
// Verify Query type:
|
||||
// interface Query extends AsyncGenerator<SDKMessage, void> {
|
||||
// interrupt(): Promise<void>;
|
||||
// setPermissionMode(mode: PermissionMode): Promise<void>;
|
||||
// }
|
||||
|
||||
// Verify UserMessage type:
|
||||
// type UserMessage = { role: "user"; content: string }
|
||||
```
|
||||
|
||||
If the SDK exports differ from this structure, adjust the implementation accordingly. The SDK documentation should be the source of truth.
|
||||
|
||||
---
|
||||
|
||||
## Key Corrections from Agent SDK Documentation
|
||||
|
||||
This refactor plan has been updated to align with the official Agent SDK documentation. Key corrections include:
|
||||
|
||||
### 1. Session ID Management
|
||||
- **Before:** Captured session ID once in UserPromptSubmit hook
|
||||
- **After:** Capture from system init message and track updates continuously
|
||||
- **Why:** Session IDs change with each conversation turn
|
||||
|
||||
### 2. Hook Configuration
|
||||
- **Before:** Mixed up SDK hook format with Claude Code hook format
|
||||
- **After:** Clarified that Claude Code uses settings.json format (external commands); SDK uses TypeScript HookMatcher (programmatic callbacks)
|
||||
- **Why:** Two separate hook systems with different purposes and configuration methods
|
||||
- **Our approach:** Use Claude Code hooks to observe the main session; SDK hooks are future enhancement
|
||||
|
||||
### 3. Message Generator and Query Interface
|
||||
- **Before:** Custom SDKMessage type with nested message structure
|
||||
- **After:** Simple UserMessage type `{ role: "user", content: string }` yielded from AsyncIterable
|
||||
- **Why:** SDK expects AsyncIterable<UserMessage>, not a custom wrapper format
|
||||
- **Query type:** Properly typed as `Query` which extends AsyncGenerator with interrupt() and setPermissionMode()
|
||||
|
||||
### 4. Tool Filtering
|
||||
- **Before:** Filter "boring tools" in PostToolUse hook
|
||||
- **After:** Use SDK's `disallowedTools` option in query configuration
|
||||
- **Why:** More efficient to prevent SDK from using tools entirely
|
||||
|
||||
### 5. Model Identifier
|
||||
- **Before:** Used `claude-haiku-4-5-20251001` (undocumented)
|
||||
- **After:** Use `claude-sonnet-4-5` (documented model name)
|
||||
- **Why:** Stick to documented model identifiers for stability
|
||||
|
||||
### 6. Error Handling
|
||||
- **Before:** Custom error handling without SDK features
|
||||
- **After:** Use AbortController and response.interrupt() for graceful cancellation
|
||||
- **Why:** SDK provides built-in cancellation mechanisms
|
||||
|
||||
### 7. Session Resumption
|
||||
- **Before:** Manual session state reconstruction
|
||||
- **After:** Leverage SDK's built-in `resume: sessionId` option
|
||||
- **Why:** SDK already handles session resumption
|
||||
|
||||
### Future Enhancements to Consider
|
||||
|
||||
1. **Permission integration** - Use canUseTool callback to make memory-aware decisions
|
||||
2. **SDK native hooks** - Replace external command hooks with SDK HookMatcher
|
||||
3. **Better session recovery** - Use SDK resumption for interrupted sessions
|
||||
|
||||
These corrections ensure our implementation follows Agent SDK best practices and avoids reinventing functionality the SDK already provides.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Validation Summary
|
||||
|
||||
This plan has been validated against the official Agent SDK documentation and confirmed to be architecturally sound.
|
||||
|
||||
### ✅ Validated Design Decisions
|
||||
|
||||
1. **Hook System Usage** - Correctly uses Claude Code external command hooks for observation; SDK programmatic hooks reserved for future enhancement
|
||||
2. **Query Function Interface** - Properly implements AsyncIterable<UserMessage> for streaming input mode
|
||||
3. **Session Management** - Leverages SDK's built-in session resumption instead of manual state reconstruction
|
||||
4. **Tool Filtering** - Uses SDK's `disallowedTools` option for efficiency
|
||||
5. **Error Handling** - Implements AbortController and interrupt() for graceful cancellation
|
||||
6. **Separation of Concerns** - Clean isolation between main Claude Code session and background SDK worker
|
||||
|
||||
### 🎯 Architecture Strengths
|
||||
|
||||
- **Non-blocking** - Hooks are fast database operations; complex logic happens in background
|
||||
- **Queue-based** - Handles parallel hook execution correctly via observation_queue table
|
||||
- **Fault-tolerant** - Failed observations stay in queue for retry; graceful degradation
|
||||
- **Platform-agnostic** - No dependency on systemd/launchd; works everywhere
|
||||
- **Type-safe** - Uses official SDK TypeScript types throughout
|
||||
|
||||
### 📋 Pre-Implementation Checklist
|
||||
|
||||
Before starting implementation, verify:
|
||||
|
||||
1. [ ] Agent SDK installed and accessible: `@anthropic-ai/agent-sdk`
|
||||
2. [ ] Verify SDK exports match expected structure (query, Query, UserMessage types)
|
||||
3. [ ] SQLite database location decided: `~/.claude-mem/db.sqlite`
|
||||
4. [ ] Claude Code settings.json hook configuration tested
|
||||
5. [ ] Background process spawning works on target platform (test detached process)
|
||||
|
||||
### 🚀 Ready for Implementation
|
||||
|
||||
The architecture is validated and ready for implementation. Follow the phased approach:
|
||||
|
||||
1. Database setup first (get schema working with bun:sqlite)
|
||||
2. Implement hooks one at a time (start with `context`, then `save`)
|
||||
3. Build SDK worker with simple message generator
|
||||
4. Test end-to-end with a real Claude Code session
|
||||
5. Iterate and refine based on real-world usage
|
||||
|
||||
**Remember:** Start simple, get one piece working, then build on it. Don't try to implement everything at once.
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
|
||||
# Claude Code Hooks Exit Code Cheat Sheet
|
||||
|
||||
## Exit Code Behavior [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output)
|
||||
|
||||
- **Exit code 0**: Success. `stdout` is shown to the user in transcript mode, except for `UserPromptSubmit` hook where stdout is injected as context [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output)
|
||||
- **Exit code 2**: Blocking error. `stderr` is fed back to Claude to process automatically [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output)
|
||||
- **Other exit codes**: Non-blocking error. `stderr` is shown to the user and execution continues [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output)
|
||||
|
||||
## Per-Hook Event Behavior [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output)
|
||||
|
||||
| Hook Event | Exit Code 2 Behavior |
|
||||
|------------|---------------------|
|
||||
| `PreToolUse` | Blocks the tool call, shows stderr to Claude [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output) |
|
||||
| `PostToolUse` | Shows stderr to Claude (tool already ran) [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output) |
|
||||
| `Notification` | N/A, shows stderr to user only [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output) |
|
||||
| `UserPromptSubmit` | Blocks prompt processing, erases prompt, shows stderr to user only [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output) |
|
||||
| `Stop` | Blocks stoppage, shows stderr to Claude [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output) |
|
||||
| `SubagentStop` | Blocks stoppage, shows stderr to Claude subagent [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output) |
|
||||
| `PreCompact` | N/A, shows stderr to user only [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output) |
|
||||
| `SessionStart` | N/A, shows stderr to user only [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output) |
|
||||
| `SessionEnd` | N/A, shows stderr to user only [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output) |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
- **Success**: `process.exit(0)` - Operation completed successfully
|
||||
- **Block & feedback**: `process.exit(2)` - Block operation and give Claude feedback via stderr
|
||||
- **Non-blocking error**: `process.exit(1)` - Show error to user but continue execution
|
||||
|
||||
**Important**: Claude Code does not see stdout if the exit code is 0, except for the `UserPromptSubmit` hook where stdout is injected as context [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output)
|
||||
@@ -0,0 +1,837 @@
|
||||
# Hooks reference
|
||||
|
||||
> This page provides reference documentation for implementing hooks in Claude Code.
|
||||
|
||||
<Tip>
|
||||
For a quickstart guide with examples, see [Get started with Claude Code hooks](/en/docs/claude-code/hooks-guide).
|
||||
</Tip>
|
||||
|
||||
## Configuration
|
||||
|
||||
Claude Code hooks are configured in your [settings files](/en/docs/claude-code/settings):
|
||||
|
||||
* `~/.claude/settings.json` - User settings
|
||||
* `.claude/settings.json` - Project settings
|
||||
* `.claude/settings.local.json` - Local project settings (not committed)
|
||||
* Enterprise managed policy settings
|
||||
|
||||
### Structure
|
||||
|
||||
Hooks are organized by matchers, where each matcher can have multiple hooks:
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"hooks": {
|
||||
"EventName": [
|
||||
{
|
||||
"matcher": "ToolPattern",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "your-command-here"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* **matcher**: Pattern to match tool names, case-sensitive (only applicable for
|
||||
`PreToolUse` and `PostToolUse`)
|
||||
* Simple strings match exactly: `Write` matches only the Write tool
|
||||
* Supports regex: `Edit|Write` or `Notebook.*`
|
||||
* Use `*` to match all tools. You can also use empty string (`""`) or leave
|
||||
`matcher` blank.
|
||||
* **hooks**: Array of commands to execute when the pattern matches
|
||||
* `type`: Currently only `"command"` is supported
|
||||
* `command`: The bash command to execute (can use `$CLAUDE_PROJECT_DIR`
|
||||
environment variable)
|
||||
* `timeout`: (Optional) How long a command should run, in seconds, before
|
||||
canceling that specific command.
|
||||
|
||||
For events like `UserPromptSubmit`, `Notification`, `Stop`, and `SubagentStop`
|
||||
that don't use matchers, you can omit the matcher field:
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "/path/to/prompt-validator.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Project-Specific Hook Scripts
|
||||
|
||||
You can use the environment variable `CLAUDE_PROJECT_DIR` (only available when
|
||||
Claude Code spawns the hook command) to reference scripts stored in your project,
|
||||
ensuring they work regardless of Claude's current directory:
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-style.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Plugin hooks
|
||||
|
||||
[Plugins](/en/docs/claude-code/plugins) can provide hooks that integrate seamlessly with your user and project hooks. Plugin hooks are automatically merged with your configuration when plugins are enabled.
|
||||
|
||||
**How plugin hooks work**:
|
||||
|
||||
* Plugin hooks are defined in the plugin's `hooks/hooks.json` file or in a file given by a custom path to the `hooks` field.
|
||||
* When a plugin is enabled, its hooks are merged with user and project hooks
|
||||
* Multiple hooks from different sources can respond to the same event
|
||||
* Plugin hooks use the `${CLAUDE_PLUGIN_ROOT}` environment variable to reference plugin files
|
||||
|
||||
**Example plugin hook configuration**:
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"description": "Automatic code formatting",
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
Plugin hooks use the same format as regular hooks with an optional `description` field to explain the hook's purpose.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
Plugin hooks run alongside your custom hooks. If multiple hooks match an event, they all execute in parallel.
|
||||
</Note>
|
||||
|
||||
**Environment variables for plugins**:
|
||||
|
||||
* `${CLAUDE_PLUGIN_ROOT}`: Absolute path to the plugin directory
|
||||
* `${CLAUDE_PROJECT_DIR}`: Project root directory (same as for project hooks)
|
||||
* All standard environment variables are available
|
||||
|
||||
See the [plugin components reference](/en/docs/claude-code/plugins-reference#hooks) for details on creating plugin hooks.
|
||||
|
||||
## Hook Events
|
||||
|
||||
### PreToolUse
|
||||
|
||||
Runs after Claude creates tool parameters and before processing the tool call.
|
||||
|
||||
**Common matchers:**
|
||||
|
||||
* `Task` - Subagent tasks (see [subagents documentation](/en/docs/claude-code/sub-agents))
|
||||
* `Bash` - Shell commands
|
||||
* `Glob` - File pattern matching
|
||||
* `Grep` - Content search
|
||||
* `Read` - File reading
|
||||
* `Edit` - File editing
|
||||
* `Write` - File writing
|
||||
* `WebFetch`, `WebSearch` - Web operations
|
||||
|
||||
### PostToolUse
|
||||
|
||||
Runs immediately after a tool completes successfully.
|
||||
|
||||
Recognizes the same matcher values as PreToolUse.
|
||||
|
||||
### Notification
|
||||
|
||||
Runs when Claude Code sends notifications. Notifications are sent when:
|
||||
|
||||
1. Claude needs your permission to use a tool. Example: "Claude needs your
|
||||
permission to use Bash"
|
||||
2. The prompt input has been idle for at least 60 seconds. "Claude is waiting
|
||||
for your input"
|
||||
|
||||
### UserPromptSubmit
|
||||
|
||||
Runs when the user submits a prompt, before Claude processes it. This allows you
|
||||
to add additional context based on the prompt/conversation, validate prompts, or
|
||||
block certain types of prompts.
|
||||
|
||||
### Stop
|
||||
|
||||
Runs when the main Claude Code agent has finished responding. Does not run if
|
||||
the stoppage occurred due to a user interrupt.
|
||||
|
||||
### SubagentStop
|
||||
|
||||
Runs when a Claude Code subagent (Task tool call) has finished responding.
|
||||
|
||||
### PreCompact
|
||||
|
||||
Runs before Claude Code is about to run a compact operation.
|
||||
|
||||
**Matchers:**
|
||||
|
||||
* `manual` - Invoked from `/compact`
|
||||
* `auto` - Invoked from auto-compact (due to full context window)
|
||||
|
||||
### SessionStart
|
||||
|
||||
Runs when Claude Code starts a new session or resumes an existing session (which
|
||||
currently does start a new session under the hood). Useful for loading in
|
||||
development context like existing issues or recent changes to your codebase.
|
||||
|
||||
**Matchers:**
|
||||
|
||||
* `startup` - Invoked from startup
|
||||
* `resume` - Invoked from `--resume`, `--continue`, or `/resume`
|
||||
* `clear` - Invoked from `/clear`
|
||||
* `compact` - Invoked from auto or manual compact.
|
||||
|
||||
### SessionEnd
|
||||
|
||||
Runs when a Claude Code session ends. Useful for cleanup tasks, logging session
|
||||
statistics, or saving session state.
|
||||
|
||||
The `reason` field in the hook input will be one of:
|
||||
|
||||
* `clear` - Session cleared with /clear command
|
||||
* `logout` - User logged out
|
||||
* `prompt_input_exit` - User exited while prompt input was visible
|
||||
* `other` - Other exit reasons
|
||||
|
||||
## Hook Input
|
||||
|
||||
Hooks receive JSON data via stdin containing session information and
|
||||
event-specific data:
|
||||
|
||||
```typescript theme={null}
|
||||
{
|
||||
// Common fields
|
||||
session_id: string
|
||||
transcript_path: string // Path to conversation JSON
|
||||
cwd: string // The current working directory when the hook is invoked
|
||||
|
||||
// Event-specific fields
|
||||
hook_event_name: string
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### PreToolUse Input
|
||||
|
||||
The exact schema for `tool_input` depends on the tool.
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
|
||||
"cwd": "/Users/...",
|
||||
"hook_event_name": "PreToolUse",
|
||||
"tool_name": "Write",
|
||||
"tool_input": {
|
||||
"file_path": "/path/to/file.txt",
|
||||
"content": "file content"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PostToolUse Input
|
||||
|
||||
The exact schema for `tool_input` and `tool_response` depends on the tool.
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
|
||||
"cwd": "/Users/...",
|
||||
"hook_event_name": "PostToolUse",
|
||||
"tool_name": "Write",
|
||||
"tool_input": {
|
||||
"file_path": "/path/to/file.txt",
|
||||
"content": "file content"
|
||||
},
|
||||
"tool_response": {
|
||||
"filePath": "/path/to/file.txt",
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Notification Input
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
|
||||
"cwd": "/Users/...",
|
||||
"hook_event_name": "Notification",
|
||||
"message": "Task completed successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### UserPromptSubmit Input
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
|
||||
"cwd": "/Users/...",
|
||||
"hook_event_name": "UserPromptSubmit",
|
||||
"prompt": "Write a function to calculate the factorial of a number"
|
||||
}
|
||||
```
|
||||
|
||||
### Stop and SubagentStop Input
|
||||
|
||||
`stop_hook_active` is true when Claude Code is already continuing as a result of
|
||||
a stop hook. Check this value or process the transcript to prevent Claude Code
|
||||
from running indefinitely.
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
|
||||
"hook_event_name": "Stop",
|
||||
"stop_hook_active": true
|
||||
}
|
||||
```
|
||||
|
||||
### PreCompact Input
|
||||
|
||||
For `manual`, `custom_instructions` comes from what the user passes into
|
||||
`/compact`. For `auto`, `custom_instructions` is empty.
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
|
||||
"hook_event_name": "PreCompact",
|
||||
"trigger": "manual",
|
||||
"custom_instructions": ""
|
||||
}
|
||||
```
|
||||
|
||||
### SessionStart Input
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
|
||||
"hook_event_name": "SessionStart",
|
||||
"source": "startup"
|
||||
}
|
||||
```
|
||||
|
||||
### SessionEnd Input
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
|
||||
"cwd": "/Users/...",
|
||||
"hook_event_name": "SessionEnd",
|
||||
"reason": "exit"
|
||||
}
|
||||
```
|
||||
|
||||
## Hook Output
|
||||
|
||||
There are two ways for hooks to return output back to Claude Code. The output
|
||||
communicates whether to block and any feedback that should be shown to Claude
|
||||
and the user.
|
||||
|
||||
### Simple: Exit Code
|
||||
|
||||
Hooks communicate status through exit codes, stdout, and stderr:
|
||||
|
||||
* **Exit code 0**: Success. `stdout` is shown to the user in transcript mode
|
||||
(CTRL-R), except for `UserPromptSubmit` and `SessionStart`, where stdout is
|
||||
added to the context.
|
||||
* **Exit code 2**: Blocking error. `stderr` is fed back to Claude to process
|
||||
automatically. See per-hook-event behavior below.
|
||||
* **Other exit codes**: Non-blocking error. `stderr` is shown to the user and
|
||||
execution continues.
|
||||
|
||||
<Warning>
|
||||
Reminder: Claude Code does not see stdout if the exit code is 0, except for
|
||||
the `UserPromptSubmit` hook where stdout is injected as context.
|
||||
</Warning>
|
||||
|
||||
#### Exit Code 2 Behavior
|
||||
|
||||
| Hook Event | Behavior |
|
||||
| ------------------ | ------------------------------------------------------------------ |
|
||||
| `PreToolUse` | Blocks the tool call, shows stderr to Claude |
|
||||
| `PostToolUse` | Shows stderr to Claude (tool already ran) |
|
||||
| `Notification` | N/A, shows stderr to user only |
|
||||
| `UserPromptSubmit` | Blocks prompt processing, erases prompt, shows stderr to user only |
|
||||
| `Stop` | Blocks stoppage, shows stderr to Claude |
|
||||
| `SubagentStop` | Blocks stoppage, shows stderr to Claude subagent |
|
||||
| `PreCompact` | N/A, shows stderr to user only |
|
||||
| `SessionStart` | N/A, shows stderr to user only |
|
||||
| `SessionEnd` | N/A, shows stderr to user only |
|
||||
|
||||
### Advanced: JSON Output
|
||||
|
||||
Hooks can return structured JSON in `stdout` for more sophisticated control:
|
||||
|
||||
#### Common JSON Fields
|
||||
|
||||
All hook types can include these optional fields:
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"continue": true, // Whether Claude should continue after hook execution (default: true)
|
||||
"stopReason": "string", // Message shown when continue is false
|
||||
|
||||
"suppressOutput": true, // Hide stdout from transcript mode (default: false)
|
||||
"systemMessage": "string" // Optional warning message shown to the user
|
||||
}
|
||||
```
|
||||
|
||||
If `continue` is false, Claude stops processing after the hooks run.
|
||||
|
||||
* For `PreToolUse`, this is different from `"permissionDecision": "deny"`, which
|
||||
only blocks a specific tool call and provides automatic feedback to Claude.
|
||||
* For `PostToolUse`, this is different from `"decision": "block"`, which
|
||||
provides automated feedback to Claude.
|
||||
* For `UserPromptSubmit`, this prevents the prompt from being processed.
|
||||
* For `Stop` and `SubagentStop`, this takes precedence over any
|
||||
`"decision": "block"` output.
|
||||
* In all cases, `"continue" = false` takes precedence over any
|
||||
`"decision": "block"` output.
|
||||
|
||||
`stopReason` accompanies `continue` with a reason shown to the user, not shown
|
||||
to Claude.
|
||||
|
||||
#### `PreToolUse` Decision Control
|
||||
|
||||
`PreToolUse` hooks can control whether a tool call proceeds.
|
||||
|
||||
* `"allow"` bypasses the permission system. `permissionDecisionReason` is shown
|
||||
to the user but not to Claude.
|
||||
* `"deny"` prevents the tool call from executing. `permissionDecisionReason` is
|
||||
shown to Claude.
|
||||
* `"ask"` asks the user to confirm the tool call in the UI.
|
||||
`permissionDecisionReason` is shown to the user but not to Claude.
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "allow" | "deny" | "ask",
|
||||
"permissionDecisionReason": "My reason here"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
The `decision` and `reason` fields are deprecated for PreToolUse hooks.
|
||||
Use `hookSpecificOutput.permissionDecision` and
|
||||
`hookSpecificOutput.permissionDecisionReason` instead. The deprecated fields
|
||||
`"approve"` and `"block"` map to `"allow"` and `"deny"` respectively.
|
||||
</Note>
|
||||
|
||||
#### `PostToolUse` Decision Control
|
||||
|
||||
`PostToolUse` hooks can provide feedback to Claude after tool execution.
|
||||
|
||||
* `"block"` automatically prompts Claude with `reason`.
|
||||
* `undefined` does nothing. `reason` is ignored.
|
||||
* `"hookSpecificOutput.additionalContext"` adds context for Claude to consider.
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"decision": "block" | undefined,
|
||||
"reason": "Explanation for decision",
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PostToolUse",
|
||||
"additionalContext": "Additional information for Claude"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `UserPromptSubmit` Decision Control
|
||||
|
||||
`UserPromptSubmit` hooks can control whether a user prompt is processed.
|
||||
|
||||
* `"block"` prevents the prompt from being processed. The submitted prompt is
|
||||
erased from context. `"reason"` is shown to the user but not added to context.
|
||||
* `undefined` allows the prompt to proceed normally. `"reason"` is ignored.
|
||||
* `"hookSpecificOutput.additionalContext"` adds the string to the context if not
|
||||
blocked.
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"decision": "block" | undefined,
|
||||
"reason": "Explanation for decision",
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "UserPromptSubmit",
|
||||
"additionalContext": "My additional context here"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `Stop`/`SubagentStop` Decision Control
|
||||
|
||||
`Stop` and `SubagentStop` hooks can control whether Claude must continue.
|
||||
|
||||
* `"block"` prevents Claude from stopping. You must populate `reason` for Claude
|
||||
to know how to proceed.
|
||||
* `undefined` allows Claude to stop. `reason` is ignored.
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"decision": "block" | undefined,
|
||||
"reason": "Must be provided when Claude is blocked from stopping"
|
||||
}
|
||||
```
|
||||
|
||||
#### `SessionStart` Decision Control
|
||||
|
||||
`SessionStart` hooks allow you to load in context at the start of a session.
|
||||
|
||||
* `"hookSpecificOutput.additionalContext"` adds the string to the context.
|
||||
* Multiple hooks' `additionalContext` values are concatenated.
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": "My additional context here"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `SessionEnd` Decision Control
|
||||
|
||||
`SessionEnd` hooks run when a session ends. They cannot block session termination
|
||||
but can perform cleanup tasks.
|
||||
|
||||
#### Exit Code Example: Bash Command Validation
|
||||
|
||||
```python theme={null}
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
# Define validation rules as a list of (regex pattern, message) tuples
|
||||
VALIDATION_RULES = [
|
||||
(
|
||||
r"\bgrep\b(?!.*\|)",
|
||||
"Use 'rg' (ripgrep) instead of 'grep' for better performance and features",
|
||||
),
|
||||
(
|
||||
r"\bfind\s+\S+\s+-name\b",
|
||||
"Use 'rg --files | rg pattern' or 'rg --files -g pattern' instead of 'find -name' for better performance",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def validate_command(command: str) -> list[str]:
|
||||
issues = []
|
||||
for pattern, message in VALIDATION_RULES:
|
||||
if re.search(pattern, command):
|
||||
issues.append(message)
|
||||
return issues
|
||||
|
||||
|
||||
try:
|
||||
input_data = json.load(sys.stdin)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
tool_name = input_data.get("tool_name", "")
|
||||
tool_input = input_data.get("tool_input", {})
|
||||
command = tool_input.get("command", "")
|
||||
|
||||
if tool_name != "Bash" or not command:
|
||||
sys.exit(1)
|
||||
|
||||
# Validate the command
|
||||
issues = validate_command(command)
|
||||
|
||||
if issues:
|
||||
for message in issues:
|
||||
print(f"• {message}", file=sys.stderr)
|
||||
# Exit code 2 blocks tool call and shows stderr to Claude
|
||||
sys.exit(2)
|
||||
```
|
||||
|
||||
#### JSON Output Example: UserPromptSubmit to Add Context and Validation
|
||||
|
||||
<Note>
|
||||
For `UserPromptSubmit` hooks, you can inject context using either method:
|
||||
|
||||
* Exit code 0 with stdout: Claude sees the context (special case for `UserPromptSubmit`)
|
||||
* JSON output: Provides more control over the behavior
|
||||
</Note>
|
||||
|
||||
```python theme={null}
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import sys
|
||||
import re
|
||||
import datetime
|
||||
|
||||
# Load input from stdin
|
||||
try:
|
||||
input_data = json.load(sys.stdin)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
prompt = input_data.get("prompt", "")
|
||||
|
||||
# Check for sensitive patterns
|
||||
sensitive_patterns = [
|
||||
(r"(?i)\b(password|secret|key|token)\s*[:=]", "Prompt contains potential secrets"),
|
||||
]
|
||||
|
||||
for pattern, message in sensitive_patterns:
|
||||
if re.search(pattern, prompt):
|
||||
# Use JSON output to block with a specific reason
|
||||
output = {
|
||||
"decision": "block",
|
||||
"reason": f"Security policy violation: {message}. Please rephrase your request without sensitive information."
|
||||
}
|
||||
print(json.dumps(output))
|
||||
sys.exit(0)
|
||||
|
||||
# Add current time to context
|
||||
context = f"Current time: {datetime.datetime.now()}"
|
||||
print(context)
|
||||
|
||||
"""
|
||||
The following is also equivalent:
|
||||
print(json.dumps({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "UserPromptSubmit",
|
||||
"additionalContext": context,
|
||||
},
|
||||
}))
|
||||
"""
|
||||
|
||||
# Allow the prompt to proceed with the additional context
|
||||
sys.exit(0)
|
||||
```
|
||||
|
||||
#### JSON Output Example: PreToolUse with Approval
|
||||
|
||||
```python theme={null}
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import sys
|
||||
|
||||
# Load input from stdin
|
||||
try:
|
||||
input_data = json.load(sys.stdin)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
tool_name = input_data.get("tool_name", "")
|
||||
tool_input = input_data.get("tool_input", {})
|
||||
|
||||
# Example: Auto-approve file reads for documentation files
|
||||
if tool_name == "Read":
|
||||
file_path = tool_input.get("file_path", "")
|
||||
if file_path.endswith((".md", ".mdx", ".txt", ".json")):
|
||||
# Use JSON output to auto-approve the tool call
|
||||
output = {
|
||||
"decision": "approve",
|
||||
"reason": "Documentation file auto-approved",
|
||||
"suppressOutput": True # Don't show in transcript mode
|
||||
}
|
||||
print(json.dumps(output))
|
||||
sys.exit(0)
|
||||
|
||||
# For other cases, let the normal permission flow proceed
|
||||
sys.exit(0)
|
||||
```
|
||||
|
||||
## Working with MCP Tools
|
||||
|
||||
Claude Code hooks work seamlessly with
|
||||
[Model Context Protocol (MCP) tools](/en/docs/claude-code/mcp). When MCP servers
|
||||
provide tools, they appear with a special naming pattern that you can match in
|
||||
your hooks.
|
||||
|
||||
### MCP Tool Naming
|
||||
|
||||
MCP tools follow the pattern `mcp__<server>__<tool>`, for example:
|
||||
|
||||
* `mcp__memory__create_entities` - Memory server's create entities tool
|
||||
* `mcp__filesystem__read_file` - Filesystem server's read file tool
|
||||
* `mcp__github__search_repositories` - GitHub server's search tool
|
||||
|
||||
### Configuring Hooks for MCP Tools
|
||||
|
||||
You can target specific MCP tools or entire MCP servers:
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "mcp__memory__.*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "echo 'Memory operation initiated' >> ~/mcp-operations.log"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "mcp__.*__write.*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "/home/user/scripts/validate-mcp-write.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
<Tip>
|
||||
For practical examples including code formatting, notifications, and file protection, see [More Examples](/en/docs/claude-code/hooks-guide#more-examples) in the get started guide.
|
||||
</Tip>
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Disclaimer
|
||||
|
||||
**USE AT YOUR OWN RISK**: Claude Code hooks execute arbitrary shell commands on
|
||||
your system automatically. By using hooks, you acknowledge that:
|
||||
|
||||
* You are solely responsible for the commands you configure
|
||||
* Hooks can modify, delete, or access any files your user account can access
|
||||
* Malicious or poorly written hooks can cause data loss or system damage
|
||||
* Anthropic provides no warranty and assumes no liability for any damages
|
||||
resulting from hook usage
|
||||
* You should thoroughly test hooks in a safe environment before production use
|
||||
|
||||
Always review and understand any hook commands before adding them to your
|
||||
configuration.
|
||||
|
||||
### Security Best Practices
|
||||
|
||||
Here are some key practices for writing more secure hooks:
|
||||
|
||||
1. **Validate and sanitize inputs** - Never trust input data blindly
|
||||
2. **Always quote shell variables** - Use `"$VAR"` not `$VAR`
|
||||
3. **Block path traversal** - Check for `..` in file paths
|
||||
4. **Use absolute paths** - Specify full paths for scripts (use
|
||||
"\$CLAUDE\_PROJECT\_DIR" for the project path)
|
||||
5. **Skip sensitive files** - Avoid `.env`, `.git/`, keys, etc.
|
||||
|
||||
### Configuration Safety
|
||||
|
||||
Direct edits to hooks in settings files don't take effect immediately. Claude
|
||||
Code:
|
||||
|
||||
1. Captures a snapshot of hooks at startup
|
||||
2. Uses this snapshot throughout the session
|
||||
3. Warns if hooks are modified externally
|
||||
4. Requires review in `/hooks` menu for changes to apply
|
||||
|
||||
This prevents malicious hook modifications from affecting your current session.
|
||||
|
||||
## Hook Execution Details
|
||||
|
||||
* **Timeout**: 60-second execution limit by default, configurable per command.
|
||||
* A timeout for an individual command does not affect the other commands.
|
||||
* **Parallelization**: All matching hooks run in parallel
|
||||
* **Deduplication**: Multiple identical hook commands are deduplicated automatically
|
||||
* **Environment**: Runs in current directory with Claude Code's environment
|
||||
* The `CLAUDE_PROJECT_DIR` environment variable is available and contains the
|
||||
absolute path to the project root directory (where Claude Code was started)
|
||||
* **Input**: JSON via stdin
|
||||
* **Output**:
|
||||
* PreToolUse/PostToolUse/Stop/SubagentStop: Progress shown in transcript (Ctrl-R)
|
||||
* Notification/SessionEnd: Logged to debug only (`--debug`)
|
||||
* UserPromptSubmit/SessionStart: stdout added as context for Claude
|
||||
|
||||
## Debugging
|
||||
|
||||
### Basic Troubleshooting
|
||||
|
||||
If your hooks aren't working:
|
||||
|
||||
1. **Check configuration** - Run `/hooks` to see if your hook is registered
|
||||
2. **Verify syntax** - Ensure your JSON settings are valid
|
||||
3. **Test commands** - Run hook commands manually first
|
||||
4. **Check permissions** - Make sure scripts are executable
|
||||
5. **Review logs** - Use `claude --debug` to see hook execution details
|
||||
|
||||
Common issues:
|
||||
|
||||
* **Quotes not escaped** - Use `\"` inside JSON strings
|
||||
* **Wrong matcher** - Check tool names match exactly (case-sensitive)
|
||||
* **Command not found** - Use full paths for scripts
|
||||
|
||||
### Advanced Debugging
|
||||
|
||||
For complex hook issues:
|
||||
|
||||
1. **Inspect hook execution** - Use `claude --debug` to see detailed hook
|
||||
execution
|
||||
2. **Validate JSON schemas** - Test hook input/output with external tools
|
||||
3. **Check environment variables** - Verify Claude Code's environment is correct
|
||||
4. **Test edge cases** - Try hooks with unusual file paths or inputs
|
||||
5. **Monitor system resources** - Check for resource exhaustion during hook
|
||||
execution
|
||||
6. **Use structured logging** - Implement logging in your hook scripts
|
||||
|
||||
### Debug Output Example
|
||||
|
||||
Use `claude --debug` to see hook execution details:
|
||||
|
||||
```
|
||||
[DEBUG] Executing hooks for PostToolUse:Write
|
||||
[DEBUG] Getting matching hook commands for PostToolUse with query: Write
|
||||
[DEBUG] Found 1 hook matchers in settings
|
||||
[DEBUG] Matched 1 hooks for query "Write"
|
||||
[DEBUG] Found 1 hook commands to execute
|
||||
[DEBUG] Executing hook command: <Your command> with timeout 60000ms
|
||||
[DEBUG] Hook command completed with status 0: <Your stdout>
|
||||
```
|
||||
|
||||
Progress messages appear in transcript mode (Ctrl-R) showing:
|
||||
|
||||
* Which hook is running
|
||||
* Command being executed
|
||||
* Success/failure status
|
||||
* Output or error messages
|
||||
@@ -0,0 +1,391 @@
|
||||
# Plugins
|
||||
|
||||
> Extend Claude Code with custom commands, agents, hooks, and MCP servers through the plugin system.
|
||||
|
||||
<Tip>
|
||||
For complete technical specifications and schemas, see [Plugins reference](/en/docs/claude-code/plugins-reference). For marketplace management, see [Plugin marketplaces](/en/docs/claude-code/plugin-marketplaces).
|
||||
</Tip>
|
||||
|
||||
Plugins let you extend Claude Code with custom functionality that can be shared across projects and teams. Install plugins from [marketplaces](/en/docs/claude-code/plugin-marketplaces) to add pre-built commands, agents, hooks, and MCP servers, or create your own to automate your workflows.
|
||||
|
||||
## Quickstart
|
||||
|
||||
Let's create a simple greeting plugin to get you familiar with the plugin system. We'll build a working plugin that adds a custom command, test it locally, and understand the core concepts.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
* Claude Code installed on your machine
|
||||
* Basic familiarity with command-line tools
|
||||
|
||||
### Create your first plugin
|
||||
|
||||
<Steps>
|
||||
<Step title="Create the marketplace structure">
|
||||
```bash theme={null}
|
||||
mkdir test-marketplace
|
||||
cd test-marketplace
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Create the plugin directory">
|
||||
```bash theme={null}
|
||||
mkdir my-first-plugin
|
||||
cd my-first-plugin
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Create the plugin manifest">
|
||||
```bash Create .claude-plugin/plugin.json theme={null}
|
||||
mkdir .claude-plugin
|
||||
cat > .claude-plugin/plugin.json << 'EOF'
|
||||
{
|
||||
"name": "my-first-plugin",
|
||||
"description": "A simple greeting plugin to learn the basics",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Your Name"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Add a custom command">
|
||||
```bash Create commands/hello.md theme={null}
|
||||
mkdir commands
|
||||
cat > commands/hello.md << 'EOF'
|
||||
---
|
||||
description: Greet the user with a personalized message
|
||||
---
|
||||
|
||||
# Hello Command
|
||||
|
||||
Greet the user warmly and ask how you can help them today. Make the greeting personal and encouraging.
|
||||
EOF
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Create the marketplace manifest">
|
||||
```bash Create marketplace.json theme={null}
|
||||
cd ..
|
||||
mkdir .claude-plugin
|
||||
cat > .claude-plugin/marketplace.json << 'EOF'
|
||||
{
|
||||
"name": "test-marketplace",
|
||||
"owner": {
|
||||
"name": "Test User"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "my-first-plugin",
|
||||
"source": "./my-first-plugin",
|
||||
"description": "My first test plugin"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Install and test your plugin">
|
||||
```bash Start Claude Code from parent directory theme={null}
|
||||
cd ..
|
||||
claude
|
||||
```
|
||||
|
||||
```shell Add the test marketplace theme={null}
|
||||
/plugin marketplace add ./test-marketplace
|
||||
```
|
||||
|
||||
```shell Install your plugin theme={null}
|
||||
/plugin install my-first-plugin@test-marketplace
|
||||
```
|
||||
|
||||
Select "Install now". You'll then need to restart Claude Code in order to use the new plugin.
|
||||
|
||||
```shell Try your new command theme={null}
|
||||
/hello
|
||||
```
|
||||
|
||||
You'll see Claude use your greeting command! Check `/help` to see your new command listed.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
You've successfully created and tested a plugin with these key components:
|
||||
|
||||
* **Plugin manifest** (`.claude-plugin/plugin.json`) - Describes your plugin's metadata
|
||||
* **Commands directory** (`commands/`) - Contains your custom slash commands
|
||||
* **Test marketplace** - Allows you to test your plugin locally
|
||||
|
||||
### Plugin structure overview
|
||||
|
||||
Your plugin follows this basic structure:
|
||||
|
||||
```
|
||||
my-first-plugin/
|
||||
├── .claude-plugin/
|
||||
│ └── plugin.json # Plugin metadata
|
||||
├── commands/ # Custom slash commands (optional)
|
||||
│ └── hello.md
|
||||
├── agents/ # Custom agents (optional)
|
||||
│ └── helper.md
|
||||
├── skills/ # Agent Skills (optional)
|
||||
│ └── my-skill/
|
||||
│ └── SKILL.md
|
||||
└── hooks/ # Event handlers (optional)
|
||||
└── hooks.json
|
||||
```
|
||||
|
||||
**Additional components you can add:**
|
||||
|
||||
* **Commands**: Create markdown files in `commands/` directory
|
||||
* **Agents**: Create agent definitions in `agents/` directory
|
||||
* **Skills**: Create `SKILL.md` files in `skills/` directory
|
||||
* **Hooks**: Create `hooks/hooks.json` for event handling
|
||||
* **MCP servers**: Create `.mcp.json` for external tool integration
|
||||
|
||||
<Note>
|
||||
**Next steps**: Ready to add more features? Jump to [Develop more complex plugins](#develop-more-complex-plugins) to add agents, hooks, and MCP servers. For complete technical specifications of all plugin components, see [Plugins reference](/en/docs/claude-code/plugins-reference).
|
||||
</Note>
|
||||
|
||||
***
|
||||
|
||||
## Install and manage plugins
|
||||
|
||||
Learn how to discover, install, and manage plugins to extend your Claude Code capabilities.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
* Claude Code installed and running
|
||||
* Basic familiarity with command-line interfaces
|
||||
|
||||
### Add marketplaces
|
||||
|
||||
Marketplaces are catalogs of available plugins. Add them to discover and install plugins:
|
||||
|
||||
```shell Add a marketplace theme={null}
|
||||
/plugin marketplace add your-org/claude-plugins
|
||||
```
|
||||
|
||||
```shell Browse available plugins theme={null}
|
||||
/plugin
|
||||
```
|
||||
|
||||
For detailed marketplace management including Git repositories, local development, and team distribution, see [Plugin marketplaces](/en/docs/claude-code/plugin-marketplaces).
|
||||
|
||||
### Install plugins
|
||||
|
||||
#### Via interactive menu (recommended for discovery)
|
||||
|
||||
```shell Open the plugin management interface theme={null}
|
||||
/plugin
|
||||
```
|
||||
|
||||
Select "Browse Plugins" to see available options with descriptions, features, and installation options.
|
||||
|
||||
#### Via direct commands (for quick installation)
|
||||
|
||||
```shell Install a specific plugin theme={null}
|
||||
/plugin install formatter@your-org
|
||||
```
|
||||
|
||||
```shell Enable a disabled plugin theme={null}
|
||||
/plugin enable plugin-name@marketplace-name
|
||||
```
|
||||
|
||||
```shell Disable without uninstalling theme={null}
|
||||
/plugin disable plugin-name@marketplace-name
|
||||
```
|
||||
|
||||
```shell Completely remove a plugin theme={null}
|
||||
/plugin uninstall plugin-name@marketplace-name
|
||||
```
|
||||
|
||||
### Verify installation
|
||||
|
||||
After installing a plugin:
|
||||
|
||||
1. **Check available commands**: Run `/help` to see new commands
|
||||
2. **Test plugin features**: Try the plugin's commands and features
|
||||
3. **Review plugin details**: Use `/plugin` → "Manage Plugins" to see what the plugin provides
|
||||
|
||||
## Set up team plugin workflows
|
||||
|
||||
Configure plugins at the repository level to ensure consistent tooling across your team. When team members trust your repository folder, Claude Code automatically installs specified marketplaces and plugins.
|
||||
|
||||
**To set up team plugins:**
|
||||
|
||||
1. Add marketplace and plugin configuration to your repository's `.claude/settings.json`
|
||||
2. Team members trust the repository folder
|
||||
3. Plugins install automatically for all team members
|
||||
|
||||
For complete instructions including configuration examples, marketplace setup, and rollout best practices, see [Configure team marketplaces](/en/docs/claude-code/plugin-marketplaces#how-to-configure-team-marketplaces).
|
||||
|
||||
***
|
||||
|
||||
## Develop more complex plugins
|
||||
|
||||
Once you're comfortable with basic plugins, you can create more sophisticated extensions.
|
||||
|
||||
### Add Skills to your plugin
|
||||
|
||||
Plugins can include [Agent Skills](/en/docs/claude-code/skills) to extend Claude's capabilities. Skills are model-invoked—Claude autonomously uses them based on the task context.
|
||||
|
||||
To add Skills to your plugin, create a `skills/` directory at your plugin root and add Skill folders with `SKILL.md` files. Plugin Skills are automatically available when the plugin is installed.
|
||||
|
||||
For complete Skill authoring guidance, see [Agent Skills](/en/docs/claude-code/skills).
|
||||
|
||||
### Organize complex plugins
|
||||
|
||||
For plugins with many components, organize your directory structure by functionality. For complete directory layouts and organization patterns, see [Plugin directory structure](/en/docs/claude-code/plugins-reference#plugin-directory-structure).
|
||||
|
||||
### Test your plugins locally
|
||||
|
||||
When developing plugins, use a local marketplace to test changes iteratively. This workflow builds on the quickstart pattern and works for plugins of any complexity.
|
||||
|
||||
<Steps>
|
||||
<Step title="Set up your development structure">
|
||||
Organize your plugin and marketplace for testing:
|
||||
|
||||
```bash Create directory structure theme={null}
|
||||
mkdir dev-marketplace
|
||||
cd dev-marketplace
|
||||
mkdir my-plugin
|
||||
```
|
||||
|
||||
This creates:
|
||||
|
||||
```
|
||||
dev-marketplace/
|
||||
├── .claude-plugin/marketplace.json (you'll create this)
|
||||
└── my-plugin/ (your plugin under development)
|
||||
├── .claude-plugin/plugin.json
|
||||
├── commands/
|
||||
├── agents/
|
||||
└── hooks/
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Create the marketplace manifest">
|
||||
```bash Create marketplace.json theme={null}
|
||||
mkdir .claude-plugin
|
||||
cat > .claude-plugin/marketplace.json << 'EOF'
|
||||
{
|
||||
"name": "dev-marketplace",
|
||||
"owner": {
|
||||
"name": "Developer"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "my-plugin",
|
||||
"source": "./my-plugin",
|
||||
"description": "Plugin under development"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Install and test">
|
||||
```bash Start Claude Code from parent directory theme={null}
|
||||
cd ..
|
||||
claude
|
||||
```
|
||||
|
||||
```shell Add your development marketplace theme={null}
|
||||
/plugin marketplace add ./dev-marketplace
|
||||
```
|
||||
|
||||
```shell Install your plugin theme={null}
|
||||
/plugin install my-plugin@dev-marketplace
|
||||
```
|
||||
|
||||
Test your plugin components:
|
||||
|
||||
* Try your commands with `/command-name`
|
||||
* Check that agents appear in `/agents`
|
||||
* Verify hooks work as expected
|
||||
</Step>
|
||||
|
||||
<Step title="Iterate on your plugin">
|
||||
After making changes to your plugin code:
|
||||
|
||||
```shell Uninstall the current version theme={null}
|
||||
/plugin uninstall my-plugin@dev-marketplace
|
||||
```
|
||||
|
||||
```shell Reinstall to test changes theme={null}
|
||||
/plugin install my-plugin@dev-marketplace
|
||||
```
|
||||
|
||||
Repeat this cycle as you develop and refine your plugin.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Note>
|
||||
**For multiple plugins**: Organize plugins in subdirectories like `./plugins/plugin-name` and update your marketplace.json accordingly. See [Plugin sources](/en/docs/claude-code/plugin-marketplaces#plugin-sources) for organization patterns.
|
||||
</Note>
|
||||
|
||||
### Debug plugin issues
|
||||
|
||||
If your plugin isn't working as expected:
|
||||
|
||||
1. **Check the structure**: Ensure your directories are at the plugin root, not inside `.claude-plugin/`
|
||||
2. **Test components individually**: Check each command, agent, and hook separately
|
||||
3. **Use validation and debugging tools**: See [Debugging and development tools](/en/docs/claude-code/plugins-reference#debugging-and-development-tools) for CLI commands and troubleshooting techniques
|
||||
|
||||
### Share your plugins
|
||||
|
||||
When your plugin is ready to share:
|
||||
|
||||
1. **Add documentation**: Include a README.md with installation and usage instructions
|
||||
2. **Version your plugin**: Use semantic versioning in your `plugin.json`
|
||||
3. **Create or use a marketplace**: Distribute through plugin marketplaces for easy installation
|
||||
4. **Test with others**: Have team members test the plugin before wider distribution
|
||||
|
||||
<Note>
|
||||
For complete technical specifications, debugging techniques, and distribution strategies, see [Plugins reference](/en/docs/claude-code/plugins-reference).
|
||||
</Note>
|
||||
|
||||
***
|
||||
|
||||
## Next steps
|
||||
|
||||
Now that you understand Claude Code's plugin system, here are suggested paths for different goals:
|
||||
|
||||
### For plugin users
|
||||
|
||||
* **Discover plugins**: Browse community marketplaces for useful tools
|
||||
* **Team adoption**: Set up repository-level plugins for your projects
|
||||
* **Marketplace management**: Learn to manage multiple plugin sources
|
||||
* **Advanced usage**: Explore plugin combinations and workflows
|
||||
|
||||
### For plugin developers
|
||||
|
||||
* **Create your first marketplace**: [Plugin marketplaces guide](/en/docs/claude-code/plugin-marketplaces)
|
||||
* **Advanced components**: Dive deeper into specific plugin components:
|
||||
* [Slash commands](/en/docs/claude-code/slash-commands) - Command development details
|
||||
* [Subagents](/en/docs/claude-code/sub-agents) - Agent configuration and capabilities
|
||||
* [Agent Skills](/en/docs/claude-code/skills) - Extend Claude's capabilities
|
||||
* [Hooks](/en/docs/claude-code/hooks) - Event handling and automation
|
||||
* [MCP](/en/docs/claude-code/mcp) - External tool integration
|
||||
* **Distribution strategies**: Package and share your plugins effectively
|
||||
* **Community contribution**: Consider contributing to community plugin collections
|
||||
|
||||
### For team leads and administrators
|
||||
|
||||
* **Repository configuration**: Set up automatic plugin installation for team projects
|
||||
* **Plugin governance**: Establish guidelines for plugin approval and security review
|
||||
* **Marketplace maintenance**: Create and maintain organization-specific plugin catalogs
|
||||
* **Training and documentation**: Help team members adopt plugin workflows effectively
|
||||
|
||||
## See also
|
||||
|
||||
* [Plugin marketplaces](/en/docs/claude-code/plugin-marketplaces) - Creating and managing plugin catalogs
|
||||
* [Slash commands](/en/docs/claude-code/slash-commands) - Understanding custom commands
|
||||
* [Subagents](/en/docs/claude-code/sub-agents) - Creating and using specialized agents
|
||||
* [Agent Skills](/en/docs/claude-code/skills) - Extend Claude's capabilities
|
||||
* [Hooks](/en/docs/claude-code/hooks) - Automating workflows with event handlers
|
||||
* [MCP](/en/docs/claude-code/mcp) - Connecting to external tools and services
|
||||
* [Settings](/en/docs/claude-code/settings) - Configuration options for plugins
|
||||
@@ -0,0 +1,218 @@
|
||||
# Models overview
|
||||
|
||||
> Claude is a family of state-of-the-art large language models developed by Anthropic. This guide introduces our models and compares their performance with legacy models.
|
||||
|
||||
export const ModelId = ({children, style = {}}) => {
|
||||
const copiedNotice = 'Copied!';
|
||||
const handleClick = e => {
|
||||
const element = e.currentTarget;
|
||||
const originalText = element.textContent;
|
||||
navigator.clipboard.writeText(children).then(() => {
|
||||
element.textContent = copiedNotice;
|
||||
element.style.backgroundColor = '#d4edda';
|
||||
element.style.color = '#155724';
|
||||
element.style.borderColor = '#c3e6cb';
|
||||
setTimeout(() => {
|
||||
element.textContent = originalText;
|
||||
element.style.backgroundColor = '#f5f5f5';
|
||||
element.style.color = '';
|
||||
element.style.borderColor = 'transparent';
|
||||
}, 2000);
|
||||
}).catch(error => {
|
||||
console.error('Failed to copy:', error);
|
||||
});
|
||||
};
|
||||
const handleMouseEnter = e => {
|
||||
const element = e.currentTarget;
|
||||
const tooltip = element.querySelector('.copy-tooltip');
|
||||
if (tooltip && element.textContent !== copiedNotice) {
|
||||
tooltip.style.opacity = '1';
|
||||
}
|
||||
element.style.backgroundColor = '#e8e8e8';
|
||||
element.style.borderColor = '#d0d0d0';
|
||||
};
|
||||
const handleMouseLeave = e => {
|
||||
const element = e.currentTarget;
|
||||
const tooltip = element.querySelector('.copy-tooltip');
|
||||
if (tooltip) {
|
||||
tooltip.style.opacity = '0';
|
||||
}
|
||||
if (element.textContent !== copiedNotice) {
|
||||
element.style.backgroundColor = '#f5f5f5';
|
||||
element.style.borderColor = 'transparent';
|
||||
}
|
||||
};
|
||||
const defaultStyle = {
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
transition: 'all 0.2s ease',
|
||||
display: 'inline-block',
|
||||
userSelect: 'none',
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '2px 4px',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'Monaco, Consolas, "Courier New", monospace',
|
||||
fontSize: '0.9em',
|
||||
border: '1px solid transparent',
|
||||
...style
|
||||
};
|
||||
return <span onClick={handleClick} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} style={defaultStyle}>
|
||||
{children}
|
||||
</span>;
|
||||
};
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Claude Sonnet 4.5" icon="star" href="/en/docs/about-claude/models/overview#model-comparison-table">
|
||||
Our best model for complex agents and coding
|
||||
|
||||
* <Icon icon="inbox-in" iconType="thin" /> Text and image input
|
||||
* <Icon icon="inbox-out" iconType="thin" /> Text output
|
||||
* <Icon icon="book" iconType="thin" /> 200k context window (1M context beta available)
|
||||
* <Icon icon="brain" iconType="thin" /> Highest intelligence across most tasks
|
||||
</Card>
|
||||
|
||||
<Card title="Claude Haiku 4.5" icon="rocket-launch" href="/en/docs/about-claude/models/overview#model-comparison-table">
|
||||
Our fastest and most intelligent Haiku model
|
||||
|
||||
* <Icon icon="inbox-in" iconType="thin" /> Text and image input
|
||||
* <Icon icon="inbox-out" iconType="thin" /> Text output
|
||||
* <Icon icon="book" iconType="thin" /> 200k context window
|
||||
* <Icon icon="zap" iconType="thin" /> Lightning-fast speed with extended thinking
|
||||
</Card>
|
||||
|
||||
<Card title="Claude Opus 4.1" icon="trophy" href="/en/docs/about-claude/models/overview#model-comparison-table">
|
||||
Exceptional model for specialized complex tasks
|
||||
|
||||
* <Icon icon="inbox-in" iconType="thin" /> Text and image input
|
||||
* <Icon icon="inbox-out" iconType="thin" /> Text output
|
||||
* <Icon icon="book" iconType="thin" /> 200k context window
|
||||
* <Icon icon="brain" iconType="thin" /> Superior reasoning capabilities
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
***
|
||||
|
||||
## Model names
|
||||
|
||||
| Model | Claude API | AWS Bedrock | GCP Vertex AI |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------- |
|
||||
| Claude Sonnet 4.5 | <ModelId>claude-sonnet-4-5-20250929</ModelId> | <ModelId>anthropic.claude-sonnet-4-5-20250929-v1:0</ModelId> | <ModelId>claude-sonnet-4-5\@20250929</ModelId> |
|
||||
| Claude Sonnet 4 | <ModelId>claude-sonnet-4-20250514</ModelId> | <ModelId>anthropic.claude-sonnet-4-20250514-v1:0</ModelId> | <ModelId>claude-sonnet-4\@20250514</ModelId> |
|
||||
| Claude Sonnet 3.7 | <ModelId>claude-3-7-sonnet-20250219</ModelId> (<ModelId>claude-3-7-sonnet-latest</ModelId>) | <ModelId>anthropic.claude-3-7-sonnet-20250219-v1:0</ModelId> | <ModelId>claude-3-7-sonnet\@20250219</ModelId> |
|
||||
| Claude Haiku 4.5 | <ModelId>claude-haiku-4-5-20251001</ModelId> | <ModelId>anthropic.claude-haiku-4-5-20251001-v1:0</ModelId> | <ModelId>claude-haiku-4-5\@20251001</ModelId> |
|
||||
| Claude Haiku 3.5 | <ModelId>claude-3-5-haiku-20241022</ModelId> (<ModelId>claude-3-5-haiku-latest</ModelId>) | <ModelId>anthropic.claude-3-5-haiku-20241022-v1:0</ModelId> | <ModelId>claude-3-5-haiku\@20241022</ModelId> |
|
||||
| Claude Haiku 3 | <ModelId>claude-3-haiku-20240307</ModelId> | <ModelId>anthropic.claude-3-haiku-20240307-v1:0</ModelId> | <ModelId>claude-3-haiku\@20240307</ModelId> |
|
||||
| Claude Opus 4.1 | <ModelId>claude-opus-4-1-20250805</ModelId> | <ModelId>anthropic.claude-opus-4-1-20250805-v1:0</ModelId> | <ModelId>claude-opus-4-1\@20250805</ModelId> |
|
||||
| Claude Opus 4 | <ModelId>claude-opus-4-20250514</ModelId> | <ModelId>anthropic.claude-opus-4-20250514-v1:0</ModelId> | <ModelId>claude-opus-4\@20250514</ModelId> |
|
||||
|
||||
<Note>Models with the same snapshot date (e.g., 20240620) are identical across all platforms and do not change. The snapshot date in the model name ensures consistency and allows developers to rely on stable performance across different environments.</Note>
|
||||
|
||||
<Note>Starting with **Claude Sonnet 4.5 and all future models**, AWS Bedrock and Google Vertex AI offer two endpoint types: **global endpoints** (dynamic routing for maximum availability) and **regional endpoints** (guaranteed data routing through specific geographic regions). For more information, see the [third-party platform pricing section](/en/docs/about-claude/pricing#third-party-platform-pricing).</Note>
|
||||
|
||||
### Model aliases
|
||||
|
||||
For convenience during development and testing, we offer aliases for our model ids. These aliases automatically point to the most recent snapshot of a given model. When we release new model snapshots, we migrate aliases to point to the newest version of a model, typically within a week of the new release.
|
||||
|
||||
<Tip>
|
||||
While aliases are useful for experimentation, we recommend using specific model versions (e.g., `claude-sonnet-4-5-20250929`) in production applications to ensure consistent behavior.
|
||||
</Tip>
|
||||
|
||||
| Model | Alias | Model ID |
|
||||
| ----------------- | ------------------------------------------- | --------------------------------------------- |
|
||||
| Claude Sonnet 4.5 | <ModelId>claude-sonnet-4-5</ModelId> | <ModelId>claude-sonnet-4-5-20250929</ModelId> |
|
||||
| Claude Sonnet 4 | <ModelId>claude-sonnet-4-0</ModelId> | <ModelId>claude-sonnet-4-20250514</ModelId> |
|
||||
| Claude Sonnet 3.7 | <ModelId>claude-3-7-sonnet-latest</ModelId> | <ModelId>claude-3-7-sonnet-20250219</ModelId> |
|
||||
| Claude Haiku 4.5 | <ModelId>claude-haiku-4-5</ModelId> | <ModelId>claude-haiku-4-5-20251001</ModelId> |
|
||||
| Claude Haiku 3.5 | <ModelId>claude-3-5-haiku-latest</ModelId> | <ModelId>claude-3-5-haiku-20241022</ModelId> |
|
||||
| Claude Opus 4.1 | <ModelId>claude-opus-4-1</ModelId> | <ModelId>claude-opus-4-1-20250805</ModelId> |
|
||||
| Claude Opus 4 | <ModelId>claude-opus-4-0</ModelId> | <ModelId>claude-opus-4-20250514</ModelId> |
|
||||
|
||||
<Note>
|
||||
Aliases are subject to the same rate limits and pricing as the underlying model version they reference.
|
||||
</Note>
|
||||
|
||||
### Model comparison table
|
||||
|
||||
To help you choose the right model for your needs, we've compiled a table comparing the key features and capabilities of each model in the Claude family:
|
||||
|
||||
| Feature | Claude Sonnet 4.5 | Claude Sonnet 4 | Claude Sonnet 3.7 | Claude Opus 4.1 | Claude Opus 4 | Claude Haiku 4.5 | Claude Haiku 3.5 | Claude Haiku 3 |
|
||||
| :-------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------- |
|
||||
| **Description** | Our best model for complex agents and coding | High-performance model | High-performance model with early extended thinking | Exceptional model for specialized complex tasks | Our previous flagship model | Our fastest and most intelligent Haiku model | Our fastest model | Fast and compact model for near-instant responsiveness |
|
||||
| **Strengths** | Highest intelligence across most tasks with exceptional agent and coding capabilities | High intelligence and balanced performance | High intelligence with toggleable extended thinking | Very high intelligence and capability for specialized tasks | Very high intelligence and capability | Near-frontier intelligence at blazing speeds with extended thinking and exceptional cost-efficiency | Intelligence at blazing speeds | Quick and accurate targeted performance |
|
||||
| **Multilingual** | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| **Vision** | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| **[Extended thinking](/en/docs/build-with-claude/extended-thinking)** | Yes | Yes | Yes | Yes | Yes | Yes | No | No |
|
||||
| **[Priority Tier](/en/api/service-tiers)** | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No |
|
||||
| **API model name** | <ModelId>claude-sonnet-4-5-20250929</ModelId> | <ModelId>claude-sonnet-4-20250514</ModelId> | <ModelId>claude-3-7-sonnet-20250219</ModelId> | <ModelId>claude-opus-4-1-20250805</ModelId> | <ModelId>claude-opus-4-20250514</ModelId> | <ModelId>claude-haiku-4-5-20251001</ModelId> | <ModelId>claude-3-5-haiku-20241022</ModelId> | <ModelId>claude-3-haiku-20240307</ModelId> |
|
||||
| **Comparative latency** | Fast | Fast | Fast | Moderately Fast | Moderately Fast | Fastest | Fastest | Fast |
|
||||
| **Context window** | <Tooltip tip="~150K words \ ~680K unicode characters">200K</Tooltip> / <br /> 1M (beta)<sup>1</sup> | <Tooltip tip="~150K words \ ~680K unicode characters">200K</Tooltip> / <br /> 1M (beta)<sup>1</sup> | <Tooltip tip="~150K words \ ~680K unicode characters">200K</Tooltip> | <Tooltip tip="~150K words \ ~680K unicode characters">200K</Tooltip> | <Tooltip tip="~150K words \ ~680K unicode characters">200K</Tooltip> | <Tooltip tip="~150K words \ ~680K unicode characters">200K</Tooltip> | <Tooltip tip="~150K words \ ~215K unicode characters">200K</Tooltip> | <Tooltip tip="~150K words \ ~680K unicode characters">200K</Tooltip> |
|
||||
| **Max output** | <Tooltip tip="~48K words \ 218K unicode characters \ ~100 single spaced pages">64000 tokens</Tooltip> | <Tooltip tip="~48K words \ 218K unicode characters \ ~100 single spaced pages">64000 tokens</Tooltip> | <Tooltip tip="~48K words \ 218K unicode characters \ ~100 single spaced pages">64000 tokens</Tooltip> | <Tooltip tip="~24K words \ 109K unicode characters \ ~50 single spaced pages">32000 tokens</Tooltip> | <Tooltip tip="~24K words \ 109K unicode characters \ ~50 single spaced pages">32000 tokens</Tooltip> | <Tooltip tip="~48K words \ 218K unicode characters \ ~100 single spaced pages">64000 tokens</Tooltip> | <Tooltip tip="~6.2K words \ 28K unicode characters \ ~12-13 single spaced pages">8192 tokens</Tooltip> | <Tooltip tip="~3.1K words \ 14K unicode characters \ ~6-7 single spaced pages">4096 tokens</Tooltip> |
|
||||
| **Reliable knowledge cutoff** | Jan 2025<sup>2</sup> | Jan 2025<sup>2</sup> | Oct 2024<sup>2</sup> | Jan 2025<sup>2</sup> | Jan 2025<sup>2</sup> | Feb 2025 | <sup>3</sup> | <sup>3</sup> |
|
||||
| **Training data cutoff** | Jul 2025 | Mar 2025 | Nov 2024 | Mar 2025 | Mar 2025 | Jul 2025 | Jul 2024 | Aug 2023 |
|
||||
|
||||
*<sup>1 - Claude Sonnet 4.5 and Claude Sonnet 4 support a [1M token context window](/en/docs/build-with-claude/context-windows#1m-token-context-window) when using the `context-1m-2025-08-07` beta header. [Long context pricing](/en/docs/about-claude/pricing#long-context-pricing) applies to requests exceeding 200K tokens.</sup>*
|
||||
|
||||
*<sup>2 - **Reliable knowledge cutoff** indicates the date through which a model's knowledge is most extensive and reliable. **Training data cutoff** is the broader date range of training data used. For example, Claude Sonnet 4.5 was trained on publicly available information through July 2025, but its knowledge is most extensive and reliable through January 2025. For more information, see [Anthropic's Transparency Hub](https://www.anthropic.com/transparency).</sup>*
|
||||
|
||||
*<sup>3 - Some Haiku models have a single training data cutoff date.</sup>*
|
||||
|
||||
<Note>
|
||||
Include the beta header `output-128k-2025-02-19` in your API request to increase the maximum output token length to 128k tokens for Claude Sonnet 3.7.
|
||||
|
||||
We strongly suggest using our [streaming Messages API](/en/docs/build-with-claude/streaming) to avoid timeouts when generating longer outputs.
|
||||
See our guidance on [long requests](/en/api/errors#long-requests) for more details.
|
||||
</Note>
|
||||
|
||||
### Model pricing
|
||||
|
||||
The table below shows the price per million tokens for each model:
|
||||
|
||||
| Model | Base Input Tokens | 5m Cache Writes | 1h Cache Writes | Cache Hits & Refreshes | Output Tokens |
|
||||
| -------------------------------------------------------------------------- | ----------------- | --------------- | --------------- | ---------------------- | ------------- |
|
||||
| Claude Opus 4.1 | \$15 / MTok | \$18.75 / MTok | \$30 / MTok | \$1.50 / MTok | \$75 / MTok |
|
||||
| Claude Opus 4 | \$15 / MTok | \$18.75 / MTok | \$30 / MTok | \$1.50 / MTok | \$75 / MTok |
|
||||
| Claude Sonnet 4.5 | \$3 / MTok | \$3.75 / MTok | \$6 / MTok | \$0.30 / MTok | \$15 / MTok |
|
||||
| Claude Sonnet 4 | \$3 / MTok | \$3.75 / MTok | \$6 / MTok | \$0.30 / MTok | \$15 / MTok |
|
||||
| Claude Sonnet 3.7 | \$3 / MTok | \$3.75 / MTok | \$6 / MTok | \$0.30 / MTok | \$15 / MTok |
|
||||
| Claude Sonnet 3.5 ([deprecated](/en/docs/about-claude/model-deprecations)) | \$3 / MTok | \$3.75 / MTok | \$6 / MTok | \$0.30 / MTok | \$15 / MTok |
|
||||
| Claude Haiku 4.5 | \$1 / MTok | \$1.25 / MTok | \$2 / MTok | \$0.10 / MTok | \$5 / MTok |
|
||||
| Claude Haiku 3.5 | \$0.80 / MTok | \$1 / MTok | \$1.6 / MTok | \$0.08 / MTok | \$4 / MTok |
|
||||
| Claude Opus 3 ([deprecated](/en/docs/about-claude/model-deprecations)) | \$15 / MTok | \$18.75 / MTok | \$30 / MTok | \$1.50 / MTok | \$75 / MTok |
|
||||
| Claude Haiku 3 | \$0.25 / MTok | \$0.30 / MTok | \$0.50 / MTok | \$0.03 / MTok | \$1.25 / MTok |
|
||||
|
||||
## Prompt and output performance
|
||||
|
||||
Claude 4 models excel in:
|
||||
|
||||
* **Performance**: Top-tier results in reasoning, coding, multilingual tasks, long-context handling, honesty, and image processing. See the [Claude 4 blog post](http://www.anthropic.com/news/claude-4) for more information.
|
||||
* **Engaging responses**: Claude models are ideal for applications that require rich, human-like interactions.
|
||||
|
||||
* If you prefer more concise responses, you can adjust your prompts to guide the model toward the desired output length. Refer to our [prompt engineering guides](/en/docs/build-with-claude/prompt-engineering) for details.
|
||||
* For specific Claude 4 prompting best practices, see our [Claude 4 best practices guide](/en/docs/build-with-claude/prompt-engineering/claude-4-best-practices).
|
||||
* **Output quality**: When migrating from previous model generations to Claude 4, you may notice larger improvements in overall performance.
|
||||
|
||||
## Migrating to Claude 4.5
|
||||
|
||||
If you're currently using Claude 3 models, we recommend migrating to Claude 4.5 to take advantage of improved intelligence and enhanced capabilities. For detailed migration instructions, see [Migrating to Claude 4.5](/en/docs/about-claude/models/migrating-to-claude-4).
|
||||
|
||||
## Get started with Claude
|
||||
|
||||
If you're ready to start exploring what Claude can do for you, let's dive in! Whether you're a developer looking to integrate Claude into your applications or a user wanting to experience the power of AI firsthand, we've got you covered.
|
||||
|
||||
<Note>Looking to chat with Claude? Visit [claude.ai](http://www.claude.ai)!</Note>
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Intro to Claude" icon="check" href="/en/docs/intro-to-claude">
|
||||
Explore Claude’s capabilities and development flow.
|
||||
</Card>
|
||||
|
||||
<Card title="Quickstart" icon="bolt-lightning" href="/en/resources/quickstarts">
|
||||
Learn how to make your first API call in minutes.
|
||||
</Card>
|
||||
|
||||
<Card title="Claude Console" icon="code" href="https://console.anthropic.com">
|
||||
Craft and test powerful prompts directly in your browser.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
If you have any questions or need assistance, don't hesitate to reach out to our [support team](https://support.claude.com/) or consult the [Discord community](https://www.anthropic.com/discord).
|
||||
@@ -0,0 +1,376 @@
|
||||
# Plugins reference
|
||||
|
||||
> Complete technical reference for Claude Code plugin system, including schemas, CLI commands, and component specifications.
|
||||
|
||||
<Tip>
|
||||
For hands-on tutorials and practical usage, see [Plugins](/en/docs/claude-code/plugins). For plugin management across teams and communities, see [Plugin marketplaces](/en/docs/claude-code/plugin-marketplaces).
|
||||
</Tip>
|
||||
|
||||
This reference provides complete technical specifications for the Claude Code plugin system, including component schemas, CLI commands, and development tools.
|
||||
|
||||
## Plugin components reference
|
||||
|
||||
This section documents the five types of components that plugins can provide.
|
||||
|
||||
### Commands
|
||||
|
||||
Plugins add custom slash commands that integrate seamlessly with Claude Code's command system.
|
||||
|
||||
**Location**: `commands/` directory in plugin root
|
||||
|
||||
**File format**: Markdown files with frontmatter
|
||||
|
||||
For complete details on plugin command structure, invocation patterns, and features, see [Plugin commands](/en/docs/claude-code/slash-commands#plugin-commands).
|
||||
|
||||
### Agents
|
||||
|
||||
Plugins can provide specialized subagents for specific tasks that Claude can invoke automatically when appropriate.
|
||||
|
||||
**Location**: `agents/` directory in plugin root
|
||||
|
||||
**File format**: Markdown files describing agent capabilities
|
||||
|
||||
**Agent structure**:
|
||||
|
||||
```markdown theme={null}
|
||||
---
|
||||
description: What this agent specializes in
|
||||
capabilities: ["task1", "task2", "task3"]
|
||||
---
|
||||
|
||||
# Agent Name
|
||||
|
||||
Detailed description of the agent's role, expertise, and when Claude should invoke it.
|
||||
|
||||
## Capabilities
|
||||
- Specific task the agent excels at
|
||||
- Another specialized capability
|
||||
- When to use this agent vs others
|
||||
|
||||
## Context and examples
|
||||
Provide examples of when this agent should be used and what kinds of problems it solves.
|
||||
```
|
||||
|
||||
**Integration points**:
|
||||
|
||||
* Agents appear in the `/agents` interface
|
||||
* Claude can invoke agents automatically based on task context
|
||||
* Agents can be invoked manually by users
|
||||
* Plugin agents work alongside built-in Claude agents
|
||||
|
||||
### Skills
|
||||
|
||||
Plugins can provide Agent Skills that extend Claude's capabilities. Skills are model-invoked—Claude autonomously decides when to use them based on the task context.
|
||||
|
||||
**Location**: `skills/` directory in plugin root
|
||||
|
||||
**File format**: Directories containing `SKILL.md` files with frontmatter
|
||||
|
||||
**Skill structure**:
|
||||
|
||||
```
|
||||
skills/
|
||||
├── pdf-processor/
|
||||
│ ├── SKILL.md
|
||||
│ ├── reference.md (optional)
|
||||
│ └── scripts/ (optional)
|
||||
└── code-reviewer/
|
||||
└── SKILL.md
|
||||
```
|
||||
|
||||
**Integration behavior**:
|
||||
|
||||
* Plugin Skills are automatically discovered when the plugin is installed
|
||||
* Claude autonomously invokes Skills based on matching task context
|
||||
* Skills can include supporting files alongside SKILL.md
|
||||
|
||||
For SKILL.md format and complete Skill authoring guidance, see:
|
||||
|
||||
* [Use Skills in Claude Code](/en/docs/claude-code/skills)
|
||||
* [Agent Skills overview](/en/docs/agents-and-tools/agent-skills/overview#skill-structure)
|
||||
|
||||
### Hooks
|
||||
|
||||
Plugins can provide event handlers that respond to Claude Code events automatically.
|
||||
|
||||
**Location**: `hooks/hooks.json` in plugin root, or inline in plugin.json
|
||||
|
||||
**Format**: JSON configuration with event matchers and actions
|
||||
|
||||
**Hook configuration**:
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format-code.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Available events**:
|
||||
|
||||
* `PreToolUse`: Before Claude uses any tool
|
||||
* `PostToolUse`: After Claude uses any tool
|
||||
* `UserPromptSubmit`: When user submits a prompt
|
||||
* `Notification`: When Claude Code sends notifications
|
||||
* `Stop`: When Claude attempts to stop
|
||||
* `SubagentStop`: When a subagent attempts to stop
|
||||
* `SessionStart`: At the beginning of sessions
|
||||
* `SessionEnd`: At the end of sessions
|
||||
* `PreCompact`: Before conversation history is compacted
|
||||
|
||||
**Hook types**:
|
||||
|
||||
* `command`: Execute shell commands or scripts
|
||||
* `validation`: Validate file contents or project state
|
||||
* `notification`: Send alerts or status updates
|
||||
|
||||
### MCP servers
|
||||
|
||||
Plugins can bundle Model Context Protocol (MCP) servers to connect Claude Code with external tools and services.
|
||||
|
||||
**Location**: `.mcp.json` in plugin root, or inline in plugin.json
|
||||
|
||||
**Format**: Standard MCP server configuration
|
||||
|
||||
**MCP server configuration**:
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"mcpServers": {
|
||||
"plugin-database": {
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/servers/db-server",
|
||||
"args": ["--config", "${CLAUDE_PLUGIN_ROOT}/config.json"],
|
||||
"env": {
|
||||
"DB_PATH": "${CLAUDE_PLUGIN_ROOT}/data"
|
||||
}
|
||||
},
|
||||
"plugin-api-client": {
|
||||
"command": "npx",
|
||||
"args": ["@company/mcp-server", "--plugin-mode"],
|
||||
"cwd": "${CLAUDE_PLUGIN_ROOT}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Integration behavior**:
|
||||
|
||||
* Plugin MCP servers start automatically when the plugin is enabled
|
||||
* Servers appear as standard MCP tools in Claude's toolkit
|
||||
* Server capabilities integrate seamlessly with Claude's existing tools
|
||||
* Plugin servers can be configured independently of user MCP servers
|
||||
|
||||
***
|
||||
|
||||
## Plugin manifest schema
|
||||
|
||||
The `plugin.json` file defines your plugin's metadata and configuration. This section documents all supported fields and options.
|
||||
|
||||
### Complete schema
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"name": "plugin-name",
|
||||
"version": "1.2.0",
|
||||
"description": "Brief plugin description",
|
||||
"author": {
|
||||
"name": "Author Name",
|
||||
"email": "author@example.com",
|
||||
"url": "https://github.com/author"
|
||||
},
|
||||
"homepage": "https://docs.example.com/plugin",
|
||||
"repository": "https://github.com/author/plugin",
|
||||
"license": "MIT",
|
||||
"keywords": ["keyword1", "keyword2"],
|
||||
"commands": ["./custom/commands/special.md"],
|
||||
"agents": "./custom/agents/",
|
||||
"hooks": "./config/hooks.json",
|
||||
"mcpServers": "./mcp-config.json"
|
||||
}
|
||||
```
|
||||
|
||||
### Required fields
|
||||
|
||||
| Field | Type | Description | Example |
|
||||
| :----- | :----- | :---------------------------------------- | :------------------- |
|
||||
| `name` | string | Unique identifier (kebab-case, no spaces) | `"deployment-tools"` |
|
||||
|
||||
### Metadata fields
|
||||
|
||||
| Field | Type | Description | Example |
|
||||
| :------------ | :----- | :---------------------------------- | :------------------------------------------------- |
|
||||
| `version` | string | Semantic version | `"2.1.0"` |
|
||||
| `description` | string | Brief explanation of plugin purpose | `"Deployment automation tools"` |
|
||||
| `author` | object | Author information | `{"name": "Dev Team", "email": "dev@company.com"}` |
|
||||
| `homepage` | string | Documentation URL | `"https://docs.example.com"` |
|
||||
| `repository` | string | Source code URL | `"https://github.com/user/plugin"` |
|
||||
| `license` | string | License identifier | `"MIT"`, `"Apache-2.0"` |
|
||||
| `keywords` | array | Discovery tags | `["deployment", "ci-cd"]` |
|
||||
|
||||
### Component path fields
|
||||
|
||||
| Field | Type | Description | Example |
|
||||
| :----------- | :------------- | :----------------------------------- | :------------------------------------- |
|
||||
| `commands` | string\|array | Additional command files/directories | `"./custom/cmd.md"` or `["./cmd1.md"]` |
|
||||
| `agents` | string\|array | Additional agent files | `"./custom/agents/"` |
|
||||
| `hooks` | string\|object | Hook config path or inline config | `"./hooks.json"` |
|
||||
| `mcpServers` | string\|object | MCP config path or inline config | `"./mcp.json"` |
|
||||
|
||||
### Path behavior rules
|
||||
|
||||
**Important**: Custom paths supplement default directories - they don't replace them.
|
||||
|
||||
* If `commands/` exists, it's loaded in addition to custom command paths
|
||||
* All paths must be relative to plugin root and start with `./`
|
||||
* Commands from custom paths use the same naming and namespacing rules
|
||||
* Multiple paths can be specified as arrays for flexibility
|
||||
|
||||
**Path examples**:
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"commands": [
|
||||
"./specialized/deploy.md",
|
||||
"./utilities/batch-process.md"
|
||||
],
|
||||
"agents": [
|
||||
"./custom-agents/reviewer.md",
|
||||
"./custom-agents/tester.md"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Environment variables
|
||||
|
||||
**`${CLAUDE_PLUGIN_ROOT}`**: Contains the absolute path to your plugin directory. Use this in hooks, MCP servers, and scripts to ensure correct paths regardless of installation location.
|
||||
|
||||
```json theme={null}
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/process.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## Plugin directory structure
|
||||
|
||||
### Standard plugin layout
|
||||
|
||||
A complete plugin follows this structure:
|
||||
|
||||
```
|
||||
enterprise-plugin/
|
||||
├── .claude-plugin/ # Metadata directory
|
||||
│ └── plugin.json # Required: plugin manifest
|
||||
├── commands/ # Default command location
|
||||
│ ├── status.md
|
||||
│ └── logs.md
|
||||
├── agents/ # Default agent location
|
||||
│ ├── security-reviewer.md
|
||||
│ ├── performance-tester.md
|
||||
│ └── compliance-checker.md
|
||||
├── skills/ # Agent Skills
|
||||
│ ├── code-reviewer/
|
||||
│ │ └── SKILL.md
|
||||
│ └── pdf-processor/
|
||||
│ ├── SKILL.md
|
||||
│ └── scripts/
|
||||
├── hooks/ # Hook configurations
|
||||
│ ├── hooks.json # Main hook config
|
||||
│ └── security-hooks.json # Additional hooks
|
||||
├── .mcp.json # MCP server definitions
|
||||
├── scripts/ # Hook and utility scripts
|
||||
│ ├── security-scan.sh
|
||||
│ ├── format-code.py
|
||||
│ └── deploy.js
|
||||
├── LICENSE # License file
|
||||
└── CHANGELOG.md # Version history
|
||||
```
|
||||
|
||||
<Warning>
|
||||
The `.claude-plugin/` directory contains the `plugin.json` file. All other directories (commands/, agents/, skills/, hooks/) must be at the plugin root, not inside `.claude-plugin/`.
|
||||
</Warning>
|
||||
|
||||
### File locations reference
|
||||
|
||||
| Component | Default Location | Purpose |
|
||||
| :-------------- | :--------------------------- | :------------------------------- |
|
||||
| **Manifest** | `.claude-plugin/plugin.json` | Required metadata file |
|
||||
| **Commands** | `commands/` | Slash command markdown files |
|
||||
| **Agents** | `agents/` | Subagent markdown files |
|
||||
| **Skills** | `skills/` | Agent Skills with SKILL.md files |
|
||||
| **Hooks** | `hooks/hooks.json` | Hook configuration |
|
||||
| **MCP servers** | `.mcp.json` | MCP server definitions |
|
||||
|
||||
***
|
||||
|
||||
## Debugging and development tools
|
||||
|
||||
### Debugging commands
|
||||
|
||||
Use `claude --debug` to see plugin loading details:
|
||||
|
||||
```bash theme={null}
|
||||
claude --debug
|
||||
```
|
||||
|
||||
This shows:
|
||||
|
||||
* Which plugins are being loaded
|
||||
* Any errors in plugin manifests
|
||||
* Command, agent, and hook registration
|
||||
* MCP server initialization
|
||||
|
||||
### Common issues
|
||||
|
||||
| Issue | Cause | Solution |
|
||||
| :--------------------- | :------------------------------ | :--------------------------------------------------- |
|
||||
| Plugin not loading | Invalid `plugin.json` | Validate JSON syntax |
|
||||
| Commands not appearing | Wrong directory structure | Ensure `commands/` at root, not in `.claude-plugin/` |
|
||||
| Hooks not firing | Script not executable | Run `chmod +x script.sh` |
|
||||
| MCP server fails | Missing `${CLAUDE_PLUGIN_ROOT}` | Use variable for all plugin paths |
|
||||
| Path errors | Absolute paths used | All paths must be relative and start with `./` |
|
||||
|
||||
***
|
||||
|
||||
## Distribution and versioning reference
|
||||
|
||||
### Version management
|
||||
|
||||
Follow semantic versioning for plugin releases:
|
||||
|
||||
```json theme={null}
|
||||
|
||||
## See also
|
||||
|
||||
- [Plugins](/en/docs/claude-code/plugins) - Tutorials and practical usage
|
||||
- [Plugin marketplaces](/en/docs/claude-code/plugin-marketplaces) - Creating and managing marketplaces
|
||||
- [Slash commands](/en/docs/claude-code/slash-commands) - Command development details
|
||||
- [Subagents](/en/docs/claude-code/sub-agents) - Agent configuration and capabilities
|
||||
- [Agent Skills](/en/docs/claude-code/skills) - Extend Claude's capabilities
|
||||
- [Hooks](/en/docs/claude-code/hooks) - Event handling and automation
|
||||
- [MCP](/en/docs/claude-code/mcp) - External tool integration
|
||||
- [Settings](/en/docs/claude-code/settings) - Configuration options for plugins
|
||||
```
|
||||
@@ -0,0 +1,295 @@
|
||||
# Streaming Input
|
||||
|
||||
> Understanding the two input modes for Claude Agent SDK and when to use each
|
||||
|
||||
## Overview
|
||||
|
||||
The Claude Agent SDK supports two distinct input modes for interacting with agents:
|
||||
|
||||
* **Streaming Input Mode** (Default & Recommended) - A persistent, interactive session
|
||||
* **Single Message Input** - One-shot queries that use session state and resuming
|
||||
|
||||
This guide explains the differences, benefits, and use cases for each mode to help you choose the right approach for your application.
|
||||
|
||||
## Streaming Input Mode (Recommended)
|
||||
|
||||
Streaming input mode is the **preferred** way to use the Claude Agent SDK. It provides full access to the agent's capabilities and enables rich, interactive experiences.
|
||||
|
||||
It allows the agent to operate as a long lived process that takes in user input, handles interruptions, surfaces permission requests, and handles session management.
|
||||
|
||||
### How It Works
|
||||
|
||||
```mermaid theme={null}
|
||||
%%{init: {"theme": "base", "themeVariables": {"edgeLabelBackground": "#F0F0EB", "lineColor": "#91918D", "primaryColor": "#F0F0EB", "primaryTextColor": "#191919", "primaryBorderColor": "#D9D8D5", "secondaryColor": "#F5E6D8", "tertiaryColor": "#CC785C", "noteBkgColor": "#FAF0E6", "noteBorderColor": "#91918D"}, "sequence": {"actorMargin": 50, "width": 150, "height": 65, "boxMargin": 10, "boxTextMargin": 5, "noteMargin": 10, "messageMargin": 35}}}%%
|
||||
sequenceDiagram
|
||||
participant App as Your Application
|
||||
participant Agent as Claude Agent
|
||||
participant Tools as Tools/Hooks
|
||||
participant FS as Environment/<br/>File System
|
||||
|
||||
App->>Agent: Initialize with AsyncGenerator
|
||||
activate Agent
|
||||
|
||||
App->>Agent: Yield Message 1
|
||||
Agent->>Tools: Execute tools
|
||||
Tools->>FS: Read files
|
||||
FS-->>Tools: File contents
|
||||
Tools->>FS: Write/Edit files
|
||||
FS-->>Tools: Success/Error
|
||||
Agent-->>App: Stream partial response
|
||||
Agent-->>App: Stream more content...
|
||||
Agent->>App: Complete Message 1
|
||||
|
||||
App->>Agent: Yield Message 2 + Image
|
||||
Agent->>Tools: Process image & execute
|
||||
Tools->>FS: Access filesystem
|
||||
FS-->>Tools: Operation results
|
||||
Agent-->>App: Stream response 2
|
||||
|
||||
App->>Agent: Queue Message 3
|
||||
App->>Agent: Interrupt/Cancel
|
||||
Agent->>App: Handle interruption
|
||||
|
||||
Note over App,Agent: Session stays alive
|
||||
Note over Tools,FS: Persistent file system<br/>state maintained
|
||||
|
||||
deactivate Agent
|
||||
```
|
||||
|
||||
### Benefits
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Image Uploads" icon="image">
|
||||
Attach images directly to messages for visual analysis and understanding
|
||||
</Card>
|
||||
|
||||
<Card title="Queued Messages" icon="layer-group">
|
||||
Send multiple messages that process sequentially, with ability to interrupt
|
||||
</Card>
|
||||
|
||||
<Card title="Tool Integration" icon="wrench">
|
||||
Full access to all tools and custom MCP servers during the session
|
||||
</Card>
|
||||
|
||||
<Card title="Hooks Support" icon="link">
|
||||
Use lifecycle hooks to customize behavior at various points
|
||||
</Card>
|
||||
|
||||
<Card title="Real-time Feedback" icon="bolt">
|
||||
See responses as they're generated, not just final results
|
||||
</Card>
|
||||
|
||||
<Card title="Context Persistence" icon="database">
|
||||
Maintain conversation context across multiple turns naturally
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
### Implementation Example
|
||||
|
||||
<CodeGroup>
|
||||
```typescript TypeScript theme={null}
|
||||
import { query } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
async function* generateMessages() {
|
||||
// First message
|
||||
yield {
|
||||
type: "user" as const,
|
||||
message: {
|
||||
role: "user" as const,
|
||||
content: "Analyze this codebase for security issues"
|
||||
}
|
||||
};
|
||||
|
||||
// Wait for conditions or user input
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Follow-up with image
|
||||
yield {
|
||||
type: "user" as const,
|
||||
message: {
|
||||
role: "user" as const,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Review this architecture diagram"
|
||||
},
|
||||
{
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
media_type: "image/png",
|
||||
data: readFileSync("diagram.png", "base64")
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Process streaming responses
|
||||
for await (const message of query({
|
||||
prompt: generateMessages(),
|
||||
options: {
|
||||
maxTurns: 10,
|
||||
allowedTools: ["Read", "Grep"]
|
||||
}
|
||||
})) {
|
||||
if (message.type === "result") {
|
||||
console.log(message.result);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```python Python theme={null}
|
||||
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, AssistantMessage, TextBlock
|
||||
import asyncio
|
||||
import base64
|
||||
|
||||
async def streaming_analysis():
|
||||
async def message_generator():
|
||||
# First message
|
||||
yield {
|
||||
"type": "user",
|
||||
"message": {
|
||||
"role": "user",
|
||||
"content": "Analyze this codebase for security issues"
|
||||
}
|
||||
}
|
||||
|
||||
# Wait for conditions
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Follow-up with image
|
||||
with open("diagram.png", "rb") as f:
|
||||
image_data = base64.b64encode(f.read()).decode()
|
||||
|
||||
yield {
|
||||
"type": "user",
|
||||
"message": {
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Review this architecture diagram"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": "image/png",
|
||||
"data": image_data
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# Use ClaudeSDKClient for streaming input
|
||||
options = ClaudeAgentOptions(
|
||||
max_turns=10,
|
||||
allowed_tools=["Read", "Grep"]
|
||||
)
|
||||
|
||||
async with ClaudeSDKClient(options) as client:
|
||||
# Send streaming input
|
||||
await client.query(message_generator())
|
||||
|
||||
# Process responses
|
||||
async for message in client.receive_response():
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
print(block.text)
|
||||
|
||||
asyncio.run(streaming_analysis())
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Single Message Input
|
||||
|
||||
Single message input is simpler but more limited.
|
||||
|
||||
### When to Use Single Message Input
|
||||
|
||||
Use single message input when:
|
||||
|
||||
* You need a one-shot response
|
||||
* You do not need image attachments, hooks, etc.
|
||||
* You need to operate in a stateless environment, such as a lambda function
|
||||
|
||||
### Limitations
|
||||
|
||||
<Warning>
|
||||
Single message input mode does **not** support:
|
||||
|
||||
* Direct image attachments in messages
|
||||
* Dynamic message queueing
|
||||
* Real-time interruption
|
||||
* Hook integration
|
||||
* Natural multi-turn conversations
|
||||
</Warning>
|
||||
|
||||
### Implementation Example
|
||||
|
||||
<CodeGroup>
|
||||
```typescript TypeScript theme={null}
|
||||
import { query } from "@anthropic-ai/claude-agent-sdk";
|
||||
|
||||
// Simple one-shot query
|
||||
for await (const message of query({
|
||||
prompt: "Explain the authentication flow",
|
||||
options: {
|
||||
maxTurns: 1,
|
||||
allowedTools: ["Read", "Grep"]
|
||||
}
|
||||
})) {
|
||||
if (message.type === "result") {
|
||||
console.log(message.result);
|
||||
}
|
||||
}
|
||||
|
||||
// Continue conversation with session management
|
||||
for await (const message of query({
|
||||
prompt: "Now explain the authorization process",
|
||||
options: {
|
||||
continue: true,
|
||||
maxTurns: 1
|
||||
}
|
||||
})) {
|
||||
if (message.type === "result") {
|
||||
console.log(message.result);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```python Python theme={null}
|
||||
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
|
||||
import asyncio
|
||||
|
||||
async def single_message_example():
|
||||
# Simple one-shot query using query() function
|
||||
async for message in query(
|
||||
prompt="Explain the authentication flow",
|
||||
options=ClaudeAgentOptions(
|
||||
max_turns=1,
|
||||
allowed_tools=["Read", "Grep"]
|
||||
)
|
||||
):
|
||||
if isinstance(message, ResultMessage):
|
||||
print(message.result)
|
||||
|
||||
# Continue conversation with session management
|
||||
async for message in query(
|
||||
prompt="Now explain the authorization process",
|
||||
options=ClaudeAgentOptions(
|
||||
continue_conversation=True,
|
||||
max_turns=1
|
||||
)
|
||||
):
|
||||
if isinstance(message, ResultMessage):
|
||||
print(message.result)
|
||||
|
||||
asyncio.run(single_message_example())
|
||||
```
|
||||
</CodeGroup>
|
||||
@@ -0,0 +1,222 @@
|
||||
# Context Engineering for AI Agents: Best Practices Cheat Sheet
|
||||
|
||||
## Core Principle
|
||||
**Find the smallest possible set of high-signal tokens that maximize the likelihood of your desired outcome.**
|
||||
|
||||
---
|
||||
|
||||
## Context Engineering vs Prompt Engineering
|
||||
|
||||
**Prompt Engineering**: Writing and organizing LLM instructions for optimal outcomes (one-time task)
|
||||
|
||||
**Context Engineering**: Curating and maintaining the optimal set of tokens during inference across multiple turns (iterative process)
|
||||
|
||||
Context engineering manages:
|
||||
- System instructions
|
||||
- Tools
|
||||
- Model Context Protocol (MCP)
|
||||
- External data
|
||||
- Message history
|
||||
- Runtime data retrieval
|
||||
|
||||
---
|
||||
|
||||
## The Problem: Context Rot
|
||||
|
||||
**Key Insight**: LLMs have an "attention budget" that gets depleted as context grows
|
||||
|
||||
- Every token attends to every other token (n² relationships)
|
||||
- As context length increases, model accuracy decreases
|
||||
- Models have less training experience with longer sequences
|
||||
- Context must be treated as a finite resource with diminishing marginal returns
|
||||
|
||||
---
|
||||
|
||||
## System Prompts: Find the "Right Altitude"
|
||||
|
||||
### The Goldilocks Zone
|
||||
|
||||
**Too Prescriptive** ❌
|
||||
- Hardcoded if-else logic
|
||||
- Brittle and fragile
|
||||
- High maintenance complexity
|
||||
|
||||
**Too Vague** ❌
|
||||
- High-level guidance without concrete signals
|
||||
- Falsely assumes shared context
|
||||
- Lacks actionable direction
|
||||
|
||||
**Just Right** ✅
|
||||
- Specific enough to guide behavior effectively
|
||||
- Flexible enough to provide strong heuristics
|
||||
- Minimal set of information that fully outlines expected behavior
|
||||
|
||||
### Best Practices
|
||||
- Use simple, direct language
|
||||
- Organize into distinct sections (`<background_information>`, `<instructions>`, `## Tool guidance`, etc.)
|
||||
- Use XML tags or Markdown headers for structure
|
||||
- Start with minimal prompt, add based on failure modes
|
||||
- Note: Minimal ≠ short (provide sufficient information upfront)
|
||||
|
||||
---
|
||||
|
||||
## Tools: Minimal and Clear
|
||||
|
||||
### Design Principles
|
||||
- **Self-contained**: Each tool has a single, clear purpose
|
||||
- **Robust to error**: Handle edge cases gracefully
|
||||
- **Extremely clear**: Intended use is unambiguous
|
||||
- **Token-efficient**: Returns relevant information without bloat
|
||||
- **Descriptive parameters**: Unambiguous input names (e.g., `user_id` not `user`)
|
||||
|
||||
### Critical Rule
|
||||
**If a human engineer can't definitively say which tool to use in a given situation, an AI agent can't be expected to do better.**
|
||||
|
||||
### Common Failure Modes to Avoid
|
||||
- Bloated tool sets covering too much functionality
|
||||
- Tools with overlapping purposes
|
||||
- Ambiguous decision points about which tool to use
|
||||
|
||||
---
|
||||
|
||||
## Examples: Diverse, Not Exhaustive
|
||||
|
||||
**Do** ✅
|
||||
- Curate a set of diverse, canonical examples
|
||||
- Show expected behavior effectively
|
||||
- Think "pictures worth a thousand words"
|
||||
|
||||
**Don't** ❌
|
||||
- Stuff in a laundry list of edge cases
|
||||
- Try to articulate every possible rule
|
||||
- Overwhelm with exhaustive scenarios
|
||||
|
||||
---
|
||||
|
||||
## Context Retrieval Strategies
|
||||
|
||||
### Just-In-Time Context (Recommended for Agents)
|
||||
**Approach**: Maintain lightweight identifiers (file paths, queries, links) and dynamically load data at runtime
|
||||
|
||||
**Benefits**:
|
||||
- Avoids context pollution
|
||||
- Enables progressive disclosure
|
||||
- Mirrors human cognition (we don't memorize everything)
|
||||
- Leverages metadata (file names, folder structure, timestamps)
|
||||
- Agents discover context incrementally
|
||||
|
||||
**Trade-offs**:
|
||||
- Slower than pre-computed retrieval
|
||||
- Requires proper tool guidance to avoid dead-ends
|
||||
|
||||
### Pre-Inference Retrieval (Traditional RAG)
|
||||
**Approach**: Use embedding-based retrieval to surface context before inference
|
||||
|
||||
**When to Use**: Static content that won't change during interaction
|
||||
|
||||
### Hybrid Strategy (Best of Both)
|
||||
**Approach**: Retrieve some data upfront, enable autonomous exploration as needed
|
||||
|
||||
**Example**: Claude Code loads CLAUDE.md files upfront, uses glob/grep for just-in-time retrieval
|
||||
|
||||
**Rule of Thumb**: "Do the simplest thing that works"
|
||||
|
||||
---
|
||||
|
||||
## Long-Horizon Tasks: Three Techniques
|
||||
|
||||
### 1. Compaction
|
||||
**What**: Summarize conversation nearing context limit, reinitiate with summary
|
||||
|
||||
**Implementation**:
|
||||
- Pass message history to model for compression
|
||||
- Preserve critical details (architectural decisions, bugs, implementation)
|
||||
- Discard redundant outputs
|
||||
- Continue with compressed context + recently accessed files
|
||||
|
||||
**Tuning Process**:
|
||||
1. **First**: Maximize recall (capture all relevant information)
|
||||
2. **Then**: Improve precision (eliminate superfluous content)
|
||||
|
||||
**Low-Hanging Fruit**: Clear old tool calls and results
|
||||
|
||||
**Best For**: Tasks requiring extensive back-and-forth
|
||||
|
||||
### 2. Structured Note-Taking (Agentic Memory)
|
||||
**What**: Agent writes notes persisted outside context window, retrieved later
|
||||
|
||||
**Examples**:
|
||||
- To-do lists
|
||||
- NOTES.md files
|
||||
- Game state tracking (Pokémon example: tracking 1,234 steps of training)
|
||||
- Project progress logs
|
||||
|
||||
**Benefits**:
|
||||
- Persistent memory with minimal overhead
|
||||
- Maintains critical context across tool calls
|
||||
- Enables multi-hour coherent strategies
|
||||
|
||||
**Best For**: Iterative development with clear milestones
|
||||
|
||||
### 3. Sub-Agent Architectures
|
||||
**What**: Specialized sub-agents handle focused tasks with clean context windows
|
||||
|
||||
**How It Works**:
|
||||
- Main agent coordinates high-level plan
|
||||
- Sub-agents perform deep technical work
|
||||
- Sub-agents explore extensively (tens of thousands of tokens)
|
||||
- Return condensed summaries (1,000-2,000 tokens)
|
||||
|
||||
**Benefits**:
|
||||
- Clear separation of concerns
|
||||
- Parallel exploration
|
||||
- Detailed context remains isolated
|
||||
|
||||
**Best For**: Complex research and analysis tasks
|
||||
|
||||
---
|
||||
|
||||
## Quick Decision Framework
|
||||
|
||||
| Scenario | Recommended Approach |
|
||||
|----------|---------------------|
|
||||
| Static content | Pre-inference retrieval or hybrid |
|
||||
| Dynamic exploration needed | Just-in-time context |
|
||||
| Extended back-and-forth | Compaction |
|
||||
| Iterative development | Structured note-taking |
|
||||
| Complex research | Sub-agent architectures |
|
||||
| Rapid model improvement | "Do the simplest thing that works" |
|
||||
|
||||
---
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
1. **Context is finite**: Treat it as a precious resource with an attention budget
|
||||
2. **Think holistically**: Consider the entire state available to the LLM
|
||||
3. **Stay minimal**: More context isn't always better
|
||||
4. **Be iterative**: Context curation happens each time you pass to the model
|
||||
5. **Design for autonomy**: As models improve, let them act intelligently
|
||||
6. **Start simple**: Test with minimal setup, add based on failure modes
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- ❌ Cramming everything into prompts
|
||||
- ❌ Creating brittle if-else logic
|
||||
- ❌ Building bloated tool sets
|
||||
- ❌ Stuffing exhaustive edge cases as examples
|
||||
- ❌ Assuming larger context windows solve everything
|
||||
- ❌ Ignoring context pollution over long interactions
|
||||
|
||||
---
|
||||
|
||||
## Remember
|
||||
|
||||
> "Even as models continue to improve, the challenge of maintaining coherence across extended interactions will remain central to building more effective agents."
|
||||
|
||||
Context engineering will evolve, but the core principle stays the same: **optimize signal-to-noise ratio in your token budget**.
|
||||
|
||||
---
|
||||
|
||||
*Based on Anthropic's "Effective context engineering for AI agents" (September 2025)*
|
||||
Vendored
-511
File diff suppressed because one or more lines are too long
@@ -0,0 +1,309 @@
|
||||
---
|
||||
title: "Database Architecture"
|
||||
description: "SQLite schema, FTS5 search, and data storage"
|
||||
---
|
||||
|
||||
# Database Architecture
|
||||
|
||||
Claude-Mem uses SQLite 3 with the better-sqlite3 native module for persistent storage and FTS5 for full-text search.
|
||||
|
||||
## Database Location
|
||||
|
||||
- **Current**: `~/.claude-mem/claude-mem.db`
|
||||
|
||||
**Note**: Despite the README claiming v4.0.0+ moved the database to `${CLAUDE_PLUGIN_ROOT}/data/`, the actual implementation still uses `~/.claude-mem/`.
|
||||
|
||||
## Database Implementation
|
||||
|
||||
**Primary Implementation**: better-sqlite3 (native SQLite module)
|
||||
- Used by: SessionStore and SessionSearch
|
||||
- Format: Synchronous API with better performance
|
||||
- **Note**: Database.ts (using bun:sqlite) is legacy code
|
||||
|
||||
## Core Tables
|
||||
|
||||
### 1. sdk_sessions
|
||||
|
||||
Tracks active and completed sessions.
|
||||
|
||||
```sql
|
||||
CREATE TABLE sdk_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT UNIQUE NOT NULL,
|
||||
claude_session_id TEXT,
|
||||
project TEXT NOT NULL,
|
||||
prompt_counter INTEGER DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
completed_at TEXT,
|
||||
completed_at_epoch INTEGER,
|
||||
last_activity_at TEXT,
|
||||
last_activity_epoch INTEGER
|
||||
);
|
||||
```
|
||||
|
||||
**Indexes**:
|
||||
- `idx_sdk_sessions_claude_session` on `claude_session_id`
|
||||
- `idx_sdk_sessions_project` on `project`
|
||||
- `idx_sdk_sessions_status` on `status`
|
||||
- `idx_sdk_sessions_created_at` on `created_at_epoch DESC`
|
||||
|
||||
### 2. observations
|
||||
|
||||
Individual tool executions with hierarchical structure.
|
||||
|
||||
```sql
|
||||
CREATE TABLE observations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
claude_session_id TEXT,
|
||||
project TEXT NOT NULL,
|
||||
prompt_number INTEGER,
|
||||
tool_name TEXT NOT NULL,
|
||||
correlation_id TEXT,
|
||||
|
||||
-- Hierarchical fields
|
||||
title TEXT,
|
||||
subtitle TEXT,
|
||||
narrative TEXT,
|
||||
text TEXT,
|
||||
facts TEXT,
|
||||
concepts TEXT,
|
||||
type TEXT,
|
||||
files_read TEXT,
|
||||
files_modified TEXT,
|
||||
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY (sdk_session_id) REFERENCES sdk_sessions(sdk_session_id)
|
||||
);
|
||||
```
|
||||
|
||||
**Observation Types**:
|
||||
- `decision` - Architectural or design decisions
|
||||
- `bugfix` - Bug fixes and corrections
|
||||
- `feature` - New features or capabilities
|
||||
- `refactor` - Code refactoring and cleanup
|
||||
- `discovery` - Learnings about the codebase
|
||||
- `change` - General changes and modifications
|
||||
|
||||
**Indexes**:
|
||||
- `idx_observations_session` on `session_id`
|
||||
- `idx_observations_sdk_session` on `sdk_session_id`
|
||||
- `idx_observations_project` on `project`
|
||||
- `idx_observations_tool_name` on `tool_name`
|
||||
- `idx_observations_created_at` on `created_at_epoch DESC`
|
||||
- `idx_observations_type` on `type`
|
||||
|
||||
### 3. session_summaries
|
||||
|
||||
AI-generated session summaries (multiple per session).
|
||||
|
||||
```sql
|
||||
CREATE TABLE session_summaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
claude_session_id TEXT,
|
||||
project TEXT NOT NULL,
|
||||
prompt_number INTEGER,
|
||||
|
||||
-- Summary fields
|
||||
request TEXT,
|
||||
investigated TEXT,
|
||||
learned TEXT,
|
||||
completed TEXT,
|
||||
next_steps TEXT,
|
||||
notes TEXT,
|
||||
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY (sdk_session_id) REFERENCES sdk_sessions(sdk_session_id)
|
||||
);
|
||||
```
|
||||
|
||||
**Indexes**:
|
||||
- `idx_session_summaries_sdk_session` on `sdk_session_id`
|
||||
- `idx_session_summaries_project` on `project`
|
||||
- `idx_session_summaries_created_at` on `created_at_epoch DESC`
|
||||
|
||||
### 4. user_prompts
|
||||
|
||||
Raw user prompts with FTS5 search (as of v4.2.0).
|
||||
|
||||
```sql
|
||||
CREATE TABLE user_prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
claude_session_id TEXT,
|
||||
project TEXT NOT NULL,
|
||||
prompt_number INTEGER,
|
||||
prompt_text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY (sdk_session_id) REFERENCES sdk_sessions(sdk_session_id)
|
||||
);
|
||||
```
|
||||
|
||||
**Indexes**:
|
||||
- `idx_user_prompts_sdk_session` on `sdk_session_id`
|
||||
- `idx_user_prompts_project` on `project`
|
||||
- `idx_user_prompts_created_at` on `created_at_epoch DESC`
|
||||
|
||||
### Legacy Tables
|
||||
|
||||
- **sessions**: Legacy session tracking (v3.x)
|
||||
- **memories**: Legacy compressed memory chunks (v3.x)
|
||||
- **overviews**: Legacy session summaries (v3.x)
|
||||
|
||||
## FTS5 Full-Text Search
|
||||
|
||||
SQLite FTS5 (Full-Text Search) virtual tables enable fast full-text search across observations, summaries, and user prompts.
|
||||
|
||||
### FTS5 Virtual Tables
|
||||
|
||||
#### observations_fts
|
||||
|
||||
```sql
|
||||
CREATE VIRTUAL TABLE observations_fts USING fts5(
|
||||
title,
|
||||
subtitle,
|
||||
narrative,
|
||||
text,
|
||||
facts,
|
||||
concepts,
|
||||
content='observations',
|
||||
content_rowid='id'
|
||||
);
|
||||
```
|
||||
|
||||
#### session_summaries_fts
|
||||
|
||||
```sql
|
||||
CREATE VIRTUAL TABLE session_summaries_fts USING fts5(
|
||||
request,
|
||||
investigated,
|
||||
learned,
|
||||
completed,
|
||||
next_steps,
|
||||
notes,
|
||||
content='session_summaries',
|
||||
content_rowid='id'
|
||||
);
|
||||
```
|
||||
|
||||
#### user_prompts_fts
|
||||
|
||||
```sql
|
||||
CREATE VIRTUAL TABLE user_prompts_fts USING fts5(
|
||||
prompt_text,
|
||||
content='user_prompts',
|
||||
content_rowid='id'
|
||||
);
|
||||
```
|
||||
|
||||
### Automatic Synchronization
|
||||
|
||||
FTS5 tables stay in sync via triggers:
|
||||
|
||||
```sql
|
||||
-- Insert trigger example
|
||||
CREATE TRIGGER observations_ai AFTER INSERT ON observations BEGIN
|
||||
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
|
||||
END;
|
||||
|
||||
-- Update trigger example
|
||||
CREATE TRIGGER observations_au AFTER UPDATE ON observations BEGIN
|
||||
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
|
||||
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
|
||||
END;
|
||||
|
||||
-- Delete trigger example
|
||||
CREATE TRIGGER observations_ad AFTER DELETE ON observations BEGIN
|
||||
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
|
||||
END;
|
||||
```
|
||||
|
||||
### FTS5 Query Syntax
|
||||
|
||||
FTS5 supports rich query syntax:
|
||||
|
||||
- **Simple**: `"error handling"`
|
||||
- **AND**: `"error" AND "handling"`
|
||||
- **OR**: `"bug" OR "fix"`
|
||||
- **NOT**: `"bug" NOT "feature"`
|
||||
- **Phrase**: `"'exact phrase'"`
|
||||
- **Column**: `title:"authentication"`
|
||||
|
||||
### Security
|
||||
|
||||
As of v4.2.3, all FTS5 queries are properly escaped to prevent SQL injection:
|
||||
- Double quotes are escaped: `query.replace(/"/g, '""')`
|
||||
- Comprehensive test suite with 332 injection attack tests
|
||||
|
||||
## Database Classes
|
||||
|
||||
### SessionStore
|
||||
|
||||
CRUD operations for sessions, observations, summaries, and user prompts.
|
||||
|
||||
**Location**: `src/services/sqlite/SessionStore.ts`
|
||||
|
||||
**Methods**:
|
||||
- `createSession()`
|
||||
- `getSession()`
|
||||
- `updateSession()`
|
||||
- `createObservation()`
|
||||
- `getObservations()`
|
||||
- `createSummary()`
|
||||
- `getSummaries()`
|
||||
- `createUserPrompt()`
|
||||
|
||||
### SessionSearch
|
||||
|
||||
FTS5 full-text search with 8 specialized search methods.
|
||||
|
||||
**Location**: `src/services/sqlite/SessionSearch.ts`
|
||||
|
||||
**Methods**:
|
||||
- `searchObservations()` - Full-text search across observations
|
||||
- `searchSessions()` - Full-text search across summaries
|
||||
- `searchUserPrompts()` - Full-text search across user prompts
|
||||
- `findByConcept()` - Find by concept tags
|
||||
- `findByFile()` - Find by file references
|
||||
- `findByType()` - Find by observation type
|
||||
- `getRecentContext()` - Get recent session context
|
||||
- `advancedSearch()` - Combined filters
|
||||
|
||||
## Migrations
|
||||
|
||||
Database schema is managed via migrations in `src/services/sqlite/migrations.ts`.
|
||||
|
||||
**Migration History**:
|
||||
- Migration 001: Initial schema (sessions, memories, overviews, diagnostics, transcript_events)
|
||||
- Migration 002: Hierarchical memory fields (title, subtitle, facts, concepts, files_touched)
|
||||
- Migration 003: SDK sessions and observations
|
||||
- Migration 004: Session summaries
|
||||
- Migration 005: Multi-prompt sessions (prompt_counter, prompt_number)
|
||||
- Migration 006: FTS5 virtual tables and triggers
|
||||
- Migration 007-010: Various improvements and user prompts table
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Indexes**: All foreign keys and frequently queried columns are indexed
|
||||
- **FTS5**: Full-text search is significantly faster than LIKE queries
|
||||
- **Triggers**: Automatic synchronization has minimal overhead
|
||||
- **Connection Pooling**: better-sqlite3 reuses connections efficiently
|
||||
- **Synchronous API**: better-sqlite3 uses synchronous API for better performance
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
See [Troubleshooting - Database Issues](../troubleshooting.md#database-issues) for common problems and solutions.
|
||||
@@ -0,0 +1,201 @@
|
||||
---
|
||||
title: "Plugin Hooks"
|
||||
description: "5 lifecycle hooks that power Claude-Mem"
|
||||
---
|
||||
|
||||
# Plugin Hooks
|
||||
|
||||
Claude-Mem integrates with Claude Code through 5 lifecycle hooks that capture events and inject context.
|
||||
|
||||
## Hook Overview
|
||||
|
||||
| Hook Name | Purpose | Timeout | Script |
|
||||
|---------------------|--------------------------------------|---------|-------------------------|
|
||||
| SessionStart | Inject context from previous sessions| 120s | context-hook.js |
|
||||
| UserPromptSubmit | Create/track new sessions | 120s | new-hook.js |
|
||||
| PostToolUse | Capture tool execution observations | 120s | save-hook.js |
|
||||
| Stop | Generate session summaries | 120s | summary-hook.js |
|
||||
| SessionEnd | Mark sessions complete | 120s | cleanup-hook.js |
|
||||
|
||||
## Hook Configuration
|
||||
|
||||
Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Claude-mem memory system hooks",
|
||||
"hooks": {
|
||||
"SessionStart": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "cd \"${CLAUDE_PLUGIN_ROOT}/..\" && npm install --prefer-offline --no-audit --no-fund --loglevel=error && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
"UserPromptSubmit": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
"PostToolUse": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
"Stop": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
"SessionEnd": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/cleanup-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 1. SessionStart Hook (`context-hook.js`)
|
||||
|
||||
**Purpose**: Inject context from previous sessions into Claude's initial context.
|
||||
|
||||
**Behavior**:
|
||||
- Ensures dependencies are installed (runs fast idempotent npm install)
|
||||
- Auto-starts PM2 worker service if not running
|
||||
- Retrieves last 10 session summaries with three-tier verbosity (v4.2.0)
|
||||
- Returns context via `hookSpecificOutput` in JSON format (fixed in v4.1.0)
|
||||
|
||||
**Input** (via stdin):
|
||||
```json
|
||||
{
|
||||
"session_id": "claude-session-123",
|
||||
"cwd": "/path/to/project",
|
||||
"source": "startup"
|
||||
}
|
||||
```
|
||||
|
||||
**Output** (via stdout):
|
||||
```json
|
||||
{
|
||||
"hookSpecificOutput": "# Recent Sessions\n\n## Session 1...\n"
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**: `src/hooks/context.ts` and `src/bin/hooks/context-hook.ts`
|
||||
|
||||
## 2. UserPromptSubmit Hook (`new-hook.js`)
|
||||
|
||||
**Purpose**: Create new session records and initialize session tracking.
|
||||
|
||||
**Behavior**:
|
||||
- Creates new session in database
|
||||
- Initializes session tracking
|
||||
- Saves raw user prompts for full-text search (as of v4.2.0)
|
||||
- Sends init signal to worker service
|
||||
|
||||
**Input** (via stdin):
|
||||
```json
|
||||
{
|
||||
"session_id": "claude-session-123",
|
||||
"cwd": "/path/to/project",
|
||||
"prompt": "User's actual prompt text"
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**: `src/hooks/new.ts` and `src/bin/hooks/new-hook.ts`
|
||||
|
||||
## 3. PostToolUse Hook (`save-hook.js`)
|
||||
|
||||
**Purpose**: Capture tool execution observations.
|
||||
|
||||
**Behavior**:
|
||||
- Fires after EVERY tool execution (Read, Write, Edit, Bash, etc.)
|
||||
- Sends observations to worker service for processing
|
||||
- Includes correlation IDs for tracing
|
||||
- Filters low-value observations
|
||||
|
||||
**Input** (via stdin):
|
||||
```json
|
||||
{
|
||||
"session_id": "claude-session-123",
|
||||
"cwd": "/path/to/project",
|
||||
"tool_name": "Read",
|
||||
"tool_input": {...},
|
||||
"tool_result": "...",
|
||||
"correlation_id": "abc-123"
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**: `src/hooks/save.ts` and `src/bin/hooks/save-hook.ts`
|
||||
|
||||
## 4. Stop Hook (`summary-hook.js`)
|
||||
|
||||
**Purpose**: Generate session summaries when Claude stops.
|
||||
|
||||
**Behavior**:
|
||||
- Triggers final summary generation
|
||||
- Sends summarize request to worker service
|
||||
- Summary includes: request, completed, learned, next_steps
|
||||
|
||||
**Input** (via stdin):
|
||||
```json
|
||||
{
|
||||
"session_id": "claude-session-123",
|
||||
"cwd": "/path/to/project",
|
||||
"source": "user_stop"
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**: `src/hooks/summary.ts` and `src/bin/hooks/summary-hook.ts`
|
||||
|
||||
## 5. SessionEnd Hook (`cleanup-hook.js`)
|
||||
|
||||
**Purpose**: Mark sessions as completed (graceful cleanup as of v4.1.0).
|
||||
|
||||
**Behavior**:
|
||||
- Marks sessions as completed
|
||||
- Skips cleanup on `/clear` commands to preserve ongoing sessions
|
||||
- Allows workers to finish pending operations naturally
|
||||
- Previously sent DELETE requests; now uses graceful completion
|
||||
|
||||
**Input** (via stdin):
|
||||
```json
|
||||
{
|
||||
"session_id": "claude-session-123",
|
||||
"cwd": "/path/to/project",
|
||||
"source": "normal"
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**: `src/hooks/cleanup.ts` and `src/bin/hooks/cleanup-hook.ts`
|
||||
|
||||
## Hook Development
|
||||
|
||||
### Adding a New Hook
|
||||
|
||||
1. Create hook implementation in `src/hooks/your-hook.ts`
|
||||
2. Create entry point in `src/bin/hooks/your-hook.ts`
|
||||
3. Add to `plugin/hooks/hooks.json`
|
||||
4. Rebuild with `npm run build`
|
||||
|
||||
### Hook Best Practices
|
||||
|
||||
- **Fast execution**: Hooks should complete quickly (< 1s ideal)
|
||||
- **Graceful degradation**: Don't block Claude if worker is down
|
||||
- **Structured logging**: Use logger for debugging
|
||||
- **Error handling**: Catch and log errors, don't crash
|
||||
- **JSON output**: Use `hookSpecificOutput` for context injection
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
See [Troubleshooting - Hook Issues](../troubleshooting.md#hook-issues) for common problems and solutions.
|
||||
@@ -0,0 +1,313 @@
|
||||
---
|
||||
title: "MCP Search Server"
|
||||
description: "7 search tools with examples and usage patterns"
|
||||
---
|
||||
|
||||
# MCP Search Server
|
||||
|
||||
Claude-Mem includes a Model Context Protocol (MCP) server that exposes 7 specialized search tools for querying stored observations and sessions.
|
||||
|
||||
## Overview
|
||||
|
||||
- **Location**: `src/servers/search-server.ts`
|
||||
- **Configuration**: `plugin/.mcp.json`
|
||||
- **Transport**: stdio
|
||||
- **Tools**: 7 specialized search functions
|
||||
- **Citations**: All results use `claude-mem://` URI scheme
|
||||
|
||||
## Configuration
|
||||
|
||||
The MCP server is automatically registered via `plugin/.mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"claude-mem-search": {
|
||||
"type": "stdio",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/search-server.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This registers the `claude-mem-search` server with Claude Code, making the 7 search tools available in all sessions. The server is automatically started when Claude Code launches and communicates via stdio transport.
|
||||
|
||||
## Search Tools
|
||||
|
||||
### 1. search_observations
|
||||
|
||||
Full-text search across observation titles, narratives, facts, and concepts.
|
||||
|
||||
**Parameters**:
|
||||
- `query` (required): Search query for FTS5 full-text search
|
||||
- `type`: Filter by observation type(s) (decision, bugfix, feature, refactor, discovery, change)
|
||||
- `concepts`: Filter by concept tags
|
||||
- `files`: Filter by file paths (partial match)
|
||||
- `project`: Filter by project name
|
||||
- `dateRange`: Filter by date range (`{start, end}`)
|
||||
- `orderBy`: Sort order (relevance, date_desc, date_asc)
|
||||
- `limit`: Maximum results (default: 20, max: 100)
|
||||
- `offset`: Number of results to skip
|
||||
- `format`: Output format ("index" for titles/dates only, "full" for complete details)
|
||||
|
||||
**Example**:
|
||||
```
|
||||
search_observations with query="build system" and type="decision"
|
||||
```
|
||||
|
||||
### 2. search_sessions
|
||||
|
||||
Full-text search across session summaries, requests, and learnings.
|
||||
|
||||
**Parameters**:
|
||||
- `query` (required): Search query for FTS5 full-text search
|
||||
- `project`: Filter by project name
|
||||
- `dateRange`: Filter by date range
|
||||
- `orderBy`: Sort order (relevance, date_desc, date_asc)
|
||||
- `limit`: Maximum results (default: 20, max: 100)
|
||||
- `offset`: Number of results to skip
|
||||
- `format`: Output format ("index" or "full")
|
||||
|
||||
**Example**:
|
||||
```
|
||||
search_sessions with query="hooks implementation"
|
||||
```
|
||||
|
||||
### 3. search_user_prompts
|
||||
|
||||
Search raw user prompts with full-text search. Use this to find what the user actually said/requested across all sessions.
|
||||
|
||||
**Parameters**:
|
||||
- `query` (required): Search query for FTS5 full-text search
|
||||
- `project`: Filter by project name
|
||||
- `dateRange`: Filter by date range
|
||||
- `orderBy`: Sort order (relevance, date_desc, date_asc)
|
||||
- `limit`: Maximum results (default: 20, max: 100)
|
||||
- `offset`: Number of results to skip
|
||||
- `format`: Output format ("index" for truncated prompts/dates, "full" for complete prompt text)
|
||||
|
||||
**Example**:
|
||||
```
|
||||
search_user_prompts with query="authentication feature"
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Full context reconstruction from user intent → Claude actions → outcomes
|
||||
- Pattern detection for repeated requests
|
||||
- Improved debugging by tracing from original user words to final implementation
|
||||
|
||||
### 4. find_by_concept
|
||||
|
||||
Find observations tagged with specific concepts.
|
||||
|
||||
**Parameters**:
|
||||
- `concept` (required): Concept tag to search for
|
||||
- `project`: Filter by project name
|
||||
- `dateRange`: Filter by date range
|
||||
- `orderBy`: Sort order (relevance, date_desc, date_asc)
|
||||
- `limit`: Maximum results (default: 20, max: 100)
|
||||
- `offset`: Number of results to skip
|
||||
- `format`: Output format ("index" or "full")
|
||||
|
||||
**Example**:
|
||||
```
|
||||
find_by_concept with concept="architecture"
|
||||
```
|
||||
|
||||
### 5. find_by_file
|
||||
|
||||
Find observations and sessions that reference specific file paths.
|
||||
|
||||
**Parameters**:
|
||||
- `filePath` (required): File path to search for (supports partial matching)
|
||||
- `project`: Filter by project name
|
||||
- `dateRange`: Filter by date range
|
||||
- `orderBy`: Sort order (relevance, date_desc, date_asc)
|
||||
- `limit`: Maximum results (default: 20, max: 100)
|
||||
- `offset`: Number of results to skip
|
||||
- `format`: Output format ("index" or "full")
|
||||
|
||||
**Example**:
|
||||
```
|
||||
find_by_file with filePath="worker-service.ts"
|
||||
```
|
||||
|
||||
### 6. find_by_type
|
||||
|
||||
Find observations by type (decision, bugfix, feature, refactor, discovery, change).
|
||||
|
||||
**Parameters**:
|
||||
- `type` (required): Observation type(s) to filter by (single type or array)
|
||||
- `project`: Filter by project name
|
||||
- `dateRange`: Filter by date range
|
||||
- `orderBy`: Sort order (relevance, date_desc, date_asc)
|
||||
- `limit`: Maximum results (default: 20, max: 100)
|
||||
- `offset`: Number of results to skip
|
||||
- `format`: Output format ("index" or "full")
|
||||
|
||||
**Example**:
|
||||
```
|
||||
find_by_type with type=["decision", "feature"]
|
||||
```
|
||||
|
||||
### 7. get_recent_context
|
||||
|
||||
Get recent session context including summaries and observations for a project.
|
||||
|
||||
**Parameters**:
|
||||
- `project`: Project name (defaults to current working directory basename)
|
||||
- `limit`: Number of recent sessions to retrieve (default: 3, max: 10)
|
||||
|
||||
**Example**:
|
||||
```
|
||||
get_recent_context with limit=5
|
||||
```
|
||||
|
||||
## Output Formats
|
||||
|
||||
All search tools support two output formats:
|
||||
|
||||
### Index Format (Default)
|
||||
|
||||
Returns titles, dates, and source URIs only. Uses ~10x fewer tokens than full format.
|
||||
|
||||
**Always use index format first** to get an overview and identify relevant results.
|
||||
|
||||
**Example Output**:
|
||||
```
|
||||
1. [decision] Implement graceful session cleanup
|
||||
Date: 2025-10-21 14:23:45
|
||||
Source: claude-mem://observation/123
|
||||
|
||||
2. [feature] Add FTS5 full-text search
|
||||
Date: 2025-10-21 13:15:22
|
||||
Source: claude-mem://observation/124
|
||||
```
|
||||
|
||||
### Full Format
|
||||
|
||||
Returns complete observation/summary details including narrative, facts, concepts, files, etc.
|
||||
|
||||
**Only use after reviewing index results** to dive deep into specific items of interest.
|
||||
|
||||
## Search Strategy
|
||||
|
||||
**Recommended Workflow**:
|
||||
|
||||
1. **Initial search**: Use default (index) format to see titles, dates, and sources
|
||||
2. **Review results**: Identify which items are most relevant to your needs
|
||||
3. **Deep dive**: Only then use `format: "full"` on specific items of interest
|
||||
4. **Narrow down**: Use filters (type, dateRange, concepts, files) to refine results
|
||||
|
||||
**Token Efficiency**:
|
||||
- Index format: ~50-100 tokens per result
|
||||
- Full format: ~500-1000 tokens per result
|
||||
- Start with 3-5 results to avoid MCP token limits
|
||||
|
||||
## Citations
|
||||
|
||||
All search results use the `claude-mem://` URI scheme for citations:
|
||||
|
||||
- `claude-mem://observation/{id}` - References specific observations
|
||||
- `claude-mem://session/{id}` - References specific sessions
|
||||
- `claude-mem://user-prompt/{id}` - References specific user prompts
|
||||
|
||||
These citations allow Claude to reference specific historical context in responses.
|
||||
|
||||
## FTS5 Query Syntax
|
||||
|
||||
The `query` parameter supports SQLite FTS5 full-text search syntax:
|
||||
|
||||
- **Simple**: `"error handling"`
|
||||
- **AND**: `"error" AND "handling"`
|
||||
- **OR**: `"bug" OR "fix"`
|
||||
- **NOT**: `"bug" NOT "feature"`
|
||||
- **Phrase**: `"'exact phrase'"`
|
||||
- **Column**: `title:"authentication"`
|
||||
|
||||
## Security
|
||||
|
||||
As of v4.2.3, all FTS5 queries are properly escaped to prevent SQL injection attacks:
|
||||
- Double quotes are escaped: `query.replace(/"/g, '""')`
|
||||
- Comprehensive test suite with 332 injection attack tests
|
||||
- Affects: `search_observations`, `search_sessions`, `search_user_prompts`
|
||||
|
||||
## Example Queries
|
||||
|
||||
```
|
||||
# Find all decisions about build system
|
||||
search_observations with query="build system" and type="decision"
|
||||
|
||||
# Show everything related to worker-service.ts
|
||||
find_by_file with filePath="worker-service.ts"
|
||||
|
||||
# Search what we learned about hooks
|
||||
search_sessions with query="hooks"
|
||||
|
||||
# Show observations tagged with 'architecture'
|
||||
find_by_concept with concept="architecture"
|
||||
|
||||
# Find what user asked about authentication
|
||||
search_user_prompts with query="authentication"
|
||||
|
||||
# Get recent context for debugging
|
||||
get_recent_context with limit=5
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
The MCP search server is implemented using:
|
||||
- `@modelcontextprotocol/sdk` (v1.20.1)
|
||||
- `SessionSearch` service for FTS5 queries
|
||||
- `SessionStore` for database access
|
||||
- `zod-to-json-schema` for parameter validation
|
||||
|
||||
**Source Code**: `src/servers/search-server.ts`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Tool Not Available
|
||||
|
||||
If search tools are not available in Claude Code sessions:
|
||||
|
||||
1. Check MCP configuration:
|
||||
```bash
|
||||
cat plugin/.mcp.json
|
||||
```
|
||||
|
||||
2. Verify search server is built:
|
||||
```bash
|
||||
ls -l plugin/scripts/search-server.js
|
||||
```
|
||||
|
||||
3. Rebuild if needed:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Search Returns No Results
|
||||
|
||||
1. Check database has data:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM observations;"
|
||||
```
|
||||
|
||||
2. Verify FTS5 tables exist:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%_fts';"
|
||||
```
|
||||
|
||||
3. Test query syntax:
|
||||
```bash
|
||||
# Simple query should work
|
||||
search_observations with query="test"
|
||||
```
|
||||
|
||||
### Token Limit Errors
|
||||
|
||||
If you hit MCP token limits:
|
||||
|
||||
1. Use `format: "index"` instead of `format: "full"`
|
||||
2. Reduce `limit` parameter (try 3-5 instead of 20)
|
||||
3. Use more specific filters to narrow results
|
||||
4. Use `offset` to paginate through results
|
||||
@@ -0,0 +1,165 @@
|
||||
---
|
||||
title: "Architecture Overview"
|
||||
description: "System components and data flow in Claude-Mem"
|
||||
---
|
||||
|
||||
# Architecture Overview
|
||||
|
||||
## System Components
|
||||
|
||||
Claude-Mem operates as a Claude Code plugin with four core components:
|
||||
|
||||
1. **Plugin Hooks** - Capture lifecycle events
|
||||
2. **Worker Service** - Process observations via Claude Agent SDK
|
||||
3. **Database Layer** - Store sessions and observations (SQLite + FTS5)
|
||||
4. **MCP Search Server** - Query historical context
|
||||
|
||||
## Technology Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|------------------------|-------------------------------------------|
|
||||
| **Language** | TypeScript (ES2022, ESNext modules) |
|
||||
| **Runtime** | Node.js 18+ |
|
||||
| **Database** | SQLite 3 with better-sqlite3 driver |
|
||||
| **HTTP Server** | Express.js 4.18 |
|
||||
| **AI SDK** | @anthropic-ai/claude-agent-sdk |
|
||||
| **Build Tool** | esbuild (bundles TypeScript) |
|
||||
| **Process Manager** | PM2 |
|
||||
| **Testing** | Node.js built-in test runner |
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Memory Pipeline
|
||||
```
|
||||
Hook (stdin) → Database → Worker Service → SDK Processor → Database → Next Session Hook
|
||||
```
|
||||
|
||||
1. **Input**: Claude Code sends tool execution data via stdin to hooks
|
||||
2. **Storage**: Hooks write observations to SQLite database
|
||||
3. **Processing**: Worker service reads observations, processes via SDK
|
||||
4. **Output**: Processed summaries written back to database
|
||||
5. **Retrieval**: Next session's context hook reads summaries from database
|
||||
|
||||
### Search Pipeline
|
||||
```
|
||||
Claude Request → MCP Server → SessionSearch Service → FTS5 Database → Search Results → Claude
|
||||
```
|
||||
|
||||
1. **Query**: Claude uses MCP search tools (e.g., `search_observations`)
|
||||
2. **Search**: MCP server calls SessionSearch service with query parameters
|
||||
3. **FTS5**: Full-text search executes against FTS5 virtual tables
|
||||
4. **Format**: Results formatted as `search_result` blocks with citations
|
||||
5. **Return**: Claude receives citable search results for analysis
|
||||
|
||||
## Session Lifecycle
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 1. Session Starts → Context Hook Fires │
|
||||
│ Injects summaries from last 3 sessions into Claude's context │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 2. User Types Prompt → UserPromptSubmit Hook Fires │
|
||||
│ Creates SDK session in database, notifies worker service │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 3. Claude Uses Tools → PostToolUse Hook Fires (100+ times) │
|
||||
│ Sends observations to worker service for processing │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 4. Worker Processes → Claude Agent SDK Analyzes │
|
||||
│ Extracts structured learnings via iterative AI processing │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 5. Claude Stops → Stop Hook Fires │
|
||||
│ Generates final summary with request, status, next steps │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 6. Session Ends → Cleanup Hook Fires │
|
||||
│ Marks session complete, ready for next session context │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
claude-mem/
|
||||
├── src/
|
||||
│ ├── bin/hooks/ # Entry point scripts for 5 hooks
|
||||
│ │ ├── context-hook.ts # SessionStart
|
||||
│ │ ├── new-hook.ts # UserPromptSubmit
|
||||
│ │ ├── save-hook.ts # PostToolUse
|
||||
│ │ ├── summary-hook.ts # Stop
|
||||
│ │ └── cleanup-hook.ts # SessionEnd
|
||||
│ │
|
||||
│ ├── hooks/ # Hook implementation logic
|
||||
│ │ ├── context.ts
|
||||
│ │ ├── new.ts
|
||||
│ │ ├── save.ts
|
||||
│ │ ├── summary.ts
|
||||
│ │ └── cleanup.ts
|
||||
│ │
|
||||
│ ├── servers/ # MCP servers
|
||||
│ │ └── search-server.ts # MCP search tools server
|
||||
│ │
|
||||
│ ├── sdk/ # Claude Agent SDK integration
|
||||
│ │ ├── prompts.ts # XML prompt builders
|
||||
│ │ ├── parser.ts # XML response parser
|
||||
│ │ └── worker.ts # Main SDK agent loop
|
||||
│ │
|
||||
│ ├── services/
|
||||
│ │ ├── worker-service.ts # Express HTTP service
|
||||
│ │ └── sqlite/ # Database layer
|
||||
│ │ ├── SessionStore.ts # CRUD operations
|
||||
│ │ ├── SessionSearch.ts # FTS5 search service
|
||||
│ │ ├── migrations.ts
|
||||
│ │ └── types.ts
|
||||
│ │
|
||||
│ ├── shared/ # Shared utilities
|
||||
│ │ ├── config.ts
|
||||
│ │ ├── paths.ts
|
||||
│ │ └── storage.ts
|
||||
│ │
|
||||
│ └── utils/
|
||||
│ ├── logger.ts
|
||||
│ ├── platform.ts
|
||||
│ └── port-allocator.ts
|
||||
│
|
||||
├── plugin/ # Plugin distribution
|
||||
│ ├── .claude-plugin/
|
||||
│ │ └── plugin.json
|
||||
│ ├── .mcp.json # MCP server configuration
|
||||
│ ├── hooks/
|
||||
│ │ └── hooks.json
|
||||
│ └── scripts/ # Built executables
|
||||
│ ├── context-hook.js
|
||||
│ ├── new-hook.js
|
||||
│ ├── save-hook.js
|
||||
│ ├── summary-hook.js
|
||||
│ ├── cleanup-hook.js
|
||||
│ ├── worker-service.cjs # Background worker
|
||||
│ └── search-server.js # MCP search server
|
||||
│
|
||||
├── tests/ # Test suite
|
||||
├── docs/ # Documentation
|
||||
└── ecosystem.config.cjs # PM2 configuration
|
||||
```
|
||||
|
||||
## Component Details
|
||||
|
||||
### 1. Plugin Hooks
|
||||
See [Plugin Hooks](/architecture/hooks) for detailed hook documentation.
|
||||
|
||||
### 2. Worker Service
|
||||
See [Worker Service](/architecture/worker-service) for HTTP API and endpoints.
|
||||
|
||||
### 3. Database Layer
|
||||
See [Database Architecture](/architecture/database) for schema and FTS5 search.
|
||||
|
||||
### 4. MCP Search Server
|
||||
See [MCP Search Server](/architecture/mcp-search) for search tools and examples.
|
||||
@@ -0,0 +1,248 @@
|
||||
---
|
||||
title: "Worker Service"
|
||||
description: "HTTP API and PM2 process management"
|
||||
---
|
||||
|
||||
# Worker Service
|
||||
|
||||
The worker service is a long-running HTTP API built with Express.js and managed by PM2. It processes observations through the Claude Agent SDK separately from hook execution to prevent timeout issues.
|
||||
|
||||
## Overview
|
||||
|
||||
- **Technology**: Express.js HTTP server
|
||||
- **Process Manager**: PM2
|
||||
- **Port**: Fixed port 37777 (configurable via `CLAUDE_MEM_WORKER_PORT`)
|
||||
- **Location**: `src/services/worker-service.ts`
|
||||
- **Built Output**: `plugin/scripts/worker-service.cjs`
|
||||
- **Model**: Configurable via `CLAUDE_MEM_MODEL` environment variable (default: claude-sonnet-4-5)
|
||||
|
||||
## REST API Endpoints
|
||||
|
||||
The worker service exposes 6 HTTP endpoints:
|
||||
|
||||
### 1. Health Check
|
||||
```
|
||||
GET /health
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"uptime": 12345,
|
||||
"port": 37777
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Initialize Session
|
||||
```
|
||||
POST /sessions/:sessionDbId/init
|
||||
```
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"sdk_session_id": "abc-123",
|
||||
"project": "my-project"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"session_id": "abc-123"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Add Observation
|
||||
```
|
||||
POST /sessions/:sessionDbId/observations
|
||||
```
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"tool_name": "Read",
|
||||
"tool_input": {...},
|
||||
"tool_result": "...",
|
||||
"correlation_id": "xyz-789"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"observation_id": 123
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Generate Summary
|
||||
```
|
||||
POST /sessions/:sessionDbId/summarize
|
||||
```
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"trigger": "stop"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"summary_id": 456
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Session Status
|
||||
```
|
||||
GET /sessions/:sessionDbId/status
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"session_id": "abc-123",
|
||||
"status": "active",
|
||||
"observation_count": 42,
|
||||
"summary_count": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Delete Session
|
||||
```
|
||||
DELETE /sessions/:sessionDbId
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: As of v4.1.0, the cleanup hook no longer calls this endpoint. Sessions are marked complete instead of deleted to allow graceful worker shutdown.
|
||||
|
||||
## PM2 Management
|
||||
|
||||
### Configuration
|
||||
|
||||
The worker is configured via `ecosystem.config.cjs`:
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: 'claude-mem-worker',
|
||||
script: './plugin/scripts/worker-service.cjs',
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: '1G',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
FORCE_COLOR: '1'
|
||||
}
|
||||
}]
|
||||
};
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
# Start worker (auto-starts on first session)
|
||||
npm run worker:start
|
||||
|
||||
# Stop worker
|
||||
npm run worker:stop
|
||||
|
||||
# Restart worker
|
||||
npm run worker:restart
|
||||
|
||||
# View logs
|
||||
npm run worker:logs
|
||||
|
||||
# Check status
|
||||
npm run worker:status
|
||||
```
|
||||
|
||||
### Auto-Start Behavior
|
||||
|
||||
As of v4.0.0, the worker service auto-starts when the SessionStart hook fires. Manual start is optional.
|
||||
|
||||
## Claude Agent SDK Integration
|
||||
|
||||
The worker service routes observations to the Claude Agent SDK for AI-powered processing:
|
||||
|
||||
### Processing Flow
|
||||
|
||||
1. **Observation Queue**: Observations accumulate in memory
|
||||
2. **SDK Processing**: Observations sent to Claude via Agent SDK
|
||||
3. **XML Parsing**: Responses parsed for structured data
|
||||
4. **Database Storage**: Processed observations stored in SQLite
|
||||
|
||||
### SDK Components
|
||||
|
||||
- **Prompts** (`src/sdk/prompts.ts`): Builds XML-structured prompts
|
||||
- **Parser** (`src/sdk/parser.ts`): Parses Claude's XML responses
|
||||
- **Worker** (`src/sdk/worker.ts`): Main SDK agent loop
|
||||
|
||||
### Model Configuration
|
||||
|
||||
Set the AI model used for processing via environment variable:
|
||||
|
||||
```bash
|
||||
export CLAUDE_MEM_MODEL=claude-sonnet-4-5
|
||||
```
|
||||
|
||||
Available models:
|
||||
- `claude-haiku-4-5` - Fast, cost-efficient
|
||||
- `claude-sonnet-4-5` - Balanced (default)
|
||||
- `claude-opus-4` - Most capable
|
||||
- `claude-3-7-sonnet` - Alternative version
|
||||
|
||||
## Port Allocation
|
||||
|
||||
The worker uses a fixed port (37777 by default) for consistent communication:
|
||||
|
||||
- **Default**: Port 37777
|
||||
- **Override**: Set `CLAUDE_MEM_WORKER_PORT` environment variable
|
||||
- **Port File**: `${CLAUDE_PLUGIN_ROOT}/data/worker.port` tracks current port
|
||||
|
||||
If port 37777 is in use, the worker will fail to start. Set a custom port via environment variable.
|
||||
|
||||
## Data Storage
|
||||
|
||||
The worker service stores data in the plugin data directory:
|
||||
|
||||
```
|
||||
${CLAUDE_PLUGIN_ROOT}/data/
|
||||
├── claude-mem.db # SQLite database
|
||||
├── worker.port # Current worker port file
|
||||
└── logs/
|
||||
├── worker-out.log # Worker stdout logs
|
||||
└── worker-error.log # Worker stderr logs
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The worker implements graceful degradation:
|
||||
|
||||
- **Database Errors**: Logged but don't crash the service
|
||||
- **SDK Errors**: Retried with exponential backoff
|
||||
- **Network Errors**: Logged and skipped
|
||||
- **Invalid Input**: Validated and rejected with error response
|
||||
|
||||
## Performance
|
||||
|
||||
- **Async Processing**: Observations processed asynchronously
|
||||
- **In-Memory Queue**: Fast observation accumulation
|
||||
- **Batch Processing**: Multiple observations processed together
|
||||
- **Connection Pooling**: SQLite connections reused
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
See [Troubleshooting - Worker Issues](../troubleshooting.md#worker-service-issues) for common problems and solutions.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,303 @@
|
||||
---
|
||||
title: "Configuration"
|
||||
description: "Environment variables and settings for Claude-Mem"
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|-------------------------|---------------------------------|---------------------------------------|
|
||||
| `CLAUDE_PLUGIN_ROOT` | Set by Claude Code | Plugin installation directory |
|
||||
| `CLAUDE_MEM_DATA_DIR` | `~/.claude-mem/` | Data directory (dev override) |
|
||||
| `CLAUDE_MEM_WORKER_PORT`| `37777` | Worker service port |
|
||||
| `CLAUDE_MEM_MODEL` | `claude-sonnet-4-5` | AI model for processing observations |
|
||||
| `NODE_ENV` | `production` | Environment mode |
|
||||
| `FORCE_COLOR` | `1` | Enable colored logs |
|
||||
|
||||
## Model Configuration
|
||||
|
||||
Configure which AI model processes your observations.
|
||||
|
||||
### Available Models
|
||||
|
||||
- `claude-haiku-4-5` - Fast, cost-efficient
|
||||
- `claude-sonnet-4-5` - Balanced (default)
|
||||
- `claude-opus-4` - Most capable
|
||||
- `claude-3-7-sonnet` - Alternative version
|
||||
|
||||
### Using the Interactive Script
|
||||
|
||||
```bash
|
||||
./claude-mem-settings.sh
|
||||
```
|
||||
|
||||
This script manages `CLAUDE_MEM_MODEL` in `~/.claude/settings.json`.
|
||||
|
||||
### Manual Configuration
|
||||
|
||||
Edit `~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"CLAUDE_MEM_MODEL": "claude-sonnet-4-5"
|
||||
}
|
||||
```
|
||||
|
||||
## Files and Directories
|
||||
|
||||
### Data Directory Structure
|
||||
|
||||
```
|
||||
~/.claude-mem/
|
||||
├── claude-mem.db # SQLite database
|
||||
├── worker.port # Current worker port file
|
||||
└── logs/
|
||||
├── worker-out.log # Worker stdout logs
|
||||
└── worker-error.log # Worker stderr logs
|
||||
```
|
||||
|
||||
### Plugin Directory Structure
|
||||
|
||||
```
|
||||
${CLAUDE_PLUGIN_ROOT}/
|
||||
├── .claude-plugin/
|
||||
│ └── plugin.json # Plugin metadata
|
||||
├── .mcp.json # MCP server configuration
|
||||
├── hooks/
|
||||
│ └── hooks.json # Hook configuration
|
||||
└── scripts/ # Built executables
|
||||
├── context-hook.js
|
||||
├── new-hook.js
|
||||
├── save-hook.js
|
||||
├── summary-hook.js
|
||||
├── cleanup-hook.js
|
||||
├── worker-service.cjs
|
||||
└── search-server.js
|
||||
```
|
||||
|
||||
## Plugin Configuration
|
||||
|
||||
### Hooks Configuration
|
||||
|
||||
Hooks are configured in `plugin/hooks/hooks.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Claude-mem memory system hooks",
|
||||
"hooks": {
|
||||
"SessionStart": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "cd \"${CLAUDE_PLUGIN_ROOT}/..\" && npm install --prefer-offline --no-audit --no-fund --loglevel=error && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
"UserPromptSubmit": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
"PostToolUse": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
"Stop": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}],
|
||||
"SessionEnd": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/cleanup-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MCP Server Configuration
|
||||
|
||||
The MCP search server is configured in `plugin/.mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"claude-mem-search": {
|
||||
"type": "stdio",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/search-server.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This registers the `claude-mem-search` server with Claude Code, making the 7 search tools available in all sessions.
|
||||
|
||||
## PM2 Configuration
|
||||
|
||||
Worker service is managed by PM2 via `ecosystem.config.cjs`:
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: 'claude-mem-worker',
|
||||
script: './plugin/scripts/worker-service.cjs',
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: '1G',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
FORCE_COLOR: '1'
|
||||
}
|
||||
}]
|
||||
};
|
||||
```
|
||||
|
||||
### PM2 Settings
|
||||
|
||||
- **instances**: 1 (single instance)
|
||||
- **autorestart**: true (auto-restart on crash)
|
||||
- **watch**: false (no file watching)
|
||||
- **max_memory_restart**: 1G (restart if memory exceeds 1GB)
|
||||
|
||||
## Customization
|
||||
|
||||
### Custom Data Directory
|
||||
|
||||
For development or testing, override the data directory:
|
||||
|
||||
```bash
|
||||
export CLAUDE_MEM_DATA_DIR=/custom/path
|
||||
```
|
||||
|
||||
### Custom Worker Port
|
||||
|
||||
If port 37777 is in use:
|
||||
|
||||
```bash
|
||||
export CLAUDE_MEM_WORKER_PORT=38000
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
### Custom Model
|
||||
|
||||
Use a different AI model:
|
||||
|
||||
```bash
|
||||
export CLAUDE_MEM_MODEL=claude-opus-4
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Hook Timeouts
|
||||
|
||||
Modify timeouts in `plugin/hooks/hooks.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"timeout": 120 // Default: 120 seconds
|
||||
}
|
||||
```
|
||||
|
||||
Recommended values:
|
||||
- SessionStart: 120s (needs time for npm install and context retrieval)
|
||||
- UserPromptSubmit: 60s
|
||||
- PostToolUse: 120s (can process many observations)
|
||||
- Stop: 60s
|
||||
- SessionEnd: 60s
|
||||
|
||||
### Worker Memory Limit
|
||||
|
||||
Modify PM2 memory limit in `ecosystem.config.cjs`:
|
||||
|
||||
```javascript
|
||||
{
|
||||
max_memory_restart: '2G' // Increase if needed
|
||||
}
|
||||
```
|
||||
|
||||
### Logging Verbosity
|
||||
|
||||
Enable debug logging:
|
||||
|
||||
```bash
|
||||
export DEBUG=claude-mem:*
|
||||
npm run worker:restart
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
## Configuration Best Practices
|
||||
|
||||
1. **Use defaults**: Default configuration works for most use cases
|
||||
2. **Override selectively**: Only change what you need
|
||||
3. **Document changes**: Keep track of custom configurations
|
||||
4. **Test after changes**: Verify worker restarts successfully
|
||||
5. **Monitor logs**: Check worker logs after configuration changes
|
||||
|
||||
## Troubleshooting Configuration
|
||||
|
||||
### Configuration Not Applied
|
||||
|
||||
1. Restart worker after changes:
|
||||
```bash
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
2. Verify environment variables:
|
||||
```bash
|
||||
echo $CLAUDE_MEM_MODEL
|
||||
echo $CLAUDE_MEM_WORKER_PORT
|
||||
```
|
||||
|
||||
3. Check worker logs:
|
||||
```bash
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
### Invalid Model Name
|
||||
|
||||
If you specify an invalid model name, the worker will fall back to `claude-sonnet-4-5` and log a warning.
|
||||
|
||||
Valid models:
|
||||
- claude-haiku-4-5
|
||||
- claude-sonnet-4-5
|
||||
- claude-opus-4
|
||||
- claude-3-7-sonnet
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
If port 37777 is already in use:
|
||||
|
||||
1. Set custom port:
|
||||
```bash
|
||||
export CLAUDE_MEM_WORKER_PORT=38000
|
||||
```
|
||||
|
||||
2. Restart worker:
|
||||
```bash
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
3. Verify new port:
|
||||
```bash
|
||||
cat ~/.claude-mem/worker.port
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Architecture Overview](architecture/overview) - Understand the system
|
||||
- [Troubleshooting](troubleshooting) - Common issues
|
||||
- [Development](development) - Building from source
|
||||
@@ -0,0 +1,562 @@
|
||||
---
|
||||
title: "Development"
|
||||
description: "Build from source, run tests, and contribute to Claude-Mem"
|
||||
---
|
||||
|
||||
# Development Guide
|
||||
|
||||
## Building from Source
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18.0.0 or higher
|
||||
- npm (comes with Node.js)
|
||||
- Git
|
||||
|
||||
### Clone and Build
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/thedotmack/claude-mem.git
|
||||
cd claude-mem
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build all components
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Build Process
|
||||
|
||||
The build process uses esbuild to compile TypeScript:
|
||||
|
||||
1. Compiles TypeScript to JavaScript
|
||||
2. Creates standalone executables for each hook in `plugin/scripts/`
|
||||
3. Bundles MCP search server to `plugin/scripts/search-server.js`
|
||||
4. Bundles worker service to `plugin/scripts/worker-service.cjs`
|
||||
|
||||
**Build Output**:
|
||||
- Hook executables: `*-hook.js` (ESM format)
|
||||
- Worker service: `worker-service.cjs` (CJS format)
|
||||
- Search server: `search-server.js` (ESM format)
|
||||
|
||||
### Build Scripts
|
||||
|
||||
```bash
|
||||
# Build everything
|
||||
npm run build
|
||||
|
||||
# Build only hooks
|
||||
npm run build:hooks
|
||||
|
||||
# The build script is defined in scripts/build-hooks.js
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### 1. Make Changes
|
||||
|
||||
Edit TypeScript source files in `src/`:
|
||||
|
||||
```
|
||||
src/
|
||||
├── bin/hooks/ # Hook entry points
|
||||
├── hooks/ # Hook implementations
|
||||
├── services/ # Worker service and database
|
||||
├── servers/ # MCP search server
|
||||
├── sdk/ # Claude Agent SDK integration
|
||||
├── shared/ # Shared utilities
|
||||
└── utils/ # General utilities
|
||||
```
|
||||
|
||||
### 2. Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 3. Test
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Test specific file
|
||||
node --test tests/session-lifecycle.test.ts
|
||||
|
||||
# Test context injection
|
||||
npm run test:context
|
||||
|
||||
# Verbose context test
|
||||
npm run test:context:verbose
|
||||
```
|
||||
|
||||
### 4. Manual Testing
|
||||
|
||||
```bash
|
||||
# Start worker manually
|
||||
npm run worker:start
|
||||
|
||||
# Check worker status
|
||||
npm run worker:status
|
||||
|
||||
# View logs
|
||||
npm run worker:logs
|
||||
|
||||
# Test hooks manually
|
||||
echo '{"session_id":"test-123","cwd":"'$(pwd)'","source":"startup"}' | node plugin/scripts/context-hook.js
|
||||
```
|
||||
|
||||
### 5. Iterate
|
||||
|
||||
Repeat steps 1-4 until your changes work as expected.
|
||||
|
||||
## Adding New Features
|
||||
|
||||
### Adding a New Hook
|
||||
|
||||
1. Create hook implementation in `src/hooks/your-hook.ts`:
|
||||
|
||||
```typescript
|
||||
import { HookInput } from './types';
|
||||
|
||||
export async function yourHook(input: HookInput) {
|
||||
// Hook implementation
|
||||
return {
|
||||
hookSpecificOutput: 'Optional output'
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
2. Create entry point in `src/bin/hooks/your-hook.ts`:
|
||||
|
||||
```typescript
|
||||
#!/usr/bin/env node
|
||||
import { readStdin } from '../../shared/stdin';
|
||||
import { yourHook } from '../../hooks/your-hook';
|
||||
|
||||
async function main() {
|
||||
const input = await readStdin();
|
||||
const result = await yourHook(input);
|
||||
console.log(JSON.stringify(result));
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
```
|
||||
|
||||
3. Add to `plugin/hooks/hooks.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"YourHook": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/your-hook.js",
|
||||
"timeout": 120
|
||||
}]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
4. Rebuild:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Modifying Database Schema
|
||||
|
||||
1. Add migration to `src/services/sqlite/migrations.ts`:
|
||||
|
||||
```typescript
|
||||
export const migration011: Migration = {
|
||||
version: 11,
|
||||
up: (db: Database) => {
|
||||
db.run(`
|
||||
ALTER TABLE observations ADD COLUMN new_field TEXT;
|
||||
`);
|
||||
},
|
||||
down: (db: Database) => {
|
||||
// Optional: define rollback
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
2. Update types in `src/services/sqlite/types.ts`:
|
||||
|
||||
```typescript
|
||||
export interface Observation {
|
||||
// ... existing fields
|
||||
new_field?: string;
|
||||
}
|
||||
```
|
||||
|
||||
3. Update database methods in `src/services/sqlite/SessionStore.ts`:
|
||||
|
||||
```typescript
|
||||
createObservation(obs: Observation) {
|
||||
// Include new_field in INSERT
|
||||
}
|
||||
```
|
||||
|
||||
4. Test migration:
|
||||
|
||||
```bash
|
||||
# Backup database first!
|
||||
cp ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.backup
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
```
|
||||
|
||||
### Extending SDK Prompts
|
||||
|
||||
1. Modify prompts in `src/sdk/prompts.ts`:
|
||||
|
||||
```typescript
|
||||
export function buildObservationPrompt(observation: Observation): string {
|
||||
return `
|
||||
<observation>
|
||||
<!-- Add new XML structure -->
|
||||
</observation>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
2. Update parser in `src/sdk/parser.ts`:
|
||||
|
||||
```typescript
|
||||
export function parseObservation(xml: string): ParsedObservation {
|
||||
// Parse new XML fields
|
||||
}
|
||||
```
|
||||
|
||||
3. Test:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
### Adding MCP Search Tools
|
||||
|
||||
1. Add tool definition in `src/servers/search-server.ts`:
|
||||
|
||||
```typescript
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name === 'your_new_tool') {
|
||||
// Implement tool logic
|
||||
const results = await search.yourNewSearch(params);
|
||||
return formatResults(results);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
2. Add search method in `src/services/sqlite/SessionSearch.ts`:
|
||||
|
||||
```typescript
|
||||
yourNewSearch(params: YourParams): SearchResult[] {
|
||||
// Implement FTS5 search
|
||||
}
|
||||
```
|
||||
|
||||
3. Rebuild and test:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm test
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# All tests
|
||||
npm test
|
||||
|
||||
# Specific test file
|
||||
node --test tests/your-test.test.ts
|
||||
|
||||
# With coverage (if configured)
|
||||
npm test -- --coverage
|
||||
```
|
||||
|
||||
### Writing Tests
|
||||
|
||||
Create test files in `tests/`:
|
||||
|
||||
```typescript
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
|
||||
describe('YourFeature', () => {
|
||||
it('should do something', () => {
|
||||
// Test implementation
|
||||
assert.strictEqual(result, expected);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Test Database
|
||||
|
||||
Use a separate test database:
|
||||
|
||||
```typescript
|
||||
import { SessionStore } from '../src/services/sqlite/SessionStore';
|
||||
|
||||
const store = new SessionStore(':memory:'); // In-memory database
|
||||
```
|
||||
|
||||
## Code Style
|
||||
|
||||
### TypeScript Guidelines
|
||||
|
||||
- Use TypeScript strict mode
|
||||
- Define interfaces for all data structures
|
||||
- Use async/await for asynchronous code
|
||||
- Handle errors explicitly
|
||||
- Add JSDoc comments for public APIs
|
||||
|
||||
### Formatting
|
||||
|
||||
- Follow existing code formatting
|
||||
- Use 2-space indentation
|
||||
- Use single quotes for strings
|
||||
- Add trailing commas in objects/arrays
|
||||
|
||||
### Example
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Create a new observation in the database
|
||||
*/
|
||||
export async function createObservation(
|
||||
obs: Observation
|
||||
): Promise<number> {
|
||||
try {
|
||||
const result = await db.insert('observations', {
|
||||
session_id: obs.session_id,
|
||||
tool_name: obs.tool_name,
|
||||
// ...
|
||||
});
|
||||
return result.id;
|
||||
} catch (error) {
|
||||
logger.error('Failed to create observation', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Enable Debug Logging
|
||||
|
||||
```bash
|
||||
export DEBUG=claude-mem:*
|
||||
npm run worker:restart
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
### Inspect Database
|
||||
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db
|
||||
|
||||
# View schema
|
||||
.schema observations
|
||||
|
||||
# Query data
|
||||
SELECT * FROM observations LIMIT 10;
|
||||
```
|
||||
|
||||
### Trace Observations
|
||||
|
||||
Use correlation IDs to trace observations through the pipeline:
|
||||
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db
|
||||
SELECT correlation_id, tool_name, created_at
|
||||
FROM observations
|
||||
WHERE session_id = 'YOUR_SESSION_ID'
|
||||
ORDER BY created_at;
|
||||
```
|
||||
|
||||
### Debug Hooks
|
||||
|
||||
Run hooks manually with test input:
|
||||
|
||||
```bash
|
||||
# Test context hook
|
||||
echo '{"session_id":"test-123","cwd":"'$(pwd)'","source":"startup"}' | node plugin/scripts/context-hook.js
|
||||
|
||||
# Test new hook
|
||||
echo '{"session_id":"test-123","cwd":"'$(pwd)'","prompt":"test"}' | node plugin/scripts/new-hook.js
|
||||
```
|
||||
|
||||
## Publishing
|
||||
|
||||
### NPM Publishing
|
||||
|
||||
```bash
|
||||
# Update version in package.json
|
||||
npm version patch # or minor, or major
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# Publish to NPM
|
||||
npm run release
|
||||
```
|
||||
|
||||
The `release` script:
|
||||
1. Runs tests
|
||||
2. Builds all components
|
||||
3. Publishes to NPM registry
|
||||
|
||||
### Creating a Release
|
||||
|
||||
1. Update version in `package.json`
|
||||
2. Update `CHANGELOG.md`
|
||||
3. Commit changes
|
||||
4. Create git tag
|
||||
5. Push to GitHub
|
||||
6. Publish to NPM
|
||||
|
||||
```bash
|
||||
# Update version
|
||||
npm version 4.2.4
|
||||
|
||||
# Update changelog
|
||||
# Edit CHANGELOG.md manually
|
||||
|
||||
# Commit
|
||||
git add .
|
||||
git commit -m "chore: Release v4.2.4"
|
||||
|
||||
# Tag
|
||||
git tag v4.2.4
|
||||
|
||||
# Push
|
||||
git push origin main --tags
|
||||
|
||||
# Publish to NPM
|
||||
npm run release
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
### Contribution Workflow
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Make your changes
|
||||
4. Write tests
|
||||
5. Update documentation
|
||||
6. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||
7. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
8. Open a Pull Request
|
||||
|
||||
### Pull Request Guidelines
|
||||
|
||||
- **Clear title**: Describe what the PR does
|
||||
- **Description**: Explain why the change is needed
|
||||
- **Tests**: Include tests for new features
|
||||
- **Documentation**: Update docs as needed
|
||||
- **Changelog**: Add entry to CHANGELOG.md
|
||||
- **Commits**: Use clear, descriptive commit messages
|
||||
|
||||
### Code Review Process
|
||||
|
||||
1. Automated tests must pass
|
||||
2. Code review by maintainer
|
||||
3. Address feedback
|
||||
4. Final approval
|
||||
5. Merge to main
|
||||
|
||||
## Development Tools
|
||||
|
||||
### Recommended VSCode Extensions
|
||||
|
||||
- TypeScript
|
||||
- ESLint
|
||||
- Prettier
|
||||
- SQLite Viewer
|
||||
|
||||
### Useful Commands
|
||||
|
||||
```bash
|
||||
# Check TypeScript types
|
||||
npx tsc --noEmit
|
||||
|
||||
# Lint code (if configured)
|
||||
npm run lint
|
||||
|
||||
# Format code (if configured)
|
||||
npm run format
|
||||
|
||||
# Clean build artifacts
|
||||
rm -rf plugin/scripts/*.js plugin/scripts/*.cjs
|
||||
```
|
||||
|
||||
## Troubleshooting Development
|
||||
|
||||
### Build Fails
|
||||
|
||||
1. Clean node_modules:
|
||||
```bash
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Check Node.js version:
|
||||
```bash
|
||||
node --version # Should be >= 18.0.0
|
||||
```
|
||||
|
||||
3. Check for syntax errors:
|
||||
```bash
|
||||
npx tsc --noEmit
|
||||
```
|
||||
|
||||
### Tests Fail
|
||||
|
||||
1. Check database:
|
||||
```bash
|
||||
rm ~/.claude-mem/claude-mem.db
|
||||
npm test
|
||||
```
|
||||
|
||||
2. Check worker status:
|
||||
```bash
|
||||
npm run worker:status
|
||||
```
|
||||
|
||||
3. View logs:
|
||||
```bash
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
### Worker Won't Start
|
||||
|
||||
1. Kill existing process:
|
||||
```bash
|
||||
pm2 delete claude-mem-worker
|
||||
```
|
||||
|
||||
2. Check port:
|
||||
```bash
|
||||
lsof -i :37777
|
||||
```
|
||||
|
||||
3. Try custom port:
|
||||
```bash
|
||||
export CLAUDE_MEM_WORKER_PORT=38000
|
||||
npm run worker:start
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Architecture Overview](architecture/overview) - Understand the system
|
||||
- [Configuration](configuration) - Customize Claude-Mem
|
||||
- [Troubleshooting](troubleshooting) - Common issues
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"$schema": "https://mintlify.com/schema.json",
|
||||
"name": "Claude-Mem",
|
||||
"description": "Persistent memory compression system that preserves context across Claude Code sessions",
|
||||
"theme": "mint",
|
||||
"favicon": "/claude-mem-logomark.webp",
|
||||
"logo": {
|
||||
"light": "/claude-mem-logo-for-light-mode.webp",
|
||||
"dark": "/claude-mem-logo-for-dark-mode.webp",
|
||||
"href": "https://github.com/thedotmack/claude-mem"
|
||||
},
|
||||
"colors": {
|
||||
"primary": "#3B82F6",
|
||||
"light": "#EFF6FF",
|
||||
"dark": "#1E40AF"
|
||||
},
|
||||
"navbar": {
|
||||
"links": [
|
||||
{
|
||||
"label": "GitHub",
|
||||
"href": "https://github.com/thedotmack/claude-mem"
|
||||
}
|
||||
],
|
||||
"primary": {
|
||||
"type": "button",
|
||||
"label": "Install",
|
||||
"href": "https://github.com/thedotmack/claude-mem#quick-start"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"groups": [
|
||||
{
|
||||
"group": "Get Started",
|
||||
"icon": "rocket",
|
||||
"pages": [
|
||||
"introduction",
|
||||
"installation",
|
||||
"usage/getting-started",
|
||||
"usage/search-tools"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Configuration & Development",
|
||||
"icon": "gear",
|
||||
"pages": [
|
||||
"configuration",
|
||||
"development",
|
||||
"troubleshooting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Architecture",
|
||||
"icon": "diagram-project",
|
||||
"pages": [
|
||||
"architecture/overview",
|
||||
"architecture/hooks",
|
||||
"architecture/worker-service",
|
||||
"architecture/database",
|
||||
"architecture/mcp-search"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"footer": {
|
||||
"socials": {
|
||||
"github": "https://github.com/thedotmack/claude-mem"
|
||||
},
|
||||
"links": [
|
||||
{
|
||||
"header": "Resources",
|
||||
"items": [
|
||||
{
|
||||
"label": "Documentation",
|
||||
"href": "https://github.com/thedotmack/claude-mem"
|
||||
},
|
||||
{
|
||||
"label": "Issues",
|
||||
"href": "https://github.com/thedotmack/claude-mem/issues"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"header": "Legal",
|
||||
"items": [
|
||||
{
|
||||
"label": "License (AGPL-3.0)",
|
||||
"href": "https://github.com/thedotmack/claude-mem/blob/main/LICENSE"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"seo": {
|
||||
"indexing": "all",
|
||||
"metatags": {
|
||||
"og:type": "website",
|
||||
"og:site_name": "Claude-Mem Documentation",
|
||||
"og:description": "Persistent memory compression system that preserves context across Claude Code sessions"
|
||||
}
|
||||
},
|
||||
"contextual": {
|
||||
"options": [
|
||||
"copy",
|
||||
"view",
|
||||
"chatgpt",
|
||||
"claude",
|
||||
"cursor"
|
||||
]
|
||||
},
|
||||
"integrations": {
|
||||
"telemetry": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
title: "Installation"
|
||||
description: "Install Claude-Mem plugin for persistent memory across sessions"
|
||||
---
|
||||
|
||||
# Installation Guide
|
||||
|
||||
## Quick Start
|
||||
|
||||
Install Claude-Mem directly from the plugin marketplace:
|
||||
|
||||
```bash
|
||||
/plugin marketplace add thedotmack/claude-mem
|
||||
/plugin install claude-mem
|
||||
```
|
||||
|
||||
That's it! The plugin will automatically:
|
||||
- Download prebuilt binaries (no compilation needed)
|
||||
- Install all dependencies (including PM2 and SQLite binaries)
|
||||
- Configure hooks for session lifecycle management
|
||||
- Set up the MCP search server
|
||||
- Auto-start the worker service on first session
|
||||
|
||||
Start a new Claude Code session and you'll see context from previous sessions automatically loaded.
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Node.js**: 18.0.0 or higher
|
||||
- **Claude Code**: Latest version with plugin support
|
||||
- **PM2**: Process manager (bundled with plugin - no global install required)
|
||||
- **SQLite 3**: For persistent storage (bundled)
|
||||
|
||||
## Advanced Installation
|
||||
|
||||
For development or testing, you can clone and build from source:
|
||||
|
||||
### Clone and Build
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/thedotmack/claude-mem.git
|
||||
cd claude-mem
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build hooks and worker service
|
||||
npm run build
|
||||
|
||||
# Worker service will auto-start on first Claude Code session
|
||||
# Or manually start with:
|
||||
npm run worker:start
|
||||
|
||||
# Verify worker is running
|
||||
npm run worker:status
|
||||
```
|
||||
|
||||
### Post-Installation Verification
|
||||
|
||||
#### 1. Automatic Dependency Installation
|
||||
|
||||
Dependencies are installed automatically during plugin installation. The SessionStart hook also ensures dependencies are up-to-date on each session start (this is fast and idempotent). Works cross-platform on Windows, macOS, and Linux.
|
||||
|
||||
#### 2. Verify Plugin Installation
|
||||
|
||||
Check that hooks are configured in Claude Code:
|
||||
```bash
|
||||
cat plugin/hooks/hooks.json
|
||||
```
|
||||
|
||||
#### 3. Data Directory Location
|
||||
|
||||
v4.0.0+ stores data in `${CLAUDE_PLUGIN_ROOT}/data/`:
|
||||
- Database: `${CLAUDE_PLUGIN_ROOT}/data/claude-mem.db`
|
||||
- Worker port file: `${CLAUDE_PLUGIN_ROOT}/data/worker.port`
|
||||
- Logs: `${CLAUDE_PLUGIN_ROOT}/data/logs/`
|
||||
|
||||
For development/testing, you can override:
|
||||
```bash
|
||||
export CLAUDE_MEM_DATA_DIR=/custom/path
|
||||
```
|
||||
|
||||
#### 4. Check Worker Logs
|
||||
|
||||
```bash
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
#### 5. Test Context Retrieval
|
||||
|
||||
```bash
|
||||
npm run test:context
|
||||
```
|
||||
|
||||
## Upgrading from v3.x
|
||||
|
||||
**BREAKING CHANGES - Please Read:**
|
||||
|
||||
v4.0.0 introduces breaking changes:
|
||||
|
||||
- **Data Location Changed**: Database moved from `~/.claude-mem/` to `${CLAUDE_PLUGIN_ROOT}/data/` (inside plugin directory)
|
||||
- **Fresh Start Required**: No automatic migration from v3.x. You must start with a clean database
|
||||
- **Worker Auto-Starts**: Worker service now starts automatically - no manual `npm run worker:start` needed
|
||||
- **MCP Search Server**: 7 new search tools with full-text search and citations
|
||||
- **Enhanced Architecture**: Improved plugin integration and data organization
|
||||
|
||||
See [CHANGELOG](https://github.com/thedotmack/claude-mem/blob/main/CHANGELOG.md) for complete details.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Getting Started Guide](usage/getting-started) - Learn how Claude-Mem works automatically
|
||||
- [MCP Search Tools](usage/search-tools) - Query your project history
|
||||
- [Configuration](configuration) - Customize Claude-Mem behavior
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: "Introduction"
|
||||
description: "Persistent memory compression system for Claude Code"
|
||||
---
|
||||
|
||||
# Claude-Mem
|
||||
|
||||
**Persistent memory compression system for Claude Code**
|
||||
|
||||
Claude-Mem seamlessly preserves context across sessions by automatically capturing tool usage observations, generating semantic summaries, and making them available to future sessions. This enables Claude to maintain continuity of knowledge about projects even after sessions end or reconnect.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Start a new Claude Code session in the terminal and enter the following commands:
|
||||
|
||||
```bash
|
||||
/plugin marketplace add thedotmack/claude-mem
|
||||
/plugin install claude-mem
|
||||
```
|
||||
|
||||
Restart Claude Code. Context from previous sessions will automatically appear in new sessions.
|
||||
|
||||
## Key Features
|
||||
|
||||
- 🧠 **Persistent Memory** - Context survives across sessions
|
||||
- 🔍 **7 Search Tools** - Query your project history via MCP
|
||||
- 🤖 **Automatic Operation** - No manual intervention required
|
||||
- 📊 **FTS5 Search** - Fast full-text search across observations
|
||||
- 🔗 **Citations** - Reference past decisions with `claude-mem://` URIs
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Session Start → Inject context from last 10 sessions │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ User Prompts → Create session, save user prompts │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Tool Executions → Capture observations (Read, Write, etc.) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Worker Processes → Extract learnings via Claude Agent SDK │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Session Ends → Generate summary, ready for next session │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Core Components:**
|
||||
1. **5 Lifecycle Hooks** - SessionStart, UserPromptSubmit, PostToolUse, Stop, SessionEnd
|
||||
2. **Worker Service** - HTTP API on port 37777 managed by PM2
|
||||
3. **SQLite Database** - Stores sessions, observations, summaries with FTS5 search
|
||||
4. **7 MCP Search Tools** - Query historical context with citations
|
||||
|
||||
See [Architecture Overview](architecture/overview) for details.
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Node.js**: 18.0.0 or higher
|
||||
- **Claude Code**: Latest version with plugin support
|
||||
- **PM2**: Process manager (bundled - no global install required)
|
||||
- **SQLite 3**: For persistent storage (bundled)
|
||||
|
||||
## What's New in v4.2.3
|
||||
|
||||
**Security:**
|
||||
- Fixed FTS5 injection vulnerability in search functions
|
||||
- Added comprehensive test suite with 332 injection attack tests
|
||||
|
||||
**Fixes:**
|
||||
- Fixed ESM/CJS compatibility for getDirname function
|
||||
- Fixed Windows PowerShell compatibility in SessionStart hook
|
||||
- Cross-platform dependency installation now works on Windows, macOS, and Linux
|
||||
|
||||
## Next Steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Installation" icon="download" href="/installation">
|
||||
Quick start & advanced installation
|
||||
</Card>
|
||||
<Card title="Getting Started" icon="rocket" href="/usage/getting-started">
|
||||
Learn how Claude-Mem works automatically
|
||||
</Card>
|
||||
<Card title="Architecture" icon="sitemap" href="/architecture/overview">
|
||||
System components & data flow
|
||||
</Card>
|
||||
<Card title="Search Tools" icon="magnifying-glass" href="/usage/search-tools">
|
||||
Query your project history
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,674 @@
|
||||
---
|
||||
title: "Troubleshooting"
|
||||
description: "Common issues and solutions for Claude-Mem"
|
||||
---
|
||||
|
||||
# Troubleshooting Guide
|
||||
|
||||
## Worker Service Issues
|
||||
|
||||
### Worker Service Not Starting
|
||||
|
||||
**Symptoms**: Worker doesn't start, or `pm2 status` shows no processes.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check if PM2 is running:
|
||||
```bash
|
||||
pm2 status
|
||||
```
|
||||
|
||||
2. Try starting manually:
|
||||
```bash
|
||||
npm run worker:start
|
||||
```
|
||||
|
||||
3. Check worker logs for errors:
|
||||
```bash
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
4. Full reset:
|
||||
```bash
|
||||
pm2 delete claude-mem-worker
|
||||
npm run worker:start
|
||||
```
|
||||
|
||||
5. Verify PM2 is installed:
|
||||
```bash
|
||||
which pm2
|
||||
npm list pm2
|
||||
```
|
||||
|
||||
### Port Allocation Failed
|
||||
|
||||
**Symptoms**: Worker fails to start with "port already in use" error.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check if port 37777 is in use:
|
||||
```bash
|
||||
lsof -i :37777
|
||||
```
|
||||
|
||||
2. Kill process using the port:
|
||||
```bash
|
||||
kill -9 $(lsof -t -i:37777)
|
||||
```
|
||||
|
||||
3. Or use a different port:
|
||||
```bash
|
||||
export CLAUDE_MEM_WORKER_PORT=38000
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
4. Verify new port:
|
||||
```bash
|
||||
cat ~/.claude-mem/worker.port
|
||||
```
|
||||
|
||||
### Worker Keeps Crashing
|
||||
|
||||
**Symptoms**: Worker restarts repeatedly, PM2 shows high restart count.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check error logs:
|
||||
```bash
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
2. Check memory usage:
|
||||
```bash
|
||||
pm2 status
|
||||
```
|
||||
|
||||
3. Increase memory limit in `ecosystem.config.cjs`:
|
||||
```javascript
|
||||
{
|
||||
max_memory_restart: '2G' // Increase if needed
|
||||
}
|
||||
```
|
||||
|
||||
4. Check database for corruption:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"
|
||||
```
|
||||
|
||||
### Worker Not Processing Observations
|
||||
|
||||
**Symptoms**: Observations saved but not processed, no summaries generated.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check worker is running:
|
||||
```bash
|
||||
npm run worker:status
|
||||
```
|
||||
|
||||
2. Check worker logs:
|
||||
```bash
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
3. Verify database has observations:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM observations;"
|
||||
```
|
||||
|
||||
4. Restart worker:
|
||||
```bash
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
## Hook Issues
|
||||
|
||||
### Hooks Not Firing
|
||||
|
||||
**Symptoms**: No context appears, observations not saved.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Verify hooks are configured:
|
||||
```bash
|
||||
cat plugin/hooks/hooks.json
|
||||
```
|
||||
|
||||
2. Test hooks manually:
|
||||
```bash
|
||||
# Test context hook
|
||||
echo '{"session_id":"test-123","cwd":"'$(pwd)'","source":"startup"}' | node plugin/scripts/context-hook.js
|
||||
```
|
||||
|
||||
3. Check hook permissions:
|
||||
```bash
|
||||
ls -la plugin/scripts/*.js
|
||||
```
|
||||
|
||||
4. Verify hooks.json is valid JSON:
|
||||
```bash
|
||||
cat plugin/hooks/hooks.json | jq .
|
||||
```
|
||||
|
||||
### Context Not Appearing
|
||||
|
||||
**Symptoms**: No session context when Claude starts.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check if summaries exist:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM session_summaries;"
|
||||
```
|
||||
|
||||
2. View recent sessions:
|
||||
```bash
|
||||
npm run test:context:verbose
|
||||
```
|
||||
|
||||
3. Check database integrity:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"
|
||||
```
|
||||
|
||||
4. Manually test context hook:
|
||||
```bash
|
||||
npm run test:context
|
||||
```
|
||||
|
||||
### Hooks Timeout
|
||||
|
||||
**Symptoms**: Hook execution times out, errors in Claude Code.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Increase timeout in `plugin/hooks/hooks.json`:
|
||||
```json
|
||||
{
|
||||
"timeout": 180 // Increase from 120
|
||||
}
|
||||
```
|
||||
|
||||
2. Check worker is running (prevents timeout waiting for worker):
|
||||
```bash
|
||||
npm run worker:status
|
||||
```
|
||||
|
||||
3. Check database size (large database = slow queries):
|
||||
```bash
|
||||
ls -lh ~/.claude-mem/claude-mem.db
|
||||
```
|
||||
|
||||
4. Optimize database:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "VACUUM;"
|
||||
```
|
||||
|
||||
### Dependencies Not Installing
|
||||
|
||||
**Symptoms**: SessionStart hook fails with "module not found" errors.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Manually install dependencies:
|
||||
```bash
|
||||
cd ~/.claude/plugins/marketplaces/thedotmack
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Check npm is available:
|
||||
```bash
|
||||
which npm
|
||||
npm --version
|
||||
```
|
||||
|
||||
3. Check package.json exists:
|
||||
```bash
|
||||
ls -la ~/.claude/plugins/marketplaces/thedotmack/package.json
|
||||
```
|
||||
|
||||
## Database Issues
|
||||
|
||||
### Database Locked
|
||||
|
||||
**Symptoms**: "database is locked" errors in logs.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Close all connections:
|
||||
```bash
|
||||
pm2 stop claude-mem-worker
|
||||
```
|
||||
|
||||
2. Check for stale locks:
|
||||
```bash
|
||||
lsof ~/.claude-mem/claude-mem.db
|
||||
```
|
||||
|
||||
3. Kill processes holding locks:
|
||||
```bash
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
||||
4. Restart worker:
|
||||
```bash
|
||||
npm run worker:start
|
||||
```
|
||||
|
||||
### Database Corruption
|
||||
|
||||
**Symptoms**: Integrity check fails, weird errors.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check database integrity:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"
|
||||
```
|
||||
|
||||
2. Backup database:
|
||||
```bash
|
||||
cp ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.backup
|
||||
```
|
||||
|
||||
3. Try to repair:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "VACUUM;"
|
||||
```
|
||||
|
||||
4. Nuclear option - recreate database:
|
||||
```bash
|
||||
rm ~/.claude-mem/claude-mem.db
|
||||
npm run worker:start # Will recreate schema
|
||||
```
|
||||
|
||||
### FTS5 Search Not Working
|
||||
|
||||
**Symptoms**: Search returns no results, FTS5 errors.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check FTS5 tables exist:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%_fts';"
|
||||
```
|
||||
|
||||
2. Rebuild FTS5 tables:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "
|
||||
INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
|
||||
INSERT INTO session_summaries_fts(session_summaries_fts) VALUES('rebuild');
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts) VALUES('rebuild');
|
||||
"
|
||||
```
|
||||
|
||||
3. Check triggers exist:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT name FROM sqlite_master WHERE type='trigger';"
|
||||
```
|
||||
|
||||
### Database Too Large
|
||||
|
||||
**Symptoms**: Slow performance, large database file.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check database size:
|
||||
```bash
|
||||
ls -lh ~/.claude-mem/claude-mem.db
|
||||
```
|
||||
|
||||
2. Vacuum database:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "VACUUM;"
|
||||
```
|
||||
|
||||
3. Delete old sessions:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "
|
||||
DELETE FROM observations WHERE created_at_epoch < $(date -v-30d +%s);
|
||||
DELETE FROM session_summaries WHERE created_at_epoch < $(date -v-30d +%s);
|
||||
DELETE FROM sdk_sessions WHERE created_at_epoch < $(date -v-30d +%s);
|
||||
"
|
||||
```
|
||||
|
||||
4. Rebuild FTS5 after deletion:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "
|
||||
INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
|
||||
INSERT INTO session_summaries_fts(session_summaries_fts) VALUES('rebuild');
|
||||
"
|
||||
```
|
||||
|
||||
## MCP Search Issues
|
||||
|
||||
### Search Tools Not Available
|
||||
|
||||
**Symptoms**: MCP search tools not visible in Claude Code.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check MCP configuration:
|
||||
```bash
|
||||
cat plugin/.mcp.json
|
||||
```
|
||||
|
||||
2. Verify search server is built:
|
||||
```bash
|
||||
ls -l plugin/scripts/search-server.js
|
||||
```
|
||||
|
||||
3. Rebuild if needed:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
4. Restart Claude Code
|
||||
|
||||
### Search Returns No Results
|
||||
|
||||
**Symptoms**: Valid queries return empty results.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check database has data:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM observations;"
|
||||
```
|
||||
|
||||
2. Verify FTS5 tables populated:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM observations_fts;"
|
||||
```
|
||||
|
||||
3. Test simple query:
|
||||
```bash
|
||||
# In Claude Code
|
||||
search_observations with query="test"
|
||||
```
|
||||
|
||||
4. Check query syntax:
|
||||
```bash
|
||||
# Bad: Special characters
|
||||
search_observations with query="[test]"
|
||||
|
||||
# Good: Simple words
|
||||
search_observations with query="test"
|
||||
```
|
||||
|
||||
### Token Limit Errors
|
||||
|
||||
**Symptoms**: "exceeded token limit" errors from MCP.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Use index format:
|
||||
```bash
|
||||
search_observations with query="..." and format="index"
|
||||
```
|
||||
|
||||
2. Reduce limit:
|
||||
```bash
|
||||
search_observations with query="..." and limit=3
|
||||
```
|
||||
|
||||
3. Use filters to narrow results:
|
||||
```bash
|
||||
search_observations with query="..." and type="decision" and limit=5
|
||||
```
|
||||
|
||||
4. Paginate results:
|
||||
```bash
|
||||
# First page
|
||||
search_observations with query="..." and limit=5 and offset=0
|
||||
|
||||
# Second page
|
||||
search_observations with query="..." and limit=5 and offset=5
|
||||
```
|
||||
|
||||
## Performance Issues
|
||||
|
||||
### Slow Context Injection
|
||||
|
||||
**Symptoms**: SessionStart hook takes too long.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Reduce context sessions:
|
||||
```typescript
|
||||
// In src/hooks/context.ts
|
||||
const CONTEXT_SESSIONS = 5; // Reduce from 10
|
||||
```
|
||||
|
||||
2. Optimize database:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "
|
||||
ANALYZE;
|
||||
VACUUM;
|
||||
"
|
||||
```
|
||||
|
||||
3. Add indexes (if missing):
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_project_created ON sdk_sessions(project, created_at_epoch DESC);
|
||||
"
|
||||
```
|
||||
|
||||
### Slow Search Queries
|
||||
|
||||
**Symptoms**: MCP search tools take too long.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Use more specific queries
|
||||
2. Add date range filters
|
||||
3. Add type/concept filters
|
||||
4. Reduce result limit
|
||||
5. Use index format instead of full format
|
||||
|
||||
### High Memory Usage
|
||||
|
||||
**Symptoms**: Worker uses too much memory, frequent restarts.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check current usage:
|
||||
```bash
|
||||
pm2 status
|
||||
```
|
||||
|
||||
2. Increase memory limit:
|
||||
```javascript
|
||||
// In ecosystem.config.cjs
|
||||
{
|
||||
max_memory_restart: '2G'
|
||||
}
|
||||
```
|
||||
|
||||
3. Restart worker:
|
||||
```bash
|
||||
npm run worker:restart
|
||||
```
|
||||
|
||||
4. Clean up old data (see "Database Too Large" above)
|
||||
|
||||
## Installation Issues
|
||||
|
||||
### Plugin Not Found
|
||||
|
||||
**Symptoms**: `/plugin install claude-mem` fails.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Add marketplace first:
|
||||
```bash
|
||||
/plugin marketplace add thedotmack/claude-mem
|
||||
```
|
||||
|
||||
2. Then install:
|
||||
```bash
|
||||
/plugin install claude-mem
|
||||
```
|
||||
|
||||
3. Verify installation:
|
||||
```bash
|
||||
ls -la ~/.claude/plugins/marketplaces/thedotmack/
|
||||
```
|
||||
|
||||
### Build Failures
|
||||
|
||||
**Symptoms**: `npm run build` fails.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Clean and reinstall:
|
||||
```bash
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Check Node.js version:
|
||||
```bash
|
||||
node --version # Should be >= 18.0.0
|
||||
```
|
||||
|
||||
3. Check for TypeScript errors:
|
||||
```bash
|
||||
npx tsc --noEmit
|
||||
```
|
||||
|
||||
### Missing Dependencies
|
||||
|
||||
**Symptoms**: "Cannot find module" errors.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Check package.json:
|
||||
```bash
|
||||
cat package.json
|
||||
```
|
||||
|
||||
3. Verify node_modules exists:
|
||||
```bash
|
||||
ls -la node_modules/
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Enable Verbose Logging
|
||||
|
||||
```bash
|
||||
export DEBUG=claude-mem:*
|
||||
npm run worker:restart
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
### Check Correlation IDs
|
||||
|
||||
Trace observations through the pipeline:
|
||||
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "
|
||||
SELECT correlation_id, tool_name, created_at
|
||||
FROM observations
|
||||
WHERE session_id = 'YOUR_SESSION_ID'
|
||||
ORDER BY created_at;
|
||||
"
|
||||
```
|
||||
|
||||
### Inspect Worker State
|
||||
|
||||
```bash
|
||||
# Check if worker is running
|
||||
pm2 status
|
||||
|
||||
# View logs
|
||||
pm2 logs claude-mem-worker
|
||||
|
||||
# Check port file
|
||||
cat ~/.claude-mem/worker.port
|
||||
|
||||
# Test worker health
|
||||
curl http://localhost:37777/health
|
||||
```
|
||||
|
||||
### Database Inspection
|
||||
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db
|
||||
|
||||
# View schema
|
||||
.schema
|
||||
|
||||
# Check table counts
|
||||
SELECT 'sessions', COUNT(*) FROM sdk_sessions
|
||||
UNION ALL
|
||||
SELECT 'observations', COUNT(*) FROM observations
|
||||
UNION ALL
|
||||
SELECT 'summaries', COUNT(*) FROM session_summaries
|
||||
UNION ALL
|
||||
SELECT 'prompts', COUNT(*) FROM user_prompts;
|
||||
|
||||
# View recent activity
|
||||
SELECT created_at, tool_name FROM observations ORDER BY created_at DESC LIMIT 10;
|
||||
```
|
||||
|
||||
## Common Error Messages
|
||||
|
||||
### "Worker service not responding"
|
||||
|
||||
**Cause**: Worker not running or port mismatch.
|
||||
|
||||
**Solution**: Restart worker with `npm run worker:restart`.
|
||||
|
||||
### "Database is locked"
|
||||
|
||||
**Cause**: Multiple processes accessing database.
|
||||
|
||||
**Solution**: Stop worker, kill stale processes, restart.
|
||||
|
||||
### "FTS5: syntax error"
|
||||
|
||||
**Cause**: Invalid search query syntax.
|
||||
|
||||
**Solution**: Use simpler query, avoid special characters.
|
||||
|
||||
### "SQLITE_CANTOPEN"
|
||||
|
||||
**Cause**: Database file permissions or missing directory.
|
||||
|
||||
**Solution**: Check `~/.claude-mem/` exists and is writable.
|
||||
|
||||
### "Module not found"
|
||||
|
||||
**Cause**: Missing dependencies.
|
||||
|
||||
**Solution**: Run `npm install`.
|
||||
|
||||
## Getting Help
|
||||
|
||||
If none of these solutions work:
|
||||
|
||||
1. **Check logs**:
|
||||
```bash
|
||||
npm run worker:logs
|
||||
```
|
||||
|
||||
2. **Create issue**: [GitHub Issues](https://github.com/thedotmack/claude-mem/issues)
|
||||
- Include error messages
|
||||
- Include relevant logs
|
||||
- Include steps to reproduce
|
||||
|
||||
3. **Check existing issues**: Someone may have already solved your problem
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Configuration](configuration) - Customize Claude-Mem
|
||||
- [Development](development) - Build from source
|
||||
- [Architecture](architecture/overview) - Understand the system
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
title: "Getting Started"
|
||||
description: "Learn how Claude-Mem works automatically in the background"
|
||||
---
|
||||
|
||||
# Getting Started with Claude-Mem
|
||||
|
||||
## Automatic Operation
|
||||
|
||||
Claude-Mem works automatically once installed. No manual intervention required!
|
||||
|
||||
### The Full Cycle
|
||||
|
||||
1. **Start Claude Code** - Context from last 3 sessions appears automatically
|
||||
2. **Work normally** - Every tool execution is captured
|
||||
3. **Stop Claude** - Summary is generated and saved
|
||||
4. **Next session** - Previous work appears in context
|
||||
|
||||
### What Gets Captured
|
||||
|
||||
Every time Claude uses a tool, claude-mem captures it:
|
||||
|
||||
- **Read** - File reads and content access
|
||||
- **Write** - New file creation
|
||||
- **Edit** - File modifications
|
||||
- **Bash** - Command executions
|
||||
- **Glob** - File pattern searches
|
||||
- **Grep** - Content searches
|
||||
- And all other Claude Code tools
|
||||
|
||||
### What Gets Processed
|
||||
|
||||
The worker service processes tool observations and extracts:
|
||||
|
||||
- **Title** - Brief description of what happened
|
||||
- **Subtitle** - Additional context
|
||||
- **Narrative** - Detailed explanation
|
||||
- **Facts** - Key learnings as bullet points
|
||||
- **Concepts** - Relevant tags and categories
|
||||
- **Type** - Classification (decision, bugfix, feature, etc.)
|
||||
- **Files** - Which files were read or modified
|
||||
|
||||
### Session Summaries
|
||||
|
||||
When you stop Claude (or a session ends), a summary is generated with:
|
||||
|
||||
- **Request** - What you asked for
|
||||
- **Investigated** - What Claude explored
|
||||
- **Learned** - Key discoveries and insights
|
||||
- **Completed** - What was accomplished
|
||||
- **Next Steps** - What to do next
|
||||
|
||||
### Context Injection
|
||||
|
||||
When you start a new Claude Code session, the SessionStart hook:
|
||||
|
||||
1. Queries the database for recent sessions in your project
|
||||
2. Retrieves the last 10 session summaries
|
||||
3. Formats them with three-tier verbosity (most recent = most detail)
|
||||
4. Injects them into Claude's initial context
|
||||
|
||||
This means Claude "remembers" what happened in previous sessions!
|
||||
|
||||
## Manual Commands (Optional)
|
||||
|
||||
### Worker Management
|
||||
|
||||
v4.0+ auto-starts the worker on first session. Manual commands below are optional.
|
||||
|
||||
```bash
|
||||
# Start worker service (optional - auto-starts automatically)
|
||||
npm run worker:start
|
||||
|
||||
# Stop worker service
|
||||
npm run worker:stop
|
||||
|
||||
# Restart worker service
|
||||
npm run worker:restart
|
||||
|
||||
# View worker logs
|
||||
npm run worker:logs
|
||||
|
||||
# Check worker status
|
||||
npm run worker:status
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Test context injection
|
||||
npm run test:context
|
||||
|
||||
# Verbose context test
|
||||
npm run test:context:verbose
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
# Build hooks and worker
|
||||
npm run build
|
||||
|
||||
# Build only hooks
|
||||
npm run build:hooks
|
||||
|
||||
# Publish to NPM (maintainers only)
|
||||
npm run publish:npm
|
||||
```
|
||||
|
||||
## Viewing Stored Context
|
||||
|
||||
Context is stored in SQLite database at `~/.claude-mem/claude-mem.db`.
|
||||
|
||||
Query the database directly:
|
||||
|
||||
```bash
|
||||
# Open database
|
||||
sqlite3 ~/.claude-mem/claude-mem.db
|
||||
|
||||
# View recent sessions
|
||||
SELECT session_id, project, created_at, status
|
||||
FROM sdk_sessions
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 10;
|
||||
|
||||
# View session summaries
|
||||
SELECT session_id, request, completed, learned
|
||||
FROM session_summaries
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 5;
|
||||
|
||||
# View observations for a session
|
||||
SELECT tool_name, created_at
|
||||
FROM observations
|
||||
WHERE session_id = 'YOUR_SESSION_ID';
|
||||
```
|
||||
|
||||
## Understanding Verbosity Levels
|
||||
|
||||
Context injection uses three-tier verbosity for efficient token usage:
|
||||
|
||||
### Tier 1 (Most Recent Session)
|
||||
- Full summary with all details
|
||||
- Request, investigated, learned, completed, next_steps, notes
|
||||
- ~500-1000 tokens
|
||||
|
||||
### Tier 2 (Sessions 2-5)
|
||||
- Medium detail
|
||||
- Request, learned, completed
|
||||
- ~200-400 tokens
|
||||
|
||||
### Tier 3 (Sessions 6-10)
|
||||
- Brief summary
|
||||
- Request and completed only
|
||||
- ~100-200 tokens
|
||||
|
||||
This ensures you get maximum detail for recent work while still having context from older sessions.
|
||||
|
||||
## Multi-Prompt Sessions
|
||||
|
||||
Claude-Mem supports sessions that span multiple user prompts:
|
||||
|
||||
- **prompt_counter**: Tracks total prompts in a session
|
||||
- **prompt_number**: Identifies specific prompt within session
|
||||
- **Session continuity**: Observations and summaries link across prompts
|
||||
|
||||
When you use `/clear`, the session doesn't end - it continues with a new prompt number. This preserves context across conversation restarts.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [MCP Search Tools](/usage/search-tools) - Learn how to search your project history
|
||||
- [Architecture Overview](/architecture/overview) - Understand how it works
|
||||
- [Troubleshooting](/troubleshooting) - Common issues and solutions
|
||||
@@ -0,0 +1,426 @@
|
||||
---
|
||||
title: "MCP Search Tools"
|
||||
description: "Query your project history with 7 specialized search tools"
|
||||
---
|
||||
|
||||
# MCP Search Tools Usage
|
||||
|
||||
Once claude-mem is installed as a plugin, 7 search tools become available in your Claude Code sessions for querying project history.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Tool | Purpose |
|
||||
|-------------------------|----------------------------------------------|
|
||||
| search_observations | Full-text search across observations |
|
||||
| search_sessions | Full-text search across session summaries |
|
||||
| search_user_prompts | Full-text search across raw user prompts |
|
||||
| find_by_concept | Find observations tagged with concepts |
|
||||
| find_by_file | Find observations referencing files |
|
||||
| find_by_type | Find observations by type |
|
||||
| get_recent_context | Get recent session context |
|
||||
|
||||
## Example Queries
|
||||
|
||||
### search_observations
|
||||
|
||||
Find all decisions about the build system:
|
||||
```
|
||||
Use search_observations to find all decisions about the build system
|
||||
```
|
||||
|
||||
Find bugs related to authentication:
|
||||
```
|
||||
search_observations with query="authentication" and type="bugfix"
|
||||
```
|
||||
|
||||
Search for refactoring work:
|
||||
```
|
||||
search_observations with query="refactor database" and type="refactor"
|
||||
```
|
||||
|
||||
### search_sessions
|
||||
|
||||
Find what we learned about hooks:
|
||||
```
|
||||
Use search_sessions to find what we learned about hooks
|
||||
```
|
||||
|
||||
Search for completed work on the API:
|
||||
```
|
||||
search_sessions with query="API implementation"
|
||||
```
|
||||
|
||||
### search_user_prompts
|
||||
|
||||
Find when user asked about authentication:
|
||||
```
|
||||
search_user_prompts with query="authentication feature"
|
||||
```
|
||||
|
||||
Trace user requests for a specific feature:
|
||||
```
|
||||
search_user_prompts with query="dark mode"
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- See exactly what the user asked for (vs what was implemented)
|
||||
- Detect patterns in repeated requests
|
||||
- Debug miscommunications between user intent and implementation
|
||||
|
||||
### find_by_file
|
||||
|
||||
Show everything related to worker-service.ts:
|
||||
```
|
||||
Use find_by_file to show me everything related to worker-service.ts
|
||||
```
|
||||
|
||||
Find all work on the database migration file:
|
||||
```
|
||||
find_by_file with filePath="migrations.ts"
|
||||
```
|
||||
|
||||
### find_by_concept
|
||||
|
||||
Show observations tagged with 'architecture':
|
||||
```
|
||||
Use find_by_concept to show observations tagged with 'architecture'
|
||||
```
|
||||
|
||||
Find all 'security' related observations:
|
||||
```
|
||||
find_by_concept with concept="security"
|
||||
```
|
||||
|
||||
### find_by_type
|
||||
|
||||
Find all feature implementations:
|
||||
```
|
||||
find_by_type with type="feature"
|
||||
```
|
||||
|
||||
Find all decisions and discoveries:
|
||||
```
|
||||
find_by_type with type=["decision", "discovery"]
|
||||
```
|
||||
|
||||
### get_recent_context
|
||||
|
||||
Get the last 5 sessions for context:
|
||||
```
|
||||
get_recent_context with limit=5
|
||||
```
|
||||
|
||||
Get recent context for debugging:
|
||||
```
|
||||
Use get_recent_context to show me what we've been working on
|
||||
```
|
||||
|
||||
## Search Strategy
|
||||
|
||||
### 1. Start with Index Format
|
||||
|
||||
**Always use index format first** to get an overview:
|
||||
|
||||
```
|
||||
search_observations with query="authentication" and format="index"
|
||||
```
|
||||
|
||||
**Why?**
|
||||
- Index format uses ~10x fewer tokens than full format
|
||||
- See titles, dates, and sources to identify relevant results
|
||||
- Avoid hitting MCP token limits
|
||||
|
||||
### 2. Review Results
|
||||
|
||||
Look at the index results to identify items of interest:
|
||||
|
||||
```
|
||||
1. [decision] Implement JWT authentication
|
||||
Date: 2025-10-21 14:23:45
|
||||
Source: claude-mem://observation/123
|
||||
|
||||
2. [feature] Add user authentication endpoints
|
||||
Date: 2025-10-21 13:15:22
|
||||
Source: claude-mem://observation/124
|
||||
|
||||
3. [bugfix] Fix authentication token expiry
|
||||
Date: 2025-10-20 16:45:30
|
||||
Source: claude-mem://observation/125
|
||||
```
|
||||
|
||||
### 3. Deep Dive with Full Format
|
||||
|
||||
Only use full format for specific items:
|
||||
|
||||
```
|
||||
search_observations with query="JWT authentication" and format="full" and limit=3
|
||||
```
|
||||
|
||||
### 4. Use Filters to Narrow Results
|
||||
|
||||
Combine filters for precise searches:
|
||||
|
||||
```
|
||||
search_observations with query="authentication" and type="decision" and dateRange={start: "2025-10-20", end: "2025-10-21"}
|
||||
```
|
||||
|
||||
## Advanced Filtering
|
||||
|
||||
### Date Ranges
|
||||
|
||||
Search within specific time periods:
|
||||
|
||||
```json
|
||||
{
|
||||
"dateRange": {
|
||||
"start": "2025-10-01",
|
||||
"end": "2025-10-31"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or use epoch timestamps:
|
||||
|
||||
```json
|
||||
{
|
||||
"dateRange": {
|
||||
"start": 1729449600,
|
||||
"end": 1732128000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Types
|
||||
|
||||
Search across multiple observation types:
|
||||
|
||||
```
|
||||
find_by_type with type=["decision", "feature", "refactor"]
|
||||
```
|
||||
|
||||
### Multiple Concepts
|
||||
|
||||
Search observations with specific concepts:
|
||||
|
||||
```
|
||||
search_observations with query="database" and concepts=["architecture", "performance"]
|
||||
```
|
||||
|
||||
### File Filtering
|
||||
|
||||
Search observations that touched specific files:
|
||||
|
||||
```
|
||||
search_observations with query="refactor" and files="worker-service.ts"
|
||||
```
|
||||
|
||||
### Project Filtering
|
||||
|
||||
Search within specific projects:
|
||||
|
||||
```
|
||||
search_observations with query="authentication" and project="my-app"
|
||||
```
|
||||
|
||||
## FTS5 Query Syntax
|
||||
|
||||
The `query` parameter supports SQLite FTS5 full-text search syntax:
|
||||
|
||||
### Simple Queries
|
||||
```
|
||||
"authentication" # Single word
|
||||
"error handling" # Multiple words (OR)
|
||||
```
|
||||
|
||||
### Boolean Operators
|
||||
```
|
||||
"error" AND "handling" # Both terms required
|
||||
"bug" OR "fix" # Either term
|
||||
"bug" NOT "feature" # First term, not second
|
||||
```
|
||||
|
||||
### Phrase Searches
|
||||
```
|
||||
"'exact phrase'" # Exact phrase match
|
||||
```
|
||||
|
||||
### Column Searches
|
||||
```
|
||||
title:"authentication" # Search specific column
|
||||
narrative:"bug fix" # Search narrative field
|
||||
```
|
||||
|
||||
## Result Metadata
|
||||
|
||||
All results include rich metadata:
|
||||
|
||||
```
|
||||
## JWT authentication decision
|
||||
|
||||
**Type**: decision
|
||||
**Date**: 2025-10-21 14:23:45
|
||||
**Concepts**: authentication, security, architecture
|
||||
**Files Read**: src/auth/middleware.ts, src/utils/jwt.ts
|
||||
**Files Modified**: src/auth/jwt-strategy.ts
|
||||
|
||||
**Narrative**:
|
||||
Decided to implement JWT-based authentication instead of session-based
|
||||
authentication for better scalability and stateless design...
|
||||
|
||||
**Facts**:
|
||||
• JWT tokens expire after 1 hour
|
||||
• Refresh tokens stored in httpOnly cookies
|
||||
• Token signing uses RS256 algorithm
|
||||
• Public keys rotated every 30 days
|
||||
```
|
||||
|
||||
## Citations
|
||||
|
||||
All search results include citations using the `claude-mem://` URI scheme:
|
||||
|
||||
- `claude-mem://observation/123` - Specific observation
|
||||
- `claude-mem://session/abc-456` - Specific session
|
||||
- `claude-mem://user-prompt/789` - Specific user prompt
|
||||
|
||||
These citations enable referencing specific historical context in your work.
|
||||
|
||||
## Token Management
|
||||
|
||||
### Token Efficiency Tips
|
||||
|
||||
1. **Start with index format**: ~50-100 tokens per result
|
||||
2. **Use small limits**: Start with 3-5 results
|
||||
3. **Apply filters**: Narrow results before searching
|
||||
4. **Paginate**: Use offset to browse results in batches
|
||||
|
||||
### Token Estimates
|
||||
|
||||
| Format | Tokens per Result |
|
||||
|--------|-------------------|
|
||||
| Index | 50-100 |
|
||||
| Full | 500-1000 |
|
||||
|
||||
**Example**:
|
||||
- 20 results in index format: ~1,000-2,000 tokens
|
||||
- 20 results in full format: ~10,000-20,000 tokens
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### 1. Debugging Issues
|
||||
|
||||
Find what went wrong:
|
||||
```
|
||||
search_observations with query="error database connection" and type="bugfix"
|
||||
```
|
||||
|
||||
### 2. Understanding Decisions
|
||||
|
||||
Review architectural choices:
|
||||
```
|
||||
find_by_type with type="decision" and format="index"
|
||||
```
|
||||
|
||||
Then deep dive on specific decisions:
|
||||
```
|
||||
search_observations with query="[DECISION TITLE]" and format="full"
|
||||
```
|
||||
|
||||
### 3. Code Archaeology
|
||||
|
||||
Find when a file was modified:
|
||||
```
|
||||
find_by_file with filePath="worker-service.ts"
|
||||
```
|
||||
|
||||
### 4. Feature History
|
||||
|
||||
Track feature development:
|
||||
```
|
||||
search_sessions with query="authentication feature"
|
||||
search_user_prompts with query="add authentication"
|
||||
```
|
||||
|
||||
### 5. Learning from Past Work
|
||||
|
||||
Review refactoring patterns:
|
||||
```
|
||||
find_by_type with type="refactor" and limit=10
|
||||
```
|
||||
|
||||
### 6. Context Recovery
|
||||
|
||||
Restore context after time away:
|
||||
```
|
||||
get_recent_context with limit=5
|
||||
search_sessions with query="[YOUR PROJECT NAME]" and orderBy="date_desc"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Index first, full later**: Always start with index format
|
||||
2. **Small limits**: Start with 3-5 results to avoid token limits
|
||||
3. **Use filters**: Narrow results before searching
|
||||
4. **Specific queries**: More specific = better results
|
||||
5. **Review citations**: Use citations to reference past decisions
|
||||
6. **Date filtering**: Use date ranges for time-based searches
|
||||
7. **Type filtering**: Use types to categorize searches
|
||||
8. **Concept tags**: Use concepts for thematic searches
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No Results Found
|
||||
|
||||
1. Check database has data:
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM observations;"
|
||||
```
|
||||
|
||||
2. Try broader query:
|
||||
```
|
||||
search_observations with query="authentication" # Good
|
||||
vs
|
||||
search_observations with query="'exact JWT authentication implementation'" # Too specific
|
||||
```
|
||||
|
||||
3. Remove filters:
|
||||
```
|
||||
# Start broad
|
||||
search_observations with query="auth"
|
||||
|
||||
# Then add filters
|
||||
search_observations with query="auth" and type="decision"
|
||||
```
|
||||
|
||||
### Token Limit Errors
|
||||
|
||||
1. Use index format:
|
||||
```
|
||||
search_observations with query="..." and format="index"
|
||||
```
|
||||
|
||||
2. Reduce limit:
|
||||
```
|
||||
search_observations with query="..." and limit=3
|
||||
```
|
||||
|
||||
3. Use pagination:
|
||||
```
|
||||
# First page
|
||||
search_observations with query="..." and limit=5 and offset=0
|
||||
|
||||
# Second page
|
||||
search_observations with query="..." and limit=5 and offset=5
|
||||
```
|
||||
|
||||
### Search Too Slow
|
||||
|
||||
1. Use more specific queries
|
||||
2. Add date range filters
|
||||
3. Add type/concept filters
|
||||
4. Reduce result limit
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [MCP Search Architecture](/architecture/mcp-search) - Technical details
|
||||
- [Database Schema](/architecture/database) - Understanding the data
|
||||
- [Getting Started](/usage/getting-started) - Automatic operation
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* PM2 Ecosystem Configuration for claude-mem Worker Service
|
||||
*
|
||||
* Usage:
|
||||
* pm2 start ecosystem.config.cjs
|
||||
* pm2 stop claude-mem-worker
|
||||
* pm2 restart claude-mem-worker
|
||||
* pm2 logs claude-mem-worker
|
||||
* pm2 status
|
||||
*/
|
||||
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
// Determine log directory
|
||||
const logDir = path.join(os.homedir(), '.claude-mem', 'logs');
|
||||
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: 'claude-mem-worker',
|
||||
script: './plugin/scripts/worker-service.cjs',
|
||||
interpreter: 'node',
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: '500M',
|
||||
min_uptime: '10s',
|
||||
max_restarts: 10,
|
||||
restart_delay: 4000,
|
||||
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
CLAUDE_MEM_WORKER_PORT: 37777, // Fixed port for reliability
|
||||
FORCE_COLOR: '1'
|
||||
},
|
||||
|
||||
// Logging
|
||||
error_file: path.join(logDir, 'worker-error.log'),
|
||||
out_file: path.join(logDir, 'worker-out.log'),
|
||||
log_date_format: 'YYYY-MM-DD HH:mm:ss.SSS Z',
|
||||
merge_logs: true,
|
||||
|
||||
// Keep logs from last 7 days
|
||||
log_type: 'json',
|
||||
|
||||
// Process management
|
||||
kill_timeout: 5000,
|
||||
listen_timeout: 10000,
|
||||
shutdown_with_message: true,
|
||||
|
||||
// PM2 Plus (optional monitoring)
|
||||
// instance_var: 'INSTANCE_ID',
|
||||
// pmx: true
|
||||
}]
|
||||
};
|
||||
@@ -1,89 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Pre-Compact Hook for Claude Memory System
|
||||
*
|
||||
* Updated to use the centralized PromptOrchestrator and HookTemplates system.
|
||||
* This hook validates the pre-compact request and executes compression using
|
||||
* standardized response templates for consistent Claude Code integration.
|
||||
*/
|
||||
|
||||
import { loadCliCommand } from './shared/config-loader.js';
|
||||
import { getLogsDir } from './shared/path-resolver.js';
|
||||
import {
|
||||
createHookResponse,
|
||||
executeCliCommand,
|
||||
validateHookPayload,
|
||||
debugLog
|
||||
} from './shared/hook-helpers.js';
|
||||
|
||||
|
||||
// Set up stdin immediately
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.resume(); // Explicitly enter flowing mode to prevent data loss
|
||||
|
||||
|
||||
// Read input from stdin
|
||||
let input = '';
|
||||
process.stdin.on('data', chunk => {
|
||||
input += chunk;
|
||||
});
|
||||
|
||||
process.stdin.on('end', async () => {
|
||||
|
||||
try {
|
||||
// Load CLI command inside try-catch to handle config errors properly
|
||||
const cliCommand = loadCliCommand();
|
||||
|
||||
const payload = JSON.parse(input);
|
||||
debugLog('Pre-compact hook started', { payload });
|
||||
|
||||
// Validate payload using centralized validation
|
||||
const validation = validateHookPayload(payload, 'PreCompact');
|
||||
if (!validation.valid) {
|
||||
const response = createHookResponse('PreCompact', false, { reason: validation.error });
|
||||
debugLog('Validation failed', { response });
|
||||
// Exit silently - validation failure is expected flow control
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Check for environment-based blocking conditions
|
||||
if (payload.trigger === 'auto' && process.env.DISABLE_AUTO_COMPRESSION === 'true') {
|
||||
const response = createHookResponse('PreCompact', false, {
|
||||
reason: 'Auto-compression disabled by configuration'
|
||||
});
|
||||
debugLog('Auto-compression disabled', { response });
|
||||
// Exit silently - disabled compression is expected flow control
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Execute compression using standardized CLI execution helper
|
||||
debugLog('Executing compression command', {
|
||||
command: cliCommand,
|
||||
args: ['compress', payload.transcript_path]
|
||||
});
|
||||
|
||||
const result = await executeCliCommand(cliCommand, ['compress', payload.transcript_path]);
|
||||
|
||||
if (!result.success) {
|
||||
const response = createHookResponse('PreCompact', false, {
|
||||
reason: `Compression failed: ${result.stderr || 'Unknown error'}`
|
||||
});
|
||||
debugLog('Compression command failed', { stderr: result.stderr, response });
|
||||
console.log(`claude-mem error: compression failed, see logs at ${getLogsDir()}`);
|
||||
process.exit(1); // Exit with error code for actual compression failure
|
||||
}
|
||||
|
||||
// Success - exit silently (suppressOutput is true)
|
||||
debugLog('Compression completed successfully');
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
const response = createHookResponse('PreCompact', false, {
|
||||
reason: `Hook execution error: ${error.message}`
|
||||
});
|
||||
debugLog('Pre-compact hook error', { error: error.message, response });
|
||||
console.log(`claude-mem error: hook failed, see logs at ${getLogsDir()}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Session End Hook - Handles session end events including /clear
|
||||
*/
|
||||
|
||||
import { loadCliCommand } from './shared/config-loader.js';
|
||||
import { getSettingsPath, getArchivesDir } from './shared/path-resolver.js';
|
||||
import { execSync } from 'child_process';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
|
||||
const cliCommand = loadCliCommand();
|
||||
|
||||
// Check if save-on-clear is enabled
|
||||
function isSaveOnClearEnabled() {
|
||||
const settingsPath = getSettingsPath();
|
||||
if (existsSync(settingsPath)) {
|
||||
try {
|
||||
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
||||
return settings.saveMemoriesOnClear === true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set up stdin immediately before any async operations
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.resume(); // Explicitly enter flowing mode to prevent data loss
|
||||
|
||||
// Read input
|
||||
let input = '';
|
||||
process.stdin.on('data', chunk => {
|
||||
input += chunk;
|
||||
});
|
||||
|
||||
process.stdin.on('end', async () => {
|
||||
const data = JSON.parse(input);
|
||||
|
||||
// Check if this is a clear event and save-on-clear is enabled
|
||||
if (data.reason === 'clear' && isSaveOnClearEnabled()) {
|
||||
console.error('🧠 Saving memories before clearing context...');
|
||||
|
||||
try {
|
||||
// Use the CLI to compress current transcript
|
||||
execSync(`${cliCommand} compress --output ${getArchivesDir()}`, {
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, CLAUDE_MEM_SILENT: 'true' }
|
||||
});
|
||||
|
||||
console.error('✅ Memories saved successfully');
|
||||
} catch (error) {
|
||||
console.error('[session-end] Failed to save memories:', error.message);
|
||||
// Don't block the clear operation if memory saving fails
|
||||
}
|
||||
}
|
||||
|
||||
// Always continue
|
||||
console.log(JSON.stringify({ continue: true }));
|
||||
});
|
||||
@@ -1,170 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Session Start Hook - Load context when Claude Code starts
|
||||
*
|
||||
* Updated to use the centralized PromptOrchestrator and HookTemplates system.
|
||||
* This hook loads previous session context using standardized formatting and
|
||||
* provides rich context messages for Claude Code integration.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { loadCliCommand } from './shared/config-loader.js';
|
||||
import {
|
||||
createHookResponse,
|
||||
formatSessionStartContext,
|
||||
executeCliCommand,
|
||||
parseContextData,
|
||||
validateHookPayload,
|
||||
debugLog
|
||||
} from './shared/hook-helpers.js';
|
||||
|
||||
const cliCommand = loadCliCommand();
|
||||
|
||||
// Set up stdin immediately before any async operations
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.resume(); // Explicitly enter flowing mode to prevent data loss
|
||||
|
||||
// Read input from stdin
|
||||
let input = '';
|
||||
process.stdin.on('data', chunk => {
|
||||
input += chunk;
|
||||
});
|
||||
|
||||
process.stdin.on('end', async () => {
|
||||
try {
|
||||
const payload = JSON.parse(input);
|
||||
debugLog('Session start hook started', { payload });
|
||||
|
||||
// Validate payload using centralized validation
|
||||
const validation = validateHookPayload(payload, 'SessionStart');
|
||||
if (!validation.valid) {
|
||||
debugLog('Payload validation failed', { error: validation.error });
|
||||
// For session start, continue even with invalid payload but log the error
|
||||
const response = createHookResponse('SessionStart', false, {
|
||||
error: `Payload validation failed: ${validation.error}`
|
||||
});
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Skip load-context when source is "resume" to avoid duplicate context
|
||||
if (payload.source === 'resume') {
|
||||
debugLog('Skipping load-context for resume source');
|
||||
// Output valid JSON response with suppressOutput for resume
|
||||
const response = createHookResponse('SessionStart', true);
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Extract project name from current working directory
|
||||
const projectName = path.basename(process.cwd());
|
||||
|
||||
// Load context using standardized CLI execution helper
|
||||
const contextResult = await executeCliCommand(cliCommand, [
|
||||
'load-context',
|
||||
'--format', 'session-start',
|
||||
'--project', projectName
|
||||
]);
|
||||
|
||||
if (!contextResult.success) {
|
||||
debugLog('Context loading failed', { stderr: contextResult.stderr });
|
||||
// Don't fail the session start, just provide error context
|
||||
const response = createHookResponse('SessionStart', false, {
|
||||
error: contextResult.stderr || 'Failed to load context'
|
||||
});
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const rawContext = contextResult.stdout;
|
||||
debugLog('Raw context loaded', { contextLength: rawContext.length });
|
||||
|
||||
// Check if the output is actually an error message (starts with ❌)
|
||||
if (rawContext && rawContext.trim().startsWith('❌')) {
|
||||
debugLog('Detected error message in stdout', { rawContext });
|
||||
// Extract the clean error message without the emoji and format
|
||||
const errorMatch = rawContext.match(/❌\s*[^:]+:\s*([^\n]+)(?:\n\n💡\s*(.+))?/);
|
||||
let errorMsg = 'No previous memories found';
|
||||
let suggestion = '';
|
||||
|
||||
if (errorMatch) {
|
||||
errorMsg = errorMatch[1] || errorMsg;
|
||||
suggestion = errorMatch[2] || '';
|
||||
}
|
||||
|
||||
// Create a clean response without duplicating the error formatting
|
||||
const response = createHookResponse('SessionStart', false, {
|
||||
error: errorMsg + (suggestion ? `. ${suggestion}` : '')
|
||||
});
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!rawContext || !rawContext.trim()) {
|
||||
debugLog('No context available, creating empty response');
|
||||
// No context available - use standardized empty response
|
||||
const response = createHookResponse('SessionStart', true);
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Parse context data and format using centralized templates
|
||||
const contextData = parseContextData(rawContext);
|
||||
contextData.projectName = projectName;
|
||||
|
||||
// If we have raw context (not structured data), use it directly
|
||||
let formattedContext;
|
||||
if (contextData.rawContext) {
|
||||
formattedContext = contextData.rawContext;
|
||||
} else {
|
||||
// Use standardized formatting for structured context
|
||||
formattedContext = formatSessionStartContext(contextData);
|
||||
}
|
||||
|
||||
debugLog('Context formatted successfully', {
|
||||
memoryCount: contextData.memoryCount,
|
||||
hasStructuredData: !contextData.rawContext
|
||||
});
|
||||
|
||||
// Create standardized session start response using HookTemplates
|
||||
const response = createHookResponse('SessionStart', true, {
|
||||
context: formattedContext
|
||||
});
|
||||
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
debugLog('Session start hook error', { error: error.message });
|
||||
// Even on error, continue the session with error information
|
||||
const response = createHookResponse('SessionStart', false, {
|
||||
error: `Hook execution error: ${error.message}`
|
||||
});
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Extracts project name from transcript path
|
||||
* @param {string} transcriptPath - Path to transcript file
|
||||
* @returns {string|null} Extracted project name or null
|
||||
*/
|
||||
function extractProjectName(transcriptPath) {
|
||||
if (!transcriptPath) return null;
|
||||
|
||||
// Look for project pattern: /path/to/PROJECT_NAME/.claude/
|
||||
// Need to get PROJECT_NAME, not the parent directory
|
||||
const parts = transcriptPath.split(path.sep);
|
||||
const claudeIndex = parts.indexOf('.claude');
|
||||
|
||||
if (claudeIndex > 0) {
|
||||
// Get the directory immediately before .claude
|
||||
return parts[claudeIndex - 1];
|
||||
}
|
||||
|
||||
// Fall back to directory containing the transcript
|
||||
const dir = path.dirname(transcriptPath);
|
||||
return path.basename(dir);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Shared configuration loader utility for Claude Memory hooks
|
||||
* Loads CLI command name from config.json with proper fallback handling
|
||||
*/
|
||||
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
|
||||
/**
|
||||
* Loads the CLI command name from the hooks config.json file
|
||||
* @returns {string} The CLI command name (defaults to 'claude-mem')
|
||||
*/
|
||||
export function loadCliCommand() {
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const configPath = join(__dirname, '..', 'config.json');
|
||||
|
||||
if (existsSync(configPath)) {
|
||||
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
||||
return config.cliCommand || 'claude-mem';
|
||||
}
|
||||
|
||||
return 'claude-mem';
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Hook Helper Functions
|
||||
*
|
||||
* This module provides JavaScript wrappers around the TypeScript PromptOrchestrator
|
||||
* and HookTemplates system, making them accessible to the JavaScript hook scripts.
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
/**
|
||||
* Creates a standardized hook response using the HookTemplates system
|
||||
* @param {string} hookType - Type of hook ('PreCompact' or 'SessionStart')
|
||||
* @param {boolean} success - Whether the operation was successful
|
||||
* @param {Object} options - Additional options
|
||||
* @returns {Object} Formatted hook response
|
||||
*/
|
||||
export function createHookResponse(hookType, success, options = {}) {
|
||||
if (hookType === 'PreCompact') {
|
||||
if (success) {
|
||||
return {
|
||||
continue: true,
|
||||
suppressOutput: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
continue: false,
|
||||
stopReason: options.reason || 'Pre-compact operation failed',
|
||||
suppressOutput: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (hookType === 'SessionStart') {
|
||||
if (success && options.context) {
|
||||
return {
|
||||
continue: true,
|
||||
suppressOutput: true,
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'SessionStart',
|
||||
additionalContext: options.context
|
||||
}
|
||||
};
|
||||
} else if (success) {
|
||||
// No context - just suppress output without any message
|
||||
return {
|
||||
continue: true,
|
||||
suppressOutput: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
continue: true, // Continue even on context loading failure
|
||||
suppressOutput: true,
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'SessionStart',
|
||||
additionalContext: `Context loading encountered an issue: ${options.error || 'Unknown error'}. Starting without previous context.`
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Generic response for unknown hook types
|
||||
return {
|
||||
continue: success,
|
||||
suppressOutput: true,
|
||||
...(options.reason && !success ? { stopReason: options.reason } : {})
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a session start context message using standardized templates
|
||||
* @param {Object} contextData - Context information
|
||||
* @returns {string} Formatted context message
|
||||
*/
|
||||
export function formatSessionStartContext(contextData) {
|
||||
const {
|
||||
projectName = 'unknown project',
|
||||
memoryCount = 0,
|
||||
lastSessionTime,
|
||||
recentComponents = [],
|
||||
recentDecisions = []
|
||||
} = contextData;
|
||||
|
||||
const timeInfo = lastSessionTime ? ` (last worked: ${lastSessionTime})` : '';
|
||||
const contextParts = [];
|
||||
|
||||
contextParts.push(`🧠 Loaded ${memoryCount} memories from previous sessions for ${projectName}${timeInfo}`);
|
||||
|
||||
if (recentComponents.length > 0) {
|
||||
contextParts.push(`\n🎯 Recent components: ${recentComponents.slice(0, 3).join(', ')}`);
|
||||
}
|
||||
|
||||
if (recentDecisions.length > 0) {
|
||||
contextParts.push(`\n🔄 Recent decisions: ${recentDecisions.slice(0, 2).join(', ')}`);
|
||||
}
|
||||
|
||||
if (memoryCount > 0) {
|
||||
contextParts.push('\n💡 Use search_nodes("keywords") to find related work or open_nodes(["entity_name"]) to load specific components');
|
||||
}
|
||||
|
||||
return contextParts.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a CLI command and returns the result
|
||||
* @param {string} command - CLI command to execute
|
||||
* @param {Array} args - Command arguments
|
||||
* @param {Object} options - Spawn options
|
||||
* @returns {Promise<{stdout: string, stderr: string, success: boolean}>}
|
||||
*/
|
||||
export async function executeCliCommand(command, args = [], options = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const process = spawn(command, args, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
...options
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
if (process.stdout) {
|
||||
process.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
}
|
||||
|
||||
if (process.stderr) {
|
||||
process.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
}
|
||||
|
||||
process.on('close', (code) => {
|
||||
resolve({
|
||||
stdout: stdout.trim(),
|
||||
stderr: stderr.trim(),
|
||||
success: code === 0
|
||||
});
|
||||
});
|
||||
|
||||
process.on('error', (error) => {
|
||||
resolve({
|
||||
stdout: '',
|
||||
stderr: error.message,
|
||||
success: false
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses context data from CLI output
|
||||
* @param {string} output - Raw CLI output
|
||||
* @returns {Object} Parsed context data
|
||||
*/
|
||||
export function parseContextData(output) {
|
||||
if (!output || !output.trim()) {
|
||||
return {
|
||||
memoryCount: 0,
|
||||
recentComponents: [],
|
||||
recentDecisions: []
|
||||
};
|
||||
}
|
||||
|
||||
// Try to parse as JSON first (if CLI outputs structured data)
|
||||
try {
|
||||
const parsed = JSON.parse(output);
|
||||
return {
|
||||
memoryCount: parsed.memoryCount || 0,
|
||||
recentComponents: parsed.recentComponents || [],
|
||||
recentDecisions: parsed.recentDecisions || [],
|
||||
lastSessionTime: parsed.lastSessionTime
|
||||
};
|
||||
} catch (e) {
|
||||
// If not JSON, treat as plain text context
|
||||
const lines = output.split('\n').filter(line => line.trim());
|
||||
return {
|
||||
memoryCount: lines.length,
|
||||
recentComponents: [],
|
||||
recentDecisions: [],
|
||||
rawContext: output
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates hook payload structure
|
||||
* @param {Object} payload - Hook payload to validate
|
||||
* @param {string} expectedHookType - Expected hook event name
|
||||
* @returns {{valid: boolean, error?: string}} Validation result
|
||||
*/
|
||||
export function validateHookPayload(payload, expectedHookType) {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return { valid: false, error: 'Payload must be a valid object' };
|
||||
}
|
||||
|
||||
if (!payload.session_id || typeof payload.session_id !== 'string') {
|
||||
return { valid: false, error: 'Missing or invalid session_id' };
|
||||
}
|
||||
|
||||
if (!payload.transcript_path || typeof payload.transcript_path !== 'string') {
|
||||
return { valid: false, error: 'Missing or invalid transcript_path' };
|
||||
}
|
||||
|
||||
if (expectedHookType && payload.hook_event_name !== expectedHookType) {
|
||||
return { valid: false, error: `Expected hook_event_name to be ${expectedHookType}` };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs debug information if debug mode is enabled
|
||||
* @param {string} message - Debug message
|
||||
* @param {Object} data - Additional data to log
|
||||
*/
|
||||
export function debugLog(message, data = {}) {
|
||||
if (process.env.DEBUG === 'true' || process.env.CLAUDE_MEM_DEBUG === 'true') {
|
||||
const timestamp = new Date().toISOString();
|
||||
console.error(`[${timestamp}] HOOK DEBUG: ${message}`, data);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Path resolver utility for Claude Memory hooks
|
||||
* Provides proper path handling using environment variables
|
||||
*/
|
||||
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
|
||||
/**
|
||||
* Gets the base data directory for claude-mem
|
||||
* @returns {string} Data directory path
|
||||
*/
|
||||
export function getDataDir() {
|
||||
return process.env.CLAUDE_MEM_DATA_DIR || join(homedir(), '.claude-mem');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the settings file path
|
||||
* @returns {string} Settings file path
|
||||
*/
|
||||
export function getSettingsPath() {
|
||||
return join(getDataDir(), 'settings.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the archives directory path
|
||||
* @returns {string} Archives directory path
|
||||
*/
|
||||
export function getArchivesDir() {
|
||||
return process.env.CLAUDE_MEM_ARCHIVES_DIR || join(getDataDir(), 'archives');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the logs directory path
|
||||
* @returns {string} Logs directory path
|
||||
*/
|
||||
export function getLogsDir() {
|
||||
return process.env.CLAUDE_MEM_LOGS_DIR || join(getDataDir(), 'logs');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the compact flag file path
|
||||
* @returns {string} Compact flag file path
|
||||
*/
|
||||
export function getCompactFlagPath() {
|
||||
return join(getDataDir(), '.compact-running');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all common paths used by hooks
|
||||
* @returns {Object} Object containing all common paths
|
||||
*/
|
||||
export function getPaths() {
|
||||
return {
|
||||
dataDir: getDataDir(),
|
||||
settingsPath: getSettingsPath(),
|
||||
archivesDir: getArchivesDir(),
|
||||
logsDir: getLogsDir(),
|
||||
compactFlagPath: getCompactFlagPath()
|
||||
};
|
||||
}
|
||||
Generated
+5259
File diff suppressed because it is too large
Load Diff
+40
-20
@@ -1,21 +1,22 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "3.6.4",
|
||||
"version": "4.2.4",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
"claude-code",
|
||||
"claude-agent-sdk",
|
||||
"mcp",
|
||||
"plugin",
|
||||
"memory",
|
||||
"compression",
|
||||
"knowledge-graph",
|
||||
"transcript",
|
||||
"cli",
|
||||
"typescript",
|
||||
"bun"
|
||||
"nodejs"
|
||||
],
|
||||
"author": "Alex Newman",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"license": "AGPL-3.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thedotmack/claude-mem.git"
|
||||
@@ -32,28 +33,47 @@
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"claude-mem": "./dist/claude-mem.min.js"
|
||||
"scripts": {
|
||||
"build": "node scripts/build-hooks.js",
|
||||
"build:hooks": "node scripts/build-hooks.js",
|
||||
"release": "node scripts/publish.js",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "node --test tests/",
|
||||
"test:context": "echo '{\"session_id\":\"test-'$(date +%s)'\",\"cwd\":\"'$(pwd)'\",\"source\":\"startup\"}' | node plugin/scripts/context-hook.js 2>/dev/null",
|
||||
"test:context:verbose": "echo '{\"session_id\":\"test-'$(date +%s)'\",\"cwd\":\"'$(pwd)'\",\"source\":\"startup\"}' | node plugin/scripts/context-hook.js",
|
||||
"import:xml": "tsx src/bin/import-xml-observations.ts",
|
||||
"cleanup:duplicates": "tsx src/bin/cleanup-duplicates.ts",
|
||||
"worker:start": "pm2 start ecosystem.config.cjs",
|
||||
"worker:stop": "pm2 stop claude-mem-worker",
|
||||
"worker:restart": "pm2 restart claude-mem-worker",
|
||||
"worker:logs": "pm2 logs claude-mem-worker",
|
||||
"worker:status": "pm2 status claude-mem-worker"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-code": "^1.0.88",
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@modelcontextprotocol/sdk": "^0.5.0",
|
||||
"boxen": "^8.0.1",
|
||||
"chalk": "^5.6.0",
|
||||
"chromadb": "^3.0.14",
|
||||
"commander": "^14.0.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.23",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"express": "^4.18.2",
|
||||
"glob": "^11.0.3",
|
||||
"gradient-string": "^3.0.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"oh-my-logo": "^0.3.2"
|
||||
"pm2": "^5.3.0",
|
||||
"zod-to-json-schema": "^3.24.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.8",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.0.0",
|
||||
"esbuild": "^0.20.0",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"hooks",
|
||||
"commands",
|
||||
"plugin",
|
||||
"src",
|
||||
".mcp.json",
|
||||
"CHANGELOG.md"
|
||||
"scripts",
|
||||
"docs",
|
||||
"ecosystem.config.cjs",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "4.2.4",
|
||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
},
|
||||
"repository": "https://github.com/thedotmack/claude-mem",
|
||||
"license": "AGPL-3.0",
|
||||
"keywords": [
|
||||
"memory",
|
||||
"context",
|
||||
"persistence",
|
||||
"hooks",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"claude-mem-search": {
|
||||
"type": "stdio",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/search-server.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"description": "Claude-mem memory system hooks",
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "cd \"${CLAUDE_PLUGIN_ROOT}/..\" && npm install --prefer-offline --no-audit --no-fund --loglevel=error && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||
"timeout": 120
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js",
|
||||
"timeout": 120
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js",
|
||||
"timeout": 120
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js",
|
||||
"timeout": 120
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionEnd": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/cleanup-hook.js",
|
||||
"timeout": 120
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/bin/bash
|
||||
# claude-mem-settings.sh - User settings manager for claude-mem plugin
|
||||
|
||||
USER_SETTINGS_FILE="$HOME/.claude/settings.json"
|
||||
|
||||
# Function to check if jq is available
|
||||
check_jq() {
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "Error: jq is required for JSON manipulation"
|
||||
echo "Install with: brew install jq"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to create settings file if it doesn't exist
|
||||
ensure_settings_file() {
|
||||
if [ ! -f "$USER_SETTINGS_FILE" ]; then
|
||||
mkdir -p "$(dirname "$USER_SETTINGS_FILE")"
|
||||
echo '{}' > "$USER_SETTINGS_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to get current model setting
|
||||
get_model() {
|
||||
if [ -f "$USER_SETTINGS_FILE" ]; then
|
||||
jq -r '.env.CLAUDE_MEM_MODEL // "claude-sonnet-4-5"' "$USER_SETTINGS_FILE"
|
||||
else
|
||||
echo "claude-sonnet-4-5"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to set model setting
|
||||
set_model() {
|
||||
local model=$1
|
||||
|
||||
ensure_settings_file
|
||||
|
||||
# Update or create the env.CLAUDE_MEM_MODEL setting
|
||||
jq --arg model "$model" '.env.CLAUDE_MEM_MODEL = $model' "$USER_SETTINGS_FILE" > tmp.json && mv tmp.json "$USER_SETTINGS_FILE"
|
||||
echo "Set CLAUDE_MEM_MODEL to: $model"
|
||||
}
|
||||
|
||||
# Function to remove model setting
|
||||
remove_model() {
|
||||
if [ -f "$USER_SETTINGS_FILE" ]; then
|
||||
jq 'del(.env.CLAUDE_MEM_MODEL)' "$USER_SETTINGS_FILE" > tmp.json && mv tmp.json "$USER_SETTINGS_FILE"
|
||||
echo "Removed CLAUDE_MEM_MODEL (will use default: claude-sonnet-4-5)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to list available models
|
||||
list_models() {
|
||||
echo "Available models:"
|
||||
echo " claude-haiku-4-5 - Fast and efficient"
|
||||
echo " claude-sonnet-4-5 - Balanced (default)"
|
||||
echo " claude-opus-4 - Most capable"
|
||||
echo " claude-3-7-sonnet - Alternative version"
|
||||
}
|
||||
|
||||
# Interactive menu
|
||||
show_menu() {
|
||||
echo "Claude Mem Plugin - Model Configuration"
|
||||
echo "======================================"
|
||||
echo "Current model: $(get_model)"
|
||||
echo "Settings file: $USER_SETTINGS_FILE"
|
||||
echo ""
|
||||
echo "1) Set model"
|
||||
echo "2) Remove model setting (use default)"
|
||||
echo "3) List available models"
|
||||
echo "4) Exit"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Main interactive loop
|
||||
main() {
|
||||
check_jq
|
||||
|
||||
while true; do
|
||||
show_menu
|
||||
read -p "Choose an option (1-4): " choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
list_models
|
||||
echo ""
|
||||
read -p "Enter model name: " model
|
||||
set_model "$model"
|
||||
;;
|
||||
2)
|
||||
remove_model
|
||||
;;
|
||||
3)
|
||||
list_models
|
||||
;;
|
||||
4)
|
||||
echo "Goodbye!"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Invalid option. Please choose 1-4."
|
||||
;;
|
||||
esac
|
||||
echo ""
|
||||
read -p "Press Enter to continue..."
|
||||
done
|
||||
}
|
||||
|
||||
# Run main if script is executed directly
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
Executable
+310
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/env node
|
||||
import P from"better-sqlite3";import{join as c,dirname as U,basename as V}from"path";import{homedir as f}from"os";import{existsSync as z,mkdirSync as w}from"fs";import{fileURLToPath as X}from"url";function M(){return typeof __dirname<"u"?__dirname:U(X(import.meta.url))}var F=M(),p=process.env.CLAUDE_MEM_DATA_DIR||c(f(),".claude-mem"),u=process.env.CLAUDE_CONFIG_DIR||c(f(),".claude"),ee=c(p,"archives"),se=c(p,"logs"),te=c(p,"trash"),re=c(p,"backups"),ne=c(p,"settings.json"),I=c(p,"claude-mem.db"),oe=c(u,"settings.json"),ie=c(u,"commands"),ae=c(u,"CLAUDE.md");function O(o){w(o,{recursive:!0})}function L(){return c(F,"..","..")}var l=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(l||{}),T=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=l[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=l[e].padEnd(5),d=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let _="";n!=null&&(this.level===0&&typeof n=="object"?_=`
|
||||
`+JSON.stringify(n,null,2):_=" "+this.formatData(n));let b="";if(r){let{sessionId:B,sdkSessionId:j,correlationId:$,...h}=r;Object.keys(h).length>0&&(b=` {${Object.entries(h).map(([y,x])=>`${y}=${x}`).join(", ")}}`)}let R=`[${i}] [${a}] [${d}] ${E}${t}${b}${_}`;e===3?console.error(R):console.log(R)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new T;var m=class{db;constructor(){O(p),this.db=new P(I),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
applied_at TEXT NOT NULL
|
||||
)
|
||||
`);let e=this.db.prepare("SELECT version FROM schema_versions ORDER BY version").all();(e.length>0?Math.max(...e.map(t=>t.version)):0)===0&&(console.error("[SessionStore] Initializing fresh database with migration004..."),this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS sdk_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT UNIQUE NOT NULL,
|
||||
sdk_session_id TEXT UNIQUE,
|
||||
project TEXT NOT NULL,
|
||||
user_prompt TEXT,
|
||||
started_at TEXT NOT NULL,
|
||||
started_at_epoch INTEGER NOT NULL,
|
||||
completed_at TEXT,
|
||||
completed_at_epoch INTEGER,
|
||||
status TEXT CHECK(status IN ('active', 'completed', 'failed')) NOT NULL DEFAULT 'active'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_claude_id ON sdk_sessions(claude_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_sdk_id ON sdk_sessions(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_project ON sdk_sessions(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_status ON sdk_sessions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_started ON sdk_sessions(started_at_epoch DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS observations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery')),
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_sdk_session ON observations(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS session_summaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT UNIQUE NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
request TEXT,
|
||||
investigated TEXT,
|
||||
learned TEXT,
|
||||
completed TEXT,
|
||||
next_steps TEXT,
|
||||
files_read TEXT,
|
||||
files_edited TEXT,
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(d=>d.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE session_summaries_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
request TEXT,
|
||||
investigated TEXT,
|
||||
learned TEXT,
|
||||
completed TEXT,
|
||||
next_steps TEXT,
|
||||
files_read TEXT,
|
||||
files_edited TEXT,
|
||||
notes TEXT,
|
||||
prompt_number INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
)
|
||||
`),this.db.exec(`
|
||||
INSERT INTO session_summaries_new
|
||||
SELECT id, sdk_session_id, project, request, investigated, learned,
|
||||
completed, next_steps, files_read, files_edited, notes,
|
||||
prompt_number, created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
`),this.db.exec("DROP TABLE session_summaries"),this.db.exec("ALTER TABLE session_summaries_new RENAME TO session_summaries"),this.db.exec(`
|
||||
CREATE INDEX idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString()),console.error("[SessionStore] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (remove UNIQUE constraint):",e.message)}}addObservationHierarchicalFields(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(8))return;if(this.db.pragma("table_info(observations)").some(r=>r.name==="title")){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(8,new Date().toISOString());return}console.error("[SessionStore] Adding hierarchical fields to observations table..."),this.db.exec(`
|
||||
ALTER TABLE observations ADD COLUMN title TEXT;
|
||||
ALTER TABLE observations ADD COLUMN subtitle TEXT;
|
||||
ALTER TABLE observations ADD COLUMN facts TEXT;
|
||||
ALTER TABLE observations ADD COLUMN narrative TEXT;
|
||||
ALTER TABLE observations ADD COLUMN concepts TEXT;
|
||||
ALTER TABLE observations ADD COLUMN files_read TEXT;
|
||||
ALTER TABLE observations ADD COLUMN files_modified TEXT;
|
||||
`),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(8,new Date().toISOString()),console.error("[SessionStore] Successfully added hierarchical fields to observations table")}catch(e){console.error("[SessionStore] Migration error (add hierarchical fields):",e.message)}}makeObservationsTextNullable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(9))return;let t=this.db.pragma("table_info(observations)").find(r=>r.name==="text");if(!t||t.notnull===0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString());return}console.error("[SessionStore] Making observations.text nullable..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE observations_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
text TEXT,
|
||||
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
|
||||
title TEXT,
|
||||
subtitle TEXT,
|
||||
facts TEXT,
|
||||
narrative TEXT,
|
||||
concepts TEXT,
|
||||
files_read TEXT,
|
||||
files_modified TEXT,
|
||||
prompt_number INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
)
|
||||
`),this.db.exec(`
|
||||
INSERT INTO observations_new
|
||||
SELECT id, sdk_session_id, project, text, type, title, subtitle, facts,
|
||||
narrative, concepts, files_read, files_modified, prompt_number,
|
||||
created_at, created_at_epoch
|
||||
FROM observations
|
||||
`),this.db.exec("DROP TABLE observations"),this.db.exec("ALTER TABLE observations_new RENAME TO observations"),this.db.exec(`
|
||||
CREATE INDEX idx_observations_sdk_session ON observations(sdk_session_id);
|
||||
CREATE INDEX idx_observations_project ON observations(project);
|
||||
CREATE INDEX idx_observations_type ON observations(type);
|
||||
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString()),console.error("[SessionStore] Successfully made observations.text nullable")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (make text nullable):",e.message)}}createUserPromptsTable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(10))return;if(this.db.pragma("table_info(user_prompts)").length>0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString());return}console.error("[SessionStore] Creating user_prompts table with FTS5 support..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE user_prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT NOT NULL,
|
||||
prompt_number INTEGER NOT NULL,
|
||||
prompt_text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(claude_session_id) REFERENCES sdk_sessions(claude_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_prompts_claude_session ON user_prompts(claude_session_id);
|
||||
CREATE INDEX idx_user_prompts_created ON user_prompts(created_at_epoch DESC);
|
||||
CREATE INDEX idx_user_prompts_prompt_number ON user_prompts(prompt_number);
|
||||
`),this.db.exec(`
|
||||
CREATE VIRTUAL TABLE user_prompts_fts USING fts5(
|
||||
prompt_text,
|
||||
content='user_prompts',
|
||||
content_rowid='id'
|
||||
);
|
||||
`),this.db.exec(`
|
||||
CREATE TRIGGER user_prompts_ai AFTER INSERT ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_ad AFTER DELETE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_au AFTER UPDATE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentSummariesWithSessionInfo(e,s=3){return this.db.prepare(`
|
||||
SELECT
|
||||
sdk_session_id, request, learned, completed, next_steps,
|
||||
prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentObservations(e,s=20){return this.db.prepare(`
|
||||
SELECT type, text, prompt_number, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
s.sdk_session_id,
|
||||
s.status,
|
||||
s.started_at,
|
||||
s.started_at_epoch,
|
||||
s.user_prompt,
|
||||
CASE WHEN sum.sdk_session_id IS NOT NULL THEN 1 ELSE 0 END as has_summary
|
||||
FROM sdk_sessions s
|
||||
LEFT JOIN session_summaries sum ON s.sdk_session_id = sum.sdk_session_id
|
||||
WHERE s.project = ? AND s.sdk_session_id IS NOT NULL
|
||||
GROUP BY s.sdk_session_id
|
||||
ORDER BY s.started_at_epoch DESC
|
||||
LIMIT ?
|
||||
)
|
||||
ORDER BY started_at_epoch ASC
|
||||
`).all(e,s)}getObservationsForSession(e){return this.db.prepare(`
|
||||
SELECT title, subtitle, type, prompt_number
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
ORDER BY created_at_epoch ASC
|
||||
`).all(e)}getSummaryForSession(e){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE sdk_session_id = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 1
|
||||
`).get(e)||null}getFilesForSession(e){let t=this.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),r=new Set,n=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(d=>n.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)||null}findActiveSDKSession(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ? AND status = 'active'
|
||||
LIMIT 1
|
||||
`).get(e)||null}findAnySDKSession(e){return this.db.prepare(`
|
||||
SELECT id
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ?
|
||||
LIMIT 1
|
||||
`).get(e)||null}reactivateSession(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'active', user_prompt = ?, worker_port = NULL
|
||||
WHERE id = ?
|
||||
`).run(s,e)}incrementPromptCounter(e){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET prompt_counter = COALESCE(prompt_counter, 0) + 1
|
||||
WHERE id = ?
|
||||
`).run(e),this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime(),a=this.db.prepare(`
|
||||
INSERT OR IGNORE INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,t,r.toISOString(),n);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||
`).get(e).id:a.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(s,e).changes===0?(A.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
`).run(s,e)}getWorkerPort(e){return this.db.prepare(`
|
||||
SELECT worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,n=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO user_prompts
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r){let n=new Date,i=n.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,n.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n.toISOString(),i)}storeSummary(e,s,t,r){let n=new Date,i=n.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,n.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n.toISOString(),i)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(s.toISOString(),t,e)}markSessionFailed(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(s.toISOString(),t,e)}cleanupOrphanedSessions(){let e=new Date,s=e.getTime();return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};import S from"path";import{existsSync as g}from"fs";import{spawn as G}from"child_process";var H=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),W=`http://127.0.0.1:${H}/health`;async function v(){try{return(await fetch(W,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function C(){try{if(await v())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=L(),e=S.join(o,"plugin","scripts","worker-service.cjs");if(!g(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=S.join(o,"ecosystem.config.cjs"),t=S.join(o,"node_modules",".bin","pm2");if(!g(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!g(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=G(t,["start",s],{detached:!0,stdio:"ignore",cwd:o});r.on("error",n=>{throw new Error(`Failed to spawn PM2: ${n.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let n=0;n<3;n++)if(await new Promise(i=>setTimeout(i,500)),await v())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}async function k(o){try{console.error("[claude-mem cleanup] Hook fired",{input:o?{session_id:o.session_id,cwd:o.cwd,reason:o.reason}:null}),o||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
|
||||
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=o;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s}),await C()||console.error("[claude-mem cleanup] Worker not available - skipping HTTP cleanup");let r=new m,n=r.findActiveSDKSession(e);n||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),r.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:n.id,sdk_session_id:n.sdk_session_id,project:n.project,worker_port:n.worker_port});try{r.markSessionCompleted(n.id),console.error("[claude-mem cleanup] Session marked as completed in database")}catch(i){console.error("[claude-mem cleanup] Failed to mark session as completed:",i)}r.close(),console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}catch(e){console.error("[claude-mem cleanup] Unexpected error in hook",{error:e.message,stack:e.stack,name:e.name}),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}}import{stdin as D}from"process";var N="";D.on("data",o=>N+=o);D.on("end",async()=>{try{let o=N.trim()?JSON.parse(N):void 0;await k(o)}catch(o){console.error(`[claude-mem cleanup-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
Executable
+330
@@ -0,0 +1,330 @@
|
||||
#!/usr/bin/env node
|
||||
import C from"path";import q from"better-sqlite3";import{join as E,dirname as W,basename as z}from"path";import{homedir as U}from"os";import{existsSync as te,mkdirSync as j}from"fs";import{fileURLToPath as H}from"url";function B(){return typeof __dirname<"u"?__dirname:W(H(import.meta.url))}var Y=B(),u=process.env.CLAUDE_MEM_DATA_DIR||E(U(),".claude-mem"),O=process.env.CLAUDE_CONFIG_DIR||E(U(),".claude"),ne=E(u,"archives"),ie=E(u,"logs"),oe=E(u,"trash"),ae=E(u,"backups"),de=E(u,"settings.json"),w=E(u,"claude-mem.db"),pe=E(O,"settings.json"),ce=E(O,"commands"),Ee=E(O,"CLAUDE.md");function $(p){j(p,{recursive:!0})}function M(){return E(Y,"..","..")}var L=(i=>(i[i.DEBUG=0]="DEBUG",i[i.INFO=1]="INFO",i[i.WARN=2]="WARN",i[i.ERROR=3]="ERROR",i[i.SILENT=4]="SILENT",i))(L||{}),A=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=L[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,i){if(e<this.level)return;let d=new Date().toISOString().replace("T"," ").substring(0,23),n=L[e].padEnd(5),c=s.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let a="";i!=null&&(this.level===0&&typeof i=="object"?a=`
|
||||
`+JSON.stringify(i,null,2):a=" "+this.formatData(i));let l="";if(r){let{sessionId:G,sdkSessionId:k,correlationId:R,...T}=r;Object.keys(T).length>0&&(l=` {${Object.entries(T).map(([g,b])=>`${g}=${b}`).join(", ")}}`)}let f=`[${d}] [${n}] [${c}] ${_}${t}${l}${a}`;e===3?console.error(f):console.log(f)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},X=new A;var N=class{db;constructor(){$(u),this.db=new q(w),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
applied_at TEXT NOT NULL
|
||||
)
|
||||
`);let e=this.db.prepare("SELECT version FROM schema_versions ORDER BY version").all();(e.length>0?Math.max(...e.map(t=>t.version)):0)===0&&(console.error("[SessionStore] Initializing fresh database with migration004..."),this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS sdk_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT UNIQUE NOT NULL,
|
||||
sdk_session_id TEXT UNIQUE,
|
||||
project TEXT NOT NULL,
|
||||
user_prompt TEXT,
|
||||
started_at TEXT NOT NULL,
|
||||
started_at_epoch INTEGER NOT NULL,
|
||||
completed_at TEXT,
|
||||
completed_at_epoch INTEGER,
|
||||
status TEXT CHECK(status IN ('active', 'completed', 'failed')) NOT NULL DEFAULT 'active'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_claude_id ON sdk_sessions(claude_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_sdk_id ON sdk_sessions(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_project ON sdk_sessions(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_status ON sdk_sessions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_started ON sdk_sessions(started_at_epoch DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS observations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery')),
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_sdk_session ON observations(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS session_summaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT UNIQUE NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
request TEXT,
|
||||
investigated TEXT,
|
||||
learned TEXT,
|
||||
completed TEXT,
|
||||
next_steps TEXT,
|
||||
files_read TEXT,
|
||||
files_edited TEXT,
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(c=>c.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE session_summaries_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
request TEXT,
|
||||
investigated TEXT,
|
||||
learned TEXT,
|
||||
completed TEXT,
|
||||
next_steps TEXT,
|
||||
files_read TEXT,
|
||||
files_edited TEXT,
|
||||
notes TEXT,
|
||||
prompt_number INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
)
|
||||
`),this.db.exec(`
|
||||
INSERT INTO session_summaries_new
|
||||
SELECT id, sdk_session_id, project, request, investigated, learned,
|
||||
completed, next_steps, files_read, files_edited, notes,
|
||||
prompt_number, created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
`),this.db.exec("DROP TABLE session_summaries"),this.db.exec("ALTER TABLE session_summaries_new RENAME TO session_summaries"),this.db.exec(`
|
||||
CREATE INDEX idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString()),console.error("[SessionStore] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (remove UNIQUE constraint):",e.message)}}addObservationHierarchicalFields(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(8))return;if(this.db.pragma("table_info(observations)").some(r=>r.name==="title")){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(8,new Date().toISOString());return}console.error("[SessionStore] Adding hierarchical fields to observations table..."),this.db.exec(`
|
||||
ALTER TABLE observations ADD COLUMN title TEXT;
|
||||
ALTER TABLE observations ADD COLUMN subtitle TEXT;
|
||||
ALTER TABLE observations ADD COLUMN facts TEXT;
|
||||
ALTER TABLE observations ADD COLUMN narrative TEXT;
|
||||
ALTER TABLE observations ADD COLUMN concepts TEXT;
|
||||
ALTER TABLE observations ADD COLUMN files_read TEXT;
|
||||
ALTER TABLE observations ADD COLUMN files_modified TEXT;
|
||||
`),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(8,new Date().toISOString()),console.error("[SessionStore] Successfully added hierarchical fields to observations table")}catch(e){console.error("[SessionStore] Migration error (add hierarchical fields):",e.message)}}makeObservationsTextNullable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(9))return;let t=this.db.pragma("table_info(observations)").find(r=>r.name==="text");if(!t||t.notnull===0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString());return}console.error("[SessionStore] Making observations.text nullable..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE observations_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
text TEXT,
|
||||
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
|
||||
title TEXT,
|
||||
subtitle TEXT,
|
||||
facts TEXT,
|
||||
narrative TEXT,
|
||||
concepts TEXT,
|
||||
files_read TEXT,
|
||||
files_modified TEXT,
|
||||
prompt_number INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
)
|
||||
`),this.db.exec(`
|
||||
INSERT INTO observations_new
|
||||
SELECT id, sdk_session_id, project, text, type, title, subtitle, facts,
|
||||
narrative, concepts, files_read, files_modified, prompt_number,
|
||||
created_at, created_at_epoch
|
||||
FROM observations
|
||||
`),this.db.exec("DROP TABLE observations"),this.db.exec("ALTER TABLE observations_new RENAME TO observations"),this.db.exec(`
|
||||
CREATE INDEX idx_observations_sdk_session ON observations(sdk_session_id);
|
||||
CREATE INDEX idx_observations_project ON observations(project);
|
||||
CREATE INDEX idx_observations_type ON observations(type);
|
||||
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString()),console.error("[SessionStore] Successfully made observations.text nullable")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (make text nullable):",e.message)}}createUserPromptsTable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(10))return;if(this.db.pragma("table_info(user_prompts)").length>0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString());return}console.error("[SessionStore] Creating user_prompts table with FTS5 support..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE user_prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT NOT NULL,
|
||||
prompt_number INTEGER NOT NULL,
|
||||
prompt_text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(claude_session_id) REFERENCES sdk_sessions(claude_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_prompts_claude_session ON user_prompts(claude_session_id);
|
||||
CREATE INDEX idx_user_prompts_created ON user_prompts(created_at_epoch DESC);
|
||||
CREATE INDEX idx_user_prompts_prompt_number ON user_prompts(prompt_number);
|
||||
`),this.db.exec(`
|
||||
CREATE VIRTUAL TABLE user_prompts_fts USING fts5(
|
||||
prompt_text,
|
||||
content='user_prompts',
|
||||
content_rowid='id'
|
||||
);
|
||||
`),this.db.exec(`
|
||||
CREATE TRIGGER user_prompts_ai AFTER INSERT ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_ad AFTER DELETE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_au AFTER UPDATE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentSummariesWithSessionInfo(e,s=3){return this.db.prepare(`
|
||||
SELECT
|
||||
sdk_session_id, request, learned, completed, next_steps,
|
||||
prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentObservations(e,s=20){return this.db.prepare(`
|
||||
SELECT type, text, prompt_number, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
s.sdk_session_id,
|
||||
s.status,
|
||||
s.started_at,
|
||||
s.started_at_epoch,
|
||||
s.user_prompt,
|
||||
CASE WHEN sum.sdk_session_id IS NOT NULL THEN 1 ELSE 0 END as has_summary
|
||||
FROM sdk_sessions s
|
||||
LEFT JOIN session_summaries sum ON s.sdk_session_id = sum.sdk_session_id
|
||||
WHERE s.project = ? AND s.sdk_session_id IS NOT NULL
|
||||
GROUP BY s.sdk_session_id
|
||||
ORDER BY s.started_at_epoch DESC
|
||||
LIMIT ?
|
||||
)
|
||||
ORDER BY started_at_epoch ASC
|
||||
`).all(e,s)}getObservationsForSession(e){return this.db.prepare(`
|
||||
SELECT title, subtitle, type, prompt_number
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
ORDER BY created_at_epoch ASC
|
||||
`).all(e)}getSummaryForSession(e){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE sdk_session_id = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 1
|
||||
`).get(e)||null}getFilesForSession(e){let t=this.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),r=new Set,i=new Set;for(let d of t){if(d.files_read)try{let n=JSON.parse(d.files_read);Array.isArray(n)&&n.forEach(c=>r.add(c))}catch{}if(d.files_modified)try{let n=JSON.parse(d.files_modified);Array.isArray(n)&&n.forEach(c=>i.add(c))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(i)}}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)||null}findActiveSDKSession(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ? AND status = 'active'
|
||||
LIMIT 1
|
||||
`).get(e)||null}findAnySDKSession(e){return this.db.prepare(`
|
||||
SELECT id
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ?
|
||||
LIMIT 1
|
||||
`).get(e)||null}reactivateSession(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'active', user_prompt = ?, worker_port = NULL
|
||||
WHERE id = ?
|
||||
`).run(s,e)}incrementPromptCounter(e){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET prompt_counter = COALESCE(prompt_counter, 0) + 1
|
||||
WHERE id = ?
|
||||
`).run(e),this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,i=r.getTime(),n=this.db.prepare(`
|
||||
INSERT OR IGNORE INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,t,r.toISOString(),i);return n.lastInsertRowid===0||n.changes===0?this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||
`).get(e).id:n.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(s,e).changes===0?(X.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
`).run(s,e)}getWorkerPort(e){return this.db.prepare(`
|
||||
SELECT worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,i=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO user_prompts
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(e,s,t,r.toISOString(),i).lastInsertRowid}storeObservation(e,s,t,r){let i=new Date,d=i.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,i.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,i.toISOString(),d)}storeSummary(e,s,t,r){let i=new Date,d=i.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,i.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,i.toISOString(),d)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(s.toISOString(),t,e)}markSessionFailed(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(s.toISOString(),t,e)}cleanupOrphanedSessions(){let e=new Date,s=e.getTime();return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};import v from"path";import{existsSync as y}from"fs";import{spawn as K}from"child_process";var V=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),J=`http://127.0.0.1:${V}/health`;async function F(){try{return(await fetch(J,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function P(){try{if(await F())return!0;console.error("[claude-mem] Worker not responding, starting...");let p=M(),e=v.join(p,"plugin","scripts","worker-service.cjs");if(!y(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=v.join(p,"ecosystem.config.cjs"),t=v.join(p,"node_modules",".bin","pm2");if(!y(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!y(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=K(t,["start",s],{detached:!0,stdio:"ignore",cwd:p});r.on("error",i=>{throw new Error(`Failed to spawn PM2: ${i.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let i=0;i<3;i++)if(await new Promise(d=>setTimeout(d,500)),await F())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(p){return console.error(`[claude-mem] Failed to start worker: ${p.message}`),!1}}var o={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",cyan:"\x1B[36m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",gray:"\x1B[90m"};function D(p,e=!1,s=!1){P();let t=p?.cwd??process.cwd(),r=t?C.basename(t):"unknown-project",i=new N;try{let d=i.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT sdk_session_id, request, learned, completed, next_steps, created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 10
|
||||
)
|
||||
ORDER BY created_at_epoch ASC
|
||||
`).all(r);if(d.length===0)return e?`
|
||||
${o.bright}${o.cyan}\u{1F4DD} [${r}] recent context${o.reset}
|
||||
${o.gray}${"\u2500".repeat(60)}${o.reset}
|
||||
|
||||
${o.dim}No previous summaries found for this project yet.${o.reset}
|
||||
`:`# [${r}] recent context
|
||||
|
||||
No previous summaries found for this project yet.`;let n=[];e?(n.push(""),n.push(`${o.bright}${o.cyan}\u{1F4DD} [${r}] recent context${o.reset}`),n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`)):(n.push(`# [${r}] recent context`),n.push(""));let c=!0;for(let _=0;_<d.length;_++){let a=d[_],l=d.length-1-_,f=l===0,G=l>=1&&l<=3,k=l>3;if(c?e&&n.push(""):e?(n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`),n.push("")):(n.push("---"),n.push("")),c=!1,k){a.request&&(e?(n.push(`${o.bright}${o.yellow}Request:${o.reset} ${a.request}`),n.push("")):(n.push(`**Request:** ${a.request}`),n.push("")));let T=new Date(a.created_at).toLocaleString();e?n.push(`${o.dim}Date: ${T}${o.reset}`):(n.push(`**Date:** ${T}`),n.push(""));continue}if(a.request&&(e?(n.push(`${o.bright}${o.yellow}Request:${o.reset} ${a.request}`),n.push("")):(n.push(`**Request:** ${a.request}`),n.push(""))),f&&a.learned&&(e?(n.push(`${o.bright}${o.blue}Learned:${o.reset} ${a.learned}`),n.push("")):(n.push(`**Learned:** ${a.learned}`),n.push(""))),a.completed&&(e?(n.push(`${o.bright}${o.green}Completed:${o.reset} ${a.completed}`),n.push("")):(n.push(`**Completed:** ${a.completed}`),n.push(""))),f&&a.next_steps&&(e?(n.push(`${o.bright}${o.magenta}Next Steps:${o.reset} ${a.next_steps}`),n.push("")):(n.push(`**Next Steps:** ${a.next_steps}`),n.push(""))),f){let T=i.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(a.sdk_session_id),h=new Set,g=new Set,b=m=>{try{return C.isAbsolute(m)?C.relative(t,m):m}catch{return m}};for(let m of T){if(m.files_read)try{let S=JSON.parse(m.files_read);Array.isArray(S)&&S.forEach(I=>h.add(b(I)))}catch{}if(m.files_modified)try{let S=JSON.parse(m.files_modified);Array.isArray(S)&&S.forEach(I=>g.add(b(I)))}catch{}}g.forEach(m=>h.delete(m)),h.size>0&&(e?n.push(`${o.dim}Files Read: ${Array.from(h).join(", ")}${o.reset}`):n.push(`**Files Read:** ${Array.from(h).join(", ")}`)),g.size>0&&(e?n.push(`${o.dim}Files Modified: ${Array.from(g).join(", ")}${o.reset}`):n.push(`**Files Modified:** ${Array.from(g).join(", ")}`))}let R=new Date(a.created_at).toLocaleString();e?n.push(`${o.dim}Date: ${R}${o.reset}`):n.push(`**Date:** ${R}`),e||n.push("")}return e&&(n.push(""),n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`)),n.join(`
|
||||
`)}finally{i.close()}}import{stdin as x}from"process";try{let p=process.argv.includes("--index");if(x.isTTY){let e=D(void 0,!0,p);console.log(e),process.exit(0)}else{let e="";x.on("data",s=>e+=s),x.on("end",()=>{let s=e.trim()?JSON.parse(e):void 0,r={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:D(s,!1,p)}};console.log(JSON.stringify(r)),process.exit(0)})}}catch(p){console.error(`[claude-mem context-hook error: ${p.message}]`),process.exit(0)}
|
||||
Executable
+309
@@ -0,0 +1,309 @@
|
||||
#!/usr/bin/env node
|
||||
import Y from"path";import W from"better-sqlite3";import{join as p,dirname as M,basename as z}from"path";import{homedir as h}from"os";import{existsSync as te,mkdirSync as P}from"fs";import{fileURLToPath as F}from"url";function H(){return typeof __dirname<"u"?__dirname:M(F(import.meta.url))}var G=H(),u=process.env.CLAUDE_MEM_DATA_DIR||p(h(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||p(h(),".claude"),oe=p(u,"archives"),ne=p(u,"logs"),ie=p(u,"trash"),ae=p(u,"backups"),de=p(u,"settings.json"),O=p(u,"claude-mem.db"),pe=p(l,"settings.json"),ce=p(l,"commands"),Ee=p(l,"CLAUDE.md");function I(n){P(n,{recursive:!0})}function L(){return p(G,"..","..")}var T=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(T||{}),S=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=T[e].padEnd(5),d=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let c="";o!=null&&(this.level===0&&typeof o=="object"?c=`
|
||||
`+JSON.stringify(o,null,2):c=" "+this.formatData(o));let _="";if(r){let{sessionId:K,sdkSessionId:V,correlationId:q,...f}=r;Object.keys(f).length>0&&(_=` {${Object.entries(f).map(([w,X])=>`${w}=${X}`).join(", ")}}`)}let R=`[${i}] [${a}] [${d}] ${E}${t}${_}${c}`;e===3?console.error(R):console.log(R)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new S;var m=class{db;constructor(){I(u),this.db=new W(O),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
applied_at TEXT NOT NULL
|
||||
)
|
||||
`);let e=this.db.prepare("SELECT version FROM schema_versions ORDER BY version").all();(e.length>0?Math.max(...e.map(t=>t.version)):0)===0&&(console.error("[SessionStore] Initializing fresh database with migration004..."),this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS sdk_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT UNIQUE NOT NULL,
|
||||
sdk_session_id TEXT UNIQUE,
|
||||
project TEXT NOT NULL,
|
||||
user_prompt TEXT,
|
||||
started_at TEXT NOT NULL,
|
||||
started_at_epoch INTEGER NOT NULL,
|
||||
completed_at TEXT,
|
||||
completed_at_epoch INTEGER,
|
||||
status TEXT CHECK(status IN ('active', 'completed', 'failed')) NOT NULL DEFAULT 'active'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_claude_id ON sdk_sessions(claude_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_sdk_id ON sdk_sessions(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_project ON sdk_sessions(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_status ON sdk_sessions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_started ON sdk_sessions(started_at_epoch DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS observations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery')),
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_sdk_session ON observations(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS session_summaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT UNIQUE NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
request TEXT,
|
||||
investigated TEXT,
|
||||
learned TEXT,
|
||||
completed TEXT,
|
||||
next_steps TEXT,
|
||||
files_read TEXT,
|
||||
files_edited TEXT,
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(d=>d.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE session_summaries_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
request TEXT,
|
||||
investigated TEXT,
|
||||
learned TEXT,
|
||||
completed TEXT,
|
||||
next_steps TEXT,
|
||||
files_read TEXT,
|
||||
files_edited TEXT,
|
||||
notes TEXT,
|
||||
prompt_number INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
)
|
||||
`),this.db.exec(`
|
||||
INSERT INTO session_summaries_new
|
||||
SELECT id, sdk_session_id, project, request, investigated, learned,
|
||||
completed, next_steps, files_read, files_edited, notes,
|
||||
prompt_number, created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
`),this.db.exec("DROP TABLE session_summaries"),this.db.exec("ALTER TABLE session_summaries_new RENAME TO session_summaries"),this.db.exec(`
|
||||
CREATE INDEX idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString()),console.error("[SessionStore] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (remove UNIQUE constraint):",e.message)}}addObservationHierarchicalFields(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(8))return;if(this.db.pragma("table_info(observations)").some(r=>r.name==="title")){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(8,new Date().toISOString());return}console.error("[SessionStore] Adding hierarchical fields to observations table..."),this.db.exec(`
|
||||
ALTER TABLE observations ADD COLUMN title TEXT;
|
||||
ALTER TABLE observations ADD COLUMN subtitle TEXT;
|
||||
ALTER TABLE observations ADD COLUMN facts TEXT;
|
||||
ALTER TABLE observations ADD COLUMN narrative TEXT;
|
||||
ALTER TABLE observations ADD COLUMN concepts TEXT;
|
||||
ALTER TABLE observations ADD COLUMN files_read TEXT;
|
||||
ALTER TABLE observations ADD COLUMN files_modified TEXT;
|
||||
`),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(8,new Date().toISOString()),console.error("[SessionStore] Successfully added hierarchical fields to observations table")}catch(e){console.error("[SessionStore] Migration error (add hierarchical fields):",e.message)}}makeObservationsTextNullable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(9))return;let t=this.db.pragma("table_info(observations)").find(r=>r.name==="text");if(!t||t.notnull===0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString());return}console.error("[SessionStore] Making observations.text nullable..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE observations_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
text TEXT,
|
||||
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
|
||||
title TEXT,
|
||||
subtitle TEXT,
|
||||
facts TEXT,
|
||||
narrative TEXT,
|
||||
concepts TEXT,
|
||||
files_read TEXT,
|
||||
files_modified TEXT,
|
||||
prompt_number INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
)
|
||||
`),this.db.exec(`
|
||||
INSERT INTO observations_new
|
||||
SELECT id, sdk_session_id, project, text, type, title, subtitle, facts,
|
||||
narrative, concepts, files_read, files_modified, prompt_number,
|
||||
created_at, created_at_epoch
|
||||
FROM observations
|
||||
`),this.db.exec("DROP TABLE observations"),this.db.exec("ALTER TABLE observations_new RENAME TO observations"),this.db.exec(`
|
||||
CREATE INDEX idx_observations_sdk_session ON observations(sdk_session_id);
|
||||
CREATE INDEX idx_observations_project ON observations(project);
|
||||
CREATE INDEX idx_observations_type ON observations(type);
|
||||
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString()),console.error("[SessionStore] Successfully made observations.text nullable")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (make text nullable):",e.message)}}createUserPromptsTable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(10))return;if(this.db.pragma("table_info(user_prompts)").length>0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString());return}console.error("[SessionStore] Creating user_prompts table with FTS5 support..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE user_prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT NOT NULL,
|
||||
prompt_number INTEGER NOT NULL,
|
||||
prompt_text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(claude_session_id) REFERENCES sdk_sessions(claude_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_prompts_claude_session ON user_prompts(claude_session_id);
|
||||
CREATE INDEX idx_user_prompts_created ON user_prompts(created_at_epoch DESC);
|
||||
CREATE INDEX idx_user_prompts_prompt_number ON user_prompts(prompt_number);
|
||||
`),this.db.exec(`
|
||||
CREATE VIRTUAL TABLE user_prompts_fts USING fts5(
|
||||
prompt_text,
|
||||
content='user_prompts',
|
||||
content_rowid='id'
|
||||
);
|
||||
`),this.db.exec(`
|
||||
CREATE TRIGGER user_prompts_ai AFTER INSERT ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_ad AFTER DELETE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_au AFTER UPDATE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentSummariesWithSessionInfo(e,s=3){return this.db.prepare(`
|
||||
SELECT
|
||||
sdk_session_id, request, learned, completed, next_steps,
|
||||
prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentObservations(e,s=20){return this.db.prepare(`
|
||||
SELECT type, text, prompt_number, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
s.sdk_session_id,
|
||||
s.status,
|
||||
s.started_at,
|
||||
s.started_at_epoch,
|
||||
s.user_prompt,
|
||||
CASE WHEN sum.sdk_session_id IS NOT NULL THEN 1 ELSE 0 END as has_summary
|
||||
FROM sdk_sessions s
|
||||
LEFT JOIN session_summaries sum ON s.sdk_session_id = sum.sdk_session_id
|
||||
WHERE s.project = ? AND s.sdk_session_id IS NOT NULL
|
||||
GROUP BY s.sdk_session_id
|
||||
ORDER BY s.started_at_epoch DESC
|
||||
LIMIT ?
|
||||
)
|
||||
ORDER BY started_at_epoch ASC
|
||||
`).all(e,s)}getObservationsForSession(e){return this.db.prepare(`
|
||||
SELECT title, subtitle, type, prompt_number
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
ORDER BY created_at_epoch ASC
|
||||
`).all(e)}getSummaryForSession(e){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE sdk_session_id = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 1
|
||||
`).get(e)||null}getFilesForSession(e){let t=this.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),r=new Set,o=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(d=>o.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)||null}findActiveSDKSession(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ? AND status = 'active'
|
||||
LIMIT 1
|
||||
`).get(e)||null}findAnySDKSession(e){return this.db.prepare(`
|
||||
SELECT id
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ?
|
||||
LIMIT 1
|
||||
`).get(e)||null}reactivateSession(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'active', user_prompt = ?, worker_port = NULL
|
||||
WHERE id = ?
|
||||
`).run(s,e)}incrementPromptCounter(e){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET prompt_counter = COALESCE(prompt_counter, 0) + 1
|
||||
WHERE id = ?
|
||||
`).run(e),this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,o=r.getTime(),a=this.db.prepare(`
|
||||
INSERT OR IGNORE INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,t,r.toISOString(),o);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||
`).get(e).id:a.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(s,e).changes===0?(A.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
`).run(s,e)}getWorkerPort(e){return this.db.prepare(`
|
||||
SELECT worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,o=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO user_prompts
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(e,s,t,r.toISOString(),o).lastInsertRowid}storeObservation(e,s,t,r){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,o.toISOString(),i)}storeSummary(e,s,t,r){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,o.toISOString(),i)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(s.toISOString(),t,e)}markSessionFailed(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(s.toISOString(),t,e)}cleanupOrphanedSessions(){let e=new Date,s=e.getTime();return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function B(n,e,s){return n==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:n==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:n==="UserPromptSubmit"||n==="PostToolUse"?{continue:!0,suppressOutput:!0}:n==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(n,e,s={}){let t=B(n,e,s);return JSON.stringify(t)}import g from"path";import{existsSync as b}from"fs";import{spawn as $}from"child_process";var k=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),j=`http://127.0.0.1:${k}/health`;async function C(){try{return(await fetch(j,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function D(){try{if(await C())return!0;console.error("[claude-mem] Worker not responding, starting...");let n=L(),e=g.join(n,"plugin","scripts","worker-service.cjs");if(!b(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=g.join(n,"ecosystem.config.cjs"),t=g.join(n,"node_modules",".bin","pm2");if(!b(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!b(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=$(t,["start",s],{detached:!0,stdio:"ignore",cwd:n});r.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(i=>setTimeout(i,500)),await C())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(n){return console.error(`[claude-mem] Failed to start worker: ${n.message}`),!1}}function y(){return k}async function x(n){if(!n)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=n,r=Y.basename(s);if(!await D())throw new Error("Worker service failed to start or become healthy");let i=new m;try{let a=i.createSDKSession(e,r,t),d=i.incrementPromptCounter(a);i.saveUserPrompt(e,d,t),console.error(`[new-hook] Session ${a}, prompt #${d}`);let E=y(),c=await fetch(`http://127.0.0.1:${E}/sessions/${a}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:t}),signal:AbortSignal.timeout(5e3)});if(!c.ok){let _=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${_}`)}console.log(v("UserPromptSubmit",!0))}finally{i.close()}}import{stdin as U}from"process";var N="";U.on("data",n=>N+=n);U.on("end",async()=>{let n=N.trim()?JSON.parse(N):void 0;await x(n),process.exit(0)});
|
||||
Executable
+309
@@ -0,0 +1,309 @@
|
||||
#!/usr/bin/env node
|
||||
import H from"better-sqlite3";import{join as p,dirname as w,basename as Q}from"path";import{homedir as I}from"os";import{existsSync as se,mkdirSync as M}from"fs";import{fileURLToPath as X}from"url";function P(){return typeof __dirname<"u"?__dirname:w(X(import.meta.url))}var F=P(),u=process.env.CLAUDE_MEM_DATA_DIR||p(I(),".claude-mem"),S=process.env.CLAUDE_CONFIG_DIR||p(I(),".claude"),re=p(u,"archives"),oe=p(u,"logs"),ne=p(u,"trash"),ie=p(u,"backups"),ae=p(u,"settings.json"),L=p(u,"claude-mem.db"),de=p(S,"settings.json"),pe=p(S,"commands"),ce=p(S,"CLAUDE.md");function A(n){M(n,{recursive:!0})}function v(){return p(F,"..","..")}var g=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(g||{}),b=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=g[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=g[e].padEnd(5),d=s.padEnd(6),c="";r?.correlationId?c=`[${r.correlationId}] `:r?.sessionId&&(c=`[session-${r.sessionId}] `);let E="";o!=null&&(this.level===0&&typeof o=="object"?E=`
|
||||
`+JSON.stringify(o,null,2):E=" "+this.formatData(o));let _="";if(r){let{sessionId:K,sdkSessionId:Y,correlationId:V,...h}=r;Object.keys(h).length>0&&(_=` {${Object.entries(h).map(([x,U])=>`${x}=${U}`).join(", ")}}`)}let l=`[${i}] [${a}] [${d}] ${c}${t}${_}${E}`;e===3?console.error(l):console.log(l)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},m=new b;var T=class{db;constructor(){A(u),this.db=new H(L),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
applied_at TEXT NOT NULL
|
||||
)
|
||||
`);let e=this.db.prepare("SELECT version FROM schema_versions ORDER BY version").all();(e.length>0?Math.max(...e.map(t=>t.version)):0)===0&&(console.error("[SessionStore] Initializing fresh database with migration004..."),this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS sdk_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT UNIQUE NOT NULL,
|
||||
sdk_session_id TEXT UNIQUE,
|
||||
project TEXT NOT NULL,
|
||||
user_prompt TEXT,
|
||||
started_at TEXT NOT NULL,
|
||||
started_at_epoch INTEGER NOT NULL,
|
||||
completed_at TEXT,
|
||||
completed_at_epoch INTEGER,
|
||||
status TEXT CHECK(status IN ('active', 'completed', 'failed')) NOT NULL DEFAULT 'active'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_claude_id ON sdk_sessions(claude_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_sdk_id ON sdk_sessions(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_project ON sdk_sessions(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_status ON sdk_sessions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_started ON sdk_sessions(started_at_epoch DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS observations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery')),
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_sdk_session ON observations(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS session_summaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT UNIQUE NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
request TEXT,
|
||||
investigated TEXT,
|
||||
learned TEXT,
|
||||
completed TEXT,
|
||||
next_steps TEXT,
|
||||
files_read TEXT,
|
||||
files_edited TEXT,
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(d=>d.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE session_summaries_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
request TEXT,
|
||||
investigated TEXT,
|
||||
learned TEXT,
|
||||
completed TEXT,
|
||||
next_steps TEXT,
|
||||
files_read TEXT,
|
||||
files_edited TEXT,
|
||||
notes TEXT,
|
||||
prompt_number INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
)
|
||||
`),this.db.exec(`
|
||||
INSERT INTO session_summaries_new
|
||||
SELECT id, sdk_session_id, project, request, investigated, learned,
|
||||
completed, next_steps, files_read, files_edited, notes,
|
||||
prompt_number, created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
`),this.db.exec("DROP TABLE session_summaries"),this.db.exec("ALTER TABLE session_summaries_new RENAME TO session_summaries"),this.db.exec(`
|
||||
CREATE INDEX idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString()),console.error("[SessionStore] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (remove UNIQUE constraint):",e.message)}}addObservationHierarchicalFields(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(8))return;if(this.db.pragma("table_info(observations)").some(r=>r.name==="title")){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(8,new Date().toISOString());return}console.error("[SessionStore] Adding hierarchical fields to observations table..."),this.db.exec(`
|
||||
ALTER TABLE observations ADD COLUMN title TEXT;
|
||||
ALTER TABLE observations ADD COLUMN subtitle TEXT;
|
||||
ALTER TABLE observations ADD COLUMN facts TEXT;
|
||||
ALTER TABLE observations ADD COLUMN narrative TEXT;
|
||||
ALTER TABLE observations ADD COLUMN concepts TEXT;
|
||||
ALTER TABLE observations ADD COLUMN files_read TEXT;
|
||||
ALTER TABLE observations ADD COLUMN files_modified TEXT;
|
||||
`),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(8,new Date().toISOString()),console.error("[SessionStore] Successfully added hierarchical fields to observations table")}catch(e){console.error("[SessionStore] Migration error (add hierarchical fields):",e.message)}}makeObservationsTextNullable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(9))return;let t=this.db.pragma("table_info(observations)").find(r=>r.name==="text");if(!t||t.notnull===0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString());return}console.error("[SessionStore] Making observations.text nullable..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE observations_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
text TEXT,
|
||||
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
|
||||
title TEXT,
|
||||
subtitle TEXT,
|
||||
facts TEXT,
|
||||
narrative TEXT,
|
||||
concepts TEXT,
|
||||
files_read TEXT,
|
||||
files_modified TEXT,
|
||||
prompt_number INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
)
|
||||
`),this.db.exec(`
|
||||
INSERT INTO observations_new
|
||||
SELECT id, sdk_session_id, project, text, type, title, subtitle, facts,
|
||||
narrative, concepts, files_read, files_modified, prompt_number,
|
||||
created_at, created_at_epoch
|
||||
FROM observations
|
||||
`),this.db.exec("DROP TABLE observations"),this.db.exec("ALTER TABLE observations_new RENAME TO observations"),this.db.exec(`
|
||||
CREATE INDEX idx_observations_sdk_session ON observations(sdk_session_id);
|
||||
CREATE INDEX idx_observations_project ON observations(project);
|
||||
CREATE INDEX idx_observations_type ON observations(type);
|
||||
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString()),console.error("[SessionStore] Successfully made observations.text nullable")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (make text nullable):",e.message)}}createUserPromptsTable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(10))return;if(this.db.pragma("table_info(user_prompts)").length>0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString());return}console.error("[SessionStore] Creating user_prompts table with FTS5 support..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE user_prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT NOT NULL,
|
||||
prompt_number INTEGER NOT NULL,
|
||||
prompt_text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(claude_session_id) REFERENCES sdk_sessions(claude_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_prompts_claude_session ON user_prompts(claude_session_id);
|
||||
CREATE INDEX idx_user_prompts_created ON user_prompts(created_at_epoch DESC);
|
||||
CREATE INDEX idx_user_prompts_prompt_number ON user_prompts(prompt_number);
|
||||
`),this.db.exec(`
|
||||
CREATE VIRTUAL TABLE user_prompts_fts USING fts5(
|
||||
prompt_text,
|
||||
content='user_prompts',
|
||||
content_rowid='id'
|
||||
);
|
||||
`),this.db.exec(`
|
||||
CREATE TRIGGER user_prompts_ai AFTER INSERT ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_ad AFTER DELETE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_au AFTER UPDATE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentSummariesWithSessionInfo(e,s=3){return this.db.prepare(`
|
||||
SELECT
|
||||
sdk_session_id, request, learned, completed, next_steps,
|
||||
prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentObservations(e,s=20){return this.db.prepare(`
|
||||
SELECT type, text, prompt_number, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
s.sdk_session_id,
|
||||
s.status,
|
||||
s.started_at,
|
||||
s.started_at_epoch,
|
||||
s.user_prompt,
|
||||
CASE WHEN sum.sdk_session_id IS NOT NULL THEN 1 ELSE 0 END as has_summary
|
||||
FROM sdk_sessions s
|
||||
LEFT JOIN session_summaries sum ON s.sdk_session_id = sum.sdk_session_id
|
||||
WHERE s.project = ? AND s.sdk_session_id IS NOT NULL
|
||||
GROUP BY s.sdk_session_id
|
||||
ORDER BY s.started_at_epoch DESC
|
||||
LIMIT ?
|
||||
)
|
||||
ORDER BY started_at_epoch ASC
|
||||
`).all(e,s)}getObservationsForSession(e){return this.db.prepare(`
|
||||
SELECT title, subtitle, type, prompt_number
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
ORDER BY created_at_epoch ASC
|
||||
`).all(e)}getSummaryForSession(e){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE sdk_session_id = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 1
|
||||
`).get(e)||null}getFilesForSession(e){let t=this.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),r=new Set,o=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(d=>o.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)||null}findActiveSDKSession(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ? AND status = 'active'
|
||||
LIMIT 1
|
||||
`).get(e)||null}findAnySDKSession(e){return this.db.prepare(`
|
||||
SELECT id
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ?
|
||||
LIMIT 1
|
||||
`).get(e)||null}reactivateSession(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'active', user_prompt = ?, worker_port = NULL
|
||||
WHERE id = ?
|
||||
`).run(s,e)}incrementPromptCounter(e){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET prompt_counter = COALESCE(prompt_counter, 0) + 1
|
||||
WHERE id = ?
|
||||
`).run(e),this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,o=r.getTime(),a=this.db.prepare(`
|
||||
INSERT OR IGNORE INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,t,r.toISOString(),o);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||
`).get(e).id:a.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(s,e).changes===0?(m.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
`).run(s,e)}getWorkerPort(e){return this.db.prepare(`
|
||||
SELECT worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,o=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO user_prompts
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(e,s,t,r.toISOString(),o).lastInsertRowid}storeObservation(e,s,t,r){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,o.toISOString(),i)}storeSummary(e,s,t,r){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,o.toISOString(),i)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(s.toISOString(),t,e)}markSessionFailed(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(s.toISOString(),t,e)}cleanupOrphanedSessions(){let e=new Date,s=e.getTime();return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function G(n,e,s){return n==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:n==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:n==="UserPromptSubmit"||n==="PostToolUse"?{continue:!0,suppressOutput:!0}:n==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function N(n,e,s={}){let t=G(n,e,s);return JSON.stringify(t)}import R from"path";import{existsSync as f}from"fs";import{spawn as W}from"child_process";var B=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),$=`http://127.0.0.1:${B}/health`;async function C(){try{return(await fetch($,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function k(){try{if(await C())return!0;console.error("[claude-mem] Worker not responding, starting...");let n=v(),e=R.join(n,"plugin","scripts","worker-service.cjs");if(!f(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=R.join(n,"ecosystem.config.cjs"),t=R.join(n,"node_modules",".bin","pm2");if(!f(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!f(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=W(t,["start",s],{detached:!0,stdio:"ignore",cwd:n});r.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(i=>setTimeout(i,500)),await C())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(n){return console.error(`[claude-mem] Failed to start worker: ${n.message}`),!1}}var j=new Set(["ListMcpResourcesTool"]);async function D(n){if(!n)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_output:r}=n;if(j.has(s)){console.log(N("PostToolUse",!0));return}if(!await k())throw new Error("Worker service failed to start or become healthy");let i=new T,a=i.createSDKSession(e,"",""),d=i.getPromptCounter(a);i.close();let c=m.formatTool(s,t),E=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);m.dataIn("HOOK",`PostToolUse: ${c}`,{sessionId:a,workerPort:E});let _=await fetch(`http://127.0.0.1:${E}/sessions/${a}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:s,tool_input:t!==void 0?JSON.stringify(t):"{}",tool_output:r!==void 0?JSON.stringify(r):"{}",prompt_number:d}),signal:AbortSignal.timeout(2e3)});if(!_.ok){let l=await _.text();throw m.failure("HOOK","Failed to send observation",{sessionId:a,status:_.status},l),new Error(`Failed to send observation to worker: ${_.status} ${l}`)}m.debug("HOOK","Observation sent successfully",{sessionId:a,toolName:s}),console.log(N("PostToolUse",!0))}import{stdin as y}from"process";var O="";y.on("data",n=>O+=n);y.on("end",async()=>{let n=O.trim()?JSON.parse(O):void 0;await D(n),process.exit(0)});
|
||||
Executable
+531
File diff suppressed because one or more lines are too long
Executable
+309
@@ -0,0 +1,309 @@
|
||||
#!/usr/bin/env node
|
||||
import H from"better-sqlite3";import{join as p,dirname as w,basename as J}from"path";import{homedir as O}from"os";import{existsSync as ee,mkdirSync as X}from"fs";import{fileURLToPath as M}from"url";function P(){return typeof __dirname<"u"?__dirname:w(M(import.meta.url))}var F=P(),c=process.env.CLAUDE_MEM_DATA_DIR||p(O(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||p(O(),".claude"),te=p(c,"archives"),re=p(c,"logs"),oe=p(c,"trash"),ne=p(c,"backups"),ie=p(c,"settings.json"),I=p(c,"claude-mem.db"),ae=p(l,"settings.json"),de=p(l,"commands"),pe=p(l,"CLAUDE.md");function L(n){X(n,{recursive:!0})}function A(){return p(F,"..","..")}var T=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(T||{}),S=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=T[e].padEnd(5),d=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let _="";o!=null&&(this.level===0&&typeof o=="object"?_=`
|
||||
`+JSON.stringify(o,null,2):_=" "+this.formatData(o));let b="";if(r){let{sessionId:j,sdkSessionId:K,correlationId:Y,...h}=r;Object.keys(h).length>0&&(b=` {${Object.entries(h).map(([x,U])=>`${x}=${U}`).join(", ")}}`)}let f=`[${i}] [${a}] [${d}] ${E}${t}${b}${_}`;e===3?console.error(f):console.log(f)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},u=new S;var m=class{db;constructor(){L(c),this.db=new H(I),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
applied_at TEXT NOT NULL
|
||||
)
|
||||
`);let e=this.db.prepare("SELECT version FROM schema_versions ORDER BY version").all();(e.length>0?Math.max(...e.map(t=>t.version)):0)===0&&(console.error("[SessionStore] Initializing fresh database with migration004..."),this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS sdk_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT UNIQUE NOT NULL,
|
||||
sdk_session_id TEXT UNIQUE,
|
||||
project TEXT NOT NULL,
|
||||
user_prompt TEXT,
|
||||
started_at TEXT NOT NULL,
|
||||
started_at_epoch INTEGER NOT NULL,
|
||||
completed_at TEXT,
|
||||
completed_at_epoch INTEGER,
|
||||
status TEXT CHECK(status IN ('active', 'completed', 'failed')) NOT NULL DEFAULT 'active'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_claude_id ON sdk_sessions(claude_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_sdk_id ON sdk_sessions(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_project ON sdk_sessions(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_status ON sdk_sessions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_started ON sdk_sessions(started_at_epoch DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS observations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery')),
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_sdk_session ON observations(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS session_summaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT UNIQUE NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
request TEXT,
|
||||
investigated TEXT,
|
||||
learned TEXT,
|
||||
completed TEXT,
|
||||
next_steps TEXT,
|
||||
files_read TEXT,
|
||||
files_edited TEXT,
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(d=>d.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(d=>d.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE session_summaries_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
request TEXT,
|
||||
investigated TEXT,
|
||||
learned TEXT,
|
||||
completed TEXT,
|
||||
next_steps TEXT,
|
||||
files_read TEXT,
|
||||
files_edited TEXT,
|
||||
notes TEXT,
|
||||
prompt_number INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
)
|
||||
`),this.db.exec(`
|
||||
INSERT INTO session_summaries_new
|
||||
SELECT id, sdk_session_id, project, request, investigated, learned,
|
||||
completed, next_steps, files_read, files_edited, notes,
|
||||
prompt_number, created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
`),this.db.exec("DROP TABLE session_summaries"),this.db.exec("ALTER TABLE session_summaries_new RENAME TO session_summaries"),this.db.exec(`
|
||||
CREATE INDEX idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString()),console.error("[SessionStore] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (remove UNIQUE constraint):",e.message)}}addObservationHierarchicalFields(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(8))return;if(this.db.pragma("table_info(observations)").some(r=>r.name==="title")){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(8,new Date().toISOString());return}console.error("[SessionStore] Adding hierarchical fields to observations table..."),this.db.exec(`
|
||||
ALTER TABLE observations ADD COLUMN title TEXT;
|
||||
ALTER TABLE observations ADD COLUMN subtitle TEXT;
|
||||
ALTER TABLE observations ADD COLUMN facts TEXT;
|
||||
ALTER TABLE observations ADD COLUMN narrative TEXT;
|
||||
ALTER TABLE observations ADD COLUMN concepts TEXT;
|
||||
ALTER TABLE observations ADD COLUMN files_read TEXT;
|
||||
ALTER TABLE observations ADD COLUMN files_modified TEXT;
|
||||
`),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(8,new Date().toISOString()),console.error("[SessionStore] Successfully added hierarchical fields to observations table")}catch(e){console.error("[SessionStore] Migration error (add hierarchical fields):",e.message)}}makeObservationsTextNullable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(9))return;let t=this.db.pragma("table_info(observations)").find(r=>r.name==="text");if(!t||t.notnull===0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString());return}console.error("[SessionStore] Making observations.text nullable..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE observations_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
text TEXT,
|
||||
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
|
||||
title TEXT,
|
||||
subtitle TEXT,
|
||||
facts TEXT,
|
||||
narrative TEXT,
|
||||
concepts TEXT,
|
||||
files_read TEXT,
|
||||
files_modified TEXT,
|
||||
prompt_number INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
)
|
||||
`),this.db.exec(`
|
||||
INSERT INTO observations_new
|
||||
SELECT id, sdk_session_id, project, text, type, title, subtitle, facts,
|
||||
narrative, concepts, files_read, files_modified, prompt_number,
|
||||
created_at, created_at_epoch
|
||||
FROM observations
|
||||
`),this.db.exec("DROP TABLE observations"),this.db.exec("ALTER TABLE observations_new RENAME TO observations"),this.db.exec(`
|
||||
CREATE INDEX idx_observations_sdk_session ON observations(sdk_session_id);
|
||||
CREATE INDEX idx_observations_project ON observations(project);
|
||||
CREATE INDEX idx_observations_type ON observations(type);
|
||||
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(9,new Date().toISOString()),console.error("[SessionStore] Successfully made observations.text nullable")}catch(r){throw this.db.exec("ROLLBACK"),r}}catch(e){console.error("[SessionStore] Migration error (make text nullable):",e.message)}}createUserPromptsTable(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(10))return;if(this.db.pragma("table_info(user_prompts)").length>0){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString());return}console.error("[SessionStore] Creating user_prompts table with FTS5 support..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE user_prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT NOT NULL,
|
||||
prompt_number INTEGER NOT NULL,
|
||||
prompt_text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(claude_session_id) REFERENCES sdk_sessions(claude_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_prompts_claude_session ON user_prompts(claude_session_id);
|
||||
CREATE INDEX idx_user_prompts_created ON user_prompts(created_at_epoch DESC);
|
||||
CREATE INDEX idx_user_prompts_prompt_number ON user_prompts(prompt_number);
|
||||
`),this.db.exec(`
|
||||
CREATE VIRTUAL TABLE user_prompts_fts USING fts5(
|
||||
prompt_text,
|
||||
content='user_prompts',
|
||||
content_rowid='id'
|
||||
);
|
||||
`),this.db.exec(`
|
||||
CREATE TRIGGER user_prompts_ai AFTER INSERT ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_ad AFTER DELETE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_au AFTER UPDATE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentSummariesWithSessionInfo(e,s=3){return this.db.prepare(`
|
||||
SELECT
|
||||
sdk_session_id, request, learned, completed, next_steps,
|
||||
prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentObservations(e,s=20){return this.db.prepare(`
|
||||
SELECT type, text, prompt_number, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
s.sdk_session_id,
|
||||
s.status,
|
||||
s.started_at,
|
||||
s.started_at_epoch,
|
||||
s.user_prompt,
|
||||
CASE WHEN sum.sdk_session_id IS NOT NULL THEN 1 ELSE 0 END as has_summary
|
||||
FROM sdk_sessions s
|
||||
LEFT JOIN session_summaries sum ON s.sdk_session_id = sum.sdk_session_id
|
||||
WHERE s.project = ? AND s.sdk_session_id IS NOT NULL
|
||||
GROUP BY s.sdk_session_id
|
||||
ORDER BY s.started_at_epoch DESC
|
||||
LIMIT ?
|
||||
)
|
||||
ORDER BY started_at_epoch ASC
|
||||
`).all(e,s)}getObservationsForSession(e){return this.db.prepare(`
|
||||
SELECT title, subtitle, type, prompt_number
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
ORDER BY created_at_epoch ASC
|
||||
`).all(e)}getSummaryForSession(e){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE sdk_session_id = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 1
|
||||
`).get(e)||null}getFilesForSession(e){let t=this.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(e),r=new Set,o=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(d=>o.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)||null}findActiveSDKSession(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ? AND status = 'active'
|
||||
LIMIT 1
|
||||
`).get(e)||null}findAnySDKSession(e){return this.db.prepare(`
|
||||
SELECT id
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ?
|
||||
LIMIT 1
|
||||
`).get(e)||null}reactivateSession(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'active', user_prompt = ?, worker_port = NULL
|
||||
WHERE id = ?
|
||||
`).run(s,e)}incrementPromptCounter(e){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET prompt_counter = COALESCE(prompt_counter, 0) + 1
|
||||
WHERE id = ?
|
||||
`).run(e),this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,o=r.getTime(),a=this.db.prepare(`
|
||||
INSERT OR IGNORE INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,t,r.toISOString(),o);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1
|
||||
`).get(e).id:a.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(s,e).changes===0?(u.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
`).run(s,e)}getWorkerPort(e){return this.db.prepare(`
|
||||
SELECT worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,o=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO user_prompts
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(e,s,t,r.toISOString(),o).lastInsertRowid}storeObservation(e,s,t,r){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,o.toISOString(),i)}storeSummary(e,s,t,r){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
SELECT id FROM sdk_sessions WHERE sdk_session_id = ?
|
||||
`).get(e)||(this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,e,s,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,o.toISOString(),i)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(s.toISOString(),t,e)}markSessionFailed(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(s.toISOString(),t,e)}cleanupOrphanedSessions(){let e=new Date,s=e.getTime();return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function G(n,e,s){return n==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:n==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:n==="UserPromptSubmit"||n==="PostToolUse"?{continue:!0,suppressOutput:!0}:n==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(n,e,s={}){let t=G(n,e,s);return JSON.stringify(t)}import g from"path";import{existsSync as R}from"fs";import{spawn as W}from"child_process";var B=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),$=`http://127.0.0.1:${B}/health`;async function C(){try{return(await fetch($,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function k(){try{if(await C())return!0;console.error("[claude-mem] Worker not responding, starting...");let n=A(),e=g.join(n,"plugin","scripts","worker-service.cjs");if(!R(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=g.join(n,"ecosystem.config.cjs"),t=g.join(n,"node_modules",".bin","pm2");if(!R(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!R(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=W(t,["start",s],{detached:!0,stdio:"ignore",cwd:n});r.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(i=>setTimeout(i,500)),await C())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(n){return console.error(`[claude-mem] Failed to start worker: ${n.message}`),!1}}async function D(n){if(!n)throw new Error("summaryHook requires input");let{session_id:e}=n;if(!await k())throw new Error("Worker service failed to start or become healthy");let t=new m,r=t.createSDKSession(e,"",""),o=t.getPromptCounter(r);t.close();let i=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);u.dataIn("HOOK","Stop: Requesting summary",{sessionId:r,workerPort:i,promptNumber:o});let a=await fetch(`http://127.0.0.1:${i}/sessions/${r}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:o}),signal:AbortSignal.timeout(2e3)});if(!a.ok){let d=await a.text();throw u.failure("HOOK","Failed to generate summary",{sessionId:r,status:a.status},d),new Error(`Failed to request summary from worker: ${a.status} ${d}`)}u.debug("HOOK","Summary request sent successfully",{sessionId:r}),console.log(v("Stop",!0))}import{stdin as y}from"process";var N="";y.on("data",n=>N+=n);y.on("end",async()=>{let n=N.trim()?JSON.parse(N):void 0;await D(n),process.exit(0)});
|
||||
Executable
+921
File diff suppressed because one or more lines are too long
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Build script for claude-mem hooks
|
||||
* Bundles TypeScript hooks into individual standalone executables using esbuild
|
||||
*/
|
||||
|
||||
import { build } from 'esbuild';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const HOOKS = [
|
||||
{ name: 'context-hook', source: 'src/bin/hooks/context-hook.ts' },
|
||||
{ name: 'new-hook', source: 'src/bin/hooks/new-hook.ts' },
|
||||
{ name: 'save-hook', source: 'src/bin/hooks/save-hook.ts' },
|
||||
{ name: 'summary-hook', source: 'src/bin/hooks/summary-hook.ts' },
|
||||
{ name: 'cleanup-hook', source: 'src/bin/hooks/cleanup-hook.ts' }
|
||||
];
|
||||
|
||||
const WORKER_SERVICE = {
|
||||
name: 'worker-service',
|
||||
source: 'src/services/worker-service.ts'
|
||||
};
|
||||
|
||||
const SEARCH_SERVER = {
|
||||
name: 'search-server',
|
||||
source: 'src/servers/search-server.ts'
|
||||
};
|
||||
|
||||
async function buildHooks() {
|
||||
console.log('🔨 Building claude-mem hooks, worker service, and search server...\n');
|
||||
|
||||
try {
|
||||
// Read version from package.json
|
||||
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
|
||||
const version = packageJson.version;
|
||||
console.log(`📌 Version: ${version}`);
|
||||
|
||||
// Create output directory
|
||||
console.log('\n📦 Preparing output directory...');
|
||||
const hooksDir = 'plugin/scripts';
|
||||
|
||||
if (!fs.existsSync(hooksDir)) {
|
||||
fs.mkdirSync(hooksDir, { recursive: true });
|
||||
}
|
||||
console.log('✓ Output directory ready');
|
||||
|
||||
// Build worker service
|
||||
console.log(`\n🔧 Building worker service...`);
|
||||
await build({
|
||||
entryPoints: [WORKER_SERVICE.source],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
format: 'cjs',
|
||||
outfile: `${hooksDir}/${WORKER_SERVICE.name}.cjs`,
|
||||
minify: true,
|
||||
external: ['better-sqlite3'],
|
||||
define: {
|
||||
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
||||
},
|
||||
banner: {
|
||||
js: '#!/usr/bin/env node'
|
||||
}
|
||||
});
|
||||
|
||||
// Make worker service executable
|
||||
fs.chmodSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`, 0o755);
|
||||
const workerStats = fs.statSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`);
|
||||
console.log(`✓ worker-service built (${(workerStats.size / 1024).toFixed(2)} KB)`);
|
||||
|
||||
// Build each hook
|
||||
for (const hook of HOOKS) {
|
||||
console.log(`\n🔧 Building ${hook.name}...`);
|
||||
|
||||
const outfile = `${hooksDir}/${hook.name}.js`;
|
||||
|
||||
await build({
|
||||
entryPoints: [hook.source],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
format: 'esm',
|
||||
outfile,
|
||||
minify: true,
|
||||
external: ['better-sqlite3'],
|
||||
define: {
|
||||
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
||||
},
|
||||
banner: {
|
||||
js: '#!/usr/bin/env node'
|
||||
}
|
||||
});
|
||||
|
||||
// Make executable
|
||||
fs.chmodSync(outfile, 0o755);
|
||||
|
||||
// Check file size
|
||||
const stats = fs.statSync(outfile);
|
||||
const sizeInKB = (stats.size / 1024).toFixed(2);
|
||||
console.log(`✓ ${hook.name} built (${sizeInKB} KB)`);
|
||||
}
|
||||
|
||||
// Build search server
|
||||
console.log(`\n🔧 Building search server...`);
|
||||
await build({
|
||||
entryPoints: [SEARCH_SERVER.source],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
format: 'esm',
|
||||
outfile: `${hooksDir}/${SEARCH_SERVER.name}.js`,
|
||||
minify: true,
|
||||
external: ['better-sqlite3'],
|
||||
define: {
|
||||
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
||||
},
|
||||
banner: {
|
||||
js: '#!/usr/bin/env node'
|
||||
}
|
||||
});
|
||||
|
||||
// Make search server executable
|
||||
fs.chmodSync(`${hooksDir}/${SEARCH_SERVER.name}.js`, 0o755);
|
||||
const searchStats = fs.statSync(`${hooksDir}/${SEARCH_SERVER.name}.js`);
|
||||
console.log(`✓ search-server built (${(searchStats.size / 1024).toFixed(2)} KB)`);
|
||||
|
||||
console.log('\n✅ All hooks, worker service, and search server built successfully!');
|
||||
console.log(` Output: ${hooksDir}/`);
|
||||
console.log(` - Hooks: *-hook.js`);
|
||||
console.log(` - Worker: worker-service.cjs`);
|
||||
console.log(` - Search: search-server.js`);
|
||||
console.log('\n💡 Note: Dependencies will be auto-installed on first hook execution');
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Build failed:', error.message);
|
||||
if (error.errors) {
|
||||
console.error('\nBuild errors:');
|
||||
error.errors.forEach(err => console.error(` - ${err.text}`));
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
buildHooks();
|
||||
@@ -0,0 +1,82 @@
|
||||
# XML Extraction Scripts
|
||||
|
||||
Scripts to extract XML observations and summaries from Claude Code transcript files.
|
||||
|
||||
## Scripts
|
||||
|
||||
### `filter-actual-xml.py`
|
||||
**Recommended for import**
|
||||
|
||||
Extracts only actual XML from assistant responses, filtering out:
|
||||
- Template/example XML (with placeholders like `[...]` or `**field**:`)
|
||||
- XML from tool_use blocks
|
||||
- XML from user messages
|
||||
|
||||
**Output:** `~/Scripts/claude-mem/actual_xml_only_with_timestamps.xml`
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
python3 scripts/extraction/filter-actual-xml.py
|
||||
```
|
||||
|
||||
### `extract-all-xml.py`
|
||||
**For debugging/analysis**
|
||||
|
||||
Extracts ALL XML blocks from transcripts without filtering.
|
||||
|
||||
**Output:** `~/Scripts/claude-mem/all_xml_fragments_with_timestamps.xml`
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
python3 scripts/extraction/extract-all-xml.py
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Extract XML from transcripts:**
|
||||
```bash
|
||||
cd ~/Scripts/claude-mem
|
||||
python3 scripts/extraction/filter-actual-xml.py
|
||||
```
|
||||
|
||||
2. **Import to database:**
|
||||
```bash
|
||||
npm run import:xml
|
||||
```
|
||||
|
||||
3. **Clean up duplicates (if needed):**
|
||||
```bash
|
||||
npm run cleanup:duplicates
|
||||
```
|
||||
|
||||
## Source Data
|
||||
|
||||
Scripts read from: `~/.claude/projects/-Users-alexnewman-Scripts-claude-mem/*.jsonl`
|
||||
|
||||
These are Claude Code session transcripts stored in JSONL (JSON Lines) format.
|
||||
|
||||
## Output Format
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<transcript_extracts>
|
||||
|
||||
<!-- Block 1 | 2025-10-19 03:03:23 UTC -->
|
||||
<observation>
|
||||
<type>discovery</type>
|
||||
<title>Example observation</title>
|
||||
...
|
||||
</observation>
|
||||
|
||||
<!-- Block 2 | 2025-10-19 03:03:45 UTC -->
|
||||
<summary>
|
||||
<request>What was accomplished</request>
|
||||
...
|
||||
</summary>
|
||||
|
||||
</transcript_extracts>
|
||||
```
|
||||
|
||||
Each XML block includes a comment with:
|
||||
- Block number
|
||||
- Original timestamp from transcript
|
||||
Executable
+128
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
def extract_xml_blocks(text):
|
||||
"""Extract complete XML blocks from text"""
|
||||
xml_patterns = [
|
||||
r'<observation>.*?</observation>',
|
||||
r'<session_summary>.*?</session_summary>',
|
||||
r'<request>.*?</request>',
|
||||
r'<summary>.*?</summary>',
|
||||
r'<facts>.*?</facts>',
|
||||
r'<fact>.*?</fact>',
|
||||
r'<concepts>.*?</concepts>',
|
||||
r'<concept>.*?</concept>',
|
||||
r'<files>.*?</files>',
|
||||
r'<file>.*?</file>',
|
||||
r'<files_read>.*?</files_read>',
|
||||
r'<files_edited>.*?</files_edited>',
|
||||
r'<files_modified>.*?</files_modified>',
|
||||
r'<narrative>.*?</narrative>',
|
||||
r'<learned>.*?</learned>',
|
||||
r'<investigated>.*?</investigated>',
|
||||
r'<completed>.*?</completed>',
|
||||
r'<next_steps>.*?</next_steps>',
|
||||
r'<notes>.*?</notes>',
|
||||
r'<title>.*?</title>',
|
||||
r'<subtitle>.*?</subtitle>',
|
||||
r'<text>.*?</text>',
|
||||
r'<type>.*?</type>',
|
||||
]
|
||||
|
||||
blocks = []
|
||||
for pattern in xml_patterns:
|
||||
matches = re.findall(pattern, text, re.DOTALL)
|
||||
blocks.extend(matches)
|
||||
|
||||
return blocks
|
||||
|
||||
def process_transcript_file(filepath):
|
||||
"""Process a single transcript file and extract XML with timestamps"""
|
||||
results = []
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
for line in f:
|
||||
try:
|
||||
data = json.loads(line)
|
||||
|
||||
# Get timestamp
|
||||
timestamp = data.get('timestamp', 'unknown')
|
||||
|
||||
# Extract text content from message
|
||||
message = data.get('message', {})
|
||||
content = message.get('content', [])
|
||||
|
||||
if isinstance(content, list):
|
||||
for item in content:
|
||||
if isinstance(item, dict):
|
||||
text = ''
|
||||
if item.get('type') == 'text':
|
||||
text = item.get('text', '')
|
||||
elif item.get('type') == 'tool_use':
|
||||
# Also check tool_use input fields
|
||||
tool_input = item.get('input', {})
|
||||
if isinstance(tool_input, dict):
|
||||
text = str(tool_input)
|
||||
|
||||
if text:
|
||||
# Extract XML blocks
|
||||
xml_blocks = extract_xml_blocks(text)
|
||||
|
||||
for block in xml_blocks:
|
||||
results.append({
|
||||
'timestamp': timestamp,
|
||||
'xml': block
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
return results
|
||||
|
||||
# Get list of transcript files
|
||||
transcript_dir = os.path.expanduser('~/.claude/projects/-Users-alexnewman-Scripts-claude-mem/')
|
||||
os.chdir(transcript_dir)
|
||||
|
||||
# Get all transcript files sorted by modification time
|
||||
result = subprocess.run(['ls', '-t'], capture_output=True, text=True)
|
||||
files = [f for f in result.stdout.strip().split('\n') if f.endswith('.jsonl')][:62]
|
||||
|
||||
all_results = []
|
||||
for filename in files:
|
||||
filepath = os.path.join(transcript_dir, filename)
|
||||
print(f"Processing {filename}...")
|
||||
results = process_transcript_file(filepath)
|
||||
all_results.extend(results)
|
||||
print(f" Found {len(results)} XML blocks")
|
||||
|
||||
# Write results with timestamps
|
||||
output_file = os.path.expanduser('~/Scripts/claude-mem/all_xml_fragments_with_timestamps.xml')
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
|
||||
f.write('<transcript_extracts>\n\n')
|
||||
|
||||
for i, item in enumerate(all_results, 1):
|
||||
timestamp = item['timestamp']
|
||||
xml = item['xml']
|
||||
|
||||
# Format timestamp nicely if it's ISO format
|
||||
if timestamp != 'unknown' and timestamp:
|
||||
try:
|
||||
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
||||
formatted_time = dt.strftime('%Y-%m-%d %H:%M:%S UTC')
|
||||
except:
|
||||
formatted_time = timestamp
|
||||
else:
|
||||
formatted_time = 'unknown'
|
||||
|
||||
f.write(f'<!-- Block {i} | {formatted_time} -->\n')
|
||||
f.write(xml)
|
||||
f.write('\n\n')
|
||||
|
||||
f.write('</transcript_extracts>\n')
|
||||
|
||||
print(f"\nExtracted {len(all_results)} XML blocks with timestamps to {output_file}")
|
||||
Executable
+168
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
def extract_xml_blocks(text):
|
||||
"""Extract complete XML blocks from text"""
|
||||
xml_patterns = [
|
||||
r'<observation>.*?</observation>',
|
||||
r'<session_summary>.*?</session_summary>',
|
||||
r'<request>.*?</request>',
|
||||
r'<summary>.*?</summary>',
|
||||
r'<facts>.*?</facts>',
|
||||
r'<fact>.*?</fact>',
|
||||
r'<concepts>.*?</concepts>',
|
||||
r'<concept>.*?</concept>',
|
||||
r'<files>.*?</files>',
|
||||
r'<file>.*?</file>',
|
||||
r'<files_read>.*?</files_read>',
|
||||
r'<files_edited>.*?</files_edited>',
|
||||
r'<files_modified>.*?</files_modified>',
|
||||
r'<narrative>.*?</narrative>',
|
||||
r'<learned>.*?</learned>',
|
||||
r'<investigated>.*?</investigated>',
|
||||
r'<completed>.*?</completed>',
|
||||
r'<next_steps>.*?</next_steps>',
|
||||
r'<notes>.*?</notes>',
|
||||
r'<title>.*?</title>',
|
||||
r'<subtitle>.*?</subtitle>',
|
||||
r'<text>.*?</text>',
|
||||
r'<type>.*?</type>',
|
||||
r'<tool_used>.*?</tool_used>',
|
||||
r'<tool_name>.*?</tool_name>',
|
||||
r'<tool_input>.*?</tool_input>',
|
||||
r'<tool_output>.*?</tool_output>',
|
||||
r'<tool_time>.*?</tool_time>',
|
||||
]
|
||||
|
||||
blocks = []
|
||||
for pattern in xml_patterns:
|
||||
matches = re.findall(pattern, text, re.DOTALL)
|
||||
blocks.extend(matches)
|
||||
|
||||
return blocks
|
||||
|
||||
def is_example_xml(xml_block):
|
||||
"""Check if XML block is an example/template"""
|
||||
# Patterns that indicate this is example/template XML
|
||||
example_indicators = [
|
||||
r'\[.*?\]', # Square brackets with placeholders
|
||||
r'\*\*\w+\*\*:', # Bold markdown like **title**:
|
||||
r'\.\.\..*?\.\.\.', # Ellipsis indicating placeholder
|
||||
r'feature\|bugfix\|refactor', # Multiple options separated by |
|
||||
r'change \| discovery \| decision', # Example types
|
||||
r'\{.*?\}', # Curly braces (template variables)
|
||||
r'Concise, self-contained statement', # Literal example text
|
||||
r'Short title capturing',
|
||||
r'One sentence explanation',
|
||||
r'What was the user trying',
|
||||
r'What code/systems did you explore',
|
||||
r'What did you learn',
|
||||
r'What was done',
|
||||
r'What should happen next',
|
||||
r'file1\.ts', # Example filenames
|
||||
r'file2\.ts',
|
||||
r'file3\.ts',
|
||||
r'Any additional context',
|
||||
]
|
||||
|
||||
for pattern in example_indicators:
|
||||
if re.search(pattern, xml_block):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def process_transcript_file(filepath):
|
||||
"""Process a single transcript file and extract only real XML from assistant responses"""
|
||||
results = []
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
for line in f:
|
||||
try:
|
||||
data = json.loads(line)
|
||||
|
||||
# Get timestamp
|
||||
timestamp = data.get('timestamp', 'unknown')
|
||||
|
||||
# Only process assistant messages
|
||||
message = data.get('message', {})
|
||||
role = message.get('role')
|
||||
|
||||
if role != 'assistant':
|
||||
continue
|
||||
|
||||
content = message.get('content', [])
|
||||
|
||||
if isinstance(content, list):
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get('type') == 'text':
|
||||
# This is text in an assistant response, not tool_use
|
||||
text = item.get('text', '')
|
||||
|
||||
# Extract XML blocks
|
||||
xml_blocks = extract_xml_blocks(text)
|
||||
|
||||
for block in xml_blocks:
|
||||
# Filter out example/template XML
|
||||
if not is_example_xml(block):
|
||||
results.append({
|
||||
'timestamp': timestamp,
|
||||
'xml': block
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
return results
|
||||
|
||||
# Get list of Oct 18 transcript files
|
||||
import subprocess
|
||||
|
||||
transcript_dir = os.path.expanduser('~/.claude/projects/-Users-alexnewman-Scripts-claude-mem/')
|
||||
os.chdir(transcript_dir)
|
||||
|
||||
# Get all transcript files sorted by modification time
|
||||
result = subprocess.run(['ls', '-t'], capture_output=True, text=True)
|
||||
files = [f for f in result.stdout.strip().split('\n') if f.endswith('.jsonl')][:62]
|
||||
|
||||
all_results = []
|
||||
for filename in files:
|
||||
filepath = os.path.join(transcript_dir, filename)
|
||||
print(f"Processing {filename}...")
|
||||
results = process_transcript_file(filepath)
|
||||
all_results.extend(results)
|
||||
print(f" Found {len(results)} actual XML blocks")
|
||||
|
||||
# Write results with timestamps
|
||||
output_file = os.path.expanduser('~/Scripts/claude-mem/actual_xml_only_with_timestamps.xml')
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
|
||||
f.write('<!-- Actual XML blocks from assistant responses only -->\n')
|
||||
f.write('<!-- Excludes: tool_use inputs, user prompts, and example/template XML -->\n')
|
||||
f.write('<transcript_extracts>\n\n')
|
||||
|
||||
for i, item in enumerate(all_results, 1):
|
||||
timestamp = item['timestamp']
|
||||
xml = item['xml']
|
||||
|
||||
# Format timestamp nicely if it's ISO format
|
||||
if timestamp != 'unknown' and timestamp:
|
||||
try:
|
||||
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
||||
formatted_time = dt.strftime('%Y-%m-%d %H:%M:%S UTC')
|
||||
except:
|
||||
formatted_time = timestamp
|
||||
else:
|
||||
formatted_time = 'unknown'
|
||||
|
||||
f.write(f'<!-- Block {i} | {formatted_time} -->\n')
|
||||
f.write(xml)
|
||||
f.write('\n\n')
|
||||
|
||||
f.write('</transcript_extracts>\n')
|
||||
|
||||
print(f"\n{'='*80}")
|
||||
print(f"Extracted {len(all_results)} actual XML blocks (filtered) to {output_file}")
|
||||
print(f"{'='*80}")
|
||||
Executable
+171
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Release script for claude-mem
|
||||
* Handles version bumping, building, and creating marketplace releases
|
||||
*/
|
||||
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import fs from 'fs';
|
||||
import readline from 'readline';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
const question = (query) => new Promise((resolve) => rl.question(query, resolve));
|
||||
|
||||
async function publish() {
|
||||
try {
|
||||
console.log('📦 Claude-mem Marketplace Release Tool\n');
|
||||
|
||||
// Check git status
|
||||
console.log('🔍 Checking git status...');
|
||||
const { stdout: gitStatus } = await execAsync('git status --porcelain');
|
||||
if (gitStatus.trim()) {
|
||||
console.log('⚠️ Uncommitted changes detected:');
|
||||
console.log(gitStatus);
|
||||
const proceed = await question('\nContinue anyway? (y/N) ');
|
||||
if (proceed.toLowerCase() !== 'y') {
|
||||
console.log('Aborted.');
|
||||
rl.close();
|
||||
process.exit(0);
|
||||
}
|
||||
} else {
|
||||
console.log('✓ Working directory clean');
|
||||
}
|
||||
|
||||
// Get current version
|
||||
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
|
||||
const currentVersion = packageJson.version;
|
||||
console.log(`\n📌 Current version: ${currentVersion}`);
|
||||
|
||||
// Ask for version bump type
|
||||
console.log('\nVersion bump type:');
|
||||
console.log(' 1. patch (x.x.X) - Bug fixes');
|
||||
console.log(' 2. minor (x.X.0) - New features');
|
||||
console.log(' 3. major (X.0.0) - Breaking changes');
|
||||
console.log(' 4. custom - Enter version manually');
|
||||
|
||||
const bumpType = await question('\nSelect bump type (1-4): ');
|
||||
let newVersion;
|
||||
|
||||
switch (bumpType.trim()) {
|
||||
case '1':
|
||||
newVersion = bumpVersion(currentVersion, 'patch');
|
||||
break;
|
||||
case '2':
|
||||
newVersion = bumpVersion(currentVersion, 'minor');
|
||||
break;
|
||||
case '3':
|
||||
newVersion = bumpVersion(currentVersion, 'major');
|
||||
break;
|
||||
case '4':
|
||||
newVersion = await question('Enter version: ');
|
||||
if (!isValidVersion(newVersion)) {
|
||||
throw new Error('Invalid version format. Use semver (e.g., 1.2.3)');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid selection');
|
||||
}
|
||||
|
||||
console.log(`\n🎯 New version: ${newVersion}`);
|
||||
const confirm = await question('\nProceed with publish? (y/N) ');
|
||||
if (confirm.toLowerCase() !== 'y') {
|
||||
console.log('Aborted.');
|
||||
rl.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Update package.json and marketplace.json versions
|
||||
console.log('\n📝 Updating package.json and marketplace.json...');
|
||||
packageJson.version = newVersion;
|
||||
fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2) + '\n');
|
||||
|
||||
const marketplaceJson = JSON.parse(fs.readFileSync('.claude-plugin/marketplace.json', 'utf-8'));
|
||||
marketplaceJson.plugins[0].version = newVersion;
|
||||
fs.writeFileSync('.claude-plugin/marketplace.json', JSON.stringify(marketplaceJson, null, 2) + '\n');
|
||||
console.log('✓ Versions updated in both files');
|
||||
|
||||
// Run build
|
||||
console.log('\n🔨 Building hooks...');
|
||||
await execAsync('npm run build');
|
||||
console.log('✓ Build complete');
|
||||
|
||||
// Run tests if they exist
|
||||
if (packageJson.scripts?.test) {
|
||||
console.log('\n🧪 Running tests...');
|
||||
try {
|
||||
await execAsync('npm test');
|
||||
console.log('✓ Tests passed');
|
||||
} catch (error) {
|
||||
console.error('❌ Tests failed:', error.message);
|
||||
const continueAnyway = await question('\nPublish anyway? (y/N) ');
|
||||
if (continueAnyway.toLowerCase() !== 'y') {
|
||||
console.log('Aborted.');
|
||||
rl.close();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Git commit and tag
|
||||
console.log('\n📌 Creating git commit and tag...');
|
||||
await execAsync('git add package.json .claude-plugin/marketplace.json plugin/');
|
||||
await execAsync(`git commit -m "chore: Release v${newVersion}
|
||||
|
||||
Marketplace release for Claude Code plugin
|
||||
https://github.com/thedotmack/claude-mem"`);
|
||||
await execAsync(`git tag v${newVersion}`);
|
||||
console.log(`✓ Created commit and tag v${newVersion}`);
|
||||
|
||||
// Push to git
|
||||
console.log('\n⬆️ Pushing to git...');
|
||||
await execAsync('git push');
|
||||
await execAsync('git push --tags');
|
||||
console.log('✓ Pushed to git');
|
||||
|
||||
console.log(`\n✅ Successfully released v${newVersion}! 🎉`);
|
||||
console.log(`\n🏷️ Tag: https://github.com/thedotmack/claude-mem/releases/tag/v${newVersion}`);
|
||||
console.log(`📦 Marketplace will sync from this tag automatically`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Release failed:', error.message);
|
||||
if (error.stderr) {
|
||||
console.error('\nError details:', error.stderr);
|
||||
}
|
||||
process.exit(1);
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
function bumpVersion(version, type) {
|
||||
const parts = version.split('.').map(Number);
|
||||
switch (type) {
|
||||
case 'patch':
|
||||
parts[2]++;
|
||||
break;
|
||||
case 'minor':
|
||||
parts[1]++;
|
||||
parts[2] = 0;
|
||||
break;
|
||||
case 'major':
|
||||
parts[0]++;
|
||||
parts[1] = 0;
|
||||
parts[2] = 0;
|
||||
break;
|
||||
}
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
function isValidVersion(version) {
|
||||
return /^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?$/.test(version);
|
||||
}
|
||||
|
||||
publish();
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Cleanup duplicate observations and summaries from the database
|
||||
* Keeps the earliest entry (MIN(id)) for each duplicate group
|
||||
*/
|
||||
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
|
||||
function main() {
|
||||
console.log('Starting duplicate cleanup...\n');
|
||||
|
||||
const db = new SessionStore();
|
||||
|
||||
// Find and delete duplicate observations
|
||||
console.log('Finding duplicate observations...');
|
||||
|
||||
const duplicateObsQuery = db['db'].prepare(`
|
||||
SELECT sdk_session_id, title, subtitle, type, COUNT(*) as count, GROUP_CONCAT(id) as ids
|
||||
FROM observations
|
||||
GROUP BY sdk_session_id, title, subtitle, type
|
||||
HAVING count > 1
|
||||
`);
|
||||
|
||||
const duplicateObs = duplicateObsQuery.all() as Array<{
|
||||
sdk_session_id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
type: string;
|
||||
count: number;
|
||||
ids: string;
|
||||
}>;
|
||||
|
||||
console.log(`Found ${duplicateObs.length} duplicate observation groups\n`);
|
||||
|
||||
let deletedObs = 0;
|
||||
for (const dup of duplicateObs) {
|
||||
const ids = dup.ids.split(',').map(id => parseInt(id, 10));
|
||||
const keepId = Math.min(...ids);
|
||||
const deleteIds = ids.filter(id => id !== keepId);
|
||||
|
||||
console.log(`Observation "${dup.title.substring(0, 60)}..."`);
|
||||
console.log(` Found ${dup.count} copies, keeping ID ${keepId}, deleting ${deleteIds.length} duplicates`);
|
||||
|
||||
const deleteStmt = db['db'].prepare(`DELETE FROM observations WHERE id IN (${deleteIds.join(',')})`);
|
||||
deleteStmt.run();
|
||||
deletedObs += deleteIds.length;
|
||||
}
|
||||
|
||||
// Find and delete duplicate summaries
|
||||
console.log('\n\nFinding duplicate summaries...');
|
||||
|
||||
const duplicateSumQuery = db['db'].prepare(`
|
||||
SELECT sdk_session_id, request, completed, learned, COUNT(*) as count, GROUP_CONCAT(id) as ids
|
||||
FROM session_summaries
|
||||
GROUP BY sdk_session_id, request, completed, learned
|
||||
HAVING count > 1
|
||||
`);
|
||||
|
||||
const duplicateSum = duplicateSumQuery.all() as Array<{
|
||||
sdk_session_id: string;
|
||||
request: string;
|
||||
completed: string;
|
||||
learned: string;
|
||||
count: number;
|
||||
ids: string;
|
||||
}>;
|
||||
|
||||
console.log(`Found ${duplicateSum.length} duplicate summary groups\n`);
|
||||
|
||||
let deletedSum = 0;
|
||||
for (const dup of duplicateSum) {
|
||||
const ids = dup.ids.split(',').map(id => parseInt(id, 10));
|
||||
const keepId = Math.min(...ids);
|
||||
const deleteIds = ids.filter(id => id !== keepId);
|
||||
|
||||
console.log(`Summary "${dup.request.substring(0, 60)}..."`);
|
||||
console.log(` Found ${dup.count} copies, keeping ID ${keepId}, deleting ${deleteIds.length} duplicates`);
|
||||
|
||||
const deleteStmt = db['db'].prepare(`DELETE FROM session_summaries WHERE id IN (${deleteIds.join(',')})`);
|
||||
deleteStmt.run();
|
||||
deletedSum += deleteIds.length;
|
||||
}
|
||||
|
||||
db.close();
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('Cleanup Complete!');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`🗑️ Deleted: ${deletedObs} duplicate observations`);
|
||||
console.log(`🗑️ Deleted: ${deletedSum} duplicate summaries`);
|
||||
console.log(`🗑️ Total: ${deletedObs + deletedSum} duplicates removed`);
|
||||
console.log('='.repeat(60));
|
||||
}
|
||||
|
||||
// Run if executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main();
|
||||
}
|
||||
-248
@@ -1,248 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// <Block> 1.1 ====================================
|
||||
// CLI Dependencies and Imports Setup
|
||||
// Natural pattern: Import what you need before using it
|
||||
import { Command } from 'commander';
|
||||
import { PACKAGE_NAME, PACKAGE_VERSION, PACKAGE_DESCRIPTION } from '../shared/config.js';
|
||||
|
||||
// Import command handlers
|
||||
import { compress } from '../commands/compress.js';
|
||||
import { install } from '../commands/install.js';
|
||||
import { uninstall } from '../commands/uninstall.js';
|
||||
import { status } from '../commands/status.js';
|
||||
import { logs } from '../commands/logs.js';
|
||||
import { loadContext } from '../commands/load-context.js';
|
||||
import { trash } from '../commands/trash.js';
|
||||
import { restore } from '../commands/restore.js';
|
||||
import { save } from '../commands/save.js';
|
||||
import { changelog } from '../commands/changelog.js';
|
||||
// Cloud functionality disabled - incomplete setup
|
||||
// import { cloudCommand } from '../commands/cloud.js';
|
||||
import { importHistory } from '../commands/import-history.js';
|
||||
import { TranscriptCompressor } from '../core/compression/TranscriptCompressor.js';
|
||||
|
||||
const program = new Command();
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.2 ====================================
|
||||
// Program Configuration
|
||||
// Natural pattern: Configure program metadata first
|
||||
program
|
||||
.name(PACKAGE_NAME)
|
||||
.description(PACKAGE_DESCRIPTION)
|
||||
.version(PACKAGE_VERSION);
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.3 ====================================
|
||||
// Compress Command Definition
|
||||
// Natural pattern: Define command with its options and handler
|
||||
// Compress command
|
||||
program
|
||||
.command('compress [transcript]')
|
||||
.description('Compress a Claude Code transcript into memory')
|
||||
.option('--output <path>', 'Output directory for compressed files')
|
||||
.option('--dry-run', 'Show what would be compressed without doing it')
|
||||
.option('-v, --verbose', 'Show detailed output')
|
||||
.action(compress);
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.4 ====================================
|
||||
// Install Command Definition
|
||||
// Natural pattern: Define command with its options and handler
|
||||
// Install command
|
||||
program
|
||||
.command('install')
|
||||
.description('Install Claude Code hooks for automatic compression')
|
||||
.option('--user', 'Install for current user (default)')
|
||||
.option('--project', 'Install for current project only')
|
||||
.option('--local', 'Install to custom local directory')
|
||||
.option('--path <path>', 'Custom installation path (with --local)')
|
||||
.option('--timeout <ms>', 'Hook execution timeout in milliseconds', '180000')
|
||||
.option('--skip-mcp', 'Skip Chroma MCP server installation')
|
||||
.option('--force', 'Force installation even if already installed')
|
||||
.action(install);
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.5 ====================================
|
||||
// Uninstall Command Definition
|
||||
// Natural pattern: Define command with its options and handler
|
||||
// Uninstall command
|
||||
program
|
||||
.command('uninstall')
|
||||
.description('Remove Claude Code hooks')
|
||||
.option('--user', 'Remove from user settings (default)')
|
||||
.option('--project', 'Remove from project settings')
|
||||
.option('--all', 'Remove from both user and project settings')
|
||||
.action(uninstall);
|
||||
// </Block> =======================================
|
||||
|
||||
|
||||
// <Block> 1.6 ====================================
|
||||
// Status Command Definition
|
||||
// Natural pattern: Define command with its handler
|
||||
// Status command
|
||||
program
|
||||
.command('status')
|
||||
.description('Check installation status of Claude Memory System')
|
||||
.action(status);
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.7 ====================================
|
||||
// Logs Command Definition
|
||||
// Natural pattern: Define command with its options and handler
|
||||
// Logs command
|
||||
program
|
||||
.command('logs')
|
||||
.description('View claude-mem operation logs')
|
||||
.option('--debug', 'Show debug logs only')
|
||||
.option('--error', 'Show error logs only')
|
||||
.option('--tail [n]', 'Show last n lines', '50')
|
||||
.option('--follow', 'Follow log output')
|
||||
.action(logs);
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.8 ====================================
|
||||
// Load-Context Command Definition
|
||||
// Natural pattern: Define command with its options and handler
|
||||
// Load-context command
|
||||
program
|
||||
.command('load-context')
|
||||
.description('Load compressed memories for current session')
|
||||
.option('--project <name>', 'Filter by project name')
|
||||
.option('--count <n>', 'Number of memories to load', '10')
|
||||
.option('--raw', 'Output raw JSON instead of formatted text')
|
||||
.option('--format <type>', 'Output format: json, session-start, or default')
|
||||
.action(loadContext);
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.9 ====================================
|
||||
// Trash and Restore Commands Definition
|
||||
// Natural pattern: Define commands for safe file operations
|
||||
|
||||
// Trash command with subcommands
|
||||
const trashCmd = program
|
||||
.command('trash')
|
||||
.description('Manage trash bin for safe file deletion')
|
||||
.argument('[files...]', 'Files to move to trash')
|
||||
.option('-r, --recursive', 'Remove directories recursively')
|
||||
.option('-R', 'Remove directories recursively (same as -r)')
|
||||
.option('-f, --force', 'Suppress errors for nonexistent files')
|
||||
.action(async (files: string[] | undefined, options: any) => {
|
||||
// If no files provided, show help
|
||||
if (!files || files.length === 0) {
|
||||
trashCmd.outputHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
// Map -R to recursive
|
||||
if (options.R) options.recursive = true;
|
||||
|
||||
await trash(files, {
|
||||
force: options.force,
|
||||
recursive: options.recursive
|
||||
});
|
||||
});
|
||||
|
||||
// Trash view subcommand
|
||||
trashCmd
|
||||
.command('view')
|
||||
.description('View contents of trash bin')
|
||||
.action(async () => {
|
||||
const { viewTrash } = await import('../commands/trash-view.js');
|
||||
await viewTrash();
|
||||
});
|
||||
|
||||
// Trash empty subcommand
|
||||
trashCmd
|
||||
.command('empty')
|
||||
.description('Permanently delete all files in trash')
|
||||
.option('-f, --force', 'Skip confirmation prompt')
|
||||
.action(async (options: any) => {
|
||||
const { emptyTrash } = await import('../commands/trash-empty.js');
|
||||
await emptyTrash(options);
|
||||
});
|
||||
|
||||
// Restore command
|
||||
program
|
||||
.command('restore')
|
||||
.description('Restore files from trash interactively')
|
||||
.action(restore);
|
||||
// </Block> =======================================
|
||||
|
||||
// Cloud command
|
||||
// Cloud functionality disabled - incomplete setup
|
||||
// program.addCommand(cloudCommand);
|
||||
|
||||
// Save command
|
||||
program
|
||||
.command('save <message>')
|
||||
.description('Save a message to the memory system')
|
||||
.action(save);
|
||||
|
||||
// Changelog command
|
||||
program
|
||||
.command('changelog')
|
||||
.description('Generate CHANGELOG.md from claude-mem memories')
|
||||
.option('--historical <n>', 'Number of versions to search (default: current version only)')
|
||||
.option('--generate <version>', 'Generate changelog for a specific version')
|
||||
.option('--start <time>', 'Start time for memory search (ISO format)')
|
||||
.option('--end <time>', 'End time for memory search (ISO format)')
|
||||
.option('--update', 'Update CHANGELOG.md from JSONL entries')
|
||||
.option('--preview', 'Preview the generated changelog')
|
||||
.option('-v, --verbose', 'Show detailed output')
|
||||
.action(changelog);
|
||||
|
||||
// Import History command
|
||||
program
|
||||
.command('import-history')
|
||||
.description('Import historical Claude Code conversations into memory')
|
||||
.option('-v, --verbose', 'Show detailed output')
|
||||
.option('-m, --multi', 'Enable multi-select mode (default is single-select)')
|
||||
.action(importHistory);
|
||||
|
||||
// <Block> 1.11 ===================================
|
||||
// Hook Commands
|
||||
// Internal commands called by hook scripts
|
||||
program
|
||||
.command('hook:pre-compact', { hidden: true })
|
||||
.description('Internal pre-compact hook handler')
|
||||
.action(async () => {
|
||||
const { preCompactHook } = await import('../commands/hooks.js');
|
||||
await preCompactHook();
|
||||
});
|
||||
|
||||
program
|
||||
.command('hook:session-start', { hidden: true })
|
||||
.description('Internal session-start hook handler')
|
||||
.action(async () => {
|
||||
const { sessionStartHook } = await import('../commands/hooks.js');
|
||||
await sessionStartHook();
|
||||
});
|
||||
|
||||
program
|
||||
.command('hook:session-end', { hidden: true })
|
||||
.description('Internal session-end hook handler')
|
||||
.action(async () => {
|
||||
const { sessionEndHook } = await import('../commands/hooks.js');
|
||||
await sessionEndHook();
|
||||
});
|
||||
|
||||
// </Block> =======================================
|
||||
|
||||
// Debug command to show filtered output
|
||||
program
|
||||
.command('debug-filter')
|
||||
.description('Show filtered transcript output (first 5 messages)')
|
||||
.argument('<transcript-path>', 'Path to transcript file')
|
||||
.action((transcriptPath) => {
|
||||
const compressor = new TranscriptCompressor();
|
||||
compressor.showFilteredOutput(transcriptPath);
|
||||
});
|
||||
|
||||
// <Block> 1.11 ===================================
|
||||
// CLI Execution
|
||||
// Natural pattern: After defining all commands, parse and execute
|
||||
// Parse arguments and execute
|
||||
program.parse();
|
||||
// </Block> =======================================
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
/**
|
||||
* Cleanup Hook Entry Point - SessionEnd
|
||||
* Standalone executable for plugin hooks
|
||||
*/
|
||||
|
||||
import { cleanupHook } from '../../hooks/cleanup.js';
|
||||
import { stdin } from 'process';
|
||||
|
||||
// Read input from stdin
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', async () => {
|
||||
try {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
await cleanupHook(parsed);
|
||||
} catch (error: any) {
|
||||
console.error(`[claude-mem cleanup-hook error: ${error.message}]`);
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
/**
|
||||
* Context Hook Entry Point - SessionStart
|
||||
* Standalone executable for plugin hooks
|
||||
*/
|
||||
|
||||
import { contextHook } from '../../hooks/context.js';
|
||||
import { stdin } from 'process';
|
||||
|
||||
try {
|
||||
// Check for --index flag
|
||||
const useIndexView = process.argv.includes('--index');
|
||||
|
||||
if (stdin.isTTY) {
|
||||
// Running manually from terminal - print formatted output with colors
|
||||
const contextOutput = contextHook(undefined, true, useIndexView);
|
||||
console.log(contextOutput);
|
||||
process.exit(0);
|
||||
} else {
|
||||
// Running from hook - wrap in JSON format without colors
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', () => {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
const contextOutput = contextHook(parsed, false, useIndexView);
|
||||
const result = {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "SessionStart",
|
||||
additionalContext: contextOutput
|
||||
}
|
||||
};
|
||||
console.log(JSON.stringify(result));
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`[claude-mem context-hook error: ${error.message}]`);
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
|
||||
/**
|
||||
* New Hook Entry Point - UserPromptSubmit
|
||||
* Standalone executable for plugin hooks
|
||||
*/
|
||||
|
||||
import { newHook } from '../../hooks/new.js';
|
||||
import { stdin } from 'process';
|
||||
|
||||
// Read input from stdin
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', async () => {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
await newHook(parsed);
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
|
||||
/**
|
||||
* Save Hook Entry Point - PostToolUse
|
||||
* Standalone executable for plugin hooks
|
||||
*/
|
||||
|
||||
import { saveHook } from '../../hooks/save.js';
|
||||
import { stdin } from 'process';
|
||||
|
||||
// Read input from stdin
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', async () => {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
await saveHook(parsed);
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
|
||||
/**
|
||||
* Summary Hook Entry Point - Stop
|
||||
* Standalone executable for plugin hooks
|
||||
*/
|
||||
|
||||
import { summaryHook } from '../../hooks/summary.js';
|
||||
import { stdin } from 'process';
|
||||
|
||||
// Read input from stdin
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', async () => {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
await summaryHook(parsed);
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Worker Entry Point
|
||||
* Standalone background process for SDK agent
|
||||
*/
|
||||
|
||||
import { main } from '../../sdk/worker.js';
|
||||
|
||||
// Entry point - just call the worker main function
|
||||
main().catch((error) => {
|
||||
console.error('[SDK Worker] Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,382 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Import XML observations back into the database
|
||||
* Parses actual_xml_only_with_timestamps.xml and inserts observations via SessionStore
|
||||
*/
|
||||
|
||||
import { readFileSync, readdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
|
||||
interface ObservationData {
|
||||
type: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
facts: string[];
|
||||
narrative: string;
|
||||
concepts: string[];
|
||||
files_read: string[];
|
||||
files_modified: string[];
|
||||
}
|
||||
|
||||
interface SummaryData {
|
||||
request: string;
|
||||
investigated: string;
|
||||
learned: string;
|
||||
completed: string;
|
||||
next_steps: string;
|
||||
notes: string | null;
|
||||
}
|
||||
|
||||
interface SessionMetadata {
|
||||
sessionId: string;
|
||||
project: string;
|
||||
}
|
||||
|
||||
interface TimestampMapping {
|
||||
[timestamp: string]: SessionMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a map of timestamp (rounded to second) -> session metadata by reading all transcript files
|
||||
* Since XML timestamps are rounded to seconds, we map by second
|
||||
*/
|
||||
function buildTimestampMap(): TimestampMapping {
|
||||
const transcriptDir = join(homedir(), '.claude', 'projects', '-Users-alexnewman-Scripts-claude-mem');
|
||||
const map: TimestampMapping = {};
|
||||
|
||||
console.log(`Reading transcript files from ${transcriptDir}...`);
|
||||
|
||||
const files = readdirSync(transcriptDir).filter(f => f.endsWith('.jsonl'));
|
||||
console.log(`Found ${files.length} transcript files`);
|
||||
|
||||
for (const filename of files) {
|
||||
const filepath = join(transcriptDir, filename);
|
||||
const content = readFileSync(filepath, 'utf-8');
|
||||
const lines = content.split('\n').filter(l => l.trim());
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
const timestamp = data.timestamp;
|
||||
const sessionId = data.sessionId;
|
||||
const project = data.cwd || '/Users/alexnewman/Scripts/claude-mem';
|
||||
|
||||
if (timestamp && sessionId) {
|
||||
// Round timestamp to second for matching with XML timestamps
|
||||
const roundedTimestamp = new Date(timestamp);
|
||||
roundedTimestamp.setMilliseconds(0);
|
||||
const key = roundedTimestamp.toISOString();
|
||||
|
||||
// Only store first occurrence for each second (they're all the same session anyway)
|
||||
if (!map[key]) {
|
||||
map[key] = { sessionId, project };
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip invalid JSON lines
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Built timestamp map with ${Object.keys(map).length} unique seconds`);
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse XML text content and extract tag value
|
||||
*/
|
||||
function extractTag(xml: string, tagName: string): string {
|
||||
const regex = new RegExp(`<${tagName}>([\\s\\S]*?)</${tagName}>`, 'i');
|
||||
const match = xml.match(regex);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse XML array tags (facts, concepts, files, etc.)
|
||||
*/
|
||||
function extractArrayTags(xml: string, containerTag: string, itemTag: string): string[] {
|
||||
const containerRegex = new RegExp(`<${containerTag}>([\\s\\S]*?)</${containerTag}>`, 'i');
|
||||
const containerMatch = xml.match(containerRegex);
|
||||
|
||||
if (!containerMatch) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const containerContent = containerMatch[1];
|
||||
const itemRegex = new RegExp(`<${itemTag}>([\\s\\S]*?)</${itemTag}>`, 'gi');
|
||||
const items: string[] = [];
|
||||
let match;
|
||||
|
||||
while ((match = itemRegex.exec(containerContent)) !== null) {
|
||||
items.push(match[1].trim());
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an observation block from XML
|
||||
*/
|
||||
function parseObservation(xml: string): ObservationData | null {
|
||||
// Must be a complete observation block
|
||||
if (!xml.includes('<observation>') || !xml.includes('</observation>')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const observation: ObservationData = {
|
||||
type: extractTag(xml, 'type'),
|
||||
title: extractTag(xml, 'title'),
|
||||
subtitle: extractTag(xml, 'subtitle'),
|
||||
facts: extractArrayTags(xml, 'facts', 'fact'),
|
||||
narrative: extractTag(xml, 'narrative'),
|
||||
concepts: extractArrayTags(xml, 'concepts', 'concept'),
|
||||
files_read: extractArrayTags(xml, 'files_read', 'file'),
|
||||
files_modified: extractArrayTags(xml, 'files_modified', 'file'),
|
||||
};
|
||||
|
||||
// Validate required fields
|
||||
if (!observation.type || !observation.title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return observation;
|
||||
} catch (e) {
|
||||
console.error('Error parsing observation:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a summary block from XML
|
||||
*/
|
||||
function parseSummary(xml: string): SummaryData | null {
|
||||
// Must be a complete summary block
|
||||
if (!xml.includes('<summary>') || !xml.includes('</summary>')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const summary: SummaryData = {
|
||||
request: extractTag(xml, 'request'),
|
||||
investigated: extractTag(xml, 'investigated'),
|
||||
learned: extractTag(xml, 'learned'),
|
||||
completed: extractTag(xml, 'completed'),
|
||||
next_steps: extractTag(xml, 'next_steps'),
|
||||
notes: extractTag(xml, 'notes') || null,
|
||||
};
|
||||
|
||||
// Validate required fields
|
||||
if (!summary.request) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return summary;
|
||||
} catch (e) {
|
||||
console.error('Error parsing summary:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract timestamp from XML comment
|
||||
* Format: <!-- Block N | 2025-10-19 03:03:23 UTC -->
|
||||
*/
|
||||
function extractTimestamp(commentLine: string): string | null {
|
||||
const match = commentLine.match(/<!-- Block \d+ \| (.+?) -->/);
|
||||
if (match) {
|
||||
// Convert "2025-10-19 03:03:23 UTC" to ISO format
|
||||
const dateStr = match[1].replace(' UTC', '').replace(' ', 'T') + 'Z';
|
||||
return new Date(dateStr).toISOString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main import function
|
||||
*/
|
||||
function main() {
|
||||
console.log('Starting XML observation import...\n');
|
||||
|
||||
// Build timestamp map
|
||||
const timestampMap = buildTimestampMap();
|
||||
|
||||
// Open database connection
|
||||
const db = new SessionStore();
|
||||
|
||||
// Create SDK sessions for all unique Claude Code sessions
|
||||
console.log('\nCreating SDK sessions for imported data...');
|
||||
const claudeSessionToSdkSession = new Map<string, string>();
|
||||
|
||||
for (const sessionMeta of Object.values(timestampMap)) {
|
||||
if (!claudeSessionToSdkSession.has(sessionMeta.sessionId)) {
|
||||
const syntheticSdkSessionId = `imported-${sessionMeta.sessionId}`;
|
||||
|
||||
// Try to find existing session first
|
||||
const existingQuery = db['db'].prepare(`
|
||||
SELECT sdk_session_id
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ?
|
||||
`);
|
||||
const existing = existingQuery.get(sessionMeta.sessionId) as { sdk_session_id: string | null } | undefined;
|
||||
|
||||
if (existing && existing.sdk_session_id) {
|
||||
// Use existing SDK session ID
|
||||
claudeSessionToSdkSession.set(sessionMeta.sessionId, existing.sdk_session_id);
|
||||
} else if (existing && !existing.sdk_session_id) {
|
||||
// Session exists but sdk_session_id is NULL, update it
|
||||
const dbId = (db['db'].prepare('SELECT id FROM sdk_sessions WHERE claude_session_id = ?').get(sessionMeta.sessionId) as { id: number }).id;
|
||||
db.updateSDKSessionId(dbId, syntheticSdkSessionId);
|
||||
claudeSessionToSdkSession.set(sessionMeta.sessionId, syntheticSdkSessionId);
|
||||
} else {
|
||||
// Create new SDK session
|
||||
const dbId = db.createSDKSession(
|
||||
sessionMeta.sessionId,
|
||||
sessionMeta.project,
|
||||
'Imported from transcript XML'
|
||||
);
|
||||
|
||||
// Update with synthetic SDK session ID
|
||||
db.updateSDKSessionId(dbId, syntheticSdkSessionId);
|
||||
|
||||
claudeSessionToSdkSession.set(sessionMeta.sessionId, syntheticSdkSessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Prepared ${claudeSessionToSdkSession.size} SDK sessions\n`);
|
||||
|
||||
// Read XML file
|
||||
const xmlPath = join(process.cwd(), 'actual_xml_only_with_timestamps.xml');
|
||||
console.log(`Reading XML file: ${xmlPath}`);
|
||||
const xmlContent = readFileSync(xmlPath, 'utf-8');
|
||||
|
||||
// Split into blocks by comment markers
|
||||
const blocks = xmlContent.split(/(?=<!-- Block \d+)/);
|
||||
console.log(`Found ${blocks.length} blocks in XML file\n`);
|
||||
|
||||
let importedObs = 0;
|
||||
let importedSum = 0;
|
||||
let skipped = 0;
|
||||
let duplicateObs = 0;
|
||||
let duplicateSum = 0;
|
||||
let noSession = 0;
|
||||
|
||||
for (const block of blocks) {
|
||||
if (!block.trim() || block.startsWith('<?xml') || block.startsWith('<transcript_extracts')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract timestamp from comment
|
||||
const timestampIso = extractTimestamp(block);
|
||||
if (!timestampIso) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Look up session metadata
|
||||
const sessionMeta = timestampMap[timestampIso];
|
||||
if (!sessionMeta) {
|
||||
noSession++;
|
||||
if (noSession <= 5) {
|
||||
console.log(`⚠️ No session found for timestamp: ${timestampIso}`);
|
||||
}
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get SDK session ID
|
||||
const sdkSessionId = claudeSessionToSdkSession.get(sessionMeta.sessionId);
|
||||
if (!sdkSessionId) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try parsing as observation first
|
||||
const observation = parseObservation(block);
|
||||
if (observation) {
|
||||
// Check for duplicate
|
||||
const existingObs = db['db'].prepare(`
|
||||
SELECT id FROM observations
|
||||
WHERE sdk_session_id = ? AND title = ? AND subtitle = ? AND type = ?
|
||||
`).get(sdkSessionId, observation.title, observation.subtitle, observation.type);
|
||||
|
||||
if (existingObs) {
|
||||
duplicateObs++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
db.storeObservation(
|
||||
sdkSessionId,
|
||||
sessionMeta.project,
|
||||
observation
|
||||
);
|
||||
importedObs++;
|
||||
|
||||
if (importedObs % 50 === 0) {
|
||||
console.log(`Imported ${importedObs} observations...`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error storing observation:`, e);
|
||||
skipped++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try parsing as summary
|
||||
const summary = parseSummary(block);
|
||||
if (summary) {
|
||||
// Check for duplicate
|
||||
const existingSum = db['db'].prepare(`
|
||||
SELECT id FROM session_summaries
|
||||
WHERE sdk_session_id = ? AND request = ? AND completed = ? AND learned = ?
|
||||
`).get(sdkSessionId, summary.request, summary.completed, summary.learned);
|
||||
|
||||
if (existingSum) {
|
||||
duplicateSum++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
db.storeSummary(
|
||||
sdkSessionId,
|
||||
sessionMeta.project,
|
||||
summary
|
||||
);
|
||||
importedSum++;
|
||||
|
||||
if (importedSum % 10 === 0) {
|
||||
console.log(`Imported ${importedSum} summaries...`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error storing summary:`, e);
|
||||
skipped++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Neither observation nor summary - skip
|
||||
skipped++;
|
||||
}
|
||||
|
||||
db.close();
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('Import Complete!');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`✓ Imported: ${importedObs} observations`);
|
||||
console.log(`✓ Imported: ${importedSum} summaries`);
|
||||
console.log(`✓ Total: ${importedObs + importedSum} items`);
|
||||
console.log(`⊘ Skipped: ${skipped} blocks (not full observations or summaries)`);
|
||||
console.log(`⊘ Duplicates skipped: ${duplicateObs} observations, ${duplicateSum} summaries`);
|
||||
console.log(`⚠️ No session: ${noSession} blocks (timestamp not in transcripts)`);
|
||||
console.log('='.repeat(60));
|
||||
}
|
||||
|
||||
// Run if executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main();
|
||||
}
|
||||
@@ -1,718 +0,0 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import { query } from '@anthropic-ai/claude-code';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getClaudePath } from '../shared/settings.js';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
interface ChangelogEntry {
|
||||
version: string;
|
||||
date: string;
|
||||
type: 'Added' | 'Changed' | 'Fixed' | 'Removed' | 'Deprecated' | 'Security';
|
||||
description: string;
|
||||
timestamp: string;
|
||||
generatedAt?: string; // When this changelog entry was created
|
||||
}
|
||||
|
||||
interface MemorySearchResult {
|
||||
version: string;
|
||||
text: string;
|
||||
metadata: any;
|
||||
}
|
||||
|
||||
export async function changelog(options: OptionValues): Promise<void> {
|
||||
try {
|
||||
// Handle --update flag to regenerate CHANGELOG.md from JSONL
|
||||
if (options.update) {
|
||||
await updateChangelogFromJsonl(options);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current version and project name from package.json
|
||||
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||
let currentVersion = 'unknown';
|
||||
let projectName = 'unknown';
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
try {
|
||||
const packageData = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
currentVersion = packageData.version || 'unknown';
|
||||
projectName = packageData.name || path.basename(process.cwd());
|
||||
} catch (e) {
|
||||
projectName = path.basename(process.cwd());
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate versions to search for based on flags
|
||||
const versionsToSearch: string[] = [];
|
||||
let historicalCount = options.historical || 1; // Default to current version only
|
||||
|
||||
// Handle --generate flag for specific version
|
||||
if (options.generate) {
|
||||
versionsToSearch.push(options.generate);
|
||||
historicalCount = 1; // Single version mode
|
||||
console.log(`🎯 Generating changelog for specific version: ${options.generate}`);
|
||||
} else if (currentVersion !== 'unknown') {
|
||||
// Normal mode: use current version or historical versions
|
||||
const parts = currentVersion.split('.');
|
||||
if (parts.length === 3) {
|
||||
let major = parseInt(parts[0]);
|
||||
let minor = parseInt(parts[1]);
|
||||
let patch = parseInt(parts[2]);
|
||||
|
||||
for (let i = 0; i < historicalCount; i++) {
|
||||
versionsToSearch.push(`${major}.${minor}.${patch}`);
|
||||
|
||||
// Decrement version
|
||||
if (patch === 0) {
|
||||
if (minor === 0) {
|
||||
// Can't go lower than x.0.0
|
||||
break;
|
||||
}
|
||||
minor--;
|
||||
patch = 9;
|
||||
} else {
|
||||
patch--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (versionsToSearch.length === 0) {
|
||||
console.log('⚠️ Could not determine versions to search. Please check package.json');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if current version already has a changelog entry
|
||||
const projectChangelogDir = path.join(
|
||||
process.env.HOME || process.env.USERPROFILE || '',
|
||||
'.claude-mem',
|
||||
'projects'
|
||||
);
|
||||
const changelogJsonlPath = path.join(projectChangelogDir, `${projectName}-changelog.jsonl`);
|
||||
|
||||
let hasCurrentVersion = false;
|
||||
|
||||
if (fs.existsSync(changelogJsonlPath)) {
|
||||
const existingLines = fs.readFileSync(changelogJsonlPath, 'utf-8').split('\n').filter(l => l.trim());
|
||||
|
||||
for (const line of existingLines) {
|
||||
try {
|
||||
const entry = JSON.parse(line);
|
||||
if (entry.version === currentVersion) {
|
||||
hasCurrentVersion = true;
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip invalid lines
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.historical && !options.generate && historicalCount === 1) {
|
||||
if (hasCurrentVersion) {
|
||||
console.log(`❌ Version ${currentVersion} already has changelog entries.`);
|
||||
console.log('\n📝 Workflow:');
|
||||
console.log(' 1. Make your code updates');
|
||||
console.log(' 2. Build and test: bun run build');
|
||||
console.log(' 3. Bump version: npm version patch');
|
||||
console.log(' 4. Generate changelog: claude-mem changelog');
|
||||
console.log(' 5. Commit and push\n');
|
||||
console.log(`💡 Or use --historical 1 to regenerate this version's changelog`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get npm publish times for all versions we need
|
||||
let versionTimeRanges: Array<{version: string, startTime: string, endTime: string}> = [];
|
||||
|
||||
// Check if custom time range is provided
|
||||
if (options.start && options.end) {
|
||||
// Use custom time range for the specified version
|
||||
const version = options.generate || currentVersion;
|
||||
versionTimeRanges.push({
|
||||
version,
|
||||
startTime: options.start,
|
||||
endTime: options.end
|
||||
});
|
||||
|
||||
console.log(`📅 Using custom time range for ${version}:`);
|
||||
console.log(` Start: ${new Date(options.start).toLocaleString()}`);
|
||||
console.log(` End: ${new Date(options.end).toLocaleString()}`);
|
||||
} else {
|
||||
try {
|
||||
const npmTimeData = execSync(`npm view ${projectName} time --json`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000
|
||||
});
|
||||
const publishTimes = JSON.parse(npmTimeData);
|
||||
|
||||
// For historical mode, we need one extra previous version to get proper time ranges
|
||||
// E.g., for 3 versions, we need 4 timestamps to create 3 ranges
|
||||
let extraPrevVersion = '';
|
||||
if (historicalCount > 1) {
|
||||
// Get the version before our oldest version in the search list
|
||||
const oldestVersion = versionsToSearch[versionsToSearch.length - 1];
|
||||
const parts = oldestVersion.split('.');
|
||||
const major = parseInt(parts[0]);
|
||||
const minor = parseInt(parts[1]);
|
||||
const patch = parseInt(parts[2]);
|
||||
|
||||
if (patch > 0) {
|
||||
extraPrevVersion = `${major}.${minor}.${patch - 1}`;
|
||||
} else if (minor > 0) {
|
||||
// Look for highest patch of previous minor
|
||||
const prevMinorPrefix = `${major}.${minor - 1}.`;
|
||||
const prevMinorVersions = Object.keys(publishTimes)
|
||||
.filter(v => v.startsWith(prevMinorPrefix))
|
||||
.sort((a, b) => {
|
||||
const aPatch = parseInt(a.split('.')[2] || '0');
|
||||
const bPatch = parseInt(b.split('.')[2] || '0');
|
||||
return bPatch - aPatch;
|
||||
});
|
||||
if (prevMinorVersions.length > 0) {
|
||||
extraPrevVersion = prevMinorVersions[0];
|
||||
}
|
||||
} else if (major > 0) {
|
||||
// Look for highest version of previous major
|
||||
const prevMajorPrefix = `${major - 1}.`;
|
||||
const prevMajorVersions = Object.keys(publishTimes)
|
||||
.filter(v => v.startsWith(prevMajorPrefix))
|
||||
.sort((a, b) => {
|
||||
const [, aMinor, aPatch] = a.split('.').map(Number);
|
||||
const [, bMinor, bPatch] = b.split('.').map(Number);
|
||||
if (aMinor !== bMinor) return bMinor - aMinor;
|
||||
return bPatch - aPatch;
|
||||
});
|
||||
if (prevMajorVersions.length > 0) {
|
||||
extraPrevVersion = prevMajorVersions[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (options.verbose && extraPrevVersion && publishTimes[extraPrevVersion]) {
|
||||
console.log(`📍 Using ${extraPrevVersion} as start boundary for time ranges`);
|
||||
}
|
||||
}
|
||||
|
||||
// Build time ranges for each version
|
||||
for (let i = 0; i < versionsToSearch.length; i++) {
|
||||
const version = versionsToSearch[i];
|
||||
|
||||
// Start time:
|
||||
// - For the first (newest) version, use the publish time of the version before it
|
||||
// - For middle versions, use the publish time of the next version in our list
|
||||
// - For the last (oldest) version, use the extra previous version we found
|
||||
let startTime = '2000-01-01T00:00:00Z'; // Default to old date
|
||||
|
||||
if (i === 0) {
|
||||
// First (newest) version - find its immediate predecessor
|
||||
const versionParts = version.split('.');
|
||||
const major = parseInt(versionParts[0]);
|
||||
const minor = parseInt(versionParts[1]);
|
||||
const patch = parseInt(versionParts[2]);
|
||||
|
||||
let prevVersion = '';
|
||||
if (patch > 0) {
|
||||
prevVersion = `${major}.${minor}.${patch - 1}`;
|
||||
} else if (minor > 0) {
|
||||
// Look for highest patch of previous minor
|
||||
const prevMinorPrefix = `${major}.${minor - 1}.`;
|
||||
const prevMinorVersions = Object.keys(publishTimes)
|
||||
.filter(v => v.startsWith(prevMinorPrefix))
|
||||
.sort((a, b) => {
|
||||
const aPatch = parseInt(a.split('.')[2] || '0');
|
||||
const bPatch = parseInt(b.split('.')[2] || '0');
|
||||
return bPatch - aPatch;
|
||||
});
|
||||
if (prevMinorVersions.length > 0) {
|
||||
prevVersion = prevMinorVersions[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (publishTimes[prevVersion]) {
|
||||
startTime = publishTimes[prevVersion];
|
||||
}
|
||||
} else if (i < versionsToSearch.length - 1) {
|
||||
// Middle versions - use the next version in our list
|
||||
const prevVersionInList = versionsToSearch[i + 1];
|
||||
if (publishTimes[prevVersionInList]) {
|
||||
startTime = publishTimes[prevVersionInList];
|
||||
}
|
||||
} else {
|
||||
// Last (oldest) version - use the extra previous version
|
||||
if (extraPrevVersion && publishTimes[extraPrevVersion]) {
|
||||
startTime = publishTimes[extraPrevVersion];
|
||||
}
|
||||
}
|
||||
|
||||
// End time is this version's publish time (or now for unreleased)
|
||||
let endTime = publishTimes[version] || new Date().toISOString();
|
||||
|
||||
versionTimeRanges.push({ version, startTime, endTime });
|
||||
|
||||
if (options.verbose) {
|
||||
console.log(`📅 Version ${version}: ${new Date(startTime).toLocaleString()} - ${new Date(endTime).toLocaleString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Always log what we're doing for single version
|
||||
if (historicalCount === 1) {
|
||||
const latestRange = versionTimeRanges[0];
|
||||
if (latestRange) {
|
||||
console.log(`📦 Using npm time range for ${latestRange.version}: ${new Date(latestRange.startTime).toLocaleString()} - ${new Date(latestRange.endTime).toLocaleString()}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('❌ Could not fetch npm publish times. Cannot proceed without time ranges.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🔍 Searching memories for versions: ${versionsToSearch.join(', ')}`);
|
||||
console.log(`📦 Project: ${projectName}\n`);
|
||||
|
||||
// Phase 1: Search for version-related memories using MCP tools
|
||||
// ALWAYS use time range search - no other method
|
||||
const searchPrompt = versionTimeRanges.length > 0 ?
|
||||
`You are helping generate a changelog by searching for memories within specific time ranges for multiple versions.
|
||||
|
||||
PROJECT: ${projectName}
|
||||
VERSION TIME RANGES:
|
||||
${versionTimeRanges.map(r => `- Version ${r.version}: ${new Date(r.startTime).toLocaleDateString()} to ${new Date(r.endTime).toLocaleDateString()}`).join('\n')}
|
||||
|
||||
YOUR TASK:
|
||||
Use mcp__claude-mem__chroma_query_documents to search for memories for each version time range.
|
||||
|
||||
SEARCH STRATEGY:
|
||||
${versionTimeRanges.map(r => {
|
||||
const startDate = new Date(r.startTime);
|
||||
const endDate = new Date(r.endTime);
|
||||
|
||||
// Generate all date prefixes between start and end
|
||||
const datePrefixes: string[] = [];
|
||||
const currentDate = new Date(startDate);
|
||||
|
||||
while (currentDate <= endDate) {
|
||||
// Add day prefix like "2025-09-09"
|
||||
const dayPrefix = currentDate.toISOString().split('T')[0];
|
||||
datePrefixes.push(dayPrefix);
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
|
||||
return `
|
||||
Version ${r.version} (${new Date(r.startTime).toLocaleDateString()} to ${new Date(r.endTime).toLocaleDateString()}):
|
||||
1. Search for memories from these dates: ${datePrefixes.join(', ')}
|
||||
2. Make multiple calls to mcp__claude-mem__chroma_query_documents:
|
||||
- collection_name: "claude_memories"
|
||||
- query_texts: Include the project name AND date in each query:
|
||||
* "${projectName} ${datePrefixes[0]} feature"
|
||||
* "${projectName} ${datePrefixes[0]} fix"
|
||||
* "${projectName} ${datePrefixes[0]} change"
|
||||
* "${projectName} ${datePrefixes[0]} improvement"
|
||||
* "${projectName} ${datePrefixes[0]} refactor"
|
||||
- n_results: 50
|
||||
3. The date in the query text helps semantic search find memories from that day
|
||||
4. Assign memories to this version if their timestamp falls within:
|
||||
- Start: ${r.startTime}
|
||||
- End: ${r.endTime}`;
|
||||
}).join('\n')}
|
||||
|
||||
IMPORTANT:
|
||||
- Always include project name and date in query_texts for best results
|
||||
- Semantic search will naturally find memories near those dates
|
||||
- Group returned memories by version based on their timestamp metadata
|
||||
|
||||
Return a JSON object with this structure:
|
||||
{
|
||||
"memories": [
|
||||
{
|
||||
"version": "version_number",
|
||||
"text": "memory content",
|
||||
"metadata": {metadata object with timestamp},
|
||||
"relevance": "high/medium/low"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Group memories by the version they belong to based on timestamp.
|
||||
Start searching now.` :
|
||||
`ERROR: No time ranges available. This should never happen.`;
|
||||
|
||||
if (versionTimeRanges.length === 0) {
|
||||
console.log('❌ No time ranges available. Cannot search memories.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (options.verbose) {
|
||||
console.log('📝 Calling Claude to search memories...');
|
||||
}
|
||||
|
||||
// Call Claude with MCP tools to search memories
|
||||
const searchResponse = await query({
|
||||
prompt: searchPrompt,
|
||||
options: {
|
||||
allowedTools: [
|
||||
'mcp__claude-mem__chroma_query_documents',
|
||||
'mcp__claude-mem__chroma_get_documents'
|
||||
],
|
||||
pathToClaudeCodeExecutable: getClaudePath()
|
||||
}
|
||||
});
|
||||
|
||||
// Extract memories from response
|
||||
let memoriesJson = '';
|
||||
if (searchResponse && typeof searchResponse === 'object' && Symbol.asyncIterator in searchResponse) {
|
||||
for await (const message of searchResponse) {
|
||||
if (message?.type === 'assistant' && message?.message?.content) {
|
||||
const content = message.message.content;
|
||||
if (typeof content === 'string') {
|
||||
memoriesJson += content;
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const block of content) {
|
||||
if (block.type === 'text' && block.text) {
|
||||
memoriesJson += block.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse memories
|
||||
let memories: MemorySearchResult[] = [];
|
||||
try {
|
||||
// Extract JSON from response (might be wrapped in markdown)
|
||||
const jsonMatch = memoriesJson.match(/```json\n([\s\S]*?)\n```/) ||
|
||||
memoriesJson.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
const parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
|
||||
if (parsed.memories && Array.isArray(parsed.memories)) {
|
||||
memories = parsed.memories;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('⚠️ Could not parse memory search results:', e);
|
||||
}
|
||||
|
||||
if (memories.length === 0) {
|
||||
console.log('\n⚠️ No version-related memories found. Try compressing more sessions first.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`✅ Found ${memories.length} version-related memories\n`);
|
||||
|
||||
// Get system date for accuracy
|
||||
const systemDate = execSync('date "+%Y-%m-%d %H:%M:%S %Z"').toString().trim();
|
||||
const todayStr = systemDate.split(' ')[0]; // YYYY-MM-DD format
|
||||
|
||||
// Phase 2: Generate changelog entries from memories
|
||||
const changelogPrompt = `Analyze these memories and generate changelog entries.
|
||||
|
||||
PROJECT: ${projectName}
|
||||
DATE: ${todayStr}
|
||||
|
||||
MEMORIES BY VERSION:
|
||||
${versionsToSearch.map(version => {
|
||||
const versionMemories = memories.filter(m => m.version === version);
|
||||
if (versionMemories.length === 0) return `### Version ${version}\nNo memories found.`;
|
||||
return `### Version ${version} (${versionMemories.length} memories):
|
||||
${versionMemories.map((m, i) => `${i + 1}. ${m.text}`).join('\n')}`;
|
||||
}).join('\n\n')}
|
||||
|
||||
INSTRUCTIONS:
|
||||
1. Extract concrete changes, fixes, and additions from the memories
|
||||
2. Categorize each change as: Added, Changed, Fixed, Removed, Deprecated, or Security
|
||||
3. Write clear, user-facing descriptions
|
||||
4. Start each entry with an action verb
|
||||
5. Focus on what matters to users, not internal implementation details
|
||||
|
||||
Return ONLY a JSON array with this structure:
|
||||
[
|
||||
{
|
||||
"version": "3.6.1",
|
||||
"type": "Added",
|
||||
"description": "New feature description"
|
||||
},
|
||||
{
|
||||
"version": "3.6.1",
|
||||
"type": "Fixed",
|
||||
"description": "Bug fix description"
|
||||
}
|
||||
]`;
|
||||
|
||||
console.log('🔄 Generating changelog entries...');
|
||||
|
||||
// Call Claude to generate changelog entries
|
||||
const changelogResponse = await query({
|
||||
prompt: changelogPrompt,
|
||||
options: {
|
||||
allowedTools: [],
|
||||
pathToClaudeCodeExecutable: getClaudePath()
|
||||
}
|
||||
});
|
||||
|
||||
// Extract JSON from response
|
||||
let entriesJson = '';
|
||||
if (changelogResponse && typeof changelogResponse === 'object' && Symbol.asyncIterator in changelogResponse) {
|
||||
for await (const message of changelogResponse) {
|
||||
if (message?.type === 'assistant' && message?.message?.content) {
|
||||
const content = message.message.content;
|
||||
if (typeof content === 'string') {
|
||||
entriesJson += content;
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const block of content) {
|
||||
if (block.type === 'text' && block.text) {
|
||||
entriesJson += block.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse changelog entries
|
||||
let entries: ChangelogEntry[] = [];
|
||||
try {
|
||||
// Extract JSON (might be wrapped in markdown)
|
||||
const jsonMatch = entriesJson.match(/```json\n([\s\S]*?)\n```/) ||
|
||||
entriesJson.match(/\[[\s\S]*\]/);
|
||||
if (jsonMatch) {
|
||||
const parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
|
||||
if (Array.isArray(parsed)) {
|
||||
const generatedAt = new Date().toISOString();
|
||||
entries = parsed.map(e => ({
|
||||
...e,
|
||||
date: todayStr,
|
||||
timestamp: e.timestamp || generatedAt, // Memory timestamp if available
|
||||
generatedAt: generatedAt // When this changelog was generated
|
||||
}));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('⚠️ Could not parse changelog entries:', e);
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
console.log('⚠️ No changelog entries generated.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Ensure project changelog directory exists
|
||||
if (!fs.existsSync(projectChangelogDir)) {
|
||||
fs.mkdirSync(projectChangelogDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Save entries to project JSONL file
|
||||
console.log(`\n💾 Saving ${entries.length} changelog entries to ${path.basename(changelogJsonlPath)}`);
|
||||
|
||||
// When using --historical or --generate, remove old entries for the versions being regenerated
|
||||
if ((options.historical && historicalCount > 1) || options.generate) {
|
||||
let existingEntries: ChangelogEntry[] = [];
|
||||
if (fs.existsSync(changelogJsonlPath)) {
|
||||
const lines = fs.readFileSync(changelogJsonlPath, 'utf-8').split('\n').filter(l => l.trim());
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const entry = JSON.parse(line);
|
||||
// Keep entries that are NOT in the versions we're regenerating
|
||||
if (!versionsToSearch.includes(entry.version)) {
|
||||
existingEntries.push(entry);
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip invalid lines
|
||||
}
|
||||
}
|
||||
}
|
||||
// Rewrite the file with filtered entries plus new ones
|
||||
const allEntries = [...existingEntries, ...entries];
|
||||
const jsonlContent = allEntries.map(entry => JSON.stringify(entry)).join('\n') + '\n';
|
||||
fs.writeFileSync(changelogJsonlPath, jsonlContent);
|
||||
console.log(`🔄 Regenerated entries for versions: ${versionsToSearch.join(', ')}`);
|
||||
} else {
|
||||
// Append new entries to JSONL
|
||||
const jsonlContent = entries.map(entry => JSON.stringify(entry)).join('\n') + '\n';
|
||||
fs.appendFileSync(changelogJsonlPath, jsonlContent);
|
||||
}
|
||||
|
||||
// Now generate markdown from all JSONL entries
|
||||
console.log('\n📝 Generating CHANGELOG.md from entries...');
|
||||
|
||||
// Read all entries from JSONL
|
||||
let allEntries: ChangelogEntry[] = [];
|
||||
if (fs.existsSync(changelogJsonlPath)) {
|
||||
const lines = fs.readFileSync(changelogJsonlPath, 'utf-8').split('\n').filter(l => l.trim());
|
||||
for (const line of lines) {
|
||||
try {
|
||||
allEntries.push(JSON.parse(line));
|
||||
} catch (e) {
|
||||
// Skip invalid lines
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group entries by version
|
||||
const entriesByVersion = new Map<string, ChangelogEntry[]>();
|
||||
for (const entry of allEntries) {
|
||||
if (!entriesByVersion.has(entry.version)) {
|
||||
entriesByVersion.set(entry.version, []);
|
||||
}
|
||||
entriesByVersion.get(entry.version)!.push(entry);
|
||||
}
|
||||
|
||||
// Generate markdown
|
||||
let markdown = '# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).\n\n';
|
||||
|
||||
// Sort versions in descending order
|
||||
const sortedVersions = Array.from(entriesByVersion.keys()).sort((a, b) => {
|
||||
const aParts = a.split('.').map(Number);
|
||||
const bParts = b.split('.').map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (aParts[i] !== bParts[i]) return bParts[i] - aParts[i];
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
for (const version of sortedVersions) {
|
||||
const versionEntries = entriesByVersion.get(version)!;
|
||||
const date = versionEntries[0].date || todayStr;
|
||||
|
||||
markdown += `\n## [${version}] - ${date}\n\n`;
|
||||
|
||||
// Group by type
|
||||
const types: Array<ChangelogEntry['type']> = ['Added', 'Changed', 'Fixed', 'Removed', 'Deprecated', 'Security'];
|
||||
for (const type of types) {
|
||||
const typeEntries = versionEntries.filter(e => e.type === type);
|
||||
if (typeEntries.length > 0) {
|
||||
markdown += `### ${type}\n`;
|
||||
for (const entry of typeEntries) {
|
||||
markdown += `- ${entry.description}\n`;
|
||||
}
|
||||
markdown += '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the CHANGELOG.md
|
||||
const changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
|
||||
fs.writeFileSync(changelogPath, markdown);
|
||||
|
||||
console.log(`✅ Generated CHANGELOG.md with ${allEntries.length} total entries across ${entriesByVersion.size} versions!`);
|
||||
|
||||
if (options.preview) {
|
||||
console.log('\n📄 Preview:\n');
|
||||
console.log(markdown.split('\n').slice(0, 30).join('\n'));
|
||||
if (markdown.split('\n').length > 30) {
|
||||
console.log('\n... (truncated for preview)');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error generating changelog:', error instanceof Error ? error.message : error);
|
||||
if (error instanceof Error && error.stack) {
|
||||
console.error('Stack:', error.stack);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateChangelogFromJsonl(options: OptionValues): Promise<void> {
|
||||
try {
|
||||
// Get project name from package.json
|
||||
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||
let projectName = 'unknown';
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
try {
|
||||
const packageData = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
projectName = packageData.name || path.basename(process.cwd());
|
||||
} catch (e) {
|
||||
projectName = path.basename(process.cwd());
|
||||
}
|
||||
}
|
||||
|
||||
const projectChangelogDir = path.join(
|
||||
process.env.HOME || process.env.USERPROFILE || '',
|
||||
'.claude-mem',
|
||||
'projects'
|
||||
);
|
||||
const changelogJsonlPath = path.join(projectChangelogDir, `${projectName}-changelog.jsonl`);
|
||||
|
||||
if (!fs.existsSync(changelogJsonlPath)) {
|
||||
console.log('❌ No changelog entries found. Generate some first with: claude-mem changelog');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('📝 Updating CHANGELOG.md from JSONL entries...');
|
||||
|
||||
// Read all entries from JSONL
|
||||
let allEntries: ChangelogEntry[] = [];
|
||||
const lines = fs.readFileSync(changelogJsonlPath, 'utf-8').split('\n').filter(l => l.trim());
|
||||
for (const line of lines) {
|
||||
try {
|
||||
allEntries.push(JSON.parse(line));
|
||||
} catch (e) {
|
||||
// Skip invalid lines
|
||||
}
|
||||
}
|
||||
|
||||
if (allEntries.length === 0) {
|
||||
console.log('❌ No valid entries found in JSONL file');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Group entries by version
|
||||
const entriesByVersion = new Map<string, ChangelogEntry[]>();
|
||||
for (const entry of allEntries) {
|
||||
if (!entriesByVersion.has(entry.version)) {
|
||||
entriesByVersion.set(entry.version, []);
|
||||
}
|
||||
entriesByVersion.get(entry.version)!.push(entry);
|
||||
}
|
||||
|
||||
// Generate markdown
|
||||
let markdown = '# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).\n\n';
|
||||
|
||||
// Sort versions in descending order
|
||||
const sortedVersions = Array.from(entriesByVersion.keys()).sort((a, b) => {
|
||||
const aParts = a.split('.').map(Number);
|
||||
const bParts = b.split('.').map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (aParts[i] !== bParts[i]) return bParts[i] - aParts[i];
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
for (const version of sortedVersions) {
|
||||
const versionEntries = entriesByVersion.get(version)!;
|
||||
const date = versionEntries[0].date;
|
||||
|
||||
markdown += `\n## [${version}] - ${date}\n\n`;
|
||||
|
||||
// Group by type
|
||||
const types: Array<ChangelogEntry['type']> = ['Added', 'Changed', 'Fixed', 'Removed', 'Deprecated', 'Security'];
|
||||
for (const type of types) {
|
||||
const typeEntries = versionEntries.filter(e => e.type === type);
|
||||
if (typeEntries.length > 0) {
|
||||
markdown += `### ${type}\n`;
|
||||
for (const entry of typeEntries) {
|
||||
markdown += `- ${entry.description}\n`;
|
||||
}
|
||||
markdown += '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the CHANGELOG.md
|
||||
const changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
|
||||
fs.writeFileSync(changelogPath, markdown);
|
||||
|
||||
console.log(`✅ Updated CHANGELOG.md with ${allEntries.length} entries across ${entriesByVersion.size} versions!`);
|
||||
|
||||
if (options.preview) {
|
||||
console.log('\n📄 Preview:\n');
|
||||
console.log(markdown.split('\n').slice(0, 30).join('\n'));
|
||||
if (markdown.split('\n').length > 30) {
|
||||
console.log('\n... (truncated for preview)');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error updating changelog:', error instanceof Error ? error.message : error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import { basename, dirname } from 'path';
|
||||
import {
|
||||
createLoadingMessage,
|
||||
createCompletionMessage,
|
||||
createOperationSummary,
|
||||
createUserFriendlyError
|
||||
} from '../prompts/templates/context/ContextTemplates.js';
|
||||
|
||||
export async function compress(transcript?: string, options: OptionValues = {}): Promise<void> {
|
||||
console.log(createLoadingMessage('compressing'));
|
||||
|
||||
if (!transcript) {
|
||||
console.log(createUserFriendlyError('Compression', 'No transcript file provided', 'Please provide a path to a transcript file'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Import and run compression
|
||||
const { TranscriptCompressor } = await import('../core/compression/TranscriptCompressor.js');
|
||||
const compressor = new TranscriptCompressor({
|
||||
verbose: options.verbose || false
|
||||
});
|
||||
|
||||
const sessionId = options.sessionId || basename(transcript, '.jsonl');
|
||||
const archivePath = await compressor.compress(transcript, sessionId);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
console.log(createCompletionMessage('Compression', undefined, `Session archived as ${basename(archivePath)}`));
|
||||
console.log(createOperationSummary('compress', { count: 1, duration, details: `Session: ${sessionId}` }));
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.log(createUserFriendlyError(
|
||||
'Compression',
|
||||
errorMessage,
|
||||
'Check that the transcript file exists and you have write permissions'
|
||||
));
|
||||
throw error; // Re-throw to maintain existing error handling behavior
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
/**
|
||||
* Hook command handlers for binary distribution
|
||||
* These execute the actual hook logic embedded in the binary
|
||||
*/
|
||||
|
||||
import { basename, sep } from 'path';
|
||||
import { compress } from './compress.js';
|
||||
import { loadContext } from './load-context.js';
|
||||
|
||||
/**
|
||||
* Pre-compact hook handler
|
||||
* Runs compression on the Claude Code transcript
|
||||
*/
|
||||
export async function preCompactHook(): Promise<void> {
|
||||
try {
|
||||
// Read hook data from stdin (Claude Code sends JSON)
|
||||
let inputData = '';
|
||||
|
||||
// Set up stdin to read data
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
// Collect all input data
|
||||
for await (const chunk of process.stdin) {
|
||||
inputData += chunk;
|
||||
}
|
||||
|
||||
// Parse the JSON input
|
||||
let transcriptPath: string | undefined;
|
||||
|
||||
if (inputData) {
|
||||
try {
|
||||
const hookData = JSON.parse(inputData);
|
||||
transcriptPath = hookData.transcript_path;
|
||||
} catch (parseError) {
|
||||
// If JSON parsing fails, treat the input as a direct path
|
||||
transcriptPath = inputData.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to environment variable or command line argument
|
||||
if (!transcriptPath) {
|
||||
transcriptPath = process.env.TRANSCRIPT_PATH || process.argv[2];
|
||||
}
|
||||
|
||||
if (!transcriptPath) {
|
||||
console.log('🗜️ Compressing session transcript...');
|
||||
console.log('❌ No transcript path provided to pre-compact hook');
|
||||
console.log('Hook data received:', inputData || 'none');
|
||||
console.log('Environment TRANSCRIPT_PATH:', process.env.TRANSCRIPT_PATH || 'not set');
|
||||
console.log('Command line args:', process.argv.slice(2));
|
||||
return;
|
||||
}
|
||||
|
||||
// Run compression with the transcript path
|
||||
await compress(transcriptPath, { dryRun: false });
|
||||
} catch (error: any) {
|
||||
console.error('Pre-compact hook failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Session-start hook handler
|
||||
* Loads context for the new session
|
||||
*/
|
||||
export async function sessionStartHook(): Promise<void> {
|
||||
try {
|
||||
// Read hook data from stdin (Claude Code sends JSON)
|
||||
let inputData = '';
|
||||
|
||||
// Set up stdin to read data
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
// Collect all input data
|
||||
for await (const chunk of process.stdin) {
|
||||
inputData += chunk;
|
||||
}
|
||||
|
||||
// Parse the JSON input to get the current working directory
|
||||
let project: string | undefined;
|
||||
|
||||
if (inputData) {
|
||||
try {
|
||||
const hookData = JSON.parse(inputData);
|
||||
// Extract project name from cwd if provided
|
||||
if (hookData.cwd) {
|
||||
project = basename(hookData.cwd);
|
||||
}
|
||||
} catch (parseError) {
|
||||
// If JSON parsing fails, continue without project filtering
|
||||
console.error('Failed to parse session-start hook data:', parseError);
|
||||
}
|
||||
}
|
||||
|
||||
// If no project from hook data, try to get from current working directory
|
||||
if (!project) {
|
||||
project = basename(process.cwd());
|
||||
}
|
||||
|
||||
// Load context with session-start format and project filtering
|
||||
await loadContext({ format: 'session-start', count: '10', project });
|
||||
} catch (error: any) {
|
||||
console.error('Session-start hook failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Session-end hook handler
|
||||
* Compresses session transcript when ending with /clear
|
||||
*/
|
||||
export async function sessionEndHook(): Promise<void> {
|
||||
try {
|
||||
// Read hook data from stdin (Claude Code sends JSON)
|
||||
let inputData = '';
|
||||
|
||||
// Set up stdin to read data
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
// Collect all input data
|
||||
for await (const chunk of process.stdin) {
|
||||
inputData += chunk;
|
||||
}
|
||||
|
||||
// Parse the JSON input to check the reason for session end
|
||||
if (inputData) {
|
||||
try {
|
||||
const hookData = JSON.parse(inputData);
|
||||
|
||||
// If reason is "clear", compress the session transcript before it's deleted
|
||||
if (hookData.reason === 'clear' && hookData.transcript_path) {
|
||||
console.log('🗜️ Compressing current session before /clear...');
|
||||
await compress(hookData.transcript_path, { dryRun: false });
|
||||
}
|
||||
} catch (parseError) {
|
||||
// If JSON parsing fails, log but don't fail the hook
|
||||
console.error('Failed to parse hook data:', parseError);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Session ended successfully');
|
||||
} catch (error: any) {
|
||||
console.error('Session-end hook failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1,541 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import * as p from '@clack/prompts';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import chalk from 'chalk';
|
||||
import { TranscriptCompressor } from '../core/compression/TranscriptCompressor.js';
|
||||
import { TitleGenerator, TitleGenerationRequest } from '../core/titles/TitleGenerator.js';
|
||||
|
||||
interface ConversationMetadata {
|
||||
sessionId: string;
|
||||
timestamp: string;
|
||||
messageCount: number;
|
||||
branch?: string;
|
||||
cwd: string;
|
||||
fileSize: number;
|
||||
}
|
||||
|
||||
interface ConversationItem extends ConversationMetadata {
|
||||
filePath: string;
|
||||
projectName: string;
|
||||
parsedDate: Date;
|
||||
relativeDate: string;
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes}B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
||||
}
|
||||
|
||||
function formatRelativeDate(date: Date): string {
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return 'just now';
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`;
|
||||
if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo ago`;
|
||||
return `${Math.floor(diffDays / 365)}y ago`;
|
||||
}
|
||||
|
||||
function parseTimestamp(timestamp: string, fallbackPath: string): Date {
|
||||
try {
|
||||
const parsed = new Date(timestamp);
|
||||
if (!isNaN(parsed.getTime())) return parsed;
|
||||
} catch {}
|
||||
|
||||
// Fallback: try to extract from filename
|
||||
const match = fallbackPath.match(/(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})/);
|
||||
if (match) {
|
||||
const [_, year, month, day, hour, minute, second] = match;
|
||||
return new Date(
|
||||
parseInt(year),
|
||||
parseInt(month) - 1,
|
||||
parseInt(day),
|
||||
parseInt(hour),
|
||||
parseInt(minute),
|
||||
parseInt(second)
|
||||
);
|
||||
}
|
||||
|
||||
// Last resort: file stats
|
||||
const stats = fs.statSync(fallbackPath);
|
||||
return stats.mtime;
|
||||
}
|
||||
|
||||
function extractFirstUserMessage(filePath: string): string {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const lines = content.trim().split('\n').filter(Boolean);
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const message = JSON.parse(line);
|
||||
if (message.type === 'user' && message.message?.content) {
|
||||
const messageContent = message.message.content;
|
||||
if (Array.isArray(messageContent)) {
|
||||
const textContent = messageContent
|
||||
.filter(item => item.type === 'text')
|
||||
.map(item => item.text)
|
||||
.join(' ');
|
||||
if (textContent.trim()) return textContent.trim();
|
||||
} else if (typeof messageContent === 'string') {
|
||||
return messageContent.trim();
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return 'Conversation'; // Fallback
|
||||
} catch {
|
||||
return 'Conversation'; // Fallback
|
||||
}
|
||||
}
|
||||
|
||||
async function loadImportedSessions(): Promise<Set<string>> {
|
||||
const importedIds = new Set<string>();
|
||||
const indexPath = path.join(os.homedir(), '.claude-mem', 'claude-mem-index.jsonl');
|
||||
|
||||
if (!fs.existsSync(indexPath)) return importedIds;
|
||||
|
||||
const content = fs.readFileSync(indexPath, 'utf-8');
|
||||
const lines = content.trim().split('\n').filter(Boolean);
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const entry = JSON.parse(line);
|
||||
// Check both session_id (from index) and sessionId (legacy)
|
||||
if (entry.session_id) {
|
||||
importedIds.add(entry.session_id);
|
||||
} else if (entry.sessionId) {
|
||||
importedIds.add(entry.sessionId);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return importedIds;
|
||||
}
|
||||
|
||||
async function scanConversations(): Promise<{ conversations: ConversationItem[]; skippedCount: number }> {
|
||||
const claudeDir = path.join(os.homedir(), '.claude', 'projects');
|
||||
|
||||
if (!fs.existsSync(claudeDir)) {
|
||||
return { conversations: [], skippedCount: 0 };
|
||||
}
|
||||
|
||||
const projects = fs.readdirSync(claudeDir)
|
||||
.filter(dir => fs.statSync(path.join(claudeDir, dir)).isDirectory());
|
||||
|
||||
const conversations: ConversationItem[] = [];
|
||||
const importedSessionIds = await loadImportedSessions();
|
||||
let skippedCount = 0;
|
||||
|
||||
for (const project of projects) {
|
||||
const projectDir = path.join(claudeDir, project);
|
||||
const files = fs.readdirSync(projectDir)
|
||||
.filter(file => file.endsWith('.jsonl'))
|
||||
.map(file => path.join(projectDir, file));
|
||||
|
||||
for (const filePath of files) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const lines = content.trim().split('\n').filter(Boolean);
|
||||
|
||||
// Parse first line for metadata
|
||||
const firstLine = JSON.parse(lines[0]);
|
||||
const messageCount = lines.length;
|
||||
const stats = fs.statSync(filePath);
|
||||
const fileSize = stats.size;
|
||||
|
||||
const metadata: ConversationMetadata = {
|
||||
sessionId: firstLine.sessionId || path.basename(filePath, '.jsonl'),
|
||||
timestamp: firstLine.timestamp || stats.mtime.toISOString(),
|
||||
messageCount,
|
||||
branch: firstLine.branch,
|
||||
cwd: firstLine.cwd || projectDir,
|
||||
fileSize
|
||||
};
|
||||
|
||||
// Skip if already imported
|
||||
if (importedSessionIds.has(metadata.sessionId)) {
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const projectName = path.basename(path.dirname(filePath));
|
||||
const parsedDate = parseTimestamp(metadata.timestamp, filePath);
|
||||
const relativeDate = formatRelativeDate(parsedDate);
|
||||
|
||||
conversations.push({
|
||||
filePath,
|
||||
...metadata,
|
||||
projectName,
|
||||
parsedDate,
|
||||
relativeDate
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
return { conversations, skippedCount };
|
||||
}
|
||||
|
||||
export async function importHistory(options: { verbose?: boolean; multi?: boolean } = {}) {
|
||||
console.clear();
|
||||
|
||||
p.intro(chalk.bgCyan.black(' CLAUDE-MEM IMPORT '));
|
||||
|
||||
const s = p.spinner();
|
||||
s.start('Scanning conversation history');
|
||||
|
||||
const { conversations, skippedCount } = await scanConversations();
|
||||
|
||||
if (conversations.length === 0) {
|
||||
s.stop('No new conversations found');
|
||||
const message = skippedCount > 0
|
||||
? `All ${skippedCount} conversation${skippedCount === 1 ? ' is' : 's are'} already imported.`
|
||||
: 'No conversations found.';
|
||||
p.outro(chalk.yellow(message));
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by date (newest first)
|
||||
conversations.sort((a, b) => b.parsedDate.getTime() - a.parsedDate.getTime());
|
||||
|
||||
const statusMessage = skippedCount > 0
|
||||
? `Found ${conversations.length} new conversation${conversations.length === 1 ? '' : 's'} (${skippedCount} already imported)`
|
||||
: `Found ${conversations.length} new conversation${conversations.length === 1 ? '' : 's'}`;
|
||||
s.stop(statusMessage);
|
||||
|
||||
// Group conversations by project for better organization
|
||||
const projectGroups = conversations.reduce((acc, conv) => {
|
||||
if (!acc[conv.projectName]) acc[conv.projectName] = [];
|
||||
acc[conv.projectName].push(conv);
|
||||
return acc;
|
||||
}, {} as Record<string, ConversationItem[]>);
|
||||
|
||||
// Create selection options
|
||||
const importMode = await p.select({
|
||||
message: 'How would you like to import?',
|
||||
options: [
|
||||
{ value: 'browse', label: 'Browse by Project', hint: 'Select project then conversations' },
|
||||
{ value: 'project', label: 'Import Entire Project', hint: 'Select project and import all conversations' },
|
||||
{ value: 'recent', label: 'Recent Conversations', hint: 'Import most recent across all projects' },
|
||||
{ value: 'search', label: 'Search', hint: 'Search for specific conversations' }
|
||||
]
|
||||
});
|
||||
|
||||
if (p.isCancel(importMode)) {
|
||||
p.cancel('Import cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
let selectedConversations: ConversationItem[] = [];
|
||||
|
||||
if (importMode === 'browse') {
|
||||
// Project selection
|
||||
const projectOptions = Object.entries(projectGroups)
|
||||
.sort((a, b) => b[1][0].parsedDate.getTime() - a[1][0].parsedDate.getTime())
|
||||
.map(([project, convs]) => ({
|
||||
value: project,
|
||||
label: project,
|
||||
hint: `${convs.length} conversation${convs.length === 1 ? '' : 's'}, latest: ${convs[0].relativeDate}`
|
||||
}));
|
||||
|
||||
const selectedProject = await p.select({
|
||||
message: 'Select a project',
|
||||
options: projectOptions
|
||||
});
|
||||
|
||||
if (p.isCancel(selectedProject)) {
|
||||
p.cancel('Import cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
const projectConvs = projectGroups[selectedProject as string];
|
||||
|
||||
// Ask about title generation
|
||||
const generateTitles = await p.confirm({
|
||||
message: 'Would you like to generate titles for easier browsing?',
|
||||
initialValue: false
|
||||
});
|
||||
|
||||
if (p.isCancel(generateTitles)) {
|
||||
p.cancel('Import cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
if (generateTitles) {
|
||||
await processTitleGeneration(projectConvs, selectedProject as string);
|
||||
}
|
||||
|
||||
// Conversation selection within project
|
||||
const titleGenerator = new TitleGenerator();
|
||||
const convOptions = projectConvs.map(conv => {
|
||||
const title = titleGenerator.getTitleForSession(conv.sessionId);
|
||||
const displayTitle = title ? `"${title}" • ` : '';
|
||||
return {
|
||||
value: conv.sessionId,
|
||||
label: `${displayTitle}${conv.relativeDate} • ${conv.messageCount} messages • ${formatFileSize(conv.fileSize)}`,
|
||||
hint: conv.branch ? `branch: ${conv.branch}` : undefined
|
||||
};
|
||||
});
|
||||
|
||||
if (options.multi) {
|
||||
const selected = await p.multiselect({
|
||||
message: `Select conversations from ${selectedProject} (Space to select, Enter to confirm)`,
|
||||
options: convOptions,
|
||||
required: false
|
||||
});
|
||||
|
||||
if (p.isCancel(selected)) {
|
||||
p.cancel('Import cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIds = selected as string[];
|
||||
selectedConversations = projectConvs.filter(c => selectedIds.includes(c.sessionId));
|
||||
} else {
|
||||
// Single select with continuous import
|
||||
let continueImporting = true;
|
||||
const importedInSession = new Set<string>();
|
||||
|
||||
while (continueImporting && projectConvs.length > importedInSession.size) {
|
||||
const availableConvs = projectConvs.filter(c => !importedInSession.has(c.sessionId));
|
||||
|
||||
if (availableConvs.length === 0) break;
|
||||
|
||||
const titleGenerator = new TitleGenerator();
|
||||
const convOptions = availableConvs.map(conv => {
|
||||
const title = titleGenerator.getTitleForSession(conv.sessionId);
|
||||
const displayTitle = title ? `"${title}" • ` : '';
|
||||
return {
|
||||
value: conv.sessionId,
|
||||
label: `${displayTitle}${conv.relativeDate} • ${conv.messageCount} messages • ${formatFileSize(conv.fileSize)}`,
|
||||
hint: conv.branch ? `branch: ${conv.branch}` : undefined
|
||||
};
|
||||
});
|
||||
|
||||
const selected = await p.select({
|
||||
message: `Select a conversation (${importedInSession.size}/${projectConvs.length} imported)`,
|
||||
options: [
|
||||
...convOptions,
|
||||
{ value: 'done', label: '✅ Done importing', hint: 'Exit import mode' }
|
||||
]
|
||||
});
|
||||
|
||||
if (p.isCancel(selected) || selected === 'done') {
|
||||
continueImporting = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const conv = availableConvs.find(c => c.sessionId === selected);
|
||||
if (conv) {
|
||||
selectedConversations = [conv];
|
||||
await processImport(selectedConversations, options.verbose);
|
||||
importedInSession.add(conv.sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
if (importedInSession.size > 0) {
|
||||
p.outro(chalk.green(`✅ Imported ${importedInSession.size} conversation${importedInSession.size === 1 ? '' : 's'}`));
|
||||
} else {
|
||||
p.outro(chalk.yellow('No conversations imported'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (importMode === 'project') {
|
||||
// Project selection for importing entire project
|
||||
const projectOptions = Object.entries(projectGroups)
|
||||
.sort((a, b) => b[1][0].parsedDate.getTime() - a[1][0].parsedDate.getTime())
|
||||
.map(([project, convs]) => ({
|
||||
value: project,
|
||||
label: project,
|
||||
hint: `${convs.length} conversation${convs.length === 1 ? '' : 's'}, latest: ${convs[0].relativeDate}`
|
||||
}));
|
||||
|
||||
const selectedProject = await p.select({
|
||||
message: 'Select a project to import all conversations',
|
||||
options: projectOptions
|
||||
});
|
||||
|
||||
if (p.isCancel(selectedProject)) {
|
||||
p.cancel('Import cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
const projectConvs = projectGroups[selectedProject as string];
|
||||
|
||||
// Ask about title generation
|
||||
const generateTitles = await p.confirm({
|
||||
message: 'Would you like to generate titles for easier browsing?',
|
||||
initialValue: false
|
||||
});
|
||||
|
||||
if (p.isCancel(generateTitles)) {
|
||||
p.cancel('Import cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
if (generateTitles) {
|
||||
await processTitleGeneration(projectConvs, selectedProject as string);
|
||||
}
|
||||
|
||||
const confirm = await p.confirm({
|
||||
message: `Import all ${projectConvs.length} conversation${projectConvs.length === 1 ? '' : 's'} from ${selectedProject}?`
|
||||
});
|
||||
|
||||
if (p.isCancel(confirm) || !confirm) {
|
||||
p.cancel('Import cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
selectedConversations = projectConvs;
|
||||
|
||||
} else if (importMode === 'recent') {
|
||||
const limit = await p.text({
|
||||
message: 'How many recent conversations?',
|
||||
placeholder: '10',
|
||||
initialValue: '10',
|
||||
validate: (value) => {
|
||||
const num = parseInt(value);
|
||||
if (isNaN(num) || num < 1) return 'Please enter a valid number';
|
||||
if (num > conversations.length) return `Only ${conversations.length} available`;
|
||||
}
|
||||
});
|
||||
|
||||
if (p.isCancel(limit)) {
|
||||
p.cancel('Import cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
const count = parseInt(limit as string);
|
||||
selectedConversations = conversations.slice(0, count);
|
||||
|
||||
} else if (importMode === 'search') {
|
||||
const searchTerm = await p.text({
|
||||
message: 'Search conversations (project name or session ID)',
|
||||
placeholder: 'Enter search term'
|
||||
});
|
||||
|
||||
if (p.isCancel(searchTerm)) {
|
||||
p.cancel('Import cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
const term = (searchTerm as string).toLowerCase();
|
||||
const matches = conversations.filter(c =>
|
||||
c.projectName.toLowerCase().includes(term) ||
|
||||
c.sessionId.toLowerCase().includes(term) ||
|
||||
(c.branch && c.branch.toLowerCase().includes(term))
|
||||
);
|
||||
|
||||
if (matches.length === 0) {
|
||||
p.outro(chalk.yellow('No matching conversations found'));
|
||||
return;
|
||||
}
|
||||
|
||||
const titleGenerator = new TitleGenerator();
|
||||
const matchOptions = matches.map(conv => {
|
||||
const title = titleGenerator.getTitleForSession(conv.sessionId);
|
||||
const displayTitle = title ? `"${title}" • ` : '';
|
||||
return {
|
||||
value: conv.sessionId,
|
||||
label: `${displayTitle}${conv.projectName} • ${conv.relativeDate} • ${conv.messageCount} msgs`,
|
||||
hint: formatFileSize(conv.fileSize)
|
||||
};
|
||||
});
|
||||
|
||||
const selected = await p.multiselect({
|
||||
message: `Found ${matches.length} matches. Select to import:`,
|
||||
options: matchOptions,
|
||||
required: false
|
||||
});
|
||||
|
||||
if (p.isCancel(selected)) {
|
||||
p.cancel('Import cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIds = selected as string[];
|
||||
selectedConversations = matches.filter(c => selectedIds.includes(c.sessionId));
|
||||
}
|
||||
|
||||
// Process the import
|
||||
if (selectedConversations.length > 0) {
|
||||
await processImport(selectedConversations, options.verbose);
|
||||
p.outro(chalk.green(`✅ Successfully imported ${selectedConversations.length} conversation${selectedConversations.length === 1 ? '' : 's'}`));
|
||||
} else {
|
||||
p.outro(chalk.yellow('No conversations selected for import'));
|
||||
}
|
||||
}
|
||||
|
||||
async function processTitleGeneration(conversations: ConversationItem[], projectName: string) {
|
||||
const titleGenerator = new TitleGenerator();
|
||||
const existingTitles = titleGenerator.getExistingTitles();
|
||||
|
||||
// Filter conversations that don't have titles yet
|
||||
const conversationsNeedingTitles = conversations.filter(conv => !existingTitles.has(conv.sessionId));
|
||||
|
||||
if (conversationsNeedingTitles.length === 0) {
|
||||
p.note('All conversations already have titles!', 'Title Generation');
|
||||
return;
|
||||
}
|
||||
|
||||
const s = p.spinner();
|
||||
s.start(`Generating titles for ${conversationsNeedingTitles.length} conversations...`);
|
||||
|
||||
const requests: TitleGenerationRequest[] = conversationsNeedingTitles.map(conv => ({
|
||||
sessionId: conv.sessionId,
|
||||
projectName: projectName,
|
||||
firstMessage: extractFirstUserMessage(conv.filePath)
|
||||
}));
|
||||
|
||||
try {
|
||||
await titleGenerator.batchGenerateTitles(requests);
|
||||
s.stop(`✅ Generated ${conversationsNeedingTitles.length} titles`);
|
||||
} catch (error) {
|
||||
s.stop(`❌ Failed to generate titles`);
|
||||
console.error(chalk.red(`Error: ${error}`));
|
||||
}
|
||||
}
|
||||
|
||||
async function processImport(conversations: ConversationItem[], verbose?: boolean) {
|
||||
const s = p.spinner();
|
||||
|
||||
for (let i = 0; i < conversations.length; i++) {
|
||||
const conv = conversations[i];
|
||||
const progress = conversations.length > 1 ? `[${i + 1}/${conversations.length}] ` : '';
|
||||
|
||||
s.start(`${progress}Importing ${conv.projectName} (${conv.relativeDate})`);
|
||||
|
||||
try {
|
||||
// Extract project name from the conversation's cwd field
|
||||
const projectName = path.basename(conv.cwd);
|
||||
|
||||
// Use TranscriptCompressor to process
|
||||
const compressor = new TranscriptCompressor();
|
||||
await compressor.compress(conv.filePath, conv.sessionId, projectName);
|
||||
|
||||
s.stop(`${progress}Imported ${conv.projectName} (${conv.messageCount} messages)`);
|
||||
|
||||
if (verbose) {
|
||||
p.note(`Session: ${conv.sessionId}\nSize: ${formatFileSize(conv.fileSize)}\nBranch: ${conv.branch || 'main'}`, 'Details');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
s.stop(`${progress}Failed to import ${conv.projectName}`);
|
||||
if (verbose) {
|
||||
console.error(chalk.red(`Error: ${error}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,198 +0,0 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import fs from 'fs';
|
||||
import { join } from 'path';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
import {
|
||||
createCompletionMessage,
|
||||
createContextualError,
|
||||
createUserFriendlyError,
|
||||
formatTimeAgo,
|
||||
outputSessionStartContent
|
||||
} from '../prompts/templates/context/ContextTemplates.js';
|
||||
|
||||
interface IndexEntry {
|
||||
summary: string;
|
||||
entity: string;
|
||||
keywords: string[];
|
||||
}
|
||||
|
||||
interface TrashStatus {
|
||||
folderCount: number;
|
||||
fileCount: number;
|
||||
totalSize: number;
|
||||
isEmpty: boolean;
|
||||
}
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function getTrashStatus(): TrashStatus {
|
||||
const trashDir = PathDiscovery.getInstance().getTrashDirectory();
|
||||
|
||||
if (!fs.existsSync(trashDir)) {
|
||||
return { folderCount: 0, fileCount: 0, totalSize: 0, isEmpty: true };
|
||||
}
|
||||
|
||||
const items = fs.readdirSync(trashDir);
|
||||
if (items.length === 0) {
|
||||
return { folderCount: 0, fileCount: 0, totalSize: 0, isEmpty: true };
|
||||
}
|
||||
|
||||
let folderCount = 0;
|
||||
let fileCount = 0;
|
||||
let totalSize = 0;
|
||||
|
||||
for (const item of items) {
|
||||
const itemPath = join(trashDir, item);
|
||||
const stats = fs.statSync(itemPath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
folderCount++;
|
||||
} else {
|
||||
fileCount++;
|
||||
}
|
||||
|
||||
totalSize += stats.size;
|
||||
}
|
||||
|
||||
return { folderCount, fileCount, totalSize, isEmpty: false };
|
||||
}
|
||||
|
||||
export async function loadContext(options: OptionValues = {}): Promise<void> {
|
||||
const pathDiscovery = PathDiscovery.getInstance();
|
||||
const indexPath = pathDiscovery.getIndexPath();
|
||||
|
||||
try {
|
||||
// Check if index file exists
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
if (options.format === 'session-start') {
|
||||
console.log(createContextualError('NO_MEMORIES', options.project || 'this project'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(indexPath, 'utf-8');
|
||||
const lines = content.trim().split('\n').filter(line => line.trim());
|
||||
|
||||
if (lines.length === 0) {
|
||||
if (options.format === 'session-start') {
|
||||
console.log(createContextualError('NO_MEMORIES', options.project || 'this project'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse JSONL format - each line is a JSON object
|
||||
const jsonObjects: any[] = [];
|
||||
for (const line of lines) {
|
||||
try {
|
||||
// Skip lines that don't look like JSON (could be legacy format)
|
||||
if (!line.trim().startsWith('{')) {
|
||||
continue;
|
||||
}
|
||||
const obj = JSON.parse(line);
|
||||
jsonObjects.push(obj);
|
||||
} catch (e) {
|
||||
// Skip malformed JSON lines
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonObjects.length === 0) {
|
||||
if (options.format === 'session-start') {
|
||||
console.log(createContextualError('NO_MEMORIES', options.project || 'this project'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Separate memories, overviews, and other types
|
||||
const memories = jsonObjects.filter(obj => obj.type === 'memory');
|
||||
const overviews = jsonObjects.filter(obj => obj.type === 'overview');
|
||||
const sessions = jsonObjects.filter(obj => obj.type === 'session');
|
||||
|
||||
// Filter each type by project if specified
|
||||
let filteredMemories = memories;
|
||||
let filteredOverviews = overviews;
|
||||
if (options.project) {
|
||||
filteredMemories = memories.filter(obj => obj.project === options.project);
|
||||
filteredOverviews = overviews.filter(obj => obj.project === options.project);
|
||||
}
|
||||
|
||||
if (options.format === 'session-start') {
|
||||
// Get last 10 memories and last 5 overviews for session-start
|
||||
const recentMemories = filteredMemories.slice(-10);
|
||||
const recentOverviews = filteredOverviews.slice(-5);
|
||||
|
||||
// Combine them for the display
|
||||
const recentObjects = [...recentMemories, ...recentOverviews];
|
||||
|
||||
// Find most recent timestamp for last session info
|
||||
let lastSessionTime = 'recently';
|
||||
const timestamps = recentObjects
|
||||
.map(obj => {
|
||||
// Get timestamp from JSON object
|
||||
return obj.timestamp ? new Date(obj.timestamp) : null;
|
||||
})
|
||||
.filter(date => date !== null)
|
||||
.sort((a, b) => b.getTime() - a.getTime());
|
||||
|
||||
if (timestamps.length > 0) {
|
||||
lastSessionTime = formatTimeAgo(timestamps[0]);
|
||||
}
|
||||
|
||||
// Use dual-stream output for session start formatting
|
||||
outputSessionStartContent({
|
||||
projectName: options.project || 'your project',
|
||||
memoryCount: recentMemories.length,
|
||||
lastSessionTime,
|
||||
recentObjects
|
||||
});
|
||||
|
||||
} else if (options.format === 'json') {
|
||||
// For JSON format, combine last 10 of each type
|
||||
const recentMemories = filteredMemories.slice(-10);
|
||||
const recentOverviews = filteredOverviews.slice(-3);
|
||||
const recentObjects = [...recentMemories, ...recentOverviews];
|
||||
console.log(JSON.stringify(recentObjects));
|
||||
} else {
|
||||
// Default format - show last 10 memories and last 3 overviews
|
||||
const recentMemories = filteredMemories.slice(-10);
|
||||
const recentOverviews = filteredOverviews.slice(-3);
|
||||
const totalCount = recentMemories.length + recentOverviews.length;
|
||||
|
||||
console.log(createCompletionMessage('Context loading', totalCount, 'recent entries found'));
|
||||
|
||||
// Show memories first
|
||||
recentMemories.forEach((obj) => {
|
||||
console.log(`${obj.text} | ${obj.document_id} | ${obj.keywords}`);
|
||||
});
|
||||
|
||||
// Then show overviews
|
||||
recentOverviews.forEach((obj) => {
|
||||
console.log(`**Overview:** ${obj.content}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Display trash status if not empty (except for JSON format to avoid breaking JSON parsing)
|
||||
if (options.format !== 'json') {
|
||||
const trashStatus = getTrashStatus();
|
||||
if (!trashStatus.isEmpty) {
|
||||
const formattedSize = formatSize(trashStatus.totalSize);
|
||||
console.log(`🗑️ Trash – ${trashStatus.folderCount} folders | ${trashStatus.fileCount} files | ${formattedSize} – use \`$ claude-mem restore\``);
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
if (options.format === 'session-start') {
|
||||
console.log(createContextualError('CONNECTION_FAILED', errorMessage));
|
||||
} else {
|
||||
console.log(createUserFriendlyError('Context loading', errorMessage, 'Check file permissions and try again'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import { readFileSync, readdirSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
|
||||
// <Block> 1.1 ====================================
|
||||
async function showLog(logPath: string, logType: string, tail: number): Promise<void> {
|
||||
// <Block> 1.2 ====================================
|
||||
try {
|
||||
const content = readFileSync(logPath, 'utf8');
|
||||
const lines = content.split('\n').filter(line => line.trim());
|
||||
const displayLines = lines.slice(-tail);
|
||||
|
||||
console.log(`📋 ${logType} Logs (last ${tail} lines):`);
|
||||
console.log(` File: ${logPath}`);
|
||||
console.log('');
|
||||
|
||||
// <Block> 1.3 ====================================
|
||||
if (displayLines.length === 0) {
|
||||
console.log(' No log entries found');
|
||||
// </Block> =======================================
|
||||
} else {
|
||||
displayLines.forEach(line => {
|
||||
console.log(` ${line}`);
|
||||
});
|
||||
}
|
||||
// </Block> =======================================
|
||||
|
||||
console.log('');
|
||||
// </Block> =======================================
|
||||
} catch (error) {
|
||||
// <Block> 1.4 ====================================
|
||||
console.log(`❌ Could not read ${logType.toLowerCase()} log: ${logPath}`);
|
||||
// </Block> =======================================
|
||||
}
|
||||
// </Block> =======================================
|
||||
}
|
||||
|
||||
// <Block> 2.1 ====================================
|
||||
export async function logs(options: OptionValues = {}): Promise<void> {
|
||||
// <Block> 2.2 ====================================
|
||||
const logsDir = PathDiscovery.getLogsDirectory();
|
||||
const tail = parseInt(options.tail) || 20;
|
||||
// </Block> =======================================
|
||||
|
||||
// Find most recent log file
|
||||
try {
|
||||
const files = readdirSync(logsDir);
|
||||
const logFiles = files
|
||||
.filter(f => f.startsWith('claude-mem-') && f.endsWith('.log'))
|
||||
.map(f => ({
|
||||
name: f,
|
||||
path: join(logsDir, f),
|
||||
mtime: statSync(join(logsDir, f)).mtime
|
||||
}))
|
||||
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
||||
|
||||
if (logFiles.length === 0) {
|
||||
console.log('❌ No log files found in ~/.claude-mem/logs/');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show most recent log
|
||||
await showLog(logFiles[0].path, 'Most Recent', tail);
|
||||
|
||||
if (options.all && logFiles.length > 1) {
|
||||
console.log(`📚 Found ${logFiles.length} total log files`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('❌ Could not read logs directory: ~/.claude-mem/logs/');
|
||||
console.log(' Run a compression first to generate logs');
|
||||
}
|
||||
|
||||
// <Block> 2.5 ====================================
|
||||
if (options.follow) {
|
||||
console.log('Following logs... (Press Ctrl+C to stop)');
|
||||
// Basic follow implementation - would need more sophisticated watching in real usage
|
||||
setInterval(() => {
|
||||
// This would need proper file watching implementation
|
||||
}, 1000);
|
||||
}
|
||||
// </Block> =======================================
|
||||
// </Block> =======================================
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { readdirSync, renameSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import * as p from '@clack/prompts';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
|
||||
export async function restore(): Promise<void> {
|
||||
const trashDir = PathDiscovery.getInstance().getTrashDirectory();
|
||||
const files = readdirSync(trashDir);
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('Trash is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const file = await p.select({
|
||||
message: 'Select file to restore:',
|
||||
options: files.map(f => ({ value: f, label: f }))
|
||||
});
|
||||
|
||||
if (p.isCancel(file)) return;
|
||||
|
||||
renameSync(join(trashDir, file), join(process.cwd(), file));
|
||||
console.log(`Restored ${file}`);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import { appendFileSync } from 'fs';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
|
||||
/**
|
||||
* Generates a descriptive session ID from the message content
|
||||
* Takes first few meaningful words and creates a readable identifier
|
||||
*/
|
||||
function generateSessionId(message: string): string {
|
||||
// Remove punctuation and split into words
|
||||
const words = message
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s]/g, ' ')
|
||||
.split(/\s+/)
|
||||
.filter(word => word.length > 2); // Skip short words like 'a', 'is', 'to'
|
||||
|
||||
// Take first 3-4 meaningful words, max 30 chars
|
||||
const sessionWords = words.slice(0, 4).join('-');
|
||||
const truncated = sessionWords.length > 30 ? sessionWords.substring(0, 27) + '...' : sessionWords;
|
||||
|
||||
// Add timestamp suffix to ensure uniqueness
|
||||
const timestamp = new Date().toISOString().substring(11, 19).replace(/:/g, '');
|
||||
|
||||
return `${truncated}-${timestamp}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save command - stores a message to both Chroma collection and JSONL index
|
||||
*/
|
||||
export async function save(message: string, options: OptionValues = {}): Promise<void> {
|
||||
if (!message || message.trim() === '') {
|
||||
console.error('Error: Message is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pathDiscovery = PathDiscovery.getInstance();
|
||||
const timestamp = new Date().toISOString();
|
||||
const projectName = PathDiscovery.getCurrentProjectName();
|
||||
const sessionId = generateSessionId(message);
|
||||
const documentId = `${projectName}_${sessionId}_overview`;
|
||||
|
||||
// 1. Save to Chroma collection (skip for now - MCP tools only available in Claude Code context)
|
||||
// TODO: Add Chroma integration when called from Claude Code with MCP server running
|
||||
|
||||
// 2. Append to JSONL index file
|
||||
const indexPath = pathDiscovery.getIndexPath();
|
||||
const indexEntry = {
|
||||
type: "overview",
|
||||
content: message,
|
||||
session_id: sessionId,
|
||||
project: projectName,
|
||||
timestamp: timestamp
|
||||
};
|
||||
|
||||
// Ensure the directory exists
|
||||
pathDiscovery.ensureDirectory(pathDiscovery.getDataDirectory());
|
||||
|
||||
// Append to JSONL file
|
||||
appendFileSync(indexPath, JSON.stringify(indexEntry) + '\n', 'utf8');
|
||||
|
||||
// 3. Return JSON response for hook compatibility
|
||||
console.log(JSON.stringify({
|
||||
success: true,
|
||||
document_id: documentId,
|
||||
session_id: sessionId,
|
||||
project: projectName,
|
||||
timestamp: timestamp,
|
||||
suppressOutput: true
|
||||
}));
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
||||
import { join, resolve, dirname } from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export async function status(): Promise<void> {
|
||||
console.log('🔍 Claude Memory System Status Check');
|
||||
console.log('=====================================\n');
|
||||
|
||||
console.log('📂 Installed Hook Scripts:');
|
||||
const pathDiscovery = PathDiscovery.getInstance();
|
||||
const claudeMemHooksDir = pathDiscovery.getHooksDirectory();
|
||||
const preCompactScript = join(claudeMemHooksDir, 'pre-compact.js');
|
||||
const sessionStartScript = join(claudeMemHooksDir, 'session-start.js');
|
||||
const sessionEndScript = join(claudeMemHooksDir, 'session-end.js');
|
||||
|
||||
const checkScript = (path: string, name: string) => {
|
||||
if (existsSync(path)) {
|
||||
console.log(` ✅ ${name}: Found at ${path}`);
|
||||
} else {
|
||||
console.log(` ❌ ${name}: Not found at ${path}`);
|
||||
}
|
||||
};
|
||||
|
||||
checkScript(preCompactScript, 'pre-compact.js');
|
||||
checkScript(sessionStartScript, 'session-start.js');
|
||||
checkScript(sessionEndScript, 'session-end.js');
|
||||
|
||||
console.log('');
|
||||
|
||||
console.log('⚙️ Settings Configuration:');
|
||||
|
||||
const checkSettings = (name: string, path: string) => {
|
||||
if (!existsSync(path)) {
|
||||
console.log(` ⏭️ ${name}: No settings file`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(` 📋 ${name}: ${path}`);
|
||||
|
||||
try {
|
||||
const settings = JSON.parse(readFileSync(path, 'utf8'));
|
||||
|
||||
const hasPreCompact = settings.hooks?.PreCompact?.some((matcher: any) =>
|
||||
matcher.hooks?.some((hook: any) =>
|
||||
hook.command?.includes('pre-compact.js') || hook.command?.includes('claude-mem')
|
||||
)
|
||||
);
|
||||
|
||||
const hasSessionStart = settings.hooks?.SessionStart?.some((matcher: any) =>
|
||||
matcher.hooks?.some((hook: any) =>
|
||||
hook.command?.includes('session-start.js') || hook.command?.includes('claude-mem')
|
||||
)
|
||||
);
|
||||
|
||||
const hasSessionEnd = settings.hooks?.SessionEnd?.some((matcher: any) =>
|
||||
matcher.hooks?.some((hook: any) =>
|
||||
hook.command?.includes('session-end.js') || hook.command?.includes('claude-mem')
|
||||
)
|
||||
);
|
||||
|
||||
console.log(` PreCompact: ${hasPreCompact ? '✅' : '❌'}`);
|
||||
console.log(` SessionStart: ${hasSessionStart ? '✅' : '❌'}`);
|
||||
console.log(` SessionEnd: ${hasSessionEnd ? '✅' : '❌'}`);
|
||||
|
||||
} catch (error: any) {
|
||||
console.log(` ⚠️ Could not parse settings`);
|
||||
}
|
||||
};
|
||||
|
||||
checkSettings('Global', pathDiscovery.getClaudeSettingsPath());
|
||||
checkSettings('Project', join(process.cwd(), '.claude', 'settings.json'));
|
||||
|
||||
console.log('');
|
||||
|
||||
console.log('📦 Compressed Transcripts:');
|
||||
const claudeProjectsDir = join(pathDiscovery.getClaudeConfigDirectory(), 'projects');
|
||||
|
||||
if (existsSync(claudeProjectsDir)) {
|
||||
try {
|
||||
let compressedCount = 0;
|
||||
let archiveCount = 0;
|
||||
|
||||
const searchDir = (dir: string, depth = 0) => {
|
||||
if (depth > 3) return;
|
||||
|
||||
const files = readdirSync(dir);
|
||||
for (const file of files) {
|
||||
const fullPath = join(dir, file);
|
||||
const stats = statSync(fullPath);
|
||||
|
||||
if (stats.isDirectory() && !file.startsWith('.')) {
|
||||
searchDir(fullPath, depth + 1);
|
||||
} else if (file.endsWith('.jsonl.compressed')) {
|
||||
compressedCount++;
|
||||
} else if (file.endsWith('.jsonl.archive')) {
|
||||
archiveCount++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
searchDir(claudeProjectsDir);
|
||||
|
||||
console.log(` Compressed files: ${compressedCount}`);
|
||||
console.log(` Archive files: ${archiveCount}`);
|
||||
|
||||
} catch (error) {
|
||||
console.log(` ⚠️ Could not scan projects directory`);
|
||||
}
|
||||
} else {
|
||||
console.log(` ℹ️ No Claude projects directory found`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
console.log('🔧 Runtime Environment:');
|
||||
|
||||
const checkCommand = (cmd: string, name: string) => {
|
||||
try {
|
||||
const version = execSync(`${cmd} --version`, { encoding: 'utf8' }).trim();
|
||||
console.log(` ✅ ${name}: ${version}`);
|
||||
} catch {
|
||||
console.log(` ❌ ${name}: Not found`);
|
||||
}
|
||||
};
|
||||
|
||||
checkCommand('node', 'Node.js');
|
||||
checkCommand('bun', 'Bun');
|
||||
|
||||
console.log('');
|
||||
|
||||
console.log('🧠 Chroma Storage Status:');
|
||||
console.log(' ✅ Storage backend: Chroma MCP');
|
||||
console.log(` 📍 Data location: ${pathDiscovery.getChromaDirectory()}`);
|
||||
console.log(' 🔍 Features: Vector search, semantic similarity, document storage');
|
||||
|
||||
console.log('');
|
||||
|
||||
console.log('📊 Summary:');
|
||||
const globalPath = pathDiscovery.getClaudeSettingsPath();
|
||||
const projectPath = join(process.cwd(), '.claude', 'settings.json');
|
||||
|
||||
let isInstalled = false;
|
||||
let installLocation = 'Not installed';
|
||||
|
||||
try {
|
||||
if (existsSync(globalPath)) {
|
||||
const settings = JSON.parse(readFileSync(globalPath, 'utf8'));
|
||||
if (settings.hooks?.PreCompact || settings.hooks?.SessionStart || settings.hooks?.SessionEnd) {
|
||||
isInstalled = true;
|
||||
installLocation = 'Global';
|
||||
}
|
||||
}
|
||||
|
||||
if (existsSync(projectPath)) {
|
||||
const settings = JSON.parse(readFileSync(projectPath, 'utf8'));
|
||||
if (settings.hooks?.PreCompact || settings.hooks?.SessionStart || settings.hooks?.SessionEnd) {
|
||||
isInstalled = true;
|
||||
installLocation = installLocation === 'Global' ? 'Global + Project' : 'Project';
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (isInstalled) {
|
||||
console.log(` ✅ Claude Memory System is installed (${installLocation})`);
|
||||
console.log('');
|
||||
console.log('💡 To test: Use /compact in Claude Code');
|
||||
} else {
|
||||
console.log(` ❌ Claude Memory System is not installed`);
|
||||
console.log('');
|
||||
console.log('💡 To install: claude-mem install');
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import { rmSync, readdirSync, existsSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import * as p from '@clack/prompts';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
|
||||
export async function emptyTrash(options: { force?: boolean } = {}): Promise<void> {
|
||||
const trashDir = PathDiscovery.getInstance().getTrashDirectory();
|
||||
|
||||
// Check if trash directory exists
|
||||
if (!existsSync(trashDir)) {
|
||||
p.log.info('🗑️ Trash is already empty');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const files = readdirSync(trashDir);
|
||||
|
||||
if (files.length === 0) {
|
||||
p.log.info('🗑️ Trash is already empty');
|
||||
return;
|
||||
}
|
||||
|
||||
// Count items
|
||||
let folderCount = 0;
|
||||
let fileCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = join(trashDir, file);
|
||||
const stats = statSync(filePath);
|
||||
if (stats.isDirectory()) {
|
||||
folderCount++;
|
||||
} else {
|
||||
fileCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm deletion unless --force flag is used
|
||||
if (!options.force) {
|
||||
const confirm = await p.confirm({
|
||||
message: `Permanently delete ${folderCount} folders and ${fileCount} files from trash?`,
|
||||
initialValue: false
|
||||
});
|
||||
|
||||
if (p.isCancel(confirm) || !confirm) {
|
||||
p.log.info('Cancelled - trash not emptied');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all files in trash
|
||||
const s = p.spinner();
|
||||
s.start('Emptying trash...');
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = join(trashDir, file);
|
||||
rmSync(filePath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
s.stop(`🗑️ Trash emptied - permanently deleted ${folderCount} folders and ${fileCount} files`);
|
||||
|
||||
} catch (error) {
|
||||
p.log.error('Failed to empty trash');
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import { readdirSync, statSync } from 'fs';
|
||||
import { join, basename } from 'path';
|
||||
import * as p from '@clack/prompts';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
|
||||
interface TrashItem {
|
||||
originalName: string;
|
||||
trashedName: string;
|
||||
size: number;
|
||||
trashedAt: Date;
|
||||
isDirectory: boolean;
|
||||
}
|
||||
|
||||
function parseTrashName(filename: string): { name: string; timestamp: number } {
|
||||
const lastDotIndex = filename.lastIndexOf('.');
|
||||
if (lastDotIndex === -1) return { name: filename, timestamp: 0 };
|
||||
|
||||
const timestamp = parseInt(filename.substring(lastDotIndex + 1));
|
||||
if (isNaN(timestamp)) return { name: filename, timestamp: 0 };
|
||||
|
||||
return {
|
||||
name: filename.substring(0, lastDotIndex),
|
||||
timestamp
|
||||
};
|
||||
}
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function getDirectorySize(dirPath: string): number {
|
||||
let size = 0;
|
||||
const files = readdirSync(dirPath);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = join(dirPath, file);
|
||||
const stats = statSync(filePath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
size += getDirectorySize(filePath);
|
||||
} else {
|
||||
size += stats.size;
|
||||
}
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
export async function viewTrash(): Promise<void> {
|
||||
const trashDir = PathDiscovery.getInstance().getTrashDirectory();
|
||||
|
||||
try {
|
||||
const files = readdirSync(trashDir);
|
||||
|
||||
if (files.length === 0) {
|
||||
p.log.info('🗑️ Trash is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const items: TrashItem[] = files.map(file => {
|
||||
const filePath = join(trashDir, file);
|
||||
const stats = statSync(filePath);
|
||||
const { name, timestamp } = parseTrashName(file);
|
||||
|
||||
const size = stats.isDirectory() ? getDirectorySize(filePath) : stats.size;
|
||||
|
||||
return {
|
||||
originalName: name,
|
||||
trashedName: file,
|
||||
size,
|
||||
trashedAt: new Date(timestamp),
|
||||
isDirectory: stats.isDirectory()
|
||||
};
|
||||
});
|
||||
|
||||
// Sort by date, newest first
|
||||
items.sort((a, b) => b.trashedAt.getTime() - a.trashedAt.getTime());
|
||||
|
||||
// Display header
|
||||
console.log('\n🗑️ Trash Contents\n');
|
||||
console.log('─'.repeat(80));
|
||||
|
||||
// Display items
|
||||
let totalSize = 0;
|
||||
let folderCount = 0;
|
||||
let fileCount = 0;
|
||||
|
||||
for (const item of items) {
|
||||
totalSize += item.size;
|
||||
if (item.isDirectory) {
|
||||
folderCount++;
|
||||
} else {
|
||||
fileCount++;
|
||||
}
|
||||
|
||||
const type = item.isDirectory ? '📁' : '📄';
|
||||
const date = item.trashedAt.toLocaleString();
|
||||
const size = formatSize(item.size);
|
||||
|
||||
console.log(`${type} ${item.originalName}`);
|
||||
console.log(` Size: ${size} | Trashed: ${date}`);
|
||||
console.log(` ID: ${item.trashedName}`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Display summary
|
||||
console.log('─'.repeat(80));
|
||||
console.log(`Total: ${folderCount} folders, ${fileCount} files (${formatSize(totalSize)})`);
|
||||
console.log('\nTo restore files: claude-mem restore');
|
||||
console.log('To empty trash: claude-mem trash empty');
|
||||
|
||||
} catch (error) {
|
||||
if ((error as any).code === 'ENOENT') {
|
||||
p.log.info('🗑️ Trash is empty');
|
||||
} else {
|
||||
p.log.error('Failed to read trash directory');
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { renameSync, existsSync, mkdirSync, statSync } from 'fs';
|
||||
import { join, basename } from 'path';
|
||||
import { glob } from 'glob';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
|
||||
interface TrashOptions {
|
||||
force?: boolean;
|
||||
recursive?: boolean;
|
||||
}
|
||||
|
||||
export async function trash(filePaths: string | string[], options: TrashOptions = {}): Promise<void> {
|
||||
const trashDir = PathDiscovery.getInstance().getTrashDirectory();
|
||||
if (!existsSync(trashDir)) mkdirSync(trashDir, { recursive: true });
|
||||
|
||||
// Handle single string or array of paths
|
||||
const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
|
||||
|
||||
for (const filePath of paths) {
|
||||
// Handle glob patterns
|
||||
const expandedPaths = await glob(filePath);
|
||||
const actualPaths = expandedPaths.length > 0 ? expandedPaths : [filePath];
|
||||
|
||||
for (const actualPath of actualPaths) {
|
||||
try {
|
||||
// Check if file exists
|
||||
if (!existsSync(actualPath)) {
|
||||
if (!options.force) {
|
||||
console.error(`trash: ${actualPath}: No such file or directory`);
|
||||
continue;
|
||||
}
|
||||
// With -f, silently skip missing files
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if it's a directory and we need recursive
|
||||
const stats = statSync(actualPath);
|
||||
if (stats.isDirectory() && !options.recursive) {
|
||||
if (!options.force) {
|
||||
console.error(`trash: ${actualPath}: is a directory`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate unique destination name to avoid conflicts
|
||||
const fileName = basename(actualPath);
|
||||
const timestamp = Date.now();
|
||||
const destination = join(trashDir, `${fileName}.${timestamp}`);
|
||||
|
||||
renameSync(actualPath, destination);
|
||||
console.log(`Moved ${fileName} to trash`);
|
||||
|
||||
} catch (error) {
|
||||
if (!options.force) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`trash: ${actualPath}: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
|
||||
export async function uninstall(options: OptionValues = {}): Promise<void> {
|
||||
console.log('🔄 Uninstalling Claude Memory System hooks...');
|
||||
|
||||
const locations = [];
|
||||
if (options.all) {
|
||||
locations.push({
|
||||
name: 'User',
|
||||
path: PathDiscovery.getInstance().getClaudeSettingsPath()
|
||||
});
|
||||
locations.push({
|
||||
name: 'Project',
|
||||
path: join(process.cwd(), '.claude', 'settings.json')
|
||||
});
|
||||
} else {
|
||||
const isProject = options.project;
|
||||
const pathDiscovery = PathDiscovery.getInstance();
|
||||
locations.push({
|
||||
name: isProject ? 'Project' : 'User',
|
||||
path: isProject ? join(process.cwd(), '.claude', 'settings.json') : pathDiscovery.getClaudeSettingsPath()
|
||||
});
|
||||
}
|
||||
|
||||
const pathDiscovery = PathDiscovery.getInstance();
|
||||
const claudeMemHooksDir = pathDiscovery.getHooksDirectory();
|
||||
const preCompactScript = join(claudeMemHooksDir, 'pre-compact.js');
|
||||
const sessionStartScript = join(claudeMemHooksDir, 'session-start.js');
|
||||
const sessionEndScript = join(claudeMemHooksDir, 'session-end.js');
|
||||
|
||||
let removedCount = 0;
|
||||
|
||||
for (const location of locations) {
|
||||
if (!existsSync(location.path)) {
|
||||
console.log(`⏭️ No settings found at ${location.name} location`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(location.path, 'utf8');
|
||||
const settings = JSON.parse(content);
|
||||
|
||||
if (!settings.hooks) {
|
||||
console.log(`⏭️ No hooks configured in ${location.name} settings`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let modified = false;
|
||||
|
||||
if (settings.hooks.PreCompact) {
|
||||
const filteredPreCompact = settings.hooks.PreCompact.filter((matcher: any) =>
|
||||
!matcher.hooks?.some((hook: any) =>
|
||||
hook.command === preCompactScript ||
|
||||
hook.command?.includes('pre-compact.js') ||
|
||||
hook.command?.includes('claude-mem')
|
||||
)
|
||||
);
|
||||
|
||||
if (filteredPreCompact.length !== settings.hooks.PreCompact.length) {
|
||||
settings.hooks.PreCompact = filteredPreCompact.length ? filteredPreCompact : undefined;
|
||||
modified = true;
|
||||
console.log(`✅ Removed PreCompact hook from ${location.name} settings`);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.hooks.SessionStart) {
|
||||
const filteredSessionStart = settings.hooks.SessionStart.filter((matcher: any) =>
|
||||
!matcher.hooks?.some((hook: any) =>
|
||||
hook.command === sessionStartScript ||
|
||||
hook.command?.includes('session-start.js') ||
|
||||
hook.command?.includes('claude-mem')
|
||||
)
|
||||
);
|
||||
|
||||
if (filteredSessionStart.length !== settings.hooks.SessionStart.length) {
|
||||
settings.hooks.SessionStart = filteredSessionStart.length ? filteredSessionStart : undefined;
|
||||
modified = true;
|
||||
console.log(`✅ Removed SessionStart hook from ${location.name} settings`);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.hooks.SessionEnd) {
|
||||
const filteredSessionEnd = settings.hooks.SessionEnd.filter((matcher: any) =>
|
||||
!matcher.hooks?.some((hook: any) =>
|
||||
hook.command === sessionEndScript ||
|
||||
hook.command?.includes('session-end.js') ||
|
||||
hook.command?.includes('claude-mem')
|
||||
)
|
||||
);
|
||||
|
||||
if (filteredSessionEnd.length !== settings.hooks.SessionEnd.length) {
|
||||
settings.hooks.SessionEnd = filteredSessionEnd.length ? filteredSessionEnd : undefined;
|
||||
modified = true;
|
||||
console.log(`✅ Removed SessionEnd hook from ${location.name} settings`);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.hooks.PreCompact === undefined) delete settings.hooks.PreCompact;
|
||||
if (settings.hooks.SessionStart === undefined) delete settings.hooks.SessionStart;
|
||||
if (settings.hooks.SessionEnd === undefined) delete settings.hooks.SessionEnd;
|
||||
if (!Object.keys(settings.hooks).length) delete settings.hooks;
|
||||
|
||||
if (modified) {
|
||||
const backupPath = location.path + '.backup.' + Date.now();
|
||||
writeFileSync(backupPath, content);
|
||||
console.log(`📋 Created backup: ${backupPath}`);
|
||||
|
||||
writeFileSync(location.path, JSON.stringify(settings, null, 2));
|
||||
removedCount++;
|
||||
console.log(`✅ Updated ${location.name} settings: ${location.path}`);
|
||||
} else {
|
||||
console.log(`ℹ️ No Claude Memory System hooks found in ${location.name} settings`);
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.log(`⚠️ Could not process ${location.name} settings: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
if (removedCount > 0) {
|
||||
console.log('✨ Uninstallation complete!');
|
||||
console.log('The Claude Memory System hooks have been removed from your settings.');
|
||||
console.log('');
|
||||
console.log('Note: Your compressed transcripts and archives are preserved.');
|
||||
console.log('To reinstall: claude-mem install');
|
||||
} else {
|
||||
console.log('ℹ️ No Claude Memory System hooks were found to remove.');
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
/**
|
||||
* Claude Memory System - Core Constants
|
||||
*
|
||||
* This file contains core application constants, CLI messages,
|
||||
* configuration templates, and infrastructure-related constants.
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURATION TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Hook configuration templates for Claude settings
|
||||
*/
|
||||
export const HOOK_CONFIG_TEMPLATES = {
|
||||
PRE_COMPACT: (scriptPath: string) => ({
|
||||
pattern: "*",
|
||||
hooks: [{
|
||||
type: "command",
|
||||
command: scriptPath,
|
||||
timeout: 180
|
||||
}]
|
||||
}),
|
||||
|
||||
SESSION_START: (scriptPath: string) => ({
|
||||
pattern: "*",
|
||||
hooks: [{
|
||||
type: "command",
|
||||
command: scriptPath,
|
||||
timeout: 30
|
||||
}]
|
||||
}),
|
||||
|
||||
SESSION_END: (scriptPath: string) => ({
|
||||
pattern: "*",
|
||||
hooks: [{
|
||||
type: "command",
|
||||
command: scriptPath,
|
||||
timeout: 180
|
||||
}]
|
||||
})
|
||||
} as const;
|
||||
|
||||
// =============================================================================
|
||||
// CLI MESSAGES AND STATUS TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Command-line interface messages
|
||||
*/
|
||||
export const CLI_MESSAGES = {
|
||||
INSTALLATION: {
|
||||
STARTING: '🚀 Installing Claude Memory System with Chroma...',
|
||||
SUCCESS: '🎉 Installation complete! Vector database ready.',
|
||||
HOOKS_INSTALLED: '✅ Installed hooks to ~/.claude-mem/hooks/',
|
||||
MCP_CONFIGURED: (path: string) => `✅ Configured MCP memory server in ${path}`,
|
||||
EMBEDDED_READY: '🧠 Chroma initialized for persistent semantic memory',
|
||||
ALREADY_INSTALLED: '⚠️ Claude Memory hooks are already installed.',
|
||||
USE_FORCE: ' Use --force to overwrite existing installation.',
|
||||
SETTINGS_WRITTEN: (type: string, path: string) =>
|
||||
`✅ Installed hooks in ${type} settings\n Settings file: ${path}`
|
||||
},
|
||||
|
||||
NEXT_STEPS: [
|
||||
'1. Restart Claude Code to load the new hooks',
|
||||
'2. Use `/clear` and `/compact` in Claude Code to save and compress session memories',
|
||||
'3. New sessions will automatically load relevant context'
|
||||
],
|
||||
|
||||
ERRORS: {
|
||||
HOOKS_NOT_FOUND: '❌ Hook source files not found',
|
||||
SETTINGS_WRITE_FAILED: (path: string, error: string) =>
|
||||
`❌ Failed to write settings file: ${error}\n Path: ${path}`,
|
||||
MCP_CONFIG_PARSE_FAILED: (error: string) =>
|
||||
`⚠️ Warning: Could not parse existing MCP config: ${error}`,
|
||||
MCP_CONFIG_WRITE_FAILED: (error: string) =>
|
||||
`⚠️ Warning: Could not write MCP config: ${error}`,
|
||||
COMPRESSION_FAILED: (error: string) => `❌ Compression failed: ${error}`,
|
||||
CONTEXT_LOAD_FAILED: (error: string) => `❌ Failed to load context: ${error}`
|
||||
},
|
||||
|
||||
STATUS: {
|
||||
NO_INDEX: '📚 No memory index found. Starting fresh session.',
|
||||
RECENT_MEMORIES: '🧠 Recent memories from previous sessions:',
|
||||
MEMORY_COUNT: (count: number) => `📚 Showing ${count} most recent memories`,
|
||||
FULL_CONTEXT_AVAILABLE: '💡 Full context available via MCP memory tools'
|
||||
}
|
||||
} as const;
|
||||
|
||||
// =============================================================================
|
||||
// DEBUG AND LOGGING TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Debug logging message templates
|
||||
*/
|
||||
export const DEBUG_MESSAGES = {
|
||||
COMPRESSION_STARTED: '🚀 COMPRESSION STARTED',
|
||||
TRANSCRIPT_PATH: (path: string) => `📁 Transcript Path: ${path}`,
|
||||
SESSION_ID: (id: string) => `🔍 Session ID: ${id}`,
|
||||
PROJECT_NAME: (name: string) => `📝 PROJECT NAME: ${name}`,
|
||||
CLAUDE_SDK_CALL: '🤖 Calling Claude SDK to analyze and populate memory database...',
|
||||
TRANSCRIPT_STATS: (size: number, count: number) =>
|
||||
`📊 Transcript size: ${size} characters, ${count} messages`,
|
||||
COMPRESSION_COMPLETE: (count: number) => `✅ COMPRESSION COMPLETE\n Total summaries extracted: ${count}`,
|
||||
CLAUDE_PATH_FOUND: (path: string) => `🎯 Found Claude Code at: ${path}`,
|
||||
MCP_CONFIG_USED: (path: string) => `📋 Using MCP config: ${path}`
|
||||
} as const;
|
||||
|
||||
// =============================================================================
|
||||
// SEARCH AND QUERY TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Memory database search templates
|
||||
*/
|
||||
export const SEARCH_TEMPLATES = {
|
||||
SEARCH_SCRIPT: (query: string) => `
|
||||
import { query } from "@anthropic-ai/claude-code";
|
||||
|
||||
const searchQuery = process.env.SEARCH_QUERY || '';
|
||||
|
||||
const result = await query({
|
||||
prompt: "Search for: " + searchQuery,
|
||||
options: {
|
||||
mcpConfig: "~/.claude/.mcp.json",
|
||||
allowedTools: ["mcp__claude-mem__chroma_query_documents"],
|
||||
outputFormat: "json"
|
||||
}
|
||||
});
|
||||
`,
|
||||
|
||||
SEARCH_PREFIX: "Search for: "
|
||||
} as const;
|
||||
|
||||
// =============================================================================
|
||||
// CHROMA INTEGRATION CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Chroma collection names for documents
|
||||
*/
|
||||
export const CHROMA_COLLECTIONS = {
|
||||
DOCUMENTS: 'claude_mem_documents',
|
||||
MEMORIES: 'claude_mem_memories'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Default Chroma configuration values
|
||||
*/
|
||||
export const CHROMA_DEFAULTS = {
|
||||
HOST: 'localhost:8000',
|
||||
COLLECTION: 'claude_mem_documents'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Chroma-specific CLI messages
|
||||
*/
|
||||
export const CHROMA_MESSAGES = {
|
||||
CONNECTION: {
|
||||
CONNECTING: '🔗 Connecting to Chroma server...',
|
||||
CONNECTED: '✅ Connected to Chroma successfully',
|
||||
FAILED: (error: string) => `❌ Failed to connect to Chroma: ${error}`,
|
||||
DISCONNECTED: '👋 Disconnected from Chroma'
|
||||
},
|
||||
|
||||
SEARCH: {
|
||||
SEMANTIC_SEARCH: '🧠 Using semantic search with Chroma...',
|
||||
KEYWORD_SEARCH: '🔍 Using keyword search with Chroma...',
|
||||
HYBRID_SEARCH: '🔬 Using hybrid search with Chroma...',
|
||||
RESULTS_FOUND: (count: number) => `📊 Found ${count} results in Chroma`
|
||||
},
|
||||
|
||||
SETUP: {
|
||||
STARTING_CHROMA: '🚀 Starting Chroma instance...',
|
||||
CHROMA_READY: '✅ Chroma is ready and accepting connections',
|
||||
INITIALIZING_COLLECTIONS: '📋 Initializing document collections...'
|
||||
}
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Chroma error messages
|
||||
*/
|
||||
export const CHROMA_ERRORS = {
|
||||
CONNECTION_FAILED: 'Could not establish connection to Chroma server',
|
||||
MCP_SERVER_NOT_FOUND: 'Chroma MCP server not found',
|
||||
INVALID_COLLECTION: (collection: string) => `Invalid Chroma collection: ${collection}`,
|
||||
QUERY_FAILED: (query: string, error: string) => `Query failed for '${query}': ${error}`,
|
||||
DOCUMENT_CREATION_FAILED: (id: string) => `Failed to create document '${id}' in Chroma`,
|
||||
COLLECTION_CREATION_FAILED: (name: string) => `Failed to create collection '${name}' in Chroma`
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Export all core constants for easy importing
|
||||
*/
|
||||
export const CONSTANTS = {
|
||||
HOOK_CONFIG_TEMPLATES,
|
||||
CLI_MESSAGES,
|
||||
DEBUG_MESSAGES,
|
||||
SEARCH_TEMPLATES,
|
||||
// Chroma constants
|
||||
CHROMA_COLLECTIONS,
|
||||
CHROMA_DEFAULTS,
|
||||
CHROMA_MESSAGES,
|
||||
CHROMA_ERRORS
|
||||
} as const;
|
||||
@@ -1,238 +0,0 @@
|
||||
/**
|
||||
* ChunkManager - Handles intelligent chunking of large transcripts
|
||||
*
|
||||
* This class manages the splitting of large filtered transcripts into chunks
|
||||
* that fit within Claude's 32k token limit while preserving conversation context
|
||||
* and maintaining message integrity.
|
||||
*/
|
||||
|
||||
export interface ChunkMetadata {
|
||||
chunkNumber: number;
|
||||
totalChunks: number;
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
messageCount: number;
|
||||
estimatedTokens: number;
|
||||
sizeBytes: number;
|
||||
hasOverlap: boolean;
|
||||
overlapMessages?: number;
|
||||
firstTimestamp?: string;
|
||||
lastTimestamp?: string;
|
||||
}
|
||||
|
||||
export interface ChunkingOptions {
|
||||
maxTokensPerChunk?: number; // default: 28000 (leaving 4k buffer)
|
||||
maxBytesPerChunk?: number; // default: 98000 (98KB)
|
||||
preserveContext?: boolean; // keep context overlap between chunks
|
||||
contextOverlap?: number; // messages to repeat (default: 2)
|
||||
parallel?: boolean; // process chunks in parallel
|
||||
}
|
||||
|
||||
export interface ChunkedMessage {
|
||||
content: string;
|
||||
estimatedTokens: number;
|
||||
}
|
||||
|
||||
export class ChunkManager {
|
||||
private static readonly DEFAULT_MAX_TOKENS = 28000;
|
||||
private static readonly DEFAULT_MAX_BYTES = 98000;
|
||||
private static readonly DEFAULT_CONTEXT_OVERLAP = 2;
|
||||
private static readonly CHARS_PER_TOKEN_ESTIMATE = 3.5;
|
||||
|
||||
private options: Required<ChunkingOptions>;
|
||||
|
||||
constructor(options: ChunkingOptions = {}) {
|
||||
this.options = {
|
||||
maxTokensPerChunk: options.maxTokensPerChunk ?? ChunkManager.DEFAULT_MAX_TOKENS,
|
||||
maxBytesPerChunk: options.maxBytesPerChunk ?? ChunkManager.DEFAULT_MAX_BYTES,
|
||||
preserveContext: options.preserveContext ?? true,
|
||||
contextOverlap: options.contextOverlap ?? ChunkManager.DEFAULT_CONTEXT_OVERLAP,
|
||||
parallel: options.parallel ?? false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates token count for a given text
|
||||
* Uses rough approximation of 3.5 characters per token
|
||||
*/
|
||||
public estimateTokenCount(text: string): number {
|
||||
return Math.ceil(text.length / ChunkManager.CHARS_PER_TOKEN_ESTIMATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the filtered output format into structured messages
|
||||
* Format: "- content"
|
||||
*/
|
||||
public parseFilteredOutput(filteredContent: string): ChunkedMessage[] {
|
||||
const lines = filteredContent.split('\n').filter(line => line.trim());
|
||||
const messages: ChunkedMessage[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
// Parse format: "- content"
|
||||
if (line.startsWith('- ')) {
|
||||
const content = line.substring(2); // Remove "- " prefix
|
||||
messages.push({
|
||||
content,
|
||||
estimatedTokens: this.estimateTokenCount(content)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunks the filtered transcript into manageable pieces
|
||||
*/
|
||||
public chunkTranscript(filteredContent: string): Array<{ content: string; metadata: ChunkMetadata }> {
|
||||
const messages = this.parseFilteredOutput(filteredContent);
|
||||
const chunks: Array<{ content: string; metadata: ChunkMetadata }> = [];
|
||||
|
||||
let currentChunk: ChunkedMessage[] = [];
|
||||
let currentTokens = 0;
|
||||
let currentBytes = 0;
|
||||
let chunkStartIndex = 0;
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const message = messages[i];
|
||||
const messageText = this.formatMessage(message);
|
||||
const messageBytes = Buffer.byteLength(messageText, 'utf8');
|
||||
const messageTokens = message.estimatedTokens;
|
||||
|
||||
// Check if adding this message would exceed limits
|
||||
if (currentChunk.length > 0 &&
|
||||
(currentTokens + messageTokens > this.options.maxTokensPerChunk ||
|
||||
currentBytes + messageBytes > this.options.maxBytesPerChunk)) {
|
||||
|
||||
// Save current chunk
|
||||
const chunkContent = this.formatChunk(currentChunk);
|
||||
chunks.push({
|
||||
content: chunkContent,
|
||||
metadata: {
|
||||
chunkNumber: chunks.length + 1,
|
||||
totalChunks: 0, // Will be updated after all chunks are created
|
||||
startIndex: chunkStartIndex,
|
||||
endIndex: i - 1,
|
||||
messageCount: currentChunk.length,
|
||||
estimatedTokens: currentTokens,
|
||||
sizeBytes: currentBytes,
|
||||
hasOverlap: false
|
||||
}
|
||||
});
|
||||
|
||||
// Start new chunk with optional context overlap
|
||||
currentChunk = [];
|
||||
currentTokens = 0;
|
||||
currentBytes = 0;
|
||||
chunkStartIndex = i;
|
||||
|
||||
// Add overlap messages from previous chunk if enabled
|
||||
if (this.options.preserveContext && chunks.length > 0) {
|
||||
const overlapStart = Math.max(0, i - this.options.contextOverlap);
|
||||
for (let j = overlapStart; j < i; j++) {
|
||||
const overlapMessage = messages[j];
|
||||
const overlapText = this.formatMessage(overlapMessage);
|
||||
currentChunk.push(overlapMessage);
|
||||
currentTokens += overlapMessage.estimatedTokens;
|
||||
currentBytes += Buffer.byteLength(overlapText, 'utf8');
|
||||
}
|
||||
|
||||
if (currentChunk.length > 0) {
|
||||
// Mark that this chunk has overlap
|
||||
chunkStartIndex = overlapStart;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add message to current chunk
|
||||
currentChunk.push(message);
|
||||
currentTokens += messageTokens;
|
||||
currentBytes += messageBytes;
|
||||
}
|
||||
|
||||
// Save final chunk if it has content
|
||||
if (currentChunk.length > 0) {
|
||||
const chunkContent = this.formatChunk(currentChunk);
|
||||
chunks.push({
|
||||
content: chunkContent,
|
||||
metadata: {
|
||||
chunkNumber: chunks.length + 1,
|
||||
totalChunks: 0,
|
||||
startIndex: chunkStartIndex,
|
||||
endIndex: messages.length - 1,
|
||||
messageCount: currentChunk.length,
|
||||
estimatedTokens: currentTokens,
|
||||
sizeBytes: currentBytes,
|
||||
hasOverlap: this.options.preserveContext && chunks.length > 0
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update total chunks count in metadata
|
||||
chunks.forEach(chunk => {
|
||||
chunk.metadata.totalChunks = chunks.length;
|
||||
});
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a single message back to the filtered output format
|
||||
*/
|
||||
private formatMessage(message: ChunkedMessage): string {
|
||||
return `- ${message.content}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a chunk of messages
|
||||
*/
|
||||
private formatChunk(messages: ChunkedMessage[]): string {
|
||||
return messages.map(m => this.formatMessage(m)).join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a header for a chunk file with metadata
|
||||
*/
|
||||
public createChunkHeader(metadata: ChunkMetadata): string {
|
||||
const lines = [];
|
||||
|
||||
// Add timestamp range if available, otherwise chunk number
|
||||
if (metadata.firstTimestamp && metadata.lastTimestamp) {
|
||||
lines.push(`# ${metadata.firstTimestamp} to ${metadata.lastTimestamp} (chunk ${metadata.chunkNumber}/${metadata.totalChunks})`);
|
||||
} else {
|
||||
lines.push(`# Chunk ${metadata.chunkNumber} of ${metadata.totalChunks}`);
|
||||
}
|
||||
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if content needs chunking based on size
|
||||
*/
|
||||
public needsChunking(content: string): boolean {
|
||||
const estimatedTokens = this.estimateTokenCount(content);
|
||||
const sizeBytes = Buffer.byteLength(content, 'utf8');
|
||||
|
||||
return estimatedTokens > this.options.maxTokensPerChunk ||
|
||||
sizeBytes > this.options.maxBytesPerChunk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets chunking statistics for logging
|
||||
*/
|
||||
public getChunkingStats(chunks: Array<{ metadata: ChunkMetadata }>): string {
|
||||
const totalMessages = chunks.reduce((sum, c) => sum + c.metadata.messageCount, 0);
|
||||
const totalTokens = chunks.reduce((sum, c) => sum + c.metadata.estimatedTokens, 0);
|
||||
const totalBytes = chunks.reduce((sum, c) => sum + c.metadata.sizeBytes, 0);
|
||||
|
||||
return [
|
||||
`📊 Chunking Statistics:`,
|
||||
` • Total chunks: ${chunks.length}`,
|
||||
` • Total messages: ${totalMessages}`,
|
||||
` • Total estimated tokens: ${totalTokens.toLocaleString()}`,
|
||||
` • Total size: ${(totalBytes / 1024).toFixed(1)} KB`,
|
||||
` • Average tokens per chunk: ${Math.round(totalTokens / chunks.length).toLocaleString()}`,
|
||||
` • Average size per chunk: ${(totalBytes / chunks.length / 1024).toFixed(1)} KB`
|
||||
].join('\n');
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,366 +0,0 @@
|
||||
/**
|
||||
* PromptOrchestrator - Single source of truth for all prompt generation
|
||||
*
|
||||
* This class serves as the central orchestrator for generating different types of prompts
|
||||
* used throughout the claude-mem system. It provides clear, well-typed interfaces and
|
||||
* methods for creating prompts for LLM analysis, human context, and system integration.
|
||||
*/
|
||||
|
||||
import { createAnalysisPrompt } from '../../prompts/templates/analysis/AnalysisTemplates.js';
|
||||
|
||||
// =============================================================================
|
||||
// CORE INTERFACES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Context data for LLM analysis prompts
|
||||
*/
|
||||
export interface AnalysisContext {
|
||||
/** The transcript content to analyze */
|
||||
transcriptContent: string;
|
||||
/** Session identifier */
|
||||
sessionId: string;
|
||||
/** Project name for context */
|
||||
projectName?: string;
|
||||
/** Custom analysis instructions */
|
||||
customInstructions?: string;
|
||||
/** Compression trigger type */
|
||||
trigger?: 'manual' | 'auto';
|
||||
/** Original token count */
|
||||
originalTokens?: number;
|
||||
/** Target compression ratio */
|
||||
targetCompressionRatio?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context data for human-facing session prompts
|
||||
*/
|
||||
export interface SessionContext {
|
||||
/** Session identifier */
|
||||
sessionId: string;
|
||||
/** Source of the session start */
|
||||
source: 'startup' | 'compact' | 'vscode' | 'web';
|
||||
/** Project name */
|
||||
projectName?: string;
|
||||
/** Additional context to provide to the human */
|
||||
additionalContext?: string;
|
||||
/** Path to the transcript file */
|
||||
transcriptPath?: string;
|
||||
/** Working directory */
|
||||
cwd?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context data for hook response generation
|
||||
*/
|
||||
export interface HookContext {
|
||||
/** The hook event name */
|
||||
hookEventName: string;
|
||||
/** Session identifier */
|
||||
sessionId: string;
|
||||
/** Success status */
|
||||
success: boolean;
|
||||
/** Optional message */
|
||||
message?: string;
|
||||
/** Additional data specific to the hook */
|
||||
data?: Record<string, unknown>;
|
||||
/** Whether to continue processing */
|
||||
shouldContinue?: boolean;
|
||||
/** Reason for stopping if applicable */
|
||||
stopReason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generated analysis prompt for LLM consumption
|
||||
*/
|
||||
export interface AnalysisPrompt {
|
||||
/** The formatted prompt text */
|
||||
prompt: string;
|
||||
/** Context used to generate the prompt */
|
||||
context: AnalysisContext;
|
||||
/** Prompt type identifier */
|
||||
type: 'analysis';
|
||||
/** Generated timestamp */
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generated session prompt for human context
|
||||
*/
|
||||
export interface SessionPrompt {
|
||||
/** The formatted message text */
|
||||
message: string;
|
||||
/** Context used to generate the prompt */
|
||||
context: SessionContext;
|
||||
/** Prompt type identifier */
|
||||
type: 'session';
|
||||
/** Generated timestamp */
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generated hook response
|
||||
*/
|
||||
export interface HookResponse {
|
||||
/** Whether to continue processing */
|
||||
continue: boolean;
|
||||
/** Reason for stopping if continue is false */
|
||||
stopReason?: string;
|
||||
/** Whether to suppress output */
|
||||
suppressOutput?: boolean;
|
||||
/** Hook-specific output data */
|
||||
hookSpecificOutput?: Record<string, unknown>;
|
||||
/** Context used to generate the response */
|
||||
context: HookContext;
|
||||
/** Response type identifier */
|
||||
type: 'hook';
|
||||
/** Generated timestamp */
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROMPT ORCHESTRATOR CLASS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Central orchestrator for all prompt generation in the claude-mem system
|
||||
*/
|
||||
export class PromptOrchestrator {
|
||||
private projectName: string;
|
||||
|
||||
constructor(projectName = 'claude-mem') {
|
||||
this.projectName = projectName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an analysis prompt for LLM processing of transcript content
|
||||
*/
|
||||
public createAnalysisPrompt(context: AnalysisContext): AnalysisPrompt {
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
const prompt = this.buildAnalysisPrompt(context);
|
||||
|
||||
return {
|
||||
prompt,
|
||||
context,
|
||||
type: 'analysis',
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a session start prompt for human context
|
||||
*/
|
||||
public createSessionStartPrompt(context: SessionContext): SessionPrompt {
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
const message = this.buildSessionStartMessage(context);
|
||||
|
||||
return {
|
||||
message,
|
||||
context,
|
||||
type: 'session',
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a hook response for system integration
|
||||
*/
|
||||
public createHookResponse(context: HookContext): HookResponse {
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
const response = this.buildHookResponse(context);
|
||||
|
||||
return {
|
||||
...response,
|
||||
context,
|
||||
type: 'hook',
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PRIVATE PROMPT BUILDERS
|
||||
// =============================================================================
|
||||
|
||||
private buildAnalysisPrompt(context: AnalysisContext): string {
|
||||
const {
|
||||
transcriptContent,
|
||||
sessionId,
|
||||
projectName = this.projectName,
|
||||
} = context;
|
||||
|
||||
// Extract project prefix from project name (convert to snake_case)
|
||||
const projectPrefix = projectName.replace(/[-\s]/g, '_').toLowerCase();
|
||||
|
||||
// Use the simple prompt with the transcript included
|
||||
return createAnalysisPrompt(
|
||||
transcriptContent,
|
||||
sessionId,
|
||||
projectPrefix
|
||||
);
|
||||
}
|
||||
|
||||
private buildSessionStartMessage(context: SessionContext): string {
|
||||
const {
|
||||
sessionId,
|
||||
source,
|
||||
projectName = this.projectName,
|
||||
additionalContext,
|
||||
transcriptPath,
|
||||
cwd,
|
||||
} = context;
|
||||
|
||||
let message = `## Session Started (${source})
|
||||
|
||||
**Project**: ${projectName}
|
||||
**Session ID**: ${sessionId} `;
|
||||
|
||||
if (transcriptPath) {
|
||||
message += `**Transcript**: ${transcriptPath} `;
|
||||
}
|
||||
|
||||
if (cwd) {
|
||||
message += `**Working Directory**: ${cwd} `;
|
||||
}
|
||||
|
||||
if (additionalContext) {
|
||||
message += `\n### Additional Context\n${additionalContext}`;
|
||||
}
|
||||
|
||||
message += `\n\nMemory system is active and ready to preserve context across sessions.`;
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private buildHookResponse(context: HookContext): Omit<HookResponse, 'context' | 'type' | 'timestamp'> {
|
||||
const {
|
||||
hookEventName,
|
||||
success,
|
||||
message,
|
||||
data,
|
||||
shouldContinue = success,
|
||||
stopReason,
|
||||
} = context;
|
||||
|
||||
const response: Omit<HookResponse, 'context' | 'type' | 'timestamp'> = {
|
||||
continue: shouldContinue,
|
||||
suppressOutput: false,
|
||||
};
|
||||
|
||||
if (!shouldContinue && stopReason) {
|
||||
response.stopReason = stopReason;
|
||||
}
|
||||
|
||||
// Add hook-specific output based on event type
|
||||
if (hookEventName === 'SessionStart') {
|
||||
response.hookSpecificOutput = {
|
||||
hookEventName: 'SessionStart',
|
||||
additionalContext: message,
|
||||
...data,
|
||||
};
|
||||
} else if (data) {
|
||||
response.hookSpecificOutput = data;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UTILITY METHODS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Validates that an AnalysisContext has required fields
|
||||
*/
|
||||
public validateAnalysisContext(context: Partial<AnalysisContext>): context is AnalysisContext {
|
||||
return !!(context.transcriptContent && context.sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a SessionContext has required fields
|
||||
*/
|
||||
public validateSessionContext(context: Partial<SessionContext>): context is SessionContext {
|
||||
return !!(context.sessionId && context.source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a HookContext has required fields
|
||||
*/
|
||||
public validateHookContext(context: Partial<HookContext>): context is HookContext {
|
||||
return !!(context.hookEventName && context.sessionId && typeof context.success === 'boolean');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the project name for this orchestrator instance
|
||||
*/
|
||||
public getProjectName(): string {
|
||||
return this.projectName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new project name for this orchestrator instance
|
||||
*/
|
||||
public setProjectName(projectName: string): void {
|
||||
this.projectName = projectName;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FACTORY FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Creates a new PromptOrchestrator instance
|
||||
*/
|
||||
export function createPromptOrchestrator(projectName?: string): PromptOrchestrator {
|
||||
return new PromptOrchestrator(projectName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an analysis context from basic parameters
|
||||
*/
|
||||
export function createAnalysisContext(
|
||||
transcriptContent: string,
|
||||
sessionId: string,
|
||||
options: Partial<Omit<AnalysisContext, 'transcriptContent' | 'sessionId'>> = {}
|
||||
): AnalysisContext {
|
||||
return {
|
||||
transcriptContent,
|
||||
sessionId,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a session context from basic parameters
|
||||
*/
|
||||
export function createSessionContext(
|
||||
sessionId: string,
|
||||
source: SessionContext['source'],
|
||||
options: Partial<Omit<SessionContext, 'sessionId' | 'source'>> = {}
|
||||
): SessionContext {
|
||||
return {
|
||||
sessionId,
|
||||
source,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a hook context from basic parameters
|
||||
*/
|
||||
export function createHookContext(
|
||||
hookEventName: string,
|
||||
sessionId: string,
|
||||
success: boolean,
|
||||
options: Partial<Omit<HookContext, 'hookEventName' | 'sessionId' | 'success'>> = {}
|
||||
): HookContext {
|
||||
return {
|
||||
hookEventName,
|
||||
sessionId,
|
||||
success,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import { query } from '@anthropic-ai/claude-code';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { getClaudePath } from '../../shared/settings.js';
|
||||
|
||||
export interface TitleGenerationRequest {
|
||||
sessionId: string;
|
||||
projectName: string;
|
||||
firstMessage: string;
|
||||
}
|
||||
|
||||
export interface GeneratedTitle {
|
||||
session_id: string;
|
||||
generated_title: string;
|
||||
timestamp: string;
|
||||
project_name: string;
|
||||
}
|
||||
|
||||
export class TitleGenerator {
|
||||
private titlesIndexPath: string;
|
||||
|
||||
constructor() {
|
||||
this.titlesIndexPath = path.join(os.homedir(), '.claude-mem', 'conversation-titles.jsonl');
|
||||
this.ensureTitlesIndex();
|
||||
}
|
||||
|
||||
private ensureTitlesIndex(): void {
|
||||
const dir = path.dirname(this.titlesIndexPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(this.titlesIndexPath)) {
|
||||
fs.writeFileSync(this.titlesIndexPath, '', 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
async generateTitle(firstMessage: string): Promise<string> {
|
||||
const prompt = `Generate a 3-7 word descriptive title for this conversation based on the first message.
|
||||
|
||||
The title should:
|
||||
- Capture the main topic or intent
|
||||
- Be concise and descriptive
|
||||
- Use proper capitalization
|
||||
- Not include "Help with" or "Question about" prefixes
|
||||
|
||||
First message: "${firstMessage.substring(0, 500)}"
|
||||
|
||||
Respond with just the title, nothing else.`;
|
||||
|
||||
const response = await query({
|
||||
prompt,
|
||||
options: {
|
||||
model: 'claude-3-5-haiku-20241022',
|
||||
pathToClaudeCodeExecutable: getClaudePath(),
|
||||
},
|
||||
});
|
||||
|
||||
let title = '';
|
||||
if (response && typeof response === 'object' && Symbol.asyncIterator in response) {
|
||||
for await (const message of response) {
|
||||
if (message?.content) title += message.content;
|
||||
if (message?.text) title += message.text;
|
||||
}
|
||||
} else if (typeof response === 'string') {
|
||||
title = response;
|
||||
}
|
||||
|
||||
return title.trim().replace(/^["']|["']$/g, '');
|
||||
}
|
||||
|
||||
async batchGenerateTitles(requests: TitleGenerationRequest[]): Promise<GeneratedTitle[]> {
|
||||
const results: GeneratedTitle[] = [];
|
||||
|
||||
for (const request of requests) {
|
||||
try {
|
||||
const title = await this.generateTitle(request.firstMessage);
|
||||
|
||||
const generatedTitle: GeneratedTitle = {
|
||||
session_id: request.sessionId,
|
||||
generated_title: title,
|
||||
timestamp: new Date().toISOString(),
|
||||
project_name: request.projectName
|
||||
};
|
||||
|
||||
results.push(generatedTitle);
|
||||
this.storeTitleInIndex(generatedTitle);
|
||||
} catch (error) {
|
||||
console.error(`Failed to generate title for ${request.sessionId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private storeTitleInIndex(title: GeneratedTitle): void {
|
||||
const line = JSON.stringify(title) + '\n';
|
||||
fs.appendFileSync(this.titlesIndexPath, line, 'utf-8');
|
||||
}
|
||||
|
||||
getExistingTitles(): Map<string, GeneratedTitle> {
|
||||
const titles = new Map<string, GeneratedTitle>();
|
||||
|
||||
if (!fs.existsSync(this.titlesIndexPath)) {
|
||||
return titles;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(this.titlesIndexPath, 'utf-8');
|
||||
const lines = content.trim().split('\n').filter(Boolean);
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const title = JSON.parse(line) as GeneratedTitle;
|
||||
titles.set(title.session_id, title);
|
||||
} catch (error) {
|
||||
// Skip invalid lines
|
||||
}
|
||||
}
|
||||
|
||||
return titles;
|
||||
}
|
||||
|
||||
getTitleForSession(sessionId: string): string | null {
|
||||
const titles = this.getExistingTitles();
|
||||
const title = titles.get(sessionId);
|
||||
return title ? title.generated_title : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { ensureWorkerRunning } from '../shared/worker-utils.js';
|
||||
|
||||
export interface SessionEndInput {
|
||||
session_id: string;
|
||||
cwd: string;
|
||||
transcript_path?: string;
|
||||
hook_event_name: string;
|
||||
reason: 'exit' | 'clear' | 'logout' | 'prompt_input_exit' | 'other';
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup Hook - SessionEnd
|
||||
* Marks session as completed when Claude Code session ends
|
||||
*
|
||||
* This hook runs when a Claude Code session ends. It:
|
||||
* 1. Finds active SDK session for this Claude session
|
||||
* 2. Marks session as completed in database
|
||||
* 3. Allows worker to finish pending operations naturally
|
||||
*/
|
||||
export async function cleanupHook(input?: SessionEndInput): Promise<void> {
|
||||
try {
|
||||
// Log hook entry point
|
||||
console.error('[claude-mem cleanup] Hook fired', {
|
||||
input: input ? {
|
||||
session_id: input.session_id,
|
||||
cwd: input.cwd,
|
||||
reason: input.reason
|
||||
} : null
|
||||
});
|
||||
|
||||
// Handle standalone execution (no input provided)
|
||||
if (!input) {
|
||||
console.log('No input provided - this script is designed to run as a Claude Code SessionEnd hook');
|
||||
console.log('\nExpected input format:');
|
||||
console.log(JSON.stringify({
|
||||
session_id: "string",
|
||||
cwd: "string",
|
||||
transcript_path: "string",
|
||||
hook_event_name: "SessionEnd",
|
||||
reason: "exit"
|
||||
}, null, 2));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const { session_id, reason } = input;
|
||||
console.error('[claude-mem cleanup] Searching for active SDK session', { session_id, reason });
|
||||
|
||||
// Ensure worker is running first (runs cleanup if restarting)
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
console.error('[claude-mem cleanup] Worker not available - skipping HTTP cleanup');
|
||||
}
|
||||
|
||||
// Find active SDK session
|
||||
const db = new SessionStore();
|
||||
const session = db.findActiveSDKSession(session_id);
|
||||
|
||||
if (!session) {
|
||||
// No active session - nothing to clean up
|
||||
console.error('[claude-mem cleanup] No active SDK session found', { session_id });
|
||||
db.close();
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.error('[claude-mem cleanup] Active SDK session found', {
|
||||
session_id: session.id,
|
||||
sdk_session_id: session.sdk_session_id,
|
||||
project: session.project,
|
||||
worker_port: session.worker_port
|
||||
});
|
||||
|
||||
// 1. Mark session as completed in DB (if not already completed)
|
||||
try {
|
||||
db.markSessionCompleted(session.id);
|
||||
console.error('[claude-mem cleanup] Session marked as completed in database');
|
||||
} catch (markErr: any) {
|
||||
console.error('[claude-mem cleanup] Failed to mark session as completed:', markErr);
|
||||
}
|
||||
|
||||
db.close();
|
||||
|
||||
console.error('[claude-mem cleanup] Cleanup completed successfully');
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
process.exit(0);
|
||||
|
||||
} catch (error: any) {
|
||||
// On error, don't block Claude Code exit
|
||||
console.error('[claude-mem cleanup] Unexpected error in hook', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name
|
||||
});
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import path from 'path';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { ensureWorkerRunning } from '../shared/worker-utils.js';
|
||||
|
||||
export interface SessionStartInput {
|
||||
session_id?: string;
|
||||
transcript_path?: string;
|
||||
cwd?: string;
|
||||
hook_event_name?: string;
|
||||
source?: "startup" | "resume" | "clear" | "compact";
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// ANSI color codes for terminal output
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
bright: '\x1b[1m',
|
||||
dim: '\x1b[2m',
|
||||
cyan: '\x1b[36m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
magenta: '\x1b[35m',
|
||||
gray: '\x1b[90m',
|
||||
};
|
||||
|
||||
/**
|
||||
* Context Hook - SessionStart
|
||||
* Shows user what happened in recent sessions
|
||||
*/
|
||||
export function contextHook(input?: SessionStartInput, useColors: boolean = false, useIndexView: boolean = false): string {
|
||||
ensureWorkerRunning();
|
||||
const cwd = input?.cwd ?? process.cwd();
|
||||
const project = cwd ? path.basename(cwd) : 'unknown-project';
|
||||
|
||||
const db = new SessionStore();
|
||||
|
||||
try {
|
||||
// Get the most recent summaries, then display them chronologically (oldest to newest, like a chat)
|
||||
const summaries = db.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT sdk_session_id, request, learned, completed, next_steps, created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 10
|
||||
)
|
||||
ORDER BY created_at_epoch ASC
|
||||
`).all(project) as Array<{
|
||||
sdk_session_id: string;
|
||||
request: string | null;
|
||||
learned: string | null;
|
||||
completed: string | null;
|
||||
next_steps: string | null;
|
||||
created_at: string;
|
||||
}>;
|
||||
|
||||
if (summaries.length === 0) {
|
||||
if (useColors) {
|
||||
return `\n${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}\n${colors.gray}${'─'.repeat(60)}${colors.reset}\n\n${colors.dim}No previous summaries found for this project yet.${colors.reset}\n`;
|
||||
}
|
||||
return `# [${project}] recent context\n\nNo previous summaries found for this project yet.`;
|
||||
}
|
||||
|
||||
const output: string[] = [];
|
||||
|
||||
if (useColors) {
|
||||
output.push('');
|
||||
output.push(`${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}`);
|
||||
output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`# [${project}] recent context`);
|
||||
output.push('');
|
||||
}
|
||||
|
||||
let isFirstSummary = true;
|
||||
|
||||
for (let i = 0; i < summaries.length; i++) {
|
||||
const summary = summaries[i];
|
||||
|
||||
// Determine verbosity tier based on position
|
||||
// Most recent summary is at the end (highest index) since we display chronologically
|
||||
const positionFromEnd = summaries.length - 1 - i;
|
||||
const isTier1 = positionFromEnd === 0; // Most recent (full verbosity)
|
||||
const isTier2 = positionFromEnd >= 1 && positionFromEnd <= 3; // Middle 3 (request + what was done)
|
||||
const isTier3 = positionFromEnd > 3; // Oldest 6 (request only)
|
||||
|
||||
// Add separator between summaries (but not before the first one)
|
||||
if (!isFirstSummary) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push('---');
|
||||
output.push('');
|
||||
}
|
||||
} else {
|
||||
if (useColors) {
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
isFirstSummary = false;
|
||||
|
||||
// TIER 3: Minimal (just Request + Date)
|
||||
if (isTier3) {
|
||||
if (summary.request) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.yellow}Request:${colors.reset} ${summary.request}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`**Request:** ${summary.request}`);
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
const dateTime = new Date(summary.created_at).toLocaleString();
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Date: ${dateTime}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`**Date:** ${dateTime}`);
|
||||
output.push('');
|
||||
}
|
||||
continue; // Skip the rest for Tier 3
|
||||
}
|
||||
|
||||
// TIER 1 & 2: Show Request
|
||||
if (summary.request) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.yellow}Request:${colors.reset} ${summary.request}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`**Request:** ${summary.request}`);
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// TIER 1 ONLY: Show Learned
|
||||
if (isTier1 && summary.learned) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.blue}Learned:${colors.reset} ${summary.learned}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`**Learned:** ${summary.learned}`);
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// TIER 1 & 2: Show Completed
|
||||
if (summary.completed) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.green}Completed:${colors.reset} ${summary.completed}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`**Completed:** ${summary.completed}`);
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// TIER 1 ONLY: Show Next Steps
|
||||
if (isTier1 && summary.next_steps) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.bright}${colors.magenta}Next Steps:${colors.reset} ${summary.next_steps}`);
|
||||
output.push('');
|
||||
} else {
|
||||
output.push(`**Next Steps:** ${summary.next_steps}`);
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// TIER 1 ONLY: Get and show files
|
||||
if (isTier1) {
|
||||
const observations = db.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).all(summary.sdk_session_id) as Array<{
|
||||
files_read: string | null;
|
||||
files_modified: string | null;
|
||||
}>;
|
||||
|
||||
const filesReadSet = new Set<string>();
|
||||
const filesModifiedSet = new Set<string>();
|
||||
|
||||
// Helper function to convert absolute paths to relative paths
|
||||
const toRelativePath = (filePath: string): string => {
|
||||
try {
|
||||
// Only convert if it's an absolute path
|
||||
if (path.isAbsolute(filePath)) {
|
||||
return path.relative(cwd, filePath);
|
||||
}
|
||||
return filePath;
|
||||
} catch {
|
||||
return filePath;
|
||||
}
|
||||
};
|
||||
|
||||
for (const obs of observations) {
|
||||
if (obs.files_read) {
|
||||
try {
|
||||
const files = JSON.parse(obs.files_read);
|
||||
if (Array.isArray(files)) {
|
||||
files.forEach(f => filesReadSet.add(toRelativePath(f)));
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
|
||||
if (obs.files_modified) {
|
||||
try {
|
||||
const files = JSON.parse(obs.files_modified);
|
||||
if (Array.isArray(files)) {
|
||||
files.forEach(f => filesModifiedSet.add(toRelativePath(f)));
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove files from filesReadSet if they're already in filesModifiedSet (avoid redundancy)
|
||||
filesModifiedSet.forEach(file => filesReadSet.delete(file));
|
||||
|
||||
if (filesReadSet.size > 0) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Files Read: ${Array.from(filesReadSet).join(', ')}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`**Files Read:** ${Array.from(filesReadSet).join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (filesModifiedSet.size > 0) {
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Files Modified: ${Array.from(filesModifiedSet).join(', ')}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`**Files Modified:** ${Array.from(filesModifiedSet).join(', ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TIER 1 & 2: Show Date
|
||||
const dateTime = new Date(summary.created_at).toLocaleString();
|
||||
if (useColors) {
|
||||
output.push(`${colors.dim}Date: ${dateTime}${colors.reset}`);
|
||||
} else {
|
||||
output.push(`**Date:** ${dateTime}`);
|
||||
}
|
||||
|
||||
if (!useColors) {
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
|
||||
if (useColors) {
|
||||
output.push('');
|
||||
output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`);
|
||||
}
|
||||
|
||||
return output.join('\n');
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
export type HookType = 'PreCompact' | 'SessionStart' | 'UserPromptSubmit' | 'PostToolUse' | 'Stop' | string;
|
||||
|
||||
export interface HookResponseOptions {
|
||||
reason?: string;
|
||||
context?: string;
|
||||
}
|
||||
|
||||
export interface HookResponse {
|
||||
continue?: boolean;
|
||||
suppressOutput?: boolean;
|
||||
stopReason?: string;
|
||||
hookSpecificOutput?: {
|
||||
hookEventName: 'SessionStart';
|
||||
additionalContext: string;
|
||||
};
|
||||
}
|
||||
|
||||
function buildHookResponse(
|
||||
hookType: HookType,
|
||||
success: boolean,
|
||||
options: HookResponseOptions
|
||||
): HookResponse {
|
||||
if (hookType === 'PreCompact') {
|
||||
if (success) {
|
||||
return {
|
||||
continue: true,
|
||||
suppressOutput: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
continue: false,
|
||||
stopReason: options.reason || 'Pre-compact operation failed',
|
||||
suppressOutput: true
|
||||
};
|
||||
}
|
||||
|
||||
if (hookType === 'SessionStart') {
|
||||
if (success && options.context) {
|
||||
return {
|
||||
continue: true,
|
||||
suppressOutput: true,
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'SessionStart',
|
||||
additionalContext: options.context
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
continue: true,
|
||||
suppressOutput: true
|
||||
};
|
||||
}
|
||||
|
||||
if (hookType === 'UserPromptSubmit' || hookType === 'PostToolUse') {
|
||||
return {
|
||||
continue: true,
|
||||
suppressOutput: true
|
||||
};
|
||||
}
|
||||
|
||||
if (hookType === 'Stop') {
|
||||
return {
|
||||
continue: true,
|
||||
suppressOutput: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
continue: success,
|
||||
suppressOutput: true,
|
||||
...(options.reason && !success ? { stopReason: options.reason } : {})
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a standardized hook response using the HookTemplates system.
|
||||
*/
|
||||
export function createHookResponse(
|
||||
hookType: HookType,
|
||||
success: boolean,
|
||||
options: HookResponseOptions = {}
|
||||
): string {
|
||||
const response = buildHookResponse(hookType, success, options);
|
||||
return JSON.stringify(response);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { contextHook } from './context.js';
|
||||
export { saveHook } from './save.js';
|
||||
export { newHook } from './new.js';
|
||||
export { summaryHook } from './summary.js';
|
||||
@@ -0,0 +1,63 @@
|
||||
import path from 'path';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
||||
|
||||
export interface UserPromptSubmitInput {
|
||||
session_id: string;
|
||||
cwd: string;
|
||||
prompt: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* New Hook - UserPromptSubmit
|
||||
* Initializes SDK memory session via HTTP POST to worker service
|
||||
*/
|
||||
export async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
||||
if (!input) {
|
||||
throw new Error('newHook requires input');
|
||||
}
|
||||
|
||||
const { session_id, cwd, prompt } = input;
|
||||
const project = path.basename(cwd);
|
||||
|
||||
// Ensure worker is running first (runs cleanup if restarting)
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
throw new Error('Worker service failed to start or become healthy');
|
||||
}
|
||||
|
||||
const db = new SessionStore();
|
||||
|
||||
try {
|
||||
// Just save session_id for indexing - no validation, no state management
|
||||
const sessionDbId = db.createSDKSession(session_id, project, prompt);
|
||||
const promptNumber = db.incrementPromptCounter(sessionDbId);
|
||||
|
||||
// Save raw user prompt for full-text search
|
||||
db.saveUserPrompt(session_id, promptNumber, prompt);
|
||||
|
||||
console.error(`[new-hook] Session ${sessionDbId}, prompt #${promptNumber}`);
|
||||
|
||||
// Get fixed port
|
||||
const port = getWorkerPort();
|
||||
|
||||
// Initialize session via HTTP
|
||||
const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/init`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ project, userPrompt: prompt }),
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to initialize session: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
console.log(createHookResponse('UserPromptSubmit', true));
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { ensureWorkerRunning } from '../shared/worker-utils.js';
|
||||
|
||||
export interface PostToolUseInput {
|
||||
session_id: string;
|
||||
cwd: string;
|
||||
tool_name: string;
|
||||
tool_input: any;
|
||||
tool_output: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// Tools to skip (low value or too frequent)
|
||||
const SKIP_TOOLS = new Set([
|
||||
'ListMcpResourcesTool'
|
||||
]);
|
||||
|
||||
/**
|
||||
* Save Hook - PostToolUse
|
||||
* Sends tool observations to worker via HTTP POST
|
||||
*/
|
||||
export async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
if (!input) {
|
||||
throw new Error('saveHook requires input');
|
||||
}
|
||||
|
||||
const { session_id, tool_name, tool_input, tool_output } = input;
|
||||
|
||||
if (SKIP_TOOLS.has(tool_name)) {
|
||||
console.log(createHookResponse('PostToolUse', true));
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure worker is running first (runs cleanup if restarting)
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
throw new Error('Worker service failed to start or become healthy');
|
||||
}
|
||||
|
||||
const db = new SessionStore();
|
||||
|
||||
// Get or create session - no validation, just use the session_id from hook
|
||||
const sessionDbId = db.createSDKSession(session_id, '', ''); // project and prompt not needed for observations
|
||||
const promptNumber = db.getPromptCounter(sessionDbId);
|
||||
db.close();
|
||||
|
||||
const toolStr = logger.formatTool(tool_name, tool_input);
|
||||
|
||||
// Use fixed worker port - no session.worker_port validation needed
|
||||
const FIXED_PORT = parseInt(process.env.CLAUDE_MEM_WORKER_PORT || '37777', 10);
|
||||
|
||||
logger.dataIn('HOOK', `PostToolUse: ${toolStr}`, {
|
||||
sessionId: sessionDbId,
|
||||
workerPort: FIXED_PORT
|
||||
});
|
||||
|
||||
const response = await fetch(`http://127.0.0.1:${FIXED_PORT}/sessions/${sessionDbId}/observations`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tool_name,
|
||||
tool_input: tool_input !== undefined ? JSON.stringify(tool_input) : '{}',
|
||||
tool_output: tool_output !== undefined ? JSON.stringify(tool_output) : '{}',
|
||||
prompt_number: promptNumber
|
||||
}),
|
||||
signal: AbortSignal.timeout(2000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
logger.failure('HOOK', 'Failed to send observation', {
|
||||
sessionId: sessionDbId,
|
||||
status: response.status
|
||||
}, errorText);
|
||||
throw new Error(`Failed to send observation to worker: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
logger.debug('HOOK', 'Observation sent successfully', { sessionId: sessionDbId, toolName: tool_name });
|
||||
console.log(createHookResponse('PostToolUse', true));
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { ensureWorkerRunning } from '../shared/worker-utils.js';
|
||||
|
||||
export interface StopInput {
|
||||
session_id: string;
|
||||
cwd: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary Hook - Stop
|
||||
* Sends SUMMARIZE message to worker via HTTP POST (not finalize - keeps SDK agent running)
|
||||
*/
|
||||
export async function summaryHook(input?: StopInput): Promise<void> {
|
||||
if (!input) {
|
||||
throw new Error('summaryHook requires input');
|
||||
}
|
||||
|
||||
const { session_id } = input;
|
||||
|
||||
// Ensure worker is running first (runs cleanup if restarting)
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
throw new Error('Worker service failed to start or become healthy');
|
||||
}
|
||||
|
||||
const db = new SessionStore();
|
||||
|
||||
// Get or create session - no validation, just use the session_id from hook
|
||||
const sessionDbId = db.createSDKSession(session_id, '', '');
|
||||
const promptNumber = db.getPromptCounter(sessionDbId);
|
||||
db.close();
|
||||
|
||||
// Use fixed worker port - no session.worker_port validation needed
|
||||
const FIXED_PORT = parseInt(process.env.CLAUDE_MEM_WORKER_PORT || '37777', 10);
|
||||
|
||||
logger.dataIn('HOOK', 'Stop: Requesting summary', {
|
||||
sessionId: sessionDbId,
|
||||
workerPort: FIXED_PORT,
|
||||
promptNumber
|
||||
});
|
||||
|
||||
const response = await fetch(`http://127.0.0.1:${FIXED_PORT}/sessions/${sessionDbId}/summarize`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ prompt_number: promptNumber }),
|
||||
signal: AbortSignal.timeout(2000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
logger.failure('HOOK', 'Failed to generate summary', {
|
||||
sessionId: sessionDbId,
|
||||
status: response.status
|
||||
}, errorText);
|
||||
throw new Error(`Failed to request summary from worker: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
logger.debug('HOOK', 'Summary request sent successfully', { sessionId: sessionDbId });
|
||||
console.log(createHookResponse('Stop', true));
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/**
|
||||
* Time utilities for formatting relative timestamps
|
||||
*/
|
||||
|
||||
export function formatRelativeTime(timestamp: string | Date): string {
|
||||
try {
|
||||
const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffSeconds = Math.floor(diffMs / 1000);
|
||||
const diffMinutes = Math.floor(diffSeconds / 60);
|
||||
const diffHours = Math.floor(diffMinutes / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
const diffWeeks = Math.floor(diffDays / 7);
|
||||
const diffMonths = Math.floor(diffDays / 30);
|
||||
|
||||
if (diffSeconds < 60) {
|
||||
return 'Just now';
|
||||
} else if (diffMinutes < 60) {
|
||||
return diffMinutes === 1 ? '1 minute ago' : `${diffMinutes} minutes ago`;
|
||||
} else if (diffHours < 24) {
|
||||
return diffHours === 1 ? '1 hour ago' : `${diffHours} hours ago`;
|
||||
} else if (diffDays === 1) {
|
||||
return 'Yesterday';
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays} days ago`;
|
||||
} else if (diffWeeks === 1) {
|
||||
return '1 week ago';
|
||||
} else if (diffWeeks < 4) {
|
||||
return `${diffWeeks} weeks ago`;
|
||||
} else if (diffMonths === 1) {
|
||||
return '1 month ago';
|
||||
} else if (diffMonths < 12) {
|
||||
return `${diffMonths} months ago`;
|
||||
} else {
|
||||
const diffYears = Math.floor(diffMonths / 12);
|
||||
return diffYears === 1 ? '1 year ago' : `${diffYears} years ago`;
|
||||
}
|
||||
} catch (error) {
|
||||
// Return a fallback for invalid timestamps
|
||||
return 'Recently';
|
||||
}
|
||||
}
|
||||
|
||||
export function parseTimestamp(entry: any): Date | null {
|
||||
// Try multiple timestamp fields that might exist
|
||||
const possibleFields = ['timestamp', 'created_at', 'date', 'time'];
|
||||
|
||||
for (const field of possibleFields) {
|
||||
if (entry[field]) {
|
||||
try {
|
||||
const date = new Date(entry[field]);
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date;
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid timestamp found, return null
|
||||
return null;
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
/**
|
||||
* Claude Memory System - Prompt-Related Constants and Templates
|
||||
*
|
||||
* This file contains all prompts, instructions, and output templates
|
||||
* for the analysis and context priming system.
|
||||
*/
|
||||
|
||||
import * as HookTemplates from './templates/hooks/HookTemplates.js';
|
||||
|
||||
// =============================================================================
|
||||
// ANALYSIS PROMPTS AND TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Entity naming patterns for the knowledge graph
|
||||
*/
|
||||
export const ENTITY_NAMING_PATTERNS = {
|
||||
component: "Component_Name",
|
||||
decision: "Decision_Name",
|
||||
pattern: "Pattern_Name",
|
||||
tool: "Tool_Name",
|
||||
fix: "Fix_Name",
|
||||
workflow: "Workflow_Name"
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Available entity types for classification
|
||||
*/
|
||||
export const ENTITY_TYPES = {
|
||||
component: "component", // UI components, modules, services
|
||||
pattern: "pattern", // Architectural or design patterns
|
||||
workflow: "workflow", // Processes, pipelines, sequences
|
||||
integration: "integration", // APIs, external services, data sources
|
||||
concept: "concept", // Abstract ideas, methodologies, principles
|
||||
decision: "decision", // Design choices, trade-offs, solutions
|
||||
tool: "tool", // Utilities, libraries, development tools
|
||||
fix: "fix" // Bug fixes, patches, workarounds
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Standard observation fields for entities
|
||||
*/
|
||||
export const OBSERVATION_FIELDS = [
|
||||
"Core purpose: [what it fundamentally does]",
|
||||
"Brief description: [one-line summary for session-start display]",
|
||||
"Implementation: [key technical details, code patterns]",
|
||||
"Dependencies: [what it requires or builds upon]",
|
||||
"Usage context: [when/why it's used]",
|
||||
"Performance characteristics: [speed, reliability, constraints]",
|
||||
"Integration points: [how it connects to other systems]",
|
||||
"Keywords: [searchable terms for this concept]",
|
||||
"Decision rationale: [why this approach was chosen]",
|
||||
"Next steps: [what needs to be done next with this component]",
|
||||
"Files modified: [list of files changed]",
|
||||
"Tools used: [development tools/commands used]"
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Relationship types for creating meaningful entity connections
|
||||
*/
|
||||
export const RELATIONSHIP_TYPES = [
|
||||
"executes_via", "orchestrates_through", "validates_using",
|
||||
"provides_auth_to", "manages_state_for", "processes_events_from",
|
||||
"caches_data_from", "routes_requests_to", "transforms_data_for",
|
||||
"extends", "enhances_performance_of", "builds_upon",
|
||||
"fixes_issue_in", "replaces", "optimizes",
|
||||
"triggers_tool", "receives_result_from"
|
||||
] as const;
|
||||
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// CONTEXT PRIMING TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* System message templates for context priming
|
||||
*/
|
||||
export const CONTEXT_TEMPLATES = {
|
||||
PRIMARY_CONTEXT: (projectName: string) =>
|
||||
`Context primed for project: ${projectName}. Access memories with chroma_query_documents(["${projectName}*"]) or chroma_get_documents(["document_id"]).`,
|
||||
|
||||
RECENT_SESSIONS: (sessionList: string) =>
|
||||
`Recent sessions available: ${sessionList}`,
|
||||
|
||||
AVAILABLE_ENTITIES: (type: string, entities: string[], hasMore: boolean, moreCount: number) =>
|
||||
`Available ${type} entities: ${entities.join(', ')}${hasMore ? ` (+${moreCount} more)` : ''}`,
|
||||
|
||||
SESSION_START_HEADER: '🧠 Active Working Context from Previous Sessions:',
|
||||
SESSION_START_SEPARATOR: '═'.repeat(70),
|
||||
|
||||
RESUME_INSTRUCTIONS: `💡 TO RESUME: Load active components with chroma_get_documents(["<exact_document_ids>"])
|
||||
📊 TO EXPLORE: Search related work with chroma_query_documents(["<keywords>"])`
|
||||
} as const;
|
||||
|
||||
// =============================================================================
|
||||
// SESSION START OUTPUT TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Session start formatting templates
|
||||
*/
|
||||
export const SESSION_START_TEMPLATES = {
|
||||
FOCUS_LINE: (focus: string) => `📌 CURRENT FOCUS: ${focus}`,
|
||||
LAST_WORKED: (timeAgo: string, projectName: string) => `Last worked: ${timeAgo} | Project: ${projectName}`,
|
||||
|
||||
SECTIONS: {
|
||||
COMPONENTS: '🎯 ACTIVE COMPONENTS (load these for context):',
|
||||
DECISIONS: '🔄 RECENT DECISIONS & PATTERNS:',
|
||||
TOOLS: '🛠️ TOOLS & INFRASTRUCTURE:',
|
||||
FIXES: '🐛 RECENT FIXES:',
|
||||
ACTIONS: '⚡ NEXT ACTIONS:'
|
||||
},
|
||||
|
||||
ACTION_PREFIX: '□ ',
|
||||
ENTITY_BULLET: '• '
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Time formatting for "time ago" displays
|
||||
*/
|
||||
export const TIME_FORMATS = {
|
||||
JUST_NOW: 'just now',
|
||||
HOURS_AGO: (hours: number) => `${hours} hour${hours > 1 ? 's' : ''} ago`,
|
||||
DAYS_AGO: (days: number) => `${days} day${days > 1 ? 's' : ''} ago`,
|
||||
RECENTLY: 'recently'
|
||||
} as const;
|
||||
|
||||
// =============================================================================
|
||||
// HOOK RESPONSE TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Standard hook response structures for Claude Code integration
|
||||
*/
|
||||
export const HOOK_RESPONSES = {
|
||||
SUCCESS: (hookEventName: string, message: string) => ({
|
||||
hookSpecificOutput: {
|
||||
hookEventName,
|
||||
status: "success",
|
||||
message
|
||||
},
|
||||
suppressOutput: true
|
||||
}),
|
||||
|
||||
SKIPPED: (hookEventName: string, message: string) => ({
|
||||
hookSpecificOutput: {
|
||||
hookEventName,
|
||||
status: "skipped",
|
||||
message
|
||||
},
|
||||
suppressOutput: true
|
||||
}),
|
||||
|
||||
BLOCKED: (reason: string) => ({
|
||||
decision: "block",
|
||||
reason
|
||||
}),
|
||||
|
||||
CONTINUE: (hookEventName: string, additionalContext?: string) => ({
|
||||
continue: true,
|
||||
...(additionalContext && {
|
||||
hookSpecificOutput: {
|
||||
hookEventName,
|
||||
additionalContext
|
||||
}
|
||||
})
|
||||
}),
|
||||
|
||||
ERROR: (reason: string) => ({
|
||||
decision: "block",
|
||||
reason
|
||||
})
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Pre-defined hook messages
|
||||
*/
|
||||
export const HOOK_MESSAGES = {
|
||||
COMPRESSION_SUCCESS: "Memory compression completed successfully",
|
||||
COMPRESSION_FAILED: (stderr: string) => `Compression failed: ${stderr}`,
|
||||
CONTEXT_LOADED: "Project context loaded successfully",
|
||||
CONTEXT_SKIPPED: "Continuing session - context loading skipped",
|
||||
NO_TRANSCRIPT: "No transcript path provided",
|
||||
HOOK_ERROR: (error: string) => `Hook error: ${error}`
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Export hook templates for direct usage
|
||||
*/
|
||||
export { HookTemplates };
|
||||
@@ -1,30 +0,0 @@
|
||||
/**
|
||||
* Prompts Module - Single source of truth for all prompt generation
|
||||
*
|
||||
* This module provides a centralized system for generating prompts across
|
||||
* the claude-mem system. It includes the core PromptOrchestrator class
|
||||
* and all related TypeScript interfaces.
|
||||
*/
|
||||
|
||||
// Export all interfaces
|
||||
export type {
|
||||
AnalysisContext,
|
||||
SessionContext,
|
||||
HookContext,
|
||||
AnalysisPrompt,
|
||||
SessionPrompt,
|
||||
HookResponse,
|
||||
} from '../core/orchestration/PromptOrchestrator.js';
|
||||
|
||||
// Export the main class
|
||||
export {
|
||||
PromptOrchestrator,
|
||||
} from '../core/orchestration/PromptOrchestrator.js';
|
||||
|
||||
// Export factory functions
|
||||
export {
|
||||
createPromptOrchestrator,
|
||||
createAnalysisContext,
|
||||
createSessionContext,
|
||||
createHookContext,
|
||||
} from '../core/orchestration/PromptOrchestrator.js';
|
||||
@@ -1,190 +0,0 @@
|
||||
# Claude Memory Templates
|
||||
|
||||
This directory contains modular templates for the Claude Memory System, including LLM analysis prompts and system integration responses.
|
||||
|
||||
## Files
|
||||
|
||||
### AnalysisTemplates.ts
|
||||
The main template system for LLM analysis prompts. Contains clean, separated template functions for:
|
||||
|
||||
- **Entity extraction instructions** - Guidelines for identifying and categorizing technical entities
|
||||
- **Relationship mapping instructions** - Rules for creating meaningful connections between entities
|
||||
- **Output format specifications** - Exact format requirements for pipe-separated summaries
|
||||
- **Example outputs** - Sample outputs to guide the LLM
|
||||
- **MCP tool usage instructions** - Step-by-step MCP tool usage workflow
|
||||
- **Dynamic content injection helpers** - Functions for injecting project/session context
|
||||
|
||||
### HookTemplates.ts
|
||||
System integration templates for Claude Code hook responses. Provides standardized templates for:
|
||||
|
||||
- **Pre-compact hook responses** - Approve/block compression operations with proper formatting
|
||||
- **Session-start hook responses** - Load and format context with rich memory information
|
||||
- **Pre-tool use hook responses** - Security policies and permission controls
|
||||
- **Error handling templates** - User-friendly error messages with troubleshooting guidance
|
||||
- **Progress indicators** - Status updates for long-running operations
|
||||
- **Response validation** - Ensures compliance with Claude Code hook specifications
|
||||
|
||||
### ContextTemplates.ts
|
||||
Human-readable formatting templates for user-facing messages during memory operations.
|
||||
|
||||
### Legacy Templates
|
||||
- `analysis-template.txt` - Legacy mustache-style template (deprecated)
|
||||
- `session-start-template.txt` - Legacy mustache-style template (deprecated)
|
||||
|
||||
## Architecture
|
||||
|
||||
The new template system follows these principles:
|
||||
|
||||
1. **Pure Functions** - Each template function takes context and returns formatted strings
|
||||
2. **Modular Design** - Complex prompts are broken into focused, reusable components
|
||||
3. **Type Safety** - Full TypeScript support with proper interfaces
|
||||
4. **Context Injection** - Dynamic content injection through helper functions
|
||||
5. **Composable Templates** - Build complex prompts by combining template sections
|
||||
|
||||
## Usage
|
||||
|
||||
### Hook Templates Usage
|
||||
```typescript
|
||||
import {
|
||||
createPreCompactSuccessResponse,
|
||||
createSessionStartMemoryResponse,
|
||||
createPreToolUseAllowResponse,
|
||||
validateHookResponse
|
||||
} from './HookTemplates.js';
|
||||
|
||||
// Pre-compact hook: approve compression
|
||||
const preCompactResponse = createPreCompactSuccessResponse();
|
||||
console.log(JSON.stringify(preCompactResponse));
|
||||
// Output: {"continue": true, "suppressOutput": true}
|
||||
|
||||
// Session start hook: load context with memories
|
||||
const sessionResponse = createSessionStartMemoryResponse({
|
||||
projectName: 'claude-mem',
|
||||
memoryCount: 15,
|
||||
lastSessionTime: '2 hours ago',
|
||||
recentComponents: ['HookTemplates', 'PromptOrchestrator'],
|
||||
recentDecisions: ['Use TypeScript for type safety']
|
||||
});
|
||||
console.log(JSON.stringify(sessionResponse));
|
||||
|
||||
// Pre-tool use: allow memory tools
|
||||
const toolResponse = createPreToolUseAllowResponse('Memory operations are always permitted');
|
||||
console.log(JSON.stringify(toolResponse));
|
||||
|
||||
// Validate responses before sending
|
||||
const validation = validateHookResponse(preCompactResponse, 'PreCompact');
|
||||
if (!validation.isValid) {
|
||||
console.error('Invalid response:', validation.errors);
|
||||
}
|
||||
```
|
||||
|
||||
### Analysis Templates Usage
|
||||
```typescript
|
||||
import { buildCompleteAnalysisPrompt } from './AnalysisTemplates.js';
|
||||
|
||||
const prompt = buildCompleteAnalysisPrompt(
|
||||
'myproject', // projectPrefix
|
||||
'session123', // sessionId
|
||||
[], // toolUseChains
|
||||
'2024-01-01', // timestamp (optional)
|
||||
'archive.jsonl' // archiveFilename (optional)
|
||||
);
|
||||
```
|
||||
|
||||
### Individual Template Components
|
||||
```typescript
|
||||
import {
|
||||
createEntityExtractionInstructions,
|
||||
createOutputFormatSpecification,
|
||||
createExampleOutput
|
||||
} from './AnalysisTemplates.js';
|
||||
|
||||
// Get just the entity extraction guidelines
|
||||
const entityInstructions = createEntityExtractionInstructions('myproject');
|
||||
|
||||
// Get output format specification
|
||||
const outputFormat = createOutputFormatSpecification('2024-01-01', 'archive.jsonl');
|
||||
|
||||
// Get example output
|
||||
const examples = createExampleOutput('myproject', 'session123');
|
||||
```
|
||||
|
||||
### Context Injection
|
||||
```typescript
|
||||
import {
|
||||
injectProjectContext,
|
||||
injectSessionContext,
|
||||
validateTemplateContext
|
||||
} from './AnalysisTemplates.js';
|
||||
|
||||
// Validate context before using templates
|
||||
const context = { projectPrefix: 'myproject', sessionId: 'session123' };
|
||||
const errors = validateTemplateContext(context);
|
||||
if (errors.length > 0) {
|
||||
console.error('Invalid context:', errors);
|
||||
}
|
||||
|
||||
// Inject dynamic content into template strings
|
||||
let template = "Working on {{projectPrefix}} session {{sessionId}}";
|
||||
template = injectProjectContext(template, 'myproject');
|
||||
template = injectSessionContext(template, 'session123');
|
||||
```
|
||||
|
||||
## Template Sections
|
||||
|
||||
### Entity Extraction Instructions
|
||||
- Categories of entities to extract (components, patterns, decisions, etc.)
|
||||
- Naming conventions with project prefixes
|
||||
- Entity type classifications
|
||||
- Observation field templates
|
||||
|
||||
### Relationship Mapping
|
||||
- Available relationship types
|
||||
- Active-voice relationship guidelines
|
||||
- Graph connection strategies
|
||||
|
||||
### Output Format
|
||||
- Pipe-separated format specification
|
||||
- Required fields and exact values
|
||||
- Summary writing guidelines
|
||||
|
||||
### MCP Tool Usage
|
||||
- Step-by-step MCP tool workflow
|
||||
- Entity creation instructions
|
||||
- Relationship creation guidelines
|
||||
|
||||
### Critical Requirements
|
||||
- Entity count requirements (3-15 entities)
|
||||
- Relationship count requirements (5-20 relationships)
|
||||
- Output line requirements (3-10 summaries)
|
||||
- Format validation rules
|
||||
|
||||
## Benefits Over Legacy System
|
||||
|
||||
1. **Maintainability** - Separated concerns make individual sections easy to update
|
||||
2. **Testability** - Pure functions can be unit tested independently
|
||||
3. **Reusability** - Template components can be reused across different contexts
|
||||
4. **Debugging** - Easy to isolate issues to specific template sections
|
||||
5. **Type Safety** - Full TypeScript support prevents runtime template errors
|
||||
6. **Performance** - No string parsing overhead, direct function composition
|
||||
|
||||
## Migration from constants.ts
|
||||
|
||||
The massive `createAnalysisPrompt` function in `constants.ts` has been refactored into this modular system:
|
||||
|
||||
**Before** (130+ lines in single function):
|
||||
```typescript
|
||||
export function createAnalysisPrompt(...) {
|
||||
// Massive template string with embedded logic
|
||||
return `You are analyzing...${incrementalSection}${toolChains}...`;
|
||||
}
|
||||
```
|
||||
|
||||
**After** (clean delegation):
|
||||
```typescript
|
||||
export function createAnalysisPrompt(...) {
|
||||
return buildCompleteAnalysisPrompt(...);
|
||||
}
|
||||
```
|
||||
|
||||
This maintains backward compatibility while providing a much cleaner, more maintainable internal structure.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user