Compare commits

...

3 Commits

Author SHA1 Message Date
Alex Newman 4f49cb1bc9 Release v3.9.13
Published from npm package build
Source: https://github.com/thedotmack/claude-mem-source
2025-10-03 21:45:36 -04:00
Alex Newman 874726b193 Release v3.9.12
Published from npm package build
Source: https://github.com/thedotmack/claude-mem-source
2025-10-03 21:09:11 -04:00
Alex Newman 5244a12422 Release v3.9.11
Published from npm package build
Source: https://github.com/thedotmack/claude-mem-source
2025-10-03 20:55:36 -04:00
15 changed files with 1823 additions and 1226 deletions
+420 -198
View File
@@ -1,248 +1,470 @@
# 🧠 Claude Memory System (claude-mem)
<div align="center">
A real-time memory system for Claude Code that captures, compresses, and retrieves conversation context across sessions using semantic search and vector embeddings.
<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" />
## ⚡️ Quick Start
<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 Node.js and works best with Claude Code
- Node.js >= 18.0.0
- Claude Code with MCP support
- macOS/Linux (POSIX-compliant system)
- Bun >= 1.0.0 (optional, for development)
<!-- Installation -->
### :gear: Installation
Install claude-mem globally via npm
```bash
npm install -g claude-mem
claude-mem install
```
Restart Claude Code. Memory capture starts automatically.
The interactive installer will guide you through three installation scopes:
## ✨ What It Does
- **User** - Install for current user (default, recommended)
- **Project** - Install for current project only
- **Local** - Install to custom directory
**Real-Time Memory Capture**
- Captures every conversation turn as it happens via streaming hooks
- User prompts stored immediately in ChromaDB with atomic facts
- Tool responses compressed asynchronously via Agent SDK
- Project-based memory isolation with hierarchical metadata
- Automatic context loading at session start and `/clear`
<!-- Running Tests -->
### :test_tube: Running Tests
**Semantic Search**
- Vector embeddings for intelligent retrieval via ChromaDB
- Find relevant context from past conversations
- Project-aware memory queries with temporal filtering
- Date-based search using query text (not metadata)
- 15+ MCP tools for memory operations
**Invisible Operation**
- Zero user configuration required
- Memory compression happens in background via SDK
- SDK transcripts auto-deleted from UI history
- Session overviews generated automatically
- Live memory viewer with SSE streaming
**Smart Trash™**
- Safe deletion with easy recovery
- Timestamped trash entries
- One-command restore
- Located at `~/.claude-mem/trash/`
## 🎯 Core Features
- **Streaming Hooks**: Real-time capture with minimal overhead (<50ms)
- **Agent SDK Integration**: Async compression without blocking conversation
- **MCP Server**: 15+ ChromaDB tools for memory operations
- **Project Isolation**: Memories segregated by project context
- **Zero Configuration**: Works out of the box after install
- **Embedded Databases**: ChromaDB and SQLite, no external dependencies
- **Invisible UX**: Memory operations don't pollute conversation UI
- **Live Memory Viewer**: Real-time slideshow of memories via SSE
## 🧭 Commands
To run tests, use the following commands
```bash
# Setup & Status
claude-mem install # Install/repair hooks and MCP integration
claude-mem status # Check installation and memory stats
claude-mem doctor # Run environment and pipeline diagnostics
claude-mem uninstall # Remove all hooks
# Run all tests
bun test
# Memory Operations
claude-mem load-context # View current session context
claude-mem logs # View operation logs
claude-mem changelog # Generate CHANGELOG.md from memories
# Run integration tests
npm run test:integration
# Storage Operations (Used by hooks/SDK)
claude-mem store-memory # Store a memory to ChromaDB + SQLite
claude-mem store-overview # Store a session overview
# Smart Trash™
claude-mem trash # View trash contents
claude-mem restore # Restore from trash
claude-mem trash-empty # Permanently delete trash
# ChromaDB Tools (15+ MCP tools available)
claude-mem chroma_* # Direct ChromaDB operations
# Run with coverage
npm run test:coverage
```
## 📁 Storage Structure
<!-- 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
├── archives/ # Compressed transcript backups
├── index/ # Legacy JSONL memory indices
├── hooks/ # Hook configuration files
├── trash/ # Smart Trash™ with recovery
├── trash/ # Smart Trash with recovery
├── hooks/ # Hook configurations
├── logs/ # Operation logs
└── claude-mem.db # SQLite metadata database
```
## 🏗️ Architecture
### Memory System
**Storage Layers**
- **ChromaDB**: Vector database for semantic search with embeddings
- **SQLite**: Metadata index (`~/.claude-mem/claude-mem.db`) with sessions, memories, overviews
- **Archives**: Compressed transcript backups in `~/.claude-mem/archives/`
**Rolling Memory** - Real-time conversation turn capture via hooks with immediate ChromaDB storage
**Hook System** (`hook-templates/`)
- `user-prompt-submit.js`: Captures user prompts immediately, stores in ChromaDB
- `post-tool-use.js`: Spawns Agent SDK for async compression of tool responses
- `stop.js`: Generates session overview, cleans up SDK transcripts from UI
- `session-start.js`: Loads relevant context on startup and `/clear`
- Shared utilities: `hook-helpers.js`, `hook-prompt-renderer.js`, `config-loader.js`, `path-resolver.js`
**TranscriptCompressor** - Intelligent chunking and compression of large conversations
**CLI Commands** (`src/commands/`)
- Installation, status, and diagnostics
- Memory storage and retrieval
- Changelog generation from memories
- Smart Trash™ management
- 15+ dynamic ChromaDB MCP tool wrappers
**MCP Server** - 15+ ChromaDB tools for memory operations and semantic search
**Services** (`src/services/`)
- SQLite stores: Session, Memory, Overview, Diagnostics, TranscriptEvent
- Path discovery for project detection
- Rolling settings and logs
**SQLite Backend** - Session tracking, metadata management, and diagnostics storage
## 🔍 How Memory Search Works
### Hook Integration
**Semantic Search Best Practices**:
```typescript
// ALWAYS include project name to avoid cross-contamination
mcp__claude-mem__chroma_query_documents({
collection_name: "claude_memories",
query_texts: ["claude-mem authentication bug"],
n_results: 10
})
Hooks communicate via JSON stdin/stdout and run with minimal overhead:
// Include dates for temporal search (dates in query text, not metadata)
mcp__claude-mem__chroma_query_documents({
collection_name: "claude_memories",
query_texts: ["project-name 2025-10-02 feature implementation"],
n_results: 5
})
1. **user-prompt-submit** - Stores user prompt immediately in ChromaDB
2. **post-tool-use** - Spawns Agent SDK subprocess for async compression
3. **stop-streaming** - Generates session overview, deletes SDK transcript
4. **session-start** - Loads project-specific context invisibly
// Intent-based queries work better than keyword matching
mcp__claude-mem__chroma_query_documents({
collection_name: "claude_memories",
query_texts: ["implementing oauth flow"],
n_results: 10
})
### Project Structure
```
src/
├── bin/ # CLI entry point
├── commands/ # Command implementations
├── core/ # Core compression logic
├── services/ # SQLite, ChromaDB, path discovery
├── shared/ # Configuration and utilities
└── mcp-server.ts # MCP server implementation
hook-templates/ # Hook source files
dist/ # Minified production bundle
test/ # Unit and integration tests
```
**What Doesn't Work** (Avoid These!)
- ❌ Complex `where` filters with `$and`/`$or` - causes errors
- ❌ Timestamp comparisons (`$gte`, `$lt`) - stored as strings
- ❌ Mixing project filters in where clause - causes "Error finding id"
## :wrench: Configuration
**Storage Collection**: `claude_memories`
- Metadata: `project`, `session_id`, `date`, `type`, `concepts`, `files`
- Embeddings: Semantic vectors for similarity search
- Documents: Atomic facts + full narrative with hierarchical structure
### Hook Timeout
## ✅ Requirements
- Node.js >= 18.0.0
- Bun >= 1.0.0 (for development)
- Claude Code with MCP support
- macOS/Linux (POSIX-compliant)
## 🛠️ Development
Default hook timeout is 180 seconds. Configure during installation:
```bash
# Development mode
bun run dev
# Build production bundle
bun run build
# Build and update hooks (RECOMMENDED for hook changes)
bun run build && bun link && claude-mem install --force
# Run tests
bun test # All tests
npm run test:integration # Integration tests
bun run test:unit # Unit tests only
# Install from source
bun run dev:install
# Live Memory Viewer
npm run memory-stream:server # Start SSE server on :3001
# Code quality
bun run lint
bun run format
claude-mem install --timeout 300000 # 5 minutes
```
## 🎨 Live Memory Viewer
### MCP Server
Real-time slideshow of memories with SSE streaming:
1. Start the server: `npm run memory-stream:server`
2. Open the viewer at `src/ui/memory-stream/`
3. Auto-connects to `~/.claude-mem/claude-mem.db`
4. New memories appear instantly as they're created
Features:
- 📡 Live SSE streaming from SQLite WAL changes
- 🎬 Auto-slideshow (5s intervals)
- ⏸️ Pause/Resume with Space bar
- ⌨️ Keyboard navigation (←/→)
- 🎨 Cyberpunk neural network aesthetic
## 🔑 Key Design Decisions
**Storage Architecture**
- Direct ChromaDB writes in `store-memory.ts` command (no async syncing)
- Each atomic fact stored as separate document + full narrative document
- Hierarchical metadata: project, session, date, type, concepts, files
- SQLite for fast metadata queries, ChromaDB for semantic search
**Hook Infrastructure**
- Streaming hooks (<50ms overhead) capture real-time events
- Shared utilities in `hook-templates/shared/` for consistency
- Force overwrite on install to ensure latest hook code deploys
- Milliseconds in `config.json`, seconds in Claude settings
**Memory Compression**
- Agent SDK spawned asynchronously for tool response compression
- User prompts stored immediately without blocking
- SDK transcripts auto-deleted to keep UI clean
- 100:1 compression ratio maintained
**Search Strategy**
- Semantic search via query text (dates embedded in queries)
- Avoid complex metadata filters (causes ChromaDB errors)
- Always include project name in queries for isolation
- Multiple query phrasings for better coverage
## 🆘 Troubleshooting
Skip MCP server installation if needed:
```bash
claude-mem status # Check installation health
claude-mem doctor # Run full diagnostics
claude-mem install --force # Repair installation
claude-mem logs # View recent operations
claude-mem install --skip-mcp
```
## 📄 License
### Force Reinstall
AGPL-3.0 - See LICENSE file for details
```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
---
**Remember more. Repeat less.** 🧠✨
**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**
+265
View File
@@ -0,0 +1,265 @@
# Windows Installation Guide
## Overview
Claude-mem now includes **experimental Windows support** as of v3.10.0 (October 2025). The cross-platform architecture uses the [Platform utility](/src/utils/platform.ts) for handling Windows-specific differences.
## System Requirements
- **Windows 10/11** (64-bit recommended)
- **Node.js** >= 18.0.0
- **PowerShell** 5.1 or later (for hook execution)
- **Claude Code** with MCP support
- **npm** package manager
**Note**: Bun (>=1.0.0) is only required for development work on claude-mem itself, not for running it.
## Known Windows-Specific Differences
### 1. Hook Execution
- **Unix/macOS**: Hooks execute via `/bin/sh`
- **Windows**: Hooks execute via `powershell`
All hooks are `.js` files that work on both platforms through Node.js.
### 2. Path Handling
Windows paths use backslashes (`\`) but Node.js normalizes these automatically. The data directory is located at:
```
C:\Users\YourUsername\.claude-mem\
```
### 3. File Permissions
- **Unix/macOS**: Hook files get `chmod 755` permissions
- **Windows**: Permission setting is a no-op (not needed)
### 4. Smart Trash™ Alias
The Smart Trash feature creates shell aliases differently:
**Windows (PowerShell)**:
```powershell
# claude-mem smart trash alias
function rm { claude-mem trash $args }
```
PowerShell profiles are located at:
- `Documents\PowerShell\Microsoft.PowerShell_profile.ps1`
- `Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1`
**Unix/macOS (Bash/Zsh)**:
```bash
# claude-mem smart trash alias
alias rm='claude-mem trash'
```
### 5. UV Package Manager Installation
The installer automatically installs `uv` using platform-specific methods:
**Windows**:
```powershell
powershell -Command "irm https://astral.sh/uv/install.ps1 | iex"
```
**Unix/macOS**:
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
## Installation Steps
### 1. Install claude-mem globally
```powershell
npm install -g claude-mem
```
### 2. Run the installer
```powershell
claude-mem install
```
The installer will:
1. ✅ Create directory structure at `C:\Users\YourUsername\.claude-mem\`
2. ✅ Install UV package manager via PowerShell
3. ✅ Install Chroma MCP server for vector database
4. ✅ Add CLAUDE.md instructions to `C:\Users\YourUsername\.claude\`
5. ✅ Install slash commands (save.md, remember.md, claude-mem.md)
6. ✅ Install memory hooks with PowerShell execution support
7. ✅ Configure Claude Code settings
### 3. Restart Claude Code
After installation, restart Claude Code to activate the memory system.
## Windows-Specific Caveats
### PowerShell Execution Policy
If hooks fail to execute, you may need to adjust your PowerShell execution policy:
```powershell
# Check current policy
Get-ExecutionPolicy
# Allow local scripts (choose one):
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
# OR for more security:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
```
### Path Environment Variable
After installing UV, you may need to restart PowerShell or update your PATH:
```powershell
# Add to PATH if needed
$env:PATH = "$env:USERPROFILE\.cargo\bin;$env:PATH"
```
The installer attempts to set this automatically, but it only affects the current process.
### Better-SQLite3 Native Module
The hooks use `better-sqlite3` for database access, which requires native compilation:
- The installer runs `npm install` in the hooks directory
- If this fails, hooks may still work if `better-sqlite3` is globally available
- Build errors are silently ignored to prevent installation failure
If you see database errors in logs:
```powershell
cd $env:USERPROFILE\.claude-mem\hooks
npm install better-sqlite3 --build-from-source
```
## File Locations on Windows
| Component | Windows Path |
|-----------|-------------|
| Data directory | `C:\Users\YourUsername\.claude-mem\` |
| ChromaDB | `C:\Users\YourUsername\.claude-mem\chroma\` |
| SQLite database | `C:\Users\YourUsername\.claude-mem\claude-mem.db` |
| Hooks | `C:\Users\YourUsername\.claude-mem\hooks\` |
| Archives | `C:\Users\YourUsername\.claude-mem\archives\` |
| Trash | `C:\Users\YourUsername\.claude-mem\trash\` |
| CLAUDE.md | `C:\Users\YourUsername\.claude\CLAUDE.md` |
| Settings | `C:\Users\YourUsername\AppData\Roaming\Claude\settings.json` |
| Commands | `C:\Users\YourUsername\.claude\commands\` |
## Testing Your Installation
### 1. Check installation status
```powershell
claude-mem status
```
### 2. Run diagnostics
```powershell
claude-mem doctor
```
This will verify:
- ✅ Directory structure
- ✅ Hook configuration
- ✅ Database accessibility
- ✅ MCP server integration
### 3. Test memory storage
Start Claude Code and have a brief conversation. Then check:
```powershell
claude-mem status
```
You should see memory counts increase.
### 4. Search memories
In Claude Code, ask:
```
Search my memories for [your topic]
```
Claude should use the `mcp__claude-mem__chroma_query_documents` tool automatically.
## Known Issues & Limitations
### ⚠️ PowerShell Profile Creation
- **Issue**: PowerShell profiles may not exist by default
- **Impact**: Smart Trash alias installation creates profile directories
- **Status**: Installer handles this automatically
### ⚠️ Native Module Compilation
- **Issue**: `better-sqlite3` requires build tools
- **Impact**: May need Visual Studio Build Tools installed
- **Workaround**: Installer attempts silent installation; manually rebuild if needed
### ⚠️ Path Case Sensitivity
- **Issue**: Windows paths are case-insensitive but ChromaDB metadata stores exact case
- **Impact**: Path comparisons may fail in edge cases
- **Status**: Generally works fine; use consistent casing
## Getting Help
If you encounter Windows-specific issues:
1. **Run diagnostics**:
```powershell
claude-mem doctor
```
2. **Check logs**:
```powershell
claude-mem logs
```
Or view directly:
```powershell
type $env:USERPROFILE\.claude-mem\logs\*.log
```
3. **Verify hook execution**:
Check Claude Code's hook output for errors during session start/stop
4. **Report issues**:
- Include `claude-mem doctor` output
- Include relevant logs
- Specify Windows version and PowerShell version
- File at: https://github.com/thedotmack/claude-mem/issues
## Development on Windows
If you're developing claude-mem on Windows:
### Prerequisites
- Install Bun (Windows support is experimental): https://bun.sh/
- Install Git for Windows
- Install Visual Studio Build Tools for native modules
### Build Commands
```powershell
# Install dependencies
npm install
# Build minified bundle
npm run build
# Link for local testing
bun link
# Reinstall with latest build
claude-mem install --force
```
**Note**: Some npm scripts use Unix-style paths and may not work on Windows. Core development is recommended on Unix/macOS systems.
## Summary
**Works on Windows**:
- Memory capture via streaming hooks
- ChromaDB semantic search
- SQLite metadata storage
- MCP server integration
- All CLI commands
- Smart Trash™ system
- Automatic context loading
⚠️ **May Require Extra Setup**:
- PowerShell execution policy
- Visual Studio Build Tools for native modules
- Manual PATH configuration
The core claude-mem functionality is fully operational on Windows, with the memory capture, storage, and retrieval systems working identically to Unix/macOS.
+213 -183
View File
File diff suppressed because one or more lines are too long
+12 -6
View File
@@ -13,11 +13,11 @@ import { fileURLToPath } from 'url';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { renderToolMessage, HOOK_CONFIG } from './shared/hook-prompt-renderer.js';
import { getProjectName } from './shared/path-resolver.js';
import { initializeDatabase, getActiveStreamingSessionsForProject } from './shared/hook-helpers.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const SESSION_DIR = path.join(process.env.HOME || '', '.claude-mem', 'sessions');
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
function debugLog(message, data = {}) {
@@ -61,15 +61,18 @@ process.stdin.on('end', async () => {
console.log(JSON.stringify({ async: true, asyncTimeout: 180000 }));
try {
// Load SDK session info
const sessionFile = path.join(SESSION_DIR, `${project}_streaming.json`);
if (!fs.existsSync(sessionFile)) {
// Load SDK session info from database
const db = initializeDatabase();
const sessions = getActiveStreamingSessionsForProject(db, project);
if (!sessions || sessions.length === 0) {
debugLog('PostToolUse: No streaming session found', { project });
db.close();
process.exit(0);
}
const sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
const sdkSessionId = sessionData.sdkSessionId;
const sessionData = sessions[0];
const sdkSessionId = sessionData.sdk_session_id;
// Convert tool response to string
const toolResponseStr = typeof tool_response === 'string'
@@ -135,6 +138,9 @@ process.stdin.on('end', async () => {
}
debugLog('PostToolUse: SDK finished processing', { tool_name, sdkSessionId });
// Close database connection
db.close();
} catch (error) {
debugLog('PostToolUse: Error sending to SDK', { error: error.message });
}
+194 -1
View File
@@ -2,7 +2,7 @@
/**
* Hook Helper Functions
*
*
* This module provides JavaScript wrappers around the TypeScript PromptOrchestrator
* and HookTemplates system, making them accessible to the JavaScript hook scripts.
*/
@@ -10,6 +10,9 @@
import { spawn } from 'child_process';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import Database from 'better-sqlite3';
import os from 'os';
import fs from 'fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -235,3 +238,193 @@ export function debugLog(message, data = {}) {
console.error(`[${timestamp}] HOOK DEBUG: ${message}`, data);
}
}
// =============================================================================
// DATABASE HELPERS (inline SQL to avoid 'claude-mem' import issues)
// =============================================================================
/**
* Get the claude-mem data directory path
*/
function getDataDirectory() {
return join(os.homedir(), '.claude-mem');
}
/**
* Get or create the database connection
*/
function getDatabase() {
const dataDir = getDataDirectory();
const dbPath = join(dataDir, 'claude-mem.db');
// Ensure directory exists
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
const db = new Database(dbPath);
// Apply optimized SQLite settings
db.pragma('journal_mode = WAL');
db.pragma('synchronous = NORMAL');
db.pragma('foreign_keys = ON');
db.pragma('temp_store = memory');
return db;
}
/**
* Ensure the streaming_sessions table exists
*/
function ensureStreamingSessionsTable(db) {
db.exec(`
CREATE TABLE IF NOT EXISTS streaming_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
claude_session_id TEXT UNIQUE NOT NULL,
sdk_session_id TEXT,
project TEXT NOT NULL,
title TEXT,
subtitle TEXT,
user_prompt TEXT,
started_at TEXT NOT NULL,
started_at_epoch INTEGER NOT NULL,
updated_at TEXT,
updated_at_epoch INTEGER,
completed_at TEXT,
completed_at_epoch INTEGER,
status TEXT NOT NULL CHECK(status IN ('active', 'completed', 'failed'))
)
`);
// Create indices if they don't exist
db.exec(`
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_claude_id
ON streaming_sessions(claude_session_id)
`);
db.exec(`
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_sdk_id
ON streaming_sessions(sdk_session_id)
`);
db.exec(`
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_project_status
ON streaming_sessions(project, status)
`);
}
/**
* Create a new streaming session record
*/
export function createStreamingSession(db, { claude_session_id, project, user_prompt, started_at }) {
ensureStreamingSessionsTable(db);
const timestamp = started_at || new Date().toISOString();
const epoch = new Date(timestamp).getTime();
const stmt = db.prepare(`
INSERT INTO streaming_sessions (
claude_session_id, project, user_prompt, started_at, started_at_epoch, status
) VALUES (?, ?, ?, ?, ?, 'active')
`);
const info = stmt.run(claude_session_id, project, user_prompt || null, timestamp, epoch);
return db.prepare('SELECT * FROM streaming_sessions WHERE id = ?').get(info.lastInsertRowid);
}
/**
* Update a streaming session by internal ID
*/
export function updateStreamingSession(db, id, updates) {
const timestamp = new Date().toISOString();
const epoch = Date.now();
const parts = [];
const values = [];
if (updates.sdk_session_id !== undefined) {
parts.push('sdk_session_id = ?');
values.push(updates.sdk_session_id);
}
if (updates.title !== undefined) {
parts.push('title = ?');
values.push(updates.title);
}
if (updates.subtitle !== undefined) {
parts.push('subtitle = ?');
values.push(updates.subtitle);
}
if (updates.status !== undefined) {
parts.push('status = ?');
values.push(updates.status);
}
if (updates.completed_at !== undefined) {
const completedTimestamp = typeof updates.completed_at === 'string'
? updates.completed_at
: new Date(updates.completed_at).toISOString();
const completedEpoch = new Date(completedTimestamp).getTime();
parts.push('completed_at = ?', 'completed_at_epoch = ?');
values.push(completedTimestamp, completedEpoch);
}
// Always update the updated_at timestamp
parts.push('updated_at = ?', 'updated_at_epoch = ?');
values.push(timestamp, epoch);
values.push(id);
const stmt = db.prepare(`
UPDATE streaming_sessions
SET ${parts.join(', ')}
WHERE id = ?
`);
stmt.run(...values);
return db.prepare('SELECT * FROM streaming_sessions WHERE id = ?').get(id);
}
/**
* Get active streaming sessions for a project
*/
export function getActiveStreamingSessionsForProject(db, project) {
ensureStreamingSessionsTable(db);
const stmt = db.prepare(`
SELECT * FROM streaming_sessions
WHERE project = ? AND status = 'active'
ORDER BY started_at_epoch DESC
`);
return stmt.all(project);
}
/**
* Mark a session as completed
*/
export function markStreamingSessionCompleted(db, id) {
const timestamp = new Date().toISOString();
const epoch = Date.now();
const stmt = db.prepare(`
UPDATE streaming_sessions
SET status = ?,
completed_at = ?,
completed_at_epoch = ?,
updated_at = ?,
updated_at_epoch = ?
WHERE id = ?
`);
stmt.run('completed', timestamp, epoch, timestamp, epoch, id);
return db.prepare('SELECT * FROM streaming_sessions WHERE id = ?').get(id);
}
/**
* Initialize database with migrations and return connection
*/
export function initializeDatabase() {
const db = getDatabase();
ensureStreamingSessionsTable(db);
return db;
}
+23 -10
View File
@@ -12,8 +12,8 @@ import fs from 'fs';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { renderEndMessage, HOOK_CONFIG } from './shared/hook-prompt-renderer.js';
import { getProjectName } from './shared/path-resolver.js';
import { initializeDatabase, getActiveStreamingSessionsForProject, markStreamingSessionCompleted } from './shared/hook-helpers.js';
const SESSION_DIR = path.join(process.env.HOME || '', '.claude-mem', 'sessions');
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
function debugLog(message, data = {}) {
@@ -50,20 +50,31 @@ process.stdin.on('end', async () => {
const { cwd } = payload;
const project = cwd ? getProjectName(cwd) : 'unknown';
// Immediately clear activity flag for UI indicator
const activityFlagPath = path.join(process.env.HOME || '', '.claude-mem', 'activity.flag');
try {
fs.writeFileSync(activityFlagPath, JSON.stringify({ active: false, timestamp: Date.now() }));
} catch (error) {
// Silent fail - non-critical
}
// Return immediately with async mode
console.log(JSON.stringify({ async: true, asyncTimeout: 180000 }));
try {
// Load SDK session info
const sessionFile = path.join(SESSION_DIR, `${project}_streaming.json`);
if (!fs.existsSync(sessionFile)) {
// Load SDK session info from database
const db = initializeDatabase();
const sessions = getActiveStreamingSessionsForProject(db, project);
if (!sessions || sessions.length === 0) {
debugLog('Stop: No streaming session found', { project });
db.close();
process.exit(0);
}
const sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
const sdkSessionId = sessionData.sdkSessionId;
const claudeSessionId = sessionData.claudeSessionId;
const sessionData = sessions[0];
const sdkSessionId = sessionData.sdk_session_id;
const claudeSessionId = sessionData.claude_session_id;
debugLog('Stop: Ending SDK session', { sdkSessionId, claudeSessionId });
@@ -108,10 +119,12 @@ process.stdin.on('end', async () => {
debugLog('Stop: Cleaned up memories transcript', { memoriesTranscriptPath });
}
// Clean up session file
fs.unlinkSync(sessionFile);
debugLog('Stop: Session ended and cleaned up', { project });
// Mark session as completed in database
markStreamingSessionCompleted(db, sessionData.id);
debugLog('Stop: Session ended and marked complete', { project, sessionId: sessionData.id });
// Close database connection
db.close();
} catch (error) {
debugLog('Stop: Error ending session', { error: error.message });
}
+36 -12
View File
@@ -13,11 +13,11 @@ import { fileURLToPath } from 'url';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { renderSystemPrompt, HOOK_CONFIG } from './shared/hook-prompt-renderer.js';
import { getProjectName } from './shared/path-resolver.js';
import { initializeDatabase, createStreamingSession, updateStreamingSession } from './shared/hook-helpers.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const SESSION_DIR = path.join(process.env.HOME || '', '.claude-mem', 'sessions');
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
function debugLog(message, data = {}) {
@@ -60,6 +60,14 @@ process.stdin.on('end', async () => {
debugLog('UserPromptSubmit: Starting streaming session', { project, session_id });
// Immediately signal activity start for UI indicator
const activityFlagPath = path.join(process.env.HOME || '', '.claude-mem', 'activity.flag');
try {
fs.writeFileSync(activityFlagPath, JSON.stringify({ active: true, project, timestamp: Date.now() }));
} catch (error) {
// Silent fail - non-critical
}
// Generate title and subtitle non-blocking
if (prompt && session_id && project) {
import('child_process').then(({ spawn }) => {
@@ -80,6 +88,22 @@ process.stdin.on('end', async () => {
}
try {
// Initialize database and create session record FIRST
const db = initializeDatabase();
// Create session record immediately - this gives us a tracking ID
const sessionRecord = createStreamingSession(db, {
claude_session_id: session_id,
project,
user_prompt: prompt,
started_at: timestamp
});
debugLog('UserPromptSubmit: Created session record', {
internalId: sessionRecord.id,
claudeSessionId: session_id
});
// Build system prompt using centralized config
const systemPrompt = renderSystemPrompt({
project,
@@ -110,19 +134,19 @@ process.stdin.on('end', async () => {
}
if (sdkSessionId) {
// Save session info for other hooks
fs.mkdirSync(SESSION_DIR, { recursive: true });
const sessionFile = path.join(SESSION_DIR, `${project}_streaming.json`);
fs.writeFileSync(sessionFile, JSON.stringify({
sdkSessionId,
claudeSessionId: session_id,
project,
startedAt: timestamp,
date
}, null, 2));
// Update session record with SDK session ID
updateStreamingSession(db, sessionRecord.id, {
sdk_session_id: sdkSessionId
});
debugLog('UserPromptSubmit: SDK session started', { sdkSessionId, sessionFile });
debugLog('UserPromptSubmit: SDK session started', {
internalId: sessionRecord.id,
sdkSessionId
});
}
// Close database connection
db.close();
} catch (error) {
debugLog('UserPromptSubmit: Error starting SDK session', { error: error.message });
}
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "3.9.10",
"version": "3.9.13",
"description": "Memory compression system for Claude Code - persist context across sessions",
"keywords": [
"claude",
@@ -51,6 +51,7 @@
"commands",
"src",
".mcp.json",
"CHANGELOG.md"
"CHANGELOG.md",
"README_WINDOWS.md"
]
}
+8 -3
View File
@@ -224,9 +224,8 @@ program
.description('Generate a session title and subtitle from a prompt')
.option('--json', 'Output as JSON')
.option('--oneline', 'Output as single line (title - subtitle)')
.option('--save', 'Save title and subtitle to session metadata')
.option('--project <name>', 'Project name (required with --save)')
.option('--session <id>', 'Session ID (required with --save)')
.option('--session-id <id>', 'Claude session ID to update')
.option('--save', 'Save the generated title to the database (requires --session-id)')
.action(generateTitle);
// </Block> =======================================
@@ -268,3 +267,9 @@ try {
// Parse arguments and execute
program.parse();
// </Block> =======================================
// <Block> 1.12 ===================================
// Module Exports for Programmatic Use
// Export database and utility classes for hooks and external consumers
export { DatabaseManager, StreamingSessionStore, migrations, initializeDatabase, getDatabase } from '../services/sqlite/index.js';
// </Block> =======================================
+46 -44
View File
@@ -1,14 +1,17 @@
import { OptionValues } from 'commander';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { getClaudePath } from '../shared/settings.js';
import path from 'path';
import fs from 'fs';
const SESSION_DIR = path.join(process.env.HOME || '', '.claude-mem', 'sessions');
import { DatabaseManager } from '../services/sqlite/Database.js';
import { StreamingSessionStore } from '../services/sqlite/StreamingSessionStore.js';
import { migrations } from '../services/sqlite/migrations.js';
/**
* Generate a session title and subtitle from a user prompt
* CLI command that uses Agent SDK (like changelog.ts)
*
* Can be called in two modes:
* 1. Standalone: generate-title "user prompt" --json
* 2. With session: generate-title "user prompt" --session-id <id> --save
*/
export async function generateTitle(prompt: string, options: OptionValues): Promise<void> {
if (!prompt || prompt.trim().length === 0) {
@@ -19,6 +22,36 @@ export async function generateTitle(prompt: string, options: OptionValues): Prom
process.exit(1);
}
// If --session-id provided, validate that session exists
let streamingStore: StreamingSessionStore | null = null;
let sessionRecord = null;
if (options.sessionId) {
try {
const dbManager = DatabaseManager.getInstance();
for (const migration of migrations) {
dbManager.registerMigration(migration);
}
const db = await dbManager.initialize();
streamingStore = new StreamingSessionStore(db);
sessionRecord = streamingStore.getByClaudeSessionId(options.sessionId);
if (!sessionRecord) {
console.error(JSON.stringify({
success: false,
error: `Session not found: ${options.sessionId}`
}));
process.exit(1);
}
} catch (error: any) {
console.error(JSON.stringify({
success: false,
error: `Database error: ${error.message}`
}));
process.exit(1);
}
}
const systemPrompt = `You are a title and subtitle generator for claude-mem session metadata.
Your job is to analyze a user's request and generate:
@@ -107,49 +140,17 @@ Now generate the title and subtitle (two lines exactly):`;
const title = lines[0].trim();
const subtitle = lines[1].trim();
// Save to session metadata if --save flag is provided
if (options.save) {
if (!options.project || !options.session) {
console.error(JSON.stringify({
success: false,
error: '--project and --session are required when using --save'
}));
process.exit(1);
}
// If --save and we have a session, update the database
if (options.save && streamingStore && sessionRecord) {
try {
const sessionFile = path.join(SESSION_DIR, `${options.project}_streaming.json`);
if (!fs.existsSync(sessionFile)) {
console.error(JSON.stringify({
success: false,
error: `Session file not found: ${sessionFile}`
}));
process.exit(1);
}
let sessionData: any = {};
try {
sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
} catch (e) {
console.error(JSON.stringify({
success: false,
error: 'Failed to parse session file'
}));
process.exit(1);
}
// Update metadata
sessionData.promptTitle = title;
sessionData.promptSubtitle = subtitle;
sessionData.updatedAt = new Date().toISOString();
// Write back to file
fs.writeFileSync(sessionFile, JSON.stringify(sessionData, null, 2));
streamingStore.update(sessionRecord.id, {
title,
subtitle
});
} catch (error: any) {
console.error(JSON.stringify({
success: false,
error: `Failed to save metadata: ${error.message}`
error: `Failed to save title: ${error.message}`
}));
process.exit(1);
}
@@ -160,7 +161,8 @@ Now generate the title and subtitle (two lines exactly):`;
console.log(JSON.stringify({
success: true,
title,
subtitle
subtitle,
sessionId: sessionRecord?.claude_session_id
}, null, 2));
} else if (options.oneline) {
console.log(`${title} - ${subtitle}`);
+176 -764
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,266 @@
import { Database } from 'better-sqlite3';
import { getDatabase } from './Database.js';
import { normalizeTimestamp } from './types.js';
/**
* Represents a streaming session row in the database
*/
export interface StreamingSessionRow {
id: number;
claude_session_id: string;
sdk_session_id?: string;
project: string;
title?: string;
subtitle?: string;
user_prompt?: string;
started_at: string;
started_at_epoch: number;
updated_at?: string;
updated_at_epoch?: number;
completed_at?: string;
completed_at_epoch?: number;
status: 'active' | 'completed' | 'failed';
}
/**
* Input type for creating a new streaming session
*/
export interface StreamingSessionInput {
claude_session_id: string;
project: string;
user_prompt?: string;
started_at?: string | Date | number;
}
/**
* Input type for updating a streaming session
*/
export interface StreamingSessionUpdate {
sdk_session_id?: string;
title?: string;
subtitle?: string;
status?: 'active' | 'completed' | 'failed';
completed_at?: string | Date | number;
}
/**
* Data Access Object for streaming session records
* Handles real-time session tracking during SDK compression
*/
export class StreamingSessionStore {
private db: Database.Database;
constructor(db?: Database.Database) {
this.db = db || getDatabase();
}
/**
* Create a new streaming session record
* This should be called immediately when the hook receives a user prompt
*/
create(input: StreamingSessionInput): StreamingSessionRow {
const { isoString, epoch } = normalizeTimestamp(input.started_at);
const stmt = this.db.prepare(`
INSERT INTO streaming_sessions (
claude_session_id, project, user_prompt, started_at, started_at_epoch, status
) VALUES (?, ?, ?, ?, ?, 'active')
`);
const info = stmt.run(
input.claude_session_id,
input.project,
input.user_prompt || null,
isoString,
epoch
);
return this.getById(info.lastInsertRowid as number)!;
}
/**
* Update a streaming session by internal ID
* Uses atomic transaction to prevent race conditions
*/
update(id: number, updates: StreamingSessionUpdate): StreamingSessionRow {
const { isoString: updatedAt, epoch: updatedEpoch } = normalizeTimestamp(new Date());
const existing = this.getById(id);
if (!existing) {
throw new Error(`Streaming session with id ${id} not found`);
}
const parts: string[] = [];
const values: any[] = [];
if (updates.sdk_session_id !== undefined) {
parts.push('sdk_session_id = ?');
values.push(updates.sdk_session_id);
}
if (updates.title !== undefined) {
parts.push('title = ?');
values.push(updates.title);
}
if (updates.subtitle !== undefined) {
parts.push('subtitle = ?');
values.push(updates.subtitle);
}
if (updates.status !== undefined) {
parts.push('status = ?');
values.push(updates.status);
}
if (updates.completed_at !== undefined) {
const { isoString, epoch } = normalizeTimestamp(updates.completed_at);
parts.push('completed_at = ?', 'completed_at_epoch = ?');
values.push(isoString, epoch);
}
// Always update the updated_at timestamp
parts.push('updated_at = ?', 'updated_at_epoch = ?');
values.push(updatedAt, updatedEpoch);
values.push(id);
const stmt = this.db.prepare(`
UPDATE streaming_sessions
SET ${parts.join(', ')}
WHERE id = ?
`);
stmt.run(...values);
return this.getById(id)!;
}
/**
* Update a streaming session by Claude session ID
* Convenience method for hooks that only have the Claude session ID
*/
updateByClaudeSessionId(claudeSessionId: string, updates: StreamingSessionUpdate): StreamingSessionRow | null {
const session = this.getByClaudeSessionId(claudeSessionId);
if (!session) {
return null;
}
return this.update(session.id, updates);
}
/**
* Get streaming session by internal ID
*/
getById(id: number): StreamingSessionRow | null {
const stmt = this.db.prepare('SELECT * FROM streaming_sessions WHERE id = ?');
return stmt.get(id) as StreamingSessionRow || null;
}
/**
* Get streaming session by Claude session ID
*/
getByClaudeSessionId(claudeSessionId: string): StreamingSessionRow | null {
const stmt = this.db.prepare('SELECT * FROM streaming_sessions WHERE claude_session_id = ?');
return stmt.get(claudeSessionId) as StreamingSessionRow || null;
}
/**
* Get streaming session by SDK session ID
*/
getBySdkSessionId(sdkSessionId: string): StreamingSessionRow | null {
const stmt = this.db.prepare('SELECT * FROM streaming_sessions WHERE sdk_session_id = ?');
return stmt.get(sdkSessionId) as StreamingSessionRow || null;
}
/**
* Check if a streaming session exists by Claude session ID
*/
has(claudeSessionId: string): boolean {
const stmt = this.db.prepare('SELECT 1 FROM streaming_sessions WHERE claude_session_id = ? LIMIT 1');
return Boolean(stmt.get(claudeSessionId));
}
/**
* Get active streaming sessions for a project
*/
getActiveForProject(project: string): StreamingSessionRow[] {
const stmt = this.db.prepare(`
SELECT * FROM streaming_sessions
WHERE project = ? AND status = 'active'
ORDER BY started_at_epoch DESC
`);
return stmt.all(project) as StreamingSessionRow[];
}
/**
* Get all active streaming sessions
*/
getAllActive(): StreamingSessionRow[] {
const stmt = this.db.prepare(`
SELECT * FROM streaming_sessions
WHERE status = 'active'
ORDER BY started_at_epoch DESC
`);
return stmt.all() as StreamingSessionRow[];
}
/**
* Get recent streaming sessions (completed or failed)
*/
getRecent(limit = 10): StreamingSessionRow[] {
const stmt = this.db.prepare(`
SELECT * FROM streaming_sessions
ORDER BY started_at_epoch DESC
LIMIT ?
`);
return stmt.all(limit) as StreamingSessionRow[];
}
/**
* Mark a session as completed
*/
markCompleted(id: number): StreamingSessionRow {
return this.update(id, {
status: 'completed',
completed_at: new Date()
});
}
/**
* Mark a session as failed
*/
markFailed(id: number): StreamingSessionRow {
return this.update(id, {
status: 'failed',
completed_at: new Date()
});
}
/**
* Delete a streaming session by ID
*/
deleteById(id: number): boolean {
const stmt = this.db.prepare('DELETE FROM streaming_sessions WHERE id = ?');
const info = stmt.run(id);
return info.changes > 0;
}
/**
* Delete a streaming session by Claude session ID
*/
deleteByClaudeSessionId(claudeSessionId: string): boolean {
const stmt = this.db.prepare('DELETE FROM streaming_sessions WHERE claude_session_id = ?');
const info = stmt.run(claudeSessionId);
return info.changes > 0;
}
/**
* Clean up old completed/failed sessions (older than N days)
*/
cleanupOldSessions(daysOld = 30): number {
const cutoffEpoch = Date.now() - (daysOld * 24 * 60 * 60 * 1000);
const stmt = this.db.prepare(`
DELETE FROM streaming_sessions
WHERE status IN ('completed', 'failed')
AND completed_at_epoch < ?
`);
const info = stmt.run(cutoffEpoch);
return info.changes;
}
}
+5 -2
View File
@@ -7,6 +7,7 @@ export { MemoryStore } from './MemoryStore.js';
export { OverviewStore } from './OverviewStore.js';
export { DiagnosticsStore } from './DiagnosticsStore.js';
export { TranscriptEventStore } from './TranscriptEventStore.js';
export { StreamingSessionStore } from './StreamingSessionStore.js';
// Export types
export * from './types.js';
@@ -26,18 +27,20 @@ export async function createStores() {
}
const db = await manager.initialize();
const { SessionStore } = await import('./SessionStore.js');
const { MemoryStore } = await import('./MemoryStore.js');
const { OverviewStore } = await import('./OverviewStore.js');
const { DiagnosticsStore } = await import('./DiagnosticsStore.js');
const { TranscriptEventStore } = await import('./TranscriptEventStore.js');
const { StreamingSessionStore } = await import('./StreamingSessionStore.js');
return {
sessions: new SessionStore(db),
memories: new MemoryStore(db),
overviews: new OverviewStore(db),
diagnostics: new DiagnosticsStore(db),
transcriptEvents: new TranscriptEventStore(db)
transcriptEvents: new TranscriptEventStore(db),
streamingSessions: new StreamingSessionStore(db)
};
}
+44 -1
View File
@@ -160,10 +160,53 @@ export const migration002: Migration = {
}
};
/**
* Migration 003 - Add streaming_sessions table for real-time session tracking
*/
export const migration003: Migration = {
version: 3,
up: (db: Database.Database) => {
// Streaming sessions table - tracks active SDK compression sessions
db.exec(`
CREATE TABLE IF NOT EXISTS streaming_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
claude_session_id TEXT UNIQUE NOT NULL,
sdk_session_id TEXT,
project TEXT NOT NULL,
title TEXT,
subtitle TEXT,
user_prompt TEXT,
started_at TEXT NOT NULL,
started_at_epoch INTEGER NOT NULL,
updated_at TEXT,
updated_at_epoch INTEGER,
completed_at TEXT,
completed_at_epoch INTEGER,
status TEXT NOT NULL DEFAULT 'active'
);
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_claude_id ON streaming_sessions(claude_session_id);
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_sdk_id ON streaming_sessions(sdk_session_id);
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_project ON streaming_sessions(project);
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_status ON streaming_sessions(status);
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_started ON streaming_sessions(started_at_epoch DESC);
`);
console.log('✅ Created streaming_sessions table for real-time session tracking');
},
down: (db: Database.Database) => {
db.exec(`
DROP TABLE IF EXISTS streaming_sessions;
`);
}
};
/**
* All migrations in order
*/
export const migrations: Migration[] = [
migration001,
migration002
migration002,
migration003
];
+112
View File
@@ -0,0 +1,112 @@
import { platform, homedir } from 'os';
import { execSync } from 'child_process';
import { chmodSync } from 'fs';
import { join } from 'path';
const isWindows = platform() === 'win32';
/**
* Platform-specific utilities for cross-platform compatibility
* Handles differences between Windows and Unix-like systems
*/
export const Platform = {
/**
* Returns the appropriate shell for the current platform
*/
getShell: (): string => {
return isWindows ? 'powershell' : '/bin/sh';
},
/**
* Returns the file extension for hook scripts
*/
getHookExtension: (): string => {
return '.js'; // Both platforms can execute Node.js scripts
},
/**
* Finds the path to an executable command
* @param name - Name of the executable to find
* @returns Full path to the executable
*/
findExecutable: (name: string): string => {
const cmd = isWindows ? `where ${name}` : `which ${name}`;
return execSync(cmd, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore']
}).trim();
},
/**
* Makes a file executable (Unix only - no-op on Windows)
* @param path - Path to the file to make executable
*/
makeExecutable: (path: string): void => {
if (!isWindows) {
chmodSync(path, 0o755);
}
},
/**
* Installs uv package manager using platform-specific method
*/
installUv: (): void => {
if (isWindows) {
execSync('powershell -Command "irm https://astral.sh/uv/install.ps1 | iex"', {
stdio: 'pipe'
});
} else {
execSync('curl -LsSf https://astral.sh/uv/install.sh | sh', {
stdio: 'pipe',
shell: '/bin/sh'
});
}
},
/**
* Returns shell configuration file paths for the current platform
* @returns Array of shell config file paths
*/
getShellConfigPaths: (): string[] => {
const home = homedir();
if (isWindows) {
return [
join(home, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'),
join(home, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1')
];
}
return [
join(home, '.bashrc'),
join(home, '.zshrc'),
join(home, '.bash_profile')
];
},
/**
* Gets the appropriate alias syntax for the current platform's shell
* @param aliasName - Name of the alias
* @param command - Command to alias
* @returns Alias definition string
*/
getAliasDefinition: (aliasName: string, command: string): string => {
if (isWindows) {
// PowerShell function syntax
return `function ${aliasName} { ${command} $args }`;
}
// Bash/Zsh alias syntax
return `alias ${aliasName}='${command}'`;
},
/**
* Returns whether the current platform is Windows
*/
isWindows: (): boolean => isWindows,
/**
* Returns whether the current platform is Unix-like (macOS/Linux)
*/
isUnix: (): boolean => !isWindows
};