feat: Implement Worker Service with session management and SDK integration
- Added WorkerService to handle long-running HTTP service with session management. - Implemented endpoints for initializing, observing, finalizing, checking status, and deleting sessions. - Integrated with Claude SDK for processing observations and generating responses. - Added port allocator utility to dynamically find available ports for the service. - Configured TypeScript settings for the project.
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"source": "./claude-mem",
|
||||
"source": "./plugin",
|
||||
"description": "Persistent memory system for Claude Code"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,473 +0,0 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="claude-mem-logo-lm.webp#gh-light-mode-only" alt="claude-mem logo" width="360" height="auto" />
|
||||
<img src="claude-mem-logo-dm.webp#gh-dark-mode-only" alt="claude-mem logo" width="360" height="auto" />
|
||||
|
||||
<p>
|
||||
Memory compression and persistence system for Claude Code conversations
|
||||
</p>
|
||||
|
||||
|
||||
<!-- Badges -->
|
||||
<p>
|
||||
<a href="https://www.npmjs.com/package/claude-mem">
|
||||
<img src="https://img.shields.io/npm/v/claude-mem.svg" alt="npm version" />
|
||||
</a>
|
||||
<a href="https://github.com/thedotmack/claude-mem/graphs/contributors">
|
||||
<img src="https://img.shields.io/github/contributors/thedotmack/claude-mem" alt="contributors" />
|
||||
</a>
|
||||
<a href="">
|
||||
<img src="https://img.shields.io/github/last-commit/thedotmack/claude-mem" alt="last update" />
|
||||
</a>
|
||||
<a href="https://github.com/thedotmack/claude-mem/network/members">
|
||||
<img src="https://img.shields.io/github/forks/thedotmack/claude-mem" alt="forks" />
|
||||
</a>
|
||||
<a href="https://github.com/thedotmack/claude-mem/stargazers">
|
||||
<img src="https://img.shields.io/github/stars/thedotmack/claude-mem" alt="stars" />
|
||||
</a>
|
||||
<a href="https://github.com/thedotmack/claude-mem/issues/">
|
||||
<img src="https://img.shields.io/github/issues/thedotmack/claude-mem" alt="open issues" />
|
||||
</a>
|
||||
<a href="https://github.com/thedotmack/claude-mem/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/badge/license-AGPL--3.0-blue.svg" alt="license" />
|
||||
</a>
|
||||
<a href="https://nodejs.org/">
|
||||
<img src="https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg" alt="node version" />
|
||||
</a>
|
||||
<a href="https://modelcontextprotocol.io">
|
||||
<img src="https://img.shields.io/badge/MCP-compatible-purple.svg" alt="MCP compatible" />
|
||||
</a>
|
||||
<a href="https://claude.com/claude-code">
|
||||
<img src="https://img.shields.io/badge/Claude%20Code-enabled-orange.svg" alt="Claude Code enabled" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h4>
|
||||
<a href="https://github.com/thedotmack/claude-mem">Documentation</a>
|
||||
<span> · </span>
|
||||
<a href="https://github.com/thedotmack/claude-mem/issues/">Report Bug</a>
|
||||
<span> · </span>
|
||||
<a href="https://github.com/thedotmack/claude-mem/issues/">Request Feature</a>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<!-- Table of Contents -->
|
||||
# :notebook_with_decorative_cover: Table of Contents
|
||||
|
||||
- [About the Project](#star2-about-the-project)
|
||||
* [Tech Stack](#space_invader-tech-stack)
|
||||
* [Features](#dart-features)
|
||||
- [Getting Started](#toolbox-getting-started)
|
||||
* [Prerequisites](#bangbang-prerequisites)
|
||||
* [Installation](#gear-installation)
|
||||
* [Running Tests](#test_tube-running-tests)
|
||||
- [Usage](#eyes-usage)
|
||||
* [Basic Commands](#basic-commands)
|
||||
* [Hook System](#hook-system)
|
||||
* [Memory Operations](#memory-operations)
|
||||
* [ChromaDB MCP Tools](#chromadb-mcp-tools)
|
||||
* [Advanced Usage](#advanced-usage)
|
||||
- [Architecture](#building_construction-architecture)
|
||||
- [Configuration](#wrench-configuration)
|
||||
- [Roadmap](#compass-roadmap)
|
||||
- [Contributing](#wave-contributing)
|
||||
- [License](#warning-license)
|
||||
- [Contact](#handshake-contact)
|
||||
- [Acknowledgements](#gem-acknowledgements)
|
||||
|
||||
|
||||
|
||||
<!-- About the Project -->
|
||||
## :star2: About the Project
|
||||
|
||||
claude-mem automatically captures, compresses, and retrieves context across Claude Code sessions, enabling true long-term memory through semantic search and intelligent compression.
|
||||
|
||||
Perfect for developers who want their AI assistant to remember project context, past decisions, and conversation history across sessions without manual context management.
|
||||
|
||||
<!-- TechStack -->
|
||||
### :space_invader: Tech Stack
|
||||
|
||||
<details>
|
||||
<summary>Core Technologies</summary>
|
||||
<ul>
|
||||
<li><a href="https://www.typescriptlang.org/">TypeScript</a></li>
|
||||
<li><a href="https://nodejs.org/">Node.js</a></li>
|
||||
<li><a href="https://bun.sh/">Bun</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Storage & Memory</summary>
|
||||
<ul>
|
||||
<li><a href="https://www.trychroma.com/">ChromaDB</a> - Vector database for semantic search</li>
|
||||
<li><a href="https://www.sqlite.org/">SQLite</a> - Metadata and session tracking</li>
|
||||
<li><a href="https://github.com/WiseLibs/better-sqlite3">better-sqlite3</a> - Fast SQLite bindings</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>AI & Integration</summary>
|
||||
<ul>
|
||||
<li><a href="https://github.com/anthropics/anthropic-sdk-typescript">Anthropic Agent SDK</a> - Async compression</li>
|
||||
<li><a href="https://modelcontextprotocol.io">Model Context Protocol (MCP)</a> - Tool integration</li>
|
||||
<li><a href="https://claude.com/claude-code">Claude Code</a> - Streaming hooks</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<!-- Features -->
|
||||
### :dart: Features
|
||||
|
||||
- :brain: **Automatic Memory Compression** - Real-time conversation capture and intelligent summarization
|
||||
- :mag: **Semantic Search** - ChromaDB-powered vector search for intelligent context retrieval
|
||||
- :package: **Project Isolation** - Memories segregated by project with multi-project support
|
||||
- :arrows_counterclockwise: **Session Persistence** - Context loads automatically at session start and `/clear` command
|
||||
- :dart: **MCP Integration** - 15+ ChromaDB tools via Model Context Protocol
|
||||
- :floppy_disk: **SQLite Storage** - Fast metadata and session tracking with embedded database
|
||||
- :wastebasket: **Smart Trash** - Safe file deletion with recovery capabilities
|
||||
- :zap: **Streaming Hooks** - Sub-50ms overhead for real-time event capture
|
||||
- :robot: **Agent SDK Compression** - Async transcript processing without blocking conversations
|
||||
- :bar_chart: **Session Overviews** - Automatic session summaries with temporal context
|
||||
|
||||
<!-- Getting Started -->
|
||||
## :toolbox: Getting Started
|
||||
|
||||
<!-- Prerequisites -->
|
||||
### :bangbang: Prerequisites
|
||||
|
||||
This project requires Bun runtime and works best with Claude Code
|
||||
|
||||
- Bun >= 1.0.0 (required for SQLite support)
|
||||
- Claude Code with MCP support
|
||||
- macOS/Linux (POSIX-compliant system)
|
||||
|
||||
<!-- Installation -->
|
||||
### :gear: Installation
|
||||
|
||||
Install claude-mem globally via npm
|
||||
|
||||
```bash
|
||||
npm install -g claude-mem
|
||||
claude-mem install
|
||||
```
|
||||
|
||||
The interactive installer will guide you through three installation scopes:
|
||||
|
||||
- **User** - Install for current user (default, recommended)
|
||||
- **Project** - Install for current project only
|
||||
- **Local** - Install to custom directory
|
||||
|
||||
<!-- Running Tests -->
|
||||
### :test_tube: Running Tests
|
||||
|
||||
To run tests, use the following commands
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
bun test
|
||||
|
||||
# Run integration tests
|
||||
npm run test:integration
|
||||
|
||||
# Run with coverage
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
<!-- Usage -->
|
||||
## :eyes: Usage
|
||||
|
||||
### Basic Commands
|
||||
|
||||
```bash
|
||||
# Check installation status
|
||||
claude-mem status
|
||||
|
||||
# View operation logs
|
||||
claude-mem logs
|
||||
|
||||
# Load context for current project
|
||||
claude-mem load-context --project my-project
|
||||
|
||||
# View compressed memories (interactive)
|
||||
claude-mem restore
|
||||
|
||||
# Manage trash bin
|
||||
claude-mem trash view
|
||||
claude-mem restore
|
||||
claude-mem trash empty
|
||||
```
|
||||
|
||||
### Hook System
|
||||
|
||||
claude-mem integrates with Claude Code via streaming hooks that capture conversation events:
|
||||
|
||||
- **user-prompt-submit** - Captures user prompts in real-time
|
||||
- **post-tool-use** - Spawns Agent SDK for async compression
|
||||
- **stop-streaming** - Generates session overview and cleanup
|
||||
- **session-start** - Loads relevant context automatically
|
||||
|
||||
Hooks are configured during installation with a 180-second timeout and run transparently in the background.
|
||||
|
||||
### Memory Operations
|
||||
|
||||
#### Manual Compression
|
||||
|
||||
```bash
|
||||
claude-mem compress
|
||||
```
|
||||
|
||||
Compress Claude Code transcripts into searchable memories with semantic embeddings.
|
||||
|
||||
#### Context Loading
|
||||
|
||||
```bash
|
||||
# Load last 10 memories for current project
|
||||
claude-mem load-context
|
||||
|
||||
# Load specific number of memories
|
||||
claude-mem load-context --count 20
|
||||
|
||||
# Filter by project
|
||||
claude-mem load-context --project my-app
|
||||
|
||||
# Output raw JSON
|
||||
claude-mem load-context --raw
|
||||
```
|
||||
|
||||
#### Trash Management
|
||||
|
||||
claude-mem includes Smart Trash for safe file operations:
|
||||
|
||||
```bash
|
||||
# Move files to trash
|
||||
claude-mem trash file.txt
|
||||
claude-mem trash -r directory/
|
||||
|
||||
# View trash contents
|
||||
claude-mem trash view
|
||||
|
||||
# Restore files interactively
|
||||
claude-mem restore
|
||||
|
||||
# Empty trash permanently
|
||||
claude-mem trash empty
|
||||
```
|
||||
|
||||
### ChromaDB MCP Tools
|
||||
|
||||
claude-mem exposes 15+ ChromaDB operations via MCP:
|
||||
|
||||
```bash
|
||||
# List collections
|
||||
claude-mem chroma-list-collections
|
||||
|
||||
# Create collection
|
||||
claude-mem chroma-create-collection --collection-name memories
|
||||
|
||||
# Query documents semantically
|
||||
claude-mem chroma-query-documents \
|
||||
--collection-name memories \
|
||||
--query-texts '["authentication implementation"]' \
|
||||
--n-results 5
|
||||
|
||||
# Add documents
|
||||
claude-mem chroma-add-documents \
|
||||
--collection-name memories \
|
||||
--documents '["content here"]' \
|
||||
--ids '["mem-001"]'
|
||||
|
||||
# Get documents by ID
|
||||
claude-mem chroma-get-documents \
|
||||
--collection-name memories \
|
||||
--ids '["mem-001"]'
|
||||
|
||||
# Update documents
|
||||
claude-mem chroma-update-documents \
|
||||
--collection-name memories \
|
||||
--ids '["mem-001"]' \
|
||||
--documents '["updated content"]'
|
||||
|
||||
# Delete documents
|
||||
claude-mem chroma-delete-documents \
|
||||
--collection-name memories \
|
||||
--ids '["mem-001"]'
|
||||
```
|
||||
|
||||
See all available Chroma MCP commands with `claude-mem --help`.
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
#### Session Title Generation
|
||||
|
||||
```bash
|
||||
# Generate title and subtitle from prompt
|
||||
claude-mem generate-title "implemented authentication with OAuth"
|
||||
|
||||
# Output as JSON
|
||||
claude-mem generate-title "fixed bug in checkout" --json
|
||||
|
||||
# Save to database
|
||||
claude-mem generate-title "added feature" --session-id abc123 --save
|
||||
```
|
||||
|
||||
#### Diagnostics
|
||||
|
||||
```bash
|
||||
# Run environment diagnostics
|
||||
claude-mem doctor
|
||||
|
||||
# Output as JSON
|
||||
claude-mem doctor --json
|
||||
```
|
||||
|
||||
#### Changelog Generation
|
||||
|
||||
```bash
|
||||
# Generate changelog from memories
|
||||
claude-mem changelog
|
||||
|
||||
# Preview without saving
|
||||
claude-mem changelog --preview
|
||||
|
||||
# Generate for specific version
|
||||
claude-mem changelog --generate 3.9.0
|
||||
|
||||
# Search historical versions
|
||||
claude-mem changelog --historical 5
|
||||
```
|
||||
|
||||
## :building_construction: Architecture
|
||||
|
||||
### Storage Structure
|
||||
|
||||
```
|
||||
~/.claude-mem/
|
||||
├── archives/ # Compressed transcript backups
|
||||
├── chroma/ # ChromaDB vector database
|
||||
├── trash/ # Smart Trash with recovery
|
||||
├── hooks/ # Hook configurations
|
||||
├── logs/ # Operation logs
|
||||
└── claude-mem.db # SQLite metadata database
|
||||
```
|
||||
|
||||
### Memory System
|
||||
|
||||
**Hook Layer** - Real-time event capture via Claude Code streaming hooks with sub-50ms overhead
|
||||
|
||||
**SDK Worker** - Agent SDK subprocess for async observation processing and session summarization
|
||||
|
||||
**Observation Queue** - SQLite-based queue for buffering tool observations between hooks and SDK
|
||||
|
||||
**Session Storage** - SQLite tables for tracking SDK sessions, observations, and summaries
|
||||
|
||||
**MCP Server** - 15+ ChromaDB tools for memory operations and semantic search
|
||||
|
||||
### Hook Integration
|
||||
|
||||
Hooks communicate via JSON stdin/stdout and run with minimal overhead:
|
||||
|
||||
1. **new** (user-prompt-submit) - Creates SDK session and initializes Agent SDK worker for async processing
|
||||
2. **save** (post-tool-use) - Queues tool observations for async SDK processing
|
||||
3. **summary** (stop-streaming) - Triggers finalization, generates session overview, and cleanup
|
||||
4. **context** (session-start) - Loads project-specific session summaries automatically
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── bin/ # CLI entry point
|
||||
├── commands/ # Command implementations
|
||||
├── hooks/ # Hook implementations (new, save, summary, context)
|
||||
├── sdk/ # Agent SDK worker, prompts, and parser
|
||||
├── services/ # SQLite database and path discovery
|
||||
├── shared/ # Configuration, paths, settings, and types
|
||||
└── utils/ # Platform utilities
|
||||
|
||||
dist/ # Minified production bundle
|
||||
tests/ # Database, SDK, and integration tests
|
||||
docs/ # Documentation (BUILD.md, CHANGELOG.md)
|
||||
scripts/ # Build and publish automation
|
||||
```
|
||||
|
||||
## :wrench: Configuration
|
||||
|
||||
### Hook Timeout
|
||||
|
||||
Default hook timeout is 180 seconds. Configure during installation:
|
||||
|
||||
```bash
|
||||
claude-mem install --timeout 300000 # 5 minutes
|
||||
```
|
||||
|
||||
### MCP Server
|
||||
|
||||
Skip MCP server installation if needed:
|
||||
|
||||
```bash
|
||||
claude-mem install --skip-mcp
|
||||
```
|
||||
|
||||
### Force Reinstall
|
||||
|
||||
```bash
|
||||
claude-mem install --force
|
||||
```
|
||||
|
||||
<!-- Roadmap -->
|
||||
## :compass: Roadmap
|
||||
|
||||
* [x] Real-time conversation capture with streaming hooks
|
||||
* [x] ChromaDB vector storage for semantic search
|
||||
* [x] SQLite metadata and session tracking
|
||||
* [x] MCP server with 15+ ChromaDB tools
|
||||
* [x] Smart Trash for safe file deletion
|
||||
* [x] Automatic session overviews
|
||||
* [ ] Web UI for memory visualization
|
||||
* [ ] Cross-platform Windows support
|
||||
* [ ] Memory analytics and insights
|
||||
|
||||
<!-- Contributing -->
|
||||
## :wave: Contributing
|
||||
|
||||
<a href="https://github.com/thedotmack/claude-mem/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=thedotmack/claude-mem" />
|
||||
</a>
|
||||
|
||||
Contributions are always welcome!
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/AmazingFeature`)
|
||||
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
<!-- License -->
|
||||
## :warning: License
|
||||
|
||||
Distributed under the AGPL-3.0 License. See [LICENSE](LICENSE) for more information.
|
||||
|
||||
<!-- Contact -->
|
||||
## :handshake: Contact
|
||||
|
||||
Alex Newman - [@thedotmack](https://github.com/thedotmack)
|
||||
|
||||
Project Link: [https://github.com/thedotmack/claude-mem](https://github.com/thedotmack/claude-mem)
|
||||
|
||||
NPM Package: [https://www.npmjs.com/package/claude-mem](https://www.npmjs.com/package/claude-mem)
|
||||
|
||||
<!-- Acknowledgments -->
|
||||
## :gem: Acknowledgements
|
||||
|
||||
- [ChromaDB](https://www.trychroma.com/) - Vector database for AI applications
|
||||
- [Anthropic](https://www.anthropic.com/) - Claude AI and Agent SDK
|
||||
- [Model Context Protocol](https://modelcontextprotocol.io) - Standardized AI tool integration
|
||||
- [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) - Fast SQLite bindings
|
||||
- [Shields.io](https://shields.io/) - Beautiful README badges
|
||||
- [Awesome README Template](https://github.com/Louis3797/awesome-readme-template) - Template inspiration
|
||||
|
||||
---
|
||||
|
||||
**Philosophy**: claude-mem follows the **Make It Work First** approach - direct execution over defensive validation, natural failures instead of artificial guards, and memory as a living, evolving system. Context improves with use through semantic search, project isolation, and temporal relevance.
|
||||
|
||||
**Built with TypeScript, ChromaDB, SQLite, and the Anthropic Agent SDK**
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
# Socket File Not Created - Debug Hypotheses
|
||||
|
||||
## Problem Statement
|
||||
Worker process logs "Socket server listening: /Users/alexnewman/.claude-mem/worker-28.sock" but the socket file never appears on the filesystem. All connection attempts fail with `ENOENT`.
|
||||
|
||||
## Hypotheses (Ordered by Likelihood)
|
||||
|
||||
### H1: Worker Process Exits Immediately After Socket Creation
|
||||
**Theory:** Worker creates socket, logs message, then crashes/exits before we poll for the file.
|
||||
|
||||
**Evidence:**
|
||||
- We see the log message
|
||||
- Socket never appears
|
||||
- No other worker output after "listening" message
|
||||
|
||||
**Tests:**
|
||||
- Check if worker process is running: `ps aux | grep worker`
|
||||
- Add worker exit handlers to see exit code
|
||||
- Check if worker.ts crashes after startSocketServer()
|
||||
|
||||
**Root Cause Possibilities:**
|
||||
- Database query fails in loadSession() (worker.ts:75)
|
||||
- SDK agent initialization crashes
|
||||
- Unhandled promise rejection in run()
|
||||
|
||||
---
|
||||
|
||||
### H2: detached=false Kills Worker Prematurely
|
||||
**Theory:** `detached: false` causes worker to die when replay script continues execution or when replay script changes process state.
|
||||
|
||||
**Evidence:**
|
||||
- Production uses `detached: true, stdio: 'ignore'`
|
||||
- Replay uses `detached: false, stdio: ['ignore', 'pipe', 'pipe']`
|
||||
- Worker might be getting killed by parent process lifecycle
|
||||
|
||||
**Tests:**
|
||||
- Change to `detached: true, stdio: 'ignore', worker.unref()`
|
||||
- Check worker persists: `ps aux | grep worker` after spawn
|
||||
|
||||
**Expected Fix:**
|
||||
- Worker should persist independently
|
||||
- Socket should remain available
|
||||
|
||||
---
|
||||
|
||||
### H3: stdio Piping Interferes with Socket Creation
|
||||
**Theory:** Piping stdout/stderr (`stdio: ['ignore', 'pipe', 'pipe']`) prevents proper socket file creation or causes worker to hang.
|
||||
|
||||
**Evidence:**
|
||||
- Production uses `stdio: 'ignore'`
|
||||
- We're trying to capture output with pipes
|
||||
- This might interfere with Unix domain socket operations
|
||||
|
||||
**Tests:**
|
||||
- Change to `stdio: 'ignore'` (no piping)
|
||||
- Worker won't output to our console but should work
|
||||
|
||||
---
|
||||
|
||||
### H4: Socket Path Mismatch
|
||||
**Theory:** Worker creates socket at different path than replay script expects.
|
||||
|
||||
**Evidence:**
|
||||
- getWorkerSocketPath(sessionId) used in both places
|
||||
- Both should resolve to ~/.claude-mem/worker-<id>.sock
|
||||
- But maybe DATA_DIR differs between environments
|
||||
|
||||
**Tests:**
|
||||
- Log actual socketPath in worker: `console.error('Creating socket at:', this.socketPath)`
|
||||
- List all sockets: `ls -la ~/.claude-mem/*.sock`
|
||||
- Check if socket appears elsewhere: `find /tmp -name "worker-*.sock"`
|
||||
|
||||
**Root Cause Possibilities:**
|
||||
- CLAUDE_MEM_DATA_DIR environment variable difference
|
||||
- Worker started with different env
|
||||
|
||||
---
|
||||
|
||||
### H5: Permissions Issue
|
||||
**Theory:** Worker can't create socket file due to directory permissions.
|
||||
|
||||
**Evidence:**
|
||||
- Socket creation might fail silently
|
||||
- Worker logs "listening" before checking if socket file was created
|
||||
|
||||
**Tests:**
|
||||
- Check ~/.claude-mem permissions: `ls -ld ~/.claude-mem`
|
||||
- Try creating socket manually: `nc -U ~/.claude-mem/test.sock`
|
||||
- Check worker user vs replay script user
|
||||
|
||||
**Expected Error:**
|
||||
- Worker should throw EACCES or EPERM but we might not see it
|
||||
|
||||
---
|
||||
|
||||
### H6: Socket Listen Callback Fires Before File Creation
|
||||
**Theory:** The server.listen() callback fires and logs "listening" before the socket file actually appears on filesystem.
|
||||
|
||||
**Evidence:**
|
||||
- Node.js/Bun might call callback before filesystem sync
|
||||
- We see log but no file
|
||||
|
||||
**Tests:**
|
||||
- Add additional wait time after seeing log
|
||||
- Add fs.existsSync check inside worker after listen()
|
||||
- Increase poll duration/frequency in replay script
|
||||
|
||||
---
|
||||
|
||||
### H7: CLI Worker Command Routing Broken
|
||||
**Theory:** `dist/claude-mem.min.js worker <sessionId>` doesn't properly route to worker.ts main().
|
||||
|
||||
**Evidence:**
|
||||
- cli.ts has .command('worker') handler
|
||||
- Handler imports and calls main() from sdk/worker.ts
|
||||
- But bundling might break this
|
||||
|
||||
**Tests:**
|
||||
- Run directly: `dist/claude-mem.min.js worker 28`
|
||||
- Check if worker main() is actually called
|
||||
- Add console.error at top of worker.ts main()
|
||||
|
||||
**Root Cause Possibilities:**
|
||||
- Bundle doesn't include worker code
|
||||
- Import path broken in minified CLI
|
||||
- Commander routing fails
|
||||
|
||||
---
|
||||
|
||||
### H8: Database Session Not Found by Worker
|
||||
**Theory:** Worker can't find session in database, exits early.
|
||||
|
||||
**Evidence:**
|
||||
- loadSession() query might return null
|
||||
- Code checks `if (!session) { exit(1) }` (worker.ts:76-79)
|
||||
- But we'd expect to see error log
|
||||
|
||||
**Tests:**
|
||||
- Verify session exists before spawn: `SELECT * FROM sdk_sessions WHERE id = ?`
|
||||
- Add debug log in loadSession() before query
|
||||
- Check DB file path matches
|
||||
|
||||
---
|
||||
|
||||
### H9: Socket File Created Then Immediately Deleted
|
||||
**Theory:** Socket is created but something deletes it (cleanup from previous run, OS, etc).
|
||||
|
||||
**Evidence:**
|
||||
- Old socket file might exist and get unlinked (worker.ts:110-112)
|
||||
- Maybe multiple workers spawning
|
||||
|
||||
**Tests:**
|
||||
- Check for multiple worker processes: `ps aux | grep worker`
|
||||
- Watch filesystem in real-time: `watch ls -la ~/.claude-mem/`
|
||||
- Add delay before cleanup code runs
|
||||
|
||||
---
|
||||
|
||||
### H10: Bun vs Node Runtime Issue
|
||||
**Theory:** Worker runs under different runtime than expected, causing socket issues.
|
||||
|
||||
**Evidence:**
|
||||
- Replay script uses bun: `#!/usr/bin/env bun`
|
||||
- Worker spawned via CLI which uses node: `#!/usr/bin/env node`
|
||||
- Runtime difference might affect socket creation
|
||||
|
||||
**Tests:**
|
||||
- Spawn with explicit bun: `bun dist/claude-mem.min.js worker 28`
|
||||
- Or spawn with explicit node
|
||||
- Check if runtime matters for Unix sockets
|
||||
|
||||
---
|
||||
|
||||
### H11: Race Condition in Socket Server Startup
|
||||
**Theory:** server.listen() completes but socket isn't ready for connections yet.
|
||||
|
||||
**Evidence:**
|
||||
- We poll for 15 seconds
|
||||
- Maybe socket file appears but isn't ready
|
||||
- Connection attempts might be too early
|
||||
|
||||
**Tests:**
|
||||
- Increase wait time after socket found
|
||||
- Try connecting with retry logic
|
||||
- Check socket file permissions/readiness
|
||||
|
||||
---
|
||||
|
||||
### H12: Worker Logs to Wrong Stream
|
||||
**Theory:** Worker logs "listening" to stdout/stderr but then crashes, and we only see initial log.
|
||||
|
||||
**Evidence:**
|
||||
- console.error used in worker (worker.ts:86)
|
||||
- With stdio: ['ignore', 'pipe', 'pipe'], stderr is piped
|
||||
- Maybe crash happens but we don't see it
|
||||
|
||||
**Tests:**
|
||||
- Check full worker output captured
|
||||
- Look for crash stack traces
|
||||
- Add more logging throughout worker.run()
|
||||
|
||||
---
|
||||
|
||||
## Recommended Debug Sequence
|
||||
|
||||
1. **Change spawn config to match production exactly**
|
||||
- `detached: true`
|
||||
- `stdio: 'ignore'`
|
||||
- `worker.unref()`
|
||||
- This eliminates H2, H3
|
||||
|
||||
2. **Check worker process persistence**
|
||||
- `ps aux | grep worker` immediately after spawn
|
||||
- If not running → H1, H7, H8
|
||||
- If running → H4, H5, H6
|
||||
|
||||
3. **Check socket file location**
|
||||
- `ls -la ~/.claude-mem/*.sock`
|
||||
- `find /tmp -name "worker-*.sock"`
|
||||
- If found elsewhere → H4
|
||||
- If not found → H1, H5, H6
|
||||
|
||||
4. **Run worker directly for debugging**
|
||||
- `dist/claude-mem.min.js worker 28` manually
|
||||
- See full output
|
||||
- Check if socket appears
|
||||
|
||||
5. **Add more worker logging**
|
||||
- Log at start of main()
|
||||
- Log after loadSession()
|
||||
- Log after startSocketServer() promise resolves
|
||||
- Log socket path being used
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"claude-mem": {
|
||||
"command": "uvx",
|
||||
"args": ["chroma-mcp", "--client-type", "persistent", "--data-dir", "${CLAUDE_PLUGIN_ROOT}/chroma"]
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -1,23 +0,0 @@
|
||||
---
|
||||
argument-hint: help | save [message] | remember [context] | (no args for help)
|
||||
description: Manage claude-mem operations and memory context
|
||||
allowed-tools: Bash(claude-mem:*), Bash(echo:*), Bash(cat:*)
|
||||
---
|
||||
|
||||
## Claude-Mem Command Handler
|
||||
|
||||
### Check for help command first
|
||||
!`[ -z "$ARGUMENTS" ] || [ "$ARGUMENTS" = "help" ] && printf '%s\n' '## 🧠 Claude-Mem Help' '' '**Available Commands:**' '' '• /claude-mem save [message] - Quick save of conversation overview' '• /claude-mem remember [query] - Search saved memories' '• /claude-mem help - Show this help' '' '**Quick Shortcuts:**' '• /save - Direct save' '• /remember - Direct search' '' '**About /save:**' 'Quick way to save an overview to claude-mem without processing the' 'entire transcript. Use this when you dont need a detailed archive,' 'just a summary of key points and decisions.' '' '**Optional Features (configure during install):**' '• Compress on /clear: Archives full transcript when clearing (off by default)' '• Session start: Loads recent memories when starting Claude Code' '' 'For more details: claude-mem --help' && exit 0`
|
||||
|
||||
### Process other commands
|
||||
Handle claude-mem operation: $ARGUMENTS
|
||||
|
||||
If $ARGUMENTS starts with "save":
|
||||
- Write an overview of the current conversation context
|
||||
- Add it to claude-mem using the chroma MCP tools
|
||||
- Save the overview using: `claude-mem save "your overview message"`
|
||||
|
||||
If $ARGUMENTS starts with "remember":
|
||||
- Search claude-mem for relevant memories using the query
|
||||
- Display the most relevant memories from previous sessions
|
||||
- Use chroma_query_documents to find and present context
|
||||
@@ -1 +0,0 @@
|
||||
Search claude-mem for #$ARGUMENTS and look up relevant context to help clarify what we are working on.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
allowed-tools: Bash
|
||||
description: Write an overview and save with claude-mem
|
||||
---
|
||||
**Write an overview** of the current conversation context and:
|
||||
1. **Add it to claude-mem** using the chroma MCP tools. Always use primitive types (strings, numbers, booleans) when calling MCP Chroma tools directly. Arrays should be comma-separated strings, and nested objects should be flattened.
|
||||
2. **Save the overview to index** using the claude-mem CLI tool: `claude-mem save "your overview message"`
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
// @bun
|
||||
import{existsSync as O,unlinkSync as U}from"fs";import{Database as N}from"bun:sqlite";import{join as $,dirname as b,basename as g}from"path";import{homedir as J}from"os";import{existsSync as P,mkdirSync as L}from"fs";var W=process.env.CLAUDE_MEM_DATA_DIR||$(J(),".claude-mem"),V=process.env.CLAUDE_CONFIG_DIR||$(J(),".claude"),y=$(W,"archives"),S=$(W,"logs"),l=$(W,"trash"),R=$(W,"backups"),k=$(W,"chroma"),j=$(W,"settings.json"),M=$(W,"claude-mem.db"),A=$(V,"settings.json"),h=$(V,"commands"),I=$(V,"CLAUDE.md");function q(z){return $(W,`worker-${z}.sock`)}function F(z){L(z,{recursive:!0})}class v{db;constructor(){F(W),this.db=new N(M,{create:!0,readwrite:!0}),this.db.run("PRAGMA journal_mode = WAL"),this.db.run("PRAGMA synchronous = NORMAL"),this.db.run("PRAGMA foreign_keys = ON")}getRecentSummaries(z,Q=10){return this.db.query(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(z,Q)}findActiveSDKSession(z){return this.db.query(`
|
||||
SELECT id, sdk_session_id, project
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ? AND status = 'active'
|
||||
LIMIT 1
|
||||
`).get(z)||null}createSDKSession(z,Q,X){let Z=new Date,Y=Z.getTime();return this.db.query(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(z,Q,X,Z.toISOString(),Y),this.db.query("SELECT last_insert_rowid() as id").get().id}updateSDKSessionId(z,Q){this.db.query(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ?
|
||||
`).run(Q,z)}storeObservation(z,Q,X,Z){let Y=new Date,K=Y.getTime();this.db.query(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(z,Q,Z,X,Y.toISOString(),K)}storeSummary(z,Q,X){let Z=new Date,Y=Z.getTime();this.db.query(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, files_read, files_edited, notes, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(z,Q,X.request||null,X.investigated||null,X.learned||null,X.completed||null,X.next_steps||null,X.files_read||null,X.files_edited||null,X.notes||null,Z.toISOString(),Y)}markSessionCompleted(z){let Q=new Date,X=Q.getTime();this.db.query(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(Q.toISOString(),X,z)}markSessionFailed(z){let Q=new Date,X=Q.getTime();this.db.query(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(Q.toISOString(),X,z)}close(){this.db.close()}}function G(z){try{if(console.error("[claude-mem cleanup] Hook fired",{input:z?{session_id:z.session_id,cwd:z.cwd,reason:z.reason}:null}),!z)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:Q,reason:X}=z;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:Q,reason:X});let Z=new v,Y=Z.findActiveSDKSession(Q);if(!Y)console.error("[claude-mem cleanup] No active SDK session found",{session_id:Q}),Z.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0);console.error("[claude-mem cleanup] Active SDK session found",{session_id:Y.id,sdk_session_id:Y.sdk_session_id,project:Y.project});let K=q(Y.id);try{if(O(K)){console.error("[claude-mem cleanup] Socket file exists, attempting cleanup",{socketPath:K});try{U(K),console.error("[claude-mem cleanup] Socket file removed successfully",{socketPath:K})}catch(B){console.error("[claude-mem cleanup] Failed to remove socket file",{error:B.message,socketPath:K})}}else console.error("[claude-mem cleanup] Socket file does not exist",{socketPath:K})}catch(B){console.error("[claude-mem cleanup] Error during cleanup",{error:B.message,stack:B.stack})}try{Z.markSessionFailed(Y.id),console.error("[claude-mem cleanup] Session marked as failed",{session_id:Y.id,reason:"SessionEnd hook - session terminated without completion"})}catch(B){console.error("[claude-mem cleanup] Failed to mark session as failed",{error:B.message,session_id:Y.id})}Z.close(),console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}catch(Q){console.error("[claude-mem cleanup] Unexpected error in hook",{error:Q.message,stack:Q.stack,name:Q.name}),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}}var x=await Bun.stdin.text();try{let z=x.trim()?JSON.parse(x):void 0;G(z)}catch(z){console.error(`[claude-mem cleanup-hook error: ${z.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}
|
||||
@@ -1,44 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
// @bun
|
||||
import U from"path";import{Database as O}from"bun:sqlite";import{join as K,dirname as b,basename as g}from"path";import{homedir as F}from"os";import{existsSync as P,mkdirSync as N}from"fs";var B=process.env.CLAUDE_MEM_DATA_DIR||K(F(),".claude-mem"),J=process.env.CLAUDE_CONFIG_DIR||K(F(),".claude"),C=K(B,"archives"),w=K(B,"logs"),k=K(B,"trash"),S=K(B,"backups"),h=K(B,"chroma"),l=K(B,"settings.json"),G=K(B,"claude-mem.db"),R=K(J,"settings.json"),A=K(J,"commands"),j=K(J,"CLAUDE.md");function L(z){N(z,{recursive:!0})}class M{db;constructor(){L(B),this.db=new O(G,{create:!0,readwrite:!0}),this.db.run("PRAGMA journal_mode = WAL"),this.db.run("PRAGMA synchronous = NORMAL"),this.db.run("PRAGMA foreign_keys = ON")}getRecentSummaries(z,Q=10){return this.db.query(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(z,Q)}findActiveSDKSession(z){return this.db.query(`
|
||||
SELECT id, sdk_session_id, project
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ? AND status = 'active'
|
||||
LIMIT 1
|
||||
`).get(z)||null}createSDKSession(z,Q,X){let $=new Date,W=$.getTime();return this.db.query(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(z,Q,X,$.toISOString(),W),this.db.query("SELECT last_insert_rowid() as id").get().id}updateSDKSessionId(z,Q){this.db.query(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ?
|
||||
`).run(Q,z)}storeObservation(z,Q,X,$){let W=new Date,Y=W.getTime();this.db.query(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(z,Q,$,X,W.toISOString(),Y)}storeSummary(z,Q,X){let $=new Date,W=$.getTime();this.db.query(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, files_read, files_edited, notes, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(z,Q,X.request||null,X.investigated||null,X.learned||null,X.completed||null,X.next_steps||null,X.files_read||null,X.files_edited||null,X.notes||null,$.toISOString(),W)}markSessionCompleted(z){let Q=new Date,X=Q.getTime();this.db.query(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(Q.toISOString(),X,z)}markSessionFailed(z){let Q=new Date,X=Q.getTime();this.db.query(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(Q.toISOString(),X,z)}close(){this.db.close()}}function q(z){let Q=z?.cwd??process.cwd(),X=Q?U.basename(Q):"unknown-project",$=new M;try{let W=$.getRecentSummaries(X,5);if(W.length===0){console.log(`# Recent Session Context
|
||||
|
||||
No previous sessions found for this project yet.`);return}let Y=[];Y.push("# Recent Session Context"),Y.push("");let v=W.length===1?"session":"sessions";Y.push(`Showing last ${W.length} ${v} for **${X}**:`),Y.push("");for(let Z of W){if(Y.push("---"),Y.push(""),Z.request)Y.push(`**Request:** ${Z.request}`);if(Z.completed)Y.push(`**Completed:** ${Z.completed}`);if(Z.learned)Y.push(`**Learned:** ${Z.learned}`);if(Z.next_steps)Y.push(`**Next Steps:** ${Z.next_steps}`);if(Z.files_read)try{let V=JSON.parse(Z.files_read);if(Array.isArray(V)&&V.length>0)Y.push(`**Files Read:** ${V.join(", ")}`)}catch{if(Z.files_read.trim())Y.push(`**Files Read:** ${Z.files_read}`)}if(Z.files_edited)try{let V=JSON.parse(Z.files_edited);if(Array.isArray(V)&&V.length>0)Y.push(`**Files Edited:** ${V.join(", ")}`)}catch{if(Z.files_edited.trim())Y.push(`**Files Edited:** ${Z.files_edited}`)}Y.push(`**Date:** ${Z.created_at.split("T")[0]}`),Y.push("")}console.log(Y.join(`
|
||||
`))}finally{$.close()}}try{if(process.stdin.isTTY)q();else{let z=await Bun.stdin.text(),Q=z.trim()?JSON.parse(z):void 0;q(Q)}}catch(z){console.error(`[claude-mem context-hook error: ${z.message}]`),process.exit(0)}
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
// @bun
|
||||
import{spawn as b}from"child_process";import L from"path";import{Database as E}from"bun:sqlite";import{join as Z,dirname as w,basename as S}from"path";import{homedir as F}from"os";import{existsSync as j,mkdirSync as f}from"fs";var $=process.env.CLAUDE_MEM_DATA_DIR||Z(F(),".claude-mem"),B=process.env.CLAUDE_CONFIG_DIR||Z(F(),".claude"),R=Z($,"archives"),y=Z($,"logs"),_=Z($,"trash"),k=Z($,"backups"),I=Z($,"chroma"),h=Z($,"settings.json"),G=Z($,"claude-mem.db"),d=Z(B,"settings.json"),D=Z(B,"commands"),m=Z(B,"CLAUDE.md");function x(Q){f(Q,{recursive:!0})}class V{db;constructor(){x($),this.db=new E(G,{create:!0,readwrite:!0}),this.db.run("PRAGMA journal_mode = WAL"),this.db.run("PRAGMA synchronous = NORMAL"),this.db.run("PRAGMA foreign_keys = ON")}getRecentSummaries(Q,X=10){return this.db.query(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(Q,X)}findActiveSDKSession(Q){return this.db.query(`
|
||||
SELECT id, sdk_session_id, project
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ? AND status = 'active'
|
||||
LIMIT 1
|
||||
`).get(Q)||null}createSDKSession(Q,X,W){let Y=new Date,z=Y.getTime();return this.db.query(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(Q,X,W,Y.toISOString(),z),this.db.query("SELECT last_insert_rowid() as id").get().id}updateSDKSessionId(Q,X){this.db.query(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ?
|
||||
`).run(X,Q)}storeObservation(Q,X,W,Y){let z=new Date,K=z.getTime();this.db.query(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(Q,X,Y,W,z.toISOString(),K)}storeSummary(Q,X,W){let Y=new Date,z=Y.getTime();this.db.query(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, files_read, files_edited, notes, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(Q,X,W.request||null,W.investigated||null,W.learned||null,W.completed||null,W.next_steps||null,W.files_read||null,W.files_edited||null,W.notes||null,Y.toISOString(),z)}markSessionCompleted(Q){let X=new Date,W=X.getTime();this.db.query(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(X.toISOString(),W,Q)}markSessionFailed(Q){let X=new Date,W=X.getTime();this.db.query(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(X.toISOString(),W,Q)}close(){this.db.close()}}function H(Q,X,W){if(Q==="PreCompact"){if(X)return{continue:!0,suppressOutput:!0};return{continue:!1,stopReason:W.reason||"Pre-compact operation failed",suppressOutput:!0}}if(Q==="SessionStart"){if(X&&W.context)return{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:W.context}};return{continue:!0,suppressOutput:!0}}if(Q==="UserPromptSubmit"||Q==="PostToolUse")return{continue:!0,suppressOutput:!0};if(Q==="Stop")return{continue:!0,suppressOutput:!0};return{continue:X,suppressOutput:!0,...W.reason&&!X?{stopReason:W.reason}:{}}}function v(Q,X,W={}){let Y=H(Q,X,W);return JSON.stringify(Y)}function O(Q){if(!Q)throw new Error("newHook requires input");let{session_id:X,cwd:W,prompt:Y}=Q,z=L.basename(W),K=new V;try{if(K.findActiveSDKSession(X)){console.log(v("UserPromptSubmit",!0));return}let M=K.createSDKSession(X,z,Y),q=process.env.CLAUDE_PLUGIN_ROOT;if(!q)throw new Error("CLAUDE_PLUGIN_ROOT not set");let U=L.join(q,"scripts","hooks","worker.js");b("bun",[U,M.toString()],{detached:!0,stdio:"ignore"}).unref(),console.log(v("UserPromptSubmit",!0))}finally{K.close()}}var N=await Bun.stdin.text();try{let Q=N.trim()?JSON.parse(N):void 0;O(Q)}catch(Q){console.error(`[claude-mem new-hook error: ${Q.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
// @bun
|
||||
import w from"net";import{Database as H}from"bun:sqlite";import{join as $,dirname as T,basename as l}from"path";import{homedir as x}from"os";import{existsSync as y,mkdirSync as E}from"fs";var z=process.env.CLAUDE_MEM_DATA_DIR||$(x(),".claude-mem"),M=process.env.CLAUDE_CONFIG_DIR||$(x(),".claude"),I=$(z,"archives"),k=$(z,"logs"),_=$(z,"trash"),h=$(z,"backups"),D=$(z,"chroma"),d=$(z,"settings.json"),N=$(z,"claude-mem.db"),m=$(M,"settings.json"),u=$(M,"commands"),c=$(M,"CLAUDE.md");function L(Q){return $(z,`worker-${Q}.sock`)}function U(Q){E(Q,{recursive:!0})}class q{db;constructor(){U(z),this.db=new H(N,{create:!0,readwrite:!0}),this.db.run("PRAGMA journal_mode = WAL"),this.db.run("PRAGMA synchronous = NORMAL"),this.db.run("PRAGMA foreign_keys = ON")}getRecentSummaries(Q,Y=10){return this.db.query(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(Q,Y)}findActiveSDKSession(Q){return this.db.query(`
|
||||
SELECT id, sdk_session_id, project
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ? AND status = 'active'
|
||||
LIMIT 1
|
||||
`).get(Q)||null}createSDKSession(Q,Y,X){let Z=new Date,W=Z.getTime();return this.db.query(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(Q,Y,X,Z.toISOString(),W),this.db.query("SELECT last_insert_rowid() as id").get().id}updateSDKSessionId(Q,Y){this.db.query(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ?
|
||||
`).run(Y,Q)}storeObservation(Q,Y,X,Z){let W=new Date,B=W.getTime();this.db.query(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(Q,Y,Z,X,W.toISOString(),B)}storeSummary(Q,Y,X){let Z=new Date,W=Z.getTime();this.db.query(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, files_read, files_edited, notes, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(Q,Y,X.request||null,X.investigated||null,X.learned||null,X.completed||null,X.next_steps||null,X.files_read||null,X.files_edited||null,X.notes||null,Z.toISOString(),W)}markSessionCompleted(Q){let Y=new Date,X=Y.getTime();this.db.query(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(Y.toISOString(),X,Q)}markSessionFailed(Q){let Y=new Date,X=Y.getTime();this.db.query(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(Y.toISOString(),X,Q)}close(){this.db.close()}}function g(Q,Y,X){if(Q==="PreCompact"){if(Y)return{continue:!0,suppressOutput:!0};return{continue:!1,stopReason:X.reason||"Pre-compact operation failed",suppressOutput:!0}}if(Q==="SessionStart"){if(Y&&X.context)return{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:X.context}};return{continue:!0,suppressOutput:!0}}if(Q==="UserPromptSubmit"||Q==="PostToolUse")return{continue:!0,suppressOutput:!0};if(Q==="Stop")return{continue:!0,suppressOutput:!0};return{continue:Y,suppressOutput:!0,...X.reason&&!Y?{stopReason:X.reason}:{}}}function J(Q,Y,X={}){let Z=g(Q,Y,X);return JSON.stringify(Z)}var C=new Set(["TodoWrite","ListMcpResourcesTool"]);function f(Q){if(!Q)throw new Error("saveHook requires input");let{session_id:Y,tool_name:X,tool_input:Z,tool_output:W}=Q;if(C.has(X)){console.log(J("PostToolUse",!0));return}let B=new q,K=B.findActiveSDKSession(Y);if(B.close(),!K){console.log(J("PostToolUse",!0));return}let F=L(K.id),b={type:"observation",tool_name:X,tool_input:JSON.stringify(Z),tool_output:JSON.stringify(W)},V=w.connect(F,()=>{V.write(JSON.stringify(b)+`
|
||||
`),V.end()}),G=!1,v=()=>{if(G)return;G=!0,console.log(J("PostToolUse",!0))};V.on("close",v),V.on("error",v)}var O=await Bun.stdin.text();try{let Q=O.trim()?JSON.parse(O):void 0;f(Q)}catch(Q){console.error(`[claude-mem save-hook error: ${Q.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
// @bun
|
||||
import E from"net";import{Database as f}from"bun:sqlite";import{join as $,dirname as w,basename as P}from"path";import{homedir as F}from"os";import{existsSync as T,mkdirSync as U}from"fs";var z=process.env.CLAUDE_MEM_DATA_DIR||$(F(),".claude-mem"),v=process.env.CLAUDE_CONFIG_DIR||$(F(),".claude"),A=$(z,"archives"),j=$(z,"logs"),R=$(z,"trash"),_=$(z,"backups"),y=$(z,"chroma"),I=$(z,"settings.json"),G=$(z,"claude-mem.db"),k=$(v,"settings.json"),h=$(v,"commands"),D=$(v,"CLAUDE.md");function x(Q){return $(z,`worker-${Q}.sock`)}function L(Q){U(Q,{recursive:!0})}class J{db;constructor(){L(z),this.db=new f(G,{create:!0,readwrite:!0}),this.db.run("PRAGMA journal_mode = WAL"),this.db.run("PRAGMA synchronous = NORMAL"),this.db.run("PRAGMA foreign_keys = ON")}getRecentSummaries(Q,Y=10){return this.db.query(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(Q,Y)}findActiveSDKSession(Q){return this.db.query(`
|
||||
SELECT id, sdk_session_id, project
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ? AND status = 'active'
|
||||
LIMIT 1
|
||||
`).get(Q)||null}createSDKSession(Q,Y,X){let Z=new Date,K=Z.getTime();return this.db.query(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(Q,Y,X,Z.toISOString(),K),this.db.query("SELECT last_insert_rowid() as id").get().id}updateSDKSessionId(Q,Y){this.db.query(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ?
|
||||
`).run(Y,Q)}storeObservation(Q,Y,X,Z){let K=new Date,B=K.getTime();this.db.query(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(Q,Y,Z,X,K.toISOString(),B)}storeSummary(Q,Y,X){let Z=new Date,K=Z.getTime();this.db.query(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, files_read, files_edited, notes, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(Q,Y,X.request||null,X.investigated||null,X.learned||null,X.completed||null,X.next_steps||null,X.files_read||null,X.files_edited||null,X.notes||null,Z.toISOString(),K)}markSessionCompleted(Q){let Y=new Date,X=Y.getTime();this.db.query(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(Y.toISOString(),X,Q)}markSessionFailed(Q){let Y=new Date,X=Y.getTime();this.db.query(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(Y.toISOString(),X,Q)}close(){this.db.close()}}function b(Q,Y,X){if(Q==="PreCompact"){if(Y)return{continue:!0,suppressOutput:!0};return{continue:!1,stopReason:X.reason||"Pre-compact operation failed",suppressOutput:!0}}if(Q==="SessionStart"){if(Y&&X.context)return{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:X.context}};return{continue:!0,suppressOutput:!0}}if(Q==="UserPromptSubmit"||Q==="PostToolUse")return{continue:!0,suppressOutput:!0};if(Q==="Stop")return{continue:!0,suppressOutput:!0};return{continue:Y,suppressOutput:!0,...X.reason&&!Y?{stopReason:X.reason}:{}}}function M(Q,Y,X={}){let Z=b(Q,Y,X);return JSON.stringify(Z)}function N(Q){if(!Q)throw new Error("summaryHook requires input");let{session_id:Y}=Q,X=new J,Z=X.findActiveSDKSession(Y);if(X.close(),!Z){console.log(M("Stop",!0));return}let K=x(Z.id),B={type:"finalize"},W=E.connect(K,()=>{W.write(JSON.stringify(B)+`
|
||||
`),W.end()}),V=!1,q=()=>{if(V)return;V=!0,console.log(M("Stop",!0))};W.on("close",q),W.on("error",q)}var O=await Bun.stdin.text();try{let Q=O.trim()?JSON.parse(O):void 0;N(Q)}catch(Q){console.error(`[claude-mem summary-hook error: ${Q.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}
|
||||
+694
File diff suppressed because one or more lines are too long
+666
File diff suppressed because one or more lines are too long
-128
@@ -1,128 +0,0 @@
|
||||
# Build & Publish Guide
|
||||
|
||||
This repository is now the primary source for `claude-mem`. All builds and publishes happen from this repo.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Bun](https://bun.sh) - Fast JavaScript runtime and bundler
|
||||
- npm account with publish access to `claude-mem`
|
||||
|
||||
## Building
|
||||
|
||||
Build the project to create a bundled, minified executable:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
# or
|
||||
node scripts/build.js
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Bundle all TypeScript source files using Bun
|
||||
2. Minify the output
|
||||
3. Add shebang (`#!/usr/bin/env node`)
|
||||
4. Set executable permissions
|
||||
5. Output to `dist/claude-mem.min.js`
|
||||
|
||||
### Build Output
|
||||
|
||||
- **Entry point:** `src/bin/cli.ts`
|
||||
- **Output:** `dist/claude-mem.min.js` (~350KB minified)
|
||||
- **Target:** Node.js (via Bun's `--target=node`)
|
||||
- **Externals:** `@anthropic-ai/claude-agent-sdk` (not bundled)
|
||||
|
||||
## Publishing
|
||||
|
||||
To publish a new version to npm:
|
||||
|
||||
```bash
|
||||
npm run publish:npm
|
||||
# or
|
||||
node scripts/publish.js
|
||||
```
|
||||
|
||||
The publish script will:
|
||||
1. Check git status (warn if uncommitted changes)
|
||||
2. Show current version and prompt for version bump type:
|
||||
- `patch` - Bug fixes (1.0.X)
|
||||
- `minor` - New features (1.X.0)
|
||||
- `major` - Breaking changes (X.0.0)
|
||||
- `custom` - Enter version manually
|
||||
3. Update `package.json` with new version
|
||||
4. Run build script
|
||||
5. Run tests (if configured)
|
||||
6. Create git commit and tag (`v{version}`)
|
||||
7. Publish to npm
|
||||
8. Push commit and tags to GitHub
|
||||
|
||||
### Manual Publishing
|
||||
|
||||
If you prefer to do it manually:
|
||||
|
||||
```bash
|
||||
# 1. Update version in package.json
|
||||
# 2. Build
|
||||
npm run build
|
||||
|
||||
# 3. Commit and tag
|
||||
git add package.json dist/
|
||||
git commit -m "Release v3.9.17"
|
||||
git tag v3.9.17
|
||||
|
||||
# 4. Publish
|
||||
npm publish
|
||||
|
||||
# 5. Push
|
||||
git push && git push --tags
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Run the CLI directly from source without building:
|
||||
|
||||
```bash
|
||||
npm run dev -- [command] [options]
|
||||
# or
|
||||
bun run src/bin/cli.ts [command] [options]
|
||||
```
|
||||
|
||||
Example:
|
||||
```bash
|
||||
npm run dev -- status
|
||||
npm run dev -- --version
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
claude-mem/
|
||||
├── src/ # TypeScript source
|
||||
│ ├── bin/cli.ts # CLI entry point
|
||||
│ ├── commands/ # Command implementations
|
||||
│ ├── hooks/ # Hook implementations
|
||||
│ ├── sdk/ # Agent SDK worker
|
||||
│ ├── services/ # SQLite and path services
|
||||
│ ├── shared/ # Configuration and types
|
||||
│ └── utils/ # Platform utilities
|
||||
├── dist/ # Build output
|
||||
│ └── claude-mem.min.js # Bundled executable
|
||||
├── tests/ # Test files
|
||||
│ ├── database-schema.test.ts
|
||||
│ ├── sdk-prompts-parser.test.ts
|
||||
│ ├── hooks-database-integration.test.ts
|
||||
│ └── session-lifecycle.test.ts
|
||||
├── docs/ # Documentation
|
||||
│ ├── BUILD.md # This file
|
||||
│ └── CHANGELOG.md # Release notes
|
||||
├── scripts/ # Build automation
|
||||
│ ├── build.js # Build script
|
||||
│ └── publish.js # Publish script
|
||||
└── package.json # Package configuration
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The build process embeds the version from `package.json` at build time
|
||||
- `prepublishOnly` script ensures build runs before npm publish
|
||||
- Dependencies are bundled except for external packages
|
||||
- The published package includes: `dist/`, `commands/`, `src/`, `docs/`
|
||||
@@ -1,119 +0,0 @@
|
||||
# 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/).
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
-1502
File diff suppressed because it is too large
Load Diff
@@ -1,308 +0,0 @@
|
||||
# Plugin Development Guide
|
||||
|
||||
This guide helps developers work with the claude-mem plugin structure during development.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Build the Plugin
|
||||
|
||||
```bash
|
||||
# Build both CLI and hooks
|
||||
npm run build
|
||||
|
||||
# Or build separately
|
||||
npm run build:cli # Just the CLI
|
||||
npm run build:hooks # Just the hooks
|
||||
```
|
||||
|
||||
### 2. Test Hooks Locally
|
||||
|
||||
Test individual hooks by piping JSON input:
|
||||
|
||||
```bash
|
||||
# Test context hook (SessionStart)
|
||||
printf '{"session_id":"test-123","cwd":"/Users/you/project","source":"startup"}' | \
|
||||
bun scripts/hooks/context-hook.js
|
||||
|
||||
# Test new hook (UserPromptSubmit)
|
||||
printf '{"session_id":"test-123","cwd":"/Users/you/project","prompt":"help me code"}' | \
|
||||
bun scripts/hooks/new-hook.js
|
||||
|
||||
# Test save hook (PostToolUse)
|
||||
printf '{"session_id":"test-123","cwd":"/Users/you/project","tool_name":"Read","tool_input":{},"tool_output":{}}' | \
|
||||
bun scripts/hooks/save-hook.js
|
||||
|
||||
# Test summary hook (Stop)
|
||||
printf '{"session_id":"test-123","cwd":"/Users/you/project"}' | \
|
||||
bun scripts/hooks/summary-hook.js
|
||||
|
||||
# Test worker (requires valid session ID in database)
|
||||
bun scripts/hooks/worker.js 999
|
||||
```
|
||||
|
||||
### 3. Test Worker with Plugin Root
|
||||
|
||||
Verify the new-hook correctly detects plugin mode:
|
||||
|
||||
```bash
|
||||
# Without CLAUDE_PLUGIN_ROOT (traditional mode)
|
||||
printf '{"session_id":"test-new","cwd":"/path","prompt":"test"}' | \
|
||||
bun scripts/hooks/new-hook.js
|
||||
|
||||
# With CLAUDE_PLUGIN_ROOT (plugin mode)
|
||||
CLAUDE_PLUGIN_ROOT=$(pwd) printf '{"session_id":"test-plugin","cwd":"/path","prompt":"test"}' | \
|
||||
bun scripts/hooks/new-hook.js
|
||||
```
|
||||
|
||||
In plugin mode, the new-hook will attempt to spawn `bun ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/worker.js`.
|
||||
In traditional mode, it will attempt to spawn `claude-mem worker`.
|
||||
|
||||
### 4. Test With No Input
|
||||
|
||||
Each hook should handle missing input gracefully:
|
||||
|
||||
```bash
|
||||
echo '' | bun scripts/hooks/context-hook.js
|
||||
# Output: No input provided - this script is designed to run as a Claude Code SessionStart hook
|
||||
```
|
||||
|
||||
## Local Plugin Testing
|
||||
|
||||
### Option 1: Dev Marketplace (Recommended)
|
||||
|
||||
Create a development marketplace to test your plugin:
|
||||
|
||||
```bash
|
||||
# Create marketplace structure
|
||||
mkdir -p ~/dev-marketplace/.claude-plugin
|
||||
|
||||
# Create marketplace manifest
|
||||
cat > ~/dev-marketplace/.claude-plugin/marketplace.json << 'EOF'
|
||||
{
|
||||
"name": "dev-marketplace",
|
||||
"owner": {
|
||||
"name": "Developer"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"source": "./claude-mem-plugin",
|
||||
"description": "Persistent memory system for Claude Code"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
# Symlink your working directory
|
||||
ln -s /path/to/your/claude-mem ~/dev-marketplace/claude-mem-plugin
|
||||
```
|
||||
|
||||
Then in Claude Code:
|
||||
|
||||
```
|
||||
/plugin marketplace add /absolute/path/to/dev-marketplace
|
||||
/plugin install claude-mem@dev-marketplace
|
||||
```
|
||||
|
||||
### Option 2: Direct Testing
|
||||
|
||||
Test the CLI commands directly:
|
||||
|
||||
```bash
|
||||
# Build first
|
||||
npm run build
|
||||
|
||||
# Test CLI commands
|
||||
./dist/claude-mem.min.js --version
|
||||
./dist/claude-mem.min.js status
|
||||
./dist/claude-mem.min.js --help
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Making Changes to Hooks
|
||||
|
||||
1. **Edit TypeScript source** in `src/hooks/` or `src/bin/hooks/`
|
||||
2. **Rebuild hooks**: `npm run build:hooks`
|
||||
3. **Test locally**: Use echo piping method above
|
||||
4. **Reinstall plugin** (if testing in Claude Code):
|
||||
```
|
||||
/plugin uninstall claude-mem@dev-marketplace
|
||||
/plugin install claude-mem@dev-marketplace
|
||||
```
|
||||
|
||||
### Making Changes to CLI
|
||||
|
||||
1. **Edit TypeScript source** in `src/`
|
||||
2. **Rebuild CLI**: `npm run build:cli`
|
||||
3. **Test directly**: `./dist/claude-mem.min.js [command]`
|
||||
|
||||
### Making Changes to Commands
|
||||
|
||||
1. **Edit markdown files** in `commands/`
|
||||
2. **No rebuild needed** (commands are read directly)
|
||||
3. **Reinstall plugin** to pick up changes:
|
||||
```
|
||||
/plugin uninstall claude-mem@dev-marketplace
|
||||
/plugin install claude-mem@dev-marketplace
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Enable Verbose Logging
|
||||
|
||||
Set environment variables for more detailed output:
|
||||
|
||||
```bash
|
||||
DEBUG=claude-mem:* bun scripts/hooks/context-hook.js
|
||||
```
|
||||
|
||||
### Check Hook Output
|
||||
|
||||
Hooks write to stdout/stderr. Capture output:
|
||||
|
||||
```bash
|
||||
echo '{"session_id":"test","cwd":"/path"}' | \
|
||||
bun scripts/hooks/context-hook.js 2>&1 | tee hook-output.log
|
||||
```
|
||||
|
||||
### Verify Plugin Root Variable
|
||||
|
||||
Test that `${CLAUDE_PLUGIN_ROOT}` resolves correctly:
|
||||
|
||||
```bash
|
||||
# Manually set it for testing
|
||||
export CLAUDE_PLUGIN_ROOT=/path/to/your/plugin
|
||||
echo '{"session_id":"test","cwd":"/path"}' | \
|
||||
bun ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/context-hook.js
|
||||
```
|
||||
|
||||
## Build System Details
|
||||
|
||||
### Hook Build Process
|
||||
|
||||
The `scripts/build-hooks.js` script:
|
||||
1. Reads each entry point from `src/bin/hooks/`
|
||||
2. Bundles with Bun build system
|
||||
3. Minifies output
|
||||
4. Adds shebang for direct execution
|
||||
5. Sets executable permissions
|
||||
6. Outputs to `scripts/hooks/`
|
||||
|
||||
### CLI Build Process
|
||||
|
||||
The `scripts/build.js` script:
|
||||
1. Bundles main CLI from `src/bin/cli.ts`
|
||||
2. Externalizes large dependencies
|
||||
3. Minifies output
|
||||
4. Adds shebang
|
||||
5. Sets executable permissions
|
||||
6. Outputs to `dist/claude-mem.min.js`
|
||||
|
||||
### Build Configuration
|
||||
|
||||
Both builds use similar Bun configuration:
|
||||
- **Target**: `bun` runtime
|
||||
- **Minify**: `true`
|
||||
- **External**: `bun:sqlite` (native module)
|
||||
- **Define**: `__DEFAULT_PACKAGE_VERSION__` from package.json
|
||||
|
||||
## Testing
|
||||
|
||||
### Run Tests
|
||||
|
||||
```bash
|
||||
bun test tests/
|
||||
```
|
||||
|
||||
### Test Database Operations
|
||||
|
||||
```bash
|
||||
# Test hooks database
|
||||
bun test tests/hooks-database-integration.test.ts
|
||||
|
||||
# Test session lifecycle
|
||||
bun test tests/session-lifecycle.test.ts
|
||||
```
|
||||
|
||||
## Publishing
|
||||
|
||||
### Pre-publish Checklist
|
||||
|
||||
- [ ] All tests pass: `bun test tests/`
|
||||
- [ ] Build succeeds: `npm run build`
|
||||
- [ ] Version updated in `package.json`
|
||||
- [ ] Changelog updated in `docs/CHANGELOG.md`
|
||||
- [ ] Plugin.json version matches package.json
|
||||
- [ ] Hooks tested locally
|
||||
- [ ] CLI tested locally
|
||||
|
||||
### Publish to npm
|
||||
|
||||
```bash
|
||||
npm run publish:npm
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Run `prepublishOnly` script (builds everything)
|
||||
2. Publish to npm registry
|
||||
3. Include files listed in `package.json` "files" array
|
||||
|
||||
### Files Included in Package
|
||||
|
||||
The npm package includes:
|
||||
- `dist/` - Compiled CLI
|
||||
- `scripts/` - Compiled hooks
|
||||
- `commands/` - Slash command definitions
|
||||
- `hooks/` - Hook configuration
|
||||
- `.claude-plugin/` - Plugin metadata
|
||||
- `src/` - TypeScript source (for reference)
|
||||
- `docs/` - Documentation
|
||||
- `.mcp.json` - MCP server configuration
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Fails
|
||||
|
||||
**Problem**: `bun: command not found`
|
||||
**Solution**: Install Bun from https://bun.sh
|
||||
|
||||
**Problem**: Build errors with external dependencies
|
||||
**Solution**: Check that `bun:sqlite` is not bundled (should be external)
|
||||
|
||||
### Hooks Don't Execute
|
||||
|
||||
**Problem**: `Permission denied` when executing hooks
|
||||
**Solution**: Ensure scripts are executable: `chmod +x scripts/hooks/*.js`
|
||||
|
||||
**Problem**: Hooks exit silently
|
||||
**Solution**: Check error handling - hooks catch all errors and exit gracefully
|
||||
|
||||
### Plugin Not Found
|
||||
|
||||
**Problem**: `/plugin install` can't find claude-mem
|
||||
**Solution**:
|
||||
1. Verify marketplace is added: `/plugin marketplace list`
|
||||
2. Check marketplace manifest includes claude-mem
|
||||
3. Refresh marketplace: `/plugin marketplace refresh`
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Use symlinks** in dev marketplace for faster iteration
|
||||
2. **Test hooks with edge cases** (empty input, malformed JSON)
|
||||
3. **Check file sizes** after build to catch bloat
|
||||
4. **Version everything together** (CLI, hooks, plugin.json)
|
||||
5. **Document breaking changes** in CHANGELOG.md
|
||||
|
||||
## Resources
|
||||
|
||||
- [Plugin Structure Documentation](./PLUGIN_STRUCTURE.md)
|
||||
- [Plugin Installation Guide](./PLUGIN_INSTALLATION.md)
|
||||
- [Build Documentation](./BUILD.md)
|
||||
- [Claude Code Plugins Docs](https://docs.claude.com/en/docs/claude-code/plugins)
|
||||
- [Bun Documentation](https://bun.sh/docs)
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Issues**: https://github.com/thedotmack/claude-mem/issues
|
||||
- **Discussions**: https://github.com/thedotmack/claude-mem/discussions
|
||||
@@ -1,273 +0,0 @@
|
||||
# Plugin Installation Guide
|
||||
|
||||
Claude-mem can be installed as a Claude Code plugin, which provides a streamlined installation experience.
|
||||
|
||||
## Plugin Structure
|
||||
|
||||
The claude-mem plugin includes:
|
||||
- **Hooks**: Automatic memory capture via SessionStart, UserPromptSubmit, PostToolUse, and Stop hooks
|
||||
- **Commands**: `/claude-mem`, `/save`, and `/remember` slash commands
|
||||
- **MCP Integration**: Chroma vector database for semantic memory search
|
||||
|
||||
## Installation Methods
|
||||
|
||||
### Option 1: From Git Repository (Recommended)
|
||||
|
||||
If the plugin is published to GitHub:
|
||||
|
||||
1. **Add the marketplace from GitHub**:
|
||||
```
|
||||
/plugin marketplace add thedotmack/claude-mem-marketplace
|
||||
```
|
||||
|
||||
2. **Install the plugin**:
|
||||
```
|
||||
/plugin install claude-mem@marketplace-name
|
||||
```
|
||||
|
||||
The plugin will automatically:
|
||||
- Configure all hooks in your Claude settings
|
||||
- Set up slash commands
|
||||
- Register plugin components
|
||||
|
||||
**Note**: You still need to install the CLI globally for hooks to work:
|
||||
```bash
|
||||
npm install -g claude-mem
|
||||
```
|
||||
|
||||
### Option 2: Local Development Installation
|
||||
|
||||
If you're testing locally during development:
|
||||
|
||||
1. **Create a development marketplace structure**:
|
||||
```bash
|
||||
mkdir dev-marketplace
|
||||
cd dev-marketplace
|
||||
mkdir .claude-plugin
|
||||
```
|
||||
|
||||
2. **Create marketplace manifest** (`.claude-plugin/marketplace.json`):
|
||||
```json
|
||||
{
|
||||
"name": "dev-marketplace",
|
||||
"owner": {
|
||||
"name": "Developer"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"source": "./claude-mem-plugin",
|
||||
"description": "Persistent memory system for Claude Code"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
3. **Create or symlink your plugin directory**:
|
||||
```bash
|
||||
# Symlink to your working directory (recommended)
|
||||
ln -s /path/to/claude-mem ./claude-mem-plugin
|
||||
|
||||
# Or copy the directory
|
||||
cp -r /path/to/claude-mem ./claude-mem-plugin
|
||||
```
|
||||
|
||||
4. **Start Claude Code and add your development marketplace**:
|
||||
```
|
||||
/plugin marketplace add /absolute/path/to/dev-marketplace
|
||||
```
|
||||
|
||||
5. **Install your plugin**:
|
||||
```
|
||||
/plugin install claude-mem@dev-marketplace
|
||||
```
|
||||
|
||||
### Option 3: Interactive Installation
|
||||
|
||||
You can also use the interactive plugin interface:
|
||||
|
||||
1. **Open plugin management**:
|
||||
```
|
||||
/plugin
|
||||
```
|
||||
|
||||
2. **Select "Browse Plugins"** to see available plugins with descriptions and installation options
|
||||
|
||||
3. **Follow the prompts** to install and configure
|
||||
|
||||
### Option 4: Traditional CLI Installation
|
||||
|
||||
You can still use the traditional CLI-based installation:
|
||||
|
||||
```bash
|
||||
npm install -g claude-mem
|
||||
claude-mem install
|
||||
```
|
||||
|
||||
This method uses an interactive wizard to guide you through setup and works independently of the plugin system.
|
||||
|
||||
## Verify Installation
|
||||
|
||||
After installing the plugin, verify it's working correctly:
|
||||
|
||||
1. **Check available commands**:
|
||||
```
|
||||
/help
|
||||
```
|
||||
You should see `/claude-mem`, `/save`, and `/remember` commands listed.
|
||||
|
||||
2. **Test plugin features**:
|
||||
```
|
||||
/claude-mem help
|
||||
```
|
||||
This should show all memory system commands and features.
|
||||
|
||||
3. **Review plugin details**:
|
||||
```
|
||||
/plugin
|
||||
```
|
||||
Select "Manage Plugins" to see what the plugin provides and its current status.
|
||||
|
||||
4. **Check hooks are registered**:
|
||||
View your `~/.claude/settings.json` to confirm hooks are configured.
|
||||
|
||||
## Managing the Plugin
|
||||
|
||||
### Check Plugin Status
|
||||
```
|
||||
/plugin list
|
||||
```
|
||||
|
||||
### Disable Plugin (temporarily)
|
||||
```
|
||||
/plugin disable claude-mem
|
||||
```
|
||||
|
||||
### Enable Plugin
|
||||
```
|
||||
/plugin enable claude-mem
|
||||
```
|
||||
|
||||
### Uninstall Plugin
|
||||
```
|
||||
/plugin uninstall claude-mem
|
||||
```
|
||||
|
||||
## Development Iteration
|
||||
|
||||
When making changes to your plugin during development:
|
||||
|
||||
1. **Uninstall current version**:
|
||||
```
|
||||
/plugin uninstall claude-mem@dev-marketplace
|
||||
```
|
||||
|
||||
2. **Make your changes** to the plugin code
|
||||
|
||||
3. **Reinstall to test changes**:
|
||||
```
|
||||
/plugin install claude-mem@dev-marketplace
|
||||
```
|
||||
|
||||
4. **Restart Claude Code** if hooks or commands don't update immediately
|
||||
|
||||
**Tip**: When using a symlink in your dev marketplace, changes to commands may be reflected without reinstalling, but hook changes typically require a reinstall.
|
||||
|
||||
## Plugin Components
|
||||
|
||||
### Hooks Configuration
|
||||
|
||||
The plugin registers these hooks automatically (defined in `hooks/hooks.json`):
|
||||
|
||||
- **SessionStart**: Loads recent session context when Claude Code starts
|
||||
- **UserPromptSubmit**: Initializes memory session and background worker
|
||||
- **PostToolUse**: Captures tool observations for memory system
|
||||
- **Stop**: Finalizes and saves session summary
|
||||
|
||||
### Commands
|
||||
|
||||
The plugin includes these slash commands (in `commands/` directory):
|
||||
|
||||
- `/claude-mem help` - Show all memory commands and features
|
||||
- `/save [message]` - Quick save of current conversation overview
|
||||
- `/remember [context]` - Search your saved memories
|
||||
|
||||
### MCP Server
|
||||
|
||||
The plugin automatically installs and configures the Chroma MCP server for vector-based memory storage.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Not Found
|
||||
If the plugin isn't found, ensure:
|
||||
1. The repository contains `.claude-plugin/plugin.json`
|
||||
2. The plugin is in the correct directory
|
||||
3. You've refreshed the plugin marketplace: `/plugin marketplace refresh`
|
||||
|
||||
### Hooks Not Running
|
||||
Check that:
|
||||
1. The plugin is enabled: `/plugin list`
|
||||
2. The `claude-mem` CLI is installed: `which claude-mem`
|
||||
3. Check Claude logs for hook errors
|
||||
|
||||
### CLI Not Found
|
||||
If hooks fail with "command not found", install the CLI:
|
||||
```bash
|
||||
npm install -g claude-mem
|
||||
```
|
||||
|
||||
The plugin hooks call the `claude-mem` CLI commands, which must be globally available.
|
||||
|
||||
## Creating Your Own Marketplace
|
||||
|
||||
To distribute claude-mem via a custom marketplace:
|
||||
|
||||
1. **Create a marketplace repository** with this structure:
|
||||
```
|
||||
my-marketplace/
|
||||
├── .claude-plugin/
|
||||
│ └── marketplace.json
|
||||
└── plugins/
|
||||
└── claude-mem/ (symlink or submodule to claude-mem)
|
||||
```
|
||||
|
||||
2. **Define `.claude-plugin/marketplace.json`**:
|
||||
```json
|
||||
{
|
||||
"name": "thedotmack",
|
||||
"owner": {
|
||||
"name": "Alex Newman"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"source": "./plugins/claude-mem",
|
||||
"description": "Persistent memory system for Claude Code"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
3. **Publish to GitHub** and users can add it with:
|
||||
```
|
||||
/plugin marketplace add username/repo-name
|
||||
```
|
||||
|
||||
4. **Users install the plugin**:
|
||||
```
|
||||
/plugin install claude-mem@marketplace-name
|
||||
```
|
||||
|
||||
## Benefits of Plugin Installation
|
||||
|
||||
✅ **One-command install**: No complex setup scripts
|
||||
✅ **Easy management**: Enable/disable without modifying settings
|
||||
✅ **Version control**: Update plugins with `/plugin update`
|
||||
✅ **Team sharing**: Distribute via marketplace
|
||||
✅ **Standard format**: Follows Claude Code best practices
|
||||
|
||||
## More Information
|
||||
|
||||
- [Claude Code Plugins Documentation](https://docs.claude.com/en/docs/claude-code/plugins)
|
||||
- [Plugin Marketplaces Guide](https://docs.claude.com/en/docs/claude-code/plugin-marketplaces)
|
||||
- [Claude-mem GitHub Repository](https://github.com/thedotmack/claude-mem)
|
||||
@@ -1,242 +0,0 @@
|
||||
# Claude-mem Plugin Structure
|
||||
|
||||
This document describes the complete plugin structure for claude-mem, which enables self-contained installation via Claude Code's plugin system.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
claude-mem/
|
||||
├── .claude-plugin/
|
||||
│ └── plugin.json # Plugin metadata
|
||||
├── commands/
|
||||
│ ├── claude-mem.md # /claude-mem slash command
|
||||
│ ├── remember.md # /remember slash command
|
||||
│ └── save.md # /save slash command
|
||||
├── hooks/
|
||||
│ └── hooks.json # Hook definitions using ${CLAUDE_PLUGIN_ROOT}
|
||||
├── scripts/
|
||||
│ ├── build-hooks.js # Build script for compiling hooks
|
||||
│ └── hooks/ # Compiled hook executables
|
||||
│ ├── context-hook.js # SessionStart hook (4KB)
|
||||
│ ├── new-hook.js # UserPromptSubmit hook (4KB)
|
||||
│ ├── save-hook.js # PostToolUse hook (4KB)
|
||||
│ ├── summary-hook.js # Stop hook (4KB)
|
||||
│ └── worker.js # Background SDK worker (232KB)
|
||||
├── src/
|
||||
│ ├── bin/
|
||||
│ │ ├── cli.ts # Main CLI entry point
|
||||
│ │ └── hooks/ # Hook entry point sources
|
||||
│ │ ├── context-hook.ts # SessionStart entry point
|
||||
│ │ ├── new-hook.ts # UserPromptSubmit entry point
|
||||
│ │ ├── save-hook.ts # PostToolUse entry point
|
||||
│ │ └── summary-hook.ts # Stop entry point
|
||||
│ ├── hooks/ # Hook implementation functions
|
||||
│ │ ├── context.ts
|
||||
│ │ ├── new.ts
|
||||
│ │ ├── save.ts
|
||||
│ │ └── summary.ts
|
||||
│ └── ... # Other source files
|
||||
└── dist/
|
||||
└── claude-mem.min.js # Bundled CLI executable
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. Plugin Installation
|
||||
|
||||
When users install the plugin via `/plugin install claude-mem`, Claude Code:
|
||||
1. Downloads the plugin from the marketplace
|
||||
2. Installs it to the local plugins directory
|
||||
3. Registers the hooks from `hooks/hooks.json`
|
||||
4. Makes slash commands from `commands/` directory available
|
||||
|
||||
### 2. Self-Contained Execution
|
||||
|
||||
The hooks are compiled as standalone executables that:
|
||||
- **Don't require global CLI installation**: All dependencies are bundled
|
||||
- **Use plugin-relative paths**: `${CLAUDE_PLUGIN_ROOT}` resolves to plugin directory
|
||||
- **Work with Bun runtime**: Scripts are compiled for Bun and include shebang
|
||||
|
||||
### 3. Hook Configuration
|
||||
|
||||
The `hooks/hooks.json` file uses `${CLAUDE_PLUGIN_ROOT}` to reference bundled scripts:
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Claude-mem memory system hooks",
|
||||
"hooks": {
|
||||
"SessionStart": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "bun ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/context-hook.js",
|
||||
"timeout": 180000
|
||||
}]
|
||||
}],
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Hook Entry Points
|
||||
|
||||
Each hook has a standalone entry point in `src/bin/hooks/` that:
|
||||
- Reads JSON input from stdin
|
||||
- Calls the hook implementation function
|
||||
- Handles errors gracefully
|
||||
- Exits with appropriate status codes
|
||||
|
||||
Example from `context-hook.ts`:
|
||||
```typescript
|
||||
#!/usr/bin/env bun
|
||||
import { contextHook } from '../../hooks/context.js';
|
||||
|
||||
const input = await Bun.stdin.text();
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
contextHook(parsed);
|
||||
```
|
||||
|
||||
### 5. Build Process
|
||||
|
||||
The build system compiles both the CLI and hook scripts:
|
||||
|
||||
```bash
|
||||
npm run build # Build both CLI and hooks
|
||||
npm run build:cli # Build only the CLI
|
||||
npm run build:hooks # Build only the hooks
|
||||
```
|
||||
|
||||
The hook build process:
|
||||
1. Compiles each hook entry point with Bun
|
||||
2. Bundles all dependencies (except bun:sqlite)
|
||||
3. Minifies the output
|
||||
4. Adds shebang (`#!/usr/bin/env bun`)
|
||||
5. Makes executable (`chmod +x`)
|
||||
6. Outputs to `scripts/hooks/`
|
||||
|
||||
## Benefits
|
||||
|
||||
### ✅ Self-Contained
|
||||
- No global CLI installation required
|
||||
- All dependencies bundled with plugin
|
||||
- Plugin directory has everything needed
|
||||
|
||||
### ✅ Easy Installation
|
||||
- Single command: `/plugin install claude-mem`
|
||||
- Hooks automatically registered
|
||||
- Slash commands immediately available
|
||||
|
||||
### ✅ Version Control
|
||||
- Plugin version tied to specific hook versions
|
||||
- No version mismatch between CLI and hooks
|
||||
- Easy updates via `/plugin update`
|
||||
|
||||
### ✅ Development Friendly
|
||||
- Source code in TypeScript
|
||||
- Compiled to optimized JavaScript
|
||||
- Fast execution with Bun runtime
|
||||
|
||||
## Usage
|
||||
|
||||
### For Users
|
||||
|
||||
Install the plugin:
|
||||
```
|
||||
/plugin install claude-mem@marketplace-name
|
||||
```
|
||||
|
||||
The plugin provides:
|
||||
- **Hooks**: Automatic memory capture on SessionStart, UserPromptSubmit, PostToolUse, Stop
|
||||
- **Commands**: `/claude-mem`, `/save`, `/remember` slash commands
|
||||
- **MCP Integration**: Chroma vector database access via MCP tools
|
||||
|
||||
### For Developers
|
||||
|
||||
Build the plugin:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Test hooks locally:
|
||||
```bash
|
||||
echo '{"session_id":"test","cwd":"/path"}' | bun scripts/hooks/context-hook.js
|
||||
```
|
||||
|
||||
Publish to marketplace:
|
||||
```bash
|
||||
npm run publish:npm
|
||||
```
|
||||
|
||||
## Worker Process Handling
|
||||
|
||||
### Background Worker
|
||||
|
||||
The `worker.js` is a special bundled script that runs as a long-lived background process. It:
|
||||
- Runs an SDK agent session in the background
|
||||
- Listens on a Unix socket for messages from hooks
|
||||
- Processes tool observations and generates summaries
|
||||
- Stores results in the database
|
||||
|
||||
### Spawning the Worker
|
||||
|
||||
The `new-hook` (UserPromptSubmit) is responsible for spawning the worker. It uses intelligent fallback:
|
||||
|
||||
```typescript
|
||||
// Plugin mode: Use bundled worker with CLAUDE_PLUGIN_ROOT
|
||||
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
||||
const workerPath = path.join(pluginRoot, 'scripts', 'hooks', 'worker.js');
|
||||
spawn('bun', [workerPath, sessionId.toString()], { detached: true });
|
||||
}
|
||||
// Traditional mode: Use global CLI
|
||||
else {
|
||||
spawn('claude-mem', ['worker', sessionId.toString()], { detached: true });
|
||||
}
|
||||
```
|
||||
|
||||
### Why This Approach?
|
||||
|
||||
1. **Self-contained plugin**: When installed as a plugin, uses bundled worker
|
||||
2. **Backwards compatible**: When installed traditionally, uses global CLI
|
||||
3. **No user intervention**: Automatically detects mode via environment variable
|
||||
|
||||
## File Sizes
|
||||
|
||||
Compiled hook scripts are optimized and small:
|
||||
- `context-hook.js`: ~4.1 KB
|
||||
- `new-hook.js`: ~4.0 KB
|
||||
- `save-hook.js`: ~4.2 KB
|
||||
- `summary-hook.js`: ~3.9 KB
|
||||
- `worker.js`: ~232 KB (includes SDK dependencies)
|
||||
|
||||
Total package overhead: ~248 KB for all hook scripts combined.
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Runtime
|
||||
- **Bun**: Required for executing hook scripts
|
||||
- **bun:sqlite**: Native SQLite module (not bundled)
|
||||
|
||||
### Build-time
|
||||
- **Bun**: Used as bundler for compilation
|
||||
- **Node.js**: Required for build scripts
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
The plugin structure maintains backwards compatibility:
|
||||
- CLI commands still work: `claude-mem context`, etc.
|
||||
- Traditional installation still supported: `npm install -g claude-mem`
|
||||
- Users can choose plugin OR CLI installation
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
- [ ] Add more hooks (e.g., PreToolUse, Error)
|
||||
- [ ] Support Node.js runtime in addition to Bun
|
||||
- [ ] Add hook configuration UI
|
||||
- [ ] Implement hook hot-reloading during development
|
||||
- [ ] Create plugin marketplace for distribution
|
||||
|
||||
## See Also
|
||||
|
||||
- [Plugin Installation Guide](./PLUGIN_INSTALLATION.md) - User-facing installation instructions
|
||||
- [Build Documentation](./BUILD.md) - Build system details
|
||||
- [Claude Code Plugins Docs](https://docs.claude.com/en/docs/claude-code/plugins) - Official plugin documentation
|
||||
@@ -1,272 +0,0 @@
|
||||
# Worker Process Architecture
|
||||
|
||||
This document explains how the SDK worker process is handled in both plugin and traditional installation modes.
|
||||
|
||||
## Overview
|
||||
|
||||
The worker is a critical background process that:
|
||||
- Runs the Claude Agent SDK in a long-lived session
|
||||
- Listens on a Unix socket for messages from hooks
|
||||
- Processes tool observations in real-time
|
||||
- Generates session summaries
|
||||
- Stores observations and summaries in the database
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Claude Code │
|
||||
│ (Main UI) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
├─ SessionStart Hook ──> context-hook.js
|
||||
│
|
||||
├─ UserPromptSubmit ───> new-hook.js ──┐
|
||||
│ │
|
||||
│ ├──> Spawns Worker
|
||||
│ │ (Background Process)
|
||||
│ │
|
||||
├─ PostToolUse Hook ───> save-hook.js ─┼──> Unix Socket
|
||||
│ │
|
||||
│ │ ┌──────────────┐
|
||||
│ └───>│ worker.js │
|
||||
│ │ │
|
||||
│ │ - SDK Agent │
|
||||
└─ Stop Hook ───────────> summary-hook.js ──┼─>- Socket Srv│
|
||||
│ - Database │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
## Worker Lifecycle
|
||||
|
||||
### 1. Session Initialization (UserPromptSubmit)
|
||||
|
||||
When a user submits a prompt, `new-hook.js` is triggered:
|
||||
|
||||
```typescript
|
||||
// 1. Check if session already exists
|
||||
const existing = db.findActiveSDKSession(session_id);
|
||||
if (existing) { /* already running */ }
|
||||
|
||||
// 2. Create new SDK session record
|
||||
const sessionId = db.createSDKSession(session_id, project, prompt);
|
||||
|
||||
// 3. Spawn worker process
|
||||
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
|
||||
if (pluginRoot) {
|
||||
// Plugin mode: use bundled worker
|
||||
spawn('bun', [`${pluginRoot}/scripts/hooks/worker.js`, sessionId]);
|
||||
} else {
|
||||
// Traditional mode: use global CLI
|
||||
spawn('claude-mem', ['worker', sessionId]);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Worker Startup
|
||||
|
||||
The worker process starts and:
|
||||
1. Loads session info from database
|
||||
2. Creates Unix socket at `~/.claude-mem/sockets/session-{id}.sock`
|
||||
3. Starts listening for messages
|
||||
4. Initializes SDK agent with streaming input
|
||||
|
||||
### 3. Tool Observation Flow (PostToolUse)
|
||||
|
||||
Every time Claude uses a tool, `save-hook.js` is triggered:
|
||||
|
||||
```typescript
|
||||
// 1. Find active SDK session
|
||||
const session = db.findActiveSDKSession(session_id);
|
||||
|
||||
// 2. Connect to worker's Unix socket
|
||||
const socketPath = getWorkerSocketPath(session.id);
|
||||
const client = net.connect(socketPath);
|
||||
|
||||
// 3. Send observation message
|
||||
client.write(JSON.stringify({
|
||||
type: 'observation',
|
||||
tool_name: 'Read',
|
||||
tool_input: '{"file_path": "/path/to/file"}',
|
||||
tool_output: '{"content": "..."}'
|
||||
}));
|
||||
```
|
||||
|
||||
The worker receives the message and:
|
||||
1. Queues it in `pendingMessages`
|
||||
2. Yields it to SDK agent via message generator
|
||||
3. Receives agent's analysis
|
||||
4. Parses and stores observations in database
|
||||
|
||||
### 4. Session Finalization (Stop)
|
||||
|
||||
When the session ends, `summary-hook.js` is triggered:
|
||||
|
||||
```typescript
|
||||
// 1. Find active session
|
||||
const session = db.findActiveSDKSession(session_id);
|
||||
|
||||
// 2. Send finalize message to worker
|
||||
const client = net.connect(socketPath);
|
||||
client.write(JSON.stringify({
|
||||
type: 'finalize'
|
||||
}));
|
||||
```
|
||||
|
||||
The worker:
|
||||
1. Stops accepting new observations
|
||||
2. Sends finalize prompt to SDK agent
|
||||
3. Receives and parses session summary
|
||||
4. Stores summary in database
|
||||
5. Cleans up socket and exits
|
||||
|
||||
## Plugin vs Traditional Mode
|
||||
|
||||
### Plugin Mode (Self-Contained)
|
||||
|
||||
When installed as a plugin:
|
||||
- `CLAUDE_PLUGIN_ROOT` environment variable is set by Claude Code
|
||||
- Hooks use bundled scripts: `${CLAUDE_PLUGIN_ROOT}/scripts/hooks/`
|
||||
- Worker is spawned as: `bun ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/worker.js`
|
||||
- **No global CLI installation required**
|
||||
|
||||
```bash
|
||||
# Plugin mode execution
|
||||
/plugin install claude-mem
|
||||
# Hooks automatically use bundled worker
|
||||
```
|
||||
|
||||
### Traditional Mode (Global CLI)
|
||||
|
||||
When installed via npm:
|
||||
- `CLAUDE_PLUGIN_ROOT` is not set
|
||||
- Hooks installed via `claude-mem install`
|
||||
- Worker is spawned as: `claude-mem worker`
|
||||
- **Requires global CLI installation**
|
||||
|
||||
```bash
|
||||
# Traditional mode installation
|
||||
npm install -g claude-mem
|
||||
claude-mem install
|
||||
```
|
||||
|
||||
## Worker Binary Size
|
||||
|
||||
The worker is the largest bundled script because it includes:
|
||||
- Claude Agent SDK (`@anthropic-ai/claude-agent-sdk`)
|
||||
- Prompt templates and parsers
|
||||
- Socket server implementation
|
||||
- Database operations
|
||||
|
||||
**Size**: ~232 KB (minified)
|
||||
|
||||
This is acceptable because:
|
||||
- Only spawned once per session (not per tool use)
|
||||
- Runs in background (doesn't block UI)
|
||||
- Contains full SDK functionality
|
||||
|
||||
## Worker Communication Protocol
|
||||
|
||||
### Message Types
|
||||
|
||||
#### 1. Observation Message
|
||||
```json
|
||||
{
|
||||
"type": "observation",
|
||||
"tool_name": "Read",
|
||||
"tool_input": "{\"file_path\": \"/path\"}",
|
||||
"tool_output": "{\"content\": \"...\"}"
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Finalize Message
|
||||
```json
|
||||
{
|
||||
"type": "finalize"
|
||||
}
|
||||
```
|
||||
|
||||
### Socket Protocol
|
||||
|
||||
- **Transport**: Unix domain socket
|
||||
- **Format**: JSON messages separated by newlines (`\n`)
|
||||
- **Location**: `~/.claude-mem/sockets/session-{id}.sock`
|
||||
- **Lifecycle**: Created on worker startup, deleted on worker exit
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Worker Startup Failures
|
||||
|
||||
If the worker fails to start:
|
||||
- New-hook logs error but doesn't block Claude Code
|
||||
- Session record remains in "pending" state
|
||||
- No observations are captured (graceful degradation)
|
||||
|
||||
### Socket Communication Failures
|
||||
|
||||
If hooks can't connect to worker socket:
|
||||
- Hook logs error but doesn't block Claude Code
|
||||
- Tool use continues normally
|
||||
- Observations are skipped for that session
|
||||
|
||||
### Worker Crashes
|
||||
|
||||
If the worker crashes mid-session:
|
||||
- Database marks session as "failed"
|
||||
- Socket is cleaned up automatically
|
||||
- Next session will spawn new worker
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Worker Directly
|
||||
|
||||
```bash
|
||||
# Build the worker
|
||||
npm run build:hooks
|
||||
|
||||
# Test worker (needs valid session ID in DB)
|
||||
bun scripts/hooks/worker.js 123
|
||||
```
|
||||
|
||||
### Test Worker Spawning
|
||||
|
||||
```bash
|
||||
# Plugin mode (with CLAUDE_PLUGIN_ROOT)
|
||||
CLAUDE_PLUGIN_ROOT=$(pwd) printf '{"session_id":"test","cwd":"/path","prompt":"help"}' | \
|
||||
bun scripts/hooks/new-hook.js
|
||||
|
||||
# Traditional mode (without CLAUDE_PLUGIN_ROOT)
|
||||
printf '{"session_id":"test","cwd":"/path","prompt":"help"}' | \
|
||||
bun scripts/hooks/new-hook.js
|
||||
```
|
||||
|
||||
### Monitor Worker Logs
|
||||
|
||||
Worker logs to stderr:
|
||||
```bash
|
||||
# Watch worker logs in real-time
|
||||
tail -f ~/.claude-mem/logs/worker-*.log
|
||||
```
|
||||
|
||||
## Benefits of This Architecture
|
||||
|
||||
1. **Self-contained**: Plugin bundles everything needed
|
||||
2. **Backwards compatible**: Works with global CLI too
|
||||
3. **Automatic detection**: Uses environment variable to choose mode
|
||||
4. **Isolated execution**: Worker runs in separate process
|
||||
5. **Async communication**: Hooks don't block on SDK operations
|
||||
6. **Graceful degradation**: Failures don't break Claude Code
|
||||
|
||||
## Future Improvements
|
||||
|
||||
Potential enhancements:
|
||||
- [ ] Worker health checks and auto-restart
|
||||
- [ ] Multiple workers for concurrent sessions
|
||||
- [ ] Worker pool management
|
||||
- [ ] WebSocket support for remote workers
|
||||
- [ ] Worker performance metrics
|
||||
|
||||
## See Also
|
||||
|
||||
- [Plugin Structure Documentation](./PLUGIN_STRUCTURE.md)
|
||||
- [Plugin Development Guide](./PLUGIN_DEVELOPMENT.md)
|
||||
- [Build Documentation](./BUILD.md)
|
||||
@@ -1,165 +0,0 @@
|
||||
# Phase 0 Task 1: Add Comprehensive Logging to Summary Hook
|
||||
|
||||
## Overview
|
||||
Added comprehensive logging to the Stop hook (summary hook) to verify it fires on normal exit and successfully sends the FINALIZE message to the worker socket.
|
||||
|
||||
## Files Modified
|
||||
|
||||
### `/Users/alexnewman/Scripts/claude-mem/src/hooks/summary.ts`
|
||||
Added 8 logging points throughout the hook execution flow.
|
||||
|
||||
## Logging Points Added
|
||||
|
||||
All logs use the `[claude-mem summary]` prefix for easy searching and use `console.error()` to output to stderr (visible in terminal).
|
||||
|
||||
### 1. Hook Entry Point (Line 18-20)
|
||||
```typescript
|
||||
console.error('[claude-mem summary] Hook fired', {
|
||||
input: input ? { session_id: input.session_id, cwd: input.cwd } : null
|
||||
});
|
||||
```
|
||||
**Purpose:** Confirms the hook was called by Claude Code and logs the input parameters.
|
||||
|
||||
### 2. Session Search (Line 34)
|
||||
```typescript
|
||||
console.error('[claude-mem summary] Searching for active SDK session', { session_id });
|
||||
```
|
||||
**Purpose:** Logs the session_id being searched for in the database.
|
||||
|
||||
### 3. Session Not Found (Line 43)
|
||||
```typescript
|
||||
console.error('[claude-mem summary] No active SDK session found', { session_id });
|
||||
```
|
||||
**Purpose:** Logs when no active session is found (normal for non-SDK sessions).
|
||||
|
||||
### 4. Session Found (Line 48-52)
|
||||
```typescript
|
||||
console.error('[claude-mem summary] Active SDK session found', {
|
||||
session_id: session.id,
|
||||
collection_name: session.collection_name,
|
||||
worker_pid: session.worker_pid
|
||||
});
|
||||
```
|
||||
**Purpose:** Logs when an active session is found with its details for verification.
|
||||
|
||||
### 5. Before Socket Send (Line 62-65)
|
||||
```typescript
|
||||
console.error('[claude-mem summary] Attempting to send FINALIZE message to worker socket', {
|
||||
socketPath,
|
||||
message
|
||||
});
|
||||
```
|
||||
**Purpose:** Logs the socket path and message content before attempting connection.
|
||||
|
||||
### 6. Socket Connection Established (Line 68)
|
||||
```typescript
|
||||
console.error('[claude-mem summary] Socket connection established, sending message');
|
||||
```
|
||||
**Purpose:** Confirms successful socket connection before writing data.
|
||||
|
||||
### 7. Socket Error Handler (Line 75-79)
|
||||
```typescript
|
||||
console.error('[claude-mem summary] Socket error occurred', {
|
||||
error: err.message,
|
||||
code: (err as any).code,
|
||||
socketPath
|
||||
});
|
||||
```
|
||||
**Purpose:** Logs detailed error information if socket connection fails (includes error code like ENOENT, ECONNREFUSED).
|
||||
|
||||
### 8. Socket Close Handler (Line 84)
|
||||
```typescript
|
||||
console.error('[claude-mem summary] Socket connection closed successfully');
|
||||
```
|
||||
**Purpose:** Confirms the socket connection closed cleanly after sending message.
|
||||
|
||||
### 9. Catch Block (Line 91-95)
|
||||
```typescript
|
||||
console.error('[claude-mem summary] Unexpected error in hook', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name
|
||||
});
|
||||
```
|
||||
**Purpose:** Logs any unexpected errors with full stack trace for debugging.
|
||||
|
||||
## How to Test
|
||||
|
||||
### Basic Test (Normal Exit)
|
||||
1. Start a Claude Code session in a project with claude-mem configured
|
||||
2. Have a conversation that triggers SDK memory operations
|
||||
3. Exit Claude Code normally (Ctrl+D or type "exit")
|
||||
4. Check terminal stderr for log sequence:
|
||||
```
|
||||
[claude-mem summary] Hook fired
|
||||
[claude-mem summary] Searching for active SDK session
|
||||
[claude-mem summary] Active SDK session found
|
||||
[claude-mem summary] Attempting to send FINALIZE message to worker socket
|
||||
[claude-mem summary] Socket connection established, sending message
|
||||
[claude-mem summary] Socket connection closed successfully
|
||||
```
|
||||
|
||||
### Test Cases
|
||||
|
||||
#### Case 1: Normal Exit with Active Session
|
||||
**Expected logs:**
|
||||
1. Hook fired (with session_id and cwd)
|
||||
2. Searching for active SDK session
|
||||
3. Active SDK session found (with session details)
|
||||
4. Attempting to send FINALIZE message (with socket path)
|
||||
5. Socket connection established
|
||||
6. Socket connection closed successfully
|
||||
|
||||
#### Case 2: Exit with No Active Session
|
||||
**Expected logs:**
|
||||
1. Hook fired
|
||||
2. Searching for active SDK session
|
||||
3. No active SDK session found
|
||||
|
||||
#### Case 3: Worker Socket Already Closed
|
||||
**Expected logs:**
|
||||
1. Hook fired
|
||||
2. Searching for active SDK session
|
||||
3. Active SDK session found
|
||||
4. Attempting to send FINALIZE message
|
||||
5. Socket error occurred (with ENOENT or ECONNREFUSED code)
|
||||
|
||||
#### Case 4: Database Error
|
||||
**Expected logs:**
|
||||
1. Hook fired
|
||||
2. Searching for active SDK session
|
||||
3. Unexpected error in hook (with stack trace)
|
||||
|
||||
### Log Filtering
|
||||
To view only summary hook logs:
|
||||
```bash
|
||||
claude-code 2>&1 | grep "\[claude-mem summary\]"
|
||||
```
|
||||
|
||||
## Behavior Guarantees
|
||||
|
||||
1. **No Breaking Changes:** All existing functionality remains identical
|
||||
2. **Non-Blocking:** All errors are caught and logged but don't block Claude Code
|
||||
3. **Clean Exit:** Hook always returns proper JSON response to Claude Code
|
||||
4. **Searchable:** All logs use consistent `[claude-mem summary]` prefix
|
||||
|
||||
## Issues and Concerns
|
||||
|
||||
### None Discovered
|
||||
- The existing error handling is robust
|
||||
- All error paths properly log and exit gracefully
|
||||
- No changes needed to logic, only observability added
|
||||
|
||||
### Potential Observations During Testing
|
||||
- If socket errors are common, may indicate worker timing issues
|
||||
- If "No active SDK session found" appears frequently, may indicate database query issues
|
||||
- If hook never fires, indicates Claude Code hook registration problem
|
||||
- If socket path is wrong, indicates paths.ts configuration issue
|
||||
|
||||
## Next Steps
|
||||
|
||||
After testing with these logs:
|
||||
1. Verify hook fires on every Claude Code exit
|
||||
2. Verify FINALIZE message reaches worker socket
|
||||
3. Check for any unexpected error patterns
|
||||
4. Use logs to diagnose any issues with worker finalization flow
|
||||
@@ -1,323 +0,0 @@
|
||||
# Phase 0 Task 2: SDK Worker Comprehensive Logging
|
||||
|
||||
## Summary
|
||||
|
||||
Added comprehensive logging to `/Users/alexnewman/Scripts/claude-mem/src/sdk/worker.ts` to trace the complete flow of the FINALIZE message from receipt through SDK agent processing to database storage.
|
||||
|
||||
## Modified Files
|
||||
|
||||
1. `/Users/alexnewman/Scripts/claude-mem/src/sdk/worker.ts` - Added 20+ logging points throughout the worker lifecycle
|
||||
|
||||
## Logging Points Added
|
||||
|
||||
All logs use the `[claude-mem worker]` prefix for easy searching and are sent to stderr using `console.error()`.
|
||||
|
||||
### 1. Worker Initialization (Lines 70-73)
|
||||
- **Location:** `constructor()`
|
||||
- **What:** Logs when worker instance is created
|
||||
- **Data:** sessionDbId, socketPath
|
||||
|
||||
### 2. Worker Run Started (Lines 80-83)
|
||||
- **Location:** `run()` method entry
|
||||
- **What:** Logs when main run loop begins
|
||||
- **Data:** sessionDbId, socketPath
|
||||
|
||||
### 3. Session Loading (Lines 89-100)
|
||||
- **Location:** `run()` method after `loadSession()`
|
||||
- **What:** Logs session load failure or success
|
||||
- **Data:**
|
||||
- Failure: sessionDbId
|
||||
- Success: sessionDbId, project, sdkSessionId, userPromptLength
|
||||
|
||||
### 4. Socket Server Started (Lines 107-110)
|
||||
- **Location:** `run()` method after `startSocketServer()`
|
||||
- **What:** Logs successful socket server initialization
|
||||
- **Data:** socketPath, sessionDbId
|
||||
|
||||
### 5. SDK Agent Starting (Lines 113-116)
|
||||
- **Location:** `run()` method before `runSDKAgent()`
|
||||
- **What:** Logs SDK agent invocation
|
||||
- **Data:** sessionDbId, model
|
||||
|
||||
### 6. SDK Agent Completed (Lines 120-123)
|
||||
- **Location:** `run()` method after `runSDKAgent()` completes
|
||||
- **What:** Logs completion before marking session as done
|
||||
- **Data:** sessionDbId, sdkSessionId
|
||||
|
||||
### 7. Fatal Error Handler (Lines 129-133)
|
||||
- **Location:** `run()` method catch block
|
||||
- **What:** Logs any fatal errors with full stack trace
|
||||
- **Data:** sessionDbId, error message, stack trace
|
||||
|
||||
### 8. Socket Connection Received (Lines 157-160)
|
||||
- **Location:** `startSocketServer()` - connection handler
|
||||
- **What:** Logs when a client connects to the Unix socket
|
||||
- **Data:** sessionDbId, socketPath
|
||||
|
||||
### 9. Data Received on Socket (Lines 164-167)
|
||||
- **Location:** `startSocketServer()` - data handler
|
||||
- **What:** Logs when data arrives on socket
|
||||
- **Data:** sessionDbId, chunk size
|
||||
|
||||
### 10. Message Parsed from Socket (Lines 178-182)
|
||||
- **Location:** `startSocketServer()` - message parsing
|
||||
- **What:** Logs successfully parsed JSON message
|
||||
- **Data:** sessionDbId, messageType, rawMessage (truncated to 500 chars)
|
||||
|
||||
### 11. Invalid Message Error (Lines 185-189)
|
||||
- **Location:** `startSocketServer()` - JSON parse error
|
||||
- **What:** Logs when message fails to parse
|
||||
- **Data:** sessionDbId, error message, rawLine (truncated to 200 chars)
|
||||
|
||||
### 12. Socket Connection Error (Lines 196-200)
|
||||
- **Location:** `startSocketServer()` - socket error handler
|
||||
- **What:** Logs socket-level errors
|
||||
- **Data:** sessionDbId, error message, stack trace
|
||||
|
||||
### 13. Server Errors (Lines 206-216)
|
||||
- **Location:** `startSocketServer()` - server error handler
|
||||
- **What:** Logs server-level errors (EADDRINUSE, etc.)
|
||||
- **Data:** sessionDbId, socketPath (if EADDRINUSE), error details
|
||||
|
||||
### 14. Message Handler Entry (Lines 233-237)
|
||||
- **Location:** `handleMessage()` method entry
|
||||
- **What:** Logs when processing any message
|
||||
- **Data:** sessionDbId, messageType, pendingMessagesCount
|
||||
|
||||
### 15. FINALIZE Message Detected (Lines 242-246)
|
||||
- **Location:** `handleMessage()` - finalize detection
|
||||
- **What:** Logs when FINALIZE message is received (CRITICAL LOG)
|
||||
- **Data:** sessionDbId, isFinalized=true, pendingMessagesCount
|
||||
|
||||
### 16. Observation Message Queued (Lines 249-254)
|
||||
- **Location:** `handleMessage()` - observation handling
|
||||
- **What:** Logs observation message details
|
||||
- **Data:** sessionDbId, toolName, input/output lengths
|
||||
|
||||
### 17. SDK Session Initialized (Lines 292-295)
|
||||
- **Location:** `runSDKAgent()` - onSystemInitMessage callback
|
||||
- **What:** Logs when SDK session ID is received
|
||||
- **Data:** sessionDbId, sdkSessionId
|
||||
|
||||
### 18. SDK Agent Response Received (Lines 301-306)
|
||||
- **Location:** `runSDKAgent()` - onAgentMessage callback
|
||||
- **What:** Logs every response from SDK agent (CRITICAL LOG)
|
||||
- **Data:** sessionDbId, sdkSessionId, contentLength, contentPreview (200 chars)
|
||||
|
||||
### 19. Initial Prompt Yielded (Lines 322-327)
|
||||
- **Location:** `createMessageGenerator()` - initial prompt
|
||||
- **What:** Logs when first prompt is sent to SDK agent
|
||||
- **Data:** sessionDbId, claudeSessionId, project, promptLength
|
||||
|
||||
### 20. FINALIZE Processing in Generator (Lines 349-352)
|
||||
- **Location:** `createMessageGenerator()` - finalize handling
|
||||
- **What:** Logs when FINALIZE is processed in async generator (CRITICAL LOG)
|
||||
- **Data:** sessionDbId, sdkSessionId
|
||||
|
||||
### 21. Finalize Prompt Yielded (Lines 357-362)
|
||||
- **Location:** `createMessageGenerator()` - after building finalize prompt
|
||||
- **What:** Logs finalize prompt being sent to SDK agent (CRITICAL LOG)
|
||||
- **Data:** sessionDbId, sdkSessionId, promptLength, promptPreview (300 chars)
|
||||
|
||||
### 22. Failed to Load Session for Finalize (Lines 371-373)
|
||||
- **Location:** `createMessageGenerator()` - error case
|
||||
- **What:** Logs if session reload fails during finalize
|
||||
- **Data:** sessionDbId
|
||||
|
||||
### 23. Observation Prompt Yielded (Lines 385-389)
|
||||
- **Location:** `createMessageGenerator()` - observation handling
|
||||
- **What:** Logs when observation prompt is sent to SDK agent
|
||||
- **Data:** sessionDbId, toolName, promptLength
|
||||
|
||||
### 24. Parsing Agent Message (Lines 406-410)
|
||||
- **Location:** `handleAgentMessage()` method entry
|
||||
- **What:** Logs when starting to parse agent response
|
||||
- **Data:** sessionDbId, sdkSessionId, contentLength
|
||||
|
||||
### 25. Observations Parsed (Lines 414-418)
|
||||
- **Location:** `handleAgentMessage()` - after parseObservations()
|
||||
- **What:** Logs how many observations were found
|
||||
- **Data:** sessionDbId, sdkSessionId, observationCount
|
||||
|
||||
### 26. Storing Observation (Lines 422-428)
|
||||
- **Location:** `handleAgentMessage()` - in observation loop
|
||||
- **What:** Logs each observation being stored
|
||||
- **Data:** sessionDbId, sdkSessionId, project, observationType, observationTextLength
|
||||
|
||||
### 27. Cannot Store Observation (Lines 431-434)
|
||||
- **Location:** `handleAgentMessage()` - error case
|
||||
- **What:** Logs when SDK session ID is missing
|
||||
- **Data:** sessionDbId, observationType
|
||||
|
||||
### 28. Attempting to Parse Summary (Lines 439-442)
|
||||
- **Location:** `handleAgentMessage()` - before parseSummary()
|
||||
- **What:** Logs when attempting summary parse (CRITICAL LOG)
|
||||
- **Data:** sessionDbId, sdkSessionId
|
||||
|
||||
### 29. Summary Parsed Successfully (Lines 446-456)
|
||||
- **Location:** `handleAgentMessage()` - after parseSummary() success
|
||||
- **What:** Logs summary structure details (CRITICAL LOG)
|
||||
- **Data:** sessionDbId, sdkSessionId, project, hasRequest, hasInvestigated, hasLearned, hasCompleted, filesReadCount, filesEditedCount
|
||||
|
||||
### 30. Storing Summary in Database (Lines 470-474)
|
||||
- **Location:** `handleAgentMessage()` - before storeSummary()
|
||||
- **What:** Logs summary about to be stored (CRITICAL LOG)
|
||||
- **Data:** sessionDbId, sdkSessionId, project
|
||||
|
||||
### 31. Summary Stored Successfully (Lines 478-482)
|
||||
- **Location:** `handleAgentMessage()` - after storeSummary()
|
||||
- **What:** Logs successful database storage (CRITICAL LOG)
|
||||
- **Data:** sessionDbId, sdkSessionId, project
|
||||
|
||||
### 32. Summary Parsed but No SDK Session (Lines 484-486)
|
||||
- **Location:** `handleAgentMessage()` - error case
|
||||
- **What:** Logs when summary found but can't store
|
||||
- **Data:** sessionDbId
|
||||
|
||||
### 33. No Summary Found (Lines 488-491)
|
||||
- **Location:** `handleAgentMessage()` - no summary case
|
||||
- **What:** Logs when response has no summary
|
||||
- **Data:** sessionDbId, sdkSessionId
|
||||
|
||||
### 34. Cleanup Started (Lines 499-504)
|
||||
- **Location:** `cleanup()` method entry
|
||||
- **What:** Logs cleanup process beginning
|
||||
- **Data:** sessionDbId, socketPath, hasServer, socketExists
|
||||
|
||||
### 35. Cleanup Complete (Lines 513-515)
|
||||
- **Location:** `cleanup()` method exit
|
||||
- **What:** Logs cleanup finished
|
||||
- **Data:** sessionDbId
|
||||
|
||||
## How to Test
|
||||
|
||||
### 1. Start the Worker
|
||||
```bash
|
||||
# Start a worker for session ID 1 (for example)
|
||||
bun run src/sdk/worker.ts 1
|
||||
```
|
||||
|
||||
Look for logs:
|
||||
- `[claude-mem worker] Worker instance created`
|
||||
- `[claude-mem worker] Worker run() started`
|
||||
- `[claude-mem worker] Session loaded successfully`
|
||||
- `[claude-mem worker] Socket server started successfully`
|
||||
- `[claude-mem worker] Starting SDK agent`
|
||||
|
||||
### 2. Send Messages via Socket
|
||||
```bash
|
||||
# From another terminal, send a message to the socket
|
||||
# Socket path format: /tmp/claude-mem-worker-{sessionDbId}.sock
|
||||
|
||||
# Send an observation
|
||||
echo '{"type":"observation","tool_name":"Read","tool_input":"...","tool_output":"..."}' | nc -U /tmp/claude-mem-worker-1.sock
|
||||
|
||||
# Send finalize
|
||||
echo '{"type":"finalize"}' | nc -U /tmp/claude-mem-worker-1.sock
|
||||
```
|
||||
|
||||
### 3. Monitor Logs
|
||||
Use grep to filter for specific events:
|
||||
|
||||
```bash
|
||||
# All worker logs
|
||||
bun run src/sdk/worker.ts 1 2>&1 | grep '\[claude-mem worker\]'
|
||||
|
||||
# Only FINALIZE-related logs
|
||||
bun run src/sdk/worker.ts 1 2>&1 | grep -i finalize
|
||||
|
||||
# Only summary-related logs
|
||||
bun run src/sdk/worker.ts 1 2>&1 | grep -i summary
|
||||
|
||||
# Only database storage logs
|
||||
bun run src/sdk/worker.ts 1 2>&1 | grep -i storing
|
||||
```
|
||||
|
||||
## What to Look for When FINALIZE is Sent
|
||||
|
||||
The expected log sequence when a FINALIZE message is sent:
|
||||
|
||||
1. **Message Receipt:**
|
||||
```
|
||||
[claude-mem worker] Data received on socket
|
||||
[claude-mem worker] Message received from socket { messageType: 'finalize', ... }
|
||||
```
|
||||
|
||||
2. **Message Handling:**
|
||||
```
|
||||
[claude-mem worker] Processing message in handleMessage() { messageType: 'finalize', ... }
|
||||
[claude-mem worker] FINALIZE message detected { isFinalized: true, ... }
|
||||
```
|
||||
|
||||
3. **Generator Processing:**
|
||||
```
|
||||
[claude-mem worker] Processing FINALIZE message in generator
|
||||
[claude-mem worker] Yielding finalize prompt to SDK agent { promptLength: ..., promptPreview: ... }
|
||||
```
|
||||
|
||||
4. **SDK Agent Response:**
|
||||
```
|
||||
[claude-mem worker] SDK agent response received { contentLength: ..., contentPreview: ... }
|
||||
```
|
||||
|
||||
5. **Parsing and Storage:**
|
||||
```
|
||||
[claude-mem worker] Parsing agent message for observations and summary
|
||||
[claude-mem worker] Observations parsed from response { observationCount: ... }
|
||||
[claude-mem worker] Attempting to parse summary from response
|
||||
[claude-mem worker] Summary parsed successfully { hasRequest: true, hasLearned: true, ... }
|
||||
[claude-mem worker] Storing summary in database
|
||||
[claude-mem worker] Summary stored successfully in database
|
||||
```
|
||||
|
||||
6. **Completion:**
|
||||
```
|
||||
[claude-mem worker] SDK agent completed, marking session as completed
|
||||
[claude-mem worker] Cleaning up worker resources
|
||||
[claude-mem worker] Cleanup complete
|
||||
```
|
||||
|
||||
## Issues and Concerns
|
||||
|
||||
### 1. Large Response Truncation
|
||||
- Raw messages are truncated to 500 chars in socket logs
|
||||
- Content previews are limited to 200-300 chars
|
||||
- This prevents log spam but might make debugging harder if the critical info is beyond the truncation point
|
||||
|
||||
### 2. Async Generator Timing
|
||||
- The generator waits in a loop (`while (!this.isFinalized)`) with 100ms sleeps
|
||||
- Logs show when messages are queued but not when the generator processes them
|
||||
- There could be a small delay between "FINALIZE message detected" and "Processing FINALIZE in generator"
|
||||
|
||||
### 3. Error Cases Not Fully Logged
|
||||
- Parser errors in `parseObservations()` and `parseSummary()` are not logged
|
||||
- Should consider adding try-catch in `handleAgentMessage()` to catch parser exceptions
|
||||
- XML parsing errors would be silent
|
||||
|
||||
### 4. No Timing Information
|
||||
- Logs don't include timestamps (relies on stderr default timestamps)
|
||||
- Could add `Date.now()` or elapsed time to measure performance bottlenecks
|
||||
|
||||
### 5. Socket Path Permissions
|
||||
- No logging for socket file permissions or creation errors
|
||||
- If socket can't be created due to permissions, error might not be clear
|
||||
|
||||
### 6. Multi-Message Batching
|
||||
- If multiple messages arrive rapidly, they're processed in a batch
|
||||
- Logs show individual messages but don't indicate batch boundaries
|
||||
- Could add batch ID or sequence numbers
|
||||
|
||||
## Recommendations for Next Steps
|
||||
|
||||
1. **Test the logging** by running the worker and sending various messages
|
||||
2. **Add parser error handling** in `handleAgentMessage()` to catch XML parse failures
|
||||
3. **Consider adding timing metrics** to measure latency at each stage
|
||||
4. **Validate socket connectivity** early in startup (try writing a test message)
|
||||
5. **Add structured logging library** if JSON logs would be easier to parse programmatically
|
||||
|
||||
## Related Files
|
||||
|
||||
- `/Users/alexnewman/Scripts/claude-mem/src/sdk/prompts.ts` - Prompt builders used in logged operations
|
||||
- `/Users/alexnewman/Scripts/claude-mem/src/sdk/parser.ts` - XML parsers for observations and summaries
|
||||
- `/Users/alexnewman/Scripts/claude-mem/src/services/sqlite/HooksDatabase.js` - Database methods being called
|
||||
- `/Users/alexnewman/Scripts/claude-mem/src/shared/paths.js` - Socket path generation
|
||||
@@ -1,100 +0,0 @@
|
||||
# Phase 0 Task 2b: TypeScript Error Fixes
|
||||
|
||||
## Summary
|
||||
|
||||
Fixed all 6 TypeScript errors in `src/sdk/worker.ts` that were introduced after adding logging functionality. All logging has been preserved.
|
||||
|
||||
## Errors Fixed
|
||||
|
||||
### 1. Line 283 (now 338): Type error with AsyncIterable - missing `parent_tool_use_id` property
|
||||
|
||||
**Error**: Return type was `AsyncIterable<{ type: 'user'; message: { role: 'user'; content: string } }>` which didn't match the SDK's `SDKUserMessage` type.
|
||||
|
||||
**Fix**:
|
||||
- Changed return type to `AsyncIterable<SDKUserMessage>`
|
||||
- Added required `session_id` and `parent_tool_use_id: null` properties to all yielded messages
|
||||
- Updated all three yield statements in the generator (initial prompt, finalize prompt, and observation prompt)
|
||||
|
||||
**Changes**:
|
||||
- Line 338: Updated function signature
|
||||
- Lines 348-356: Added `session_id` and `parent_tool_use_id` to initial prompt yield
|
||||
- Lines 385-393: Added `session_id` and `parent_tool_use_id` to finalize prompt yield
|
||||
- Lines 416-424: Added `session_id` and `parent_tool_use_id` to observation prompt yield
|
||||
|
||||
### 2. Line 289 (now removed): `onSystemInitMessage` doesn't exist in type 'Options'
|
||||
|
||||
**Error**: The `Options` type from the Claude Agent SDK doesn't have an `onSystemInitMessage` callback property.
|
||||
|
||||
**Fix**:
|
||||
- Removed the invalid callback options from the `query()` call
|
||||
- Changed to iterate over the returned `Query` async generator
|
||||
- Handle system init messages in the iteration loop by checking message type
|
||||
|
||||
**Changes**:
|
||||
- Lines 290-298: Removed callback options, kept valid options only
|
||||
- Lines 300-312: Added iteration loop to handle system init messages
|
||||
- The session ID is now captured when processing messages with `type === 'system' && subtype === 'init'`
|
||||
|
||||
### 3. Line 289 (now removed): Parameter 'msg' implicitly has 'any' type
|
||||
|
||||
**Error**: The callback parameter didn't have a type annotation.
|
||||
|
||||
**Fix**: This error was resolved by removing the invalid callback entirely (see fix #2).
|
||||
|
||||
### 4. Line 300 (now removed): Parameter 'msg' implicitly has 'any' type
|
||||
|
||||
**Error**: The callback parameter didn't have a type annotation.
|
||||
|
||||
**Fix**: This error was resolved by removing the invalid callback entirely (see fix #2).
|
||||
|
||||
### 5. Line 380 (now 404): Argument type error for Observation - missing `id` and `created_at_epoch`
|
||||
|
||||
**Error**: The `buildObservationPrompt()` function expects an `Observation` type with `id` and `created_at_epoch` properties, but the code was only passing `tool_name`, `tool_input`, and `tool_output`.
|
||||
|
||||
**Fix**:
|
||||
- Added the missing `id: 0` (with comment explaining it's not needed for prompt generation)
|
||||
- Added `created_at_epoch: Date.now()` to provide the current timestamp
|
||||
|
||||
**Changes**:
|
||||
- Lines 404-410: Complete Observation object with all required properties
|
||||
|
||||
### 6. Line 527 (now 555): Property 'main' does not exist on type 'ImportMeta'
|
||||
|
||||
**Error**: TypeScript's default `ImportMeta` interface doesn't include Bun's custom `main` property.
|
||||
|
||||
**Fix**:
|
||||
- Added a global type declaration to extend the `ImportMeta` interface with Bun's `main` property
|
||||
- Used TypeScript's declaration merging to add the property type-safely
|
||||
|
||||
**Changes**:
|
||||
- Lines 7-12: Added global declaration block extending `ImportMeta` with `main: boolean`
|
||||
|
||||
## Additional Changes
|
||||
|
||||
### Import Updates
|
||||
- Line 17: Added import of `SDKUserMessage` and `SDKSystemMessage` types from the SDK package
|
||||
|
||||
### SDK Message Handling
|
||||
- Lines 300-331: Refactored from callback-based approach to iteration-based approach
|
||||
- Added proper message type checking and handling for both system and assistant messages
|
||||
- Added content extraction logic for assistant messages (lines 316-320) to handle both array and string content types
|
||||
|
||||
## Verification
|
||||
|
||||
All TypeScript errors have been resolved:
|
||||
- ✅ AsyncIterable type now matches SDK expectations
|
||||
- ✅ No invalid callback options used
|
||||
- ✅ All parameters have explicit types
|
||||
- ✅ Observation objects have all required properties
|
||||
- ✅ ImportMeta.main property is properly typed for Bun
|
||||
|
||||
## Logging Preservation
|
||||
|
||||
All logging statements have been preserved:
|
||||
- ✅ All `console.error()` statements remain intact
|
||||
- ✅ Debug logging for socket operations preserved
|
||||
- ✅ Worker lifecycle logging preserved
|
||||
- ✅ Message processing logging preserved
|
||||
- ✅ SDK agent interaction logging preserved
|
||||
|
||||
The refactoring from callbacks to iteration actually improved logging by making the message handling flow more explicit and easier to follow.
|
||||
@@ -1,275 +0,0 @@
|
||||
# Phase 0 Task 3: Context Hook Logging Implementation
|
||||
|
||||
## Summary
|
||||
|
||||
Added comprehensive logging to the context hook (`src/hooks/context.ts`) to verify it correctly loads summaries from the database and outputs them as Claude's context. All logging uses `console.error` to avoid polluting stdout, which is reserved for the markdown context output that becomes part of Claude's context.
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `/Users/alexnewman/Scripts/claude-mem/src/hooks/context.ts`
|
||||
|
||||
## Logging Points Added
|
||||
|
||||
All log messages use the `[claude-mem context]` prefix for easy searching and filtering.
|
||||
|
||||
### 1. Hook Invocation (Line 18-23)
|
||||
```typescript
|
||||
console.error('[claude-mem context] Hook fired with input:', JSON.stringify({
|
||||
session_id: input?.session_id,
|
||||
cwd: input?.cwd,
|
||||
source: input?.source,
|
||||
has_input: !!input
|
||||
}));
|
||||
```
|
||||
**Purpose:** Logs that the hook was called and shows the input parameters, especially the `source` field which determines if context should be loaded.
|
||||
|
||||
### 2. Standalone Mode Detection (Line 27)
|
||||
```typescript
|
||||
console.error('[claude-mem context] No input provided - exiting (standalone mode)');
|
||||
```
|
||||
**Purpose:** Logs when the hook is run standalone without Claude Code input.
|
||||
|
||||
### 3. Source Check - Skip (Line 34)
|
||||
```typescript
|
||||
console.error('[claude-mem context] Source is not "startup" (got:', input.source, ') - skipping context load');
|
||||
```
|
||||
**Purpose:** Logs when the source is not "startup" (e.g., "resume"), indicating context loading is being skipped.
|
||||
|
||||
### 4. Source Check - Proceed (Line 39)
|
||||
```typescript
|
||||
console.error('[claude-mem context] Source check passed - proceeding with context load');
|
||||
```
|
||||
**Purpose:** Confirms we're proceeding with context loading because source check passed.
|
||||
|
||||
### 5. Project Extraction (Line 43)
|
||||
```typescript
|
||||
console.error('[claude-mem context] Extracted project name:', project, 'from cwd:', input.cwd);
|
||||
```
|
||||
**Purpose:** Shows the project name extracted from the cwd, which is used to query summaries.
|
||||
|
||||
### 6. Database Query Start (Line 46)
|
||||
```typescript
|
||||
console.error('[claude-mem context] Querying database for recent summaries...');
|
||||
```
|
||||
**Purpose:** Indicates we're about to query the database.
|
||||
|
||||
### 7. Database Query Results (Line 51)
|
||||
```typescript
|
||||
console.error('[claude-mem context] Database query complete - found', summaries.length, 'summaries');
|
||||
```
|
||||
**Purpose:** Reports how many summaries were found in the database.
|
||||
|
||||
### 8. Summary Previews (Lines 54-60)
|
||||
```typescript
|
||||
if (summaries.length > 0) {
|
||||
console.error('[claude-mem context] Summary previews:');
|
||||
summaries.forEach((summary, idx) => {
|
||||
const preview = summary.request?.substring(0, 100) || summary.completed?.substring(0, 100) || '(no content)';
|
||||
console.error(` [${idx + 1}]`, preview + (preview.length >= 100 ? '...' : ''));
|
||||
});
|
||||
}
|
||||
```
|
||||
**Purpose:** Shows a preview (first 100 chars) of each summary found, helping verify the correct data was retrieved.
|
||||
|
||||
### 9. No Summaries Found (Line 64)
|
||||
```typescript
|
||||
console.error('[claude-mem context] No summaries found - outputting empty context message');
|
||||
```
|
||||
**Purpose:** Logs when no summaries exist for the project.
|
||||
|
||||
### 10. Markdown Building Start (Line 70)
|
||||
```typescript
|
||||
console.error('[claude-mem context] Building markdown context from summaries...');
|
||||
```
|
||||
**Purpose:** Indicates we're starting to build the markdown output.
|
||||
|
||||
### 11. Markdown Output Details (Lines 117-120)
|
||||
```typescript
|
||||
console.error('[claude-mem context] Markdown built successfully');
|
||||
console.error('[claude-mem context] Output length:', markdownOutput.length, 'characters,', output.length, 'lines');
|
||||
console.error('[claude-mem context] Output preview (first 200 chars):', markdownOutput.substring(0, 200) + '...');
|
||||
console.error('[claude-mem context] Outputting context to stdout for Claude Code injection');
|
||||
```
|
||||
**Purpose:** Reports the markdown was built successfully, shows its length, and provides a preview before sending to stdout.
|
||||
|
||||
### 12. Successful Completion (Line 125)
|
||||
```typescript
|
||||
console.error('[claude-mem context] Context hook completed successfully');
|
||||
```
|
||||
**Purpose:** Confirms the hook completed without errors.
|
||||
|
||||
### 13. Error Handling (Lines 130-133)
|
||||
```typescript
|
||||
console.error('[claude-mem context] ERROR occurred during context hook execution');
|
||||
console.error('[claude-mem context] Error message:', error.message);
|
||||
console.error('[claude-mem context] Error stack:', error.stack);
|
||||
console.error('[claude-mem context] Exiting gracefully to avoid blocking Claude Code');
|
||||
```
|
||||
**Purpose:** Provides detailed error information if anything goes wrong, including stack trace for debugging.
|
||||
|
||||
## Critical Implementation Detail: stdout vs stderr
|
||||
|
||||
**IMPORTANT:** All logging uses `console.error` (stderr) because:
|
||||
- The context hook outputs markdown to `console.log` (stdout)
|
||||
- Claude Code reads stdout to inject context into Claude's conversation
|
||||
- Any logging to stdout would pollute the context and break the feature
|
||||
- stderr is safe for logging and will appear in Claude Code's logs/terminal
|
||||
|
||||
## How to Test
|
||||
|
||||
### Testing with an Existing Project with Summaries
|
||||
|
||||
1. **Ensure you have previous summaries saved:**
|
||||
```bash
|
||||
# Check if summaries exist for your project
|
||||
sqlite3 ~/.config/claude-code/hooks/claude-mem.db "SELECT * FROM summaries WHERE project = 'your-project-name' LIMIT 5;"
|
||||
```
|
||||
|
||||
2. **Start a new Claude Code session:**
|
||||
```bash
|
||||
cd /path/to/your-project
|
||||
claude-code
|
||||
```
|
||||
|
||||
3. **Check the logs:**
|
||||
- Look for `[claude-mem context]` messages in stderr
|
||||
- Claude Code should show these logs during startup
|
||||
- The context should appear in Claude's initial knowledge
|
||||
|
||||
### Testing with a New Project (No Summaries)
|
||||
|
||||
1. **Navigate to a project without previous summaries:**
|
||||
```bash
|
||||
cd /path/to/new-project
|
||||
claude-code
|
||||
```
|
||||
|
||||
2. **Expected behavior:**
|
||||
- Hook fires and logs indicate no summaries found
|
||||
- Output should be: "No previous sessions found for this project yet."
|
||||
|
||||
### Testing Standalone Mode
|
||||
|
||||
```bash
|
||||
# Run the hook directly (not via Claude Code)
|
||||
tsx src/hooks/context.ts
|
||||
|
||||
# Expected output:
|
||||
# [claude-mem context] Hook fired with input: {...}
|
||||
# [claude-mem context] No input provided - exiting (standalone mode)
|
||||
# No input provided - this script is designed to run as a Claude Code SessionStart hook
|
||||
```
|
||||
|
||||
### Testing Source Check (Resume vs Startup)
|
||||
|
||||
The hook should only load context on `source: "startup"`, not on session resume. This is harder to test directly but the logs will show:
|
||||
- On startup: "Source check passed - proceeding with context load"
|
||||
- On resume: "Source is not 'startup' (got: resume) - skipping context load"
|
||||
|
||||
## Expected Log Sequence for a Session with Previous Summaries
|
||||
|
||||
When you start Claude Code in a project with existing summaries, you should see this sequence in stderr:
|
||||
|
||||
```
|
||||
[claude-mem context] Hook fired with input: {"session_id":"...","cwd":"/path/to/project","source":"startup","has_input":true}
|
||||
[claude-mem context] Source check passed - proceeding with context load
|
||||
[claude-mem context] Extracted project name: project from cwd: /path/to/project
|
||||
[claude-mem context] Querying database for recent summaries...
|
||||
[claude-mem context] Database query complete - found 3 summaries
|
||||
[claude-mem context] Summary previews:
|
||||
[1] Added logging to the save hook to track when summaries are being persisted to the database...
|
||||
[2] Implemented the worker hook to generate summaries from session transcripts using Claude API...
|
||||
[3] Created database schema and initial setup for storing session summaries...
|
||||
[claude-mem context] Building markdown context from summaries...
|
||||
[claude-mem context] Markdown built successfully
|
||||
[claude-mem context] Output length: 1247 characters, 45 lines
|
||||
[claude-mem context] Output preview (first 200 chars): # Recent Session Context
|
||||
|
||||
Here's what happened in recent project sessions:
|
||||
|
||||
---
|
||||
|
||||
**Request:** Added logging to the save hook to track when summaries are being persisted to the database
|
||||
|
||||
**Completed:** ...
|
||||
[claude-mem context] Outputting context to stdout for Claude Code injection
|
||||
[claude-mem context] Context hook completed successfully
|
||||
```
|
||||
|
||||
## What to Look For in Logs
|
||||
|
||||
### Success Indicators
|
||||
1. Hook fires with `has_input: true` and `source: "startup"`
|
||||
2. Source check passes
|
||||
3. Project name is correctly extracted
|
||||
4. Database query finds summaries (count > 0)
|
||||
5. Summary previews show meaningful content
|
||||
6. Markdown is built with reasonable length (> 100 characters)
|
||||
7. Hook completes successfully
|
||||
|
||||
### Warning Signs
|
||||
1. Hook fires with `has_input: false` - means Claude Code didn't provide input
|
||||
2. Source is not "startup" - context won't load (expected on resume)
|
||||
3. Database query finds 0 summaries - either first session or save hook not working
|
||||
4. Summary previews show "(no content)" - data might be corrupt
|
||||
5. Markdown length is very small - formatting might be broken
|
||||
6. Error messages appear - check stack trace for issues
|
||||
|
||||
### Common Issues to Debug
|
||||
|
||||
**No summaries found:**
|
||||
- Check if save hook is configured and working
|
||||
- Verify worker hook generated summaries
|
||||
- Ensure project name matches (case-sensitive)
|
||||
|
||||
**Hook doesn't fire:**
|
||||
- Verify hooks are configured in Claude Code settings
|
||||
- Check that the hook path is correct
|
||||
- Ensure the built JavaScript exists (`dist/hooks/context.js`)
|
||||
|
||||
**Context not appearing in Claude:**
|
||||
- Check if markdown is being output to stdout (should see in logs)
|
||||
- Verify stdout isn't being polluted by other logs
|
||||
- Check Claude Code configuration for SessionStart hooks
|
||||
|
||||
## Issues or Concerns Discovered
|
||||
|
||||
### None - Implementation is Clean
|
||||
|
||||
The implementation is straightforward and follows best practices:
|
||||
|
||||
1. **Separation of concerns:** stdout for context, stderr for logging
|
||||
2. **Comprehensive coverage:** Every critical step is logged
|
||||
3. **Safe error handling:** Errors are logged but don't block Claude Code
|
||||
4. **No performance impact:** Logging is lightweight
|
||||
5. **Easy debugging:** All logs are prefixed and searchable
|
||||
|
||||
### Future Enhancements (Optional)
|
||||
|
||||
1. **Log levels:** Could add debug/info/error levels for filtering
|
||||
2. **Timing information:** Could log how long database queries take
|
||||
3. **Conditional logging:** Could enable/disable via environment variable
|
||||
4. **Structured logging:** Could output logs as JSON for parsing
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Start Claude Code in a project with existing summaries
|
||||
- [ ] Verify logs appear in stderr with `[claude-mem context]` prefix
|
||||
- [ ] Confirm context appears in Claude's initial knowledge
|
||||
- [ ] Check summary previews match actual summary content
|
||||
- [ ] Verify markdown length is reasonable
|
||||
- [ ] Test with a new project (no summaries)
|
||||
- [ ] Confirm "No previous sessions found" message appears
|
||||
- [ ] Run hook standalone and verify it exits gracefully
|
||||
- [ ] Check that all log points are hit in sequence
|
||||
- [ ] Verify no logs appear in stdout (only markdown context)
|
||||
|
||||
## Conclusion
|
||||
|
||||
The context hook now has comprehensive logging at every critical step. This will make it easy to:
|
||||
- Verify summaries are being loaded from the database
|
||||
- Debug issues with context not appearing
|
||||
- Confirm the markdown output is correct
|
||||
- Track the complete flow from hook invocation to Claude context injection
|
||||
|
||||
All logging uses stderr to avoid polluting the stdout channel that carries the actual context markdown to Claude Code.
|
||||
@@ -1,279 +0,0 @@
|
||||
# Phase 0 Task 4 Summary: Pre-Test Diagnostics
|
||||
|
||||
**Date:** 2025-10-16
|
||||
**Task:** Verify logging changes and prepare end-to-end test plan
|
||||
|
||||
---
|
||||
|
||||
## Diagnostics Performed
|
||||
|
||||
### 1. Compiled Hook File Verification
|
||||
Checked three compiled JavaScript files to verify logging survived the build process:
|
||||
|
||||
**Files Checked:**
|
||||
- `/Users/alexnewman/Scripts/claude-mem/scripts/hooks/summary-hook.js` (4.6K)
|
||||
- `/Users/alexnewman/Scripts/claude-mem/scripts/hooks/context-hook.js` (5.8K)
|
||||
- `/Users/alexnewman/Scripts/claude-mem/scripts/hooks/worker.js` (238K)
|
||||
|
||||
**Results:**
|
||||
- summary-hook.js: Contains 3 instances of `[claude-mem summary]` logging
|
||||
- context-hook.js: Contains 3 instances of `[claude-mem context]` logging
|
||||
- worker.js: Contains multiple instances of `[claude-mem worker]` logging
|
||||
|
||||
**Status:** PASS - All logging statements are present in compiled files
|
||||
|
||||
### 2. Database State Analysis
|
||||
Queried the claude-mem database to understand current state:
|
||||
|
||||
**Database Location:** `~/.claude-mem/claude-mem.db`
|
||||
|
||||
**Findings:**
|
||||
- Total SDK sessions recorded: 37
|
||||
- Active sessions: 0
|
||||
- Completed sessions: 22
|
||||
- Failed sessions: 0 (inferred)
|
||||
- Session summaries: 0
|
||||
|
||||
**Recent Sessions:**
|
||||
```
|
||||
ID 37: completed at 2025-10-16T21:39:18.888Z, project: claude-mem
|
||||
ID 36: completed at 2025-10-16T21:24:30.850Z, project: claude-mem
|
||||
ID 35: completed at 2025-10-16T21:11:12.929Z, project: claude-mem-test
|
||||
ID 34: completed at 2025-10-16T20:59:43.438Z, project: claude-mem-test
|
||||
ID 33: completed at 2025-10-16T20:55:15.426Z, project: claude-mem-test
|
||||
```
|
||||
|
||||
**Database Tables Present:**
|
||||
- diagnostics
|
||||
- memories
|
||||
- observations
|
||||
- overviews
|
||||
- schema_versions
|
||||
- sdk_sessions (properly indexed)
|
||||
- session_locks
|
||||
- session_summaries
|
||||
- sessions
|
||||
- sqlite_sequence
|
||||
- transcript_events
|
||||
|
||||
**Status:** Database structure is correct, but summary generation appears to have issues
|
||||
|
||||
### 3. Hooks Configuration Verification
|
||||
Checked the Claude Code hooks configuration:
|
||||
|
||||
**Hooks File Location:** `/Users/alexnewman/Scripts/claude-mem/hooks/hooks.json`
|
||||
|
||||
**Configured Hooks:**
|
||||
- SessionStart: Runs `context-hook.js` to inject previous session context
|
||||
- UserPromptSubmit: Runs `new-hook.js` to create SDK session and spawn worker
|
||||
- PostToolUse: Runs `save-hook.js` to record tool observations
|
||||
- Stop: Runs `summary-hook.js` to finalize session and generate summary
|
||||
|
||||
**Status:** All hooks properly configured with appropriate timeouts
|
||||
|
||||
### 4. Worker Process Check
|
||||
Checked for running worker processes and socket files:
|
||||
|
||||
**Commands Used:**
|
||||
```bash
|
||||
ps aux | grep claude-mem-worker | grep -v grep
|
||||
ls -la /tmp/claude-mem-worker-*.sock
|
||||
```
|
||||
|
||||
**Results:**
|
||||
- No running worker processes detected
|
||||
- No socket files found in /tmp/
|
||||
|
||||
**Status:** Clean slate - no zombie workers or stale sockets
|
||||
|
||||
### 5. Test Plan Creation
|
||||
Created comprehensive test plan document at:
|
||||
`/Users/alexnewman/Scripts/claude-mem/docs/plans/phase0-test-plan.md`
|
||||
|
||||
**Contents:**
|
||||
- Pre-test checklist with current system state
|
||||
- Step-by-step test execution instructions
|
||||
- Expected log sequences for each component
|
||||
- Log collection and filtering commands
|
||||
- Success criteria checklist
|
||||
- Troubleshooting guide
|
||||
|
||||
---
|
||||
|
||||
## Current State of the System
|
||||
|
||||
### Overall Health: READY FOR TESTING
|
||||
The system is in a clean state with no active sessions or running workers. Logging is confirmed to be present in all compiled hook files.
|
||||
|
||||
### Component Status
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| summary-hook.js | READY | Logging present, executable, configured in hooks.json |
|
||||
| context-hook.js | READY | Logging present, executable, configured in hooks.json |
|
||||
| new-hook.js | READY | Executable, configured in hooks.json |
|
||||
| save-hook.js | READY | Executable, configured in hooks.json |
|
||||
| worker.js | READY | Logging present, executable |
|
||||
| Database | READY | Clean, no active sessions |
|
||||
| Worker processes | CLEAN | No running workers |
|
||||
| Socket files | CLEAN | No stale sockets |
|
||||
| Hooks configuration | READY | All lifecycle events properly configured |
|
||||
|
||||
### File Permissions
|
||||
All hook files have execute permissions:
|
||||
```
|
||||
-rwxr-xr-x context-hook.js
|
||||
-rwxr-xr-x new-hook.js
|
||||
-rwxr-xr-x save-hook.js
|
||||
-rwxr-xr-x summary-hook.js
|
||||
-rwxr-xr-x worker.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Issues Found
|
||||
|
||||
### Critical Issue: Zero Summaries Despite Completed Sessions
|
||||
**Severity:** HIGH
|
||||
**Description:** The database shows 22 completed SDK sessions but 0 session_summaries. This suggests the summary generation pipeline may not be working correctly.
|
||||
|
||||
**Possible Causes:**
|
||||
1. Worker may not be receiving FINALIZE messages
|
||||
2. SDK agent may not be responding with expected XML format
|
||||
3. Summary parsing may be failing silently
|
||||
4. Database write may be failing
|
||||
|
||||
**Impact:** This is the core functionality we're testing - summaries must be generated for context to work
|
||||
|
||||
**Next Steps:** The end-to-end test will help diagnose where in the pipeline the failure occurs
|
||||
|
||||
### Minor Issue: Multiple Database Files
|
||||
**Severity:** LOW
|
||||
**Description:** Multiple database files found in ~/.claude-mem/:
|
||||
- memories.db
|
||||
- claude-mem.db
|
||||
- index.db
|
||||
- memory.db
|
||||
- hooks.db
|
||||
|
||||
**Impact:** Potential confusion about which database is active. Code appears to use `~/.claude-mem/claude-mem.db`
|
||||
|
||||
**Recommendation:** Clean up old/unused database files after confirming current one is correct
|
||||
|
||||
---
|
||||
|
||||
## Logging Implementation Verification
|
||||
|
||||
### Summary Hook Logging
|
||||
Located in compiled `summary-hook.js` at multiple points:
|
||||
1. Hook entry point: "Hook fired"
|
||||
2. Session search: "Searching for active SDK session"
|
||||
3. Session found: "Active SDK session found"
|
||||
4. Socket operations: "Attempting to send FINALIZE message", "Socket connection established"
|
||||
5. Completion: "Socket connection closed successfully"
|
||||
|
||||
### Context Hook Logging
|
||||
Located in compiled `context-hook.js` at multiple points:
|
||||
1. Hook entry: "Hook fired with input:"
|
||||
2. Source validation: "Source check passed"
|
||||
3. Project extraction: "Extracted project name"
|
||||
4. Database query: "Querying database for recent summaries..."
|
||||
5. Results: "Database query complete - found X summaries"
|
||||
6. Markdown generation: "Building markdown context from summaries..."
|
||||
7. Completion: "Context hook completed successfully"
|
||||
|
||||
### Worker Logging
|
||||
Located in compiled `worker.js` throughout the lifecycle:
|
||||
1. Instance creation: "Worker instance created"
|
||||
2. Session loading: "Session loaded successfully"
|
||||
3. Socket server: "Socket server started successfully"
|
||||
4. SDK agent: "Starting SDK agent", "SDK session initialized"
|
||||
5. Message handling: "Message received from socket"
|
||||
6. Summary parsing: "Summary parsed successfully", "Storing summary in database"
|
||||
7. Cleanup: "Cleaning up worker resources"
|
||||
|
||||
---
|
||||
|
||||
## Recommendations for Next Steps
|
||||
|
||||
### Immediate: Run End-to-End Test
|
||||
1. Follow the test plan in `phase0-test-plan.md`
|
||||
2. Capture all logs (redirect stderr to file)
|
||||
3. Pay special attention to summary generation
|
||||
4. Verify each success criterion
|
||||
|
||||
### Priority: Investigate Summary Generation Failure
|
||||
The zero summaries issue needs immediate attention:
|
||||
1. Check if workers are being spawned by new-hook.js
|
||||
2. Verify SDK agent responses include expected XML
|
||||
3. Add more detailed logging in summary parsing
|
||||
4. Check database write permissions and constraints
|
||||
|
||||
### Monitoring During Test
|
||||
Watch these areas closely:
|
||||
1. Worker process spawning (should happen in new-hook)
|
||||
2. Socket creation in /tmp/
|
||||
3. FINALIZE message delivery
|
||||
4. Summary parsing and storage
|
||||
5. Context injection in second session
|
||||
|
||||
### After Test
|
||||
1. Document all findings from test execution
|
||||
2. Collect and analyze all logs
|
||||
3. Update code to fix any issues found
|
||||
4. Consider adding automated tests
|
||||
5. Update documentation based on learnings
|
||||
|
||||
---
|
||||
|
||||
## Test Environment Details
|
||||
|
||||
**Operating System:** macOS (Darwin 25.0.0)
|
||||
**Working Directory:** /Users/alexnewman/Scripts/claude-mem
|
||||
**Git Branch:** feature/source-repo
|
||||
**Database Path:** ~/.claude-mem/claude-mem.db
|
||||
**Socket Path Pattern:** /tmp/claude-mem-worker-{sessionId}.sock
|
||||
**Hook Directory:** /Users/alexnewman/Scripts/claude-mem/scripts/hooks/
|
||||
|
||||
**Claude Code Configuration:**
|
||||
- Config directory: ~/.claude/
|
||||
- Project hooks file: /Users/alexnewman/Scripts/claude-mem/hooks/hooks.json
|
||||
- Hooks properly configured for all lifecycle events:
|
||||
- SessionStart: context-hook.js (180s timeout)
|
||||
- UserPromptSubmit: new-hook.js (60s timeout)
|
||||
- PostToolUse: save-hook.js (180s timeout)
|
||||
- Stop: summary-hook.js (60s timeout)
|
||||
|
||||
---
|
||||
|
||||
## Deliverables
|
||||
|
||||
1. **Test Plan Document:** `/Users/alexnewman/Scripts/claude-mem/docs/plans/phase0-test-plan.md`
|
||||
- Comprehensive testing instructions
|
||||
- Success criteria
|
||||
- Log collection commands
|
||||
- Troubleshooting guide
|
||||
|
||||
2. **This Summary Document:** `/Users/alexnewman/Scripts/claude-mem/docs/plans/phase0-task4-summary.md`
|
||||
- Diagnostic results
|
||||
- System state analysis
|
||||
- Issues identified
|
||||
- Recommendations
|
||||
|
||||
3. **Pre-Test Validation:** COMPLETE
|
||||
- Logging verified in all compiled files
|
||||
- Database state documented
|
||||
- Worker state confirmed clean
|
||||
- System ready for testing
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The system is ready for end-to-end testing. All logging has successfully survived the build process and is present in the compiled hook files. The database is in a clean state with no active sessions or zombie workers.
|
||||
|
||||
However, the zero summaries despite 22 completed sessions is a critical issue that the end-to-end test should help diagnose. The test plan provides detailed instructions for execution, log collection, and success verification.
|
||||
|
||||
**Status:** READY TO PROCEED with end-to-end testing
|
||||
|
||||
**Next Action:** Execute the test plan in `phase0-test-plan.md` and collect all logs for analysis
|
||||
@@ -1,310 +0,0 @@
|
||||
# Phase 0 End-to-End Test Plan
|
||||
|
||||
## Overview
|
||||
This test plan validates the complete claude-mem pipeline from session start through context injection in a new session. The test verifies that logging, worker processes, database updates, and context retrieval all function correctly.
|
||||
|
||||
---
|
||||
|
||||
## Section 1: Pre-Test Checklist
|
||||
|
||||
### Current Database State (as of 2025-10-16)
|
||||
- **Database Location:** `~/.claude-mem/claude-mem.db`
|
||||
- **Total SDK Sessions:** 37 sessions recorded
|
||||
- **Active Sessions:** 0 (all sessions properly closed)
|
||||
- **Completed Sessions:** 22
|
||||
- **Session Summaries:** 0 (ISSUE: No summaries despite completed sessions)
|
||||
- **Recent Sessions:**
|
||||
```
|
||||
ID 37: completed at 2025-10-16T21:39:18.888Z, project: claude-mem
|
||||
ID 36: completed at 2025-10-16T21:24:30.850Z, project: claude-mem
|
||||
ID 35: completed at 2025-10-16T21:11:12.929Z, project: claude-mem-test
|
||||
ID 34: completed at 2025-10-16T20:59:43.438Z, project: claude-mem-test
|
||||
ID 33: completed at 2025-10-16T20:55:15.426Z, project: claude-mem-test
|
||||
```
|
||||
|
||||
### Current Worker State
|
||||
- **Running Workers:** None detected
|
||||
- **Socket Files:** No active sockets in /tmp/
|
||||
- **Command Used:** `ps aux | grep claude-mem-worker | grep -v grep`
|
||||
|
||||
### Logging Verification in Compiled Files
|
||||
- **summary-hook.js:** Contains 3 instances of `[claude-mem summary]` logging
|
||||
- **context-hook.js:** Contains 3 instances of `[claude-mem context]` logging
|
||||
- **worker.js:** Contains multiple instances of `[claude-mem worker]` logging
|
||||
- **Status:** CONFIRMED - All logging survived the build process
|
||||
|
||||
### Pre-Test Issues Identified
|
||||
1. Zero session_summaries despite 22 completed SDK sessions - suggests summary generation may not be working
|
||||
2. No active workers or sockets - clean state for testing
|
||||
|
||||
---
|
||||
|
||||
## Section 2: Test Execution Steps
|
||||
|
||||
### Step 1: Clean Slate (Optional - if you want to start fresh)
|
||||
```bash
|
||||
# Backup current database
|
||||
cp ~/.claude-mem/claude-mem.db ~/.claude-mem/backups/claude-mem-backup-$(date +%Y%m%d-%H%M%S).db
|
||||
|
||||
# Optional: Clear old sessions if desired
|
||||
# sqlite3 ~/.claude-mem/claude-mem.db "DELETE FROM sdk_sessions WHERE status = 'completed'"
|
||||
# sqlite3 ~/.claude-mem/claude-mem.db "DELETE FROM session_summaries"
|
||||
```
|
||||
|
||||
### Step 2: Start Claude Code Session 1
|
||||
```bash
|
||||
# Navigate to the test project
|
||||
cd /Users/alexnewman/Scripts/claude-mem
|
||||
|
||||
# Start Claude Code
|
||||
# Logs will show context-hook.js firing
|
||||
# Expected: "[claude-mem context] Hook fired with input:"
|
||||
claude
|
||||
```
|
||||
|
||||
### Step 3: Do Some Work in Session 1
|
||||
Within the Claude Code session, ask Claude to perform meaningful work:
|
||||
```
|
||||
Please help me:
|
||||
1. Read the README.md file
|
||||
2. Analyze the project structure
|
||||
3. List the main TypeScript files in src/
|
||||
4. Create a simple test file at test/example.test.ts with a placeholder test
|
||||
```
|
||||
|
||||
Wait for Claude to complete all tasks.
|
||||
|
||||
### Step 4: Exit Session 1
|
||||
```bash
|
||||
# Type exit or Ctrl+D to end the session
|
||||
# Expected: summary-hook.js will fire
|
||||
# Expected: "[claude-mem summary] Hook fired" message
|
||||
# Expected: Worker will process and generate summary
|
||||
exit
|
||||
```
|
||||
|
||||
### Step 5: Check Database for Summary
|
||||
```bash
|
||||
# Wait 5-10 seconds for worker to complete processing
|
||||
sleep 10
|
||||
|
||||
# Check if a new SDK session was created
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT id, status, started_at, project FROM sdk_sessions ORDER BY started_at_epoch DESC LIMIT 3"
|
||||
|
||||
# Check if summary was generated
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT id, request, completed, created_at FROM session_summaries ORDER BY created_at_epoch DESC LIMIT 1"
|
||||
|
||||
# Check observations
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM observations WHERE sdk_session_id = (SELECT sdk_session_id FROM sdk_sessions ORDER BY started_at_epoch DESC LIMIT 1)"
|
||||
```
|
||||
|
||||
### Step 6: Start Claude Code Session 2
|
||||
```bash
|
||||
# Start a new session in the same project
|
||||
cd /Users/alexnewman/Scripts/claude-mem
|
||||
claude
|
||||
```
|
||||
|
||||
### Step 7: Ask Claude About Previous Session
|
||||
Within Session 2, ask:
|
||||
```
|
||||
What did we work on in the previous session? What files were modified?
|
||||
```
|
||||
|
||||
Claude should reference the previous session context that was injected.
|
||||
|
||||
### Step 8: Collect All Logs
|
||||
```bash
|
||||
# Exit session 2
|
||||
exit
|
||||
|
||||
# Collect logs (location depends on your Claude Code setup)
|
||||
# Check stderr output from both sessions
|
||||
# Filter for claude-mem messages
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Section 3: What to Look For
|
||||
|
||||
### Expected Log Sequence from Summary Hook
|
||||
```
|
||||
[claude-mem summary] Hook fired
|
||||
[claude-mem summary] Searching for active SDK session
|
||||
[claude-mem summary] Active SDK session found
|
||||
[claude-mem summary] Attempting to send FINALIZE message to worker socket
|
||||
[claude-mem summary] Socket connection established, sending message
|
||||
[claude-mem summary] Socket connection closed successfully
|
||||
```
|
||||
|
||||
### Expected Log Sequence from Worker
|
||||
```
|
||||
[claude-mem worker] Worker instance created
|
||||
[claude-mem worker] Worker run() started
|
||||
[claude-mem worker] Session loaded successfully
|
||||
[claude-mem worker] Socket server started successfully
|
||||
[claude-mem worker] Starting SDK agent
|
||||
[claude-mem worker] SDK session initialized
|
||||
[claude-mem worker] SDK agent response received
|
||||
[claude-mem worker] Parsing agent message for observations and summary
|
||||
[claude-mem worker] Summary parsed successfully
|
||||
[claude-mem worker] Storing summary in database
|
||||
[claude-mem worker] Summary stored successfully in database
|
||||
[claude-mem worker] SDK agent completed, marking session as completed
|
||||
[claude-mem worker] Cleaning up worker resources
|
||||
```
|
||||
|
||||
### Expected Log Sequence from Context Hook
|
||||
```
|
||||
[claude-mem context] Hook fired with input:
|
||||
[claude-mem context] Source check passed - proceeding with context load
|
||||
[claude-mem context] Extracted project name: claude-mem from cwd: /Users/alexnewman/Scripts/claude-mem
|
||||
[claude-mem context] Querying database for recent summaries...
|
||||
[claude-mem context] Database query complete - found X summaries
|
||||
[claude-mem context] Building markdown context from summaries...
|
||||
[claude-mem context] Markdown built successfully
|
||||
[claude-mem context] Outputting context to stdout for Claude Code injection
|
||||
[claude-mem context] Context hook completed successfully
|
||||
```
|
||||
|
||||
### How to Verify Summary in Database
|
||||
After Session 1 exits, the summary should contain:
|
||||
- **request:** Description of what was asked
|
||||
- **investigated:** Files/areas examined
|
||||
- **learned:** Key findings
|
||||
- **completed:** What was accomplished
|
||||
- **next_steps:** Recommendations
|
||||
- **files_read:** JSON array of files read
|
||||
- **files_edited:** JSON array of files modified (should include test/example.test.ts)
|
||||
|
||||
### How to Verify Context Was Loaded
|
||||
In Session 2:
|
||||
1. Claude should reference the previous session without being told
|
||||
2. The context-hook.js logs should show summaries were found and loaded
|
||||
3. Claude's response should mention specific files or tasks from Session 1
|
||||
|
||||
---
|
||||
|
||||
## Section 4: Log Collection Commands
|
||||
|
||||
### Filter Logs for Summary Hook
|
||||
```bash
|
||||
# From Claude Code stderr output
|
||||
grep "\[claude-mem summary\]" ~/.claude-code/logs/*.log 2>/dev/null || echo "Check your Claude Code log location"
|
||||
|
||||
# Alternative: redirect stderr during session
|
||||
claude 2>&1 | tee /tmp/claude-session.log
|
||||
# Then: grep "\[claude-mem summary\]" /tmp/claude-session.log
|
||||
```
|
||||
|
||||
### Filter Logs for Context Hook
|
||||
```bash
|
||||
grep "\[claude-mem context\]" /tmp/claude-session.log
|
||||
```
|
||||
|
||||
### Filter Logs for Worker
|
||||
```bash
|
||||
grep "\[claude-mem worker\]" /tmp/claude-session.log
|
||||
```
|
||||
|
||||
### Search for Errors
|
||||
```bash
|
||||
# Search for any errors in the logs
|
||||
grep -i "error\|fail\|exception" /tmp/claude-session.log | grep claude-mem
|
||||
|
||||
# Check for database errors
|
||||
grep "sqlite\|database" /tmp/claude-session.log | grep -i error
|
||||
```
|
||||
|
||||
### Verify Each Step of the Pipeline
|
||||
```bash
|
||||
# 1. Verify session was created
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT * FROM sdk_sessions WHERE id = (SELECT MAX(id) FROM sdk_sessions)"
|
||||
|
||||
# 2. Verify worker socket was created (during session)
|
||||
ls -la /tmp/claude-mem-worker-*.sock
|
||||
|
||||
# 3. Verify observations were recorded
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT type, text FROM observations WHERE sdk_session_id = (SELECT sdk_session_id FROM sdk_sessions ORDER BY started_at_epoch DESC LIMIT 1)"
|
||||
|
||||
# 4. Verify summary was created
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "SELECT request, completed, files_edited FROM session_summaries ORDER BY created_at_epoch DESC LIMIT 1"
|
||||
```
|
||||
|
||||
### Monitor Worker Process
|
||||
```bash
|
||||
# During session, check if worker is running
|
||||
watch -n 1 "ps aux | grep claude-mem-worker | grep -v grep"
|
||||
|
||||
# Check worker socket
|
||||
watch -n 1 "ls -la /tmp/claude-mem-worker-*.sock 2>/dev/null"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Section 5: Success Criteria
|
||||
|
||||
### Must Pass (Critical)
|
||||
- [ ] Session 1 creates an entry in sdk_sessions with status='active'
|
||||
- [ ] Context hook fires at Session 1 start and logs show it ran
|
||||
- [ ] Summary hook fires at Session 1 exit and logs show it ran
|
||||
- [ ] Worker process starts and creates a socket file
|
||||
- [ ] Worker receives FINALIZE message from summary hook
|
||||
- [ ] Summary is successfully parsed and stored in session_summaries table
|
||||
- [ ] Session status changes from 'active' to 'completed'
|
||||
- [ ] Socket file is cleaned up after worker exits
|
||||
- [ ] Session 2 starts and context hook fires
|
||||
- [ ] Context hook finds summaries and injects them as markdown
|
||||
- [ ] Claude references previous session in Session 2
|
||||
|
||||
### Should Pass (Important)
|
||||
- [ ] Observations are recorded in the observations table
|
||||
- [ ] files_read and files_edited are populated in summary
|
||||
- [ ] No error messages in logs
|
||||
- [ ] Worker process exits cleanly
|
||||
- [ ] No zombie workers or stale sockets remain
|
||||
|
||||
### Nice to Have
|
||||
- [ ] All log messages are clear and informative
|
||||
- [ ] Timing is reasonable (summary generation < 30 seconds)
|
||||
- [ ] Multiple sessions can be loaded in context
|
||||
- [ ] Context markdown is well-formatted
|
||||
|
||||
### Known Issues to Monitor
|
||||
- [ ] Zero summaries in current database despite 22 completed sessions - needs investigation
|
||||
- [ ] Verify worker is actually spawned (new-hook.js responsible for this)
|
||||
- [ ] Confirm SDK session ID is properly set
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting Guide
|
||||
|
||||
### If Summary Hook Doesn't Fire
|
||||
1. Check that hooks are properly configured in ~/.claude/hooks.json
|
||||
2. Verify summary-hook.js has execute permissions
|
||||
3. Check Claude Code version supports hooks
|
||||
|
||||
### If Worker Doesn't Start
|
||||
1. Check new-hook.js logs - it should spawn the worker
|
||||
2. Verify worker.js has execute permissions
|
||||
3. Check for port/socket conflicts
|
||||
|
||||
### If Summary Is Not Generated
|
||||
1. Check worker logs for parsing errors
|
||||
2. Verify SDK agent is responding with expected XML format
|
||||
3. Check database write permissions
|
||||
|
||||
### If Context Doesn't Load
|
||||
1. Verify summaries exist in database
|
||||
2. Check context-hook.js logs for query results
|
||||
3. Verify project name extraction is correct
|
||||
|
||||
---
|
||||
|
||||
## Next Steps After Test
|
||||
|
||||
1. If test passes: Proceed to Phase 1 (advanced features)
|
||||
2. If test fails: Collect all logs and diagnostic info
|
||||
3. Document any issues found
|
||||
4. Update code as needed
|
||||
5. Re-run test until success criteria met
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* PM2 Ecosystem Configuration for claude-mem Worker Service
|
||||
*
|
||||
* Usage:
|
||||
* pm2 start ecosystem.config.cjs
|
||||
* pm2 stop claude-mem-worker
|
||||
* pm2 restart claude-mem-worker
|
||||
* pm2 logs claude-mem-worker
|
||||
* pm2 status
|
||||
*/
|
||||
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
// Determine log directory
|
||||
const logDir = path.join(os.homedir(), '.claude-mem', 'logs');
|
||||
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: 'claude-mem-worker',
|
||||
script: './dist/worker-service.cjs',
|
||||
interpreter: 'node',
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: '500M',
|
||||
min_uptime: '10s',
|
||||
max_restarts: 10,
|
||||
restart_delay: 4000,
|
||||
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
CLAUDE_MEM_WORKER_PORT: 0, // Dynamic port allocation
|
||||
FORCE_COLOR: '1'
|
||||
},
|
||||
|
||||
// Logging
|
||||
error_file: path.join(logDir, 'worker-error.log'),
|
||||
out_file: path.join(logDir, 'worker-out.log'),
|
||||
log_date_format: 'YYYY-MM-DD HH:mm:ss.SSS Z',
|
||||
merge_logs: true,
|
||||
|
||||
// Keep logs from last 7 days
|
||||
log_type: 'json',
|
||||
|
||||
// Process management
|
||||
kill_timeout: 5000,
|
||||
listen_timeout: 10000,
|
||||
shutdown_with_message: true,
|
||||
|
||||
// PM2 Plus (optional monitoring)
|
||||
// instance_var: 'INSTANCE_ID',
|
||||
// pmx: true
|
||||
}]
|
||||
};
|
||||
Generated
+4202
File diff suppressed because it is too large
Load Diff
+20
-5
@@ -13,7 +13,7 @@
|
||||
"knowledge-graph",
|
||||
"transcript",
|
||||
"typescript",
|
||||
"bun"
|
||||
"nodejs"
|
||||
],
|
||||
"author": "Alex Newman",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
@@ -31,22 +31,37 @@
|
||||
},
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"bun": ">=1.0.0"
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node scripts/build-hooks.js",
|
||||
"build:hooks": "node scripts/build-hooks.js",
|
||||
"publish:npm": "node scripts/publish.js",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "bun test tests/",
|
||||
"test:context": "echo '{\"session_id\":\"test-'$(date +%s)'\",\"cwd\":\"'$(pwd)'\",\"source\":\"startup\"}' | bun scripts/hooks/context-hook.js 2>/dev/null",
|
||||
"test:context:verbose": "echo '{\"session_id\":\"test-'$(date +%s)'\",\"cwd\":\"'$(pwd)'\",\"source\":\"startup\"}' | bun scripts/hooks/context-hook.js"
|
||||
"test": "node --test tests/",
|
||||
"test:context": "echo '{\"session_id\":\"test-'$(date +%s)'\",\"cwd\":\"'$(pwd)'\",\"source\":\"startup\"}' | node claude-mem/scripts/context-hook.js 2>/dev/null",
|
||||
"test:context:verbose": "echo '{\"session_id\":\"test-'$(date +%s)'\",\"cwd\":\"'$(pwd)'\",\"source\":\"startup\"}' | node claude-mem/scripts/context-hook.js",
|
||||
"worker:start": "pm2 start ecosystem.config.cjs",
|
||||
"worker:stop": "pm2 stop claude-mem-worker",
|
||||
"worker:restart": "pm2 restart claude-mem-worker",
|
||||
"worker:logs": "pm2 logs claude-mem-worker",
|
||||
"worker:status": "pm2 status claude-mem-worker"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.0",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"express": "^4.18.2",
|
||||
"glob": "^11.0.3",
|
||||
"handlebars": "^4.7.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.8",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.0.0",
|
||||
"esbuild": "^0.20.0",
|
||||
"pm2": "^5.3.0",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"files": [
|
||||
"hooks",
|
||||
"scripts",
|
||||
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env node
|
||||
import h from"better-sqlite3";import{join as i,dirname as T,basename as w}from"path";import{homedir as u}from"os";import{existsSync as x,mkdirSync as S}from"fs";var a=process.env.CLAUDE_MEM_DATA_DIR||i(u(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||i(u(),".claude"),I=i(a,"archives"),O=i(a,"logs"),L=i(a,"trash"),y=i(a,"backups"),C=i(a,"settings.json"),m=i(a,"claude-mem.db"),P=i(l,"settings.json"),H=i(l,"commands"),N=i(l,"CLAUDE.md");function _(o){S(o,{recursive:!0})}var c=class{db;constructor(){_(a),this.db=new h(m),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn()}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("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentObservations(e,s=20){return this.db.prepare(`
|
||||
SELECT type, text, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}findActiveSDKSession(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ? AND status = 'active'
|
||||
LIMIT 1
|
||||
`).get(e)||null}findAnySDKSession(e){return this.db.prepare(`
|
||||
SELECT id
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ?
|
||||
LIMIT 1
|
||||
`).get(e)||null}reactivateSession(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'active', user_prompt = ?, worker_port = NULL
|
||||
WHERE id = ?
|
||||
`).run(s,e)}createSDKSession(e,s,t){let r=new Date,n=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}updateSDKSessionId(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ?
|
||||
`).run(s,e)}setWorkerPort(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
`).run(s,e)}getWorkerPort(e){return this.db.prepare(`
|
||||
SELECT worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}storeObservation(e,s,t,r){let n=new Date,d=n.getTime();this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,r,t,n.toISOString(),d)}storeSummary(e,s,t){let r=new Date,n=r.getTime();this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, files_read, files_edited, notes, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request||null,t.investigated||null,t.learned||null,t.completed||null,t.next_steps||null,t.files_read||null,t.files_edited||null,t.notes||null,r.toISOString(),n)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(s.toISOString(),t,e)}markSessionFailed(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(s.toISOString(),t,e)}cleanupOrphanedSessions(){let e=new Date,s=e.getTime();return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};async function g(o){try{console.error("[claude-mem cleanup] Hook fired",{input:o?{session_id:o.session_id,cwd:o.cwd,reason:o.reason}:null}),o||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
|
||||
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=o;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s});let t=new c,r=t.findActiveSDKSession(e);if(r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),t.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{t.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)}t.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 E}from"process";var p="";E.on("data",o=>p+=o);E.on("end",async()=>{try{let o=p.trim()?JSON.parse(p):void 0;await g(o)}catch(o){console.error(`[claude-mem cleanup-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env node
|
||||
import R from"path";import D from"better-sqlite3";import{join as c,dirname as w,basename as v}from"path";import{homedir as f}from"os";import{existsSync as C,mkdirSync as k}from"fs";var l=process.env.CLAUDE_MEM_DATA_DIR||c(f(),".claude-mem"),g=process.env.CLAUDE_CONFIG_DIR||c(f(),".claude"),L=c(l,"archives"),P=c(l,"logs"),N=c(l,"trash"),j=c(l,"backups"),H=c(l,"settings.json"),E=c(l,"claude-mem.db"),U=c(g,"settings.json"),W=c(g,"commands"),$=c(g,"CLAUDE.md");function S(p){k(p,{recursive:!0})}var u=class{db;constructor(){S(l),this.db=new D(E),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn()}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("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,t)}getRecentObservations(e,t=20){return this.db.prepare(`
|
||||
SELECT type, text, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,t)}findActiveSDKSession(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ? AND status = 'active'
|
||||
LIMIT 1
|
||||
`).get(e)||null}findAnySDKSession(e){return this.db.prepare(`
|
||||
SELECT id
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ?
|
||||
LIMIT 1
|
||||
`).get(e)||null}reactivateSession(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'active', user_prompt = ?, worker_port = NULL
|
||||
WHERE id = ?
|
||||
`).run(t,e)}createSDKSession(e,t,s){let o=new Date,i=o.getTime();return this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,t,s,o.toISOString(),i).lastInsertRowid}updateSDKSessionId(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ?
|
||||
`).run(t,e)}setWorkerPort(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
`).run(t,e)}getWorkerPort(e){return this.db.prepare(`
|
||||
SELECT worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}storeObservation(e,t,s,o){let i=new Date,r=i.getTime();this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,o,s,i.toISOString(),r)}storeSummary(e,t,s){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, files_read, files_edited, notes, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,s.request||null,s.investigated||null,s.learned||null,s.completed||null,s.next_steps||null,s.files_read||null,s.files_edited||null,s.notes||null,o.toISOString(),i)}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 = ?
|
||||
`).run(t.toISOString(),s,e)}markSessionFailed(e){let t=new Date,s=t.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(t.toISOString(),s,e)}cleanupOrphanedSessions(){let e=new Date,t=e.getTime();return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function _(p){let e=p?.cwd??process.cwd(),t=e?R.basename(e):"unknown-project",s=new u;try{let o=s.getRecentSummaries(t,5),i=s.getRecentObservations(t,20);if(o.length===0&&i.length===0){console.log(`# Recent Session Context
|
||||
|
||||
No previous sessions found for this project yet.`);return}let r=[];if(r.push("# Recent Session Context"),r.push(""),i.length>0){r.push(`## Recent Observations (${i.length})`),r.push("");let n={};for(let a of i)n[a.type]||(n[a.type]=[]),n[a.type].push({text:a.text,created_at:a.created_at});let d=["feature","bugfix","refactor","discovery","decision"];for(let a of d)if(n[a]&&n[a].length>0){r.push(`### ${a.charAt(0).toUpperCase()+a.slice(1)}s`);for(let b of n[a])r.push(`- ${b.text}`);r.push("")}}if(o.length===0){console.log(r.join(`
|
||||
`));return}r.push("## Recent Sessions"),r.push("");let m=o.length===1?"session":"sessions";r.push(`Showing last ${o.length} ${m} for **${t}**:`),r.push("");for(let n of o){if(r.push("---"),r.push(""),n.request&&r.push(`**Request:** ${n.request}`),n.completed&&r.push(`**Completed:** ${n.completed}`),n.learned&&r.push(`**Learned:** ${n.learned}`),n.next_steps&&r.push(`**Next Steps:** ${n.next_steps}`),n.files_read)try{let d=JSON.parse(n.files_read);Array.isArray(d)&&d.length>0&&r.push(`**Files Read:** ${d.join(", ")}`)}catch{n.files_read.trim()&&r.push(`**Files Read:** ${n.files_read}`)}if(n.files_edited)try{let d=JSON.parse(n.files_edited);Array.isArray(d)&&d.length>0&&r.push(`**Files Edited:** ${d.join(", ")}`)}catch{n.files_edited.trim()&&r.push(`**Files Edited:** ${n.files_edited}`)}r.push(`**Date:** ${n.created_at.split("T")[0]}`),r.push("")}console.log(r.join(`
|
||||
`))}finally{s.close()}}import{stdin as h}from"process";try{if(h.isTTY)_();else{let p="";h.on("data",e=>p+=e),h.on("end",()=>{let e=p.trim()?JSON.parse(p):void 0;_(e)})}}catch(p){console.error(`[claude-mem context-hook error: ${p.message}]`),process.exit(0)}
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env node
|
||||
import T from"path";import D from"better-sqlite3";import{join as a,dirname as v,basename as P}from"path";import{homedir as k}from"os";import{existsSync as U,mkdirSync as w}from"fs";var p=process.env.CLAUDE_MEM_DATA_DIR||a(k(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||a(k(),".claude"),L=a(p,"archives"),N=a(p,"logs"),W=a(p,"trash"),j=a(p,"backups"),M=a(p,"settings.json"),f=a(p,"claude-mem.db"),F=a(l,"settings.json"),$=a(l,"commands"),q=a(l,"CLAUDE.md");function E(r){w(r,{recursive:!0})}var d=class{db;constructor(){E(p),this.db=new D(f),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn()}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("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,t)}getRecentObservations(e,t=20){return this.db.prepare(`
|
||||
SELECT type, text, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,t)}findActiveSDKSession(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ? AND status = 'active'
|
||||
LIMIT 1
|
||||
`).get(e)||null}findAnySDKSession(e){return this.db.prepare(`
|
||||
SELECT id
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ?
|
||||
LIMIT 1
|
||||
`).get(e)||null}reactivateSession(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'active', user_prompt = ?, worker_port = NULL
|
||||
WHERE id = ?
|
||||
`).run(t,e)}createSDKSession(e,t,s){let o=new Date,n=o.getTime();return this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,t,s,o.toISOString(),n).lastInsertRowid}updateSDKSessionId(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ?
|
||||
`).run(t,e)}setWorkerPort(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
`).run(t,e)}getWorkerPort(e){return this.db.prepare(`
|
||||
SELECT worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}storeObservation(e,t,s,o){let n=new Date,i=n.getTime();this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,o,s,n.toISOString(),i)}storeSummary(e,t,s){let o=new Date,n=o.getTime();this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, files_read, files_edited, notes, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,s.request||null,s.investigated||null,s.learned||null,s.completed||null,s.next_steps||null,s.files_read||null,s.files_edited||null,s.notes||null,o.toISOString(),n)}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 = ?
|
||||
`).run(t.toISOString(),s,e)}markSessionFailed(e){let t=new Date,s=t.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(t.toISOString(),s,e)}cleanupOrphanedSessions(){let e=new Date,t=e.getTime();return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function R(r,e,t){return r==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:t.reason||"Pre-compact operation failed",suppressOutput:!0}:r==="SessionStart"?e&&t.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:t.context}}:{continue:!0,suppressOutput:!0}:r==="UserPromptSubmit"||r==="PostToolUse"?{continue:!0,suppressOutput:!0}:r==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...t.reason&&!e?{stopReason:t.reason}:{}}}function u(r,e,t={}){let s=R(r,e,t);return JSON.stringify(s)}async function A(){let{readFileSync:r,existsSync:e}=await import("fs"),{join:t}=await import("path"),{homedir:s}=await import("os"),o=t(s(),".claude-mem","worker.port");if(!e(o))return null;try{let n=r(o,"utf8").trim();return parseInt(n,10)}catch{return null}}async function b(r){if(!r)throw new Error("newHook requires input");let{session_id:e,cwd:t,prompt:s}=r,o=T.basename(t),n=new d;try{let i=n.findActiveSDKSession(e),c;if(i){c=i.id,console.log(u("UserPromptSubmit",!0));return}let g=n.findAnySDKSession(e);g?(c=g.id,n.reactivateSession(c,s),console.error(`[new-hook] Reactivated session ${c} for Claude session ${e}`)):(c=n.createSDKSession(e,o,s),console.error(`[new-hook] Created new session ${c} for Claude session ${e}`));let _=await A();if(!_){console.error("[new-hook] Worker service not running. Start with: npm run worker:start"),console.log(u("UserPromptSubmit",!0));return}let S=await fetch(`http://127.0.0.1:${_}/sessions/${c}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:o,userPrompt:s}),signal:AbortSignal.timeout(5e3)});S.ok||console.error("[new-hook] Failed to init session:",await S.text()),console.log(u("UserPromptSubmit",!0))}catch(i){console.error("[new-hook] FATAL ERROR:",i.message),console.error("[new-hook] Stack:",i.stack),console.error("[new-hook] Full error:",JSON.stringify(i,Object.getOwnPropertyNames(i))),console.log(u("UserPromptSubmit",!0))}finally{n.close()}}import{stdin as h}from"process";var m="";h.on("data",r=>m+=r);h.on("end",async()=>{try{let r=m.trim()?JSON.parse(m):void 0;await b(r)}catch(r){console.error(`[claude-mem new-hook error: ${r.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env node
|
||||
import b from"better-sqlite3";import{join as n,dirname as w,basename as O}from"path";import{homedir as g}from"os";import{existsSync as P,mkdirSync as f}from"fs";var c=process.env.CLAUDE_MEM_DATA_DIR||n(g(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||n(g(),".claude"),y=n(c,"archives"),H=n(c,"logs"),C=n(c,"trash"),L=n(c,"backups"),U=n(c,"settings.json"),_=n(c,"claude-mem.db"),N=n(l,"settings.json"),W=n(l,"commands"),M=n(l,"CLAUDE.md");function S(o){f(o,{recursive:!0})}var d=class{db;constructor(){S(c),this.db=new b(_),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn()}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("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,t)}getRecentObservations(e,t=20){return this.db.prepare(`
|
||||
SELECT type, text, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,t)}findActiveSDKSession(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ? AND status = 'active'
|
||||
LIMIT 1
|
||||
`).get(e)||null}findAnySDKSession(e){return this.db.prepare(`
|
||||
SELECT id
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ?
|
||||
LIMIT 1
|
||||
`).get(e)||null}reactivateSession(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'active', user_prompt = ?, worker_port = NULL
|
||||
WHERE id = ?
|
||||
`).run(t,e)}createSDKSession(e,t,s){let r=new Date,i=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,t,s,r.toISOString(),i).lastInsertRowid}updateSDKSessionId(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ?
|
||||
`).run(t,e)}setWorkerPort(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
`).run(t,e)}getWorkerPort(e){return this.db.prepare(`
|
||||
SELECT worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}storeObservation(e,t,s,r){let i=new Date,a=i.getTime();this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,r,s,i.toISOString(),a)}storeSummary(e,t,s){let r=new Date,i=r.getTime();this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, files_read, files_edited, notes, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,s.request||null,s.investigated||null,s.learned||null,s.completed||null,s.next_steps||null,s.files_read||null,s.files_edited||null,s.notes||null,r.toISOString(),i)}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 = ?
|
||||
`).run(t.toISOString(),s,e)}markSessionFailed(e){let t=new Date,s=t.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(t.toISOString(),s,e)}cleanupOrphanedSessions(){let e=new Date,t=e.getTime();return this.db.prepare(`
|
||||
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(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 u(o,e,t={}){let s=h(o,e,t);return JSON.stringify(s)}var T=new Set(["TodoWrite","ListMcpResourcesTool"]);async function E(o){if(!o)throw new Error("saveHook requires input");let{session_id:e,tool_name:t,tool_input:s,tool_output:r}=o;if(T.has(t)){console.log(u("PostToolUse",!0));return}let i=new d,a=i.findActiveSDKSession(e);if(i.close(),!a){console.log(u("PostToolUse",!0));return}if(!a.worker_port){console.error("[save-hook] No worker port for session",a.id),console.log(u("PostToolUse",!0));return}try{let p=await fetch(`http://127.0.0.1:${a.worker_port}/sessions/${a.id}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:t,tool_input:JSON.stringify(s),tool_output:JSON.stringify(r)}),signal:AbortSignal.timeout(2e3)});p.ok||console.error("[save-hook] Failed to send observation:",await p.text())}catch(p){console.error("[save-hook] Error:",p.message)}finally{console.log(u("PostToolUse",!0))}}import{stdin as k}from"process";var m="";k.on("data",o=>m+=o);k.on("end",async()=>{try{let o=m.trim()?JSON.parse(m):void 0;await E(o)}catch(o){console.error(`[claude-mem save-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env node
|
||||
import b from"better-sqlite3";import{join as n,dirname as w,basename as A}from"path";import{homedir as m}from"os";import{existsSync as v,mkdirSync as f}from"fs";var i=process.env.CLAUDE_MEM_DATA_DIR||n(m(),".claude-mem"),d=process.env.CLAUDE_CONFIG_DIR||n(m(),".claude"),P=n(i,"archives"),y=n(i,"logs"),H=n(i,"trash"),C=n(i,"backups"),L=n(i,"settings.json"),g=n(i,"claude-mem.db"),U=n(d,"settings.json"),N=n(d,"commands"),W=n(d,"CLAUDE.md");function _(r){f(r,{recursive:!0})}var c=class{db;constructor(){_(i),this.db=new b(g),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn()}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("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,t)}getRecentObservations(e,t=20){return this.db.prepare(`
|
||||
SELECT type, text, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,t)}findActiveSDKSession(e){return this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ? AND status = 'active'
|
||||
LIMIT 1
|
||||
`).get(e)||null}findAnySDKSession(e){return this.db.prepare(`
|
||||
SELECT id
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ?
|
||||
LIMIT 1
|
||||
`).get(e)||null}reactivateSession(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'active', user_prompt = ?, worker_port = NULL
|
||||
WHERE id = ?
|
||||
`).run(t,e)}createSDKSession(e,t,s){let o=new Date,a=o.getTime();return this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,t,s,o.toISOString(),a).lastInsertRowid}updateSDKSessionId(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ?
|
||||
`).run(t,e)}setWorkerPort(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
`).run(t,e)}getWorkerPort(e){return this.db.prepare(`
|
||||
SELECT worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}storeObservation(e,t,s,o){let a=new Date,u=a.getTime();this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,o,s,a.toISOString(),u)}storeSummary(e,t,s){let o=new Date,a=o.getTime();this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, files_read, files_edited, notes, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,s.request||null,s.investigated||null,s.learned||null,s.completed||null,s.next_steps||null,s.files_read||null,s.files_edited||null,s.notes||null,o.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 = ?
|
||||
`).run(t.toISOString(),s,e)}markSessionFailed(e){let t=new Date,s=t.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(t.toISOString(),s,e)}cleanupOrphanedSessions(){let e=new Date,t=e.getTime();return this.db.prepare(`
|
||||
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(r,e,t){return r==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:t.reason||"Pre-compact operation failed",suppressOutput:!0}:r==="SessionStart"?e&&t.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:t.context}}:{continue:!0,suppressOutput:!0}:r==="UserPromptSubmit"||r==="PostToolUse"?{continue:!0,suppressOutput:!0}:r==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...t.reason&&!e?{stopReason:t.reason}:{}}}function p(r,e,t={}){let s=h(r,e,t);return JSON.stringify(s)}async function S(r){if(!r)throw new Error("summaryHook requires input");let{session_id:e}=r,t=new c,s=t.findActiveSDKSession(e);if(t.close(),!s){console.log(p("Stop",!0));return}if(!s.worker_port){console.error("[summary-hook] No worker port for session",s.id),console.log(p("Stop",!0));return}try{let o=await fetch(`http://127.0.0.1:${s.worker_port}/sessions/${s.id}/finalize`,{method:"POST",headers:{"Content-Type":"application/json"},signal:AbortSignal.timeout(2e3)});o.ok||console.error("[summary-hook] Failed to finalize:",await o.text())}catch(o){console.error("[summary-hook] Error:",o.message)}finally{console.log(p("Stop",!0))}}import{stdin as E}from"process";var l="";E.on("data",r=>l+=r);E.on("end",async()=>{try{let r=l.trim()?JSON.parse(l):void 0;await S(r)}catch(r){console.error(`[claude-mem summary-hook error: ${r.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
+69
-44
@@ -2,27 +2,31 @@
|
||||
|
||||
/**
|
||||
* Build script for claude-mem hooks
|
||||
* Bundles TypeScript hooks into individual standalone executables
|
||||
* Bundles TypeScript hooks into individual standalone executables using esbuild
|
||||
*/
|
||||
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { build } from 'esbuild';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const HOOKS = [
|
||||
{ name: 'context-hook', source: 'src/bin/hooks/context-hook.ts' },
|
||||
{ name: 'new-hook', source: 'src/bin/hooks/new-hook.ts' },
|
||||
{ name: 'save-hook', source: 'src/bin/hooks/save-hook.ts' },
|
||||
{ name: 'summary-hook', source: 'src/bin/hooks/summary-hook.ts' },
|
||||
{ name: 'cleanup-hook', source: 'src/bin/hooks/cleanup-hook.ts' },
|
||||
{ name: 'worker', source: 'src/bin/hooks/worker.ts' }
|
||||
{ name: 'cleanup-hook', source: 'src/bin/hooks/cleanup-hook.ts' }
|
||||
];
|
||||
|
||||
const WORKER_SERVICE = {
|
||||
name: 'worker-service',
|
||||
source: 'src/services/worker-service.ts'
|
||||
};
|
||||
|
||||
async function buildHooks() {
|
||||
console.log('🔨 Building claude-mem hooks...\n');
|
||||
console.log('🔨 Building claude-mem hooks and worker service...\n');
|
||||
|
||||
try {
|
||||
// Read version from package.json
|
||||
@@ -30,46 +34,65 @@ async function buildHooks() {
|
||||
const version = packageJson.version;
|
||||
console.log(`📌 Version: ${version}`);
|
||||
|
||||
// Check if bun is installed
|
||||
try {
|
||||
await execAsync('bun --version');
|
||||
console.log('✓ Bun detected');
|
||||
} catch {
|
||||
console.error('❌ Bun is not installed. Please install it from https://bun.sh');
|
||||
process.exit(1);
|
||||
}
|
||||
// Create output directories
|
||||
console.log('\n📦 Preparing output directories...');
|
||||
const hooksDir = 'plugin/scripts';
|
||||
const distDir = 'dist';
|
||||
|
||||
// Create scripts directory
|
||||
console.log('\n📦 Preparing scripts directory...');
|
||||
const scriptsDir = 'claude-mem/scripts';
|
||||
if (!fs.existsSync(scriptsDir)) {
|
||||
fs.mkdirSync(scriptsDir, { recursive: true });
|
||||
if (!fs.existsSync(hooksDir)) {
|
||||
fs.mkdirSync(hooksDir, { recursive: true });
|
||||
}
|
||||
console.log('✓ Scripts directory ready');
|
||||
if (!fs.existsSync(distDir)) {
|
||||
fs.mkdirSync(distDir, { recursive: true });
|
||||
}
|
||||
console.log('✓ Output directories ready');
|
||||
|
||||
// Build worker service
|
||||
console.log(`\n🔧 Building worker service...`);
|
||||
await build({
|
||||
entryPoints: [WORKER_SERVICE.source],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
format: 'cjs',
|
||||
outfile: `${distDir}/${WORKER_SERVICE.name}.cjs`,
|
||||
minify: true,
|
||||
external: ['better-sqlite3'],
|
||||
define: {
|
||||
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
||||
},
|
||||
banner: {
|
||||
js: '#!/usr/bin/env node'
|
||||
}
|
||||
});
|
||||
|
||||
// Make worker service executable
|
||||
fs.chmodSync(`${distDir}/${WORKER_SERVICE.name}.cjs`, 0o755);
|
||||
const workerStats = fs.statSync(`${distDir}/${WORKER_SERVICE.name}.cjs`);
|
||||
console.log(`✓ worker-service built (${(workerStats.size / 1024).toFixed(2)} KB)`);
|
||||
|
||||
// Build each hook
|
||||
for (const hook of HOOKS) {
|
||||
console.log(`\n🔧 Building ${hook.name}...`);
|
||||
|
||||
const outfile = `${scriptsDir}/${hook.name}.js`;
|
||||
const buildCommand = [
|
||||
'bun build',
|
||||
hook.source,
|
||||
'--target=bun',
|
||||
`--outfile=${outfile}`,
|
||||
'--minify',
|
||||
'--external bun:sqlite',
|
||||
`--define __DEFAULT_PACKAGE_VERSION__='"${version}"'`
|
||||
].join(' ');
|
||||
const outfile = `${hooksDir}/${hook.name}.js`;
|
||||
|
||||
const { stdout, stderr } = await execAsync(buildCommand);
|
||||
if (stdout) console.log(stdout);
|
||||
if (stderr && !stderr.includes('warn')) console.error(stderr);
|
||||
|
||||
// Add shebang
|
||||
let content = fs.readFileSync(outfile, 'utf-8');
|
||||
content = content.replace(/^#!.*\n/gm, '');
|
||||
fs.writeFileSync(outfile, `#!/usr/bin/env bun\n${content}`);
|
||||
await build({
|
||||
entryPoints: [hook.source],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
format: 'esm',
|
||||
outfile,
|
||||
minify: true,
|
||||
external: ['better-sqlite3'],
|
||||
define: {
|
||||
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
||||
},
|
||||
banner: {
|
||||
js: '#!/usr/bin/env node'
|
||||
}
|
||||
});
|
||||
|
||||
// Make executable
|
||||
fs.chmodSync(outfile, 0o755);
|
||||
@@ -80,13 +103,15 @@ async function buildHooks() {
|
||||
console.log(`✓ ${hook.name} built (${sizeInKB} KB)`);
|
||||
}
|
||||
|
||||
console.log('\n✅ All hooks built successfully!');
|
||||
console.log(` Output: ${scriptsDir}/`);
|
||||
console.log('\n✅ All hooks and worker service built successfully!');
|
||||
console.log(` Hooks: ${hooksDir}/`);
|
||||
console.log(` Worker: ${distDir}/worker-service.cjs`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Hook build failed:', error.message);
|
||||
if (error.stderr) {
|
||||
console.error('\nError details:', error.stderr);
|
||||
console.error('\n❌ Build failed:', error.message);
|
||||
if (error.errors) {
|
||||
console.error('\nBuild errors:');
|
||||
error.errors.forEach(err => console.error(` - ${err.text}`));
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Cleanup Hook Entry Point - SessionEnd
|
||||
@@ -6,15 +5,18 @@
|
||||
*/
|
||||
|
||||
import { cleanupHook } from '../../hooks/cleanup.js';
|
||||
import { stdin } from 'process';
|
||||
|
||||
// Read input from stdin
|
||||
const input = await Bun.stdin.text();
|
||||
|
||||
try {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
cleanupHook(parsed);
|
||||
} catch (error: any) {
|
||||
console.error(`[claude-mem cleanup-hook error: ${error.message}]`);
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
process.exit(0);
|
||||
}
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', async () => {
|
||||
try {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
await cleanupHook(parsed);
|
||||
} catch (error: any) {
|
||||
console.error(`[claude-mem cleanup-hook error: ${error.message}]`);
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Context Hook Entry Point - SessionStart
|
||||
@@ -6,14 +5,18 @@
|
||||
*/
|
||||
|
||||
import { contextHook } from '../../hooks/context.js';
|
||||
import { stdin } from 'process';
|
||||
|
||||
try {
|
||||
if (process.stdin.isTTY) {
|
||||
if (stdin.isTTY) {
|
||||
contextHook();
|
||||
} else {
|
||||
const input = await Bun.stdin.text();
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
contextHook(parsed);
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', () => {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
contextHook(parsed);
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`[claude-mem context-hook error: ${error.message}]`);
|
||||
|
||||
+13
-11
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* New Hook Entry Point - UserPromptSubmit
|
||||
@@ -6,15 +5,18 @@
|
||||
*/
|
||||
|
||||
import { newHook } from '../../hooks/new.js';
|
||||
import { stdin } from 'process';
|
||||
|
||||
// Read input from stdin
|
||||
const input = await Bun.stdin.text();
|
||||
|
||||
try {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
newHook(parsed);
|
||||
} catch (error: any) {
|
||||
console.error(`[claude-mem new-hook error: ${error.message}]`);
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
process.exit(0);
|
||||
}
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', async () => {
|
||||
try {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
await newHook(parsed);
|
||||
} catch (error: any) {
|
||||
console.error(`[claude-mem new-hook error: ${error.message}]`);
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
+13
-11
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Save Hook Entry Point - PostToolUse
|
||||
@@ -6,15 +5,18 @@
|
||||
*/
|
||||
|
||||
import { saveHook } from '../../hooks/save.js';
|
||||
import { stdin } from 'process';
|
||||
|
||||
// Read input from stdin
|
||||
const input = await Bun.stdin.text();
|
||||
|
||||
try {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
saveHook(parsed);
|
||||
} catch (error: any) {
|
||||
console.error(`[claude-mem save-hook error: ${error.message}]`);
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
process.exit(0);
|
||||
}
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', async () => {
|
||||
try {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
await saveHook(parsed);
|
||||
} catch (error: any) {
|
||||
console.error(`[claude-mem save-hook error: ${error.message}]`);
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Summary Hook Entry Point - Stop
|
||||
@@ -6,15 +5,18 @@
|
||||
*/
|
||||
|
||||
import { summaryHook } from '../../hooks/summary.js';
|
||||
import { stdin } from 'process';
|
||||
|
||||
// Read input from stdin
|
||||
const input = await Bun.stdin.text();
|
||||
|
||||
try {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
summaryHook(parsed);
|
||||
} catch (error: any) {
|
||||
console.error(`[claude-mem summary-hook error: ${error.message}]`);
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
process.exit(0);
|
||||
}
|
||||
let input = '';
|
||||
stdin.on('data', (chunk) => input += chunk);
|
||||
stdin.on('end', async () => {
|
||||
try {
|
||||
const parsed = input.trim() ? JSON.parse(input) : undefined;
|
||||
await summaryHook(parsed);
|
||||
} catch (error: any) {
|
||||
console.error(`[claude-mem summary-hook error: ${error.message}]`);
|
||||
console.log('{"continue": true, "suppressOutput": true}');
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
+24
-48
@@ -1,6 +1,4 @@
|
||||
import { existsSync, unlinkSync } from 'fs';
|
||||
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
|
||||
import { getWorkerSocketPath } from '../shared/paths.js';
|
||||
|
||||
export interface SessionEndInput {
|
||||
session_id: string;
|
||||
@@ -12,15 +10,14 @@ export interface SessionEndInput {
|
||||
|
||||
/**
|
||||
* Cleanup Hook - SessionEnd
|
||||
* Cleans up worker process and marks session as terminated
|
||||
* Cleans up worker session via HTTP DELETE
|
||||
*
|
||||
* This hook runs when a Claude Code session ends. It:
|
||||
* 1. Finds active SDK session for this Claude session
|
||||
* 2. Terminates worker process if still running
|
||||
* 3. Removes stale socket file
|
||||
* 4. Marks session as failed (since no Stop hook completed it)
|
||||
* 2. Sends DELETE request to worker service
|
||||
* 3. Marks session as failed if not already completed
|
||||
*/
|
||||
export function cleanupHook(input?: SessionEndInput): void {
|
||||
export async function cleanupHook(input?: SessionEndInput): Promise<void> {
|
||||
try {
|
||||
// Log hook entry point
|
||||
console.error('[claude-mem cleanup] Hook fired', {
|
||||
@@ -63,57 +60,36 @@ export function cleanupHook(input?: SessionEndInput): void {
|
||||
console.error('[claude-mem cleanup] Active SDK session found', {
|
||||
session_id: session.id,
|
||||
sdk_session_id: session.sdk_session_id,
|
||||
project: session.project
|
||||
project: session.project,
|
||||
worker_port: session.worker_port
|
||||
});
|
||||
|
||||
// Get worker PID and socket path
|
||||
const socketPath = getWorkerSocketPath(session.id);
|
||||
// 1. Delete session via HTTP
|
||||
if (session.worker_port) {
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}`, {
|
||||
method: 'DELETE',
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
// 1. Kill worker process if it exists
|
||||
try {
|
||||
// Try to read PID from socket file existence
|
||||
if (existsSync(socketPath)) {
|
||||
console.error('[claude-mem cleanup] Socket file exists, attempting cleanup', { socketPath });
|
||||
|
||||
// Remove socket file
|
||||
try {
|
||||
unlinkSync(socketPath);
|
||||
console.error('[claude-mem cleanup] Socket file removed successfully', { socketPath });
|
||||
} catch (unlinkErr: any) {
|
||||
console.error('[claude-mem cleanup] Failed to remove socket file', {
|
||||
error: unlinkErr.message,
|
||||
socketPath
|
||||
});
|
||||
if (response.ok) {
|
||||
console.error('[claude-mem cleanup] Session deleted successfully via HTTP');
|
||||
} else {
|
||||
console.error('[claude-mem cleanup] Failed to delete session:', await response.text());
|
||||
}
|
||||
} else {
|
||||
console.error('[claude-mem cleanup] Socket file does not exist', { socketPath });
|
||||
} catch (error: any) {
|
||||
console.error('[claude-mem cleanup] HTTP DELETE error:', error.message);
|
||||
}
|
||||
|
||||
// Note: We don't kill the worker process here because:
|
||||
// 1. Workers have a 2-hour watchdog timer that will kill them automatically
|
||||
// 2. Killing by PID is fragile (PID might be reused)
|
||||
// 3. The worker will exit on its own when it can't reach the socket
|
||||
// We just clean up the socket file to prevent stale socket issues
|
||||
|
||||
} catch (cleanupErr: any) {
|
||||
console.error('[claude-mem cleanup] Error during cleanup', {
|
||||
error: cleanupErr.message,
|
||||
stack: cleanupErr.stack
|
||||
});
|
||||
} else {
|
||||
console.error('[claude-mem cleanup] No worker port, cannot send DELETE request');
|
||||
}
|
||||
|
||||
// 2. Mark session as failed (since Stop hook didn't complete it)
|
||||
// 2. Mark session as failed in DB (if not already completed)
|
||||
try {
|
||||
db.markSessionFailed(session.id);
|
||||
console.error('[claude-mem cleanup] Session marked as failed', {
|
||||
session_id: session.id,
|
||||
reason: 'SessionEnd hook - session terminated without completion'
|
||||
});
|
||||
console.error('[claude-mem cleanup] Session marked as failed in database');
|
||||
} catch (markErr: any) {
|
||||
console.error('[claude-mem cleanup] Failed to mark session as failed', {
|
||||
error: markErr.message,
|
||||
session_id: session.id
|
||||
});
|
||||
console.error('[claude-mem cleanup] Failed to mark session as failed:', markErr);
|
||||
}
|
||||
|
||||
db.close();
|
||||
|
||||
+35
-1
@@ -24,8 +24,9 @@ export function contextHook(input?: SessionStartInput): void {
|
||||
|
||||
try {
|
||||
const summaries = db.getRecentSummaries(project, 5);
|
||||
const observations = db.getRecentObservations(project, 20);
|
||||
|
||||
if (summaries.length === 0) {
|
||||
if (summaries.length === 0 && observations.length === 0) {
|
||||
// Output directly to stdout for injection into context
|
||||
console.log('# Recent Session Context\n\nNo previous sessions found for this project yet.');
|
||||
return;
|
||||
@@ -34,6 +35,39 @@ export function contextHook(input?: SessionStartInput): void {
|
||||
const output: string[] = [];
|
||||
output.push('# Recent Session Context');
|
||||
output.push('');
|
||||
|
||||
// Show observations first
|
||||
if (observations.length > 0) {
|
||||
output.push(`## Recent Observations (${observations.length})`);
|
||||
output.push('');
|
||||
|
||||
// Group observations by type
|
||||
const byType: Record<string, Array<{text: string; created_at: string}>> = {};
|
||||
for (const obs of observations) {
|
||||
if (!byType[obs.type]) byType[obs.type] = [];
|
||||
byType[obs.type].push({ text: obs.text, created_at: obs.created_at });
|
||||
}
|
||||
|
||||
// Display each type
|
||||
const typeOrder = ['feature', 'bugfix', 'refactor', 'discovery', 'decision'];
|
||||
for (const type of typeOrder) {
|
||||
if (byType[type] && byType[type].length > 0) {
|
||||
output.push(`### ${type.charAt(0).toUpperCase() + type.slice(1)}s`);
|
||||
for (const obs of byType[type]) {
|
||||
output.push(`- ${obs.text}`);
|
||||
}
|
||||
output.push('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (summaries.length === 0) {
|
||||
console.log(output.join('\n'));
|
||||
return;
|
||||
}
|
||||
|
||||
output.push('## Recent Sessions');
|
||||
output.push('');
|
||||
const sessionWord = summaries.length === 1 ? 'session' : 'sessions';
|
||||
output.push(`Showing last ${summaries.length} ${sessionWord} for **${project}**:`);
|
||||
output.push('');
|
||||
|
||||
+63
-15
@@ -1,4 +1,3 @@
|
||||
import { spawn } from 'child_process';
|
||||
import path from 'path';
|
||||
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
@@ -11,10 +10,32 @@ export interface UserPromptSubmitInput {
|
||||
}
|
||||
|
||||
/**
|
||||
* New Hook - UserPromptSubmit
|
||||
* Initializes SDK memory session in background
|
||||
* Get worker service port from file
|
||||
*/
|
||||
export function newHook(input?: UserPromptSubmitInput): void {
|
||||
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');
|
||||
|
||||
if (!existsSync(portFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const portStr = readFileSync(portFile, 'utf8').trim();
|
||||
return parseInt(portStr, 10);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* New Hook - UserPromptSubmit
|
||||
* Initializes SDK memory session via HTTP POST to worker service
|
||||
*/
|
||||
export async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
||||
if (!input) {
|
||||
throw new Error('newHook requires input');
|
||||
}
|
||||
@@ -24,30 +45,57 @@ export function newHook(input?: UserPromptSubmitInput): void {
|
||||
const db = new HooksDatabase();
|
||||
|
||||
try {
|
||||
const existing = db.findActiveSDKSession(session_id);
|
||||
// Check for any existing session (active, failed, or completed)
|
||||
let existing = db.findActiveSDKSession(session_id);
|
||||
let sessionDbId: number;
|
||||
|
||||
if (existing) {
|
||||
// Session already active, just continue
|
||||
sessionDbId = existing.id;
|
||||
console.log(createHookResponse('UserPromptSubmit', true));
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionId = db.createSDKSession(session_id, project, prompt);
|
||||
// Check for inactive sessions we can reuse
|
||||
const inactive = db.findAnySDKSession(session_id);
|
||||
|
||||
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
|
||||
|
||||
if (!pluginRoot) {
|
||||
throw new Error('CLAUDE_PLUGIN_ROOT not set');
|
||||
if (inactive) {
|
||||
// Reactivate the existing session
|
||||
sessionDbId = inactive.id;
|
||||
db.reactivateSession(sessionDbId, prompt);
|
||||
console.error(`[new-hook] Reactivated session ${sessionDbId} for Claude session ${session_id}`);
|
||||
} else {
|
||||
// Create new session
|
||||
sessionDbId = db.createSDKSession(session_id, project, prompt);
|
||||
console.error(`[new-hook] Created new session ${sessionDbId} for Claude session ${session_id}`);
|
||||
}
|
||||
|
||||
const workerPath = path.join(pluginRoot, 'scripts', 'hooks', 'worker.js');
|
||||
const child = spawn('bun', [workerPath, sessionId.toString()], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
// Find worker service port
|
||||
const port = await getWorkerPort();
|
||||
if (!port) {
|
||||
console.error('[new-hook] Worker service not running. Start with: npm run worker:start');
|
||||
console.log(createHookResponse('UserPromptSubmit', true)); // Don't block Claude
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize session via HTTP
|
||||
const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/init`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ project, userPrompt: prompt }),
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
child.unref();
|
||||
if (!response.ok) {
|
||||
console.error('[new-hook] Failed to init session:', await response.text());
|
||||
}
|
||||
|
||||
console.log(createHookResponse('UserPromptSubmit', true));
|
||||
} catch (error: any) {
|
||||
console.error('[new-hook] FATAL ERROR:', error.message);
|
||||
console.error('[new-hook] Stack:', error.stack);
|
||||
console.error('[new-hook] Full error:', JSON.stringify(error, Object.getOwnPropertyNames(error)));
|
||||
console.log(createHookResponse('UserPromptSubmit', true)); // Don't block Claude
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
|
||||
+26
-26
@@ -1,6 +1,4 @@
|
||||
import net from 'net';
|
||||
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
|
||||
import { getWorkerSocketPath } from '../shared/paths.js';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
|
||||
export interface PostToolUseInput {
|
||||
@@ -20,9 +18,9 @@ const SKIP_TOOLS = new Set([
|
||||
|
||||
/**
|
||||
* Save Hook - PostToolUse
|
||||
* Sends tool observations to worker via Unix socket
|
||||
* Sends tool observations to worker via HTTP POST
|
||||
*/
|
||||
export function saveHook(input?: PostToolUseInput): void {
|
||||
export async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
if (!input) {
|
||||
throw new Error('saveHook requires input');
|
||||
}
|
||||
@@ -43,28 +41,30 @@ export function saveHook(input?: PostToolUseInput): void {
|
||||
return;
|
||||
}
|
||||
|
||||
const socketPath = getWorkerSocketPath(session.id);
|
||||
const message = {
|
||||
type: 'observation',
|
||||
tool_name,
|
||||
tool_input: JSON.stringify(tool_input),
|
||||
tool_output: JSON.stringify(tool_output)
|
||||
};
|
||||
|
||||
const client = net.connect(socketPath, () => {
|
||||
client.write(JSON.stringify(message) + '\n');
|
||||
client.end();
|
||||
});
|
||||
|
||||
let responded = false;
|
||||
const respond = () => {
|
||||
if (responded) {
|
||||
return;
|
||||
}
|
||||
responded = true;
|
||||
if (!session.worker_port) {
|
||||
console.error('[save-hook] No worker port for session', session.id);
|
||||
console.log(createHookResponse('PostToolUse', true));
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
client.on('close', respond);
|
||||
client.on('error', respond);
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}/observations`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tool_name,
|
||||
tool_input: JSON.stringify(tool_input),
|
||||
tool_output: JSON.stringify(tool_output)
|
||||
}),
|
||||
signal: AbortSignal.timeout(2000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[save-hook] Failed to send observation:', await response.text());
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[save-hook] Error:', error.message);
|
||||
} finally {
|
||||
console.log(createHookResponse('PostToolUse', true));
|
||||
}
|
||||
}
|
||||
|
||||
+21
-23
@@ -1,6 +1,4 @@
|
||||
import net from 'net';
|
||||
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
|
||||
import { getWorkerSocketPath } from '../shared/paths.js';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
|
||||
export interface StopInput {
|
||||
@@ -11,9 +9,9 @@ export interface StopInput {
|
||||
|
||||
/**
|
||||
* Summary Hook - Stop
|
||||
* Sends FINALIZE message to worker via Unix socket
|
||||
* Sends FINALIZE message to worker via HTTP POST
|
||||
*/
|
||||
export function summaryHook(input?: StopInput): void {
|
||||
export async function summaryHook(input?: StopInput): Promise<void> {
|
||||
if (!input) {
|
||||
throw new Error('summaryHook requires input');
|
||||
}
|
||||
@@ -28,25 +26,25 @@ export function summaryHook(input?: StopInput): void {
|
||||
return;
|
||||
}
|
||||
|
||||
const socketPath = getWorkerSocketPath(session.id);
|
||||
const message = {
|
||||
type: 'finalize'
|
||||
};
|
||||
|
||||
const client = net.connect(socketPath, () => {
|
||||
client.write(JSON.stringify(message) + '\n');
|
||||
client.end();
|
||||
});
|
||||
|
||||
let responded = false;
|
||||
const respond = () => {
|
||||
if (responded) {
|
||||
return;
|
||||
}
|
||||
responded = true;
|
||||
if (!session.worker_port) {
|
||||
console.error('[summary-hook] No worker port for session', session.id);
|
||||
console.log(createHookResponse('Stop', true));
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
client.on('close', respond);
|
||||
client.on('error', respond);
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}/finalize`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal: AbortSignal.timeout(2000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[summary-hook] Failed to finalize:', await response.text());
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[summary-hook] Error:', error.message);
|
||||
} finally {
|
||||
console.log(createHookResponse('Stop', true));
|
||||
}
|
||||
}
|
||||
|
||||
+59
-34
@@ -22,7 +22,7 @@ export interface SDKSession {
|
||||
* Build initial prompt to initialize the SDK agent
|
||||
*/
|
||||
export function buildInitPrompt(project: string, sessionId: string, userPrompt: string): string {
|
||||
return `You are a memory assistant for the "${project}" project.
|
||||
return `You are a memory processor for the "${project}" project.
|
||||
|
||||
SESSION CONTEXT
|
||||
---------------
|
||||
@@ -32,23 +32,36 @@ Date: ${new Date().toISOString().split('T')[0]}
|
||||
|
||||
YOUR ROLE
|
||||
---------
|
||||
You will observe tool executions during this Claude Code session. Your job is to:
|
||||
You will PROCESS tool executions during this Claude Code session. Your job is to:
|
||||
|
||||
1. Extract meaningful insights (not just raw data)
|
||||
2. Store atomic observations in SQLite
|
||||
3. Focus on: key decisions, patterns discovered, problems solved, technical insights
|
||||
1. ANALYZE each tool response for meaningful content
|
||||
2. DECIDE whether it contains something worth storing
|
||||
3. EXTRACT the key insight
|
||||
4. STORE it as an observation in the XML format below
|
||||
|
||||
WHAT TO CAPTURE
|
||||
----------------
|
||||
✓ Architecture decisions (e.g., "chose PostgreSQL over MongoDB for ACID guarantees")
|
||||
For MOST meaningful tool outputs, you should generate an observation. Only skip truly routine operations.
|
||||
|
||||
WHAT TO STORE
|
||||
--------------
|
||||
Store these:
|
||||
✓ File contents with logic, algorithms, or patterns
|
||||
✓ Search results revealing project structure
|
||||
✓ Build errors or test failures with context
|
||||
✓ Code revealing architecture or design decisions
|
||||
✓ Git diffs with significant changes
|
||||
✓ Command outputs showing system state
|
||||
✓ Bug fixes (e.g., "fixed race condition in auth middleware by adding mutex")
|
||||
✓ New features (e.g., "implemented JWT refresh token flow")
|
||||
✓ Refactorings (e.g., "extracted validation logic into separate service")
|
||||
✓ Discoveries (e.g., "found that API rate limit is 100 req/min")
|
||||
|
||||
✗ NOT routine operations (reading files, listing directories)
|
||||
✗ NOT work-in-progress (only completed work)
|
||||
✗ NOT obvious facts (e.g., "TypeScript file has types")
|
||||
WHAT TO SKIP
|
||||
------------
|
||||
Skip these:
|
||||
✗ Simple status checks (git status with no changes)
|
||||
✗ Trivial edits (one-line config changes)
|
||||
✗ Repeated operations
|
||||
✗ Anything without semantic value
|
||||
|
||||
HOW TO STORE OBSERVATIONS
|
||||
--------------------------
|
||||
@@ -73,50 +86,62 @@ The SDK worker will parse all <observation> blocks from your response using rege
|
||||
|
||||
You can include your reasoning before or after the observation block, or just output the observation by itself.
|
||||
|
||||
EXAMPLE
|
||||
-------
|
||||
Bad: "Read src/auth.ts file"
|
||||
Good: "Implemented JWT token refresh flow with 7-day expiry"
|
||||
|
||||
Wait for tool observations. Acknowledge this message briefly.`;
|
||||
Ready to process tool responses.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build prompt to send tool observation to SDK agent
|
||||
*/
|
||||
export function buildObservationPrompt(obs: Observation): string {
|
||||
// Safely parse tool_input and tool_output - they're already JSON strings
|
||||
let toolInput: any;
|
||||
let toolOutput: any;
|
||||
|
||||
try {
|
||||
toolInput = typeof obs.tool_input === 'string' ? JSON.parse(obs.tool_input) : obs.tool_input;
|
||||
} catch {
|
||||
toolInput = obs.tool_input; // If parse fails, use raw value
|
||||
}
|
||||
|
||||
try {
|
||||
toolOutput = typeof obs.tool_output === 'string' ? JSON.parse(obs.tool_output) : obs.tool_output;
|
||||
} catch {
|
||||
toolOutput = obs.tool_output; // If parse fails, use raw value
|
||||
}
|
||||
|
||||
return `TOOL OBSERVATION
|
||||
================
|
||||
Tool: ${obs.tool_name}
|
||||
Time: ${new Date(obs.created_at_epoch).toISOString()}
|
||||
|
||||
Input:
|
||||
${JSON.stringify(JSON.parse(obs.tool_input), null, 2)}
|
||||
${JSON.stringify(toolInput, null, 2)}
|
||||
|
||||
Output:
|
||||
${JSON.stringify(JSON.parse(obs.tool_output), null, 2)}
|
||||
${JSON.stringify(toolOutput, null, 2)}
|
||||
|
||||
ANALYSIS TASK
|
||||
-------------
|
||||
1. Does this observation contain something worth remembering?
|
||||
2. If YES: Output the observation in this EXACT XML format:
|
||||
ANALYZE this tool response and DECIDE: Does it contain something worth storing?
|
||||
|
||||
\`\`\`xml
|
||||
<observation>
|
||||
<type>feature</type>
|
||||
<text>Your concise observation here</text>
|
||||
</observation>
|
||||
\`\`\`
|
||||
Most Read, Edit, Grep, Bash, and Write operations contain meaningful content.
|
||||
|
||||
Requirements:
|
||||
- Use one of these types: decision, bugfix, feature, refactor, discovery
|
||||
- Keep text concise (one sentence preferred)
|
||||
- No markdown formatting inside <text>
|
||||
- No additional XML fields
|
||||
If this contains something worth remembering, output the observation in this EXACT XML format:
|
||||
|
||||
3. If NO: Just acknowledge and wait for next observation
|
||||
\`\`\`xml
|
||||
<observation>
|
||||
<type>feature</type>
|
||||
<text>Your concise observation here</text>
|
||||
</observation>
|
||||
\`\`\`
|
||||
|
||||
Remember: Quality over quantity. Only store meaningful insights.`;
|
||||
Requirements:
|
||||
- Use one of these types: decision, bugfix, feature, refactor, discovery
|
||||
- Keep text concise (one sentence preferred)
|
||||
- No markdown formatting inside <text>
|
||||
- No additional XML fields
|
||||
|
||||
If this is truly routine (e.g., empty git status), you can skip it. Otherwise, PROCESS and STORE it.`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Database } from 'bun:sqlite';
|
||||
import Database from 'better-sqlite3';
|
||||
import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
|
||||
|
||||
/**
|
||||
@@ -11,12 +11,33 @@ export class HooksDatabase {
|
||||
|
||||
constructor() {
|
||||
ensureDir(DATA_DIR);
|
||||
this.db = new Database(DB_PATH, { create: true, readwrite: true });
|
||||
this.db = new Database(DB_PATH);
|
||||
|
||||
// Ensure optimized settings
|
||||
this.db.run('PRAGMA journal_mode = WAL');
|
||||
this.db.run('PRAGMA synchronous = NORMAL');
|
||||
this.db.run('PRAGMA foreign_keys = ON');
|
||||
this.db.pragma('journal_mode = WAL');
|
||||
this.db.pragma('synchronous = NORMAL');
|
||||
this.db.pragma('foreign_keys = ON');
|
||||
|
||||
// Run migration to add worker_port column if it doesn't exist
|
||||
this.ensureWorkerPortColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure worker_port column exists (migration)
|
||||
*/
|
||||
private ensureWorkerPortColumn(): void {
|
||||
try {
|
||||
// Check if column exists
|
||||
const tableInfo = this.db.pragma('table_info(sdk_sessions)');
|
||||
const hasWorkerPort = (tableInfo as any[]).some((col: any) => col.name === 'worker_port');
|
||||
|
||||
if (!hasWorkerPort) {
|
||||
this.db.exec('ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER');
|
||||
console.error('[HooksDatabase] Added worker_port column to sdk_sessions table');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[HooksDatabase] Migration error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,7 +54,7 @@ export class HooksDatabase {
|
||||
notes: string | null;
|
||||
created_at: string;
|
||||
}> {
|
||||
const query = this.db.query(`
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, created_at
|
||||
@@ -43,7 +64,26 @@ export class HooksDatabase {
|
||||
LIMIT ?
|
||||
`);
|
||||
|
||||
return query.all(project, limit) as any[];
|
||||
return stmt.all(project, limit) as any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent observations for a project
|
||||
*/
|
||||
getRecentObservations(project: string, limit: number = 20): Array<{
|
||||
type: string;
|
||||
text: string;
|
||||
created_at: string;
|
||||
}> {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT type, text, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
|
||||
return stmt.all(project, limit) as any[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,15 +93,43 @@ export class HooksDatabase {
|
||||
id: number;
|
||||
sdk_session_id: string | null;
|
||||
project: string;
|
||||
worker_port: number | null;
|
||||
} | null {
|
||||
const query = this.db.query(`
|
||||
SELECT id, sdk_session_id, project
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ? AND status = 'active'
|
||||
LIMIT 1
|
||||
`);
|
||||
|
||||
return query.get(claudeSessionId) as any || null;
|
||||
return stmt.get(claudeSessionId) as any || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find any SDK session for a Claude session (active, failed, or completed)
|
||||
*/
|
||||
findAnySDKSession(claudeSessionId: string): { id: number } | null {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT id
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ?
|
||||
LIMIT 1
|
||||
`);
|
||||
|
||||
return stmt.get(claudeSessionId) as any || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reactivate an existing session
|
||||
*/
|
||||
reactivateSession(id: number, userPrompt: string): void {
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'active', user_prompt = ?, worker_port = NULL
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
stmt.run(userPrompt, id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,31 +139,55 @@ export class HooksDatabase {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
|
||||
const query = this.db.query(`
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`);
|
||||
|
||||
query.run(claudeSessionId, project, userPrompt, now.toISOString(), nowEpoch);
|
||||
|
||||
// Get the last inserted ID
|
||||
const lastIdQuery = this.db.query('SELECT last_insert_rowid() as id');
|
||||
const result = lastIdQuery.get() as { id: number };
|
||||
return result.id;
|
||||
const result = stmt.run(claudeSessionId, project, userPrompt, now.toISOString(), nowEpoch);
|
||||
return result.lastInsertRowid as number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update SDK session ID (captured from init message)
|
||||
*/
|
||||
updateSDKSessionId(id: number, sdkSessionId: string): void {
|
||||
const query = this.db.query(`
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
query.run(sdkSessionId, id);
|
||||
stmt.run(sdkSessionId, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set worker port for a session
|
||||
*/
|
||||
setWorkerPort(id: number, port: number): void {
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
stmt.run(port, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get worker port for a session
|
||||
*/
|
||||
getWorkerPort(id: number): number | null {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT worker_port
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`);
|
||||
|
||||
const result = stmt.get(id) as { worker_port: number | null } | undefined;
|
||||
return result?.worker_port || null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,13 +202,13 @@ export class HooksDatabase {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
|
||||
const query = this.db.query(`
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
query.run(sdkSessionId, project, text, type, now.toISOString(), nowEpoch);
|
||||
stmt.run(sdkSessionId, project, text, type, now.toISOString(), nowEpoch);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,14 +231,14 @@ export class HooksDatabase {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
|
||||
const query = this.db.query(`
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, files_read, files_edited, notes, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
query.run(
|
||||
stmt.run(
|
||||
sdkSessionId,
|
||||
project,
|
||||
summary.request || null,
|
||||
@@ -169,13 +261,13 @@ export class HooksDatabase {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
|
||||
const query = this.db.query(`
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
query.run(now.toISOString(), nowEpoch, id);
|
||||
stmt.run(now.toISOString(), nowEpoch, id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,13 +277,30 @@ export class HooksDatabase {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
|
||||
const query = this.db.query(`
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
query.run(now.toISOString(), nowEpoch, id);
|
||||
stmt.run(now.toISOString(), nowEpoch, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up orphaned active sessions (called on worker startup)
|
||||
*/
|
||||
cleanupOrphanedSessions(): number {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`);
|
||||
|
||||
const result = stmt.run(now.toISOString(), nowEpoch);
|
||||
return result.changes;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,487 @@
|
||||
/**
|
||||
* Worker Service - Long-running HTTP service managed by PM2
|
||||
* Replaces detached Bun worker processes with single persistent Node service
|
||||
*/
|
||||
|
||||
import express, { Request, Response } from 'express';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { SDKUserMessage, SDKSystemMessage } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { HooksDatabase } from './sqlite/HooksDatabase.js';
|
||||
import { buildInitPrompt, buildObservationPrompt, buildFinalizePrompt } from '../sdk/prompts.js';
|
||||
import { parseObservations, parseSummary } from '../sdk/parser.js';
|
||||
import type { SDKSession } from '../sdk/prompts.js';
|
||||
import { findAvailablePort } from '../utils/port-allocator.js';
|
||||
|
||||
const MODEL = 'claude-sonnet-4-5';
|
||||
const DISALLOWED_TOOLS = ['Glob', 'Grep', 'ListMcpResourcesTool', 'WebSearch'];
|
||||
|
||||
interface ObservationMessage {
|
||||
type: 'observation';
|
||||
tool_name: string;
|
||||
tool_input: string;
|
||||
tool_output: string;
|
||||
}
|
||||
|
||||
interface FinalizeMessage {
|
||||
type: 'finalize';
|
||||
}
|
||||
|
||||
type WorkerMessage = ObservationMessage | FinalizeMessage;
|
||||
|
||||
/**
|
||||
* Active session state
|
||||
*/
|
||||
interface ActiveSession {
|
||||
sessionDbId: number;
|
||||
sdkSessionId: string | null;
|
||||
project: string;
|
||||
userPrompt: string;
|
||||
isFinalized: boolean;
|
||||
pendingMessages: WorkerMessage[];
|
||||
abortController: AbortController;
|
||||
generatorPromise: Promise<void> | null;
|
||||
}
|
||||
|
||||
class WorkerService {
|
||||
private app: express.Application;
|
||||
private port: number | null = null;
|
||||
private sessions: Map<number, ActiveSession> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.app = express();
|
||||
this.app.use(express.json({ limit: '50mb' }));
|
||||
|
||||
// Health check
|
||||
this.app.get('/health', this.handleHealth.bind(this));
|
||||
|
||||
// Session endpoints
|
||||
this.app.post('/sessions/:sessionDbId/init', this.handleInit.bind(this));
|
||||
this.app.post('/sessions/:sessionDbId/observations', this.handleObservation.bind(this));
|
||||
this.app.post('/sessions/:sessionDbId/finalize', this.handleFinalize.bind(this));
|
||||
this.app.get('/sessions/:sessionDbId/status', this.handleStatus.bind(this));
|
||||
this.app.delete('/sessions/:sessionDbId', this.handleDelete.bind(this));
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
// Find available port
|
||||
const port = await findAvailablePort();
|
||||
if (!port) {
|
||||
throw new Error('No available ports in range 37000-37999');
|
||||
}
|
||||
|
||||
this.port = port;
|
||||
|
||||
// Clean up orphaned sessions from previous worker instances
|
||||
const db = new HooksDatabase();
|
||||
const cleanedCount = db.cleanupOrphanedSessions();
|
||||
db.close();
|
||||
|
||||
if (cleanedCount > 0) {
|
||||
console.error(`[WorkerService] Cleaned up ${cleanedCount} orphaned sessions`);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.app.listen(port, '127.0.0.1', () => {
|
||||
console.error(`[WorkerService] Started on http://127.0.0.1:${port}`);
|
||||
console.error(`[WorkerService] PID: ${process.pid}`);
|
||||
console.error(`[WorkerService] Active sessions: ${this.sessions.size}`);
|
||||
|
||||
// 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');
|
||||
writeFileSync(portFile, port.toString(), 'utf8');
|
||||
|
||||
resolve();
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /health
|
||||
*/
|
||||
private handleHealth(req: Request, res: Response): void {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
port: this.port,
|
||||
pid: process.pid,
|
||||
activeSessions: this.sessions.size,
|
||||
uptime: process.uptime(),
|
||||
memory: process.memoryUsage()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /sessions/:sessionDbId/init
|
||||
* Body: { project, userPrompt }
|
||||
*/
|
||||
private async handleInit(req: Request, res: Response): Promise<void> {
|
||||
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
||||
const { project, userPrompt } = req.body;
|
||||
|
||||
console.error(`[WorkerService] Initializing session ${sessionDbId}`, { project });
|
||||
|
||||
if (this.sessions.has(sessionDbId)) {
|
||||
res.status(409).json({ error: 'Session already exists' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Create session state
|
||||
const session: ActiveSession = {
|
||||
sessionDbId,
|
||||
sdkSessionId: null,
|
||||
project,
|
||||
userPrompt,
|
||||
isFinalized: false,
|
||||
pendingMessages: [],
|
||||
abortController: new AbortController(),
|
||||
generatorPromise: null
|
||||
};
|
||||
|
||||
this.sessions.set(sessionDbId, session);
|
||||
|
||||
// Update port in database
|
||||
const db = new HooksDatabase();
|
||||
db.setWorkerPort(sessionDbId, this.port!);
|
||||
db.close();
|
||||
|
||||
// Start SDK agent in background
|
||||
session.generatorPromise = this.runSDKAgent(session).catch(err => {
|
||||
console.error(`[WorkerService] SDK agent error for session ${sessionDbId}:`, err);
|
||||
const db = new HooksDatabase();
|
||||
db.markSessionFailed(sessionDbId);
|
||||
db.close();
|
||||
this.sessions.delete(sessionDbId);
|
||||
});
|
||||
|
||||
res.json({
|
||||
status: 'initialized',
|
||||
sessionDbId,
|
||||
port: this.port
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /sessions/:sessionDbId/observations
|
||||
* Body: { tool_name, tool_input, tool_output }
|
||||
*/
|
||||
private handleObservation(req: Request, res: Response): void {
|
||||
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
||||
const { tool_name, tool_input, tool_output } = req.body;
|
||||
|
||||
const session = this.sessions.get(sessionDbId);
|
||||
if (!session) {
|
||||
res.status(404).json({ error: 'Session not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.isFinalized) {
|
||||
res.status(400).json({ error: 'Session already finalized' });
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`[WorkerService] Queueing observation for session ${sessionDbId}:`, tool_name);
|
||||
|
||||
session.pendingMessages.push({
|
||||
type: 'observation',
|
||||
tool_name,
|
||||
tool_input,
|
||||
tool_output
|
||||
});
|
||||
|
||||
res.json({ status: 'queued', queueLength: session.pendingMessages.length });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /sessions/:sessionDbId/finalize
|
||||
*/
|
||||
private handleFinalize(req: Request, res: Response): void {
|
||||
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
||||
|
||||
const session = this.sessions.get(sessionDbId);
|
||||
if (!session) {
|
||||
res.status(404).json({ error: 'Session not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.isFinalized) {
|
||||
res.status(400).json({ error: 'Session already finalized' });
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`[WorkerService] Finalizing session ${sessionDbId}`);
|
||||
|
||||
session.pendingMessages.push({ type: 'finalize' });
|
||||
|
||||
res.json({ status: 'finalizing' });
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /sessions/:sessionDbId/status
|
||||
*/
|
||||
private handleStatus(req: Request, res: Response): void {
|
||||
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
||||
|
||||
const session = this.sessions.get(sessionDbId);
|
||||
if (!session) {
|
||||
res.status(404).json({ error: 'Session not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
sessionDbId,
|
||||
sdkSessionId: session.sdkSessionId,
|
||||
project: session.project,
|
||||
isFinalized: session.isFinalized,
|
||||
pendingMessages: session.pendingMessages.length
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /sessions/:sessionDbId
|
||||
*/
|
||||
private async handleDelete(req: Request, res: Response): Promise<void> {
|
||||
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
||||
|
||||
const session = this.sessions.get(sessionDbId);
|
||||
if (!session) {
|
||||
res.status(404).json({ error: 'Session not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`[WorkerService] Deleting session ${sessionDbId}`);
|
||||
|
||||
// Abort SDK agent
|
||||
session.abortController.abort();
|
||||
|
||||
// Wait for generator to finish (with timeout)
|
||||
if (session.generatorPromise) {
|
||||
await Promise.race([
|
||||
session.generatorPromise,
|
||||
new Promise(resolve => setTimeout(resolve, 5000))
|
||||
]);
|
||||
}
|
||||
|
||||
// Mark as failed if not completed
|
||||
if (!session.isFinalized) {
|
||||
const db = new HooksDatabase();
|
||||
db.markSessionFailed(sessionDbId);
|
||||
db.close();
|
||||
}
|
||||
|
||||
this.sessions.delete(sessionDbId);
|
||||
|
||||
res.json({ status: 'deleted' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Run SDK agent for a session
|
||||
*/
|
||||
private async runSDKAgent(session: ActiveSession): Promise<void> {
|
||||
console.error(`[WorkerService] Starting SDK agent for session ${session.sessionDbId}`);
|
||||
|
||||
const claudePath = process.env.CLAUDE_CODE_PATH || '/Users/alexnewman/.nvm/versions/node/v24.5.0/bin/claude';
|
||||
|
||||
try {
|
||||
const queryResult = query({
|
||||
prompt: this.createMessageGenerator(session),
|
||||
options: {
|
||||
model: MODEL,
|
||||
disallowedTools: DISALLOWED_TOOLS,
|
||||
abortController: session.abortController,
|
||||
pathToClaudeCodeExecutable: claudePath
|
||||
}
|
||||
});
|
||||
|
||||
for await (const message of queryResult) {
|
||||
// Handle system init message
|
||||
if (message.type === 'system' && message.subtype === 'init') {
|
||||
const systemMsg = message as SDKSystemMessage;
|
||||
if (systemMsg.session_id) {
|
||||
console.error(`[WorkerService] SDK session initialized:`, systemMsg.session_id);
|
||||
session.sdkSessionId = systemMsg.session_id;
|
||||
|
||||
// Update in database
|
||||
const db = new HooksDatabase();
|
||||
db.updateSDKSessionId(session.sessionDbId, systemMsg.session_id);
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
// Handle assistant messages
|
||||
else if (message.type === 'assistant') {
|
||||
const content = message.message.content;
|
||||
const textContent = Array.isArray(content)
|
||||
? content.filter((c: any) => c.type === 'text').map((c: any) => c.text).join('\n')
|
||||
: typeof content === 'string' ? content : '';
|
||||
|
||||
console.error(`[WorkerService] SDK response (${textContent.length} chars)`);
|
||||
|
||||
// Parse and store
|
||||
this.handleAgentMessage(session, textContent);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark completed
|
||||
console.error(`[WorkerService] SDK agent completed for session ${session.sessionDbId}`);
|
||||
const db = new HooksDatabase();
|
||||
db.markSessionCompleted(session.sessionDbId);
|
||||
db.close();
|
||||
|
||||
this.sessions.delete(session.sessionDbId);
|
||||
|
||||
} catch (error: any) {
|
||||
if (error.name === 'AbortError') {
|
||||
console.error(`[WorkerService] SDK agent aborted for session ${session.sessionDbId}`);
|
||||
} else {
|
||||
console.error(`[WorkerService] SDK agent error for session ${session.sessionDbId}:`, error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create async message generator for SDK streaming
|
||||
*/
|
||||
private async* createMessageGenerator(session: ActiveSession): AsyncIterable<SDKUserMessage> {
|
||||
const claudeSessionId = `session-${session.sessionDbId}`;
|
||||
const initPrompt = buildInitPrompt(session.project, claudeSessionId, session.userPrompt);
|
||||
|
||||
console.error(`[WorkerService] Yielding init prompt (${initPrompt.length} chars)`);
|
||||
|
||||
yield {
|
||||
type: 'user',
|
||||
session_id: session.sdkSessionId || claudeSessionId,
|
||||
parent_tool_use_id: null,
|
||||
message: {
|
||||
role: 'user',
|
||||
content: initPrompt
|
||||
}
|
||||
};
|
||||
|
||||
// Process messages as they arrive
|
||||
while (!session.isFinalized) {
|
||||
if (session.pendingMessages.length === 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
continue;
|
||||
}
|
||||
|
||||
while (session.pendingMessages.length > 0) {
|
||||
const message = session.pendingMessages.shift()!;
|
||||
|
||||
if (message.type === 'finalize') {
|
||||
console.error(`[WorkerService] Processing FINALIZE for session ${session.sessionDbId}`);
|
||||
session.isFinalized = true;
|
||||
|
||||
const db = new HooksDatabase();
|
||||
const dbSession = db.db.prepare(`
|
||||
SELECT id, sdk_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
`).get(session.sessionDbId) as SDKSession | undefined;
|
||||
db.close();
|
||||
|
||||
if (dbSession) {
|
||||
const finalizePrompt = buildFinalizePrompt(dbSession);
|
||||
console.error(`[WorkerService] Yielding finalize prompt (${finalizePrompt.length} chars)`);
|
||||
|
||||
yield {
|
||||
type: 'user',
|
||||
session_id: session.sdkSessionId || claudeSessionId,
|
||||
parent_tool_use_id: null,
|
||||
message: {
|
||||
role: 'user',
|
||||
content: finalizePrompt
|
||||
}
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (message.type === 'observation') {
|
||||
const observationPrompt = buildObservationPrompt({
|
||||
id: 0,
|
||||
tool_name: message.tool_name,
|
||||
tool_input: message.tool_input,
|
||||
tool_output: message.tool_output,
|
||||
created_at_epoch: Date.now()
|
||||
});
|
||||
|
||||
console.error(`[WorkerService] Yielding observation: ${message.tool_name}`);
|
||||
|
||||
yield {
|
||||
type: 'user',
|
||||
session_id: session.sdkSessionId || claudeSessionId,
|
||||
parent_tool_use_id: null,
|
||||
message: {
|
||||
role: 'user',
|
||||
content: observationPrompt
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle agent message - parse and store observations/summaries
|
||||
*/
|
||||
private handleAgentMessage(session: ActiveSession, content: string): void {
|
||||
// Parse observations
|
||||
const observations = parseObservations(content);
|
||||
console.error(`[WorkerService] Parsed ${observations.length} observations`);
|
||||
|
||||
const db = new HooksDatabase();
|
||||
for (const obs of observations) {
|
||||
if (session.sdkSessionId) {
|
||||
db.storeObservation(session.sdkSessionId, session.project, obs.type, obs.text);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse summary
|
||||
const summary = parseSummary(content);
|
||||
if (summary && session.sdkSessionId) {
|
||||
console.error(`[WorkerService] Parsed summary for session ${session.sessionDbId}`);
|
||||
|
||||
const summaryWithArrays = {
|
||||
request: summary.request,
|
||||
investigated: summary.investigated,
|
||||
learned: summary.learned,
|
||||
completed: summary.completed,
|
||||
next_steps: summary.next_steps,
|
||||
files_read: JSON.stringify(summary.files_read),
|
||||
files_edited: JSON.stringify(summary.files_edited),
|
||||
notes: summary.notes
|
||||
};
|
||||
|
||||
db.storeSummary(session.sdkSessionId, session.project, summaryWithArrays);
|
||||
}
|
||||
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Main entry point
|
||||
async function main() {
|
||||
const service = new WorkerService();
|
||||
await service.start();
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.error('[WorkerService] Shutting down gracefully...');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.error('[WorkerService] Shutting down gracefully...');
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-start when run directly (not when imported)
|
||||
main().catch(err => {
|
||||
console.error('[WorkerService] Fatal error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
export { WorkerService };
|
||||
@@ -18,7 +18,6 @@ export const ARCHIVES_DIR = join(DATA_DIR, 'archives');
|
||||
export const LOGS_DIR = join(DATA_DIR, 'logs');
|
||||
export const TRASH_DIR = join(DATA_DIR, 'trash');
|
||||
export const BACKUPS_DIR = join(DATA_DIR, 'backups');
|
||||
export const CHROMA_DIR = join(DATA_DIR, 'chroma');
|
||||
export const USER_SETTINGS_PATH = join(DATA_DIR, 'settings.json');
|
||||
export const DB_PATH = join(DATA_DIR, 'claude-mem.db');
|
||||
|
||||
@@ -57,7 +56,6 @@ export function ensureAllDataDirs(): void {
|
||||
ensureDir(LOGS_DIR);
|
||||
ensureDir(TRASH_DIR);
|
||||
ensureDir(BACKUPS_DIR);
|
||||
ensureDir(CHROMA_DIR);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import net from 'net';
|
||||
|
||||
/**
|
||||
* Port Allocator Utility
|
||||
* Finds available ports dynamically for worker service
|
||||
*/
|
||||
|
||||
const PORT_RANGE_START = 37000;
|
||||
const PORT_RANGE_END = 37999;
|
||||
|
||||
/**
|
||||
* Check if a port is available
|
||||
*/
|
||||
function isPortAvailable(port: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const server = net.createServer();
|
||||
|
||||
server.once('error', (err: any) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
resolve(false);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
server.once('listening', () => {
|
||||
server.close();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
server.listen(port, '127.0.0.1');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an available port in the configured range
|
||||
* Returns a port number or null if none available
|
||||
*/
|
||||
export async function findAvailablePort(): Promise<number | null> {
|
||||
// Try random ports first (faster for sparse allocation)
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const randomPort = Math.floor(Math.random() * (PORT_RANGE_END - PORT_RANGE_START + 1)) + PORT_RANGE_START;
|
||||
if (await isPortAvailable(randomPort)) {
|
||||
return randomPort;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to sequential search
|
||||
for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
|
||||
if (await isPortAvailable(port)) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific port is available
|
||||
*/
|
||||
export async function checkPort(port: number): Promise<boolean> {
|
||||
return isPortAvailable(port);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"types": ["node"],
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"tests"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user