feat: Release v4.0.0 - Plugin data directory and auto-starting worker
BREAKING CHANGES:
- Data directory moved from ~/.claude-mem/ to ${CLAUDE_PLUGIN_ROOT}/data/
- Fresh start required - no migration from v3.x databases
- Worker service now auto-starts on SessionStart hook
New Features:
- MCP Search Server with 6 specialized search tools
- FTS5 full-text search across observations and sessions
- Auto-starting worker service in SessionStart hook
- Citation support for search results (claude-mem:// URIs)
Changes:
- Updated paths.ts to use CLAUDE_PLUGIN_ROOT for data directory
- Added worker auto-start logic to context hook
- Updated worker service to write port file to plugin data dir
- Bumped version to 4.0.0 in package.json and plugin.json
- Created comprehensive CHANGELOG.md documenting v4.0.0 changes
- Updated README.md with v4.0.0 breaking changes and features
- Rebuilt all hooks and worker service
Technical Improvements:
- Improved error handling and graceful degradation
- Structured logging across all components
- Enhanced plugin integration with Claude Code
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+151
@@ -0,0 +1,151 @@
|
||||
# Changelog
|
||||
|
||||
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/).
|
||||
|
||||
|
||||
## [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
|
||||
- Update save documentation to include allowed-tools and description metadata fields
|
||||
|
||||
### Removed
|
||||
- Remove deprecated markdown to JSONL migration script
|
||||
|
||||
|
||||
## [3.6.3] - 2025-09-11
|
||||
|
||||
### Changed
|
||||
- Updated changelog generation prompts to use date strings in query text for temporal filtering
|
||||
|
||||
### Fixed
|
||||
- Resolved changelog timestamp filtering by using semantic search instead of metadata queries, enabling proper date-based searches
|
||||
- Corrected install.ts search instructions to remove misleading metadata filtering guidance that caused 'Error finding id' errors
|
||||
|
||||
|
||||
## [3.6.2] - 2025-09-10
|
||||
|
||||
### Added
|
||||
- Visual feedback to changelog command showing current version, next version, and number of overviews being processed
|
||||
- Generate changelog for specific versions using `--generate` flag with npm publish time boundaries
|
||||
- Introduce 'Who Wants To Be a Memoryonaire?' trivia game that generates personalized questions from your stored memories
|
||||
- Add interactive terminal UI with lifelines (50:50, Phone-a-Friend, Audience Poll) and cross-platform audio support
|
||||
- Implement permanent question caching with --regenerate flag for instant game loading
|
||||
- Enable hybrid vector search to discover related memory chains during question generation
|
||||
|
||||
### Changed
|
||||
- Changelog regeneration automatically removes old entries from JSONL file when using `--generate` or `--historical` flags
|
||||
- Switch to direct JSONL file loading for instant memory access without API calls
|
||||
- Optimize AI generation with faster 'sonnet' model for improved performance
|
||||
- Reduce memory query limit from 100 to 50 to prevent token overflow
|
||||
|
||||
### Fixed
|
||||
- Changelog command now uses npm publish timestamps exclusively for accurate version time ranges
|
||||
- Resolved timestamp filtering issues with Chroma database by leveraging semantic search with embedded dates
|
||||
- Resolve game hanging at startup due to confirmation loop
|
||||
- Fix memory integration bypass that prevented questions from using actual stored memories
|
||||
- Consolidate 500+ lines of duplicate code for better maintainability
|
||||
|
||||
|
||||
## [3.6.1] - 2025-09-10
|
||||
|
||||
### Changed
|
||||
- Refactored pre-compact hook to work independently without status line updates
|
||||
|
||||
### Removed
|
||||
- Removed status line integration and ccstatusline configuration support
|
||||
|
||||
|
||||
## [3.5.5] - 2025-09-10
|
||||
|
||||
### Changed
|
||||
- Standardized GitHub release naming to lowercase 'claude-mem vX.X.X' format for consistent branding
|
||||
@@ -5,11 +5,25 @@
|
||||
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.
|
||||
|
||||
[](LICENSE)
|
||||
[](package.json)
|
||||
[](package.json)
|
||||
[](package.json)
|
||||
|
||||
---
|
||||
|
||||
## What's New in v4.0.0
|
||||
|
||||
**BREAKING CHANGES - Please Read:**
|
||||
|
||||
- **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. Users must start with a clean database
|
||||
- **Worker Auto-Starts**: Worker service now starts automatically - no manual `npm run worker:start` needed
|
||||
- **MCP Search Server**: 6 new search tools with full-text search and citations
|
||||
- **Enhanced Architecture**: Improved plugin integration and data organization
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md) for complete details.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
@@ -40,6 +54,7 @@ Claude-Mem is a **Claude Code plugin** that provides persistent memory across se
|
||||
|
||||
- **Session Continuity**: Knowledge persists across Claude Code sessions
|
||||
- **Automatic Context Injection**: Recent session summaries appear when Claude starts
|
||||
- **Auto-Starting Worker**: Worker service starts automatically on first session (v4.0+)
|
||||
- **MCP Search Server**: Search and retrieve observations and sessions via 6 specialized tools
|
||||
- **Structured Observations**: XML-formatted extraction of learnings
|
||||
- **Smart Filtering**: Skips low-value tool observations
|
||||
@@ -149,7 +164,7 @@ All search results are returned in `search_result` format with **citations enabl
|
||||
|
||||
#### 5. Database Layer
|
||||
|
||||
SQLite database (`~/.claude-mem/claude-mem.db`) with tables:
|
||||
SQLite database (`${CLAUDE_PLUGIN_ROOT}/data/claude-mem.db`) with tables:
|
||||
|
||||
- **sdk_sessions**: Active/completed session tracking
|
||||
- **observations**: Individual tool executions with FTS5 full-text search
|
||||
@@ -182,7 +197,8 @@ npm install
|
||||
# Build hooks and worker service
|
||||
npm run build
|
||||
|
||||
# Start the worker service
|
||||
# Worker service will auto-start on first Claude Code session
|
||||
# Or manually start with:
|
||||
npm run worker:start
|
||||
|
||||
# Verify worker is running
|
||||
@@ -207,15 +223,18 @@ npm install -g claude-mem
|
||||
cat plugin/hooks/hooks.json
|
||||
```
|
||||
|
||||
2. **Set Environment Variable (Optional)**
|
||||
2. **Data Directory Location**
|
||||
|
||||
To customize data directory:
|
||||
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
|
||||
```
|
||||
|
||||
Default: `~/.claude-mem/`
|
||||
|
||||
3. **Check Worker Logs**
|
||||
|
||||
```bash
|
||||
@@ -271,8 +290,10 @@ All results include:
|
||||
|
||||
#### Worker Management
|
||||
|
||||
**Note**: v4.0+ auto-starts the worker on first session. Manual commands below are optional.
|
||||
|
||||
```bash
|
||||
# Start worker service
|
||||
# Start worker service (optional - auto-starts automatically)
|
||||
npm run worker:start
|
||||
|
||||
# Stop worker service
|
||||
@@ -316,10 +337,15 @@ npm run publish:npm
|
||||
|
||||
### Viewing Stored Context
|
||||
|
||||
Context is stored in SQLite database at `~/.claude-mem/claude-mem.db`. You can query it directly:
|
||||
Context is stored in SQLite database. Location varies by version:
|
||||
- v4.0+: `${CLAUDE_PLUGIN_ROOT}/data/claude-mem.db` (inside plugin)
|
||||
- v3.x: `~/.claude-mem/claude-mem.db` (legacy)
|
||||
|
||||
Query the database directly:
|
||||
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db
|
||||
# v4.0+ - find your plugin directory first
|
||||
sqlite3 path/to/plugin/data/claude-mem.db
|
||||
|
||||
# View recent sessions
|
||||
SELECT session_id, project, created_at, status FROM sdk_sessions ORDER BY created_at DESC LIMIT 10;
|
||||
@@ -446,17 +472,20 @@ Claude Request → MCP Server → SessionSearch Service → FTS5 Database → Se
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|-------------------------|----------------------|---------------------------------------|
|
||||
| `CLAUDE_MEM_DATA_DIR` | `~/.claude-mem/` | Data directory for DB and logs |
|
||||
| `CLAUDE_MEM_WORKER_PORT`| `0` (dynamic) | Worker service port (37000-37999) |
|
||||
| `NODE_ENV` | `production` | Environment mode |
|
||||
| `FORCE_COLOR` | `1` | Enable colored logs |
|
||||
| Variable | Default | Description |
|
||||
|-------------------------|---------------------------------|---------------------------------------|
|
||||
| `CLAUDE_PLUGIN_ROOT` | Set by Claude Code | Plugin installation directory |
|
||||
| `CLAUDE_MEM_DATA_DIR` | `${CLAUDE_PLUGIN_ROOT}/data/` | Data directory override (dev only) |
|
||||
| `CLAUDE_MEM_WORKER_PORT`| `0` (dynamic) | Worker service port (37000-37999) |
|
||||
| `NODE_ENV` | `production` | Environment mode |
|
||||
| `FORCE_COLOR` | `1` | Enable colored logs |
|
||||
|
||||
### Files and Directories
|
||||
|
||||
**v4.0.0+ Structure:**
|
||||
|
||||
```
|
||||
~/.claude-mem/
|
||||
${CLAUDE_PLUGIN_ROOT}/data/
|
||||
├── claude-mem.db # SQLite database
|
||||
├── worker.port # Current worker port file
|
||||
└── logs/
|
||||
@@ -464,6 +493,12 @@ Claude Request → MCP Server → SessionSearch Service → FTS5 Database → Se
|
||||
└── worker-error.log # Worker stderr logs
|
||||
```
|
||||
|
||||
**Legacy (v3.x):**
|
||||
|
||||
```
|
||||
~/.claude-mem/ # Old location (no longer used)
|
||||
```
|
||||
|
||||
### Plugin Configuration
|
||||
|
||||
#### Hooks Configuration
|
||||
@@ -600,8 +635,8 @@ npm run worker:start
|
||||
**Problem**: Port allocation failed
|
||||
|
||||
```bash
|
||||
# Check if port file exists
|
||||
cat ~/.claude-mem/worker.port
|
||||
# Check if port file exists (v4.0+)
|
||||
cat ${CLAUDE_PLUGIN_ROOT}/data/worker.port
|
||||
|
||||
# Manually specify port
|
||||
CLAUDE_MEM_WORKER_PORT=37500 npm run worker:start
|
||||
@@ -625,14 +660,14 @@ cat plugin/hooks/hooks.json | jq .
|
||||
**Problem**: Context not appearing
|
||||
|
||||
```bash
|
||||
# Check if summaries exist
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM session_summaries;"
|
||||
# Check if summaries exist (adjust path for v4.0+)
|
||||
sqlite3 path/to/plugin/data/claude-mem.db "SELECT COUNT(*) FROM session_summaries;"
|
||||
|
||||
# View recent sessions
|
||||
npm run test:context:verbose
|
||||
|
||||
# Check database integrity
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"
|
||||
sqlite3 path/to/plugin/data/claude-mem.db "PRAGMA integrity_check;"
|
||||
```
|
||||
|
||||
### Database Issues
|
||||
@@ -643,12 +678,12 @@ sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;"
|
||||
# Close all connections
|
||||
pm2 stop claude-mem-worker
|
||||
|
||||
# Check for stale locks
|
||||
lsof ~/.claude-mem/claude-mem.db
|
||||
# Check for stale locks (v4.0+ path)
|
||||
lsof path/to/plugin/data/claude-mem.db
|
||||
|
||||
# Backup and recreate (nuclear option)
|
||||
cp ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.backup
|
||||
rm ~/.claude-mem/claude-mem.db
|
||||
# Backup and recreate (nuclear option) - adjust paths for v4.0+
|
||||
cp path/to/plugin/data/claude-mem.db path/to/plugin/data/claude-mem.db.backup
|
||||
rm path/to/plugin/data/claude-mem.db
|
||||
npm run worker:start # Will recreate schema
|
||||
```
|
||||
|
||||
@@ -667,7 +702,7 @@ npm run worker:logs
|
||||
Check 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;"
|
||||
sqlite3 path/to/plugin/data/claude-mem.db "SELECT correlation_id, tool_name, created_at FROM observations WHERE session_id = 'YOUR_SESSION_ID' ORDER BY created_at;"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -720,11 +755,22 @@ For more information about AGPL-3.0, see: https://www.gnu.org/licenses/agpl-3.0.
|
||||
|
||||
## Changelog
|
||||
|
||||
### v3.9.17 (Current)
|
||||
### v4.0.0 (Current)
|
||||
|
||||
- **NEW**: MCP Search Server with 6 specialized search tools
|
||||
- **NEW**: FTS5 full-text search across observations and session summaries
|
||||
- **BREAKING**: Data directory moved to `${CLAUDE_PLUGIN_ROOT}/data/`
|
||||
- **NEW**: Auto-starting worker service
|
||||
- **NEW**: Session continuity with automatic context injection
|
||||
- Refactored summary and context handling in hooks
|
||||
- Implemented structured logging across the application
|
||||
- Improved error handling and graceful degradation
|
||||
|
||||
### v3.9.17
|
||||
|
||||
- **FIX**: Context hook now uses proper `hookSpecificOutput` JSON format
|
||||
- MCP Search Server with 6 specialized search tools
|
||||
- FTS5 full-text search across observations and session summaries
|
||||
- Refactored summary and context handling in hooks
|
||||
- Implemented structured logging across the application
|
||||
- Fixed race condition in summary generation
|
||||
|
||||
Vendored
+29
-29
File diff suppressed because one or more lines are too long
+7
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "3.9.17",
|
||||
"version": "4.0.0",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
@@ -65,12 +65,13 @@
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"files": [
|
||||
"hooks",
|
||||
"scripts",
|
||||
".claude-plugin",
|
||||
"plugin",
|
||||
"dist",
|
||||
"src",
|
||||
"scripts",
|
||||
"docs",
|
||||
".mcp.json",
|
||||
"README_WINDOWS.md"
|
||||
"ecosystem.config.cjs",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "3.9.17",
|
||||
"version": "4.0.0",
|
||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import N from"better-sqlite3";import{join as a,dirname as B,basename as H}from"path";import{homedir as S}from"os";import{existsSync as j,mkdirSync as I}from"fs";var c=process.env.CLAUDE_MEM_DATA_DIR||a(S(),".claude-mem"),u=process.env.CLAUDE_CONFIG_DIR||a(S(),".claude"),W=a(c,"archives"),G=a(c,"logs"),q=a(c,"trash"),K=a(c,"backups"),Y=a(c,"settings.json"),R=a(c,"claude-mem.db"),J=a(u,"settings.json"),V=a(u,"commands"),Q=a(u,"CLAUDE.md");function L(o){I(o,{recursive:!0})}var _=(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))(_||{}),E=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=_[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,t){return`obs-${e}-${t}`}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 t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;try{let s=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&s.command){let r=s.command.length>50?s.command.substring(0,50)+"...":s.command;return`${e}(${r})`}if(e==="Read"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Edit"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Write"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,t,s,r,n){if(e<this.level)return;let d=new Date().toISOString().replace("T"," ").substring(0,23),l=_[e].padEnd(5),T=t.padEnd(6),i="";r?.correlationId?i=`[${r.correlationId}] `:r?.sessionId&&(i=`[session-${r.sessionId}] `);let p="";n!=null&&(this.level===0&&typeof n=="object"?p=`
|
||||
`+JSON.stringify(n,null,2):p=" "+this.formatData(n));let b="";if(r){let{sessionId:C,sdkSessionId:y,correlationId:x,...h}=r;Object.keys(h).length>0&&(b=` {${Object.entries(h).map(([O,v])=>`${O}=${v}`).join(", ")}}`)}let f=`[${d}] [${l}] [${T}] ${i}${s}${b}${p}`;e===3?console.error(f):console.log(f)}debug(e,t,s,r){this.log(0,e,t,s,r)}info(e,t,s,r){this.log(1,e,t,s,r)}warn(e,t,s,r){this.log(2,e,t,s,r)}error(e,t,s,r){this.log(3,e,t,s,r)}dataIn(e,t,s,r){this.info(e,`\u2192 ${t}`,s,r)}dataOut(e,t,s,r){this.info(e,`\u2190 ${t}`,s,r)}success(e,t,s,r){this.info(e,`\u2713 ${t}`,s,r)}failure(e,t,s,r){this.error(e,`\u2717 ${t}`,s,r)}timing(e,t,s,r){this.info(e,`\u23F1 ${t}`,r,{duration:`${s}ms`})}},k=new E;var m=class{db;constructor(){L(c),this.db=new N(R),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(s=>s.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"))}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{this.db.pragma("table_info(sdk_sessions)").some(i=>i.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(i=>i.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(i=>i.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"));let T=this.db.pragma("index_list(session_summaries)").some(i=>i.unique===1)}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(s=>s.unique===1))return;console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
import N from"better-sqlite3";import{join as i,dirname as H,basename as $}from"path";import{homedir as S}from"os";import{existsSync as W,mkdirSync as I}from"fs";var C=()=>process.env.CLAUDE_PLUGIN_ROOT?i(process.env.CLAUDE_PLUGIN_ROOT,"data"):process.env.CLAUDE_MEM_DATA_DIR?process.env.CLAUDE_MEM_DATA_DIR:i(S(),".claude-mem"),c=C(),m=process.env.CLAUDE_CONFIG_DIR||i(S(),".claude"),G=i(c,"archives"),q=i(c,"logs"),K=i(c,"trash"),Y=i(c,"backups"),J=i(c,"settings.json"),R=i(c,"claude-mem.db"),V=i(m,"settings.json"),Q=i(m,"commands"),z=i(m,"CLAUDE.md");function L(o){I(o,{recursive:!0})}var _=(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))(_||{}),E=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=_[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,t){return`obs-${e}-${t}`}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 t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;try{let s=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&s.command){let r=s.command.length>50?s.command.substring(0,50)+"...":s.command;return`${e}(${r})`}if(e==="Read"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Edit"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Write"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,t,s,r,n){if(e<this.level)return;let d=new Date().toISOString().replace("T"," ").substring(0,23),p=_[e].padEnd(5),T=t.padEnd(6),a="";r?.correlationId?a=`[${r.correlationId}] `:r?.sessionId&&(a=`[session-${r.sessionId}] `);let l="";n!=null&&(this.level===0&&typeof n=="object"?l=`
|
||||
`+JSON.stringify(n,null,2):l=" "+this.formatData(n));let b="";if(r){let{sessionId:y,sdkSessionId:x,correlationId:U,...h}=r;Object.keys(h).length>0&&(b=` {${Object.entries(h).map(([O,v])=>`${O}=${v}`).join(", ")}}`)}let f=`[${d}] [${p}] [${T}] ${a}${s}${b}${l}`;e===3?console.error(f):console.log(f)}debug(e,t,s,r){this.log(0,e,t,s,r)}info(e,t,s,r){this.log(1,e,t,s,r)}warn(e,t,s,r){this.log(2,e,t,s,r)}error(e,t,s,r){this.log(3,e,t,s,r)}dataIn(e,t,s,r){this.info(e,`\u2192 ${t}`,s,r)}dataOut(e,t,s,r){this.info(e,`\u2190 ${t}`,s,r)}success(e,t,s,r){this.info(e,`\u2713 ${t}`,s,r)}failure(e,t,s,r){this.error(e,`\u2717 ${t}`,s,r)}timing(e,t,s,r){this.info(e,`\u23F1 ${t}`,r,{duration:`${s}ms`})}},A=new E;var u=class{db;constructor(){L(c),this.db=new N(R),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(s=>s.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"))}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{this.db.pragma("table_info(sdk_sessions)").some(a=>a.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(a=>a.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(a=>a.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"));let T=this.db.pragma("index_list(session_summaries)").some(a=>a.unique===1)}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(s=>s.unique===1))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,
|
||||
@@ -146,7 +146,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(t,e).changes===0?(k.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:t}),!1):!0}setWorkerPort(e,t){this.db.prepare(`
|
||||
`).run(t,e).changes===0?(A.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:t}),!1):!0}setWorkerPort(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
@@ -177,5 +177,5 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};async function A(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:t}=o;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:t});let s=new m,r=s.findActiveSDKSession(e);if(r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),s.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:r.id,sdk_session_id:r.sdk_session_id,project:r.project,worker_port:r.worker_port}),r.worker_port)try{let n=await fetch(`http://127.0.0.1:${r.worker_port}/sessions/${r.id}`,{method:"DELETE",signal:AbortSignal.timeout(5e3)});n.ok?console.error("[claude-mem cleanup] Session deleted successfully via HTTP"):console.error("[claude-mem cleanup] Failed to delete session:",await n.text())}catch(n){console.error("[claude-mem cleanup] HTTP DELETE error:",n.message)}else console.error("[claude-mem cleanup] No worker port, cannot send DELETE request");try{s.markSessionFailed(r.id),console.error("[claude-mem cleanup] Session marked as failed in database")}catch(n){console.error("[claude-mem cleanup] Failed to mark session as failed:",n)}s.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 g="";D.on("data",o=>g+=o);D.on("end",async()=>{try{let o=g.trim()?JSON.parse(g):void 0;await A(o)}catch(o){console.error(`[claude-mem cleanup-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};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:t}=o;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:t});let s=new u,r=s.findActiveSDKSession(e);if(r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),s.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:r.id,sdk_session_id:r.sdk_session_id,project:r.project,worker_port:r.worker_port}),r.worker_port)try{let n=await fetch(`http://127.0.0.1:${r.worker_port}/sessions/${r.id}`,{method:"DELETE",signal:AbortSignal.timeout(5e3)});n.ok?console.error("[claude-mem cleanup] Session deleted successfully via HTTP"):console.error("[claude-mem cleanup] Failed to delete session:",await n.text())}catch(n){console.error("[claude-mem cleanup] HTTP DELETE error:",n.message)}else console.error("[claude-mem cleanup] No worker port, cannot send DELETE request");try{s.markSessionFailed(r.id),console.error("[claude-mem cleanup] Session marked as failed in database")}catch(n){console.error("[claude-mem cleanup] Failed to mark session as failed:",n)}s.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 g="";D.on("data",o=>g+=o);D.on("end",async()=>{try{let o=g.trim()?JSON.parse(g):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)}});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import C from"path";import D from"better-sqlite3";import{join as d,dirname as P,basename as B}from"path";import{homedir as R}from"os";import{existsSync as X,mkdirSync as v}from"fs";var m=process.env.CLAUDE_MEM_DATA_DIR||d(R(),".claude-mem"),_=process.env.CLAUDE_CONFIG_DIR||d(R(),".claude"),W=d(m,"archives"),G=d(m,"logs"),q=d(m,"trash"),J=d(m,"backups"),Y=d(m,"settings.json"),L=d(m,"claude-mem.db"),K=d(_,"settings.json"),V=d(_,"commands"),Q=d(_,"CLAUDE.md");function A(c){v(c,{recursive:!0})}var E=(r=>(r[r.DEBUG=0]="DEBUG",r[r.INFO=1]="INFO",r[r.WARN=2]="WARN",r[r.ERROR=3]="ERROR",r[r.SILENT=4]="SILENT",r))(E||{}),g=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=E[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,t){return`obs-${e}-${t}`}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 t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;try{let s=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&s.command){let n=s.command.length>50?s.command.substring(0,50)+"...":s.command;return`${e}(${n})`}if(e==="Read"&&s.file_path){let n=s.file_path.split("/").pop()||s.file_path;return`${e}(${n})`}if(e==="Edit"&&s.file_path){let n=s.file_path.split("/").pop()||s.file_path;return`${e}(${n})`}if(e==="Write"&&s.file_path){let n=s.file_path.split("/").pop()||s.file_path;return`${e}(${n})`}return e}catch{return e}}log(e,t,s,n,r){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),o=E[e].padEnd(5),u=t.padEnd(6),a="";n?.correlationId?a=`[${n.correlationId}] `:n?.sessionId&&(a=`[session-${n.sessionId}] `);let p="";r!=null&&(this.level===0&&typeof r=="object"?p=`
|
||||
`+JSON.stringify(r,null,2):p=" "+this.formatData(r));let b="";if(n){let{sessionId:k,sdkSessionId:I,correlationId:x,...S}=n;Object.keys(S).length>0&&(b=` {${Object.entries(S).map(([y,N])=>`${y}=${N}`).join(", ")}}`)}let f=`[${i}] [${o}] [${u}] ${a}${s}${b}${p}`;e===3?console.error(f):console.log(f)}debug(e,t,s,n){this.log(0,e,t,s,n)}info(e,t,s,n){this.log(1,e,t,s,n)}warn(e,t,s,n){this.log(2,e,t,s,n)}error(e,t,s,n){this.log(3,e,t,s,n)}dataIn(e,t,s,n){this.info(e,`\u2192 ${t}`,s,n)}dataOut(e,t,s,n){this.info(e,`\u2190 ${t}`,s,n)}success(e,t,s,n){this.info(e,`\u2713 ${t}`,s,n)}failure(e,t,s,n){this.error(e,`\u2717 ${t}`,s,n)}timing(e,t,s,n){this.info(e,`\u23F1 ${t}`,n,{duration:`${s}ms`})}},O=new g;var l=class{db;constructor(){A(m),this.db=new D(L),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(s=>s.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"))}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{this.db.pragma("table_info(sdk_sessions)").some(a=>a.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(a=>a.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(a=>a.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"));let u=this.db.pragma("index_list(session_summaries)").some(a=>a.unique===1)}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(s=>s.unique===1))return;console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
var _=(i=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(i,{get:(e,t)=>(typeof require<"u"?require:e)[t]}):i)(function(i){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+i+'" is not supported')});import b from"path";import{existsSync as T}from"fs";import{spawn as I}from"child_process";import j from"better-sqlite3";import{join as d,dirname as E,basename as J}from"path";import{homedir as O}from"os";import{existsSync as w,mkdirSync as M}from"fs";import{fileURLToPath as $}from"url";var P=()=>process.env.CLAUDE_PLUGIN_ROOT?d(process.env.CLAUDE_PLUGIN_ROOT,"data"):process.env.CLAUDE_MEM_DATA_DIR?process.env.CLAUDE_MEM_DATA_DIR:d(O(),".claude-mem"),u=P(),g=process.env.CLAUDE_CONFIG_DIR||d(O(),".claude"),z=d(u,"archives"),Z=d(u,"logs"),ee=d(u,"trash"),se=d(u,"backups"),te=d(u,"settings.json"),v=d(u,"claude-mem.db"),re=d(g,"settings.json"),ne=d(g,"commands"),oe=d(g,"CLAUDE.md");function y(){return d(u,"worker.port")}function D(i){M(i,{recursive:!0})}function N(){try{let t=_.resolve("claude-mem/package.json");return E(t)}catch{}let i=$(import.meta.url),e=E(i);for(let t=0;t<10;t++){let s=d(e,"package.json");if(w(s)&&_(s).name==="claude-mem")return e;let n=E(e);if(n===e)break;e=n}throw new Error("Cannot locate claude-mem package root. Ensure claude-mem is properly installed.")}var h=(r=>(r[r.DEBUG=0]="DEBUG",r[r.INFO=1]="INFO",r[r.WARN=2]="WARN",r[r.ERROR=3]="ERROR",r[r.SILENT=4]="SILENT",r))(h||{}),f=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=h[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,t){return`obs-${e}-${t}`}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 t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;try{let s=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&s.command){let n=s.command.length>50?s.command.substring(0,50)+"...":s.command;return`${e}(${n})`}if(e==="Read"&&s.file_path){let n=s.file_path.split("/").pop()||s.file_path;return`${e}(${n})`}if(e==="Edit"&&s.file_path){let n=s.file_path.split("/").pop()||s.file_path;return`${e}(${n})`}if(e==="Write"&&s.file_path){let n=s.file_path.split("/").pop()||s.file_path;return`${e}(${n})`}return e}catch{return e}}log(e,t,s,n,r){if(e<this.level)return;let a=new Date().toISOString().replace("T"," ").substring(0,23),o=h[e].padEnd(5),m=t.padEnd(6),c="";n?.correlationId?c=`[${n.correlationId}] `:n?.sessionId&&(c=`[session-${n.sessionId}] `);let p="";r!=null&&(this.level===0&&typeof r=="object"?p=`
|
||||
`+JSON.stringify(r,null,2):p=" "+this.formatData(r));let L="";if(n){let{sessionId:B,sdkSessionId:W,correlationId:H,...k}=n;Object.keys(k).length>0&&(L=` {${Object.entries(k).map(([x,U])=>`${x}=${U}`).join(", ")}}`)}let A=`[${a}] [${o}] [${m}] ${c}${s}${L}${p}`;e===3?console.error(A):console.log(A)}debug(e,t,s,n){this.log(0,e,t,s,n)}info(e,t,s,n){this.log(1,e,t,s,n)}warn(e,t,s,n){this.log(2,e,t,s,n)}error(e,t,s,n){this.log(3,e,t,s,n)}dataIn(e,t,s,n){this.info(e,`\u2192 ${t}`,s,n)}dataOut(e,t,s,n){this.info(e,`\u2190 ${t}`,s,n)}success(e,t,s,n){this.info(e,`\u2713 ${t}`,s,n)}failure(e,t,s,n){this.error(e,`\u2717 ${t}`,s,n)}timing(e,t,s,n){this.info(e,`\u23F1 ${t}`,n,{duration:`${s}ms`})}},C=new f;var l=class{db;constructor(){D(u),this.db=new j(v),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(s=>s.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"))}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{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"));let m=this.db.pragma("index_list(session_summaries)").some(c=>c.unique===1)}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(s=>s.unique===1))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,
|
||||
@@ -146,7 +146,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(t,e).changes===0?(O.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:t}),!1):!0}setWorkerPort(e,t){this.db.prepare(`
|
||||
`).run(t,e).changes===0?(C.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:t}),!1):!0}setWorkerPort(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
@@ -155,17 +155,17 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}storeObservation(e,t,s,n){let r=new Date,i=r.getTime();this.db.prepare(`
|
||||
`).get(e)?.worker_port||null}storeObservation(e,t,s,n){let r=new Date,a=r.getTime();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,t,s.type,s.title,s.subtitle,JSON.stringify(s.facts),s.narrative,JSON.stringify(s.concepts),JSON.stringify(s.files_read),JSON.stringify(s.files_modified),n||null,r.toISOString(),i)}storeSummary(e,t,s,n){let r=new Date,i=r.getTime();this.db.prepare(`
|
||||
`).run(e,t,s.type,s.title,s.subtitle,JSON.stringify(s.facts),s.narrative,JSON.stringify(s.concepts),JSON.stringify(s.files_read),JSON.stringify(s.files_modified),n||null,r.toISOString(),a)}storeSummary(e,t,s,n){let r=new Date,a=r.getTime();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,t,s.request,s.investigated,s.learned,s.completed,s.next_steps,s.notes,n||null,r.toISOString(),i)}markSessionCompleted(e){let t=new Date,s=t.getTime();this.db.prepare(`
|
||||
`).run(e,t,s.request,s.investigated,s.learned,s.completed,s.next_steps,s.notes,n||null,r.toISOString(),a)}markSessionCompleted(e){let t=new Date,s=t.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
@@ -177,7 +177,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function h(c){let e=c?.cwd??process.cwd(),t=e?C.basename(e):"unknown-project",s=new l;try{let n=s.getRecentSessionsWithStatus(t,3);if(n.length===0)return`# Recent Session Context
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function F(){try{let i=y();if(T(i))return;console.error("[claude-mem] Worker not running, starting...");let e=N(),t=b.join(e,"dist","worker-service.cjs");if(!T(t)){console.error(`[claude-mem] Worker service not found at ${t}`);return}let s=b.join(e,"ecosystem.config.cjs");if(T(s))try{I("pm2",["start",s],{detached:!0,stdio:"ignore",cwd:e}).unref(),console.error("[claude-mem] Worker started with PM2");return}catch{console.error("[claude-mem] PM2 not available, using direct spawn")}I("node",[t],{detached:!0,stdio:"ignore",env:{...process.env,NODE_ENV:"production"}}).unref(),console.error("[claude-mem] Worker started in background")}catch(i){console.error(`[claude-mem] Failed to start worker: ${i.message}`)}}function S(i){F();let e=i?.cwd??process.cwd(),t=e?b.basename(e):"unknown-project",s=new l;try{let n=s.getRecentSessionsWithStatus(t,3);if(n.length===0)return`# Recent Session Context
|
||||
|
||||
No previous sessions found for this project yet.`;let r=[];r.push("# Recent Session Context"),r.push(""),r.push(`Showing last ${n.length} session(s) for **${t}**:`),r.push("");for(let i of n)if(i.sdk_session_id){if(r.push("---"),r.push(""),i.has_summary){let o=s.getSummaryForSession(i.sdk_session_id);if(o){let u=o.prompt_number?` (Prompt #${o.prompt_number})`:"";if(r.push(`**Summary${u}**`),r.push(""),o.request&&r.push(`**Request:** ${o.request}`),o.completed&&r.push(`**Completed:** ${o.completed}`),o.learned&&r.push(`**Learned:** ${o.learned}`),o.next_steps&&r.push(`**Next Steps:** ${o.next_steps}`),o.files_read)try{let p=JSON.parse(o.files_read);Array.isArray(p)&&p.length>0&&r.push(`**Files Read:** ${p.join(", ")}`)}catch{o.files_read.trim()&&r.push(`**Files Read:** ${o.files_read}`)}if(o.files_edited)try{let p=JSON.parse(o.files_edited);Array.isArray(p)&&p.length>0&&r.push(`**Files Edited:** ${p.join(", ")}`)}catch{o.files_edited.trim()&&r.push(`**Files Edited:** ${o.files_edited}`)}let a=new Date(o.created_at).toLocaleString();r.push(`**Date:** ${a}`)}}else if(i.status==="active"){r.push("**In Progress**"),r.push(""),i.user_prompt&&r.push(`**Request:** ${i.user_prompt}`);let o=s.getObservationsForSession(i.sdk_session_id);if(o.length>0){r.push(""),r.push(`**Observations (${o.length}):**`);for(let a of o)r.push(`- ${a.title}`)}else r.push(""),r.push("*No observations yet*");r.push(""),r.push("**Status:** Active - summary pending");let u=new Date(i.started_at).toLocaleString();r.push(`**Date:** ${u}`)}else{let o=i.status==="failed"?"stopped":i.status;r.push(`**${o.charAt(0).toUpperCase()+o.slice(1)}**`),r.push(""),i.user_prompt&&r.push(`**Request:** ${i.user_prompt}`),r.push(""),r.push(`**Status:** ${o} - no summary available`);let u=new Date(i.started_at).toLocaleString();r.push(`**Date:** ${u}`)}r.push("")}return r.join(`
|
||||
`)}finally{s.close()}}import{stdin as T}from"process";try{if(T.isTTY){let e={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:h()}};console.log(JSON.stringify(e)),process.exit(0)}else{let c="";T.on("data",e=>c+=e),T.on("end",()=>{let e=c.trim()?JSON.parse(c):void 0,s={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:h(e)}};console.log(JSON.stringify(s)),process.exit(0)})}}catch(c){console.error(`[claude-mem context-hook error: ${c.message}]`),process.exit(0)}
|
||||
No previous sessions found for this project yet.`;let r=[];r.push("# Recent Session Context"),r.push(""),r.push(`Showing last ${n.length} session(s) for **${t}**:`),r.push("");for(let a of n)if(a.sdk_session_id){if(r.push("---"),r.push(""),a.has_summary){let o=s.getSummaryForSession(a.sdk_session_id);if(o){let m=o.prompt_number?` (Prompt #${o.prompt_number})`:"";if(r.push(`**Summary${m}**`),r.push(""),o.request&&r.push(`**Request:** ${o.request}`),o.completed&&r.push(`**Completed:** ${o.completed}`),o.learned&&r.push(`**Learned:** ${o.learned}`),o.next_steps&&r.push(`**Next Steps:** ${o.next_steps}`),o.files_read)try{let p=JSON.parse(o.files_read);Array.isArray(p)&&p.length>0&&r.push(`**Files Read:** ${p.join(", ")}`)}catch{o.files_read.trim()&&r.push(`**Files Read:** ${o.files_read}`)}if(o.files_edited)try{let p=JSON.parse(o.files_edited);Array.isArray(p)&&p.length>0&&r.push(`**Files Edited:** ${p.join(", ")}`)}catch{o.files_edited.trim()&&r.push(`**Files Edited:** ${o.files_edited}`)}let c=new Date(o.created_at).toLocaleString();r.push(`**Date:** ${c}`)}}else if(a.status==="active"){r.push("**In Progress**"),r.push(""),a.user_prompt&&r.push(`**Request:** ${a.user_prompt}`);let o=s.getObservationsForSession(a.sdk_session_id);if(o.length>0){r.push(""),r.push(`**Observations (${o.length}):**`);for(let c of o)r.push(`- ${c.title}`)}else r.push(""),r.push("*No observations yet*");r.push(""),r.push("**Status:** Active - summary pending");let m=new Date(a.started_at).toLocaleString();r.push(`**Date:** ${m}`)}else{let o=a.status==="failed"?"stopped":a.status;r.push(`**${o.charAt(0).toUpperCase()+o.slice(1)}**`),r.push(""),a.user_prompt&&r.push(`**Request:** ${a.user_prompt}`),r.push(""),r.push(`**Status:** ${o} - no summary available`);let m=new Date(a.started_at).toLocaleString();r.push(`**Date:** ${m}`)}r.push("")}return r.join(`
|
||||
`)}finally{s.close()}}import{stdin as R}from"process";try{if(R.isTTY){let e={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:S()}};console.log(JSON.stringify(e)),process.exit(0)}else{let i="";R.on("data",e=>i+=e),R.on("end",()=>{let e=i.trim()?JSON.parse(i):void 0,s={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:S(e)}};console.log(JSON.stringify(s)),process.exit(0)})}}catch(i){console.error(`[claude-mem context-hook error: ${i.message}]`),process.exit(0)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import x from"path";import v from"better-sqlite3";import{join as u,dirname as j,basename as X}from"path";import{homedir as R}from"os";import{existsSync as K,mkdirSync as I}from"fs";var m=process.env.CLAUDE_MEM_DATA_DIR||u(R(),".claude-mem"),g=process.env.CLAUDE_CONFIG_DIR||u(R(),".claude"),J=u(m,"archives"),Y=u(m,"logs"),V=u(m,"trash"),Q=u(m,"backups"),z=u(m,"settings.json"),k=u(m,"claude-mem.db"),Z=u(g,"settings.json"),ee=u(g,"commands"),se=u(g,"CLAUDE.md");function O(o){I(o,{recursive:!0})}var b=(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))(b||{}),T=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=b[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 a=new Date().toISOString().replace("T"," ").substring(0,23),i=b[e].padEnd(5),d=s.padEnd(6),c="";r?.correlationId?c=`[${r.correlationId}] `:r?.sessionId&&(c=`[session-${r.sessionId}] `);let p="";n!=null&&(this.level===0&&typeof n=="object"?p=`
|
||||
`+JSON.stringify(n,null,2):p=" "+this.formatData(n));let l="";if(r){let{sessionId:U,sdkSessionId:P,correlationId:M,...h}=r;Object.keys(h).length>0&&(l=` {${Object.entries(h).map(([N,y])=>`${N}=${y}`).join(", ")}}`)}let S=`[${a}] [${i}] [${d}] ${c}${t}${l}${p}`;e===3?console.error(S):console.log(S)}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 _=class{db;constructor(){O(m),this.db=new v(k),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(t=>t.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"))}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{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"));let d=this.db.pragma("index_list(session_summaries)").some(c=>c.unique===1)}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(t=>t.unique===1))return;console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
import U from"path";import x from"better-sqlite3";import{join as p,dirname as W,basename as G}from"path";import{homedir as R}from"os";import{existsSync as Y,mkdirSync as v}from"fs";var y=()=>process.env.CLAUDE_PLUGIN_ROOT?p(process.env.CLAUDE_PLUGIN_ROOT,"data"):process.env.CLAUDE_MEM_DATA_DIR?process.env.CLAUDE_MEM_DATA_DIR:p(R(),".claude-mem"),m=y(),g=process.env.CLAUDE_CONFIG_DIR||p(R(),".claude"),V=p(m,"archives"),Q=p(m,"logs"),z=p(m,"trash"),Z=p(m,"backups"),ee=p(m,"settings.json"),O=p(m,"claude-mem.db"),se=p(g,"settings.json"),te=p(g,"commands"),re=p(g,"CLAUDE.md");function k(){return p(m,"worker.port")}function A(o){v(o,{recursive:!0})}var b=(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))(b||{}),T=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=b[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 a=new Date().toISOString().replace("T"," ").substring(0,23),i=b[e].padEnd(5),d=s.padEnd(6),c="";r?.correlationId?c=`[${r.correlationId}] `:r?.sessionId&&(c=`[session-${r.sessionId}] `);let u="";n!=null&&(this.level===0&&typeof n=="object"?u=`
|
||||
`+JSON.stringify(n,null,2):u=" "+this.formatData(n));let l="";if(r){let{sessionId:M,sdkSessionId:H,correlationId:$,...h}=r;Object.keys(h).length>0&&(l=` {${Object.entries(h).map(([D,I])=>`${D}=${I}`).join(", ")}}`)}let S=`[${a}] [${i}] [${d}] ${c}${t}${l}${u}`;e===3?console.error(S):console.log(S)}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`})}},L=new T;var _=class{db;constructor(){A(m),this.db=new x(O),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(t=>t.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"))}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{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"));let d=this.db.pragma("index_list(session_summaries)").some(c=>c.unique===1)}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(t=>t.unique===1))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,
|
||||
@@ -146,7 +146,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
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(`
|
||||
`).run(s,e).changes===0?(L.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 = ?
|
||||
@@ -177,4 +177,4 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function D(o,e,s){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function E(o,e,s={}){let t=D(o,e,s);return JSON.stringify(t)}async function w(){let{readFileSync:o,existsSync:e}=await import("fs"),{join:s}=await import("path"),{homedir:t}=await import("os"),r=s(t(),".claude-mem","worker.port");if(!e(r))return null;try{let n=o(r,"utf8").trim();return parseInt(n,10)}catch{return null}}async function L(o){if(!o)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=o,r=x.basename(s),n=new _;try{let a=n.findActiveSDKSession(e),i,d=!1;if(a){i=a.id;let p=n.incrementPromptCounter(i);console.error(`[new-hook] Continuing session ${i}, prompt #${p}`)}else{let p=n.findAnySDKSession(e);if(p){i=p.id,n.reactivateSession(i,t);let l=n.incrementPromptCounter(i);d=!0,console.error(`[new-hook] Reactivated session ${i}, prompt #${l}`)}else{i=n.createSDKSession(e,r,t);let l=n.incrementPromptCounter(i);d=!0,console.error(`[new-hook] Created new session ${i}, prompt #${l}`)}}let c=await w();if(!c){console.error("[new-hook] Worker service not running. Start with: npm run worker:start"),console.log(E("UserPromptSubmit",!0));return}if(d){let p=await fetch(`http://127.0.0.1:${c}/sessions/${i}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:t}),signal:AbortSignal.timeout(5e3)});p.ok||console.error("[new-hook] Failed to init session:",await p.text())}console.log(E("UserPromptSubmit",!0))}catch(a){console.error("[new-hook] FATAL ERROR:",a.message),console.error("[new-hook] Stack:",a.stack),console.error("[new-hook] Full error:",JSON.stringify(a,Object.getOwnPropertyNames(a))),console.log(E("UserPromptSubmit",!0))}finally{n.close()}}import{stdin as C}from"process";var f="";C.on("data",o=>f+=o);C.on("end",async()=>{try{let o=f.trim()?JSON.parse(f):void 0;await L(o),process.exit(0)}catch(o){console.error(`[claude-mem new-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function w(o,e,s){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function E(o,e,s={}){let t=w(o,e,s);return JSON.stringify(t)}async function P(){let{readFileSync:o,existsSync:e}=await import("fs"),s=k();if(!e(s))return null;try{let t=o(s,"utf8").trim();return parseInt(t,10)}catch{return null}}async function C(o){if(!o)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=o,r=U.basename(s),n=new _;try{let a=n.findActiveSDKSession(e),i,d=!1;if(a){i=a.id;let u=n.incrementPromptCounter(i);console.error(`[new-hook] Continuing session ${i}, prompt #${u}`)}else{let u=n.findAnySDKSession(e);if(u){i=u.id,n.reactivateSession(i,t);let l=n.incrementPromptCounter(i);d=!0,console.error(`[new-hook] Reactivated session ${i}, prompt #${l}`)}else{i=n.createSDKSession(e,r,t);let l=n.incrementPromptCounter(i);d=!0,console.error(`[new-hook] Created new session ${i}, prompt #${l}`)}}let c=await P();if(!c){console.error("[new-hook] Worker service not running. Start with: npm run worker:start"),console.log(E("UserPromptSubmit",!0));return}if(d){let u=await fetch(`http://127.0.0.1:${c}/sessions/${i}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:t}),signal:AbortSignal.timeout(5e3)});u.ok||console.error("[new-hook] Failed to init session:",await u.text())}console.log(E("UserPromptSubmit",!0))}catch(a){console.error("[new-hook] FATAL ERROR:",a.message),console.error("[new-hook] Stack:",a.stack),console.error("[new-hook] Full error:",JSON.stringify(a,Object.getOwnPropertyNames(a))),console.log(E("UserPromptSubmit",!0))}finally{n.close()}}import{stdin as N}from"process";var f="";N.on("data",o=>f+=o);N.on("end",async()=>{try{let o=f.trim()?JSON.parse(f):void 0;await C(o),process.exit(0)}catch(o){console.error(`[claude-mem new-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import y from"better-sqlite3";import{join as c,dirname as X,basename as j}from"path";import{homedir as O}from"os";import{existsSync as K,mkdirSync as C}from"fs";var p=process.env.CLAUDE_MEM_DATA_DIR||c(O(),".claude-mem"),g=process.env.CLAUDE_CONFIG_DIR||c(O(),".claude"),q=c(p,"archives"),J=c(p,"logs"),Y=c(p,"trash"),V=c(p,"backups"),Q=c(p,"settings.json"),L=c(p,"claude-mem.db"),z=c(g,"settings.json"),Z=c(g,"commands"),ee=c(g,"CLAUDE.md");function k(n){C(n,{recursive:!0})}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||{}),b=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}
|
||||
import y from"better-sqlite3";import{join as c,dirname as F,basename as j}from"path";import{homedir as O}from"os";import{existsSync as q,mkdirSync as C}from"fs";var D=()=>process.env.CLAUDE_PLUGIN_ROOT?c(process.env.CLAUDE_PLUGIN_ROOT,"data"):process.env.CLAUDE_MEM_DATA_DIR?process.env.CLAUDE_MEM_DATA_DIR:c(O(),".claude-mem"),p=D(),g=process.env.CLAUDE_CONFIG_DIR||c(O(),".claude"),J=c(p,"archives"),Y=c(p,"logs"),V=c(p,"trash"),Q=c(p,"backups"),z=c(p,"settings.json"),L=c(p,"claude-mem.db"),Z=c(g,"settings.json"),ee=c(g,"commands"),se=c(g,"CLAUDE.md");function A(n){C(n,{recursive:!0})}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||{}),b=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),d=T[e].padEnd(5),_=s.padEnd(6),a="";r?.correlationId?a=`[${r.correlationId}] `:r?.sessionId&&(a=`[session-${r.sessionId}] `);let m="";o!=null&&(this.level===0&&typeof o=="object"?m=`
|
||||
`+JSON.stringify(o,null,2):m=" "+this.formatData(o));let S="";if(r){let{sessionId:U,sdkSessionId:w,correlationId:P,...R}=r;Object.keys(R).length>0&&(S=` {${Object.entries(R).map(([N,v])=>`${N}=${v}`).join(", ")}}`)}let h=`[${i}] [${d}] [${_}] ${a}${t}${S}${m}`;e===3?console.error(h):console.log(h)}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 b;var E=class{db;constructor(){k(p),this.db=new y(L),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(t=>t.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"))}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{this.db.pragma("table_info(sdk_sessions)").some(a=>a.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(a=>a.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(a=>a.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"));let _=this.db.pragma("index_list(session_summaries)").some(a=>a.unique===1)}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(t=>t.unique===1))return;console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
`+JSON.stringify(o,null,2):m=" "+this.formatData(o));let S="";if(r){let{sessionId:P,sdkSessionId:w,correlationId:M,...R}=r;Object.keys(R).length>0&&(S=` {${Object.entries(R).map(([v,N])=>`${v}=${N}`).join(", ")}}`)}let h=`[${i}] [${d}] [${_}] ${a}${t}${S}${m}`;e===3?console.error(h):console.log(h)}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 b;var E=class{db;constructor(){A(p),this.db=new y(L),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(t=>t.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"))}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{this.db.pragma("table_info(sdk_sessions)").some(a=>a.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(a=>a.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(a=>a.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"));let _=this.db.pragma("index_list(session_summaries)").some(a=>a.unique===1)}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(t=>t.unique===1))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,
|
||||
@@ -177,4 +177,4 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function D(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 l(n,e,s={}){let t=D(n,e,s);return JSON.stringify(t)}var x=new Set(["ListMcpResourcesTool"]);async function A(n){if(!n)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_output:r}=n;if(x.has(s)){console.log(l("PostToolUse",!0));return}let o=new E,i=o.findActiveSDKSession(e);if(!i){o.close(),console.log(l("PostToolUse",!0));return}if(!i.worker_port){o.close(),u.error("HOOK","No worker port for session",{sessionId:i.id}),console.log(l("PostToolUse",!0));return}let d=o.getPromptCounter(i.id);o.close();let _=u.formatTool(s,t);try{u.dataIn("HOOK",`PostToolUse: ${_}`,{sessionId:i.id,workerPort:i.worker_port});let a=await fetch(`http://127.0.0.1:${i.worker_port}/sessions/${i.id}/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(a.ok)u.debug("HOOK","Observation sent successfully",{sessionId:i.id,toolName:s});else{let m=await a.text();u.failure("HOOK","Failed to send observation",{sessionId:i.id,status:a.status},m)}}catch(a){u.failure("HOOK","Error sending observation",{sessionId:i.id},a)}finally{console.log(l("PostToolUse",!0))}}import{stdin as I}from"process";var f="";I.on("data",n=>f+=n);I.on("end",async()=>{try{let n=f.trim()?JSON.parse(f):void 0;await A(n),process.exit(0)}catch(n){console.error(`[claude-mem save-hook error: ${n.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function x(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 l(n,e,s={}){let t=x(n,e,s);return JSON.stringify(t)}var U=new Set(["ListMcpResourcesTool"]);async function k(n){if(!n)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_output:r}=n;if(U.has(s)){console.log(l("PostToolUse",!0));return}let o=new E,i=o.findActiveSDKSession(e);if(!i){o.close(),console.log(l("PostToolUse",!0));return}if(!i.worker_port){o.close(),u.error("HOOK","No worker port for session",{sessionId:i.id}),console.log(l("PostToolUse",!0));return}let d=o.getPromptCounter(i.id);o.close();let _=u.formatTool(s,t);try{u.dataIn("HOOK",`PostToolUse: ${_}`,{sessionId:i.id,workerPort:i.worker_port});let a=await fetch(`http://127.0.0.1:${i.worker_port}/sessions/${i.id}/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(a.ok)u.debug("HOOK","Observation sent successfully",{sessionId:i.id,toolName:s});else{let m=await a.text();u.failure("HOOK","Failed to send observation",{sessionId:i.id,status:a.status},m)}}catch(a){u.failure("HOOK","Error sending observation",{sessionId:i.id},a)}finally{console.log(l("PostToolUse",!0))}}import{stdin as I}from"process";var f="";I.on("data",n=>f+=n);I.on("end",async()=>{try{let n=f.trim()?JSON.parse(f):void 0;await k(n),process.exit(0)}catch(n){console.error(`[claude-mem save-hook error: ${n.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import v from"better-sqlite3";import{join as a,dirname as $,basename as X}from"path";import{homedir as O}from"os";import{existsSync as G,mkdirSync as N}from"fs";var c=process.env.CLAUDE_MEM_DATA_DIR||a(O(),".claude-mem"),E=process.env.CLAUDE_CONFIG_DIR||a(O(),".claude"),q=a(c,"archives"),K=a(c,"logs"),J=a(c,"trash"),Y=a(c,"backups"),V=a(c,"settings.json"),k=a(c,"claude-mem.db"),Q=a(E,"settings.json"),z=a(E,"commands"),Z=a(E,"CLAUDE.md");function L(o){N(o,{recursive:!0})}var g=(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))(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,t){return`obs-${e}-${t}`}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 t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;try{let s=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&s.command){let r=s.command.length>50?s.command.substring(0,50)+"...":s.command;return`${e}(${r})`}if(e==="Read"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Edit"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Write"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,t,s,r,n){if(e<this.level)return;let u=new Date().toISOString().replace("T"," ").substring(0,23),d=g[e].padEnd(5),f=t.padEnd(6),i="";r?.correlationId?i=`[${r.correlationId}] `:r?.sessionId&&(i=`[session-${r.sessionId}] `);let _="";n!=null&&(this.level===0&&typeof n=="object"?_=`
|
||||
`+JSON.stringify(n,null,2):_=" "+this.formatData(n));let S="";if(r){let{sessionId:x,sdkSessionId:U,correlationId:w,...R}=r;Object.keys(R).length>0&&(S=` {${Object.entries(R).map(([y,C])=>`${y}=${C}`).join(", ")}}`)}let h=`[${u}] [${d}] [${f}] ${i}${s}${S}${_}`;e===3?console.error(h):console.log(h)}debug(e,t,s,r){this.log(0,e,t,s,r)}info(e,t,s,r){this.log(1,e,t,s,r)}warn(e,t,s,r){this.log(2,e,t,s,r)}error(e,t,s,r){this.log(3,e,t,s,r)}dataIn(e,t,s,r){this.info(e,`\u2192 ${t}`,s,r)}dataOut(e,t,s,r){this.info(e,`\u2190 ${t}`,s,r)}success(e,t,s,r){this.info(e,`\u2713 ${t}`,s,r)}failure(e,t,s,r){this.error(e,`\u2717 ${t}`,s,r)}timing(e,t,s,r){this.info(e,`\u23F1 ${t}`,r,{duration:`${s}ms`})}},p=new b;var m=class{db;constructor(){L(c),this.db=new v(k),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(s=>s.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"))}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{this.db.pragma("table_info(sdk_sessions)").some(i=>i.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(i=>i.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(i=>i.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"));let f=this.db.pragma("index_list(session_summaries)").some(i=>i.unique===1)}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(s=>s.unique===1))return;console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
import v from"better-sqlite3";import{join as i,dirname as X,basename as F}from"path";import{homedir as O}from"os";import{existsSync as q,mkdirSync as N}from"fs";var D=()=>process.env.CLAUDE_PLUGIN_ROOT?i(process.env.CLAUDE_PLUGIN_ROOT,"data"):process.env.CLAUDE_MEM_DATA_DIR?process.env.CLAUDE_MEM_DATA_DIR:i(O(),".claude-mem"),c=D(),E=process.env.CLAUDE_CONFIG_DIR||i(O(),".claude"),K=i(c,"archives"),J=i(c,"logs"),Y=i(c,"trash"),V=i(c,"backups"),Q=i(c,"settings.json"),L=i(c,"claude-mem.db"),z=i(E,"settings.json"),Z=i(E,"commands"),ee=i(E,"CLAUDE.md");function A(o){N(o,{recursive:!0})}var g=(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))(g||{}),T=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,t){return`obs-${e}-${t}`}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 t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;try{let s=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&s.command){let r=s.command.length>50?s.command.substring(0,50)+"...":s.command;return`${e}(${r})`}if(e==="Read"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Edit"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Write"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,t,s,r,n){if(e<this.level)return;let u=new Date().toISOString().replace("T"," ").substring(0,23),d=g[e].padEnd(5),f=t.padEnd(6),a="";r?.correlationId?a=`[${r.correlationId}] `:r?.sessionId&&(a=`[session-${r.sessionId}] `);let _="";n!=null&&(this.level===0&&typeof n=="object"?_=`
|
||||
`+JSON.stringify(n,null,2):_=" "+this.formatData(n));let S="";if(r){let{sessionId:U,sdkSessionId:w,correlationId:P,...R}=r;Object.keys(R).length>0&&(S=` {${Object.entries(R).map(([C,y])=>`${C}=${y}`).join(", ")}}`)}let h=`[${u}] [${d}] [${f}] ${a}${s}${S}${_}`;e===3?console.error(h):console.log(h)}debug(e,t,s,r){this.log(0,e,t,s,r)}info(e,t,s,r){this.log(1,e,t,s,r)}warn(e,t,s,r){this.log(2,e,t,s,r)}error(e,t,s,r){this.log(3,e,t,s,r)}dataIn(e,t,s,r){this.info(e,`\u2192 ${t}`,s,r)}dataOut(e,t,s,r){this.info(e,`\u2190 ${t}`,s,r)}success(e,t,s,r){this.info(e,`\u2713 ${t}`,s,r)}failure(e,t,s,r){this.error(e,`\u2717 ${t}`,s,r)}timing(e,t,s,r){this.info(e,`\u23F1 ${t}`,r,{duration:`${s}ms`})}},p=new T;var m=class{db;constructor(){A(c),this.db=new v(L),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(s=>s.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"))}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{this.db.pragma("table_info(sdk_sessions)").some(a=>a.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(a=>a.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(a=>a.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"));let f=this.db.pragma("index_list(session_summaries)").some(a=>a.unique===1)}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(s=>s.unique===1))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,
|
||||
@@ -177,4 +177,4 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function D(o,e,t){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:t.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&t.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:t.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...t.reason&&!e?{stopReason:t.reason}:{}}}function l(o,e,t={}){let s=D(o,e,t);return JSON.stringify(s)}async function A(o){if(!o)throw new Error("summaryHook requires input");let{session_id:e}=o,t=new m,s=t.findActiveSDKSession(e);if(!s){t.close(),console.log(l("Stop",!0));return}if(!s.worker_port){t.close(),p.error("HOOK","No worker port for session",{sessionId:s.id}),console.log(l("Stop",!0));return}let r=t.getPromptCounter(s.id);t.close();try{p.dataIn("HOOK","Stop: Requesting summary",{sessionId:s.id,workerPort:s.worker_port,promptNumber:r});let n=await fetch(`http://127.0.0.1:${s.worker_port}/sessions/${s.id}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:r}),signal:AbortSignal.timeout(2e3)});if(n.ok)p.debug("HOOK","Summary request sent successfully",{sessionId:s.id});else{let u=await n.text();p.failure("HOOK","Failed to generate summary",{sessionId:s.id,status:n.status},u)}}catch(n){p.failure("HOOK","Error requesting summary",{sessionId:s.id},n)}finally{console.log(l("Stop",!0))}}import{stdin as I}from"process";var T="";I.on("data",o=>T+=o);I.on("end",async()=>{try{let o=T.trim()?JSON.parse(T):void 0;await A(o),process.exit(0)}catch(o){console.error(`[claude-mem summary-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function x(o,e,t){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:t.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&t.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:t.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...t.reason&&!e?{stopReason:t.reason}:{}}}function l(o,e,t={}){let s=x(o,e,t);return JSON.stringify(s)}async function k(o){if(!o)throw new Error("summaryHook requires input");let{session_id:e}=o,t=new m,s=t.findActiveSDKSession(e);if(!s){t.close(),console.log(l("Stop",!0));return}if(!s.worker_port){t.close(),p.error("HOOK","No worker port for session",{sessionId:s.id}),console.log(l("Stop",!0));return}let r=t.getPromptCounter(s.id);t.close();try{p.dataIn("HOOK","Stop: Requesting summary",{sessionId:s.id,workerPort:s.worker_port,promptNumber:r});let n=await fetch(`http://127.0.0.1:${s.worker_port}/sessions/${s.id}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:r}),signal:AbortSignal.timeout(2e3)});if(n.ok)p.debug("HOOK","Summary request sent successfully",{sessionId:s.id});else{let u=await n.text();p.failure("HOOK","Failed to generate summary",{sessionId:s.id,status:n.status},u)}}catch(n){p.failure("HOOK","Error requesting summary",{sessionId:s.id},n)}finally{console.log(l("Stop",!0))}}import{stdin as I}from"process";var b="";I.on("data",o=>b+=o);I.on("end",async()=>{try{let o=b.trim()?JSON.parse(b):void 0;await k(o),process.exit(0)}catch(o){console.error(`[claude-mem summary-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
|
||||
@@ -1,361 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Transcript Replay Tool
|
||||
*
|
||||
* Plays back a Claude Code transcript through the memory system to test:
|
||||
* 1. Tool observation capture
|
||||
* 2. SDK worker processing
|
||||
* 3. SQLite storage
|
||||
* 4. Session summary generation
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import * as net from 'net';
|
||||
import { HooksDatabase } from '../src/services/sqlite/HooksDatabase';
|
||||
import { getWorkerSocketPath } from '../src/shared/paths';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
interface TranscriptLine {
|
||||
type: string;
|
||||
message?: {
|
||||
role?: string;
|
||||
content?: Array<{
|
||||
type: string;
|
||||
name?: string;
|
||||
input?: any;
|
||||
output?: string;
|
||||
id?: string;
|
||||
}>;
|
||||
};
|
||||
uuid?: string;
|
||||
sessionId?: string;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
interface ToolUse {
|
||||
id: string;
|
||||
name: string;
|
||||
input: any;
|
||||
output?: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse transcript JSONL file and extract tool uses with their results
|
||||
*/
|
||||
function parseTranscript(filePath: string): ToolUse[] {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const lines = content.trim().split('\n');
|
||||
|
||||
const toolUses: Map<string, ToolUse> = new Map();
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const event: TranscriptLine = JSON.parse(line);
|
||||
|
||||
// Capture tool_use from assistant messages
|
||||
if (event.type === 'assistant' && event.message?.content) {
|
||||
for (const item of event.message.content) {
|
||||
if (item.type === 'tool_use' && item.name && item.id) {
|
||||
toolUses.set(item.id, {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
input: item.input,
|
||||
timestamp: event.timestamp || new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture tool_result from user messages
|
||||
// Tool results come in user messages with tool_use_id
|
||||
if (event.type === 'user' && event.message?.content) {
|
||||
const content = event.message.content;
|
||||
|
||||
// Content can be array or single object
|
||||
const items = Array.isArray(content) ? content : [content];
|
||||
|
||||
for (const item of items) {
|
||||
if (item && typeof item === 'object' && 'type' in item && item.type === 'tool_result') {
|
||||
const toolUseId = (item as any).tool_use_id;
|
||||
const toolContent = (item as any).content;
|
||||
|
||||
if (toolUseId) {
|
||||
const toolUse = toolUses.get(toolUseId);
|
||||
if (toolUse) {
|
||||
toolUse.output = toolContent || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Skip invalid lines
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(toolUses.values()).filter(t => t.output !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send observation to SDK worker via Unix socket
|
||||
*/
|
||||
async function sendObservation(
|
||||
socketPath: string,
|
||||
toolName: string,
|
||||
toolInput: any,
|
||||
toolOutput: string
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = net.createConnection(socketPath, () => {
|
||||
const message = JSON.stringify({
|
||||
type: 'observation',
|
||||
tool_name: toolName,
|
||||
tool_input: toolInput,
|
||||
tool_output: toolOutput,
|
||||
});
|
||||
|
||||
client.write(message + '\n');
|
||||
client.end();
|
||||
resolve();
|
||||
});
|
||||
|
||||
client.on('error', reject);
|
||||
client.setTimeout(5000);
|
||||
client.on('timeout', () => {
|
||||
client.destroy();
|
||||
reject(new Error('Socket timeout'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send finalize message to SDK worker
|
||||
*/
|
||||
async function sendFinalize(socketPath: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = net.createConnection(socketPath, () => {
|
||||
const message = JSON.stringify({ type: 'finalize' });
|
||||
client.write(message + '\n');
|
||||
client.end();
|
||||
resolve();
|
||||
});
|
||||
|
||||
client.on('error', reject);
|
||||
client.setTimeout(5000);
|
||||
client.on('timeout', () => {
|
||||
client.destroy();
|
||||
reject(new Error('Socket timeout'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Main replay function
|
||||
*/
|
||||
async function replayTranscript(transcriptPath: string, projectName: string = 'claude-mem-test') {
|
||||
console.log('🎬 Starting transcript replay...\n');
|
||||
|
||||
// Parse transcript
|
||||
console.log(`📖 Parsing transcript: ${transcriptPath}`);
|
||||
const toolUses = parseTranscript(transcriptPath);
|
||||
console.log(` Found ${toolUses.length} tool uses\n`);
|
||||
|
||||
// Initialize database
|
||||
const hooksDb = new HooksDatabase();
|
||||
|
||||
// Create SDK session
|
||||
console.log('🔧 Creating SDK session...');
|
||||
const claudeSessionId = `replay-${Date.now()}`;
|
||||
const userPrompt = 'Replaying transcript for testing';
|
||||
|
||||
const sessionId = await hooksDb.createSDKSession(
|
||||
claudeSessionId,
|
||||
projectName,
|
||||
userPrompt
|
||||
);
|
||||
console.log(` Session ID: ${sessionId}`);
|
||||
|
||||
// Verify session was created
|
||||
const verifyQuery = (hooksDb as any).db.query(`
|
||||
SELECT id, claude_session_id, project FROM sdk_sessions WHERE id = ?
|
||||
`);
|
||||
const session = verifyQuery.get(sessionId);
|
||||
|
||||
if (!session) {
|
||||
console.error(' ❌ Session not found in database after creation!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(` ✅ Session verified in database\n`);
|
||||
|
||||
// Spawn SDK worker
|
||||
console.log('🚀 Spawning SDK worker...');
|
||||
const socketPath = getWorkerSocketPath(sessionId);
|
||||
|
||||
// Spawn worker exactly as production hooks do
|
||||
const workerPath = join(process.cwd(), 'scripts/hooks/worker.js');
|
||||
const worker = spawn('node', [workerPath, String(sessionId)], {
|
||||
detached: false, // Keep attached to see errors
|
||||
stdio: ['ignore', 'pipe', 'pipe'] // Pipe output to see what's happening
|
||||
});
|
||||
|
||||
worker.stdout?.on('data', (data) => {
|
||||
console.log(` [worker stdout] ${data}`);
|
||||
});
|
||||
|
||||
worker.stderr?.on('data', (data) => {
|
||||
console.error(` [worker stderr] ${data}`);
|
||||
});
|
||||
|
||||
worker.on('exit', (code, signal) => {
|
||||
console.error(` [worker] Exited with code ${code}, signal ${signal}`);
|
||||
});
|
||||
|
||||
worker.on('error', (err) => {
|
||||
console.error(`\n [worker] Process error:`, err.message);
|
||||
});
|
||||
|
||||
// Wait for socket to be ready
|
||||
console.log(` Waiting for socket: ${socketPath}`);
|
||||
|
||||
// Poll for socket existence
|
||||
let socketReady = false;
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
try {
|
||||
const fs = await import('fs');
|
||||
if (fs.existsSync(socketPath)) {
|
||||
socketReady = true;
|
||||
console.log(` ✅ Socket ready after ${(i + 1) * 500}ms`);
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
// Continue waiting
|
||||
}
|
||||
}
|
||||
|
||||
if (!socketReady) {
|
||||
console.log(` ⚠️ Socket not found after 15s, attempting to connect anyway...`);
|
||||
}
|
||||
|
||||
// Additional wait for worker to be fully initialized
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Send observations
|
||||
console.log(`\n📤 Sending ${toolUses.length} observations...`);
|
||||
let sent = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const toolUse of toolUses) {
|
||||
try {
|
||||
await sendObservation(
|
||||
socketPath,
|
||||
toolUse.name,
|
||||
toolUse.input,
|
||||
toolUse.output || ''
|
||||
);
|
||||
sent++;
|
||||
process.stdout.write(`\r Sent: ${sent}/${toolUses.length}`);
|
||||
|
||||
// Small delay between observations
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
} catch (err) {
|
||||
failed++;
|
||||
console.error(`\n ❌ Failed to send observation: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n ✅ Successfully sent ${sent} observations`);
|
||||
if (failed > 0) {
|
||||
console.log(` ⚠️ Failed to send ${failed} observations`);
|
||||
}
|
||||
|
||||
// Wait for processing
|
||||
console.log('\n⏳ Waiting for SDK to process observations...');
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
// Send finalize
|
||||
console.log('\n🏁 Sending finalize message...');
|
||||
try {
|
||||
await sendFinalize(socketPath);
|
||||
console.log(' ✅ Finalize message sent');
|
||||
} catch (err) {
|
||||
console.error(` ❌ Failed to send finalize: ${err.message}`);
|
||||
}
|
||||
|
||||
// Wait for summary generation
|
||||
console.log('\n⏳ Waiting for summary generation...');
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
|
||||
// Verify results
|
||||
console.log('\n🔍 Verifying results...\n');
|
||||
|
||||
// Check observations using direct DB query
|
||||
const observations = (hooksDb as any).db.query(`
|
||||
SELECT sdk_session_id, project, text, type, created_at
|
||||
FROM observations
|
||||
WHERE sdk_session_id = (
|
||||
SELECT sdk_session_id FROM sdk_sessions WHERE id = ?
|
||||
)
|
||||
ORDER BY created_at_epoch ASC
|
||||
`).all(sessionId);
|
||||
|
||||
console.log(` 📝 Observations stored: ${observations.length}`);
|
||||
|
||||
if (observations.length > 0) {
|
||||
console.log(' Sample observations:');
|
||||
observations.slice(0, 3).forEach((obs: any, i: number) => {
|
||||
console.log(` ${i + 1}. [${obs.type}] ${obs.text.substring(0, 60)}...`);
|
||||
});
|
||||
}
|
||||
|
||||
// Check summary using direct DB query
|
||||
const summary = (hooksDb as any).db.query(`
|
||||
SELECT request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, created_at
|
||||
FROM session_summaries
|
||||
WHERE sdk_session_id = (
|
||||
SELECT sdk_session_id FROM sdk_sessions WHERE id = ?
|
||||
)
|
||||
LIMIT 1
|
||||
`).get(sessionId);
|
||||
|
||||
if (summary) {
|
||||
console.log(`\n 📋 Summary generated:`);
|
||||
console.log(` Request: ${(summary as any).request?.substring(0, 60)}...`);
|
||||
console.log(` Completed: ${(summary as any).completed?.substring(0, 60)}...`);
|
||||
const filesRead = JSON.parse((summary as any).files_read || '[]');
|
||||
const filesEdited = JSON.parse((summary as any).files_edited || '[]');
|
||||
console.log(` Files read: ${filesRead.length}`);
|
||||
console.log(` Files edited: ${filesEdited.length}`);
|
||||
} else {
|
||||
console.log(`\n ⚠️ No summary generated`);
|
||||
}
|
||||
|
||||
// Cleanup (worker is detached and will exit on its own)
|
||||
console.log('\n✅ Replay complete!\n');
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
observationsCount: observations.length,
|
||||
hasSummary: !!summary,
|
||||
};
|
||||
}
|
||||
|
||||
// CLI interface
|
||||
const args = process.argv.slice(2);
|
||||
const transcriptPath = args[0] || join(process.cwd(), 'test-data/sample-transcript.jsonl');
|
||||
const projectName = args[1] || 'claude-mem-test';
|
||||
|
||||
replayTranscript(transcriptPath, projectName)
|
||||
.then((result) => {
|
||||
console.log('Results:', result);
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('❌ Replay failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,5 +1,8 @@
|
||||
import path from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { spawn } from 'child_process';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { getWorkerPortFilePath, getPackageRoot } from '../shared/paths.js';
|
||||
|
||||
export interface SessionStartInput {
|
||||
session_id?: string;
|
||||
@@ -10,6 +13,61 @@ export interface SessionStartInput {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure worker service is running
|
||||
* Auto-starts worker if not running (v4.0.0 feature)
|
||||
*/
|
||||
function ensureWorkerRunning(): void {
|
||||
try {
|
||||
const portFile = getWorkerPortFilePath();
|
||||
|
||||
// Check if worker is already running
|
||||
if (existsSync(portFile)) {
|
||||
// Worker appears to be running (port file exists)
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('[claude-mem] Worker not running, starting...');
|
||||
|
||||
// Find worker service path
|
||||
const packageRoot = getPackageRoot();
|
||||
const workerPath = path.join(packageRoot, 'dist', 'worker-service.cjs');
|
||||
|
||||
if (!existsSync(workerPath)) {
|
||||
console.error(`[claude-mem] Worker service not found at ${workerPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to start with PM2 first (preferred for production)
|
||||
const ecosystemPath = path.join(packageRoot, 'ecosystem.config.cjs');
|
||||
if (existsSync(ecosystemPath)) {
|
||||
try {
|
||||
spawn('pm2', ['start', ecosystemPath], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
cwd: packageRoot
|
||||
}).unref();
|
||||
console.error('[claude-mem] Worker started with PM2');
|
||||
return;
|
||||
} catch (pm2Error) {
|
||||
console.error('[claude-mem] PM2 not available, using direct spawn');
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: spawn worker directly
|
||||
spawn('node', [workerPath], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
env: { ...process.env, NODE_ENV: 'production' }
|
||||
}).unref();
|
||||
console.error('[claude-mem] Worker started in background');
|
||||
|
||||
} catch (error: any) {
|
||||
// Don't fail the hook if worker start fails
|
||||
console.error(`[claude-mem] Failed to start worker: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Context Hook - SessionStart
|
||||
* Shows user what happened in recent sessions
|
||||
@@ -17,6 +75,8 @@ export interface SessionStartInput {
|
||||
* Output: Returns formatted context string to be wrapped in hookSpecificOutput
|
||||
*/
|
||||
export function contextHook(input?: SessionStartInput): string {
|
||||
// v4.0.0: Ensure worker is running before loading context
|
||||
ensureWorkerRunning();
|
||||
const cwd = input?.cwd ?? process.cwd();
|
||||
const project = cwd ? path.basename(cwd) : 'unknown-project';
|
||||
|
||||
|
||||
+2
-3
@@ -1,6 +1,7 @@
|
||||
import path from 'path';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
import { getWorkerPortFilePath } from '../shared/paths.js';
|
||||
|
||||
export interface UserPromptSubmitInput {
|
||||
session_id: string;
|
||||
@@ -14,10 +15,8 @@ export interface UserPromptSubmitInput {
|
||||
*/
|
||||
async function getWorkerPort(): Promise<number | null> {
|
||||
const { readFileSync, existsSync } = await import('fs');
|
||||
const { join } = await import('path');
|
||||
const { homedir } = await import('os');
|
||||
|
||||
const portFile = join(homedir(), '.claude-mem', 'worker.port');
|
||||
const portFile = getWorkerPortFilePath();
|
||||
|
||||
if (!existsSync(portFile)) {
|
||||
return null;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { parseObservations, parseSummary } from '../sdk/parser.js';
|
||||
import type { SDKSession } from '../sdk/prompts.js';
|
||||
import { findAvailablePort } from '../utils/port-allocator.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { getWorkerPortFilePath, ensureAllDataDirs } from '../shared/paths.js';
|
||||
|
||||
const MODEL = 'claude-sonnet-4-5';
|
||||
const DISALLOWED_TOOLS = ['Glob', 'Grep', 'ListMcpResourcesTool', 'WebSearch'];
|
||||
@@ -91,10 +92,10 @@ class WorkerService {
|
||||
|
||||
// Write port to file for hooks to discover
|
||||
const { writeFileSync } = require('fs');
|
||||
const { join } = require('path');
|
||||
const { homedir } = require('os');
|
||||
const portFile = join(homedir(), '.claude-mem', 'worker.port');
|
||||
ensureAllDataDirs(); // Ensure data directory exists
|
||||
const portFile = getWorkerPortFilePath();
|
||||
writeFileSync(portFile, port.toString(), 'utf8');
|
||||
logger.info('SYSTEM', `Port file written to ${portFile}`);
|
||||
|
||||
resolve();
|
||||
}).on('error', reject);
|
||||
|
||||
+21
-1
@@ -7,10 +7,23 @@ import { fileURLToPath } from 'url';
|
||||
/**
|
||||
* Simple path configuration for claude-mem
|
||||
* Standard paths based on Claude Code conventions
|
||||
*
|
||||
* v4.0.0: Data directory now uses CLAUDE_PLUGIN_ROOT when available
|
||||
*/
|
||||
|
||||
// Base directories
|
||||
export const DATA_DIR = process.env.CLAUDE_MEM_DATA_DIR || join(homedir(), '.claude-mem');
|
||||
// Priority: CLAUDE_PLUGIN_ROOT/data > CLAUDE_MEM_DATA_DIR > ~/.claude-mem
|
||||
const getDataDir = (): string => {
|
||||
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
||||
return join(process.env.CLAUDE_PLUGIN_ROOT, 'data');
|
||||
}
|
||||
if (process.env.CLAUDE_MEM_DATA_DIR) {
|
||||
return process.env.CLAUDE_MEM_DATA_DIR;
|
||||
}
|
||||
return join(homedir(), '.claude-mem');
|
||||
};
|
||||
|
||||
export const DATA_DIR = getDataDir();
|
||||
export const CLAUDE_CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
|
||||
|
||||
// Data subdirectories
|
||||
@@ -40,6 +53,13 @@ export function getWorkerSocketPath(sessionId: number): string {
|
||||
return join(DATA_DIR, `worker-${sessionId}.sock`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get worker port file path
|
||||
*/
|
||||
export function getWorkerPortFilePath(): string {
|
||||
return join(DATA_DIR, 'worker.port');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a directory exists
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user