Add standalone hook entry points for context, new, save, summary, and worker

- Implemented context-hook.ts for handling session start events.
- Created new-hook.ts for user prompt submission events.
- Developed save-hook.ts for post tool use events.
- Added summary-hook.ts for handling stop events.
- Introduced worker.ts as a standalone background process for the SDK agent.
- Each hook reads input from stdin, processes it, and handles errors gracefully.
This commit is contained in:
Alex Newman
2025-10-16 15:39:30 -04:00
parent 723f1f5374
commit 834cf4095e
20 changed files with 1563 additions and 63 deletions
+13
View File
@@ -0,0 +1,13 @@
{
"name": "thedotmack",
"owner": {
"name": "Alex Newman"
},
"plugins": [
{
"name": "claude-mem",
"source": "./claude-mem",
"description": "Persistent memory system for Claude Code"
}
]
}
+90
View File
@@ -0,0 +1,90 @@
# Claude Code Plugins Quick Reference
For custom files in your claude-mem plugin, you have several designated locations based on the standard plugin structure [(1)](https://docs.claude.com/en/docs/claude-code/plugins-reference#standard-plugin-layout):
## Standard Plugin Directory Structure [(1)](https://docs.claude.com/en/docs/claude-code/plugins-reference#standard-plugin-layout)
```
enterprise-plugin/
├── .claude-plugin/ # Metadata directory
│ └── plugin.json # Required: plugin manifest
├── commands/ # Default command location
│ ├── status.md
│ └── logs.md
├── agents/ # Default agent location
│ ├── security-reviewer.md
│ ├── performance-tester.md
│ └── compliance-checker.md
├── hooks/ # Hook configurations
│ ├── hooks.json # Main hook config
│ └── security-hooks.json # Additional hooks
├── .mcp.json # MCP server definitions
├── scripts/ # Hook and utility scripts
│ ├── security-scan.sh
│ ├── format-code.py
│ └── deploy.js
├── LICENSE # License file
└── CHANGELOG.md # Version history
```
## Where to Put Your Custom Files
### Hook Scripts [(1)](https://docs.claude.com/en/docs/claude-code/plugins-reference#standard-plugin-layout)
Put your hook execution scripts in the `scripts/` directory [(1)](https://docs.claude.com/en/docs/claude-code/plugins-reference#standard-plugin-layout). For your claude-mem hooks:
```
claude-mem-plugin/
├── scripts/
│ ├── context-hook.js # Your SessionStart hook
│ ├── new-hook.js # Your UserPromptSubmit hook
│ ├── save-hook.js # Your PostToolUse hook
│ └── summary-hook.js # Your Stop hook
```
### Hook Configuration [(4)](https://docs.claude.com/en/docs/claude-code/hooks#plugin-hooks)
Your hook configuration goes in `hooks/hooks.json` and can reference plugin files using the `${CLAUDE_PLUGIN_ROOT}` environment variable [(4)](https://docs.claude.com/en/docs/claude-code/hooks#plugin-hooks):
```json
{
"description": "Claude-mem memory system hooks",
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
"timeout": 180
}
]
}
]
}
}
```
### Commands [(1)](https://docs.claude.com/en/docs/claude-code/plugins-reference#commands)
Your slash commands go in the `commands/` directory as markdown files [(1)](https://docs.claude.com/en/docs/claude-code/plugins-reference#commands):
```
claude-mem-plugin/
├── commands/
│ ├── claude-mem.md
│ ├── save.md
│ └── remember.md
```
### Additional Custom Files
For any other custom files (configuration, templates, data files), you can create additional directories in your plugin root. The plugin system will make them available via `${CLAUDE_PLUGIN_ROOT}` [(4)](https://docs.claude.com/en/docs/claude-code/hooks#plugin-hooks).
## File Location Reference [(1)](https://docs.claude.com/en/docs/claude-code/plugins-reference#file-locations-reference)
| Component | Default Location | Purpose |
|-----------|------------------|---------|
| **Manifest** | `.claude-plugin/plugin.json` | Required metadata file |
| **Commands** | `commands/` | Slash command markdown files |
| **Agents** | `agents/` | Subagent markdown files |
| **Hooks** | `hooks/hooks.json` | Hook configuration |
| **MCP servers** | `.mcp.json` | MCP server definitions |
The key point is that all component directories (commands/, agents/, hooks/, scripts/) must be at the plugin root, not inside `.claude-plugin/` [(1)](https://docs.claude.com/en/docs/claude-code/plugins-reference#standard-plugin-layout).
+58 -53
View File
File diff suppressed because one or more lines are too long
+308
View File
@@ -0,0 +1,308 @@
# 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
+242
View File
@@ -0,0 +1,242 @@
# 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
+272
View File
@@ -0,0 +1,272 @@
# 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)
+5 -4
View File
@@ -1,11 +1,12 @@
{
"description": "Claude-mem memory system hooks",
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "claude-mem context",
"command": "bun ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/context-hook.js",
"timeout": 180000
}
]
@@ -16,7 +17,7 @@
"hooks": [
{
"type": "command",
"command": "claude-mem new",
"command": "bun ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/new-hook.js",
"timeout": 60000
}
]
@@ -28,7 +29,7 @@
"hooks": [
{
"type": "command",
"command": "claude-mem save",
"command": "bun ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/save-hook.js",
"timeout": 180000
}
]
@@ -39,7 +40,7 @@
"hooks": [
{
"type": "command",
"command": "claude-mem summary",
"command": "bun ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/summary-hook.js",
"timeout": 60000
}
]
+4 -1
View File
@@ -36,7 +36,9 @@
"claude-mem": "./dist/claude-mem.min.js"
},
"scripts": {
"build": "node scripts/build.js",
"build": "node scripts/build.js && node scripts/build-hooks.js",
"build:cli": "node scripts/build.js",
"build:hooks": "node scripts/build-hooks.js",
"publish:npm": "node scripts/publish.js",
"dev": "bun run src/bin/cli.ts",
"prepublishOnly": "npm run build",
@@ -56,6 +58,7 @@
"dist",
"commands",
"hooks",
"scripts",
".claude-plugin",
"src",
"docs",
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env node
/**
* Build script for claude-mem hooks
* Bundles TypeScript hooks into individual standalone executables
*/
import { exec } from 'child_process';
import { promisify } from 'util';
import fs from 'fs';
import path from 'path';
const execAsync = promisify(exec);
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: 'worker', source: 'src/bin/hooks/worker.ts' }
];
async function buildHooks() {
console.log('🔨 Building claude-mem hooks...\n');
try {
// Read version from package.json
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
const version = packageJson.version;
console.log(`📌 Version: ${version}`);
// 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 scripts directory
console.log('\n📦 Preparing scripts directory...');
const scriptsDir = 'scripts/hooks';
if (!fs.existsSync(scriptsDir)) {
fs.mkdirSync(scriptsDir, { recursive: true });
}
console.log('✓ Scripts directory ready');
// 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 { 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}`);
// Make executable
fs.chmodSync(outfile, 0o755);
// Check file size
const stats = fs.statSync(outfile);
const sizeInKB = (stats.size / 1024).toFixed(2);
console.log(`${hook.name} built (${sizeInKB} KB)`);
}
console.log('\n✅ All hooks built successfully!');
console.log(` Output: ${scriptsDir}/`);
} catch (error) {
console.error('\n❌ Hook build failed:', error.message);
if (error.stderr) {
console.error('\nError details:', error.stderr);
}
process.exit(1);
}
}
buildHooks();
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bun
// @bun
import{Database as N}from"bun:sqlite";import{join as $,dirname as b,basename as f}from"path";import{homedir as J}from"os";import{existsSync as P,mkdirSync as L}from"fs";var K=process.env.CLAUDE_MEM_DATA_DIR||$(J(),".claude-mem"),V=process.env.CLAUDE_CONFIG_DIR||$(J(),".claude"),w=$(K,"archives"),C=$(K,"logs"),k=$(K,"trash"),l=$(K,"backups"),S=$(K,"chroma"),h=$(K,"settings.json"),M=$(K,"claude-mem.db"),R=$(V,"settings.json"),A=$(V,"commands"),j=$(V,"CLAUDE.md");function q(z){L(z,{recursive:!0})}class v{db;constructor(){q(K),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,W=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,W)}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,W,X){let Z=new Date,Q=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,W,X,Z.toISOString(),Q),this.db.query("SELECT last_insert_rowid() as id").get().id}updateSDKSessionId(z,W){this.db.query(`
UPDATE sdk_sessions
SET sdk_session_id = ?
WHERE id = ?
`).run(W,z)}storeObservation(z,W,X,Z){let Q=new Date,Y=Q.getTime();this.db.query(`
INSERT INTO observations
(sdk_session_id, project, text, type, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?)
`).run(z,W,Z,X,Q.toISOString(),Y)}storeSummary(z,W,X){let Z=new Date,Q=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,W,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(),Q)}markSessionCompleted(z){let W=new Date,X=W.getTime();this.db.query(`
UPDATE sdk_sessions
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
WHERE id = ?
`).run(W.toISOString(),X,z)}markSessionFailed(z){let W=new Date,X=W.getTime();this.db.query(`
UPDATE sdk_sessions
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE id = ?
`).run(W.toISOString(),X,z)}close(){this.db.close()}}import O from"path";function F(z){try{if(!z)console.log("No input provided - this script is designed to run as a Claude Code SessionStart hook"),process.exit(0);if(z.source&&z.source!=="startup")console.log(""),process.exit(0);let W=O.basename(z.cwd),X=new v,Z=X.getRecentSummaries(W,5);if(X.close(),Z.length===0)console.log(`# Recent Session Context
No previous sessions found for this project yet.`),process.exit(0);let Q=[];Q.push("# Recent Session Context"),Q.push(""),Q.push(`Here's what happened in recent ${W} sessions:`),Q.push("");for(let Y of Z){if(Q.push("---"),Q.push(""),Y.request)Q.push(`**Request:** ${Y.request}`);if(Y.completed)Q.push(`**Completed:** ${Y.completed}`);if(Y.learned)Q.push(`**Learned:** ${Y.learned}`);if(Y.next_steps)Q.push(`**Next Steps:** ${Y.next_steps}`);if(Y.files_edited)try{let B=JSON.parse(Y.files_edited);if(Array.isArray(B)&&B.length>0)Q.push(`**Files Edited:** ${B.join(", ")}`)}catch{if(Y.files_edited.trim())Q.push(`**Files Edited:** ${Y.files_edited}`)}Q.push(`**Date:** ${Y.created_at.split("T")[0]}`),Q.push("")}console.log(Q.join(`
`)),process.exit(0)}catch(W){console.error(`[claude-mem context error: ${W.message}]`),process.exit(0)}}var G=await Bun.stdin.text();try{let z=G.trim()?JSON.parse(G):void 0;F(z)}catch(z){console.error(`[claude-mem context-hook error: ${z.message}]`),process.exit(0)}
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bun
// @bun
import{Database as E}from"bun:sqlite";import{join as X,dirname as g,basename as C}from"path";import{homedir as F}from"os";import{existsSync as w,mkdirSync as H}from"fs";var Z=process.env.CLAUDE_MEM_DATA_DIR||X(F(),".claude-mem"),v=process.env.CLAUDE_CONFIG_DIR||X(F(),".claude"),y=X(Z,"archives"),l=X(Z,"logs"),h=X(Z,"trash"),j=X(Z,"backups"),A=X(Z,"chroma"),R=X(Z,"settings.json"),G=X(Z,"claude-mem.db"),_=X(v,"settings.json"),I=X(v,"commands"),c=X(v,"CLAUDE.md");function x(z){H(z,{recursive:!0})}class J{db;constructor(){x(Z),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(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,W){let Y=new Date,$=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(z,Q,W,Y.toISOString(),$),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,W,Y){let $=new Date,K=$.getTime();this.db.query(`
INSERT INTO observations
(sdk_session_id, project, text, type, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?)
`).run(z,Q,Y,W,$.toISOString(),K)}storeSummary(z,Q,W){let Y=new Date,$=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(z,Q,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(),$)}markSessionCompleted(z){let Q=new Date,W=Q.getTime();this.db.query(`
UPDATE sdk_sessions
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
WHERE id = ?
`).run(Q.toISOString(),W,z)}markSessionFailed(z){let Q=new Date,W=Q.getTime();this.db.query(`
UPDATE sdk_sessions
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE id = ?
`).run(Q.toISOString(),W,z)}close(){this.db.close()}}import L from"path";import{spawn as O}from"child_process";function N(z){try{if(!z)console.log("No input provided - this script is designed to run as a Claude Code UserPromptSubmit hook"),console.log(`
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",prompt:"string"},null,2)),process.exit(0);let{session_id:Q,cwd:W,prompt:Y}=z,$=L.basename(W),K=new J;if(K.findActiveSDKSession(Q))K.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0);let B=K.createSDKSession(Q,$,Y);K.close();let q=process.env.CLAUDE_PLUGIN_ROOT,V;if(q){let f=L.join(q,"scripts","hooks","worker.js");V=O("bun",[f,B.toString()],{detached:!0,stdio:"ignore"})}else V=O("claude-mem",["worker",B.toString()],{detached:!0,stdio:"ignore"});V.unref(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}catch(Q){console.error(`[claude-mem new error: ${Q.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}}var U=await Bun.stdin.text();try{let z=U.trim()?JSON.parse(U):void 0;N(z)}catch(z){console.error(`[claude-mem new-hook error: ${z.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bun
// @bun
import b from"net";import{Database as O}from"bun:sqlite";import{join as Y,dirname as w,basename as C}from"path";import{homedir as F}from"os";import{existsSync as S,mkdirSync as H}from"fs";var $=process.env.CLAUDE_MEM_DATA_DIR||Y(F(),".claude-mem"),J=process.env.CLAUDE_CONFIG_DIR||Y(F(),".claude"),h=Y($,"archives"),l=Y($,"logs"),R=Y($,"trash"),j=Y($,"backups"),A=Y($,"chroma"),I=Y($,"settings.json"),G=Y($,"claude-mem.db"),_=Y(J,"settings.json"),p=Y(J,"commands"),d=Y(J,"CLAUDE.md");function v(z){return Y($,`worker-${z}.sock`)}function x(z){H(z,{recursive:!0})}class M{db;constructor(){x($),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,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(z,X)}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,X,Q){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(z,X,Q,Z.toISOString(),W),this.db.query("SELECT last_insert_rowid() as id").get().id}updateSDKSessionId(z,X){this.db.query(`
UPDATE sdk_sessions
SET sdk_session_id = ?
WHERE id = ?
`).run(X,z)}storeObservation(z,X,Q,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(z,X,Z,Q,W.toISOString(),B)}storeSummary(z,X,Q){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(z,X,Q.request||null,Q.investigated||null,Q.learned||null,Q.completed||null,Q.next_steps||null,Q.files_read||null,Q.files_edited||null,Q.notes||null,Z.toISOString(),W)}markSessionCompleted(z){let X=new Date,Q=X.getTime();this.db.query(`
UPDATE sdk_sessions
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
WHERE id = ?
`).run(X.toISOString(),Q,z)}markSessionFailed(z){let X=new Date,Q=X.getTime();this.db.query(`
UPDATE sdk_sessions
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE id = ?
`).run(X.toISOString(),Q,z)}close(){this.db.close()}}var E=new Set(["TodoWrite","ListMcpResourcesTool"]);function N(z){try{if(!z)console.log("No input provided - this script is designed to run as a Claude Code PostToolUse hook"),console.log(`
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",tool_name:"string",tool_input:{},tool_output:{}},null,2)),process.exit(0);let{session_id:X,tool_name:Q,tool_input:Z,tool_output:W}=z;if(E.has(Q))console.log('{"continue": true, "suppressOutput": true}'),process.exit(0);let B=new M,K=B.findActiveSDKSession(X);if(B.close(),!K)console.log('{"continue": true, "suppressOutput": true}'),process.exit(0);let q=v(K.id),U={type:"observation",tool_name:Q,tool_input:JSON.stringify(Z),tool_output:JSON.stringify(W)},V=b.connect(q,()=>{V.write(JSON.stringify(U)+`
`),V.end()});V.on("error",(f)=>{console.error(`[claude-mem save] Socket error: ${f.message}`)}),V.on("close",()=>{console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)})}catch(X){console.error(`[claude-mem save error: ${X.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}}var L=await Bun.stdin.text();try{let z=L.trim()?JSON.parse(L):void 0;N(z)}catch(z){console.error(`[claude-mem save-hook error: ${z.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bun
// @bun
import U from"net";import{Database as O}from"bun:sqlite";import{join as Y,dirname as b,basename as E}from"path";import{homedir as M}from"os";import{existsSync as C,mkdirSync as N}from"fs";var $=process.env.CLAUDE_MEM_DATA_DIR||Y(M(),".claude-mem"),V=process.env.CLAUDE_CONFIG_DIR||Y(M(),".claude"),P=Y($,"archives"),k=Y($,"logs"),l=Y($,"trash"),S=Y($,"backups"),y=Y($,"chroma"),h=Y($,"settings.json"),q=Y($,"claude-mem.db"),R=Y(V,"settings.json"),j=Y(V,"commands"),A=Y(V,"CLAUDE.md");function F(z){return Y($,`worker-${z}.sock`)}function G(z){N(z,{recursive:!0})}class v{db;constructor(){G($),this.db=new O(q,{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,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(z,X)}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,X,Q){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(z,X,Q,Z.toISOString(),K),this.db.query("SELECT last_insert_rowid() as id").get().id}updateSDKSessionId(z,X){this.db.query(`
UPDATE sdk_sessions
SET sdk_session_id = ?
WHERE id = ?
`).run(X,z)}storeObservation(z,X,Q,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(z,X,Z,Q,K.toISOString(),B)}storeSummary(z,X,Q){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(z,X,Q.request||null,Q.investigated||null,Q.learned||null,Q.completed||null,Q.next_steps||null,Q.files_read||null,Q.files_edited||null,Q.notes||null,Z.toISOString(),K)}markSessionCompleted(z){let X=new Date,Q=X.getTime();this.db.query(`
UPDATE sdk_sessions
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
WHERE id = ?
`).run(X.toISOString(),Q,z)}markSessionFailed(z){let X=new Date,Q=X.getTime();this.db.query(`
UPDATE sdk_sessions
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE id = ?
`).run(X.toISOString(),Q,z)}close(){this.db.close()}}function x(z){try{if(!z)console.log("No input provided - this script is designed to run as a Claude Code Stop hook"),console.log(`
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string"},null,2)),process.exit(0);let{session_id:X}=z,Q=new v,Z=Q.findActiveSDKSession(X);if(Q.close(),!Z)console.log('{"continue": true, "suppressOutput": true}'),process.exit(0);let K=F(Z.id),B={type:"finalize"},W=U.connect(K,()=>{W.write(JSON.stringify(B)+`
`),W.end()});W.on("error",(J)=>{console.error(`[claude-mem summary] Socket error: ${J.message}`)}),W.on("close",()=>{console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)})}catch(X){console.error(`[claude-mem summary error: ${X.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}}var L=await Bun.stdin.text();try{let z=L.trim()?JSON.parse(L):void 0;x(z)}catch(z){console.error(`[claude-mem summary-hook error: ${z.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}
+193
View File
File diff suppressed because one or more lines are too long
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bun
/**
* Context Hook Entry Point - SessionStart
* Standalone executable for plugin hooks
*/
import { contextHook } from '../../hooks/context.js';
// Read input from stdin
const input = await Bun.stdin.text();
try {
const parsed = input.trim() ? JSON.parse(input) : undefined;
contextHook(parsed);
} catch (error: any) {
console.error(`[claude-mem context-hook error: ${error.message}]`);
process.exit(0);
}
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bun
/**
* New Hook Entry Point - UserPromptSubmit
* Standalone executable for plugin hooks
*/
import { newHook } from '../../hooks/new.js';
// 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);
}
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bun
/**
* Save Hook Entry Point - PostToolUse
* Standalone executable for plugin hooks
*/
import { saveHook } from '../../hooks/save.js';
// 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);
}
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bun
/**
* Summary Hook Entry Point - Stop
* Standalone executable for plugin hooks
*/
import { summaryHook } from '../../hooks/summary.js';
// 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);
}
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bun
/**
* Worker Entry Point
* Standalone background process for SDK agent
*/
import { main } from '../../sdk/worker.js';
// Entry point - just call the worker main function
main().catch((error) => {
console.error('[SDK Worker] Fatal error:', error);
process.exit(1);
});
+19 -5
View File
@@ -48,11 +48,25 @@ export function newHook(input?: UserPromptSubmitInput): void {
db.close();
// Start SDK worker in background as detached process
// Use 'claude-mem worker' CLI command which is always available
const child = spawn('claude-mem', ['worker', sessionId.toString()], {
detached: true,
stdio: 'ignore'
});
// In plugin mode, use bundled worker; otherwise use global CLI
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
let child;
if (pluginRoot) {
// Plugin mode: use bundled worker
const workerPath = path.join(pluginRoot, 'scripts', 'hooks', 'worker.js');
child = spawn('bun', [workerPath, sessionId.toString()], {
detached: true,
stdio: 'ignore'
});
} else {
// Traditional mode: use global CLI
child = spawn('claude-mem', ['worker', sessionId.toString()], {
detached: true,
stdio: 'ignore'
});
}
child.unref();
// Output hook response