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:
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "thedotmack",
|
||||
"owner": {
|
||||
"name": "Alex Newman"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"source": "./claude-mem",
|
||||
"description": "Persistent memory system for Claude Code"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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).
|
||||
Vendored
+58
-53
File diff suppressed because one or more lines are too long
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
Executable
+44
@@ -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)}
|
||||
Executable
+42
@@ -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)}
|
||||
Executable
+43
@@ -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)}
|
||||
Executable
+43
@@ -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)}
|
||||
Executable
+193
File diff suppressed because one or more lines are too long
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user